feat: add AI and Backend evolution history with interactive demos, and refine Frontend evolution demo

This commit is contained in:
sanbuphy
2026-01-18 10:24:35 +08:00
parent 82be39a9ac
commit 26ed39e1eb
44 changed files with 9868 additions and 2633 deletions
@@ -1,102 +1,109 @@
<template>
<div class="browser-rendering-demo">
<div class="control-bar">
<div class="step-indicator">Step: {{ currentStep + 1 }} / 4</div>
<div class="steps-nav">
<button
v-for="(step, index) in steps"
:key="index"
:class="{ active: currentStep === index }"
@click="currentStep = index"
>
{{ step.label }}
</button>
</div>
<div class="stepper">
<button
v-for="(step, index) in steps"
:key="index"
class="step-btn"
:class="{ active: currentStep === index, completed: currentStep > index }"
@click="currentStep = index"
>
<span class="step-num">{{ index + 1 }}</span>
<span class="step-label">{{ step.label }}</span>
</button>
</div>
<div class="workspace">
<!-- Left: Source Code -->
<div class="source-panel">
<div class="panel-label">HTML / CSS</div>
<div class="code-block">
<div class="line">&lt;div id="app"&gt;</div>
<div class="line indent">&lt;h1&gt;Hello&lt;/h1&gt;</div>
<div class="line indent">&lt;p&gt;World&lt;/p&gt;</div>
<div class="line">&lt;/div&gt;</div>
<div class="line mt-2">h1 { color: red; }</div>
<div class="stage-container">
<div class="stage-info">
<h3>{{ steps[currentStep].title }}</h3>
<p>{{ steps[currentStep].desc }}</p>
</div>
<div class="visualization-window">
<!-- HTML/CSS Source -->
<div class="source-view">
<div class="window-title">积木说明书 (HTML/CSS)</div>
<div class="code-content">
<!-- HTML Highlighted always after Step 0 -->
<div class="line" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'html' }" @mouseenter="hoveredPart = 'html'" @mouseleave="hoveredPart = null">&lt;!DOCTYPE html&gt;</div>
<div class="line" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'html' }" @mouseenter="hoveredPart = 'html'" @mouseleave="hoveredPart = null">&lt;html&gt;</div>
<div class="line indent" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'body' }" @mouseenter="hoveredPart = 'body'" @mouseleave="hoveredPart = null">&lt;body&gt;</div>
<div class="line indent-2" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'card' }" @mouseenter="hoveredPart = 'card'" @mouseleave="hoveredPart = null">&lt;div class="card"&gt;</div>
<div class="line indent-3" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'img' }" @mouseenter="hoveredPart = 'img'" @mouseleave="hoveredPart = null">&lt;img class="icon" src="castle.png" /&gt;</div>
<div class="line indent-3" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'title' }" @mouseenter="hoveredPart = 'title'" @mouseleave="hoveredPart = null">&lt;h2 class="title"&gt;乐高城堡&lt;/h2&gt;</div>
<div class="line indent-3" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'btn' }" @mouseenter="hoveredPart = 'btn'" @mouseleave="hoveredPart = null">&lt;button class="btn"&gt;购买&lt;/button&gt;</div>
<div class="line indent-2" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'card' }" @mouseenter="hoveredPart = 'card'" @mouseleave="hoveredPart = null">&lt;/div&gt;</div>
<div class="line indent" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'body' }" @mouseenter="hoveredPart = 'body'" @mouseleave="hoveredPart = null">&lt;/body&gt;</div>
<div class="line" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'html' }" @mouseenter="hoveredPart = 'html'" @mouseleave="hoveredPart = null">&lt;/html&gt;</div>
<div class="spacer"></div>
<!-- CSS Highlighted precisely based on step usage -->
<!-- Layout properties -->
<div class="line" :class="{ active: currentStep === 2, hovered: hoveredPart === 'card' }" @mouseenter="hoveredPart = 'card'" @mouseleave="hoveredPart = null">.card { display: flex; padding: 10px; }</div>
<div class="line" :class="{ active: currentStep === 2, hovered: hoveredPart === 'img' }" @mouseenter="hoveredPart = 'img'" @mouseleave="hoveredPart = null">.icon { width: 50px; height: 50px; }</div>
<!-- Style properties -->
<div class="line" :class="{ active: currentStep === 1 || currentStep === 3, hovered: hoveredPart === 'title' }" @mouseenter="hoveredPart = 'title'" @mouseleave="hoveredPart = null">.title { color: red; }</div>
<div class="line" :class="{ active: currentStep === 1 || currentStep === 3, hovered: hoveredPart === 'btn' }" @mouseenter="hoveredPart = 'btn'" @mouseleave="hoveredPart = null">.btn { background: blue; }</div>
</div>
</div>
<div class="transform-arrow"></div>
<!-- Render Result -->
<div class="result-view">
<div class="window-title">{{ steps[currentStep].resultTitle }}</div>
<div class="render-canvas">
<!-- Step 1: DOM (Skeleton) -->
<transition-group name="block">
<div v-if="currentStep >= 0" key="html" class="block-box root" :class="{ hovered: hoveredPart === 'html' }" @mouseenter.stop="hoveredPart = 'html'" @mouseleave="hoveredPart = null">
<span class="block-label">html</span>
<div class="block-box body" :class="{ hovered: hoveredPart === 'body' }" @mouseenter.stop="hoveredPart = 'body'" @mouseleave="hoveredPart = null">
<span class="block-label">body</span>
<!-- Product Card -->
<div class="block-box card" :class="{ layout: currentStep >= 2, hovered: hoveredPart === 'card' }" @mouseenter.stop="hoveredPart = 'card'" @mouseleave="hoveredPart = null">
<span class="block-label">div.card</span>
<!-- Image -->
<div class="block-box img" :class="{ layout: currentStep >= 2, hovered: hoveredPart === 'img' }" @mouseenter.stop="hoveredPart = 'img'" @mouseleave="hoveredPart = null">
<span class="block-label">img.icon</span>
<span v-if="currentStep >= 3" class="content-img">🏰</span>
</div>
<!-- Title -->
<div class="block-box title" :class="{ styled: currentStep >= 1, layout: currentStep >= 2, hovered: hoveredPart === 'title' }" @mouseenter.stop="hoveredPart = 'title'" @mouseleave="hoveredPart = null">
<span class="block-label">h2.title</span>
<span v-if="currentStep >= 3" class="content">乐高城堡</span>
</div>
<!-- Button -->
<div class="block-box btn" :class="{ styled: currentStep >= 1, layout: currentStep >= 2, hovered: hoveredPart === 'btn' }" @mouseenter.stop="hoveredPart = 'btn'" @mouseleave="hoveredPart = null">
<span class="block-label">button.btn</span>
<span v-if="currentStep >= 3" class="content-btn">购买</span>
</div>
</div>
</div>
</div>
</transition-group>
<!-- Overlays for different steps -->
<div v-if="currentStep === 1" class="overlay-info style-info">
<div class="brush">🖌 正在上色 (Style)...</div>
</div>
<div v-if="currentStep === 2" class="overlay-info layout-info">
<div class="ruler">📏 正在排版 (Layout)...</div>
</div>
<div v-if="currentStep === 3" class="overlay-info paint-info">
<div class="paint"> 绘制完成 (Paint)!</div>
</div>
</div>
</div>
</div>
<!-- Arrow -->
<div class="arrow"></div>
<!-- Right: Visualization -->
<div class="viz-panel">
<div class="panel-label">{{ steps[currentStep].title }}</div>
<transition name="fade" mode="out-in">
<!-- Step 1: DOM Tree -->
<div v-if="currentStep === 0" class="tree-viz">
<div class="node root">Document</div>
<div class="tree-lines">
<div class="line-v"></div>
</div>
<div class="node element">html</div>
<div class="tree-lines">
<div class="line-v"></div>
</div>
<div class="node element">body</div>
<div class="tree-children">
<div class="node element active">div#app</div>
<div class="tree-children row">
<div class="node element">h1</div>
<div class="node element">p</div>
</div>
</div>
</div>
<!-- Step 2: Render Tree -->
<div v-else-if="currentStep === 1" class="tree-viz render-tree">
<div class="node render-obj">RenderBlock (div)</div>
<div class="tree-children row">
<div class="node render-obj red">
RenderText (h1) <br /><small>color: red</small>
</div>
<div class="node render-obj">RenderText (p)</div>
</div>
</div>
<!-- Step 3: Layout -->
<div v-else-if="currentStep === 2" class="layout-viz">
<div class="layout-box root">
<span class="dims">100% x 100%</span>
<div class="layout-box container">
<span class="dims">div: 100% x auto</span>
<div class="layout-box item h1">h1: 100% x 32px (x:0, y:0)</div>
<div class="layout-box item p">p: 100% x 16px (x:0, y:32)</div>
</div>
</div>
</div>
<!-- Step 4: Paint -->
<div v-else-if="currentStep === 3" class="paint-viz">
<div class="browser-window">
<div class="painted-content">
<h1 style="color: red; margin: 0">Hello</h1>
<p style="margin: 0">World</p>
</div>
<div class="paint-layers">
<div class="layer-item">Layer 1: Background</div>
<div class="layer-item">Layer 2: Text</div>
</div>
</div>
</div>
</transition>
</div>
</div>
<div class="info-footer">
<p>{{ steps[currentStep].desc }}</p>
</div>
</div>
</template>
@@ -104,204 +111,340 @@
<script setup>
import { ref } from 'vue'
const currentStep = ref(0)
const steps = [
{
label: '1. DOM',
title: 'DOM Tree Construction',
desc: '浏览器解析 HTML 标记,构建 DOM (文档对象模型) 树。每个标签成为一个节点。'
label: 'DOM (搭骨架)',
title: '1. 搭建骨架 (DOM)',
desc: '浏览器工头 (Parser) 解析 HTML 代码,构建出完整的文档树结构。注意:即使代码中省略了 html/body,浏览器也会自动补全。',
resultTitle: 'DOM 树结构'
},
{
label: '2. Render Tree',
title: 'Render Tree Construction',
desc: '结合 DOM 和 CSSOM,生成渲染树。只有可见元素会被包含(display: none 的元素会被排除)。'
label: 'Style (上色)',
title: '2. 计算样式 (Recalculate Style)',
desc: '装修工 (CSS Parser) 匹配 CSS 规则。比如发现 .title 需要红色,.btn 需要蓝色背景。此时只关心"长什么样",不关心"在哪"。',
resultTitle: '附带样式的节点'
},
{
label: '3. Layout',
title: 'Layout (Reflow)',
desc: '计算每个节点在屏幕上的确切位置和大小。这一步也叫"回流"。'
label: 'Layout (排版)',
title: '3. 布局排版 (Layout/Reflow)',
desc: '测量员 (Layout) 根据 display:flex 和 padding 等属性,计算每个盒子的精确位置和大小。图片在左,文字在右。',
resultTitle: '几何布局'
},
{
label: '4. Paint',
title: 'Painting & Composite',
desc: '将各个节点绘制到屏幕像素。现代浏览器会将不同部分绘制到不同图层,最后合成。'
label: 'Paint (绘制)',
title: '4. 像素绘制 (Paint)',
desc: '画家 (Paint) 按照计算好的位置和样式,真正把像素点画在屏幕上。最终你看到了一个完整的商品卡片。',
resultTitle: '最终画面'
}
]
const currentStep = ref(0)
const hoveredPart = ref(null)
</script>
<style scoped>
.browser-rendering-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
background: var(--vp-c-bg);
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
overflow: hidden;
}
.control-bar {
.stepper {
display: flex;
justify-content: space-between;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.step-btn {
flex: 1;
padding: 1rem;
border: none;
background: transparent;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 1.5rem;
}
.steps-nav {
display: flex;
gap: 0.5rem;
}
.steps-nav button {
padding: 0.4rem 0.8rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.8rem;
position: relative;
transition: all 0.2s;
}
.steps-nav button.active {
.step-btn:hover {
background: var(--vp-c-bg-alt);
}
.step-btn.active {
color: var(--vp-c-brand);
background: var(--vp-c-bg);
font-weight: bold;
}
.step-btn.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: var(--vp-c-brand);
}
.step-num {
background: var(--vp-c-bg-alt);
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
border: 1px solid var(--vp-c-divider);
}
.step-btn.active .step-num,
.step-btn.completed .step-num {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.workspace {
display: flex;
gap: 1rem;
align-items: center;
height: 300px;
.stage-container {
padding: 1.5rem;
}
.source-panel,
.viz-panel {
flex: 1;
height: 100%;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 1rem;
display: flex;
flex-direction: column;
}
.panel-label {
font-weight: bold;
font-size: 0.8rem;
margin-bottom: 1rem;
color: var(--vp-c-text-2);
.stage-info {
margin-bottom: 2rem;
text-align: center;
}
.code-block {
font-family: monospace;
font-size: 0.8rem;
.stage-info h3 {
margin: 0 0 0.5rem 0;
color: var(--vp-c-text-1);
}
.line.indent {
padding-left: 1rem;
}
.line.mt-2 {
margin-top: 1rem;
}
.viz-panel {
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
/* Visualization Styles */
.node {
padding: 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg-alt);
text-align: center;
font-size: 0.8rem;
margin: 0.2rem;
}
.node.active {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.node.root {
background: #e5e7eb;
color: #374151;
}
.tree-viz {
display: flex;
flex-direction: column;
align-items: center;
}
.tree-children.row {
display: flex;
gap: 1rem;
}
.render-obj.red {
border-color: red;
color: red;
}
.layout-box {
border: 1px dashed var(--vp-c-text-3);
padding: 1rem;
position: relative;
}
.layout-box .dims {
position: absolute;
top: 2px;
right: 2px;
font-size: 0.6rem;
color: var(--vp-c-text-3);
}
.layout-box.container {
border-color: var(--vp-c-brand);
margin: 0.5rem;
}
.layout-box.item {
border-style: solid;
margin-bottom: 0.5rem;
background: var(--vp-c-bg-soft);
}
.painted-content {
background: white;
padding: 1rem;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 1rem;
}
.paint-layers {
font-size: 0.7rem;
color: var(--vp-c-text-3);
}
.info-footer {
margin-top: 1.5rem;
padding: 1rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
.stage-info p {
margin: 0;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
.visualization-window {
display: flex;
gap: 1rem;
align-items: stretch;
min-height: 400px;
}
.fade-enter-from,
.fade-leave-to {
.source-view, .result-view {
flex: 1;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-alt);
display: flex;
flex-direction: column;
}
.window-title {
padding: 0.5rem;
border-bottom: 1px solid var(--vp-c-divider);
font-size: 0.75rem;
font-weight: bold;
color: var(--vp-c-text-2);
background: var(--vp-c-bg-soft);
text-align: center;
}
.code-content {
padding: 1rem;
font-size: 0.8rem;
font-family: monospace;
overflow-y: auto;
}
.line {
padding: 2px 4px;
border-radius: 2px;
opacity: 0.3;
transition: opacity 0.5s;
white-space: nowrap;
}
.line.active {
opacity: 1;
background: rgba(59, 130, 246, 0.1);
font-weight: bold;
color: #2563eb;
}
.line.indent { padding-left: 1rem; }
.line.indent-2 { padding-left: 2rem; }
.line.indent-3 { padding-left: 3rem; }
.line.mt-2 { margin-top: 1rem; }
.transform-arrow {
display: flex;
align-items: center;
font-size: 1.5rem;
color: var(--vp-c-text-3);
}
.result-view {
background: white;
position: relative;
}
.render-canvas {
padding: 2rem;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
overflow-y: auto;
}
/* Blocks Animation */
.block-box {
border: 1px dashed #9ca3af;
background: #f3f4f6;
padding: 0.5rem;
margin: 0.2rem;
border-radius: 2px;
transition: all 0.8s cubic-bezier(0.25, 0.8, 0.25, 1);
position: relative;
min-width: 50px;
min-height: 30px;
display: flex;
flex-direction: column;
}
.block-box.root { width: 95%; border-color: #e5e7eb; background: #fff; }
.block-box.body { width: 90%; border-color: #d1d5db; background: #f9fafb; }
.block-box.card { width: 80%; border-color: #9ca3af; background: #e5e7eb; }
.block-label {
font-size: 0.6rem;
color: #9ca3af;
position: absolute;
top: -8px;
left: 4px;
background: white;
padding: 0 2px;
}
/* Step 2: Style */
.block-box.title.styled {
color: red; /* Text color applied but not painted yet */
border: 1px solid red; /* Visual cue for style applied */
background: #fee2e2;
}
.block-box.btn.styled {
background: blue;
color: white;
border: 1px solid blue;
}
/* Step 3: Layout */
.block-box.card.layout {
display: flex;
flex-direction: row; /* Horizontal layout */
align-items: center;
gap: 10px;
padding: 15px;
background: white;
border: 1px solid #ccc;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.block-box.img.layout {
width: 50px;
height: 50px;
background: #eee;
border: none;
}
.block-box.title.layout {
border: none;
background: transparent;
margin: 0;
padding: 0;
}
.block-box.btn.layout {
margin-left: auto; /* Push to right */
padding: 5px 15px;
border-radius: 4px;
}
/* Content visibility for Paint step */
.content, .content-img, .content-btn {
font-size: 1rem;
font-weight: bold;
animation: fadeIn 0.5s;
align-self: center;
}
.content-img { font-size: 2rem; }
.content-btn { font-size: 0.8rem; }
/* Overlay Info */
.overlay-info {
position: absolute;
bottom: 1rem;
left: 0;
right: 0;
text-align: center;
animation: bounceIn 0.5s;
pointer-events: none;
}
.brush, .ruler, .paint {
display: inline-block;
background: rgba(0,0,0,0.8);
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.8rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
/* Vue Transitions */
.block-enter-active,
.block-leave-active {
transition: all 0.5s ease;
}
.block-enter-from {
opacity: 0;
transform: scale(0.9);
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes bounceIn {
0% { transform: scale(0.8); opacity: 0; }
60% { transform: scale(1.1); opacity: 1; }
100% { transform: scale(1); }
}
/* Hover Interactions */
.line.hovered {
background: rgba(59, 130, 246, 0.15);
opacity: 1 !important;
cursor: crosshair;
}
.block-box.hovered {
box-shadow: 0 0 0 2px #3b82f6;
z-index: 10;
background-color: rgba(59, 130, 246, 0.05);
cursor: crosshair;
}
</style>
@@ -0,0 +1,260 @@
<template>
<div class="component-reusability-demo">
<div class="toolbox">
<div class="tool-title">Component Library</div>
<button class="spawn-btn" @click="spawn('counter')"> New Counter</button>
<button class="spawn-btn" @click="spawn('card')"> New Card</button>
</div>
<div class="workspace">
<div class="workspace-label">App Workspace</div>
<div class="instances-container">
<transition-group name="list">
<div
v-for="item in instances"
:key="item.id"
class="instance-wrapper"
>
<!-- Counter Component -->
<div v-if="item.type === 'counter'" class="comp-instance counter">
<div class="comp-header">
<span>Counter #{{ item.id }}</span>
<button class="close-btn" @click="remove(item.id)">×</button>
</div>
<div class="comp-body">
<span class="count-val">{{ item.data.count }}</span>
<button class="mini-btn" @click="item.data.count++">+</button>
</div>
</div>
<!-- Card Component -->
<div v-if="item.type === 'card'" class="comp-instance card">
<div class="comp-header">
<span>Card #{{ item.id }}</span>
<button class="close-btn" @click="remove(item.id)">×</button>
</div>
<div class="comp-body">
<div class="skeleton-img"></div>
<div class="skeleton-text"></div>
<button
class="like-btn"
:class="{ liked: item.data.liked }"
@click="item.data.liked = !item.data.liked"
>
{{ item.data.liked ? '❤️ Liked' : '♡ Like' }}
</button>
</div>
</div>
</div>
</transition-group>
<div v-if="instances.length === 0" class="empty-hint">
Click buttons above to add components.
<br>
Notice how each one works independently!
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const instances = ref([])
let nextId = 1
const spawn = (type) => {
if (type === 'counter') {
instances.value.push({
id: nextId++,
type: 'counter',
data: { count: 0 }
})
} else if (type === 'card') {
instances.value.push({
id: nextId++,
type: 'card',
data: { liked: false }
})
}
}
const remove = (id) => {
instances.value = instances.value.filter(i => i.id !== id)
}
</script>
<style scoped>
.component-reusability-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
overflow: hidden;
margin: 1rem 0;
display: flex;
flex-direction: column;
}
.toolbox {
background: var(--vp-c-bg-soft);
padding: 1rem;
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
gap: 1rem;
align-items: center;
}
.tool-title {
font-weight: bold;
font-size: 0.9rem;
color: var(--vp-c-text-2);
margin-right: auto;
}
.spawn-btn {
background: white;
border: 1px solid var(--vp-c-divider);
padding: 0.4rem 0.8rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
transition: all 0.2s;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.spawn-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
transform: translateY(-1px);
}
.workspace {
background: var(--vp-c-bg-alt);
padding: 1.5rem;
min-height: 200px;
position: relative;
}
.workspace-label {
position: absolute;
top: 0.5rem;
left: 0.5rem;
font-size: 0.7rem;
color: var(--vp-c-text-3);
text-transform: uppercase;
letter-spacing: 1px;
}
.instances-container {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: flex-start;
}
.empty-hint {
width: 100%;
text-align: center;
color: var(--vp-c-text-3);
margin-top: 3rem;
line-height: 1.6;
}
.instance-wrapper {
transition: all 0.4s;
}
.comp-instance {
background: white;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
width: 140px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
overflow: hidden;
}
.comp-header {
background: #f1f5f9;
padding: 4px 8px;
font-size: 0.7rem;
color: #64748b;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #e2e8f0;
}
.close-btn {
border: none;
background: transparent;
color: #94a3b8;
cursor: pointer;
font-size: 1rem;
line-height: 1;
}
.close-btn:hover { color: #ef4444; }
.comp-body {
padding: 0.8rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
/* Counter Style */
.counter .count-val {
font-size: 1.5rem;
font-weight: bold;
color: var(--vp-c-brand);
}
.mini-btn {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
width: 100%;
border-radius: 4px;
cursor: pointer;
}
.mini-btn:hover { background: #e2e8f0; }
/* Card Style */
.skeleton-img {
width: 100%;
height: 40px;
background: #e2e8f0;
border-radius: 4px;
}
.skeleton-text {
width: 80%;
height: 8px;
background: #f1f5f9;
border-radius: 2px;
}
.like-btn {
font-size: 0.75rem;
border: 1px solid #e2e8f0;
background: white;
padding: 2px 8px;
border-radius: 10px;
cursor: pointer;
margin-top: 4px;
}
.like-btn.liked {
border-color: #fecaca;
color: #ef4444;
background: #fef2f2;
}
/* Transitions */
.list-enter-active,
.list-leave-active {
transition: all 0.4s ease;
}
.list-enter-from {
opacity: 0;
transform: translateY(20px);
}
.list-leave-to {
opacity: 0;
transform: scale(0.8);
}
</style>
@@ -0,0 +1,237 @@
<template>
<div class="css-props-ref">
<div class="intro">
CSS 属性就像装修队的施工指令常用的其实只有几十个这里有一份装修菜单供你参考
</div>
<div class="categories">
<div
v-for="(cat, index) in categories"
:key="index"
class="category"
>
<div class="cat-title">{{ cat.title }}</div>
<div class="props-grid">
<div
v-for="prop in cat.props"
:key="prop.name"
class="prop-item"
@click="activeProp = prop"
:class="{ active: activeProp && activeProp.name === prop.name }"
>
<div class="prop-name">{{ prop.name }}</div>
<div class="prop-desc">{{ prop.desc }}</div>
</div>
</div>
</div>
</div>
<div v-if="activeProp" class="prop-detail">
<div class="detail-header">
<span class="detail-name">{{ activeProp.name }}</span>
<span class="detail-cat-badge">{{ activeProp.categoryLabel }}</span>
</div>
<div class="detail-desc">{{ activeProp.fullDesc }}</div>
<div class="detail-code">
<div class="code-label">示例代码</div>
<pre><code>{{ activeProp.example }}</code></pre>
</div>
</div>
<div v-else class="prop-detail empty">
点击上面的属性看看它能做什么 👆
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeProp = ref(null)
const categories = [
{
title: '📝 文字与排版',
props: [
{ name: 'color', desc: '文字颜色', categoryLabel: '文字', fullDesc: '改变文字的颜色。可以使用英文单词(red)、十六进制(#ff0000)或RGB值。', example: 'color: #333333;' },
{ name: 'font-size', desc: '字号大小', categoryLabel: '文字', fullDesc: '设置文字的大小。常用单位是 px (像素) 或 rem。', example: 'font-size: 16px;' },
{ name: 'font-weight', desc: '字体粗细', categoryLabel: '文字', fullDesc: '设置文字的粗细。bold 是加粗,normal 是正常。', example: 'font-weight: bold;' },
{ name: 'text-align', desc: '对齐方式', categoryLabel: '排版', fullDesc: '设置文字水平对齐方式:左对齐(left)、居中(center)、右对齐(right)。', example: 'text-align: center;' },
{ name: 'line-height', desc: '行高', categoryLabel: '排版', fullDesc: '设置行间距。通常设为 1.5 左右让阅读更舒服。', example: 'line-height: 1.5;' },
]
},
{
title: '📦 盒子与大小',
props: [
{ name: 'width / height', desc: '宽 / 高', categoryLabel: '尺寸', fullDesc: '设置元素的宽度和高度。', example: 'width: 100px;\nheight: 50px;' },
{ name: 'padding', desc: '内边距', categoryLabel: '间距', fullDesc: '盒子内部的空间(内容距离边框的距离)。像填充泡沫一样撑大盒子。', example: 'padding: 20px;' },
{ name: 'margin', desc: '外边距', categoryLabel: '间距', fullDesc: '盒子外部的空间(盒子与其他元素之间的距离)。', example: 'margin: 20px;' },
{ name: 'background', desc: '背景', categoryLabel: '外观', fullDesc: '设置背景颜色或背景图片。', example: 'background: #f0f0f0;' },
]
},
{
title: '🎨 边框与装饰',
props: [
{ name: 'border', desc: '边框', categoryLabel: '边框', fullDesc: '设置边框的粗细、样式和颜色。', example: 'border: 1px solid #ccc;' },
{ name: 'border-radius', desc: '圆角', categoryLabel: '边框', fullDesc: '让盒子的角变圆润。现在的按钮通常都有点圆角。', example: 'border-radius: 8px;' },
{ name: 'box-shadow', desc: '阴影', categoryLabel: '装饰', fullDesc: '给盒子添加阴影效果,增加立体感和层次感。', example: 'box-shadow: 0 4px 6px rgba(0,0,0,0.1);' },
{ name: 'opacity', desc: '透明度', categoryLabel: '装饰', fullDesc: '设置元素的透明度,0 是全透明(看不见但还在),1 是不透明。', example: 'opacity: 0.8;' },
]
},
{
title: '📐 布局与定位',
props: [
{ name: 'display', desc: '显示模式', categoryLabel: '布局', fullDesc: '决定盒子怎么摆。block(独占一行), flex(弹性布局), none(隐藏)。', example: 'display: flex;' },
{ name: 'position', desc: '定位方式', categoryLabel: '定位', fullDesc: '决定盒子怎么定位。relative(相对), absolute(绝对), fixed(固定在屏幕)。', example: 'position: absolute;\ntop: 0;\nleft: 0;' },
{ name: 'z-index', desc: '层级', categoryLabel: '定位', fullDesc: '决定谁叠在谁上面。数字越大越靠上。', example: 'z-index: 100;' },
{ name: 'cursor', desc: '鼠标手势', categoryLabel: '交互', fullDesc: '鼠标移上去变成什么样。pointer(小手), text(输入光标)。', example: 'cursor: pointer;' },
]
}
]
</script>
<style scoped>
.css-props-ref {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 20px;
margin: 20px 0;
}
.intro {
font-size: 14px;
color: var(--vp-c-text-2);
margin-bottom: 16px;
}
.categories {
display: flex;
flex-direction: column;
gap: 16px;
}
.cat-title {
font-size: 13px;
font-weight: 700;
color: var(--vp-c-text-1);
margin-bottom: 8px;
border-left: 3px solid var(--vp-c-brand);
padding-left: 8px;
}
.props-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 8px;
}
.prop-item {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 8px 12px;
cursor: pointer;
transition: all 0.2s;
}
.prop-item:hover {
border-color: var(--vp-c-brand);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.prop-item.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-dimm);
}
.prop-name {
font-family: var(--vp-font-family-mono);
font-size: 13px;
color: var(--vp-c-brand);
font-weight: 600;
margin-bottom: 2px;
}
.prop-desc {
font-size: 12px;
color: var(--vp-c-text-2);
}
.prop-detail {
margin-top: 20px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
animation: fadeIn 0.3s ease;
}
.prop-detail.empty {
text-align: center;
color: var(--vp-c-text-3);
font-size: 13px;
border-style: dashed;
}
.detail-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.detail-name {
font-family: var(--vp-font-family-mono);
font-size: 16px;
font-weight: 700;
color: var(--vp-c-text-1);
}
.detail-cat-badge {
font-size: 11px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
padding: 2px 6px;
border-radius: 4px;
color: var(--vp-c-text-2);
}
.detail-desc {
font-size: 14px;
color: var(--vp-c-text-2);
line-height: 1.5;
margin-bottom: 12px;
}
.detail-code {
background: var(--vp-c-bg-alt);
padding: 12px;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.code-label {
font-size: 11px;
color: var(--vp-c-text-3);
margin-bottom: 4px;
font-weight: 600;
}
pre {
margin: 0;
background: transparent !important;
padding: 0 !important;
}
code {
font-family: var(--vp-font-family-mono);
font-size: 13px;
color: var(--vp-c-text-1);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
@@ -0,0 +1,295 @@
<!--
CssLayoutDemo.vue
布局演示Flexbox 核心概念交互
-->
<template>
<div class="layout-demo">
<div class="controls">
<div class="control-group">
<label>排列方向 (flex-direction)</label>
<div class="btn-group">
<button
v-for="val in ['row', 'column']"
:key="val"
:class="{ active: direction === val }"
@click="direction = val"
>
{{ val }}
</button>
</div>
</div>
<div class="control-group">
<label>主轴对齐 (justify-content)</label>
<div class="btn-group">
<button
v-for="val in ['flex-start', 'center', 'space-between', 'space-around']"
:key="val"
:class="{ active: justify === val }"
@click="justify = val"
>
{{ val }}
</button>
</div>
</div>
<div class="control-group">
<label>交叉轴对齐 (align-items)</label>
<div class="btn-group">
<button
v-for="val in ['stretch', 'center', 'flex-start', 'flex-end']"
:key="val"
:class="{ active: align === val }"
@click="align = val"
>
{{ val }}
</button>
</div>
</div>
<div class="control-group">
<label>换行 (flex-wrap)</label>
<div class="btn-group">
<button
v-for="val in ['nowrap', 'wrap']"
:key="val"
:class="{ active: wrap === val }"
@click="wrap = val"
>
{{ val }}
</button>
</div>
</div>
</div>
<div class="preview-area">
<div class="container" :style="containerStyle">
<div
v-for="n in itemCount"
:key="n"
class="item"
:style="[itemStyle, getItemColor(n)]"
>
{{ n }}
</div>
</div>
</div>
<div class="code-display">
<div class="code-header">👆 点击代码行可以暂时禁用该属性</div>
<pre>.container {
display: flex;
<div
class="code-line"
:class="{ disabled: !activeProps.direction }"
@click="toggleProp('direction')"
>flex-direction: <span class="val">{{ direction }}</span>;</div>
<div
class="code-line"
:class="{ disabled: !activeProps.justify }"
@click="toggleProp('justify')"
>justify-content: <span class="val">{{ justify }}</span>;</div>
<div
class="code-line"
:class="{ disabled: !activeProps.align }"
@click="toggleProp('align')"
>align-items: <span class="val">{{ align }}</span>;</div>
<div
class="code-line"
:class="{ disabled: !activeProps.wrap }"
@click="toggleProp('wrap')"
>flex-wrap: <span class="val">{{ wrap }}</span>;</div>
/* ...其他样式 */
}</pre>
</div>
</div>
</template>
<script setup>
import { ref, computed, reactive } from 'vue'
const direction = ref('row')
const justify = ref('center')
const align = ref('center')
const wrap = ref('nowrap')
const activeProps = reactive({
direction: true,
justify: true,
align: true,
wrap: true
})
const toggleProp = (prop) => {
activeProps[prop] = !activeProps[prop]
}
const containerStyle = computed(() => {
const style = { display: 'flex' }
if (activeProps.direction) style.flexDirection = direction.value
if (activeProps.justify) style.justifyContent = justify.value
if (activeProps.align) style.alignItems = align.value
if (activeProps.wrap) style.flexWrap = wrap.value
return style
})
const itemStyle = computed(() => {
const style = {}
// Default fixed size
style.width = '60px'
style.height = '60px'
// Adjust for stretch - use effective align/direction values
const effectiveAlign = activeProps.align ? align.value : 'stretch'
const effectiveDirection = activeProps.direction ? direction.value : 'row'
if (effectiveAlign === 'stretch') {
if (effectiveDirection === 'row') {
style.height = 'auto'
} else {
style.width = 'auto'
}
}
return style
})
const itemCount = computed(() => (wrap.value === 'wrap' ? 12 : 5))
const colors = [
'#3b82f6', '#8b5cf6', '#ec4899', '#f59e0b', '#10b981',
'#6366f1', '#14b8a6', '#f97316', '#ef4444', '#84cc16',
'#06b6d4', '#d946ef'
]
const getItemColor = (n) => {
return { background: colors[(n - 1) % colors.length] }
}
</script>
<style scoped>
.layout-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 20px;
margin: 20px 0;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 20px;
}
.control-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.control-group label {
font-size: 12px;
font-weight: 600;
color: var(--vp-c-text-2);
}
.btn-group {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.btn-group button {
padding: 4px 12px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.btn-group button:hover {
border-color: var(--vp-c-brand);
}
.btn-group button.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.preview-area {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
height: 200px;
margin-bottom: 16px;
overflow: hidden;
}
.container {
display: flex;
width: 100%;
height: 100%;
padding: 10px;
gap: 10px;
background-image: radial-gradient(var(--vp-c-divider) 1px, transparent 1px);
background-size: 10px 10px;
}
.item {
/* Dimensions handled by inline style */
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
flex-shrink: 0;
}
.code-display {
background: #1e293b;
padding: 16px;
border-radius: 8px;
color: #e2e8f0;
font-family: monospace;
font-size: 13px;
overflow-x: auto;
}
.code-header {
font-size: 12px;
color: #94a3b8;
margin-bottom: 8px;
font-style: italic;
}
.code-line {
cursor: pointer;
padding: 2px 4px;
border-radius: 4px;
transition: all 0.2s;
width: fit-content;
}
.code-line:hover {
background: rgba(255, 255, 255, 0.1);
}
.code-line.disabled {
opacity: 0.4;
text-decoration: line-through;
color: #94a3b8;
}
.code-line.disabled .val {
color: #94a3b8;
font-weight: normal;
}
pre { margin: 0; }
.val { color: #f472b6; font-weight: bold; }
</style>
@@ -0,0 +1,193 @@
<template>
<div class="css-playground">
<div class="demo-box">
<div
class="target-element"
:style="{
backgroundColor: bgColor,
color: textColor,
fontSize: fontSize + 'px',
padding: padding + 'px',
borderRadius: borderRadius + 'px',
border: `${borderWidth}px solid ${borderColor}`
}"
>
我是演示元素
</div>
</div>
<div class="controls">
<div class="control-group">
<label>背景颜色 (background-color)</label>
<input type="color" v-model="bgColor" />
<span class="value">{{ bgColor }}</span>
</div>
<div class="control-group">
<label>文字颜色 (color)</label>
<input type="color" v-model="textColor" />
<span class="value">{{ textColor }}</span>
</div>
<div class="control-group">
<label>字体大小 (font-size)</label>
<input type="range" v-model="fontSize" min="12" max="48" />
<span class="value">{{ fontSize }}px</span>
</div>
<div class="control-group">
<label>内边距 (padding)</label>
<input type="range" v-model="padding" min="0" max="50" />
<span class="value">{{ padding }}px</span>
</div>
<div class="control-group">
<label>圆角 (border-radius)</label>
<input type="range" v-model="borderRadius" min="0" max="50" />
<span class="value">{{ borderRadius }}px</span>
</div>
<div class="control-group">
<label>边框宽度 (border-width)</label>
<input type="range" v-model="borderWidth" min="0" max="10" />
<span class="value">{{ borderWidth }}px</span>
</div>
<div class="control-group">
<label>边框颜色 (border-color)</label>
<input type="color" v-model="borderColor" />
<span class="value">{{ borderColor }}</span>
</div>
</div>
<div class="code-preview">
<div class="code-title">生成的 CSS 代码</div>
<pre><code>.element {
background-color: <span class="highlight">{{ bgColor }}</span>;
color: <span class="highlight">{{ textColor }}</span>;
font-size: <span class="highlight">{{ fontSize }}px</span>;
padding: <span class="highlight">{{ padding }}px</span>;
border-radius: <span class="highlight">{{ borderRadius }}px</span>;
border: <span class="highlight">{{ borderWidth }}px</span> solid <span class="highlight">{{ borderColor }}</span>;
}</code></pre>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const bgColor = ref('#3b82f6')
const textColor = ref('#ffffff')
const fontSize = ref(16)
const padding = ref(20)
const borderRadius = ref(8)
const borderWidth = ref(0)
const borderColor = ref('#000000')
</script>
<style scoped>
.css-playground {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 20px;
margin: 20px 0;
display: flex;
flex-direction: column;
gap: 20px;
}
.demo-box {
background: var(--vp-c-bg);
border: 1px dashed var(--vp-c-divider);
border-radius: 8px;
height: 150px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.target-element {
transition: all 0.2s ease;
text-align: center;
/* Ensure it doesn't overflow easily */
max-width: 90%;
max-height: 90%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
}
.control-group {
display: flex;
align-items: center;
gap: 10px;
background: var(--vp-c-bg);
padding: 8px 12px;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.control-group label {
font-size: 13px;
color: var(--vp-c-text-2);
flex: 1;
}
.value {
font-family: var(--vp-font-family-mono);
font-size: 12px;
color: var(--vp-c-text-1);
width: 60px;
text-align: right;
}
input[type="range"] {
flex: 1;
cursor: pointer;
}
input[type="color"] {
width: 30px;
height: 30px;
padding: 0;
border: none;
background: none;
cursor: pointer;
border-radius: 4px;
}
.code-preview {
background: #1e1e1e;
border-radius: 8px;
padding: 16px;
color: #d4d4d4;
font-family: var(--vp-font-family-mono);
font-size: 13px;
}
.code-title {
color: #808080;
margin-bottom: 8px;
font-size: 12px;
}
pre {
margin: 0;
white-space: pre-wrap;
}
.highlight {
color: #9cdcfe;
font-weight: bold;
}
</style>
@@ -0,0 +1,253 @@
<template>
<div class="selectors-demo">
<div class="hint">👇 鼠标悬停在左侧 CSS 代码上看看右侧 HTML 谁会被选中</div>
<div class="comparison">
<!-- Left: CSS Rules -->
<div class="column css-col">
<div class="col-title">CSS (样式表)</div>
<div class="rules-list">
<div
class="rule-item"
:class="{ active: activeType === 'tag' }"
@mouseenter="activeType = 'tag'"
@mouseleave="activeType = null"
>
<div class="selector">p</div>
<div class="block">{ color: #333; }</div>
<div class="explanation">
<span class="badge tag">标签选择器</span>
直接写标签名选中所有 <code>&lt;p&gt;</code>
</div>
</div>
<div
class="rule-item"
:class="{ active: activeType === 'class' }"
@mouseenter="activeType = 'class'"
@mouseleave="activeType = null"
>
<div class="selector">.card</div>
<div class="block">{ background: white; }</div>
<div class="explanation">
<span class="badge class">类选择器</span>
<code>.</code> 开头选中所有 <code>class="card"</code>
</div>
</div>
<div
class="rule-item"
:class="{ active: activeType === 'id' }"
@mouseenter="activeType = 'id'"
@mouseleave="activeType = null"
>
<div class="selector">#submit-btn</div>
<div class="block">{ font-weight: bold; }</div>
<div class="explanation">
<span class="badge id">ID 选择器</span>
<code>#</code> 开头选中唯一 <code>id="submit-btn"</code>
</div>
</div>
</div>
</div>
<!-- Center: Connector -->
<div class="connector">
<div class="line-path" :class="activeType"></div>
<div class="icon">🔗</div>
</div>
<!-- Right: HTML Structure -->
<div class="column html-col">
<div class="col-title">HTML (结构)</div>
<div class="code-view">
<div
class="html-line"
:class="{ highlight: activeType === 'tag' }"
>
&lt;p&gt;我是普通段落&lt;/p&gt;
</div>
<div
class="html-line"
:class="{ highlight: activeType === 'class' }"
>
&lt;div <span class="attr">class="card"</span>&gt;
</div>
<div
class="html-line indent"
:class="{ highlight: activeType === 'tag' || activeType === 'class' }"
>
&lt;p&gt;我是卡片里的段落&lt;/p&gt;
</div>
<div
class="html-line"
:class="{ highlight: activeType === 'class' }"
>
&lt;/div&gt;
</div>
<div
class="html-line"
:class="{ highlight: activeType === 'id' }"
>
&lt;button <span class="attr">id="submit-btn"</span>&gt;提交&lt;/button&gt;
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeType = ref(null)
</script>
<style scoped>
.selectors-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 20px;
margin: 20px 0;
font-family: var(--vp-font-family-mono);
font-size: 13px;
}
.hint {
text-align: center;
color: var(--vp-c-text-2);
margin-bottom: 16px;
font-family: var(--vp-font-family-base);
font-size: 14px;
}
.comparison {
display: flex;
gap: 10px;
align-items: stretch;
}
.column {
flex: 1;
display: flex;
flex-direction: column;
}
.col-title {
font-weight: bold;
margin-bottom: 10px;
text-align: center;
color: var(--vp-c-text-1);
}
/* CSS Column */
.rule-item {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
margin-bottom: 10px;
padding: 10px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.rule-item:hover, .rule-item.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-dimm);
transform: translateX(5px);
}
.selector {
color: #d73a49; /* Red-ish for selector */
font-weight: bold;
}
.rule-item:nth-child(2) .selector { color: #6f42c1; } /* Purple for class */
.rule-item:nth-child(3) .selector { color: #005cc5; } /* Blue for ID */
.explanation {
margin-top: 6px;
font-size: 12px;
color: var(--vp-c-text-2);
display: flex;
align-items: center;
gap: 6px;
}
.badge {
font-size: 10px;
padding: 2px 4px;
border-radius: 4px;
color: white;
}
.badge.tag { background: #d73a49; }
.badge.class { background: #6f42c1; }
.badge.id { background: #005cc5; }
/* HTML Column */
.code-view {
background: #1e1e1e;
color: #abb2bf;
padding: 15px;
border-radius: 6px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.html-line {
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s;
border: 1px solid transparent;
}
.html-line.indent {
margin-left: 20px;
}
.html-line.highlight {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
color: white;
text-shadow: 0 0 5px rgba(255,255,255,0.5);
}
.attr {
color: #98c379;
}
/* Connector */
.connector {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
position: relative;
}
.icon {
font-size: 20px;
z-index: 2;
background: var(--vp-c-bg-soft);
}
@media (max-width: 600px) {
.comparison {
flex-direction: column;
}
.rule-item:hover, .rule-item.active {
transform: translateY(2px);
}
.connector {
width: 100%;
height: 30px;
}
}
</style>
@@ -1,289 +1,167 @@
<template>
<div class="dns-lookup-demo">
<div class="control-panel">
<div class="input-group">
<label>Domain</label>
<input
v-model="domain"
placeholder="www.example.com"
class="domain-input"
/>
</div>
<div class="toggle-group">
<label class="toggle">
<input type="checkbox" v-model="useCache" />
<span class="slider"></span>
<span class="label">Simulate Cache Hit</span>
</label>
</div>
<button @click="startLookup" :disabled="isLooking" class="lookup-btn">
{{ isLooking ? 'Resolving...' : 'Lookup' }}
</button>
<div class="dns-lookup-demo simple-mode">
<div class="concept-explanation">
<p class="why-text">
<strong>为什么需要 DNS(查导航)</strong>
</p>
<p class="why-desc-zh">
你知道店铺名字叫 "Shop.com"但快递员需要知道具体的经纬度坐标 (IP 地址) 才能送达
<br>
DNS 就像是<strong>地图导航</strong>输入店名它告诉你具体的坐标
</p>
</div>
<div class="viz-area">
<!-- Client -->
<div class="node client">
<span class="icon">💻</span>
<span>Browser</span>
<div class="demo-stage">
<div class="input-area">
<span class="label">店铺名称 (域名)</span>
<div class="fake-input">shop.com</div>
</div>
<!-- Servers -->
<div class="servers-container">
<div
v-for="(server, index) in servers"
:key="server.name"
class="node server"
:class="{
active: currentServer === index,
success: completed && currentServer === index
}"
>
<div class="server-icon">{{ server.icon }}</div>
<div class="server-name">{{ server.name }}</div>
<div class="server-desc">{{ server.desc }}</div>
<div class="process-animation">
<div class="arrow-down"></div>
<div class="dns-box">
<div class="icon">🧭</div>
<div class="title">DNS (地图导航)</div>
<div class="desc">正在查找 shop.com 的位置...</div>
</div>
<div class="arrow-down"></div>
</div>
<!-- Packet Animation -->
<div v-if="packet.visible" class="packet" :style="packetStyle">
{{ packet.text }}
</div>
</div>
<div class="log-panel" ref="logPanel">
<div v-for="(log, i) in logs" :key="i" class="log-entry">
<span class="time">{{ log.time }}ms</span>
<span class="msg">{{ log.message }}</span>
<div class="output-area">
<span class="label">GPS 坐标 (IP 地址)</span>
<div class="fake-output">93.184.216.34</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, nextTick } from 'vue'
const domain = ref('www.google.com')
const useCache = ref(false)
const isLooking = ref(false)
const currentServer = ref(-1)
const completed = ref(false)
const logs = ref([])
const packet = ref({ visible: false, text: '?', x: 0, y: 0 })
const servers = [
{ name: 'Root (.)', icon: '🌲', desc: 'Global Root' },
{ name: 'TLD (.com)', icon: '🏢', desc: 'Top Level' },
{ name: 'Authoritative', icon: '📝', desc: 'example.com' }
]
const packetStyle = computed(() => ({
transform: `translate(${packet.value.x}px, ${packet.value.y}px)`,
opacity: packet.value.visible ? 1 : 0
}))
const addLog = (message) => {
logs.value.push({
time: Math.floor(performance.now() % 10000),
message
})
// Auto scroll
nextTick(() => {
const el = document.querySelector('.log-panel')
if (el) el.scrollTop = el.scrollHeight
})
}
const startLookup = async () => {
if (isLooking.value) return
isLooking.value = true
currentServer.value = -1
completed.value = false
logs.value = []
addLog(`Starting DNS lookup for ${domain.value}`)
if (useCache.value) {
await wait(500)
addLog('✅ Cache HIT! IP found in browser cache.')
completed.value = true
isLooking.value = false
return
}
// Recursive Query Simulation
for (let i = 0; i < servers.length; i++) {
const server = servers[i]
addLog(`Querying ${server.name}...`)
// Simulate network delay
currentServer.value = i
await wait(800)
if (i < servers.length - 1) {
addLog(`Received referral to ${servers[i + 1].name}`)
} else {
addLog(`✅ Resolved IP: 142.250.185.238`)
}
}
completed.value = true
isLooking.value = false
}
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
// Simplified: No need for complex i18n logic anymore as we display both.
defineProps({
lang: String // Accepted but ignored
})
</script>
<style scoped>
.dns-lookup-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
background: var(--vp-c-bg);
padding: 1.5rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.control-panel {
display: flex;
gap: 1rem;
align-items: center;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.input-group {
display: flex;
flex-direction: column;
flex: 1;
}
.domain-input {
padding: 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
}
.lookup-btn {
padding: 0.5rem 1.5rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
height: 40px;
align-self: flex-end;
}
.lookup-btn:disabled {
opacity: 0.5;
}
.viz-area {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
position: relative;
min-height: 150px;
}
.node {
display: flex;
flex-direction: column;
align-items: center;
padding: 1rem;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
transition: all 0.3s;
width: 100px;
}
.node.client {
border-color: #3b82f6;
}
.servers-container {
display: flex;
gap: 1rem;
}
.node.server.active {
border-color: #f59e0b;
transform: scale(1.1);
box-shadow: 0 0 15px rgba(245, 158, 11, 0.3);
}
.node.server.success {
border-color: #10b981;
}
.server-icon {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.server-name {
font-weight: bold;
font-size: 0.8rem;
}
.server-desc {
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
.log-panel {
background: #1e1e1e;
color: #10b981;
.concept-explanation {
background: var(--vp-c-bg-soft);
padding: 1rem;
border-radius: 6px;
height: 150px;
overflow-y: auto;
font-family: monospace;
font-size: 0.8rem;
margin-bottom: 2rem;
}
.log-entry {
margin-bottom: 0.3rem;
}
.log-entry .time {
color: #6b7280;
margin-right: 0.5rem;
.why-text {
margin: 0 0 0.5rem 0;
color: var(--vp-c-brand);
font-weight: bold;
}
/* Toggle Switch */
.toggle {
.why-desc-en {
margin: 0 0 0.5rem 0;
color: var(--vp-c-text-1);
line-height: 1.5;
font-size: 0.95rem;
}
.why-desc-zh {
margin: 0;
color: var(--vp-c-text-2);
line-height: 1.5;
font-size: 0.9rem;
}
.demo-stage {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.input-area, .output-area {
text-align: center;
width: 100%;
}
.label {
font-size: 0.8rem;
color: var(--vp-c-text-3);
display: block;
margin-bottom: 0.5rem;
}
.fake-input, .fake-output {
background: var(--vp-c-bg-alt);
padding: 0.8rem;
border-radius: 8px;
font-size: 1.2rem;
font-weight: bold;
border: 2px solid var(--vp-c-divider);
}
.fake-input {
border-color: #3b82f6;
color: #3b82f6;
}
.fake-output {
border-color: #10b981;
color: #10b981;
}
.process-animation {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
gap: 0.5rem;
}
.toggle input {
display: none;
.dns-box {
background: #fffbeb;
border: 2px solid #f59e0b;
padding: 1rem 2rem;
border-radius: 12px;
text-align: center;
width: 240px; /* Slightly wider for bilingual text */
}
.toggle .slider {
width: 40px;
height: 20px;
background: #ccc;
border-radius: 20px;
position: relative;
transition: 0.3s;
.html.dark .dns-box {
background: #451a03;
}
.toggle .slider:before {
content: '';
position: absolute;
width: 16px;
height: 16px;
border-radius: 50%;
background: white;
top: 2px;
left: 2px;
transition: 0.3s;
.icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.toggle input:checked + .slider {
background: var(--vp-c-brand);
.title {
font-weight: bold;
color: #d97706;
font-size: 0.9rem;
}
.toggle input:checked + .slider:before {
transform: translateX(20px);
.desc {
font-size: 0.8rem;
color: #b45309;
margin-top: 0.2rem;
}
.arrow-down {
font-size: 1.5rem;
color: var(--vp-c-text-3);
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(5px); }
}
</style>
@@ -0,0 +1,668 @@
<template>
<div class="frontend-evolution-demo">
<!-- Modern Timeline -->
<div class="timeline-container">
<div class="timeline-track"></div>
<button
v-for="(stage, index) in stages"
:key="index"
class="timeline-node"
:class="{ active: currentStage === index, passed: currentStage > index }"
@click="currentStage = index"
>
<div class="node-dot">
<div class="inner-dot"></div>
</div>
<div class="node-content">
<span class="year-badge">{{ stage.year }}</span>
<span class="node-label">{{ stage.label }}</span>
</div>
</button>
</div>
<div class="content-wrapper">
<transition name="fade-slide" mode="out-in">
<div :key="currentStage" class="stage-content">
<div class="header-section">
<h3>
<span class="stage-index">{{ indexToRoman(currentStage + 1) }}.</span>
{{ stages[currentStage].title }}
</h3>
<p>{{ stages[currentStage].desc }}</p>
</div>
<div class="visualization-grid">
<!-- Code Editor -->
<div class="mac-window code-window">
<div class="window-bar">
<div class="traffic-lights">
<span class="light red"></span>
<span class="light yellow"></span>
<span class="light green"></span>
</div>
<div class="window-title">{{ stages[currentStage].codeTitle }}</div>
</div>
<div class="editor-content">
<pre><code>{{ stages[currentStage].code }}</code></pre>
</div>
</div>
<!-- Diagram View -->
<div class="mac-window diagram-window">
<div class="window-bar">
<div class="window-title">Architecture Pattern</div>
</div>
<div class="diagram-canvas">
<!-- Stage 0: Static -->
<div v-if="currentStage === 0" class="diagram static">
<div class="flow-stack">
<div class="concept-box html">
<span class="icon">📄</span> HTML (Content)
</div>
<div class="flow-arrow"></div>
<div class="concept-box browser">
<span class="icon">🌍</span> Browser (Display)
</div>
</div>
<div class="side-note">Server sends complete HTML</div>
</div>
<!-- Stage 1: jQuery -->
<div v-if="currentStage === 1" class="diagram jquery">
<div class="concept-box dom">
<span class="icon">🌳</span> DOM Tree
</div>
<div class="chaos-arrows">
<svg viewBox="0 0 100 60" class="chaos-svg">
<path d="M10,10 Q50,5 90,10" class="arrow-path" marker-end="url(#arrowhead)"/>
<path d="M90,50 Q50,55 10,50" class="arrow-path" marker-end="url(#arrowhead)"/>
<path d="M20,20 Q50,40 80,20" class="arrow-path dashed" marker-end="url(#arrowhead)"/>
</svg>
<span class="label-action">Direct Manipulation</span>
<span class="label-event">Events</span>
</div>
<div class="concept-box js">
<span class="icon">🍝</span> jQuery / JS
</div>
<!-- SVG Marker Definition -->
<svg style="position: absolute; width: 0; height: 0;">
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#666" />
</marker>
</defs>
</svg>
</div>
<!-- Stage 2: MVC -->
<div v-if="currentStage === 2" class="diagram mvc">
<div class="mvc-triangle">
<div class="concept-box model">Model</div>
<div class="concept-box view">View</div>
<div class="concept-box controller">Controller</div>
<!-- Connecting Lines -->
<div class="line m-v"></div>
<div class="line v-c"></div>
<div class="line c-m"></div>
</div>
<div class="mvc-desc">Two-way Binding</div>
</div>
<!-- Stage 3: Component -->
<div v-if="currentStage === 3" class="diagram component">
<div class="comp-structure">
<div class="comp-box root">
<span class="comp-label">App</span>
<div class="comp-children">
<div class="comp-box header">Header</div>
<div class="comp-box list">
ProductList
<div class="comp-children row">
<div class="comp-box item">Item</div>
<div class="comp-box item">Item</div>
</div>
</div>
</div>
</div>
</div>
<div class="flow-pill">
State UI = f(State)
</div>
</div>
</div>
</div>
</div>
</div>
</transition>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentStage = ref(0)
const indexToRoman = (num) => {
const map = { 1: 'I', 2: 'II', 3: 'III', 4: 'IV' }
return map[num] || num
}
const stages = [
{
year: '1990s',
label: 'Static Web',
title: 'The Static Era',
desc: 'Web pages were just digital documents. The server sent HTML, and the browser rendered it. Want new content? Refresh the whole page.',
codeTitle: 'index.html',
code: `<html>
<body>
<h1>Hello World</h1>
<p>Static content served by server.</p>
</body>
</html>`
},
{
year: '2005+',
label: 'jQuery Era',
title: 'Imperative DOM',
desc: 'JS directly manipulated the DOM. "Find that button, add a click listener, change that div\'s color". Logic became tangled like "spaghetti".',
codeTitle: 'script.js',
code: `$('#btn').click(function() {
// Find & Modify directly
$('.box').show();
$('.text').text('Loading...');
// Callback hell...
$.ajax('/api', function(data) {
$('.content').html(data);
});
});`
},
{
year: '2010+',
label: 'MVC/MVVM',
title: 'Framework Era',
desc: 'Separation of concerns. Data (Model) and View were separated. Two-way data binding (like in AngularJS) was magic but performance-heavy.',
codeTitle: 'controller.js',
code: `$scope.user = { name: 'Bob' };
// Magic: Data changes -> View updates
$scope.updateName = function() {
$scope.user.name = 'Alice';
};`
},
{
year: '2013+',
label: 'Component',
title: 'Component Era',
desc: 'UI is broken into independent "Lego blocks" (Components). Declarative: You define "What it looks like given State X", framework handles the "How".',
codeTitle: 'ProductCard.vue',
code: `<template>
<div class="card">
<h3>{{ product.name }}</h3>
<button @click="buy">Buy</button>
</div>
</template>
<script>
// State driven
export default {
props: ['product'],
methods: { buy() { ... } }
}
<\/script>`
}
]
</script>
<style scoped>
.frontend-evolution-demo {
border-radius: 16px;
background: var(--vp-c-bg);
box-shadow: 0 8px 30px rgba(0,0,0,0.05);
border: 1px solid var(--vp-c-divider);
overflow: hidden;
margin: 2rem 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
/* --- Timeline --- */
.timeline-container {
padding: 2rem 1rem 1rem;
background: linear-gradient(to bottom, var(--vp-c-bg-soft), var(--vp-c-bg));
display: flex;
justify-content: space-between;
position: relative;
border-bottom: 1px solid var(--vp-c-divider);
}
.timeline-track {
position: absolute;
top: 2.5rem; /* Center with dots */
left: 3rem;
right: 3rem;
height: 2px;
background: var(--vp-c-divider);
z-index: 0;
}
.timeline-node {
position: relative;
z-index: 1;
background: transparent;
border: none;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
padding: 0;
width: 25%;
transition: all 0.3s ease;
opacity: 0.6;
}
.timeline-node:hover {
opacity: 0.9;
}
.timeline-node.active, .timeline-node.passed {
opacity: 1;
}
.node-dot {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-text-3);
margin-bottom: 0.8rem;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.inner-dot {
width: 0;
height: 0;
border-radius: 50%;
background: var(--vp-c-brand);
transition: all 0.3s;
}
.timeline-node.active .node-dot {
border-color: var(--vp-c-brand);
transform: scale(1.3);
box-shadow: 0 0 0 4px var(--vp-c-bg-soft);
}
.timeline-node.active .inner-dot {
width: 8px;
height: 8px;
}
.timeline-node.passed .node-dot {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
}
.node-content {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.2rem;
}
.year-badge {
font-size: 0.75rem;
font-family: var(--vp-font-family-mono);
background: var(--vp-c-bg-alt);
padding: 2px 6px;
border-radius: 4px;
color: var(--vp-c-text-2);
}
.node-label {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
/* --- Content --- */
.content-wrapper {
padding: 2rem;
min-height: 400px;
}
.header-section {
text-align: center;
margin-bottom: 2rem;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.header-section h3 {
font-size: 1.5rem;
margin-bottom: 0.5rem;
background: linear-gradient(120deg, var(--vp-c-brand), #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.stage-index {
color: var(--vp-c-text-3);
-webkit-text-fill-color: var(--vp-c-text-3);
margin-right: 0.5rem;
font-weight: normal;
}
.header-section p {
font-size: 1rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
/* --- Visualization Grid --- */
.visualization-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
align-items: stretch;
}
@media (max-width: 768px) {
.visualization-grid {
grid-template-columns: 1fr;
}
}
.mac-window {
border-radius: 12px;
border: 1px solid rgba(0,0,0,0.1);
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
overflow: hidden;
display: flex;
flex-direction: column;
background: white;
transition: transform 0.3s ease;
}
.mac-window:hover {
transform: translateY(-5px);
}
.code-window {
background: #1e1e2e; /* Dark theme */
}
.diagram-window {
background: var(--vp-c-bg-alt);
}
.window-bar {
padding: 0.8rem 1rem;
background: rgba(255,255,255,0.05); /* Transparent on dark */
border-bottom: 1px solid rgba(255,255,255,0.1);
display: flex;
align-items: center;
position: relative;
}
.diagram-window .window-bar {
background: white;
border-bottom: 1px solid rgba(0,0,0,0.05);
}
.traffic-lights {
display: flex;
gap: 6px;
}
.light {
width: 10px;
height: 10px;
border-radius: 50%;
}
.light.red { background: #ff5f56; }
.light.yellow { background: #ffbd2e; }
.light.green { background: #27c93f; }
.window-title {
position: absolute;
left: 0;
right: 0;
text-align: center;
font-size: 0.8rem;
color: #9ca3af;
font-family: var(--vp-font-family-mono);
}
.diagram-window .window-title {
color: var(--vp-c-text-2);
font-weight: 600;
}
.editor-content {
padding: 1rem;
overflow: auto;
flex: 1;
}
.editor-content pre {
margin: 0;
background: transparent;
padding: 0;
}
.editor-content code {
font-family: 'Fira Code', 'Menlo', monospace;
font-size: 0.85rem;
line-height: 1.5;
color: #a6accd;
}
.diagram-canvas {
padding: 2rem;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
min-height: 250px;
position: relative;
}
/* --- Diagram Specifics --- */
.concept-box {
background: white;
border: 1px solid rgba(0,0,0,0.1);
padding: 0.8rem 1.2rem;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
font-weight: 600;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 0.5rem;
z-index: 2;
}
.icon { font-size: 1.2rem; }
/* Static Diagram */
.diagram.static .flow-stack {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.side-note {
margin-top: 1rem;
font-size: 0.8rem;
color: var(--vp-c-text-3);
background: rgba(0,0,0,0.05);
padding: 4px 8px;
border-radius: 4px;
}
/* jQuery Diagram */
.diagram.jquery {
flex-direction: column;
gap: 1rem;
width: 100%;
}
.chaos-arrows {
position: relative;
height: 80px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.chaos-svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: visible;
}
.arrow-path {
fill: none;
stroke: #9ca3af;
stroke-width: 2;
}
.arrow-path.dashed {
stroke-dasharray: 4;
}
.label-action, .label-event {
font-size: 0.75rem;
background: white;
padding: 2px 4px;
border-radius: 4px;
z-index: 1;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.label-action { transform: translate(-20px, -10px); }
.label-event { transform: translate(20px, 10px); }
/* MVC Diagram */
.diagram.mvc {
flex-direction: column;
gap: 1rem;
}
.mvc-triangle {
position: relative;
width: 200px;
height: 160px;
}
.mvc-triangle .model { position: absolute; top: 0; left: 50%; transform: translateX(-50%); }
.mvc-triangle .view { position: absolute; bottom: 0; left: 0; }
.mvc-triangle .controller { position: absolute; bottom: 0; right: 0; }
.line {
position: absolute;
background: #cbd5e1;
z-index: 1;
}
.line.m-v {
height: 2px;
width: 110px;
top: 45%;
left: 20px;
transform: rotate(60deg);
}
.line.v-c {
height: 2px;
width: 100px;
bottom: 20px;
left: 50px;
}
.line.c-m {
height: 2px;
width: 110px;
top: 45%;
right: 20px;
transform: rotate(-60deg);
}
.mvc-desc {
margin-top: 1rem;
font-size: 0.85rem;
font-weight: bold;
color: var(--vp-c-brand);
}
/* Component Diagram */
.diagram.component {
flex-direction: column;
gap: 1.5rem;
}
.comp-structure {
display: flex;
justify-content: center;
}
.comp-box {
background: white;
border: 2px solid #3b82f6;
border-radius: 6px;
padding: 6px;
font-size: 0.8rem;
text-align: center;
box-shadow: 0 4px 0 rgba(59, 130, 246, 0.2);
}
.comp-box.root {
background: #eff6ff;
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px;
}
.comp-label {
font-weight: bold;
color: #1e40af;
}
.comp-children {
display: flex;
gap: 8px;
justify-content: center;
}
.comp-children.row {
margin-top: 4px;
}
.comp-box.header { background: #dbeafe; border-style: dashed; }
.comp-box.list { background: #dbeafe; }
.comp-box.item { background: #bfdbfe; font-size: 0.7rem; padding: 4px; }
.flow-pill {
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
color: white;
padding: 0.5rem 1.5rem;
border-radius: 20px;
font-weight: bold;
font-size: 0.9rem;
box-shadow: 0 4px 10px rgba(59, 130, 246, 0.3);
}
/* Transitions */
.fade-slide-enter-active,
.fade-slide-leave-active {
transition: all 0.4s ease;
}
.fade-slide-enter-from {
opacity: 0;
transform: translateY(20px);
}
.fade-slide-leave-to {
opacity: 0;
transform: translateY(-20px);
}
</style>
@@ -1,82 +1,101 @@
<template>
<div class="http-exchange-demo">
<div class="demo-container">
<!-- Client Side -->
<div class="panel client-panel">
<div class="panel-header">
<span class="icon">💻</span> Client (Browser)
</div>
<div class="browser-frame">
<!-- Address Bar (Simplified) -->
<div class="address-bar">
<select v-model="method" class="method-select" :disabled="loading">
<option>GET</option>
<option>POST</option>
<option>PUT</option>
<option>DELETE</option>
</select>
<input v-model="path" class="url-input" :disabled="loading" />
<button @click="sendRequest" :disabled="loading" class="send-btn">
{{ loading ? '...' : t.send }}
</button>
</div>
<div class="request-builder">
<div class="input-row">
<select v-model="method" class="method-select">
<option>GET</option>
<option>POST</option>
<option>PUT</option>
<option>DELETE</option>
</select>
<input
v-model="path"
class="path-input"
placeholder="/index.html"
/>
<div class="split-view">
<!-- Network Log (Left) -->
<div class="network-log">
<div class="log-header">
<span>{{ t.cols.name }}</span>
<span>{{ t.cols.status }}</span>
<span>{{ t.cols.type }}</span>
<span>{{ t.cols.time }}</span>
</div>
<div class="headers-section">
<div class="section-title">Headers</div>
<div v-for="(value, key) in headers" :key="key" class="header-row">
<span class="header-key">{{ key }}:</span>
<span class="header-value">{{ value }}</span>
</div>
</div>
<button
@click="sendRequest"
class="send-btn"
:disabled="isProcessing"
<div
class="log-row"
:class="{ active: requestSent, selected: true }"
v-if="requestSent"
>
{{ isProcessing ? 'Sending...' : 'Send Request' }}
</button>
<span class="col-name">{{ path.split('/').pop() || 'index' }}</span>
<span class="col-status" :class="statusClass">{{ responseStatus }}</span>
<span class="col-type">document</span>
<span class="col-time">{{ loading ? 'Pending' : '45ms' }}</span>
</div>
<div v-else class="empty-state">{{ t.noRequests }}</div>
</div>
</div>
<!-- Network Visualization -->
<div class="network-space">
<div class="connection-line"></div>
<div
v-if="currentPacket"
class="packet"
:class="currentPacket.type"
:style="{ left: packetPosition + '%' }"
>
{{ currentPacket.label }}
</div>
</div>
<!-- Details Panel (Right) -->
<div class="details-panel" v-if="requestSent">
<div class="tabs">
<button
v-for="tabKey in ['headers', 'response', 'preview']"
:key="tabKey"
:class="{ active: activeTab === tabKey }"
@click="activeTab = tabKey"
>
{{ t.tabs[tabKey] }}
</button>
</div>
<!-- Server Side -->
<div class="panel server-panel">
<div class="panel-header"><span class="icon">🖥</span> Server</div>
<div class="response-viewer" :class="{ empty: !response }">
<div v-if="response">
<div class="status-row" :class="statusClass">
{{ response.status }} {{ response.statusText }}
</div>
<div class="headers-section">
<div
v-for="(value, key) in response.headers"
:key="key"
class="header-row"
>
<span class="header-key">{{ key }}:</span>
<span class="header-value">{{ value }}</span>
<div class="tab-content">
<!-- Headers Tab -->
<div v-if="activeTab === 'headers'" class="headers-view">
<div class="section">
<div class="section-title">{{ t.general }}</div>
<div class="kv-row">
<span class="key">{{ t.requestUrl }}:</span>
<span class="value">https://api.example.com{{ path }}</span>
</div>
<div class="kv-row">
<span class="key">{{ t.requestMethod }}:</span>
<span class="value">{{ method }}</span>
</div>
<div class="kv-row">
<span class="key">{{ t.statusCode }}:</span>
<span class="value">
<span class="status-dot" :class="statusClass"></span>
{{ responseStatus || '...' }}
</span>
</div>
</div>
<div class="section">
<div class="section-title">{{ t.responseHeaders }}</div>
<div class="kv-row" v-for="(val, key) in responseHeaders" :key="key">
<span class="key">{{ key }}:</span>
<span class="value">{{ val }}</span>
</div>
</div>
</div>
<div class="body-preview">
{{ response.body }}
<!-- Response Tab -->
<div v-if="activeTab === 'response'" class="code-view">
<pre>{{ responseBody }}</pre>
</div>
<!-- Preview Tab -->
<div v-if="activeTab === 'preview'" class="preview-view">
<div v-if="method === 'GET'" class="html-preview" v-html="responseBody"></div>
<div v-else class="json-preview">
JSON Data: {{ responseBody }}
</div>
</div>
</div>
<div v-else class="placeholder">Waiting for request...</div>
</div>
<div v-else class="details-placeholder">
{{ t.placeholder }}
</div>
</div>
</div>
@@ -86,252 +105,260 @@
<script setup>
import { ref, computed } from 'vue'
const method = ref('GET')
const path = ref('/index.html')
const isProcessing = ref(false)
const packetPosition = ref(10)
const currentPacket = ref(null)
const response = ref(null)
const headers = ref({
Host: 'www.example.com',
'User-Agent': 'Mozilla/5.0',
Accept: 'text/html'
const props = defineProps({
lang: {
type: String,
default: 'zh'
}
})
const responses = {
GET: {
status: 200,
statusText: 'OK',
headers: {
'Content-Type': 'text/html',
Server: 'Nginx'
},
body: '<!DOCTYPE html>\n<html>\n <body>Hello World</body>\n</html>'
const t = {
send: '提交订单 (发送请求)',
noRequests: '购物车是空的 (无请求)',
placeholder: '点击 "提交订单" 向店员购买玩具',
general: '订单详情 (General)',
requestUrl: '商品地址 (URL)',
requestMethod: '操作类型 (Method)',
statusCode: '店员回复 (Status)',
responseHeaders: '包裹标签 (Headers)',
tabs: {
headers: '订单信息',
response: '包裹内容',
preview: '玩具预览'
},
POST: {
status: 201,
statusText: 'Created',
headers: {
'Content-Type': 'application/json'
},
body: '{"success": true, "id": 123}'
cols: {
name: '商品',
status: '状态',
type: '类型',
time: '耗时'
}
}
const method = ref('GET')
const path = ref('/toys/lego-castle')
const loading = ref(false)
const requestSent = ref(false)
const activeTab = ref('headers')
const responseStatus = ref('')
const responseBody = ref('')
const responseHeaders = ref({})
const sendRequest = async () => {
if (loading.value) return
loading.value = true
requestSent.value = true
responseStatus.value = '处理中...'
await new Promise(r => setTimeout(r, 800))
loading.value = false
if (method.value === 'GET') {
responseStatus.value = '200 OK (有货)'
responseHeaders.value = {
'Content-Type': 'application/json (积木)',
'Date': new Date().toLocaleString(),
'Store': '乐高官方店'
}
responseBody.value = `{\n "id": 101,\n "name": "Lego Castle",\n "pieces": 500,\n "price": "$99"\n}`
} else {
responseStatus.value = '201 Created (下单成功)'
responseHeaders.value = {
'Content-Type': 'application/json',
'Date': new Date().toLocaleString()
}
responseBody.value = `{\n "success": true,\n "message": "Order placed"\n}`
}
}
const statusClass = computed(() => {
if (!response.value) return ''
const code = response.value.status
if (code >= 200 && code < 300) return 'success'
if (code >= 400) return 'error'
return ''
if (loading.value) return 'pending'
if (responseStatus.value.startsWith('2')) return 'success'
return 'error'
})
const sendRequest = () => {
if (isProcessing.value) return
isProcessing.value = true
response.value = null
// Animate Request
currentPacket.value = {
type: 'request',
label: `${method.value} ${path.value}`
}
animatePacket(10, 90, () => {
// Server Processing
setTimeout(() => {
// Prepare Response
const mockResponse = responses[method.value] || responses['GET']
// Animate Response
currentPacket.value = {
type: 'response',
label: `${mockResponse.status} ${mockResponse.statusText}`
}
animatePacket(90, 10, () => {
response.value = mockResponse
currentPacket.value = null
isProcessing.value = false
})
}, 800)
})
}
const animatePacket = (start, end, callback) => {
let pos = start
const step = (end - start) / 50
const interval = setInterval(() => {
pos += step
packetPosition.value = pos
if ((step > 0 && pos >= end) || (step < 0 && pos <= end)) {
clearInterval(interval)
callback()
}
}, 10)
}
</script>
<style scoped>
.http-exchange-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.demo-container {
display: flex;
justify-content: space-between;
align-items: stretch;
gap: 1rem;
height: 300px;
}
.panel {
flex: 1;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 1rem;
margin: 1rem 0;
font-family: system-ui, -apple-system, sans-serif;
overflow: hidden;
}
.browser-frame {
display: flex;
flex-direction: column;
height: 400px;
}
.panel-header {
font-weight: bold;
.address-bar {
padding: 0.5rem;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
padding-bottom: 0.5rem;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.network-space {
width: 20%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.connection-line {
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 2px;
background: var(--vp-c-divider);
border-top: 1px dashed var(--vp-c-text-3);
}
.packet {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
padding: 0.3rem 0.6rem;
border-radius: 4px;
font-size: 0.7rem;
white-space: nowrap;
z-index: 10;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.packet.request {
background: #3b82f6;
color: white;
}
.packet.response {
background: #10b981;
color: white;
}
.input-row {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.method-select {
padding: 0.3rem;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
font-weight: bold;
}
.path-input {
.url-input {
flex: 1;
padding: 0.3rem;
border-radius: 4px;
padding: 0.3rem 0.5rem;
border: 1px solid var(--vp-c-divider);
}
.headers-section {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-bottom: 1rem;
background: var(--vp-c-bg-alt);
padding: 0.5rem;
border-radius: 4px;
}
.header-row {
display: flex;
justify-content: space-between;
}
.send-btn {
width: 100%;
padding: 0.5rem;
background: var(--vp-c-brand);
color: white;
border: none;
padding: 0 1rem;
border-radius: 4px;
cursor: pointer;
}
.send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.response-viewer {
.split-view {
flex: 1;
font-size: 0.8rem;
overflow-y: auto;
}
.response-viewer.empty {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.placeholder {
.network-log {
width: 40%;
border-right: 1px solid var(--vp-c-divider);
display: flex;
flex-direction: column;
font-size: 0.8rem;
}
.log-header {
display: flex;
padding: 0.5rem;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
font-weight: bold;
color: var(--vp-c-text-2);
}
.log-header span { flex: 1; }
.log-row {
display: flex;
padding: 0.5rem;
cursor: pointer;
border-bottom: 1px solid var(--vp-c-divider);
}
.log-row.selected {
background: #e0f2fe; /* Light blue */
}
html.dark .log-row.selected {
background: #1e3a8a;
}
.log-row span { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.col-status.success { color: #10b981; }
.col-status.pending { color: #9ca3af; }
.empty-state {
padding: 2rem;
text-align: center;
color: var(--vp-c-text-3);
}
.status-row {
.details-panel {
flex: 1;
display: flex;
flex-direction: column;
font-size: 0.85rem;
}
.details-placeholder {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: var(--vp-c-text-3);
}
.tabs {
display: flex;
border-bottom: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
}
.tabs button {
padding: 0.5rem 1rem;
background: none;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
color: var(--vp-c-text-2);
}
.tabs button.active {
border-bottom-color: var(--vp-c-brand);
color: var(--vp-c-text-1);
}
.tab-content {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.section {
margin-bottom: 1.5rem;
}
.section-title {
font-weight: bold;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.status-row.success {
color: #10b981;
}
.status-row.error {
color: #ef4444;
}
.body-preview {
margin-top: 1rem;
padding: 0.5rem;
background: var(--vp-c-bg-alt);
border-radius: 4px;
.kv-row {
display: flex;
margin-bottom: 0.3rem;
font-family: monospace;
white-space: pre-wrap;
}
.kv-row .key {
width: 120px;
color: var(--vp-c-text-2);
flex-shrink: 0;
}
.kv-row .value {
color: var(--vp-c-text-1);
word-break: break-all;
}
.code-view pre {
margin: 0;
white-space: pre-wrap;
font-family: monospace;
}
.status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 4px;
}
.status-dot.success { background: #10b981; }
.status-dot.pending { background: #9ca3af; }
</style>
@@ -0,0 +1,222 @@
<template>
<div class="imperative-declarative-demo">
<div class="demo-grid">
<!-- Imperative (jQuery Style) -->
<div class="panel imperative">
<div class="panel-header">
<span class="badge yellow">Imperative (命令式)</span>
<span class="sub-text">jQuery Style</span>
</div>
<div class="code-preview">
<code>
// 手动操作 DOM<br>
$('#count').text(val);<br>
if (val > 5) $('#msg').show();
</code>
</div>
<div class="interactive-area">
<div class="output-box">
Count: <span id="imp-count-display">{{ impCount }}</span>
<div v-show="impShowMsg" class="warning-msg"> Count is high!</div>
</div>
<div class="actions">
<button @click="impIncrement" class="btn">Step 1: Value++</button>
<button @click="impUpdateText" class="btn" :disabled="!impChanged">Step 2: Update Text</button>
<button @click="impCheckState" class="btn" :disabled="!impTextUpdated">Step 3: Check Logic</button>
</div>
<div class="status-log">{{ impStatus }}</div>
</div>
</div>
<!-- Declarative (Vue Style) -->
<div class="panel declarative">
<div class="panel-header">
<span class="badge green">Declarative (声明式)</span>
<span class="sub-text">Vue/React Style</span>
</div>
<div class="code-preview">
<code>
// 只需要绑定数据<br>
{{ '{' + '{ count }' + '}' }}<br>
&lt;div v-if="count > 5"&gt;...&lt;/div&gt;
</code>
</div>
<div class="interactive-area">
<div class="output-box">
Count: <span>{{ decCount }}</span>
<div v-if="decCount > 5" class="warning-msg"> Count is high!</div>
</div>
<div class="actions">
<button @click="decIncrement" class="btn primary">Value++ (Auto Render)</button>
</div>
<div class="status-log">Framework handles DOM updates automatically.</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
// Imperative State
const impCount = ref(0)
const impShowMsg = ref(false)
const impChanged = ref(false)
const impTextUpdated = ref(false)
const impStatus = ref('Ready.')
const impIncrement = () => {
// Logic layer changes, but DOM doesn't
impStatus.value = 'Variable `count` is now ' + (impCount.value + 1) + '. DOM is NOT updated.'
impCount.value++
impChanged.value = true
impTextUpdated.value = false
}
const impUpdateText = () => {
// Manually update text
impStatus.value = 'DOM text updated manually.'
impChanged.value = false
impTextUpdated.value = true
}
const impCheckState = () => {
// Manually check logic
if (impCount.value > 5) {
impShowMsg.value = true
impStatus.value = 'Logic checked: > 5. Manually showing message.'
} else {
impShowMsg.value = false
impStatus.value = 'Logic checked: <= 5. No message.'
}
}
// Declarative State
const decCount = ref(0)
const decIncrement = () => {
decCount.value++
}
</script>
<style scoped>
.imperative-declarative-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
}
.demo-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
@media (max-width: 640px) {
.demo-grid { grid-template-columns: 1fr; }
}
.panel {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.panel-header {
padding: 0.8rem;
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
justify-content: space-between;
align-items: center;
background: var(--vp-c-bg-alt);
}
.badge {
font-size: 0.8rem;
font-weight: bold;
padding: 2px 6px;
border-radius: 4px;
color: white;
}
.badge.yellow { background: #f59e0b; }
.badge.green { background: #10b981; }
.sub-text {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.code-preview {
background: #1e1e2e;
padding: 0.8rem;
font-family: monospace;
font-size: 0.8rem;
color: #a6accd;
height: 80px;
}
.interactive-area {
padding: 1rem;
flex: 1;
display: flex;
flex-direction: column;
gap: 1rem;
}
.output-box {
background: var(--vp-c-bg-soft);
padding: 1rem;
border-radius: 6px;
text-align: center;
font-weight: bold;
min-height: 80px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.warning-msg {
color: #ef4444;
margin-top: 0.5rem;
font-size: 0.9rem;
animation: popIn 0.3s;
}
.actions {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.btn {
padding: 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 0.8rem;
transition: all 0.2s;
}
.btn:hover:not(:disabled) { background: #f3f4f6; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn.primary { background: #3b82f6; color: white; border: none; }
.btn.primary:hover { background: #2563eb; }
.status-log {
font-size: 0.75rem;
color: var(--vp-c-text-3);
font-style: italic;
min-height: 1.2em;
}
@keyframes popIn {
0% { transform: scale(0.8); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
</style>
@@ -1,100 +1,153 @@
<template>
<div class="tcp-handshake-demo">
<div class="diagram">
<!-- Client Column -->
<div class="column client">
<div class="actor-icon">💻 Client</div>
<div class="state-label">{{ clientState }}</div>
<div class="controls">
<div class="status-indicator">
{{ t.statusLabel }}: <span :class="connectionStatus.toLowerCase()">{{ statusText }}</span>
</div>
<div class="buttons">
<button v-if="step === 0" @click="startHandshake" class="action-btn">{{ t.connect }}</button>
<button v-else @click="reset" class="reset-btn">{{ t.reset }}</button>
</div>
</div>
<div class="sequence-diagram">
<!-- Client Timeline -->
<div class="timeline client">
<div class="actor">
<span class="icon">💻</span>
<span class="name">{{ t.client }}</span>
</div>
<div class="line"></div>
<div class="state-marker" :class="{ active: step >= 1 }">SYN_SENT</div>
<div class="state-marker" :class="{ active: step >= 3 }">ESTABLISHED</div>
</div>
<!-- Interaction Area -->
<div class="interaction-zone">
<!-- Step 1: SYN -->
<div class="packet-row" :class="{ active: step === 1, done: step > 1 }">
<button
@click="sendSyn"
:disabled="step !== 0"
class="packet-btn syn"
>
SYN (SEQ=x)
</button>
<div class="interaction-space">
<!-- SYN Packet -->
<div class="packet-track">
<transition name="slide-right">
<div v-if="showSyn" class="packet syn">
<div class="packet-body">SYN</div>
<div class="packet-detail">SEQ=0</div>
</div>
</transition>
</div>
<!-- Step 2: SYN-ACK -->
<div
class="packet-row reverse"
:class="{ active: step === 2, done: step > 2 }"
>
<button
@click="sendSynAck"
:disabled="step !== 1"
class="packet-btn syn-ack"
>
SYN-ACK (ACK=x+1, SEQ=y)
</button>
<!-- SYN-ACK Packet -->
<div class="packet-track reverse">
<transition name="slide-left">
<div v-if="showSynAck" class="packet syn-ack">
<div class="packet-body">SYN-ACK</div>
<div class="packet-detail">SEQ=0, ACK=1</div>
</div>
</transition>
</div>
<!-- Step 3: ACK -->
<div class="packet-row" :class="{ active: step === 3, done: step > 3 }">
<button
@click="sendAck"
:disabled="step !== 2"
class="packet-btn ack"
>
ACK (ACK=y+1)
</button>
<!-- ACK Packet -->
<div class="packet-track">
<transition name="slide-right">
<div v-if="showAck" class="packet ack">
<div class="packet-body">ACK</div>
<div class="packet-detail">SEQ=1, ACK=1</div>
</div>
</transition>
</div>
</div>
<!-- Server Column -->
<div class="column server">
<div class="actor-icon">🖥 Server</div>
<div class="state-label">{{ serverState }}</div>
<!-- Server Timeline -->
<div class="timeline server">
<div class="actor">
<span class="icon">🖥</span>
<span class="name">{{ t.server }}</span>
</div>
<div class="line"></div>
<div class="state-marker" :class="{ active: step >= 2 }">SYN_RCVD</div>
<div class="state-marker" :class="{ active: step >= 3 }">ESTABLISHED</div>
</div>
</div>
<div class="status-message">
<p v-if="step === 0">点击 <strong>SYN</strong> 开始连接</p>
<p v-if="step === 1">
服务器收到了请求现在需要回复 <strong>SYN-ACK</strong>
</p>
<p v-if="step === 2">
客户端收到了确认最后发送 <strong>ACK</strong> 完成握手
</p>
<p v-if="step === 3" class="success">🎉 连接已建立 (ESTABLISHED)!</p>
<div class="description-box">
<p>{{ currentDescription }}</p>
</div>
<button v-if="step === 3" @click="reset" class="reset-btn">Reset</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
const props = defineProps({
lang: {
type: String,
default: 'zh'
}
})
// Bilingual text directly
const t = {
statusLabel: '通话状态',
connect: '拨打电话',
reset: '挂断重拨',
client: '我 (顾客)',
server: '玩具店',
status: {
closed: '未通话',
handshaking: '正在拨号...',
established: '通话中 (连接已建立)'
},
steps: {
0: '点击 "拨打电话" 开始确认店铺是否营业。',
1: '步骤 1: 我问 "喂?有人在吗?" (SYN)',
2: '步骤 2: 店员答 "在的!请问有什么事?" (SYN-ACK)',
3: '步骤 3: 我说 "太好了,我想买东西!" (ACK)'
}
}
const step = ref(0)
const clientState = ref('CLOSED')
const serverState = ref('LISTEN')
const showSyn = ref(false)
const showSynAck = ref(false)
const showAck = ref(false)
const sendSyn = () => {
const connectionStatus = computed(() => {
if (step.value === 0) return 'closed'
if (step.value < 3) return 'handshaking'
return 'established'
})
const statusText = computed(() => {
const s = connectionStatus.value
return t.status[s] || s.toUpperCase()
})
const currentDescription = computed(() => {
return t.steps[step.value] || ''
})
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
const startHandshake = async () => {
if (step.value > 0) return
// Step 1: SYN
step.value = 1
clientState.value = 'SYN_SENT'
}
const sendSynAck = () => {
showSyn.value = true
await wait(1500)
// Step 2: SYN-ACK
step.value = 2
serverState.value = 'SYN_RCVD'
}
const sendAck = () => {
showSynAck.value = true
await wait(1500)
// Step 3: ACK
step.value = 3
clientState.value = 'ESTABLISHED'
serverState.value = 'ESTABLISHED'
showAck.value = true
}
const reset = () => {
step.value = 0
clientState.value = 'CLOSED'
serverState.value = 'LISTEN'
showSyn.value = false
showSynAck.value = false
showAck.value = false
}
</script>
@@ -102,127 +155,200 @@ const reset = () => {
.tcp-handshake-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
background: var(--vp-c-bg);
padding: 1.5rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
text-align: center;
}
.diagram {
.controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.column {
width: 120px;
display: flex;
flex-direction: column;
gap: 1rem;
}
.actor-icon {
font-size: 1.2rem;
.status-indicator {
font-weight: bold;
}
.status-indicator span.closed { color: var(--vp-c-text-3); }
.status-indicator span.handshaking { color: #f59e0b; }
.status-indicator span.established { color: #10b981; }
.state-label {
padding: 0.5rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.interaction-zone {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
gap: 1.5rem;
padding: 0 2rem;
}
.packet-row {
display: flex;
justify-content: flex-start;
opacity: 0.3;
transition: all 0.3s;
}
.packet-row.reverse {
justify-content: flex-end;
}
.packet-row.active {
opacity: 1;
transform: scale(1.05);
}
.packet-row.done {
opacity: 1;
}
.packet-btn {
padding: 0.5rem 1rem;
border-radius: 20px;
border: none;
cursor: pointer;
font-family: monospace;
font-size: 0.85rem;
transition: all 0.2s;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
}
.packet-btn:not(:disabled):hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.packet-btn.syn {
.action-btn {
background: #3b82f6;
color: white;
}
.packet-btn.syn-ack {
background: #f59e0b;
color: white;
}
.packet-btn.ack {
background: #10b981;
color: white;
}
.packet-btn:disabled {
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-3);
cursor: not-allowed;
border-color: transparent;
}
.status-message {
height: 2rem;
margin-bottom: 1rem;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.status-message .success {
color: #10b981;
font-weight: bold;
border: none;
padding: 0.5rem 1.5rem;
border-radius: 4px;
cursor: pointer;
}
.reset-btn {
padding: 0.5rem 1.5rem;
background: var(--vp-c-bg);
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
padding: 0.5rem 1.5rem;
border-radius: 4px;
cursor: pointer;
}
.reset-btn:hover {
.sequence-diagram {
display: flex;
justify-content: space-between;
height: 300px;
position: relative;
}
.timeline {
display: flex;
flex-direction: column;
align-items: center;
width: 100px;
position: relative;
}
.actor {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 1rem;
z-index: 2;
background: var(--vp-c-bg);
}
.timeline .line {
width: 2px;
background: var(--vp-c-divider);
flex: 1;
}
.state-marker {
margin-top: 2rem;
padding: 0.3rem 0.6rem;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 0.7rem;
color: var(--vp-c-text-3);
transition: all 0.3s;
}
.state-marker.active {
background: #10b981;
color: white;
border-color: #10b981;
}
.interaction-space {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-around;
padding: 2rem 0;
}
.packet-track {
height: 40px;
position: relative;
display: flex;
align-items: center;
}
.packet-track.reverse {
justify-content: flex-end;
}
.packet {
background: #3b82f6;
color: white;
padding: 0.5rem 1rem;
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
min-width: 120px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
z-index: 10;
}
.packet.syn-ack { background: #f59e0b; }
.packet.ack { background: #10b981; }
.packet-body { font-weight: bold; }
.packet-detail { font-size: 0.7rem; opacity: 0.9; }
/* Animations */
.slide-right-enter-active {
animation: slide-right 1.5s linear;
}
.slide-left-enter-active {
animation: slide-left 1.5s linear;
}
@keyframes slide-right {
0% { transform: translateX(0); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translateX(100%); opacity: 1; } /* Not quite right, need to stick */
}
/*
Vue transitions are tricky for "moving across".
Let's use a simpler approach: CSS transitions on left/right property or keyframes.
Actually, for a "send" animation, we want it to move from A to B and then stay or disappear.
Here I want it to appear and move.
*/
.slide-right-enter-active,
.slide-left-enter-active {
transition: all 1.5s cubic-bezier(0.25, 1, 0.5, 1);
}
.slide-right-enter-from {
transform: translateX(-150px);
opacity: 0;
}
.slide-right-enter-to {
transform: translateX(0);
opacity: 1;
}
/* This is getting complicated with Vue transitions for simple movement.
Let's just use CSS keyframes on the element itself when it renders.
*/
.packet {
animation-duration: 1s;
animation-fill-mode: forwards;
animation-timing-function: ease-in-out;
}
.packet-track .packet {
animation-name: moveRight;
}
.packet-track.reverse .packet {
animation-name: moveLeft;
}
@keyframes moveRight {
from { transform: translateX(-100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes moveLeft {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.description-box {
margin-top: 1rem;
padding: 1rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
text-align: center;
min-height: 3rem;
color: var(--vp-c-text-2);
}
</style>
@@ -1,76 +1,129 @@
<template>
<div class="url-parser-demo">
<div class="control-panel">
<div class="input-group">
<label>输入 URL</label>
<div class="browser-bar">
<div class="nav-buttons">
<span class="nav-btn"></span>
<span class="nav-btn"></span>
<span class="nav-btn"></span>
</div>
<div class="omnibox">
<span class="lock-icon">🔒</span>
<!-- Segmented URL Display -->
<div class="segmented-url" v-if="parsedUrl">
<span
class="url-part protocol"
:class="{ active: highlightedPart === 'protocol' }"
@mouseover="highlightedPart = 'protocol'"
@mouseleave="highlightedPart = null"
>{{ parts.protocol }}:</span>
<span class="divider">//</span>
<span
class="url-part host"
:class="{ active: highlightedPart === 'host' }"
@mouseover="highlightedPart = 'host'"
@mouseleave="highlightedPart = null"
>{{ parts.host }}</span>
<span
v-if="parts.port"
class="url-part port"
:class="{ active: highlightedPart === 'port' }"
@mouseover="highlightedPart = 'port'"
@mouseleave="highlightedPart = null"
>:{{ parts.port }}</span>
<span
class="url-part pathname"
:class="{ active: highlightedPart === 'pathname' }"
@mouseover="highlightedPart = 'pathname'"
@mouseleave="highlightedPart = null"
>{{ parts.pathname }}</span>
<span
v-if="parts.search"
class="url-part search"
:class="{ active: highlightedPart === 'search' }"
@mouseover="highlightedPart = 'search'"
@mouseleave="highlightedPart = null"
>{{ parts.search }}</span>
<span
v-if="parts.hash"
class="url-part hash"
:class="{ active: highlightedPart === 'hash' }"
@mouseover="highlightedPart = 'hash'"
@mouseleave="highlightedPart = null"
>{{ parts.hash }}</span>
</div>
<input
v-else
v-model="inputUrl"
type="text"
placeholder="https://www.example.com:8080/path?query=1#fragment"
class="url-input"
placeholder="https://example.com"
/>
</div>
<div class="encoding-toggle">
<button @click="encodeUrl" class="action-btn">Encode</button>
<button @click="decodeUrl" class="action-btn">Decode</button>
</div>
</div>
<div class="visualization-area">
<div v-if="parsedUrl" class="url-parts">
<div
v-for="(part, key) in parts"
<div v-if="parsedUrl" class="url-breakdown">
<div
v-for="(part, key) in parts"
:key="key"
class="url-part"
:class="key"
class="url-segment"
:class="[key, { active: highlightedPart === key }]"
@mouseover="highlightedPart = key"
@mouseleave="highlightedPart = null"
>
<div class="part-label">{{ labels[key] }}</div>
<div class="part-value">{{ part || '-' }}</div>
<div class="part-desc" v-if="highlightedPart === key">
{{ descriptions[key] }}
<div class="segment-header">
<span class="segment-icon">{{ icons[key] }}</span>
<span class="segment-label">{{ labels[key] }}</span>
</div>
<div class="segment-value">{{ part || '-' }}</div>
<div class="segment-desc">{{ descriptions[key] }}</div>
</div>
</div>
<div v-else class="error-message">无效的 URL 格式</div>
</div>
<div class="info-box">
<p>
<span class="icon">💡</span>
<strong>Note:</strong>
URL (统一资源定位符)
是互联网资源的地址浏览器首先需要将它拆解成不同的部分才能知道要去哪里域名用什么方式协议找什么东西路径
</p>
<div v-else class="error-state">
Invalid URL format / 无效的 URL 格式
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ref, computed } from 'vue'
const inputUrl = ref(
'https://www.example.com:8080/search/results?q=vue&page=1#top'
)
const props = defineProps({
lang: {
type: String,
default: 'zh'
}
})
const inputUrl = ref('https://shop.com/toys/lego-castle?color=red#summary')
const highlightedPart = ref(null)
const icons = {
protocol: '🚛',
host: '🏢',
port: '🚪',
pathname: '🧸',
search: '📝',
hash: '📍'
}
const labels = {
protocol: '协议 (Protocol)',
host: '域名 (Host)',
port: '端口 (Port)',
pathname: '路径 (Path)',
search: '查询 (Query)',
hash: '锚点 (Fragment)'
protocol: '交通方式 (Protocol)',
host: '店铺地址 (Host)',
port: '大门号 (Port)',
pathname: '商品位置 (Path)',
search: '备注要求 (Query)',
hash: '快速定位 (Hash)'
}
const descriptions = {
protocol: '告诉浏览器使用什么方式连接(如 https 安全连接)',
host: '服务器的地址,需要通过 DNS 解析为 IP',
port: '服务器的门牌号(http默认80,https默认443',
pathname: '资源在服务器上的具体位置',
search: '传递给服务器的额外参数',
hash: '页面内的定位标记,不会发送给服务器'
protocol: '怎么去?(例如 https = 坐装甲车去,很安全)',
host: '去哪家店?(域名,例如 shop.com)',
port: '从哪个门进?(默认 443 号门)',
pathname: '商品在哪个货架?(路径)',
search: '给店员的备注 (例如 ?color=red)',
hash: '直接翻到说明书第几页 (锚点)'
}
const parsedUrl = computed(() => {
@@ -86,162 +139,173 @@ const parts = computed(() => {
return {
protocol: parsedUrl.value.protocol.replace(':', ''),
host: parsedUrl.value.hostname,
port:
parsedUrl.value.port ||
(parsedUrl.value.protocol === 'https:' ? '443' : '80'),
port: parsedUrl.value.port || (parsedUrl.value.protocol === 'https:' ? '443' : '80'),
pathname: parsedUrl.value.pathname,
search: parsedUrl.value.search,
hash: parsedUrl.value.hash
}
})
const encodeUrl = () => {
inputUrl.value = encodeURI(inputUrl.value)
}
const decodeUrl = () => {
inputUrl.value = decodeURI(inputUrl.value)
}
</script>
<style scoped>
.url-parser-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
background-color: var(--vp-c-bg);
overflow: hidden;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.control-panel {
margin-bottom: 2rem;
.browser-bar {
background: var(--vp-c-bg-soft);
padding: 0.8rem;
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
}
.nav-buttons {
display: flex;
gap: 0.5rem;
color: var(--vp-c-text-2);
font-size: 1.2rem;
user-select: none;
}
.omnibox {
flex: 1;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 20px;
padding: 0.4rem 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
font-size: 0.9rem;
overflow: hidden;
}
.lock-icon {
font-size: 0.8rem;
}
/* Segmented URL Styles */
.segmented-url {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.url-part {
padding: 2px 4px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-weight: bold;
}
.url-part:hover, .url-part.active {
transform: scale(1.1);
}
.url-part.protocol { color: #ef4444; }
.url-part.host { color: #3b82f6; }
.url-part.port { color: #f59e0b; }
.url-part.pathname { color: #10b981; }
.url-part.search { color: #8b5cf6; }
.url-part.hash { color: #ec4899; }
.url-part.active.protocol { background: #fef2f2; }
.url-part.active.host { background: #eff6ff; }
.url-part.active.port { background: #fffbeb; }
.url-part.active.pathname { background: #ecfdf5; }
.url-part.active.search { background: #f5f3ff; }
.url-part.active.hash { background: #fdf2f8; }
.divider {
color: var(--vp-c-text-3);
margin: 0 1px;
}
.visualization-area {
padding: 1.5rem;
}
.url-breakdown {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 1rem;
}
.input-group {
.url-segment {
padding: 1rem;
border-radius: 8px;
border: 2px solid transparent; /* Prepare for border color */
background: var(--vp-c-bg-alt);
transition: all 0.2s;
cursor: default;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.url-input {
padding: 0.8rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-family: monospace;
width: 100%;
.url-segment.active {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
}
.encoding-toggle {
/* Color Coding for Cards */
.url-segment.protocol { border-color: #ef4444; }
.url-segment.host { border-color: #3b82f6; }
.url-segment.port { border-color: #f59e0b; }
.url-segment.pathname { border-color: #10b981; }
.url-segment.search { border-color: #8b5cf6; }
.url-segment.hash { border-color: #ec4899; }
.url-segment.active.protocol { background: #fef2f2; }
.url-segment.active.host { background: #eff6ff; }
.url-segment.active.port { background: #fffbeb; }
.url-segment.active.pathname { background: #ecfdf5; }
.url-segment.active.search { background: #f5f3ff; }
.url-segment.active.hash { background: #fdf2f8; }
.segment-header {
display: flex;
gap: 1rem;
align-items: center;
gap: 0.5rem;
border-bottom: 1px solid rgba(0,0,0,0.05);
padding-bottom: 0.5rem;
}
.action-btn {
padding: 0.4rem 1rem;
border-radius: 4px;
background: var(--vp-c-brand);
color: white;
border: none;
cursor: pointer;
font-size: 0.9rem;
.segment-icon {
font-size: 1.2rem;
}
.action-btn:hover {
background: var(--vp-c-brand-dark);
}
.visualization-area {
margin-bottom: 1.5rem;
min-height: 200px;
}
.url-parts {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.url-part {
flex: 1;
min-width: 140px;
padding: 1rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
position: relative;
transition: all 0.2s;
cursor: default;
}
.url-part:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: var(--vp-c-brand);
}
.url-part.protocol {
border-left: 4px solid #ef4444;
}
.url-part.host {
border-left: 4px solid #3b82f6;
}
.url-part.port {
border-left: 4px solid #f59e0b;
}
.url-part.pathname {
border-left: 4px solid #10b981;
}
.url-part.search {
border-left: 4px solid #8b5cf6;
}
.url-part.hash {
border-left: 4px solid #ec4899;
}
.part-label {
.segment-label {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
font-weight: bold;
color: var(--vp-c-text-1);
}
.part-value {
font-size: 1rem;
.segment-value {
font-size: 1.1rem;
font-weight: bold;
word-break: break-all;
font-family: monospace;
}
.part-desc {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--vp-c-text-inverse-1);
color: var(--vp-c-text-inverse-2);
padding: 0.5rem;
border-radius: 4px;
.segment-desc {
font-size: 0.8rem;
z-index: 10;
margin-top: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 1rem;
border-radius: 6px;
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.4;
}
.error-message {
color: #ef4444;
.error-state {
text-align: center;
color: #ef4444;
padding: 2rem;
}
</style>
@@ -1,33 +1,43 @@
<template>
<div class="url-to-browser-demo">
<div class="stage-nav">
<div class="stage-tracker">
<button
v-for="(stage, index) in stages"
:key="index"
:class="{ active: currentStage === index }"
class="tracker-node"
:class="{ active: currentStage === index, visited: currentStage > index }"
@click="currentStage = index"
>
<span class="stage-num">{{ index + 1 }}</span>
<span class="stage-name">{{ stage.name }}</span>
<div class="node-circle">
<span class="icon">{{ stage.icon }}</span>
</div>
<span class="node-label">{{ stage.name }}</span>
</button>
<div class="tracker-line">
<div class="line-fill" :style="{ width: (currentStage / (stages.length - 1)) * 100 + '%' }"></div>
</div>
</div>
<div class="stage-content">
<transition name="fade" mode="out-in">
<div :key="currentStage" class="stage-viz">
<div class="viz-icon">{{ stages[currentStage].icon }}</div>
<div class="viz-desc">
<h3>{{ stages[currentStage].title }}</h3>
<p>{{ stages[currentStage].desc }}</p>
</div>
<div class="viz-action">
<component
:is="stages[currentStage].component"
v-if="stages[currentStage].component"
/>
</div>
</div>
</transition>
<div class="stage-display">
<div class="header">
<h2>{{ stages[currentStage].title }}</h2>
<p>{{ stages[currentStage].desc }}</p>
</div>
<div class="component-wrapper">
<transition name="fade" mode="out-in">
<component
:is="stages[currentStage].component"
:key="currentStage"
/>
</transition>
</div>
<div class="action-footer" v-if="currentStage < stages.length - 1">
<button class="next-btn" @click="nextStage">
下一步
</button>
</div>
</div>
</div>
</template>
@@ -40,117 +50,185 @@ const currentStage = ref(0)
const stages = [
{
name: 'URL',
title: 'Parsing the Address',
desc: 'Browser breaks down the URL to understand protocol, domain, and path.',
icon: '🔍',
title: '1. 填写购物单 (URL)',
desc: '你想买一个玩具。首先要在订单上写清楚:去哪家店 (域名)、买什么 (路径)、用什么快递 (协议)。',
icon: '📝',
component: 'UrlParserDemo'
},
{
name: 'DNS',
title: 'Finding the IP',
desc: 'Browser asks DNS servers to translate the domain name into an IP address.',
icon: '🌐',
title: '2. 查找店铺地址 (DNS)',
desc: '快递员不知道 "玩具店" 在哪。他需要查地图 (DNS),把店名翻译成具体的 GPS 坐标 (IP 地址)。',
icon: '🧭',
component: 'DnsLookupDemo'
},
{
name: 'TCP',
title: 'Establishing Connection',
desc: 'Browser and Server perform a 3-way handshake to create a reliable connection.',
icon: '🤝',
title: '3. 建立通话 (TCP)',
desc: '找到店了!进店前先敲门确认:"有人吗?" "有!" "那我进来了!"。确保连接通畅,不会白跑一趟。',
icon: '📞',
component: 'TcpHandshakeDemo'
},
{
name: 'HTTP',
title: 'Exchanging Data',
desc: 'Browser sends a request, and the server sends back the website content.',
icon: '📨',
title: '4. 购买商品 (HTTP)',
desc: '进店后,你递交订单:"我要这个玩具"。店员去仓库找货,最后把装有玩具的包裹 (HTML) 递给你。',
icon: '📦',
component: 'HttpExchangeDemo'
},
{
name: 'Render',
title: 'Painting the Page',
desc: 'Browser parses HTML/CSS and paints pixels on your screen.',
icon: '🎨',
title: '5. 拆盒组装 (渲染)',
desc: '回到家,拆开包裹。照着说明书 (HTML),把积木 (DOM) 搭起来,涂上颜色 (CSS),玩具就变好看了!',
icon: '🧩',
component: 'BrowserRenderingDemo'
}
]
const nextStage = () => {
if (currentStage.value < stages.length - 1) {
currentStage.value++
}
}
</script>
<style scoped>
.url-to-browser-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
border-radius: 12px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
overflow: hidden;
margin: 2rem 0;
box-shadow: 0 4px 24px rgba(0,0,0,0.05);
}
.stage-nav {
.stage-tracker {
display: flex;
justify-content: space-between;
margin-bottom: 2rem;
padding: 2rem 2rem 1rem;
position: relative;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
}
.stage-nav::before {
content: '';
.tracker-line {
position: absolute;
top: 50%;
left: 0;
right: 0;
top: 3.2rem; /* Adjusted for padding */
left: 3.5rem;
right: 3.5rem;
height: 2px;
background: var(--vp-c-divider);
z-index: 0;
}
.stage-nav button {
.line-fill {
height: 100%;
background: var(--vp-c-brand);
transition: width 0.3s ease;
}
.tracker-node {
position: relative;
z-index: 1;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 20px;
padding: 0.5rem 1rem;
background: transparent;
border: none;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
cursor: pointer;
transition: all 0.2s;
padding: 0;
width: 60px;
}
.stage-nav button.active {
.node-circle {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
transition: all 0.3s;
}
.tracker-node.visited .node-circle {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
}
.tracker-node.active .node-circle {
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 4px var(--vp-c-brand-dimm);
transform: scale(1.1);
background: var(--vp-c-bg);
}
.stage-num {
.node-label {
font-size: 0.75rem;
font-weight: bold;
font-family: monospace;
color: var(--vp-c-text-2);
}
.stage-content {
.tracker-node.active .node-label {
color: var(--vp-c-brand);
}
.stage-display {
padding: 2rem;
}
.header {
text-align: center;
margin-bottom: 2rem;
}
.header h2 {
border: none;
margin: 0 0 0.5rem 0;
padding: 0;
font-size: 1.5rem;
}
.header p {
margin: 0;
color: var(--vp-c-text-2);
max-width: 600px;
margin: 0 auto;
}
.component-wrapper {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 2rem;
text-align: center;
min-height: 200px;
/* padding: 1rem; */
}
.viz-icon {
font-size: 3rem;
margin-bottom: 1rem;
.action-footer {
margin-top: 2rem;
display: flex;
justify-content: center;
}
.viz-desc h3 {
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
.next-btn {
padding: 0.8rem 2rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 25px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.viz-desc p {
color: var(--vp-c-text-2);
max-width: 500px;
margin: 0 auto;
.next-btn:hover {
background: var(--vp-c-brand-dark);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0,0,0,0.15);
}
.fade-enter-active,
@@ -23,6 +23,37 @@
</div>
</div>
<!-- 🎨 交互式配置面板 -->
<div class="config-panel" v-if="current === 'css'">
<div class="config-header">
<div class="config-title">🎨 教学演示参数调整 ( CSS 模式生效)</div>
<button class="reset-btn" @click="resetColors">重置默认</button>
</div>
<div class="config-items">
<div class="config-item">
<label>Primary Color (主题色)</label>
<div class="input-group">
<input type="color" v-model="customColors.primary" />
<input type="text" v-model="customColors.primary" class="hex-input" />
</div>
</div>
<div class="config-item">
<label>Text Color (文字色)</label>
<div class="input-group">
<input type="color" v-model="customColors.text" />
<input type="text" v-model="customColors.text" class="hex-input" />
</div>
</div>
<div class="config-item">
<label>Button Text (按钮文字)</label>
<div class="input-group">
<input type="color" v-model="customColors.btnText" />
<input type="text" v-model="customColors.btnText" class="hex-input" />
</div>
</div>
</div>
</div>
<div class="preview" :class="current">
<div class="hint">点一下标题/段落/按钮我会在下面的代码里高亮对应行</div>
<h1
@@ -98,7 +129,48 @@
</template>
<script setup>
import { computed, ref } from 'vue'
import { computed, ref, reactive } from 'vue'
const props = defineProps({
primaryColor: {
type: String,
default: '#0ea5e9'
},
textColor: {
type: String,
default: '#111827'
},
btnTextColor: {
type: String,
default: '#fff'
}
})
// 🎨 用户自定义颜色状态
const customColors = reactive({
primary: props.primaryColor,
text: props.textColor,
btnText: props.btnTextColor
})
// 重置为默认值
const resetColors = () => {
customColors.primary = props.primaryColor
customColors.text = props.textColor
customColors.btnText = props.btnTextColor
}
// =============================================================================
// 🔧 演示参数配置 (Demo Configuration)
// =============================================================================
// 优先使用用户自定义的颜色,如果有变化的话
const DEMO_CONFIG = computed(() => ({
colors: {
primary: customColors.primary,
text: customColors.text,
btnText: customColors.btnText
}
}))
const modes = [
{ id: 'html', label: '看骨架 (HTML)' },
@@ -126,9 +198,9 @@ const codeLines = computed(() => {
}
if (current.value === 'css') {
return [
{ key: 'h1', text: '.hero { color: #0ea5e9; font-size: 24px; }' },
{ key: 'p', text: '.desc { color: #111827; }' },
{ key: 'btn', text: '.cta { background: #0ea5e9; color: #fff; border-radius: 10px; }' }
{ key: 'h1', text: `.hero { color: ${DEMO_CONFIG.value.colors.primary}; font-size: 24px; }` },
{ key: 'p', text: `.desc { color: ${DEMO_CONFIG.value.colors.text}; }` },
{ key: 'btn', text: `.cta { background: ${DEMO_CONFIG.value.colors.primary}; color: ${DEMO_CONFIG.value.colors.btnText}; border-radius: 10px; }` }
]
}
return [
@@ -211,6 +283,91 @@ const increment = () => {
gap: 12px;
}
/* New Config Panel Styles */
.config-panel {
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
margin-bottom: 20px;
}
.config-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.config-title {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.config-items {
display: flex;
flex-wrap: wrap;
gap: 24px;
}
.config-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.config-item label {
font-size: 12px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.input-group {
display: flex;
align-items: center;
gap: 8px;
background: var(--vp-c-bg);
padding: 4px;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
input[type="color"] {
width: 28px;
height: 28px;
border: none;
padding: 0;
background: none;
cursor: pointer;
border-radius: 4px;
}
.hex-input {
border: none;
background: transparent;
color: var(--vp-c-text-1);
font-size: 13px;
width: 65px;
font-family: var(--vp-font-family-mono);
outline: none;
}
.reset-btn {
font-size: 12px;
color: var(--vp-c-brand);
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-brand);
border-radius: 4px;
cursor: pointer;
padding: 4px 12px;
transition: all 0.2s;
}
.reset-btn:hover {
background: var(--vp-c-brand);
color: white;
}
.top { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }
.title { font-weight: 800; font-size: 16px; }
.subtitle { color: var(--vp-c-text-2); font-size: 13px; margin-top: 4px; }
@@ -282,18 +439,43 @@ const increment = () => {
.click-tip { margin-top: 6px; color: var(--vp-c-text-2); font-size: 13px; }
.preview.css .hero { color: #0ea5e9; }
.preview.css .desc { color: var(--vp-c-text-1); }
.preview.css .cta { background: #0ea5e9; color: #fff; border-color: #0ea5e9; box-shadow: 0 4px 12px rgba(14,165,233,0.25); }
.preview.css .hero { color: v-bind('DEMO_CONFIG.colors.primary'); font-size: 24px; }
.preview.css .desc { color: v-bind('DEMO_CONFIG.colors.text'); }
.preview.css .cta {
background: v-bind('DEMO_CONFIG.colors.primary');
color: v-bind('DEMO_CONFIG.colors.btnText');
border-color: v-bind('DEMO_CONFIG.colors.primary');
border-radius: 10px;
}
.preview.js .cta { background: #22c55e; color: #fff; border-color: #22c55e; box-shadow: 0 4px 12px rgba(34,197,94,0.25); }
.preview.js { border-color: rgba(34, 197, 94, 0.4); }
.code-block { background: var(--vp-c-bg-alt); border: 1px solid var(--vp-c-divider); border-radius: 10px; padding: 16px; }
.code-title { font-weight: 700; margin-bottom: 8px; font-size: 13px; color: var(--vp-c-text-2); }
pre { margin: 0; background: #0b1221; color: #e5e7eb; border-radius: 8px; padding: 16px; font-family: var(--vp-font-family-mono); font-size: 13px; overflow-x: auto; line-height: 1.6; }
.code-content {
background: #0b1221;
color: #e5e7eb;
border-radius: 8px;
padding: 16px;
font-family: var(--vp-font-family-mono);
font-size: 13px;
overflow-x: auto;
line-height: 1.6;
}
.line { min-height: 1.6em; }
.hl { background: rgba(34, 197, 94, 0.2); border-radius: 4px; display: block; width: 100%; }
.hl {
background: var(--vp-c-brand-dimm);
border-left: 3px solid var(--vp-c-brand);
border-radius: 4px;
display: block;
width: 100%;
padding-left: 8px; /* Offset text to account for border */
font-weight: bold; /* Make text bolder */
/* color: white; Removed to fix visibility issue on light theme if brand-dimm is light */
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); /* Add subtle shadow */
}
.explain { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }
.card { background: var(--vp-c-bg); border: 1px dashed var(--vp-c-divider); border-radius: 10px; padding: 10px; }