feat: add AI and Backend evolution history with interactive demos, and refine Frontend evolution demo
This commit is contained in:
@@ -534,13 +534,6 @@ export default defineConfig({
|
||||
text: '人工智能基础',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: '大语言模型', link: '/zh-cn/appendix/llm-intro' },
|
||||
{ text: '多模态大模型', link: '/zh-cn/appendix/vlm-intro' },
|
||||
{
|
||||
text: 'AI 绘画原理',
|
||||
link: '/zh-cn/appendix/image-gen-intro'
|
||||
},
|
||||
{ text: 'AI 音频模型', link: '/zh-cn/appendix/audio-intro' },
|
||||
{
|
||||
text: '提示词工程',
|
||||
link: '/zh-cn/appendix/prompt-engineering'
|
||||
@@ -549,6 +542,14 @@ export default defineConfig({
|
||||
text: '上下文工程',
|
||||
link: '/zh-cn/appendix/context-engineering'
|
||||
},
|
||||
{ text: '人工智能进化史', link: '/zh-cn/appendix/ai-evolution' },
|
||||
{ text: '大语言模型', link: '/zh-cn/appendix/llm-intro' },
|
||||
{ text: '多模态大模型', link: '/zh-cn/appendix/vlm-intro' },
|
||||
{
|
||||
text: 'AI 绘画原理',
|
||||
link: '/zh-cn/appendix/image-gen-intro'
|
||||
},
|
||||
{ text: 'AI 音频模型', link: '/zh-cn/appendix/audio-intro' },
|
||||
{ text: 'Agent 智能体', link: '/zh-cn/appendix/agent-intro' },
|
||||
{
|
||||
text: 'AI 能力词典',
|
||||
@@ -564,10 +565,19 @@ export default defineConfig({
|
||||
text: 'HTML/CSS/JS 基础',
|
||||
link: '/zh-cn/appendix/web-basics'
|
||||
},
|
||||
{
|
||||
text: '前端进化史',
|
||||
link: '/zh-cn/appendix/frontend-evolution'
|
||||
},
|
||||
{
|
||||
text: '后端进化史',
|
||||
link: '/zh-cn/appendix/backend-evolution'
|
||||
},
|
||||
{
|
||||
text: 'URL 到浏览器显示',
|
||||
link: '/zh-cn/appendix/url-to-browser'
|
||||
}
|
||||
},
|
||||
{ text: '浏览器调试器', link: '/zh-cn/appendix/browser-devtools' }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
<template>
|
||||
<div class="ai-evolution-demo">
|
||||
<!-- 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>
|
||||
|
||||
<!-- Content -->
|
||||
<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">
|
||||
<!-- Concept/Logic View -->
|
||||
<div class="mac-window concept-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">Core Logic</div>
|
||||
</div>
|
||||
<div class="concept-canvas">
|
||||
<!-- Stage 0: Symbolism -->
|
||||
<div v-if="currentStage === 0" class="vis-symbolism">
|
||||
<div class="logic-gate">
|
||||
<div class="input-group">
|
||||
<span class="input-val">A: True</span>
|
||||
<span class="input-val">B: False</span>
|
||||
</div>
|
||||
<div class="gate-box">
|
||||
AND Rule
|
||||
</div>
|
||||
<div class="output-val">Output: False</div>
|
||||
</div>
|
||||
<div class="math-note">If A and B then C</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage 1: Expert Systems -->
|
||||
<div v-if="currentStage === 1" class="vis-expert">
|
||||
<div class="decision-tree">
|
||||
<div class="tree-node root">Is it raining?</div>
|
||||
<div class="branches">
|
||||
<div class="branch">
|
||||
<span class="condition">Yes</span>
|
||||
<div class="tree-node leaf">Take Umbrella</div>
|
||||
</div>
|
||||
<div class="branch">
|
||||
<span class="condition">No</span>
|
||||
<div class="tree-node leaf">Go Out</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kb-note">Knowledge Base + Inference Engine</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage 2: Deep Learning -->
|
||||
<div v-if="currentStage === 2" class="vis-dl">
|
||||
<div class="neural-net">
|
||||
<div class="layer input">
|
||||
<div class="neuron" v-for="n in 3" :key="`i-${n}`"></div>
|
||||
</div>
|
||||
<div class="layer hidden">
|
||||
<div class="neuron" v-for="n in 4" :key="`h-${n}`"></div>
|
||||
</div>
|
||||
<div class="layer output">
|
||||
<div class="neuron" v-for="n in 2" :key="`o-${n}`"></div>
|
||||
</div>
|
||||
<!-- Connections drawn via CSS/SVG ideally, simplified here -->
|
||||
<svg class="connections">
|
||||
<line x1="10" y1="20" x2="60" y2="10" />
|
||||
<line x1="10" y1="20" x2="60" y2="30" />
|
||||
<!-- Abstract lines -->
|
||||
</svg>
|
||||
</div>
|
||||
<div class="dl-note">Feature Extraction (Black Box)</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage 3: GenAI -->
|
||||
<div v-if="currentStage === 3" class="vis-genai">
|
||||
<div class="transformer-block">
|
||||
<div class="block-layer attn">Self-Attention</div>
|
||||
<div class="block-layer ff">Feed Forward</div>
|
||||
<div class="block-layer norm">Norm & Add</div>
|
||||
</div>
|
||||
<div class="chat-sim">
|
||||
<div class="msg user">"Draw a cat"</div>
|
||||
<div class="msg ai">Generates 🐱...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Application/Impact View -->
|
||||
<div class="mac-window app-window">
|
||||
<div class="window-bar">
|
||||
<div class="window-title">Real-world Impact</div>
|
||||
</div>
|
||||
<div class="app-canvas">
|
||||
<div class="impact-card">
|
||||
<div class="impact-icon">{{ stages[currentStage].icon }}</div>
|
||||
<div class="impact-title">{{ stages[currentStage].appTitle }}</div>
|
||||
<div class="impact-desc">{{ stages[currentStage].appDesc }}</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: '1950s-1970s',
|
||||
label: 'Symbolism',
|
||||
title: 'The Dawn: Logic & Rules',
|
||||
desc: 'AI started as "Symbolic AI". Scientists believed intelligence could be described by formal logic and rules. If we can write down all the rules of the world, a computer can be intelligent.',
|
||||
icon: '♟️',
|
||||
appTitle: 'Chess & Logic',
|
||||
appDesc: 'Programs could solve logic puzzles and play simple chess, but failed at "common sense" or recognizing a cat in a photo.'
|
||||
},
|
||||
{
|
||||
year: '1980s-1990s',
|
||||
label: 'Expert Systems',
|
||||
title: 'Knowledge Engineering',
|
||||
desc: 'The era of "Expert Systems". We tried to hard-code human expertise (e.g., medical diagnosis rules) into databases. Useful for specific domains, but brittle and hard to maintain.',
|
||||
icon: '🏥',
|
||||
appTitle: 'MYCIN / Deep Blue',
|
||||
appDesc: 'Systems that could diagnose blood infections or beat Garry Kasparov at chess (Deep Blue, 1997), but still lacked true learning capability.'
|
||||
},
|
||||
{
|
||||
year: '2010s',
|
||||
label: 'Deep Learning',
|
||||
title: 'Connectionism & Big Data',
|
||||
desc: 'The breakthrough of Neural Networks. Inspired by the human brain, computers learned patterns from massive data instead of being told rules. AlexNet (2012) changed everything.',
|
||||
icon: '🧠',
|
||||
appTitle: 'AlphaGo & FaceID',
|
||||
appDesc: 'AI learned to see (ImageNet), hear (Siri), and play Go (AlphaGo). It surpassed humans in specific perceptual tasks.'
|
||||
},
|
||||
{
|
||||
year: '2020s+',
|
||||
label: 'Generative AI',
|
||||
title: 'Generative Intelligence (LLMs)',
|
||||
desc: 'The Transformer architecture allowed AI to understand context and generate new content. AI moved from "classifying" (is this a cat?) to "creating" (draw a cat).',
|
||||
icon: '✨',
|
||||
appTitle: 'ChatGPT & Midjourney',
|
||||
appDesc: 'AI that can write code, poetry, paint images, and reason across multiple domains. A step towards AGI (General Intelligence).'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ai-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;
|
||||
}
|
||||
|
||||
/* Reusing Timeline Styles from FrontendEvolutionDemo for consistency */
|
||||
.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;
|
||||
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;
|
||||
}
|
||||
|
||||
.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.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
/* Content Area */
|
||||
.content-wrapper { padding: 2rem; min-height: 400px; }
|
||||
|
||||
.header-section {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
max-width: 600px;
|
||||
margin: 0 auto 2rem;
|
||||
}
|
||||
|
||||
.header-section h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: linear-gradient(120deg, #10b981, #3b82f6); /* Green to Blue for AI */
|
||||
-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 */
|
||||
.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.05);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: white;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.mac-window:hover { transform: translateY(-5px); }
|
||||
|
||||
.concept-window { background: #f8fafc; }
|
||||
.app-window { background: white; }
|
||||
|
||||
.window-bar {
|
||||
padding: 0.8rem 1rem;
|
||||
background: white;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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: var(--vp-c-text-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.concept-canvas, .app-canvas {
|
||||
padding: 2rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
/* Visualizations */
|
||||
/* Symbolism */
|
||||
.logic-gate {
|
||||
border: 2px solid #334155;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
background: white;
|
||||
}
|
||||
.input-group { display: flex; gap: 1rem; justify-content: center; margin-bottom: 0.5rem; font-family: monospace; }
|
||||
.gate-box { background: #334155; color: white; padding: 4px 10px; margin: 0.5rem 0; border-radius: 4px; }
|
||||
.math-note { margin-top: 1rem; font-family: monospace; color: #64748b; font-size: 0.8rem; }
|
||||
|
||||
/* Expert Systems */
|
||||
.decision-tree { display: flex; flex-direction: column; align-items: center; gap: 1rem; }
|
||||
.tree-node { border: 1px solid #cbd5e1; padding: 6px 12px; border-radius: 20px; background: white; font-size: 0.8rem; }
|
||||
.tree-node.root { border-color: #3b82f6; color: #3b82f6; font-weight: bold; }
|
||||
.branches { display: flex; gap: 2rem; }
|
||||
.branch { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; }
|
||||
.condition { font-size: 0.7rem; color: #64748b; background: #f1f5f9; padding: 2px 6px; border-radius: 4px; }
|
||||
.kb-note { margin-top: 1rem; font-size: 0.8rem; color: #64748b; font-style: italic; }
|
||||
|
||||
/* Deep Learning */
|
||||
.neural-net { display: flex; gap: 2rem; align-items: center; position: relative; }
|
||||
.layer { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.neuron { width: 12px; height: 12px; border-radius: 50%; background: #cbd5e1; border: 1px solid #94a3b8; }
|
||||
.layer.input .neuron { background: #93c5fd; }
|
||||
.layer.hidden .neuron { background: #fca5a5; }
|
||||
.layer.output .neuron { background: #86efac; }
|
||||
.connections { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; opacity: 0.2; }
|
||||
.connections line { stroke: #000; stroke-width: 1; }
|
||||
.dl-note { margin-top: 2rem; font-size: 0.8rem; color: #64748b; }
|
||||
|
||||
/* GenAI */
|
||||
.vis-genai { display: flex; flex-direction: column; gap: 1rem; align-items: center; width: 100%; }
|
||||
.transformer-block { border: 2px solid #8b5cf6; border-radius: 8px; padding: 0.5rem; width: 120px; text-align: center; background: #f5f3ff; }
|
||||
.block-layer { border: 1px solid #ddd6fe; background: white; margin: 4px 0; padding: 4px; font-size: 0.7rem; border-radius: 4px; }
|
||||
.chat-sim { width: 100%; border: 1px solid #e2e8f0; border-radius: 8px; padding: 1rem; background: white; font-size: 0.8rem; }
|
||||
.msg { padding: 6px 10px; border-radius: 12px; margin-bottom: 0.5rem; max-width: 80%; }
|
||||
.msg.user { background: #eff6ff; margin-left: auto; color: #1e40af; }
|
||||
.msg.ai { background: #f0fdf4; margin-right: auto; color: #166534; }
|
||||
|
||||
/* Impact Card */
|
||||
.impact-card { text-align: center; }
|
||||
.impact-icon { font-size: 4rem; margin-bottom: 1rem; }
|
||||
.impact-title { font-size: 1.2rem; font-weight: bold; margin-bottom: 0.5rem; color: var(--vp-c-text-1); }
|
||||
.impact-desc { font-size: 0.9rem; color: var(--vp-c-text-2); line-height: 1.5; }
|
||||
|
||||
/* 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>
|
||||
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<div class="perceptron-demo">
|
||||
<div class="neuron-viz">
|
||||
<!-- Inputs -->
|
||||
<div class="inputs-col">
|
||||
<div class="input-node">
|
||||
<span class="label">Input 1 (x₁)</span>
|
||||
<input type="number" v-model="x1" class="val-input">
|
||||
</div>
|
||||
<div class="input-node">
|
||||
<span class="label">Input 2 (x₂)</span>
|
||||
<input type="number" v-model="x2" class="val-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Weights (Edges) -->
|
||||
<div class="weights-col">
|
||||
<div class="weight-line" :style="{ width: Math.abs(w1) * 2 + 2 + 'px', opacity: Math.abs(w1)/5 + 0.2 }"></div>
|
||||
<div class="weight-control top">
|
||||
w₁: <input type="range" v-model="w1" min="-5" max="5" step="0.1"> {{ w1 }}
|
||||
</div>
|
||||
|
||||
<div class="weight-line" :style="{ width: Math.abs(w2) * 2 + 2 + 'px', opacity: Math.abs(w2)/5 + 0.2 }"></div>
|
||||
<div class="weight-control bottom">
|
||||
w₂: <input type="range" v-model="w2" min="-5" max="5" step="0.1"> {{ w2 }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Neuron (Sum & Activation) -->
|
||||
<div class="neuron-body">
|
||||
<div class="sum-part">
|
||||
<div class="math">∑</div>
|
||||
<div class="val">{{ weightedSum.toFixed(1) }}</div>
|
||||
</div>
|
||||
<div class="bias-control">
|
||||
Bias: <input type="number" v-model="bias" class="bias-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Output -->
|
||||
<div class="output-col">
|
||||
<div class="arrow">➔</div>
|
||||
<div class="output-node" :class="{ active: output > 0 }">
|
||||
<span class="label">Output (y)</span>
|
||||
<div class="val">{{ output }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="formula-bar">
|
||||
Formula: <code>({{ x1 }} * {{ w1 }}) + ({{ x2 }} * {{ w2 }}) + {{ bias }} = {{ weightedSum.toFixed(1) }}</code>
|
||||
<br>
|
||||
Activation: <code>Step( {{ weightedSum.toFixed(1) }} ) = {{ output }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const x1 = ref(1)
|
||||
const x2 = ref(0)
|
||||
const w1 = ref(2.0)
|
||||
const w2 = ref(-1.0)
|
||||
const bias = ref(0)
|
||||
|
||||
const weightedSum = computed(() => {
|
||||
return (x1.value * w1.value) + (x2.value * w2.value) + bias.value
|
||||
})
|
||||
|
||||
const output = computed(() => {
|
||||
return weightedSum.value > 0 ? 1 : 0
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.perceptron-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.neuron-viz {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 500px;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.inputs-col, .output-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-node, .output-node {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #94a3b8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f1f5f9;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.output-node.active {
|
||||
background: #4ade80;
|
||||
border-color: #16a34a;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.6rem;
|
||||
position: absolute;
|
||||
top: -15px;
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.val-input {
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.weights-col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.weight-line {
|
||||
height: 2px;
|
||||
background: #475569;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform-origin: left center;
|
||||
}
|
||||
/* Simplified visual lines for CSS only demo - ideally SVG */
|
||||
/* This is a simplified representation */
|
||||
|
||||
.weight-control {
|
||||
font-size: 0.7rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: white;
|
||||
padding: 2px 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.neuron-body {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
box-shadow: 0 4px 10px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.sum-part {
|
||||
text-align: center;
|
||||
}
|
||||
.math { font-size: 1.5rem; }
|
||||
.val { font-weight: bold; }
|
||||
|
||||
.bias-control {
|
||||
position: absolute;
|
||||
bottom: -30px;
|
||||
background: white;
|
||||
color: #333;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
font-size: 0.7rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.bias-input {
|
||||
width: 30px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.formula-bar {
|
||||
margin-top: 2rem;
|
||||
background: #f8fafc;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
color: #334155;
|
||||
text-align: center;
|
||||
border: 1px dashed #cbd5e1;
|
||||
}
|
||||
|
||||
input[type=range] { width: 60px; }
|
||||
</style>
|
||||
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<div class="rule-learning-demo">
|
||||
<div class="demo-grid">
|
||||
<!-- Rule Based System -->
|
||||
<div class="panel rule-based">
|
||||
<div class="panel-header">
|
||||
<span class="icon">📜</span> Rule-Based System
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="code-block">
|
||||
if (size > <input v-model="ruleThreshold" type="number" class="mini-input">) {<br>
|
||||
return "Big 🍎"<br>
|
||||
} else {<br>
|
||||
return "Small 🍒"<br>
|
||||
}
|
||||
</div>
|
||||
<div class="test-area">
|
||||
Test Input: <input v-model="testInput" type="range" min="1" max="10" class="slider"> {{ testInput }}
|
||||
<div class="result-box" :class="ruleResult === 'Big 🍎' ? 'big' : 'small'">
|
||||
Result: {{ ruleResult }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="note">You must explicitly program the rule.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Machine Learning System -->
|
||||
<div class="panel learning">
|
||||
<div class="panel-header">
|
||||
<span class="icon">🧠</span> Machine Learning
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="training-data">
|
||||
<div class="data-point" v-for="(p, i) in trainingData" :key="i">
|
||||
{{ p.size }}={{ p.label }}
|
||||
</div>
|
||||
<button class="train-btn" @click="trainModel" :disabled="isTrained">
|
||||
{{ isTrained ? 'Model Trained ✅' : '⚡ Train Model' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="test-area">
|
||||
Test Input: <input v-model="testInput" type="range" min="1" max="10" class="slider"> {{ testInput }}
|
||||
<div class="result-box" :class="mlResult === 'Big 🍎' ? 'big' : 'small'">
|
||||
Result: {{ mlResult }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="note">
|
||||
Model "learned" threshold is ~{{ learnedThreshold }}. <br>
|
||||
(Derived from data, not coded)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const testInput = ref(5)
|
||||
|
||||
// Rule Based Logic
|
||||
const ruleThreshold = ref(6)
|
||||
const ruleResult = computed(() => {
|
||||
return testInput.value > ruleThreshold.value ? 'Big 🍎' : 'Small 🍒'
|
||||
})
|
||||
|
||||
// ML Logic
|
||||
const trainingData = [
|
||||
{ size: 2, label: '🍒' },
|
||||
{ size: 3, label: '🍒' },
|
||||
{ size: 8, label: '🍎' },
|
||||
{ size: 9, label: '🍎' }
|
||||
]
|
||||
const isTrained = ref(false)
|
||||
const learnedThreshold = ref(5.5) // Simplified mock learning
|
||||
|
||||
const trainModel = () => {
|
||||
// Simulate training delay
|
||||
setTimeout(() => {
|
||||
isTrained.value = true
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const mlResult = computed(() => {
|
||||
if (!isTrained.value) return '❓ Untrained'
|
||||
return testInput.value > learnedThreshold.value ? 'Big 🍎' : 'Small 🍒'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rule-learning-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;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 0.8rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #1e1e2e;
|
||||
color: #a6accd;
|
||||
padding: 0.8rem;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.mini-input {
|
||||
width: 40px;
|
||||
background: #334155;
|
||||
border: 1px solid #475569;
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.test-area {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.8rem;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 100%;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.result-box {
|
||||
margin-top: 0.5rem;
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.result-box.big { color: #ef4444; border-color: #fecaca; background: #fef2f2; }
|
||||
.result-box.small { color: #db2777; border-color: #fce7f3; background: #fdf2f8; }
|
||||
|
||||
.note {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.training-data {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.data-point {
|
||||
background: #e2e8f0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.train-btn {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.train-btn:disabled {
|
||||
background: #10b981;
|
||||
cursor: default;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,404 @@
|
||||
<template>
|
||||
<div class="backend-evolution-demo">
|
||||
<!-- 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>
|
||||
|
||||
<!-- Content -->
|
||||
<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">
|
||||
<!-- Architecture Diagram -->
|
||||
<div class="mac-window arch-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">Server Architecture</div>
|
||||
</div>
|
||||
<div class="arch-canvas">
|
||||
|
||||
<!-- Stage 0: CGI/Static -->
|
||||
<div v-if="currentStage === 0" class="arch-static">
|
||||
<div class="server-box">
|
||||
<div class="server-icon">🖥️ Physical Server</div>
|
||||
<div class="file-system">
|
||||
<div class="file">index.html</div>
|
||||
<div class="file">script.pl</div>
|
||||
<div class="file">image.jpg</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="request-arrow">
|
||||
<span>GET /index.html</span>
|
||||
<span class="arrow">➔</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage 1: Monolith -->
|
||||
<div v-if="currentStage === 1" class="arch-monolith">
|
||||
<div class="server-box big">
|
||||
<div class="server-icon">🦍 Monolithic App (Tomcat/Django)</div>
|
||||
<div class="modules-grid">
|
||||
<div class="module">User</div>
|
||||
<div class="module">Order</div>
|
||||
<div class="module">Payment</div>
|
||||
<div class="module">Product</div>
|
||||
</div>
|
||||
<div class="db-connection">
|
||||
<span>⬇ SQL</span>
|
||||
<div class="db-icon">🗄️ Single DB</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage 2: Microservices -->
|
||||
<div v-if="currentStage === 2" class="arch-micro">
|
||||
<div class="cloud-bg">
|
||||
<div class="service-mesh">
|
||||
<div class="service user">
|
||||
<span>User Svc</span>
|
||||
<small>Redis</small>
|
||||
</div>
|
||||
<div class="service order">
|
||||
<span>Order Svc</span>
|
||||
<small>MySQL</small>
|
||||
</div>
|
||||
<div class="service pay">
|
||||
<span>Pay Svc</span>
|
||||
<small>Postgres</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comm-lines">HTTP/gRPC</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage 3: Serverless -->
|
||||
<div v-if="currentStage === 3" class="arch-serverless">
|
||||
<div class="function-cloud">
|
||||
<div class="func-node">λ Login</div>
|
||||
<div class="func-node">λ Checkout</div>
|
||||
<div class="func-node">λ ResizeImg</div>
|
||||
</div>
|
||||
<div class="baas-layer">
|
||||
<span>BaaS (Auth0, Supabase, Stripe)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Deployment/Ops View -->
|
||||
<div class="mac-window ops-window">
|
||||
<div class="window-bar">
|
||||
<div class="window-title">Deployment & Ops</div>
|
||||
</div>
|
||||
<div class="ops-canvas">
|
||||
<div class="ops-card">
|
||||
<div class="ops-icon">{{ stages[currentStage].opsIcon }}</div>
|
||||
<div class="ops-title">{{ stages[currentStage].opsTitle }}</div>
|
||||
<div class="ops-desc">{{ stages[currentStage].opsDesc }}</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: 'CGI / Static',
|
||||
title: 'Physical Servers & Scripts',
|
||||
desc: 'In the beginning, servers were physical machines. We uploaded files via FTP. Backend logic was often simple Perl/CGI scripts executing one by one.',
|
||||
opsIcon: '🐌',
|
||||
opsTitle: 'Manual FTP Upload',
|
||||
opsDesc: 'Development was slow. "It works on my machine" was the common nightmare. Scaling meant buying a bigger physical computer.'
|
||||
},
|
||||
{
|
||||
year: '2000s',
|
||||
label: 'Monolith',
|
||||
title: 'The Monolithic Era',
|
||||
desc: 'Frameworks like Java Spring, Rails, Django appeared. All logic (User, Order, Pay) was packed into ONE giant process. Simple to develop, hard to scale.',
|
||||
opsIcon: '🐳',
|
||||
opsTitle: 'Virtual Machines (VM)',
|
||||
opsDesc: 'We started using VMs (AWS EC2). Scaling meant copying the entire giant application to multiple servers behind a Load Balancer.'
|
||||
},
|
||||
{
|
||||
year: '2014+',
|
||||
label: 'Microservices',
|
||||
title: 'Microservices & Containers',
|
||||
desc: 'Breaking the monolith! Each function (User, Order) became a separate tiny server. Docker changed the game by packaging dependencies together.',
|
||||
opsIcon: '☸️',
|
||||
opsTitle: 'Kubernetes (K8s)',
|
||||
opsDesc: 'Orchestrating thousands of containers. Complexity exploded, but teams could work independently and scale specific parts (e.g., just the Payment service).'
|
||||
},
|
||||
{
|
||||
year: '2020s+',
|
||||
label: 'Serverless',
|
||||
title: 'Serverless & Edge',
|
||||
desc: 'No more managing servers. You just write a function (e.g., "resize image") and upload it. The cloud provider runs it only when needed (Pay-per-use).',
|
||||
opsIcon: '⚡',
|
||||
opsTitle: 'GitOps & Edge',
|
||||
opsDesc: 'Push to Git -> Auto Deploy to global Edge nodes (Vercel, Cloudflare). Backend becomes "Functions + Managed Services (BaaS)".'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.backend-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 (Reused) */
|
||||
.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;
|
||||
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;
|
||||
}
|
||||
|
||||
.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.85rem;
|
||||
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: 0 auto 2rem;
|
||||
}
|
||||
|
||||
.header-section h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: linear-gradient(120deg, #f59e0b, #ea580c); /* Orange/Amber for Backend */
|
||||
-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; }
|
||||
|
||||
/* Visualizations */
|
||||
.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.05);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: white;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.mac-window:hover { transform: translateY(-5px); }
|
||||
|
||||
.arch-window { background: #f1f5f9; }
|
||||
.ops-window { background: white; }
|
||||
|
||||
.window-bar {
|
||||
padding: 0.8rem 1rem;
|
||||
background: white;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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: var(--vp-c-text-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.arch-canvas, .ops-canvas {
|
||||
padding: 2rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
/* Arch Styles */
|
||||
.server-box {
|
||||
background: #cbd5e1;
|
||||
border: 2px solid #94a3b8;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.file-system { margin-top: 1rem; background: white; padding: 0.5rem; border-radius: 4px; font-family: monospace; font-size: 0.8rem; }
|
||||
.request-arrow { margin-top: 1rem; display: flex; flex-direction: column; align-items: center; font-size: 0.8rem; color: #64748b; }
|
||||
|
||||
.server-box.big { background: #dbeafe; border-color: #3b82f6; width: 100%; max-width: 250px; }
|
||||
.modules-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; margin: 1rem 0; }
|
||||
.module { background: #bfdbfe; padding: 4px; border-radius: 4px; font-size: 0.8rem; color: #1e40af; }
|
||||
.db-connection { font-size: 0.8rem; display: flex; flex-direction: column; align-items: center; }
|
||||
|
||||
.cloud-bg { width: 100%; }
|
||||
.service-mesh { display: flex; gap: 1rem; justify-content: center; }
|
||||
.service { background: white; border: 1px solid #e2e8f0; padding: 0.8rem; border-radius: 6px; text-align: center; box-shadow: 0 4px 6px rgba(0,0,0,0.05); display: flex; flex-direction: column; }
|
||||
.service small { color: #64748b; font-size: 0.7rem; margin-top: 4px; }
|
||||
.comm-lines { margin-top: 1rem; font-size: 0.8rem; color: #94a3b8; text-align: center; border-top: 1px dashed #cbd5e1; width: 80%; padding-top: 4px; }
|
||||
|
||||
.function-cloud { display: flex; flex-wrap: wrap; gap: 1rem; justify-content: center; margin-bottom: 1.5rem; }
|
||||
.func-node { background: #fef3c7; border: 1px solid #f59e0b; color: #b45309; padding: 6px 12px; border-radius: 20px; font-family: monospace; font-size: 0.8rem; }
|
||||
.baas-layer { width: 100%; background: #e0e7ff; padding: 0.5rem; text-align: center; border-radius: 6px; font-size: 0.8rem; color: #4338ca; font-weight: bold; }
|
||||
|
||||
/* Ops Card */
|
||||
.ops-card { text-align: center; }
|
||||
.ops-icon { font-size: 4rem; margin-bottom: 1rem; }
|
||||
.ops-title { font-size: 1.2rem; font-weight: bold; margin-bottom: 0.5rem; color: var(--vp-c-text-1); }
|
||||
.ops-desc { font-size: 0.9rem; color: var(--vp-c-text-2); line-height: 1.5; }
|
||||
|
||||
/* 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>
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<div class="monolith-microservice-demo">
|
||||
<div class="controls">
|
||||
<button class="action-btn crash-btn" @click="triggerCrash">💥 Simulate Order Service Crash</button>
|
||||
<button class="action-btn reset-btn" @click="reset">🔄 Reset</button>
|
||||
</div>
|
||||
|
||||
<div class="comparison-view">
|
||||
<!-- Monolith -->
|
||||
<div class="architecture-block monolith">
|
||||
<div class="arch-header">Monolith Architecture</div>
|
||||
<div class="server-container" :class="{ crashed: monolithCrashed }">
|
||||
<div class="process-box">
|
||||
<div class="module user">User</div>
|
||||
<div class="module order" :class="{ error: monolithCrashed }">Order</div>
|
||||
<div class="module pay">Payment</div>
|
||||
</div>
|
||||
<div class="status-indicator">
|
||||
Status: {{ monolithCrashed ? 'SYSTEM DOWN (Critical Failure)' : 'Healthy' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="desc">
|
||||
One process. If "Order" module has a memory leak or fatal error, <strong>the entire server crashes</strong>. Everyone is affected.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Microservices -->
|
||||
<div class="architecture-block microservices">
|
||||
<div class="arch-header">Microservices Architecture</div>
|
||||
<div class="services-container">
|
||||
<div class="service-box user">
|
||||
<span>User Svc</span>
|
||||
<div class="dot green"></div>
|
||||
</div>
|
||||
<div class="service-box order" :class="{ crashed: microCrashed }">
|
||||
<span>Order Svc</span>
|
||||
<div class="dot" :class="microCrashed ? 'red' : 'green'"></div>
|
||||
</div>
|
||||
<div class="service-box pay">
|
||||
<span>Payment Svc</span>
|
||||
<div class="dot green"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-indicator">
|
||||
Status: {{ microCrashed ? 'Partial Outage (Order Down)' : 'Healthy' }}
|
||||
</div>
|
||||
<div class="desc">
|
||||
Isolated processes. If "Order" crashes, User and Payment services <strong>keep running</strong>. The system degrades gracefully.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const monolithCrashed = ref(false)
|
||||
const microCrashed = ref(false)
|
||||
|
||||
const triggerCrash = () => {
|
||||
monolithCrashed.value = true
|
||||
microCrashed.value = true
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
monolithCrashed.value = false
|
||||
microCrashed.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.monolith-microservice-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.crash-btn {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
.crash-btn:hover { background: #dc2626; }
|
||||
|
||||
.reset-btn {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.comparison-view {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.comparison-view { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.architecture-block {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.arch-header {
|
||||
font-weight: bold;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
/* Monolith Visuals */
|
||||
.server-container {
|
||||
border: 2px solid #3b82f6;
|
||||
background: #eff6ff;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.server-container.crashed {
|
||||
border-color: #ef4444;
|
||||
background: #fef2f2;
|
||||
animation: shake 0.5s;
|
||||
}
|
||||
|
||||
.process-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
background: white;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
border: 1px dashed #93c5fd;
|
||||
}
|
||||
|
||||
.module {
|
||||
background: #dbeafe;
|
||||
padding: 4px;
|
||||
border-radius: 2px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.module.error {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Microservices Visuals */
|
||||
.services-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.service-box {
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 0.8rem;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.service-box.crashed {
|
||||
border-color: #ef4444;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.dot.green { background: #22c55e; box-shadow: 0 0 4px #22c55e; }
|
||||
.dot.red { background: #ef4444; box-shadow: 0 0 4px #ef4444; }
|
||||
|
||||
.status-indicator {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-3);
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0% { transform: translate(1px, 1px) rotate(0deg); }
|
||||
10% { transform: translate(-1px, -2px) rotate(-1deg); }
|
||||
20% { transform: translate(-3px, 0px) rotate(1deg); }
|
||||
30% { transform: translate(3px, 2px) rotate(0deg); }
|
||||
40% { transform: translate(1px, -1px) rotate(1deg); }
|
||||
50% { transform: translate(-1px, 2px) rotate(-1deg); }
|
||||
60% { transform: translate(-3px, 1px) rotate(0deg); }
|
||||
70% { transform: translate(3px, 1px) rotate(-1deg); }
|
||||
80% { transform: translate(-1px, -1px) rotate(1deg); }
|
||||
90% { transform: translate(1px, 2px) rotate(0deg); }
|
||||
100% { transform: translate(1px, -2px) rotate(-1deg); }
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
+457
@@ -0,0 +1,457 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
|
||||
const activeTab = ref('elements')
|
||||
const selectedNode = ref('h1') // 'container', 'h1', 'button'
|
||||
|
||||
// Live State for the Virtual Page
|
||||
const liveStyles = reactive({
|
||||
container: {
|
||||
backgroundColor: '#f9f9f9',
|
||||
padding: '40px',
|
||||
textAlign: 'center',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.05)'
|
||||
},
|
||||
h1: {
|
||||
color: '#2c3e50',
|
||||
fontSize: '28px',
|
||||
fontWeight: '700',
|
||||
marginBottom: '16px',
|
||||
marginTop: '0px',
|
||||
fontFamily: 'sans-serif'
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#42b983',
|
||||
color: '#ffffff',
|
||||
borderRadius: '6px',
|
||||
padding: '10px 24px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600'
|
||||
}
|
||||
})
|
||||
|
||||
const liveContent = reactive({
|
||||
h1: 'Hello, Easy Vibe!',
|
||||
button: 'Click Me'
|
||||
})
|
||||
|
||||
// Presets Definition
|
||||
const stylePresets = {
|
||||
h1: [
|
||||
{ name: '默认样式 (Default)', style: { color: '#2c3e50', fontSize: '28px', fontFamily: 'sans-serif', fontWeight: '700' } },
|
||||
{ name: '活力红 (Vibrant Red)', style: { color: '#e74c3c', fontSize: '36px', fontFamily: 'serif', fontWeight: '800' } },
|
||||
{ name: '科技蓝 (Tech Blue)', style: { color: '#3498db', fontSize: '32px', fontFamily: 'monospace', fontWeight: '500' } },
|
||||
{ name: '优雅紫 (Elegant Purple)', style: { color: '#9b59b6', fontSize: '24px', fontFamily: 'cursive', fontWeight: '400' } }
|
||||
],
|
||||
button: [
|
||||
{ name: '默认样式 (Default)', style: { backgroundColor: '#42b983', color: '#ffffff', borderRadius: '6px', border: 'none' } },
|
||||
{ name: '警告风格 (Warning)', style: { backgroundColor: '#f1c40f', color: '#333333', borderRadius: '24px', border: '2px solid #e67e22' } },
|
||||
{ name: '幽灵按钮 (Ghost)', style: { backgroundColor: 'transparent', color: '#42b983', borderRadius: '6px', border: '1px solid #42b983' } },
|
||||
{ name: '深黑按钮 (Dark)', style: { backgroundColor: '#34495e', color: '#ecf0f1', borderRadius: '2px', border: '1px solid #2c3e50' } }
|
||||
],
|
||||
container: [
|
||||
{ name: '默认卡片 (Card)', style: { backgroundColor: '#f9f9f9', borderRadius: '12px', boxShadow: '0 4px 12px rgba(0,0,0,0.05)' } },
|
||||
{ name: '深色模式 (Dark)', style: { backgroundColor: '#2c3e50', borderRadius: '8px', boxShadow: '0 8px 16px rgba(0,0,0,0.2)' } },
|
||||
{ name: '极简白 (Minimal)', style: { backgroundColor: '#ffffff', borderRadius: '0px', boxShadow: 'none' } }
|
||||
]
|
||||
}
|
||||
|
||||
// Helper to get current styles for the selected node
|
||||
const currentStyles = computed(() => {
|
||||
return liveStyles[selectedNode.value] || {}
|
||||
})
|
||||
|
||||
// Helper for presets
|
||||
const availablePresets = computed(() => {
|
||||
return stylePresets[selectedNode.value] || []
|
||||
})
|
||||
|
||||
const applyPreset = (event) => {
|
||||
const presetName = event.target.value
|
||||
const preset = availablePresets.value.find(p => p.name === presetName)
|
||||
if (preset) {
|
||||
Object.assign(liveStyles[selectedNode.value], preset.style)
|
||||
}
|
||||
}
|
||||
|
||||
// Tabs definition
|
||||
const tabs = [
|
||||
{ id: 'elements', label: '元素' },
|
||||
{ id: 'console', label: '控制台' },
|
||||
{ id: 'sources', label: '源代码' },
|
||||
{ id: 'network', label: '网络' },
|
||||
{ id: 'application', label: '应用' }
|
||||
]
|
||||
|
||||
const selectNode = (node) => {
|
||||
selectedNode.value = node
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="live-demo-wrapper">
|
||||
<!-- Virtual Web Page Preview -->
|
||||
<div class="virtual-page-container">
|
||||
<div class="virtual-browser-bar">
|
||||
<div class="dots">
|
||||
<span class="dot red"></span>
|
||||
<span class="dot yellow"></span>
|
||||
<span class="dot green"></span>
|
||||
</div>
|
||||
<div class="address-bar">http://localhost:3000/demo</div>
|
||||
</div>
|
||||
<div class="virtual-page-content" :style="liveStyles.container" @click.self="selectNode('container')">
|
||||
<h1 :style="liveStyles.h1" @click.stop="selectNode('h1')">{{ liveContent.h1 }}</h1>
|
||||
<button :style="liveStyles.button" @click.stop="selectNode('button')">{{ liveContent.button }}</button>
|
||||
</div>
|
||||
<div class="instruction-overlay">
|
||||
👆 点击上方元素,下方 DevTools 实时联动
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DevTools Simulator -->
|
||||
<div class="browser-devtools-demo">
|
||||
<!-- Header -->
|
||||
<div class="devtools-header">
|
||||
<div class="header-left">
|
||||
<div class="icon-btn element-picker" title="选择页面中的元素以进行检查">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="#6e6e6e"><path d="M4 4h9v2H4V4zm0 4h5v2H4V8zm0 4h5v2H4v-2zm12-5l-4 4h3v4h2v-4h3l-4-4z"/></svg>
|
||||
</div>
|
||||
<div class="icon-btn device-toggle" title="切换设备工具栏">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="#6e6e6e"><path d="M17 1.01L7 1c-1.1 0-2 .9-2 2v18c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM17 19H7V5h10v14z"/></svg>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<div class="tabs">
|
||||
<div v-for="tab in tabs" :key="tab.id" class="tab" :class="{ active: activeTab === tab.id }" @click="activeTab = tab.id">
|
||||
{{ tab.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="icon-btn settings">⚙️</div>
|
||||
<div class="icon-btn close">×</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="devtools-body">
|
||||
|
||||
<!-- Elements Panel -->
|
||||
<div v-if="activeTab === 'elements'" class="panel elements-panel">
|
||||
<div class="dom-tree-panel">
|
||||
<div class="dom-tree-content">
|
||||
<div class="dom-node" @click="selectNode('container')" :class="{ selected: selectedNode === 'container' }">
|
||||
<div class="line-content">
|
||||
<span class="arrow expanded">▼</span>
|
||||
<span class="tag-name">div</span>
|
||||
<span class="attr-name">class</span>=<span class="attr-val">"virtual-page-content"</span>
|
||||
<span class="node-trail" v-if="selectedNode === 'container'">== $0</span>
|
||||
</div>
|
||||
<div class="children">
|
||||
<div class="dom-node" @click.stop="selectNode('h1')" :class="{ selected: selectedNode === 'h1' }">
|
||||
<div class="line-content">
|
||||
<span class="indent"></span>
|
||||
<span class="tag-name">h1</span>
|
||||
<span class="node-trail" v-if="selectedNode === 'h1'">== $0</span>
|
||||
</div>
|
||||
<div class="line-content">
|
||||
<span class="indent"></span>
|
||||
<input v-model="liveContent.h1" class="dom-text-input" @click.stop="selectNode('h1')" />
|
||||
</div>
|
||||
<div class="line-content"><span class="indent"></span><span class="tag-name">/h1</span></div>
|
||||
</div>
|
||||
<div class="dom-node" @click.stop="selectNode('button')" :class="{ selected: selectedNode === 'button' }">
|
||||
<div class="line-content">
|
||||
<span class="indent"></span>
|
||||
<span class="tag-name">button</span>
|
||||
<span class="node-trail" v-if="selectedNode === 'button'">== $0</span>
|
||||
</div>
|
||||
<div class="line-content">
|
||||
<span class="indent"></span>
|
||||
<input v-model="liveContent.button" class="dom-text-input" @click.stop="selectNode('button')" />
|
||||
</div>
|
||||
<div class="line-content"><span class="indent"></span><span class="tag-name">/button</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line-content"><span class="tag-name">/div</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="breadcrumbs">
|
||||
html > body > div.virtual-page-content {{ selectedNode !== 'container' ? '> ' + selectedNode : '' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interactive Styles Panel -->
|
||||
<div class="styles-panel">
|
||||
<div class="styles-tabs">
|
||||
<div class="style-tab active">样式 (Styles)</div>
|
||||
<div class="style-tab">计算 (Computed)</div>
|
||||
</div>
|
||||
<div class="styles-content">
|
||||
<!-- Preset Selector -->
|
||||
<div class="style-section" v-if="availablePresets.length > 0">
|
||||
<div class="style-section-title">✨ 快速预设 (Presets)</div>
|
||||
<select class="preset-select" @change="applyPreset">
|
||||
<option value="" disabled selected>选择一种风格 (Select Preset)...</option>
|
||||
<option v-for="preset in availablePresets" :key="preset.name" :value="preset.name">
|
||||
{{ preset.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- CSS Properties -->
|
||||
<div class="style-rule">
|
||||
<div class="selector">element.style {</div>
|
||||
<div class="property" v-for="(val, key) in currentStyles" :key="key">
|
||||
<span class="prop-name">{{ key }}</span>:
|
||||
<input v-model="liveStyles[selectedNode][key]" class="style-input" />
|
||||
;
|
||||
</div>
|
||||
<div class="selector">}</div>
|
||||
</div>
|
||||
<div class="style-add-hint"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Other Panels (Simplified placeholders) -->
|
||||
<div v-else class="panel placeholder-panel">
|
||||
<div class="placeholder-text">此演示主要展示 Elements 面板的实时编辑功能。请切换回 "元素" 面板。</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.live-demo-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Virtual Page Preview */
|
||||
.virtual-page-container {
|
||||
border: 1px solid #d0d7de;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.virtual-browser-bar {
|
||||
background: #f1f3f4;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dots { display: flex; gap: 8px; }
|
||||
.dot { width: 12px; height: 12px; border-radius: 50%; border: 1px solid rgba(0,0,0,0.1); }
|
||||
.dot.red { background: #ff5f56; }
|
||||
.dot.yellow { background: #ffbd2e; }
|
||||
.dot.green { background: #27c93f; }
|
||||
|
||||
.address-bar {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 4px 12px;
|
||||
font-size: 12px;
|
||||
color: #5f6368;
|
||||
border: 1px solid #e0e0e0;
|
||||
text-align: center;
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
.virtual-page-content {
|
||||
min-height: 180px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
background-image: radial-gradient(#e1e1e1 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
.instruction-overlay {
|
||||
background: #e8f0fe;
|
||||
color: #1a73e8;
|
||||
font-size: 12px;
|
||||
padding: 6px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #d2e3fc;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* DevTools Simulator (Enhanced) */
|
||||
.browser-devtools-demo {
|
||||
border: 1px solid #d0d7de;
|
||||
border-radius: 8px;
|
||||
background-color: #ffffff;
|
||||
color: #202124;
|
||||
font-family: 'Segoe UI', '.SFNSDisplay', 'Roboto', sans-serif;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 320px;
|
||||
position: relative;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Header & Tabs */
|
||||
.devtools-header {
|
||||
background-color: #f3f3f3;
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 28px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.header-left, .header-right { display: flex; align-items: center; height: 100%; }
|
||||
.icon-btn {
|
||||
padding: 0 8px;
|
||||
cursor: pointer;
|
||||
color: #6e6e6e;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.icon-btn:hover { color: #202124; background-color: #eaeaea; }
|
||||
.separator { width: 1px; height: 16px; background-color: #ccc; margin: 0 8px; }
|
||||
|
||||
.tabs { display: flex; height: 100%; overflow-x: auto; }
|
||||
.tab {
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #5f6368;
|
||||
border-bottom: 2px solid transparent;
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tab:hover { background-color: #e8eaed; color: #202124; }
|
||||
.tab.active { color: #1a73e8; border-bottom: 2px solid #1a73e8; font-weight: 500; }
|
||||
|
||||
.devtools-body { flex: 1; display: flex; overflow: hidden; background-color: #fff; }
|
||||
.panel { flex: 1; display: flex; overflow: hidden; width: 100%; }
|
||||
|
||||
/* Elements Panel */
|
||||
.elements-panel { display: flex; flex-direction: row; }
|
||||
.dom-tree-panel {
|
||||
flex: 3;
|
||||
border-right: 1px solid #d0d7de;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
padding: 6px 0;
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
.dom-node {
|
||||
padding-left: 14px;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dom-node.selected { background-color: #cfe8fc; }
|
||||
.dom-node:hover:not(.selected) { background-color: #f0f4f8; }
|
||||
|
||||
.line-content { display: flex; align-items: center; }
|
||||
.node-trail { color: #5f6368; margin-left: 6px; }
|
||||
.arrow { color: #5f6368; font-size: 10px; display: inline-block; width: 14px; margin-left: -14px; text-align: center; }
|
||||
.tag-name { color: #a90d91; }
|
||||
.attr-name { color: #994500; margin-left: 4px; }
|
||||
.attr-val { color: #1a1aa6; }
|
||||
.text-node { color: #222; }
|
||||
.indent { display: inline-block; width: 14px; }
|
||||
|
||||
.breadcrumbs {
|
||||
border-top: 1px solid #ccc;
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
color: #5f6368;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.styles-panel { flex: 2; display: flex; flex-direction: column; background: #fff; border-left: 1px solid #d0d7de; min-width: 280px; }
|
||||
.styles-tabs { display: flex; background: #f1f3f4; border-bottom: 1px solid #ccc; height: 26px; }
|
||||
.style-tab { padding: 0 12px; display: flex; align-items: center; color: #5f6368; cursor: pointer; border-bottom: 2px solid transparent; font-size: 11px; }
|
||||
.style-tab:hover { background-color: #e8eaed; color: #202124; }
|
||||
.style-tab.active { color: #1a73e8; border-bottom: 2px solid #1a73e8; font-weight: 500; }
|
||||
.styles-content { padding: 0; overflow: auto; background: #fff; flex: 1; }
|
||||
|
||||
.style-section { padding: 8px 12px; border-bottom: 1px solid #eee; background: #fafafa; }
|
||||
.style-section-title { font-weight: 600; color: #5f6368; margin-bottom: 6px; font-size: 11px; text-transform: uppercase; }
|
||||
.preset-select { width: 100%; padding: 4px; border: 1px solid #ccc; border-radius: 4px; font-size: 11px; }
|
||||
|
||||
.content-editor-row { font-family: Consolas, Menlo, monospace; font-size: 11px; display: flex; align-items: center; }
|
||||
.content-input {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
padding: 2px 4px;
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
color: #222;
|
||||
flex: 1;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.content-input:focus { border-color: #1a73e8; outline: none; }
|
||||
|
||||
.style-rule { margin-bottom: 8px; font-family: Consolas, Menlo, monospace; font-size: 11px; border-bottom: 1px solid #eee; padding: 8px 12px; }
|
||||
.selector { color: #a90d91; font-weight: bold; }
|
||||
.property { padding-left: 16px; display: flex; align-items: center; margin-bottom: 2px; line-height: 1.6; }
|
||||
.prop-name { color: #c80000; margin-right: 4px; }
|
||||
/* CSS Properties Input Styling */
|
||||
.style-input {
|
||||
border: 1px solid transparent;
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
font-size: 11px;
|
||||
color: #1a1aa6;
|
||||
width: 140px;
|
||||
background: transparent;
|
||||
padding: 0 2px;
|
||||
margin-left: -2px;
|
||||
}
|
||||
.style-input:hover { border: 1px solid #ccc; background: #fff; }
|
||||
.style-input:focus { outline: none; border: 1px solid #ccc; background: #fff; box-shadow: 0 0 0 1px #e0e0e0; }
|
||||
|
||||
/* DOM Text Input Styling */
|
||||
.dom-text-input {
|
||||
border: 1px solid transparent;
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
color: #222;
|
||||
background: transparent;
|
||||
padding: 0 2px;
|
||||
margin-left: -2px;
|
||||
width: 200px; /* Give it enough space */
|
||||
}
|
||||
.dom-text-input:hover { border: 1px solid #ccc; background: #fff; }
|
||||
.dom-text-input:focus { outline: none; border: 1px solid #ccc; background: #fff; box-shadow: 0 0 0 1px #e0e0e0; }
|
||||
|
||||
.placeholder-panel {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,204 +1,442 @@
|
||||
<template>
|
||||
<div class="three-areas-demo">
|
||||
<div class="panel">
|
||||
<div class="areas-container">
|
||||
<div class="area working">
|
||||
<div class="area-header">
|
||||
<span class="area-icon">💻</span>
|
||||
<span class="area-name">工作区</span>
|
||||
<span class="area-count">{{ workingFiles.length }}</span>
|
||||
<div class="scene">
|
||||
<!-- 1. Working Directory (Desk) -->
|
||||
<div class="zone working">
|
||||
<div class="zone-header">
|
||||
<span class="zone-icon">💻</span>
|
||||
<div class="zone-info">
|
||||
<span class="zone-title">工作区 (Desk)</span>
|
||||
<span class="zone-desc">你的书桌,随便乱放</span>
|
||||
</div>
|
||||
<div class="file-list">
|
||||
<div v-for="file in workingFiles" :key="file.name" class="file-item">
|
||||
<span class="file-icon">📄</span>
|
||||
<span class="file-name">{{ file.name }}</span>
|
||||
<button @click="addToStaging(file)" class="mini-btn">+</button>
|
||||
</div>
|
||||
<div v-if="workingFiles.length === 0" class="empty">无文件</div>
|
||||
<div class="desk-surface">
|
||||
<transition-group name="file-pop">
|
||||
<div
|
||||
v-for="file in workingFiles"
|
||||
:key="file.id"
|
||||
class="file-card"
|
||||
@click="addToStaging(file)"
|
||||
>
|
||||
<div class="file-icon">{{ file.icon }}</div>
|
||||
<div class="file-name">{{ file.name }}</div>
|
||||
<div class="action-hint">Add +</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
<div v-if="workingFiles.length === 0" class="empty-state">
|
||||
桌上很干净 ✨
|
||||
<button class="create-btn" @click="createNewFile">新建文件 📝</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="area staging">
|
||||
<div class="area-header">
|
||||
<span class="area-icon">📋</span>
|
||||
<span class="area-name">暂存区</span>
|
||||
<span class="area-count">{{ stagedFiles.length }}</span>
|
||||
<!-- Arrow -->
|
||||
<div class="flow-arrow">
|
||||
<div class="arrow-line"></div>
|
||||
<div class="arrow-label">git add</div>
|
||||
<div class="arrow-head">▶</div>
|
||||
</div>
|
||||
<div class="file-list">
|
||||
<div v-for="file in stagedFiles" :key="file.name" class="file-item">
|
||||
<span class="file-icon">📄</span>
|
||||
<span class="file-name">{{ file.name }}</span>
|
||||
<button @click="commitFile(file)" class="mini-btn">✓</button>
|
||||
|
||||
<!-- 2. Staging Area (Box) -->
|
||||
<div class="zone staging">
|
||||
<div class="zone-header">
|
||||
<span class="zone-icon">📦</span>
|
||||
<div class="zone-info">
|
||||
<span class="zone-title">暂存区 (Box)</span>
|
||||
<span class="zone-desc">快递盒,准备打包</span>
|
||||
</div>
|
||||
<div v-if="stagedFiles.length === 0" class="empty">无文件</div>
|
||||
</div>
|
||||
<div class="box-container">
|
||||
<div class="box-body">
|
||||
<transition-group name="file-drop">
|
||||
<div
|
||||
v-for="file in stagedFiles"
|
||||
:key="file.id"
|
||||
class="file-card mini"
|
||||
@click="unstageFile(file)"
|
||||
>
|
||||
<div class="file-icon">{{ file.icon }}</div>
|
||||
<div class="file-name">{{ file.name }}</div>
|
||||
<div class="action-hint">Remove -</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
<div v-if="stagedFiles.length === 0" class="empty-state box-empty">
|
||||
盒子是空的 🕸️
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-flap left"></div>
|
||||
<div class="box-flap right"></div>
|
||||
</div>
|
||||
<div class="staging-actions">
|
||||
<button
|
||||
class="commit-btn"
|
||||
:disabled="stagedFiles.length === 0"
|
||||
@click="commitFiles"
|
||||
>
|
||||
封箱寄出 (git commit) 🚚
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="area repo">
|
||||
<div class="area-header">
|
||||
<span class="area-icon">📦</span>
|
||||
<span class="area-name">仓库</span>
|
||||
<span class="area-count">{{ commits.length }}</span>
|
||||
<!-- Arrow -->
|
||||
<div class="flow-arrow">
|
||||
<div class="arrow-line"></div>
|
||||
<div class="arrow-label">git commit</div>
|
||||
<div class="arrow-head">▶</div>
|
||||
</div>
|
||||
<div class="commit-list">
|
||||
<div v-for="commit in commits.slice(-3).reverse()" :key="commit.hash" class="commit-item">
|
||||
<span class="commit-hash">{{ commit.hash.substring(0, 6) }}</span>
|
||||
|
||||
<!-- 3. Repository (Cabinet) -->
|
||||
<div class="zone repo">
|
||||
<div class="zone-header">
|
||||
<span class="zone-icon">🗄️</span>
|
||||
<div class="zone-info">
|
||||
<span class="zone-title">仓库 (Cabinet)</span>
|
||||
<span class="zone-desc">档案柜,永久保存</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cabinet-body">
|
||||
<transition-group name="drawer-slide">
|
||||
<div
|
||||
v-for="commit in commits.slice().reverse()"
|
||||
:key="commit.hash"
|
||||
class="drawer-item"
|
||||
>
|
||||
<div class="drawer-handle"></div>
|
||||
<div class="commit-info">
|
||||
<span class="commit-hash">#{{ commit.hash }}</span>
|
||||
<span class="commit-msg">{{ commit.message }}</span>
|
||||
</div>
|
||||
<div v-if="commits.length === 0" class="empty">无提交</div>
|
||||
<div class="commit-files">
|
||||
<span v-for="f in commit.files" :key="f" class="tiny-file">📄</span>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
<div v-if="commits.length === 0" class="empty-state">
|
||||
柜子是空的 💨
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p><strong>💡 三区工作流:</strong> 工作区修改 → 添加到暂存区 → 提交到仓库</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const fileIdCounter = ref(1)
|
||||
|
||||
const createId = () => `file-${fileIdCounter.value++}`
|
||||
|
||||
const workingFiles = ref([
|
||||
{ name: 'index.vue' },
|
||||
{ name: 'style.css' }
|
||||
{ id: createId(), name: 'essay.txt', icon: '📝' },
|
||||
{ id: createId(), name: 'photo.jpg', icon: '🖼️' },
|
||||
{ id: createId(), name: 'style.css', icon: '🎨' }
|
||||
])
|
||||
|
||||
const stagedFiles = ref([])
|
||||
const commits = ref([])
|
||||
|
||||
const createNewFile = () => {
|
||||
const types = [
|
||||
{ name: 'script.js', icon: '📜' },
|
||||
{ name: 'data.json', icon: '📊' },
|
||||
{ name: 'readme.md', icon: '📘' }
|
||||
]
|
||||
const randomType = types[Math.floor(Math.random() * types.length)]
|
||||
workingFiles.value.push({
|
||||
id: createId(),
|
||||
name: randomType.name,
|
||||
icon: randomType.icon
|
||||
})
|
||||
}
|
||||
|
||||
const addToStaging = (file) => {
|
||||
const index = workingFiles.value.findIndex(f => f.name === file.name)
|
||||
const index = workingFiles.value.findIndex(f => f.id === file.id)
|
||||
if (index !== -1) {
|
||||
workingFiles.value.splice(index, 1)
|
||||
stagedFiles.value.push(file)
|
||||
}
|
||||
}
|
||||
|
||||
const commitFile = (file) => {
|
||||
const index = stagedFiles.value.findIndex(f => f.name === file.name)
|
||||
const unstageFile = (file) => {
|
||||
const index = stagedFiles.value.findIndex(f => f.id === file.id)
|
||||
if (index !== -1) {
|
||||
stagedFiles.value.splice(index, 1)
|
||||
commits.value.push({
|
||||
hash: Math.random().toString(16).substr(2, 7),
|
||||
message: file.name
|
||||
})
|
||||
workingFiles.value.push(file)
|
||||
}
|
||||
}
|
||||
|
||||
const commitFiles = () => {
|
||||
if (stagedFiles.value.length === 0) return
|
||||
|
||||
const files = [...stagedFiles.value]
|
||||
stagedFiles.value = []
|
||||
|
||||
const msgs = ['Fix bug', 'Add feature', 'Update docs', 'Refactor code', 'Initial commit']
|
||||
const randomMsg = msgs[Math.floor(Math.random() * msgs.length)]
|
||||
|
||||
commits.value.push({
|
||||
hash: Math.random().toString(16).substr(2, 6),
|
||||
message: randomMsg,
|
||||
files: files.map(f => f.name)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.three-areas-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.areas-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.area {
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
border-radius: 12px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
margin: 1rem 0;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.area.working { border-color: #f59e0b; }
|
||||
.area.staging { border-color: #3b82f6; }
|
||||
.area.repo { border-color: #10b981; }
|
||||
|
||||
.area-header {
|
||||
.scene {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
.area-icon { font-size: 1.25rem; }
|
||||
.area-name { flex: 1; font-weight: 600; }
|
||||
.area-count {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.125rem 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.file-list, .commit-list {
|
||||
/* Common Zone Styles */
|
||||
.zone {
|
||||
flex: 1;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
border: 2px solid transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
min-height: 80px;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.file-item, .commit-item {
|
||||
.zone-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px dashed var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.file-icon { font-size: 1rem; }
|
||||
.file-name { flex: 1; font-size: 0.875rem; }
|
||||
.zone-icon { font-size: 1.5rem; }
|
||||
.zone-info { display: flex; flex-direction: column; }
|
||||
.zone-title { font-weight: bold; font-size: 0.9rem; }
|
||||
.zone-desc { font-size: 0.7rem; color: var(--vp-c-text-2); }
|
||||
|
||||
.mini-btn {
|
||||
padding: 0.125rem 0.5rem;
|
||||
border: 1px solid var(--vp-c-brand);
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-brand);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.mini-btn:hover { background: var(--vp-c-brand); color: var(--vp-c-bg); }
|
||||
|
||||
.commit-hash {
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.commit-msg {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.empty {
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-3);
|
||||
font-style: italic;
|
||||
font-size: 0.875rem;
|
||||
padding: 1rem;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-left: 4px solid var(--vp-c-brand);
|
||||
/* 1. Working Desk */
|
||||
.zone.working { border-color: #f59e0b; background: #fffbeb; }
|
||||
.desk-surface {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
background-image: radial-gradient(#e5e7eb 1px, transparent 1px);
|
||||
background-size: 10px 10px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.file-card {
|
||||
background: white;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
width: 80px;
|
||||
height: 90px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-card:hover {
|
||||
transform: translateY(-4px) rotate(2deg);
|
||||
box-shadow: 0 8px 12px rgba(0,0,0,0.1);
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
|
||||
.file-icon { font-size: 2rem; margin-bottom: 4px; }
|
||||
.file-name { font-size: 0.7rem; text-align: center; word-break: break-all; line-height: 1.2; }
|
||||
.action-hint {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(245, 158, 11, 0.9);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
font-weight: bold;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.file-card:hover .action-hint { opacity: 1; }
|
||||
|
||||
.create-btn {
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* 2. Staging Box */
|
||||
.zone.staging { border-color: #3b82f6; background: #eff6ff; }
|
||||
.box-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.box-body {
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
background: #dbeafe;
|
||||
border: 2px solid #3b82f6;
|
||||
border-top: none;
|
||||
border-radius: 0 0 8px 8px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
padding: 8px;
|
||||
gap: 4px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.file-card.mini {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
padding: 4px 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
.file-card.mini .file-icon { font-size: 1rem; margin: 0; }
|
||||
.file-card.mini .file-name { font-size: 0.8rem; }
|
||||
.file-card.mini:hover { border-color: #ef4444; }
|
||||
.file-card.mini .action-hint { background: rgba(239, 68, 68, 0.9); }
|
||||
|
||||
.box-flap {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
width: 45%;
|
||||
height: 20px;
|
||||
background: #93c5fd;
|
||||
border: 2px solid #3b82f6;
|
||||
border-bottom: none;
|
||||
transition: all 0.5s;
|
||||
}
|
||||
.box-flap.left { left: 0; border-radius: 4px 0 0 0; transform-origin: bottom left; transform: rotate(10deg); }
|
||||
.box-flap.right { right: 0; border-radius: 0 4px 0 0; transform-origin: bottom right; transform: rotate(-10deg); }
|
||||
|
||||
.staging-actions { margin-top: 12px; text-align: center; }
|
||||
.commit-btn {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
.commit-btn:disabled { background: #93c5fd; cursor: not-allowed; box-shadow: none; }
|
||||
.commit-btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 6px 8px rgba(59, 130, 246, 0.4); }
|
||||
|
||||
/* 3. Repo Cabinet */
|
||||
.zone.repo { border-color: #10b981; background: #ecfdf5; }
|
||||
.cabinet-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.drawer-item {
|
||||
background: white;
|
||||
border: 1px solid #10b981;
|
||||
border-radius: 4px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
gap: 12px;
|
||||
box-shadow: 0 2px 0 #059669; /* 3D effect */
|
||||
position: relative;
|
||||
}
|
||||
.drawer-handle {
|
||||
width: 30px;
|
||||
height: 6px;
|
||||
background: #d1fae5;
|
||||
border: 1px solid #10b981;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.commit-info { flex: 1; display: flex; flex-direction: column; }
|
||||
.commit-hash { font-size: 0.6rem; color: #10b981; font-family: monospace; }
|
||||
.commit-msg { font-size: 0.8rem; font-weight: bold; }
|
||||
.commit-files { display: flex; gap: 2px; }
|
||||
.tiny-file { font-size: 0.6rem; }
|
||||
|
||||
.info-box p {
|
||||
margin: 0;
|
||||
color: var(--vp-c-text-1);
|
||||
line-height: 1.6;
|
||||
/* Arrows */
|
||||
.flow-arrow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
.arrow-line { width: 100%; height: 2px; background: currentColor; }
|
||||
.arrow-label { font-size: 0.7rem; margin: 4px 0; font-weight: bold; white-space: nowrap; }
|
||||
.arrow-head { font-size: 0.8rem; }
|
||||
|
||||
/* Transitions */
|
||||
.file-pop-enter-active, .file-pop-leave-active { transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
|
||||
.file-pop-enter-from { opacity: 0; transform: scale(0.5); }
|
||||
.file-pop-leave-to { opacity: 0; transform: scale(0); }
|
||||
|
||||
.file-drop-enter-active, .file-drop-leave-active { transition: all 0.3s ease; }
|
||||
.file-drop-enter-from { opacity: 0; transform: translateY(-20px); }
|
||||
.file-drop-leave-to { opacity: 0; transform: translateX(20px); }
|
||||
|
||||
.drawer-slide-enter-active { transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
|
||||
.drawer-slide-enter-from { opacity: 0; transform: translateX(50px); }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.areas-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.scene { flex-direction: column; min-width: auto; }
|
||||
.flow-arrow { transform: rotate(90deg); margin: 10px 0; width: 100%; align-items: center; }
|
||||
.arrow-line { width: 2px; height: 20px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,707 @@
|
||||
<template>
|
||||
<div class="ai-help-demo">
|
||||
<div class="desktop-container">
|
||||
<!-- 1. VS Code 窗口 (全功能模拟) -->
|
||||
<div class="window vscode" :class="getWindowClass('vscode')">
|
||||
<!-- 标题栏 -->
|
||||
<div class="title-bar">
|
||||
<div class="controls">
|
||||
<span class="dot red"></span>
|
||||
<span class="dot yellow"></span>
|
||||
<span class="dot green"></span>
|
||||
</div>
|
||||
<div class="title-text">App.vue - easy-vibe - Visual Studio Code</div>
|
||||
</div>
|
||||
|
||||
<div class="main-layout">
|
||||
<!-- 侧边栏 (Activity Bar) -->
|
||||
<div class="activity-bar">
|
||||
<div class="icon active">📁</div>
|
||||
<div class="icon">🔍</div>
|
||||
<div class="icon">🌿</div>
|
||||
<div class="icon">🐛</div>
|
||||
<div class="icon">🧩</div>
|
||||
</div>
|
||||
|
||||
<!-- 资源管理器 (Sidebar) -->
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-title">EXPLORER</div>
|
||||
<div class="file-tree">
|
||||
<div class="tree-item expanded">
|
||||
<span class="arrow">▼</span> src
|
||||
</div>
|
||||
<div class="tree-item indent">
|
||||
<span class="icon">📄</span> main.js
|
||||
</div>
|
||||
<div class="tree-item indent active">
|
||||
<span class="icon">V</span> App.vue
|
||||
</div>
|
||||
<div class="tree-item indent">
|
||||
<span class="icon">🎨</span> style.css
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑器区域 -->
|
||||
<div class="editor-area">
|
||||
<div class="tab-bar">
|
||||
<div class="tab active">
|
||||
<span class="icon">V</span> App.vue <span class="close">×</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-content">
|
||||
<div class="line-numbers">
|
||||
<div v-for="n in 15" :key="n">{{ n }}</div>
|
||||
</div>
|
||||
<div class="code-lines">
|
||||
<div class="line"><span class="kwd">import</span> { <span class="var">ref</span>, <span class="var">onMounted</span>, <span class="var">nextTick</span> } <span class="kwd">from</span> <span class="str">'vue'</span></div>
|
||||
<div class="line"></div>
|
||||
<div class="line"><span class="kwd">const</span> <span class="var">chartRef</span> = <span class="func">ref</span>(<span class="kwd">null</span>)</div>
|
||||
<div class="line"><span class="kwd">const</span> <span class="var">data</span> = <span class="func">ref</span>([])</div>
|
||||
<div class="line"></div>
|
||||
<div class="line"><span class="kwd">const</span> <span class="func">initChart</span> = <span class="kwd">async</span> () => {</div>
|
||||
<div class="line"> <span class="comment">// 等待数据加载完成</span></div>
|
||||
<div class="line"> <span class="kwd">await</span> <span class="func">fetchData</span>()</div>
|
||||
<div class="line"> </div>
|
||||
<div class="line" ref="targetCode">
|
||||
<span class="comment">// 👈 等待 DOM 更新后再渲染图表</span>
|
||||
</div>
|
||||
<div class="line" ref="targetCode2">
|
||||
<span class="kwd">await</span> <span class="func">nextTick</span>()
|
||||
</div>
|
||||
<div class="line"> </div>
|
||||
<div class="line"> <span class="kwd">const</span> <span class="var">chart</span> = <span class="var">echarts</span>.<span class="func">init</span>(<span class="var">chartRef</span>.<span class="var">value</span>)</div>
|
||||
<div class="line"> <span class="var">chart</span>.<span class="func">setOption</span>({ ... })</div>
|
||||
<div class="line">}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 截图选框 (Overlay) - Moved to main-layout level -->
|
||||
<div class="screenshot-overlay" v-if="step === 'selecting' || step === 'captured'">
|
||||
<div class="selection-box" :class="{ flashed: step === 'captured' }">
|
||||
<div class="selection-handle top-left"></div>
|
||||
<div class="selection-handle top-right"></div>
|
||||
<div class="selection-handle bottom-left"></div>
|
||||
<div class="selection-handle bottom-right"></div>
|
||||
<div class="cursor-crosshair" v-if="step === 'selecting'"></div>
|
||||
<div class="selection-size" v-if="step === 'selecting'">220 × 350</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模拟操作引导 -->
|
||||
<div class="guide-overlay" v-if="step === 'idle'">
|
||||
<div class="start-btn" @click="startDemo">
|
||||
<span>📸 演示:遇到代码不懂怎么问 AI?</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. ChatGPT 窗口 -->
|
||||
<div class="window chatgpt" :class="getWindowClass('chatgpt')">
|
||||
<div class="chat-sidebar">
|
||||
<div class="new-chat">+ New chat</div>
|
||||
<div class="history-item">Previous 7 Days</div>
|
||||
<div class="history-item active">Vue nextTick explanation</div>
|
||||
<div class="history-item">CSS Grid layout</div>
|
||||
</div>
|
||||
<div class="chat-main">
|
||||
<div class="chat-model-selector">
|
||||
<span>GPT-4o</span> <span class="arrow">▼</span>
|
||||
</div>
|
||||
|
||||
<div class="messages-container" ref="messagesContainer">
|
||||
<div class="msg-row user" v-if="stepInt >= 5">
|
||||
<div class="avatar">U</div>
|
||||
<div class="msg-bubble">
|
||||
<div class="pasted-image" v-if="stepInt >= 5">
|
||||
<div class="ui-snapshot">
|
||||
<div class="snapshot-rect menu-rect"></div>
|
||||
<div class="snapshot-text">Menu Bar.png</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="msg-text">{{ typedText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="msg-row ai" v-if="stepInt >= 7">
|
||||
<div class="avatar gpt">
|
||||
<svg viewBox="0 0 41 41" class="gpt-logo"><path d="M37.532 16.87a9.963 9.963 0 00-.856-8.184c-3.15-5.49-10.25-7.38-15.738-4.23-.718.412-1.35.914-1.896 1.488a9.965 9.965 0 00-7.144-1.156 9.972 9.972 0 00-6.73 4.966c-3.15 5.49-1.26 12.59 4.23 15.738.412.237.854.43 1.306.586a9.963 9.963 0 00.856 8.184c3.15 5.49 10.25 7.38 15.738 4.23.718-.412 1.35-.914 1.896-1.488a9.965 9.965 0 007.144 1.156 9.972 9.972 0 006.73-4.966c3.15-5.49 1.26-12.59-4.23-15.738a9.953 9.953 0 00-1.306-.586zM20.5 29.5a9 9 0 110-18 9 9 0 010 18z" fill="currentColor"></path></svg>
|
||||
</div>
|
||||
<div class="msg-bubble ai-bubble">
|
||||
<p>这是 VS Code 的 <strong>顶部菜单栏 (Menu Bar)</strong>,包含了软件的所有功能入口。</p>
|
||||
<p><strong>常用菜单解释:</strong></p>
|
||||
<ul>
|
||||
<li><strong>File (文件)</strong>:新建、打开、保存文件或项目。</li>
|
||||
<li><strong>Edit (编辑)</strong>:复制粘贴、查找替换、撤销重做。</li>
|
||||
<li><strong>View (视图)</strong>:控制界面显示,比如打开侧边栏、终端等。</li>
|
||||
<li><strong>Terminal (终端)</strong>:打开内置命令行工具。</li>
|
||||
</ul>
|
||||
<p>💡 <strong>小技巧</strong>:如果不记得某个功能在哪,可以按 <code>F1</code> 或 <code>Ctrl+Shift+P</code> 打开命令面板直接搜索功能名字!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-input-area">
|
||||
<div class="input-wrapper">
|
||||
<div class="input-preview" v-if="stepInt === 4 || (stepInt === 5 && isTyping)">
|
||||
<div class="mini-snapshot-ui">
|
||||
<div class="mini-menu-rect"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fake-input">
|
||||
<span v-if="stepInt < 5" class="placeholder">Message ChatGPT...</span>
|
||||
<span v-else class="typing-text">{{ typedText }}<span class="cursor" v-if="isTyping">|</span></span>
|
||||
</div>
|
||||
<button class="send-btn" :class="{ active: typedText.length > 5 }">↑</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 全局重置按钮 -->
|
||||
<button class="reset-btn" v-if="step === 'finished'" @click="reset">
|
||||
🔄 重播
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// 状态机
|
||||
// idle: 初始状态
|
||||
// selecting: 正在截图(选框出现)
|
||||
// captured: 截图完成(闪烁)
|
||||
// switching: 窗口切换中
|
||||
// pasting: ChatGPT 界面,显示输入动作
|
||||
// typing: 正在打字
|
||||
// sending: 发送中
|
||||
// responding: AI 回复中
|
||||
// finished: 结束
|
||||
const step = ref('idle')
|
||||
const typedText = ref('')
|
||||
const isTyping = ref(false)
|
||||
|
||||
const stepInt = computed(() => {
|
||||
const map = {
|
||||
idle: 0,
|
||||
selecting: 1,
|
||||
captured: 2,
|
||||
switching: 3,
|
||||
pasting: 4,
|
||||
typing: 5,
|
||||
sending: 6,
|
||||
responding: 7,
|
||||
finished: 8
|
||||
}
|
||||
return map[step.value] || 0
|
||||
})
|
||||
|
||||
const getWindowClass = (winName) => {
|
||||
if (winName === 'vscode') {
|
||||
if (stepInt.value < 3) return 'active'
|
||||
if (stepInt.value === 3) return 'inactive zoom-out'
|
||||
return 'inactive hidden'
|
||||
}
|
||||
if (winName === 'chatgpt') {
|
||||
if (stepInt.value < 3) return 'inactive hidden'
|
||||
if (stepInt.value === 3) return 'active zoom-in'
|
||||
return 'active'
|
||||
}
|
||||
}
|
||||
|
||||
const startDemo = async () => {
|
||||
step.value = 'selecting'
|
||||
|
||||
// 1. 模拟截图过程 (1.5s)
|
||||
await wait(1500)
|
||||
step.value = 'captured'
|
||||
|
||||
// 2. 截图闪烁确认 (0.5s)
|
||||
await wait(600)
|
||||
|
||||
// 3. 窗口切换 (0.8s)
|
||||
step.value = 'switching'
|
||||
await wait(800)
|
||||
|
||||
// 4. ChatGPT 界面准备 (粘贴动作)
|
||||
step.value = 'pasting'
|
||||
await wait(800)
|
||||
|
||||
// 5. 打字
|
||||
step.value = 'typing'
|
||||
isTyping.value = true
|
||||
const question = "帮我看下这张图,左边红框里那一块是干嘛用的?"
|
||||
for (let i = 0; i < question.length; i++) {
|
||||
typedText.value += question[i]
|
||||
await wait(50)
|
||||
}
|
||||
isTyping.value = false
|
||||
await wait(300)
|
||||
|
||||
// 6. 发送
|
||||
step.value = 'sending'
|
||||
await wait(500)
|
||||
|
||||
// 7. AI 回复
|
||||
step.value = 'responding'
|
||||
await wait(2500) // 模拟阅读时间
|
||||
|
||||
step.value = 'finished'
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
step.value = 'idle'
|
||||
typedText.value = ''
|
||||
}
|
||||
|
||||
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ai-help-demo {
|
||||
margin: 40px 0;
|
||||
perspective: 1000px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
.desktop-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px; /* 增加高度以容纳更多内容 */
|
||||
background: #333; /* 桌面背景 */
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 50px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
/* 通用窗口样式 */
|
||||
.window {
|
||||
position: absolute;
|
||||
top: 5%;
|
||||
left: 5%;
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 窗口状态动画 */
|
||||
.window.active {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
z-index: 2;
|
||||
filter: blur(0);
|
||||
}
|
||||
|
||||
.window.inactive {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.window.zoom-out {
|
||||
transform: scale(0.9);
|
||||
opacity: 0;
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
.window.zoom-in {
|
||||
animation: zoomIn 0.5s forwards;
|
||||
}
|
||||
|
||||
@keyframes zoomIn {
|
||||
from { transform: scale(1.1); opacity: 0; filter: blur(4px); }
|
||||
to { transform: scale(1); opacity: 1; filter: blur(0); }
|
||||
}
|
||||
|
||||
/* ================= VS Code 样式 ================= */
|
||||
.vscode {
|
||||
background: #1e1e1e;
|
||||
color: #ccc;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
height: 30px;
|
||||
background: #252526;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.controls { display: flex; gap: 6px; margin-right: 16px; }
|
||||
.dot { width: 10px; height: 10px; border-radius: 50%; }
|
||||
.red { background: #ff5f56; }
|
||||
.yellow { background: #ffbd2e; }
|
||||
.green { background: #27c93f; }
|
||||
|
||||
.main-layout { flex: 1; display: flex; overflow: hidden; }
|
||||
|
||||
.activity-bar {
|
||||
width: 40px;
|
||||
background: #333;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 10px;
|
||||
gap: 15px;
|
||||
}
|
||||
.activity-bar .icon { font-size: 18px; opacity: 0.5; filter: grayscale(1); }
|
||||
.activity-bar .icon.active { opacity: 1; border-left: 2px solid white; filter: grayscale(0); }
|
||||
|
||||
.sidebar {
|
||||
width: 180px;
|
||||
background: #252526;
|
||||
border-right: 1px solid #1e1e1e;
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
}
|
||||
.sidebar-title { padding: 10px; font-weight: bold; font-size: 11px; }
|
||||
.tree-item { padding: 4px 10px; display: flex; align-items: center; gap: 6px; cursor: pointer; }
|
||||
.tree-item.active { background: #37373d; }
|
||||
.tree-item.indent { padding-left: 20px; }
|
||||
.tree-item .arrow { font-size: 10px; color: #999; }
|
||||
|
||||
.editor-area { flex: 1; display: flex; flex-direction: column; background: #1e1e1e; position: relative; }
|
||||
.tab-bar { height: 35px; background: #252526; display: flex; }
|
||||
.tab {
|
||||
background: #1e1e1e;
|
||||
padding: 0 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
border-top: 1px solid #007acc;
|
||||
}
|
||||
.tab .close { margin-left: 8px; font-size: 14px; }
|
||||
|
||||
.code-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding: 10px 0;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
position: relative;
|
||||
}
|
||||
.line-numbers {
|
||||
width: 40px;
|
||||
text-align: right;
|
||||
padding-right: 15px;
|
||||
color: #6e7681;
|
||||
user-select: none;
|
||||
}
|
||||
.code-lines { flex: 1; padding-left: 5px; }
|
||||
|
||||
/* 语法高亮 */
|
||||
.kwd { color: #569cd6; }
|
||||
.var { color: #9cdcfe; }
|
||||
.func { color: #dcdcaa; }
|
||||
.str { color: #ce9178; }
|
||||
.comment { color: #6a9955; }
|
||||
|
||||
/* 截图覆盖层 */
|
||||
.screenshot-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.3);
|
||||
z-index: 10;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.selection-box {
|
||||
position: absolute;
|
||||
top: 40px; /* 覆盖左侧 Sidebar */
|
||||
left: 0;
|
||||
width: 280px;
|
||||
height: 320px;
|
||||
border: 3px solid #ff5f56; /* 醒目的红框 */
|
||||
background: rgba(255, 95, 86, 0.1);
|
||||
box-shadow: 0 0 0 9999px rgba(0,0,0,0.5); /* 遮罩效果 */
|
||||
animation: selectAnim 0.5s ease-out;
|
||||
}
|
||||
|
||||
.selection-box.flashed {
|
||||
animation: flash 0.3s;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
@keyframes selectAnim {
|
||||
from { width: 0; height: 0; }
|
||||
to { width: 280px; height: 320px; }
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
0% { background: rgba(255, 255, 255, 0.8); }
|
||||
100% { background: rgba(255, 255, 255, 0.1); }
|
||||
}
|
||||
|
||||
.selection-size {
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
right: 0;
|
||||
background: #ff5f56;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
/* UI Snapshot Styling */
|
||||
.ui-snapshot {
|
||||
background: #252526;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.snapshot-rect {
|
||||
width: 30px;
|
||||
height: 20px;
|
||||
border: 2px solid #ff5f56;
|
||||
background: rgba(255, 95, 86, 0.2);
|
||||
}
|
||||
.snapshot-text {
|
||||
font-size: 11px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.mini-snapshot-ui {
|
||||
width: 40px;
|
||||
height: 30px;
|
||||
background: #252526;
|
||||
border: 1px solid #565869;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.mini-rect {
|
||||
width: 20px;
|
||||
height: 14px;
|
||||
border: 1px solid #ff5f56;
|
||||
}
|
||||
|
||||
/* ================= ChatGPT 样式 ================= */
|
||||
.chatgpt {
|
||||
background: #343541;
|
||||
color: #ececf1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.chat-sidebar {
|
||||
width: 200px;
|
||||
background: #202123;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.new-chat {
|
||||
border: 1px solid #565869;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.history-item {
|
||||
font-size: 13px;
|
||||
color: #ececf1;
|
||||
padding: 8px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.history-item.active { background: #343541; }
|
||||
|
||||
.chat-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
background: #343541;
|
||||
}
|
||||
|
||||
.chat-model-selector {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
font-weight: 600;
|
||||
color: #d2d6db;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.msg-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
max-width: 80%;
|
||||
}
|
||||
.msg-row.user { align-self: flex-end; flex-direction: row-reverse; }
|
||||
.msg-row.ai { align-self: flex-start; }
|
||||
|
||||
.avatar {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
.user .avatar { background: #5436da; color: white; border-radius: 50%; }
|
||||
.ai .avatar { background: #19c37d; color: white; }
|
||||
.gpt-logo { width: 20px; height: 20px; }
|
||||
|
||||
.msg-bubble {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.user .msg-bubble { text-align: right; }
|
||||
.ai-bubble {
|
||||
background: #444654;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.pasted-image {
|
||||
margin-bottom: 8px;
|
||||
display: inline-block;
|
||||
border: 1px solid #565869;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.code-snapshot {
|
||||
background: #1e1e1e;
|
||||
padding: 8px;
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.chat-input-area {
|
||||
padding: 20px;
|
||||
background-image: linear-gradient(180deg, rgba(53,55,64,0), #353740 50%);
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
background: #40414f;
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.input-preview {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
margin-bottom: 8px;
|
||||
background: #40414f;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #565869;
|
||||
animation: slideUp 0.3s;
|
||||
}
|
||||
|
||||
.fake-input {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
min-height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.placeholder { color: #8e8ea0; }
|
||||
|
||||
.send-btn {
|
||||
background: #19c37d;
|
||||
border: none;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.send-btn.active { opacity: 1; }
|
||||
|
||||
/* 引导层 */
|
||||
.guide-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(2px);
|
||||
z-index: 100;
|
||||
}
|
||||
.start-btn {
|
||||
background: white;
|
||||
color: #333;
|
||||
padding: 12px 24px;
|
||||
border-radius: 30px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.start-btn:hover { transform: scale(1.05); }
|
||||
|
||||
.reset-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
@@ -1625,21 +1625,24 @@ onUnmounted(() => {
|
||||
width: 100%;
|
||||
}
|
||||
.tour-btn {
|
||||
background: #007acc;
|
||||
background: linear-gradient(135deg, #007acc 0%, #005999 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 122, 204, 0.3);
|
||||
}
|
||||
|
||||
.tour-btn:hover {
|
||||
background: #005fa3;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px rgba(0, 122, 204, 0.4);
|
||||
}
|
||||
.tour-btn.stop {
|
||||
background: #e51400;
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
<h3 class="stage-title">Step 1: Tokenization (分词)</h3>
|
||||
<p class="stage-desc">
|
||||
计算机首先将文本切分为最小的语义单位(Token)。
|
||||
<span style="font-size: 0.85em; color: var(--vp-c-text-2); display: block; margin-top: 4px;">
|
||||
(注:此处演示简化为按字切分,真实模型通常使用 BPE 算法,如“人工智能”可能合并为一个 Token)
|
||||
</span>
|
||||
</p>
|
||||
<div class="token-container">
|
||||
<div
|
||||
@@ -177,7 +180,8 @@ const getTokenColor = (idx) => colors[idx % colors.length]
|
||||
const getHeatmapColor = (val) => {
|
||||
// val is -1 to 1
|
||||
// Map to blue (negative) -> white (0) -> red (positive)
|
||||
const opacity = Math.abs(val)
|
||||
// Reduce max opacity to avoid confusion with "selection" or "special token"
|
||||
const opacity = Math.abs(val) * 0.6 + 0.1
|
||||
if (val > 0) return `rgba(239, 68, 68, ${opacity})` // Red
|
||||
return `rgba(59, 130, 246, ${opacity})` // Blue
|
||||
}
|
||||
@@ -341,6 +345,7 @@ const getHeatmapColor = (val) => {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center; /* Add centering */
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,57 +1,110 @@
|
||||
<template>
|
||||
<div class="attn-demo">
|
||||
<div class="controls">
|
||||
<span class="hint">🖱️ 把鼠标悬停在方块上,查看它的“注意力”分配</span>
|
||||
<div class="header">
|
||||
<div class="title">Self-Attention Mechanism</div>
|
||||
<div class="subtitle">自注意力机制:全局信息交互</div>
|
||||
</div>
|
||||
|
||||
<div class="visual-area">
|
||||
<div class="image-grid" @mouseleave="hoverIndex = -1">
|
||||
<div
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
class="grid-cell"
|
||||
:class="{ active: hoverIndex === index }"
|
||||
@mouseenter="hoverIndex = index"
|
||||
>
|
||||
{{ item.icon }}
|
||||
<div class="cell-label">{{ item.label }}</div>
|
||||
</div>
|
||||
|
||||
<!-- SVG Overlay for lines -->
|
||||
<svg class="connections" v-if="hoverIndex !== -1">
|
||||
<div class="visual-stage">
|
||||
<!-- Grid Layout -->
|
||||
<div class="grid-container" @mouseleave="hoverIndex = -1">
|
||||
<!-- SVG Layer for Connection Lines -->
|
||||
<svg class="connections-layer">
|
||||
<defs>
|
||||
<marker id="arrowhead" markerWidth="6" markerHeight="4" refX="18" refY="2" orient="auto">
|
||||
<polygon points="0 0, 6 2, 0 4" fill="var(--vp-c-brand)" opacity="0.6"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<!-- Draw lines from hoverIndex to ALL other nodes -->
|
||||
<g v-if="hoverIndex !== -1">
|
||||
<line
|
||||
v-for="(target, tIndex) in items"
|
||||
:key="tIndex"
|
||||
v-if="tIndex !== hoverIndex"
|
||||
v-show="tIndex !== hoverIndex"
|
||||
:x1="getCenter(hoverIndex).x"
|
||||
:y1="getCenter(hoverIndex).y"
|
||||
:x2="getCenter(tIndex).x"
|
||||
:y2="getCenter(tIndex).y"
|
||||
:stroke="getAttentionColor(hoverIndex, tIndex)"
|
||||
:stroke-width="getAttentionWidth(hoverIndex, tIndex)"
|
||||
:stroke="getLineColor(hoverIndex, tIndex)"
|
||||
:stroke-width="getLineWidth(hoverIndex, tIndex)"
|
||||
stroke-linecap="round"
|
||||
:opacity="getLineOpacity(hoverIndex, tIndex)"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<!-- Cells -->
|
||||
<div
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
class="grid-cell"
|
||||
:class="{
|
||||
'is-source': hoverIndex === index,
|
||||
'is-target': hoverIndex !== -1 && hoverIndex !== index,
|
||||
'is-strong-attn': hoverIndex !== -1 && getAttentionScore(hoverIndex, index) > 0.5
|
||||
}"
|
||||
@mouseenter="hoverIndex = index"
|
||||
:style="{
|
||||
left: getCenter(index).x - 30 + 'px',
|
||||
top: getCenter(index).y - 30 + 'px'
|
||||
}"
|
||||
>
|
||||
<div class="cell-content">
|
||||
<span class="cell-icon">{{ item.icon }}</span>
|
||||
<span class="cell-label">{{ item.label }}</span>
|
||||
</div>
|
||||
<!-- Attention Score Badge -->
|
||||
<div
|
||||
class="attn-badge"
|
||||
v-if="hoverIndex !== -1 && hoverIndex !== index"
|
||||
:style="{ opacity: Math.max(0.3, getAttentionScore(hoverIndex, index)) }"
|
||||
>
|
||||
{{ (getAttentionScore(hoverIndex, index) * 100).toFixed(0) }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-panel" :class="{ visible: hoverIndex !== -1 }">
|
||||
<div class="info-title">Patch: {{ items[hoverIndex]?.label }}</div>
|
||||
<div class="info-desc">正在关注:</div>
|
||||
<ul class="attn-list" v-if="hoverIndex !== -1">
|
||||
<li
|
||||
v-for="(weight, targetIdx) in getTopAttentions(hoverIndex)"
|
||||
:key="targetIdx"
|
||||
>
|
||||
<span class="target-icon">{{ items[targetIdx].icon }}</span>
|
||||
<span class="target-name">{{ items[targetIdx].label }}</span>
|
||||
<div class="bar-bg">
|
||||
<div
|
||||
class="bar-fill"
|
||||
:style="{ width: weight * 100 + '%' }"
|
||||
></div>
|
||||
<!-- Info Panel -->
|
||||
<div class="info-panel">
|
||||
<div v-if="hoverIndex === -1" class="placeholder-text">
|
||||
<span class="cursor-icon">👆</span>
|
||||
把鼠标悬停在任意方块上,<br>观察它在"关注"谁
|
||||
</div>
|
||||
<div v-else class="active-info">
|
||||
<div class="source-info">
|
||||
<span class="label">当前 Patch:</span>
|
||||
<div class="patch-tag">
|
||||
{{ items[hoverIndex].icon }} {{ items[hoverIndex].label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="attn-list">
|
||||
<div class="list-header">Attention Weights (注意力权重)</div>
|
||||
<div
|
||||
class="attn-item"
|
||||
v-for="(score, idx) in getTopAttentions(hoverIndex)"
|
||||
:key="idx"
|
||||
>
|
||||
<div class="item-left">
|
||||
<span class="item-icon">{{ items[idx].icon }}</span>
|
||||
<span class="item-name">{{ items[idx].label }}</span>
|
||||
</div>
|
||||
<div class="item-right">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: score * 100 + '%' }"></div>
|
||||
</div>
|
||||
<span class="score-text">{{ (score * 100).toFixed(0) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="insight-box">
|
||||
<span class="bulb">💡</span>
|
||||
<span class="insight-text">
|
||||
{{ getInsightText(hoverIndex) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,207 +115,309 @@ import { ref } from 'vue'
|
||||
|
||||
const hoverIndex = ref(-1)
|
||||
|
||||
// 3x3 Grid Data (Cat in grass)
|
||||
const items = [
|
||||
{ icon: '🌲', label: '背景' },
|
||||
{ icon: '🌲', label: '背景' },
|
||||
{ icon: '☁️', label: '天空' },
|
||||
{ icon: '👂', label: '猫耳' },
|
||||
{ icon: '😼', label: '猫脸' },
|
||||
{ icon: '🌲', label: '背景' },
|
||||
{ icon: '🐾', label: '猫爪' },
|
||||
{ icon: '🧶', label: '毛线' },
|
||||
{ icon: '🌱', label: '草地' }
|
||||
{ icon: '🌿', label: '草地' }, // 0
|
||||
{ icon: '🌿', label: '草地' }, // 1
|
||||
{ icon: '🦋', label: '蝴蝶' }, // 2
|
||||
{ icon: '🌿', label: '草地' }, // 3
|
||||
{ icon: '🐱', label: '猫头' }, // 4
|
||||
{ icon: '🌿', label: '草地' }, // 5
|
||||
{ icon: '🧶', label: '毛球' }, // 6
|
||||
{ icon: '🐾', label: '猫爪' }, // 7
|
||||
{ icon: '🌿', label: '草地' } // 8
|
||||
]
|
||||
|
||||
// 3x3 Grid
|
||||
// Layout Logic
|
||||
const getCenter = (index) => {
|
||||
const row = Math.floor(index / 3)
|
||||
const col = index % 3
|
||||
// Assuming 80px cell + 10px gap
|
||||
const cellSize = 80
|
||||
const gap = 10
|
||||
const offset = cellSize / 2
|
||||
const gap = 100
|
||||
const offsetX = 50
|
||||
const offsetY = 50
|
||||
return {
|
||||
x: col * (cellSize + gap) + offset,
|
||||
y: row * (cellSize + gap) + offset
|
||||
x: col * gap + offsetX,
|
||||
y: row * gap + offsetY
|
||||
}
|
||||
}
|
||||
|
||||
// Mock attention weights
|
||||
const getAttentionWeight = (source, target) => {
|
||||
// Self attention is ignored for visualization clarity usually, but let's say:
|
||||
// Attention Logic
|
||||
const getAttentionScore = (source, target) => {
|
||||
if (source === target) return 0
|
||||
|
||||
// Cat parts (3, 4, 6) attend strongly to each other
|
||||
const catParts = [3, 4, 6]
|
||||
const isSourceCat = catParts.includes(source)
|
||||
const isTargetCat = catParts.includes(target)
|
||||
// Cat Head (4) attends strongly to:
|
||||
if (source === 4) {
|
||||
if (target === 7) return 0.95 // Paws (Body parts connected)
|
||||
if (target === 2) return 0.8 // Butterfly (Interest)
|
||||
if (target === 6) return 0.6 // Yarn (Toy)
|
||||
return 0.1 // Background
|
||||
}
|
||||
|
||||
if (isSourceCat && isTargetCat) return 0.9 // Strong connection between cat parts
|
||||
// Cat Paws (7) attends strongly to:
|
||||
if (source === 7) {
|
||||
if (target === 4) return 0.95 // Head
|
||||
if (target === 6) return 0.9 // Yarn (Touching)
|
||||
return 0.1
|
||||
}
|
||||
|
||||
// Cat interacts with Yarn (7)
|
||||
if (isSourceCat && target === 7) return 0.7
|
||||
if (source === 7 && isTargetCat) return 0.7
|
||||
// Butterfly (2)
|
||||
if (source === 2) {
|
||||
if (target === 4) return 0.7 // Danger?
|
||||
return 0.2
|
||||
}
|
||||
|
||||
// Background parts attend to each other
|
||||
const bgParts = [0, 1, 2, 5, 8]
|
||||
if (bgParts.includes(source) && bgParts.includes(target)) return 0.5
|
||||
// Grass (Background)
|
||||
// Background patches attend to each other for texture consistency
|
||||
const bgIndices = [0, 1, 3, 5, 8]
|
||||
if (bgIndices.includes(source)) {
|
||||
if (bgIndices.includes(target)) return 0.6
|
||||
return 0.05
|
||||
}
|
||||
|
||||
return 0.1 // Weak attention otherwise
|
||||
// Default fallback
|
||||
return 0.1
|
||||
}
|
||||
|
||||
const getAttentionColor = (source, target) => {
|
||||
const weight = getAttentionWeight(source, target)
|
||||
// Green for strong, gray for weak
|
||||
if (weight > 0.6) return `rgba(16, 185, 129, ${weight})`
|
||||
return `rgba(156, 163, 175, ${weight * 0.5})`
|
||||
const getLineColor = (source, target) => {
|
||||
const score = getAttentionScore(source, target)
|
||||
return score > 0.5 ? 'var(--vp-c-brand)' : 'var(--vp-c-text-3)'
|
||||
}
|
||||
|
||||
const getAttentionWidth = (source, target) => {
|
||||
const weight = getAttentionWeight(source, target)
|
||||
return weight * 5
|
||||
const getLineWidth = (source, target) => {
|
||||
const score = getAttentionScore(source, target)
|
||||
return 1 + score * 4
|
||||
}
|
||||
|
||||
const getLineOpacity = (source, target) => {
|
||||
const score = getAttentionScore(source, target)
|
||||
return 0.2 + score * 0.8
|
||||
}
|
||||
|
||||
const getTopAttentions = (source) => {
|
||||
const weights = {}
|
||||
const scores = {}
|
||||
items.forEach((_, idx) => {
|
||||
if (idx !== source) {
|
||||
weights[idx] = getAttentionWeight(source, idx)
|
||||
scores[idx] = getAttentionScore(source, idx)
|
||||
}
|
||||
})
|
||||
// Sort by weight desc
|
||||
return weights
|
||||
// Sort descending
|
||||
const sortedKeys = Object.keys(scores).sort((a, b) => scores[b] - scores[a])
|
||||
const top3 = {}
|
||||
sortedKeys.slice(0, 3).forEach(key => {
|
||||
top3[key] = scores[key]
|
||||
})
|
||||
return top3
|
||||
}
|
||||
|
||||
const getInsightText = (idx) => {
|
||||
if (idx === 4) return "猫头最关注猫爪(组成身体)和蝴蝶(捕猎目标)。"
|
||||
if (idx === 7) return "猫爪最关注毛球(正在玩耍)和猫头。"
|
||||
if (idx === 2) return "蝴蝶关注到了猫,可能是因为它是个威胁。"
|
||||
if ([0,1,3,5,8].includes(idx)) return "草地主要关注周围的草地,确认背景纹理。"
|
||||
if (idx === 6) return "毛球和猫爪有很强的互动关系。"
|
||||
return "Self-Attention 让每个部分找到它的上下文关联。"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.attn-demo {
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin: 20px 0;
|
||||
user-select: none;
|
||||
font-family: 'Menlo', 'Monaco', sans-serif;
|
||||
}
|
||||
|
||||
.controls {
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 0.9em;
|
||||
color: var(--vp-c-text-2);
|
||||
background: var(--vp-c-bg);
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.visual-area {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 40px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 80px);
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.grid-cell {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.grid-cell:hover,
|
||||
.grid-cell.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
background: var(--vp-c-bg-mute);
|
||||
}
|
||||
|
||||
.cell-label {
|
||||
font-size: 0.8em;
|
||||
.subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.connections {
|
||||
.visual-stage {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Grid Area */
|
||||
.grid-container {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
position: relative;
|
||||
/* background: rgba(0,0,0,0.02); */
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.connections-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.grid-cell {
|
||||
position: absolute;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.cell-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cell-icon {
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.cell-label {
|
||||
font-size: 10px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Interaction States */
|
||||
.grid-cell:hover, .grid-cell.is-source {
|
||||
z-index: 10;
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg);
|
||||
transform: scale(1.15);
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.grid-cell.is-strong-attn {
|
||||
border-color: var(--vp-c-brand-light);
|
||||
background: var(--vp-c-brand-dimm);
|
||||
}
|
||||
|
||||
.attn-badge {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
font-size: 9px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Info Panel */
|
||||
.info-panel {
|
||||
width: 200px;
|
||||
width: 280px;
|
||||
min-height: 260px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
pointer-events: none;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.info-panel.visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
.placeholder-text {
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.info-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: var(--vp-c-brand);
|
||||
.cursor-icon {
|
||||
font-size: 32px;
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
.info-desc {
|
||||
font-size: 0.85em;
|
||||
.source-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px dashed var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.patch-tag {
|
||||
background: var(--vp-c-brand-dimm);
|
||||
color: var(--vp-c-brand-dark);
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
font-size: 11px;
|
||||
color: var(--vp-c-text-3);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.attn-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
.attn-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.attn-list li {
|
||||
.item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.85em;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.target-icon {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
.item-icon { font-size: 16px; }
|
||||
.item-name { font-size: 12px; font-weight: 500; }
|
||||
|
||||
.item-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.target-name {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.bar-bg {
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
@@ -270,9 +425,50 @@ const getTopAttentions = (source) => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar-fill {
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--vp-c-brand);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.score-text {
|
||||
font-size: 11px;
|
||||
color: var(--vp-c-text-2);
|
||||
width: 30px;
|
||||
text-align: right;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.insight-box {
|
||||
margin-top: 15px;
|
||||
background: var(--vp-c-yellow-dimm);
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.bulb { font-size: 16px; }
|
||||
.insight-text {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-1);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-5px); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.visual-stage {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.info-panel {
|
||||
width: 100%;
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
<div class="demo-container">
|
||||
<!-- Step 1: Patch -->
|
||||
<div class="step-box">
|
||||
<div class="label">1. Patch (4x4)</div>
|
||||
<div class="label">1. Patch (16×16×3) (示意 / Toy)</div>
|
||||
<div class="grid-patch">
|
||||
<div
|
||||
v-for="n in 16"
|
||||
v-for="n in patchCellCount"
|
||||
:key="n"
|
||||
class="pixel"
|
||||
:style="{ backgroundColor: getPixelColor(n) }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="desc">768 像素点</div>
|
||||
<div class="desc">16×16 像素 × 3 通道 = 768 标量值</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow">➜</div>
|
||||
@@ -22,13 +22,14 @@
|
||||
<div class="label">2. Flatten</div>
|
||||
<div class="vector-container">
|
||||
<div
|
||||
v-for="n in 16"
|
||||
v-for="n in flattenSampleCount"
|
||||
:key="n"
|
||||
class="vector-cell"
|
||||
:style="{ backgroundColor: getPixelColor(n) }"
|
||||
></div>
|
||||
<div class="vector-ellipsis">…</div>
|
||||
</div>
|
||||
<div class="desc">拉平成向量</div>
|
||||
<div class="desc">得到 1×768 向量 (Vector)</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow">× W</div>
|
||||
@@ -39,13 +40,16 @@
|
||||
<div class="embedding-container">
|
||||
<div v-for="n in 8" :key="n" class="embed-cell"></div>
|
||||
</div>
|
||||
<div class="desc">压缩特征 (D=8)</div>
|
||||
<div class="desc">映射到 D 维 (示意 D=8;常见 D=768)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const patchCellCount = 16 * 16
|
||||
const flattenSampleCount = 32
|
||||
|
||||
const getPixelColor = (n) => {
|
||||
// Generate a gradient of colors
|
||||
const hue = (n * 20) % 360
|
||||
@@ -89,8 +93,8 @@ const getPixelColor = (n) => {
|
||||
|
||||
.grid-patch {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 2px;
|
||||
grid-template-columns: repeat(16, 1fr);
|
||||
gap: 1px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
@@ -105,7 +109,7 @@ const getPixelColor = (n) => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
height: 120px;
|
||||
height: 140px;
|
||||
width: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -115,6 +119,14 @@ const getPixelColor = (n) => {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.vector-ellipsis {
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
color: var(--vp-c-text-3);
|
||||
text-align: center;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.embedding-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
+255
-222
@@ -8,113 +8,135 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="toggle-label">
|
||||
<span :class="{ active: !isVLM }">Pure LLM</span>
|
||||
<span :class="{ active: !isVLM }">Pure LLM (纯文本)</span>
|
||||
<span class="arrow">→</span>
|
||||
<span :class="{ active: isVLM }">Multimodal VLM</span>
|
||||
<span :class="{ active: isVLM }">Multimodal VLM (多模态)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-desc">
|
||||
{{
|
||||
isVLM
|
||||
? '给大脑装上眼睛:视觉信号经过翻译,变成 Token 混入文字流。'
|
||||
: '纯文本大脑:只能听懂 Token 语言,无法感知图像。'
|
||||
? 'Tokens from vision are translated and placed before text tokens. (视觉信息被翻译成 Token,放在文字 Token 之前。)'
|
||||
: 'Text-only tokens flow into the LLM. (只有文字 Token 流入大模型。)'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="diagram-stage" :class="{ 'vlm-mode': isVLM }">
|
||||
<!-- Vision Pipeline (Only visible in VLM mode) -->
|
||||
<div class="pipeline vision-pipeline">
|
||||
<div class="node-group">
|
||||
<div class="node input-node image-node">
|
||||
<span class="icon">�️</span>
|
||||
<span class="label">Image</span>
|
||||
<div class="diagram-stage">
|
||||
<div class="lanes">
|
||||
<div class="lane lane-vision" v-show="isVLM">
|
||||
<div class="lane-title">Vision Path (视觉路径)</div>
|
||||
<div class="lane-flow">
|
||||
<div class="node input-node">
|
||||
<span class="icon">🖼️</span>
|
||||
<span class="label">Image (图片)</span>
|
||||
</div>
|
||||
<div class="flow-arrow">⬇</div>
|
||||
<div
|
||||
class="node process-node vit-node"
|
||||
title="Vision Transformer: The Eye"
|
||||
>
|
||||
<span class="icon">�️</span>
|
||||
<span class="label">ViT</span>
|
||||
<span class="mini-arrow">→</span>
|
||||
<div class="node process-node vit-node">
|
||||
<span class="icon">👁️</span>
|
||||
<span class="label">ViT (视觉模型)</span>
|
||||
</div>
|
||||
<div class="flow-arrow">⬇</div>
|
||||
<div
|
||||
class="node adapter-node projector-node"
|
||||
title="Projector: The Translator"
|
||||
>
|
||||
<span class="mini-arrow">→</span>
|
||||
<div class="node adapter-node">
|
||||
<span class="icon">🔌</span>
|
||||
<span class="label">Projector</span>
|
||||
<span class="label">Projector (投影器)</span>
|
||||
</div>
|
||||
<span class="mini-arrow">→</span>
|
||||
<div class="token-box token-box-vision">
|
||||
<div class="token-box-title">Vision Tokens (视觉 Token)</div>
|
||||
<div class="tokens">
|
||||
<span class="token vision">v1</span>
|
||||
<span class="token vision">v2</span>
|
||||
<span class="token vision">v3</span>
|
||||
<span class="token vision">…</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-arrow connector-arrow">⤵</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text Pipeline (Always visible) -->
|
||||
<div class="pipeline text-pipeline">
|
||||
<div class="node-group horizontal">
|
||||
<div class="node input-node text-node">
|
||||
<span class="icon">�</span>
|
||||
<span class="label">Prompt</span>
|
||||
<div class="lane lane-text">
|
||||
<div class="lane-title">Text Path (文字路径)</div>
|
||||
<div class="lane-flow">
|
||||
<div class="node input-node">
|
||||
<span class="icon">⌨️</span>
|
||||
<span class="label">Prompt (提示词)</span>
|
||||
</div>
|
||||
<span class="mini-arrow">→</span>
|
||||
<div class="node process-node">
|
||||
<span class="icon">🔤</span>
|
||||
<span class="label">Embed (向量化)</span>
|
||||
</div>
|
||||
<span class="mini-arrow">→</span>
|
||||
<div class="token-box">
|
||||
<div class="token-box-title">Text Tokens (文字 Token)</div>
|
||||
<div class="tokens">
|
||||
<span class="token text">t1</span>
|
||||
<span class="token text">t2</span>
|
||||
<span class="token text">t3</span>
|
||||
<span class="token text">…</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-arrow">➜</div>
|
||||
<div class="node process-node embed-node">
|
||||
<span class="icon">�</span>
|
||||
<span class="label">Embed</span>
|
||||
</div>
|
||||
|
||||
<!-- Merge Point Visualization -->
|
||||
<div class="merge-point" :class="{ active: isVLM }">
|
||||
<div class="plus-icon">+</div>
|
||||
<div class="merge-label">Concat</div>
|
||||
<div class="merge-stage">
|
||||
<div class="merge-title">Token Sequence (输入序列)</div>
|
||||
<div class="sequence">
|
||||
<div v-if="isVLM" class="sequence-row">
|
||||
<span class="sequence-tag vision">Vision (视觉)</span>
|
||||
<div class="tokens">
|
||||
<span class="token vision">v1</span>
|
||||
<span class="token vision">v2</span>
|
||||
<span class="token vision">v3</span>
|
||||
<span class="token vision">…</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sequence-row">
|
||||
<span class="sequence-tag text">Text (文字)</span>
|
||||
<div class="tokens">
|
||||
<span class="token text">t1</span>
|
||||
<span class="token text">t2</span>
|
||||
<span class="token text">t3</span>
|
||||
<span class="token text">…</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sequence-hint">
|
||||
<span v-if="isVLM">Concat: [Vision Tokens] + [Text Tokens] (拼接:视觉在前,文字在后)</span>
|
||||
<span v-else>Only [Text Tokens] (只有文字 Token)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-arrow">➜</div>
|
||||
<div class="node core-node llm-node">
|
||||
<div class="core-stage">
|
||||
<span class="big-arrow">→</span>
|
||||
<div class="node core-node">
|
||||
<span class="icon">🧠</span>
|
||||
<span class="label">LLM Backbone</span>
|
||||
<div class="inner-flow">
|
||||
<span class="dot t1"></span>
|
||||
<span class="dot t2"></span>
|
||||
<span class="dot v1" v-if="isVLM"></span>
|
||||
<span class="label">LLM Backbone (大模型)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-arrow">➜</div>
|
||||
<span class="big-arrow">→</span>
|
||||
<div class="node output-node">
|
||||
<span class="icon">💬</span>
|
||||
<span class="label">Response</span>
|
||||
<span class="label">Response (回复)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="interactive-info">
|
||||
<div class="info-card" v-if="!isVLM">
|
||||
<h3>Standard LLM Flow</h3>
|
||||
<p>
|
||||
Text is converted into vectors (Embeddings) and processed by the
|
||||
Transformer to predict the next word.
|
||||
</p>
|
||||
<transition name="fade" mode="out-in">
|
||||
<div class="info-card" v-if="!isVLM" key="llm">
|
||||
<h3>Standard LLM Flow (标准大模型流程)</h3>
|
||||
<p>Prompt → Embedding → Token Sequence → LLM → Response。</p>
|
||||
</div>
|
||||
<div class="info-card vlm-info" v-else>
|
||||
<h3>VLM = LLM + Vision Encoder</h3>
|
||||
<div class="info-card vlm-info" v-else key="vlm">
|
||||
<h3>VLM = LLM + Vision Encoder (视觉大模型原理)</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>ViT (The Eye):</strong> Slices image into patches and
|
||||
extracts features.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Projector (The Translator):</strong> Converts visual
|
||||
features into the same "language" (vector dimension) as text
|
||||
embeddings.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Concatenation:</strong> The translated visual tokens are
|
||||
pasted <em>before</em> the text tokens. The LLM sees them as
|
||||
"foreign words" it learned to understand.
|
||||
</li>
|
||||
<li><strong>ViT (The Eye):</strong> 把图片编码成视觉特征。</li>
|
||||
<li><strong>Projector (The Translator):</strong> 把视觉特征映射到 LLM 的 Token 空间。</li>
|
||||
<li><strong>Concatenation (拼接):</strong> 把视觉 Token 放在文字 Token 之前,作为同一条输入序列。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -140,12 +162,11 @@ const toggleMode = () => {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
.controls-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
margin-bottom: 18px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@@ -216,105 +237,160 @@ const toggleMode = () => {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
text-align: center;
|
||||
height: 20px;
|
||||
line-height: 1.5;
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
/* Diagram Stage */
|
||||
.diagram-stage {
|
||||
position: relative;
|
||||
height: 240px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.lanes {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Pipelines */
|
||||
.pipeline {
|
||||
transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.text-pipeline {
|
||||
position: absolute;
|
||||
bottom: 80px; /* Centered vertically in LLM mode */
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.vlm-mode .text-pipeline {
|
||||
bottom: 40px; /* Move down in VLM mode */
|
||||
}
|
||||
|
||||
.vision-pipeline {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20%; /* Align with input side */
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.vlm-mode .vision-pipeline {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.node-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.node-group.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.vision-pipeline .node-group {
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.lane {
|
||||
background: var(--vp-c-bg-mute);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.lane-title {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lane-flow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.merge-stage {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.merge-title {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sequence {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.sequence-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sequence-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sequence-tag {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.sequence-tag.vision {
|
||||
border-color: var(--vp-c-yellow);
|
||||
}
|
||||
|
||||
.sequence-tag.text {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.sequence-hint {
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.core-stage {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.big-arrow {
|
||||
font-size: 18px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.mini-arrow {
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-3);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
/* Nodes */
|
||||
.node {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 70px;
|
||||
min-width: 110px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.input-node {
|
||||
border-color: #aaa;
|
||||
}
|
||||
|
||||
.process-node {
|
||||
border-color: var(--vp-c-brand-dimm);
|
||||
}
|
||||
|
||||
.core-node {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-dimm);
|
||||
min-width: 100px;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.output-node {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
@@ -323,101 +399,64 @@ const toggleMode = () => {
|
||||
border-color: var(--vp-c-yellow);
|
||||
background: rgba(255, 197, 23, 0.05);
|
||||
}
|
||||
.projector-node {
|
||||
|
||||
.adapter-node {
|
||||
border-color: var(--vp-c-yellow);
|
||||
background: var(--vp-c-yellow-dimm);
|
||||
}
|
||||
|
||||
/* Arrows */
|
||||
.flow-arrow {
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 16px;
|
||||
.token-box {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.connector-arrow {
|
||||
font-size: 24px;
|
||||
color: var(--vp-c-yellow);
|
||||
margin-top: -10px;
|
||||
margin-bottom: -10px;
|
||||
transform: rotate(-45deg) translateX(10px);
|
||||
.token-box-vision {
|
||||
border-color: var(--vp-c-yellow);
|
||||
}
|
||||
|
||||
/* Merge Point */
|
||||
.merge-point {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
transition: all 0.5s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.merge-point.active {
|
||||
width: 40px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.plus-icon {
|
||||
font-weight: bold;
|
||||
.token-box-title {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 18px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.merge-label {
|
||||
font-size: 9px;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
/* Inner Flow Animation inside LLM */
|
||||
.inner-flow {
|
||||
.tokens {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
height: 6px;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
opacity: 0.6;
|
||||
animation: pulse 1s infinite alternate;
|
||||
.token {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.t1 {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
.t2 {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
.v1 {
|
||||
background: var(--vp-c-yellow);
|
||||
animation-delay: 0.4s;
|
||||
.token.vision {
|
||||
border-color: var(--vp-c-yellow);
|
||||
background: rgba(255, 197, 23, 0.12);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
from {
|
||||
opacity: 0.3;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.token.text {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: rgba(59, 130, 246, 0.12);
|
||||
}
|
||||
|
||||
/* Interactive Info */
|
||||
.interactive-info {
|
||||
margin-top: 20px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: var(--vp-c-bg-mute);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
animation: fadeIn 0.3s;
|
||||
}
|
||||
|
||||
.info-card h3 {
|
||||
@@ -439,31 +478,25 @@ const toggleMode = () => {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
/* Mobile Adjustments */
|
||||
@media (max-width: 600px) {
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.diagram-stage {
|
||||
height: 300px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.text-pipeline {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
width: 90%;
|
||||
.node {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.vision-pipeline {
|
||||
left: 10%;
|
||||
.token-box {
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,67 +6,137 @@
|
||||
<div class="patchify-demo">
|
||||
<div class="control-panel">
|
||||
<div class="controls">
|
||||
<button class="action-btn" @click="toggleState">
|
||||
{{ isPatchified ? '还原图片 (Restore)' : '切分图片 (Patchify)' }}
|
||||
<button
|
||||
class="action-btn"
|
||||
@click="prevStep"
|
||||
:disabled="currentStep === 0"
|
||||
>
|
||||
⬅ 上一步 (Prev)
|
||||
</button>
|
||||
<span class="step-indicator">Step {{ currentStep + 1 }} / 4</span>
|
||||
<button
|
||||
class="action-btn primary"
|
||||
@click="nextStep"
|
||||
:disabled="currentStep === 3"
|
||||
>
|
||||
{{ currentStep === 3 ? '完成 (Done)' : '下一步 (Next) ➡' }}
|
||||
</button>
|
||||
<div class="info">
|
||||
<span>Resolution: 224x224</span>
|
||||
<span>Patch Size: 16x16</span>
|
||||
<span>Total Patches: {{ 14 * 14 }}</span>
|
||||
</div>
|
||||
<div class="step-desc">
|
||||
{{ stepDescriptions[currentStep] }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="visual-area">
|
||||
<!-- 原始/切分视图容器 -->
|
||||
<div class="image-container" :class="{ 'is-patchified': isPatchified }">
|
||||
<!--
|
||||
Step 0: Show container background, cells hidden
|
||||
Step 1: Show container background, grid overlay visible (cells with border)
|
||||
Step 2+: Container background hidden, cells visible with individual backgrounds
|
||||
-->
|
||||
<div
|
||||
class="image-container"
|
||||
:class="{
|
||||
'is-pixelated': currentStep >= 1,
|
||||
'is-patchified': currentStep >= 2
|
||||
}"
|
||||
>
|
||||
<div class="grid-overlay" v-if="currentStep === 1"></div>
|
||||
<div
|
||||
v-for="n in 196"
|
||||
:key="n"
|
||||
class="patch"
|
||||
:style="{
|
||||
'--delay': `${n * 0.005}s`,
|
||||
'--hue': `${(n % 14) * 20 + Math.floor(n / 14) * 20}`
|
||||
}"
|
||||
:style="getPatchStyle(n)"
|
||||
>
|
||||
<span class="patch-id" v-if="isPatchified">{{ n }}</span>
|
||||
<!-- Show number only in Pixelated stage to represent 'digitization' -->
|
||||
<span class="pixel-val" v-if="currentStep === 1">{{ Math.floor(Math.random() * 9) }}</span>
|
||||
<!-- Show ID in Patchified stage -->
|
||||
<span class="patch-id" v-if="currentStep >= 2">{{ n }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow" v-if="isPatchified">⬇</div>
|
||||
<div class="arrow-down" v-if="currentStep >= 3">⬇</div>
|
||||
|
||||
<!-- 线性序列视图 -->
|
||||
<div class="sequence-container" v-if="isPatchified">
|
||||
<div class="sequence-label">Flattened Sequence (Token Input)</div>
|
||||
<div class="sequence-container" v-if="currentStep >= 3">
|
||||
<div class="sequence-label">Token Sequence: 196×D (每个 Token 是 D 维向量)</div>
|
||||
<div class="token-stream">
|
||||
<div
|
||||
v-for="n in 196"
|
||||
:key="n"
|
||||
class="mini-patch"
|
||||
:style="{ '--hue': `${(n % 14) * 20 + Math.floor(n / 14) * 20}` }"
|
||||
:style="getMiniPatchStyle(n)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="explanation">
|
||||
<p>
|
||||
<span class="icon">💡</span>
|
||||
计算机将图片切成 <strong>14x14 = 196</strong> 个小方块(Patch)。
|
||||
然后把这些方块“拉直”成一长串序列,就像把一段话里的单词排成一排一样。
|
||||
这就是 <strong>Visual Tokenization</strong>。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const isPatchified = ref(false)
|
||||
const currentStep = ref(0)
|
||||
|
||||
const toggleState = () => {
|
||||
isPatchified.value = !isPatchified.value
|
||||
const stepDescriptions = [
|
||||
"1. 原始图片 (Original Image): 计算机看到的原始输入。",
|
||||
"2. 数字化 (Digitization): 图片本质上是一个数字矩阵 (H x W x C)。",
|
||||
"3. 切块 (Patchify): 典型设置:224×224 按 16×16 切成 14×14=196 个 Patch(此处等比示意)。",
|
||||
"4. 序列化 (Serialize): 将二维分布的 Patch “拍扁”成一维序列 (Spatial Flatten)。现在它看起来就像一串“视觉单词”,可以被 Transformer 逐个读取。"
|
||||
]
|
||||
|
||||
const nextStep = () => {
|
||||
if (currentStep.value < 3) currentStep.value++
|
||||
}
|
||||
|
||||
const prevStep = () => {
|
||||
if (currentStep.value > 0) currentStep.value--
|
||||
}
|
||||
|
||||
// 模拟一张风景图的 CSS 渐变
|
||||
// Sky (Blue) -> Mountains (Green/Grey) -> Sun (Yellow)
|
||||
const bgImage = 'linear-gradient(to bottom, #87CEEB 0%, #87CEEB 50%, #228B22 50%, #228B22 100%)'
|
||||
// Add a sun using radial gradient
|
||||
const complexBg = 'radial-gradient(circle at 70% 20%, #FFD700 0%, #FFD700 10%, transparent 10.5%), linear-gradient(to bottom, #87CEEB 0%, #87CEEB 60%, #4CA1AF 60%, #2C3E50 100%)'
|
||||
|
||||
const getPatchStyle = (n) => {
|
||||
const row = Math.floor((n - 1) / 14)
|
||||
const col = (n - 1) % 14
|
||||
|
||||
// Calculate background position for each patch to match the original image
|
||||
// The container is 280px, each patch is 20px.
|
||||
// 14 cols.
|
||||
const posX = col * -20
|
||||
const posY = row * -20
|
||||
|
||||
const isPatchified = currentStep.value >= 2
|
||||
|
||||
return {
|
||||
backgroundImage: complexBg,
|
||||
backgroundPosition: `${posX}px ${posY}px`,
|
||||
backgroundSize: '280px 280px',
|
||||
// In Step 0, patches are hidden to show pure container background
|
||||
// In Step 1, patches are visible but transparent background to show numbers/borders over container background
|
||||
// In Step 2, patches take over with their own background
|
||||
opacity: currentStep.value === 0 ? 0 : 1,
|
||||
// In Step 1, background must be transparent to see container bg
|
||||
backgroundImage: isPatchified ? complexBg : 'none',
|
||||
transform: isPatchified ? 'scale(0.9)' : 'scale(1)',
|
||||
transition: 'all 0.5s ease',
|
||||
}
|
||||
}
|
||||
|
||||
const getMiniPatchStyle = (n) => {
|
||||
const row = Math.floor((n - 1) / 14)
|
||||
const col = (n - 1) % 14
|
||||
const posX = col * -20
|
||||
const posY = row * -20
|
||||
|
||||
return {
|
||||
backgroundImage: complexBg,
|
||||
backgroundPosition: `${posX}px ${posY}px`,
|
||||
backgroundSize: '280px 280px',
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -77,40 +147,68 @@ const toggleState = () => {
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
margin: 20px 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
.step-indicator {
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 0.9em;
|
||||
color: var(--vp-c-text-1);
|
||||
text-align: center;
|
||||
background: var(--vp-c-bg-mute);
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.info {
|
||||
min-height: 40px;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: var(--vp-c-bg-mute);
|
||||
color: var(--vp-c-text-1);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.9em;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-btn:not(:disabled):hover {
|
||||
opacity: 0.8;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.visual-area {
|
||||
@@ -118,7 +216,7 @@ const toggleState = () => {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
min-height: 300px;
|
||||
min-height: 350px;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
@@ -126,31 +224,55 @@ const toggleState = () => {
|
||||
grid-template-columns: repeat(14, 1fr);
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
gap: 0;
|
||||
background: #333;
|
||||
/* Step 0 & 1 Background */
|
||||
background-image: radial-gradient(circle at 70% 20%, #FFD700 0%, #FFD700 10%, transparent 10.5%), linear-gradient(to bottom, #87CEEB 0%, #87CEEB 60%, #4CA1AF 60%, #2C3E50 100%);
|
||||
position: relative;
|
||||
transition: all 0.5s ease;
|
||||
border: 2px solid var(--vp-c-text-1);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* Step 2+: Remove container background, let patches show */
|
||||
.image-container.is-patchified {
|
||||
background-image: none;
|
||||
background-color: transparent;
|
||||
gap: 2px;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.patch {
|
||||
background-color: hsl(var(--hue), 70%, 60%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 8px;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
transition: all 0.5s ease;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.is-patchified .patch {
|
||||
/* Step 1: Pixelated Overlay Effect */
|
||||
.image-container.is-pixelated:not(.is-patchified) .patch {
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
/* Use pseudo-element or just opacity logic in JS */
|
||||
}
|
||||
|
||||
/* Step 1: Digitization numbers */
|
||||
.pixel-val {
|
||||
font-family: monospace;
|
||||
font-size: 8px;
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
.patch-id {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
padding: 1px 2px;
|
||||
border-radius: 2px;
|
||||
transform: scale(0.9);
|
||||
font-size: 7px;
|
||||
}
|
||||
|
||||
.arrow-down {
|
||||
font-size: 24px;
|
||||
color: var(--vp-c-text-2);
|
||||
animation: bounce 1s infinite;
|
||||
}
|
||||
|
||||
.sequence-container {
|
||||
@@ -159,7 +281,7 @@ const toggleState = () => {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
animation: fadeIn 0.5s ease;
|
||||
animation: slideUp 0.5s ease;
|
||||
}
|
||||
|
||||
.sequence-label {
|
||||
@@ -171,50 +293,48 @@ const toggleState = () => {
|
||||
|
||||
.token-stream {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
flex-wrap: nowrap;
|
||||
gap: 1px;
|
||||
overflow-x: auto;
|
||||
padding: 10px 5px; /* Space for brackets */
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Add Matrix Brackets */
|
||||
.token-stream::before,
|
||||
.token-stream::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 6px;
|
||||
height: 36px; /* Match vector height + padding */
|
||||
border: 2px solid var(--vp-c-text-3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.token-stream::before {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.token-stream::after {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.mini-patch {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: hsl(var(--hue), 70%, 60%);
|
||||
width: 6px; /* Thinner to allow more density */
|
||||
height: 32px; /* Taller to represent Vector Dimension D */
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.explanation {
|
||||
margin-top: 20px;
|
||||
padding: 12px;
|
||||
background: var(--vp-c-bg-mute);
|
||||
border-radius: 6px;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 24px;
|
||||
color: var(--vp-c-text-2);
|
||||
animation: bounce 1s infinite;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(5px);
|
||||
}
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(5px); }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="pipeline">
|
||||
<!-- 1. Transformer Output Grid -->
|
||||
<div class="stage">
|
||||
<div class="stage-label">1. Processed Patches (Grid)</div>
|
||||
<div class="stage-label">1. Patch Tokens (Shown as Grid) (Patch Token 网格示意)</div>
|
||||
<div class="grid-container">
|
||||
<div
|
||||
v-for="(item, index) in items"
|
||||
@@ -19,13 +19,13 @@
|
||||
|
||||
<div class="arrow-section">
|
||||
<div class="arrow-line"></div>
|
||||
<div class="arrow-text">Flatten & Output</div>
|
||||
<div class="arrow-text">Reshape for View: Grid ⇄ Sequence (重排显示:网格⇄序列)</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Feature Vector Sequence -->
|
||||
<div class="stage">
|
||||
<div class="stage-label">
|
||||
2. Feature Vector Sequence (The "Image Sentence")
|
||||
2. Output Token Sequence (N×D) (输出序列)
|
||||
</div>
|
||||
<div class="vector-sequence">
|
||||
<div
|
||||
|
||||
@@ -1,307 +1,450 @@
|
||||
<template>
|
||||
<div class="browser-rendering-demo">
|
||||
<div class="control-bar">
|
||||
<div class="step-indicator">Step: {{ currentStep + 1 }} / 4</div>
|
||||
<div class="steps-nav">
|
||||
<div class="stepper">
|
||||
<button
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
:class="{ active: currentStep === index }"
|
||||
class="step-btn"
|
||||
:class="{ active: currentStep === index, completed: currentStep > index }"
|
||||
@click="currentStep = index"
|
||||
>
|
||||
{{ step.label }}
|
||||
<span class="step-num">{{ index + 1 }}</span>
|
||||
<span class="step-label">{{ step.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="workspace">
|
||||
<!-- Left: Source Code -->
|
||||
<div class="source-panel">
|
||||
<div class="panel-label">HTML / CSS</div>
|
||||
<div class="code-block">
|
||||
<div class="line"><div id="app"></div>
|
||||
<div class="line indent"><h1>Hello</h1></div>
|
||||
<div class="line indent"><p>World</p></div>
|
||||
<div class="line"></div></div>
|
||||
<div class="line mt-2">h1 { color: red; }</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrow -->
|
||||
<div class="arrow">→</div>
|
||||
|
||||
<!-- Right: Visualization -->
|
||||
<div class="viz-panel">
|
||||
<div class="panel-label">{{ steps[currentStep].title }}</div>
|
||||
|
||||
<transition name="fade" mode="out-in">
|
||||
<!-- Step 1: DOM Tree -->
|
||||
<div v-if="currentStep === 0" class="tree-viz">
|
||||
<div class="node root">Document</div>
|
||||
<div class="tree-lines">
|
||||
<div class="line-v"></div>
|
||||
</div>
|
||||
<div class="node element">html</div>
|
||||
<div class="tree-lines">
|
||||
<div class="line-v"></div>
|
||||
</div>
|
||||
<div class="node element">body</div>
|
||||
<div class="tree-children">
|
||||
<div class="node element active">div#app</div>
|
||||
<div class="tree-children row">
|
||||
<div class="node element">h1</div>
|
||||
<div class="node element">p</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Render Tree -->
|
||||
<div v-else-if="currentStep === 1" class="tree-viz render-tree">
|
||||
<div class="node render-obj">RenderBlock (div)</div>
|
||||
<div class="tree-children row">
|
||||
<div class="node render-obj red">
|
||||
RenderText (h1) <br /><small>color: red</small>
|
||||
</div>
|
||||
<div class="node render-obj">RenderText (p)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Layout -->
|
||||
<div v-else-if="currentStep === 2" class="layout-viz">
|
||||
<div class="layout-box root">
|
||||
<span class="dims">100% x 100%</span>
|
||||
<div class="layout-box container">
|
||||
<span class="dims">div: 100% x auto</span>
|
||||
<div class="layout-box item h1">h1: 100% x 32px (x:0, y:0)</div>
|
||||
<div class="layout-box item p">p: 100% x 16px (x:0, y:32)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Paint -->
|
||||
<div v-else-if="currentStep === 3" class="paint-viz">
|
||||
<div class="browser-window">
|
||||
<div class="painted-content">
|
||||
<h1 style="color: red; margin: 0">Hello</h1>
|
||||
<p style="margin: 0">World</p>
|
||||
</div>
|
||||
<div class="paint-layers">
|
||||
<div class="layer-item">Layer 1: Background</div>
|
||||
<div class="layer-item">Layer 2: Text</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-footer">
|
||||
<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"><!DOCTYPE html></div>
|
||||
<div class="line" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'html' }" @mouseenter="hoveredPart = 'html'" @mouseleave="hoveredPart = null"><html></div>
|
||||
<div class="line indent" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'body' }" @mouseenter="hoveredPart = 'body'" @mouseleave="hoveredPart = null"><body></div>
|
||||
<div class="line indent-2" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'card' }" @mouseenter="hoveredPart = 'card'" @mouseleave="hoveredPart = null"><div class="card"></div>
|
||||
<div class="line indent-3" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'img' }" @mouseenter="hoveredPart = 'img'" @mouseleave="hoveredPart = null"><img class="icon" src="castle.png" /></div>
|
||||
<div class="line indent-3" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'title' }" @mouseenter="hoveredPart = 'title'" @mouseleave="hoveredPart = null"><h2 class="title">乐高城堡</h2></div>
|
||||
<div class="line indent-3" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'btn' }" @mouseenter="hoveredPart = 'btn'" @mouseleave="hoveredPart = null"><button class="btn">购买</button></div>
|
||||
<div class="line indent-2" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'card' }" @mouseenter="hoveredPart = 'card'" @mouseleave="hoveredPart = null"></div></div>
|
||||
<div class="line indent" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'body' }" @mouseenter="hoveredPart = 'body'" @mouseleave="hoveredPart = null"></body></div>
|
||||
<div class="line" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'html' }" @mouseenter="hoveredPart = 'html'" @mouseleave="hoveredPart = null"></html></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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const currentStep = ref(0)
|
||||
|
||||
const steps = [
|
||||
{
|
||||
label: '1. DOM',
|
||||
title: 'DOM Tree Construction',
|
||||
desc: '浏览器解析 HTML 标记,构建 DOM (文档对象模型) 树。每个标签成为一个节点。'
|
||||
label: '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><p></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' }"
|
||||
>
|
||||
<p>我是普通段落</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="html-line"
|
||||
:class="{ highlight: activeType === 'class' }"
|
||||
>
|
||||
<div <span class="attr">class="card"</span>>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="html-line indent"
|
||||
:class="{ highlight: activeType === 'tag' || activeType === 'class' }"
|
||||
>
|
||||
<p>我是卡片里的段落</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="html-line"
|
||||
:class="{ highlight: activeType === 'class' }"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="html-line"
|
||||
:class="{ highlight: activeType === 'id' }"
|
||||
>
|
||||
<button <span class="attr">id="submit-btn"</span>>提交</button>
|
||||
</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="request-builder">
|
||||
<div class="input-row">
|
||||
<select v-model="method" class="method-select">
|
||||
<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="path-input"
|
||||
placeholder="/index.html"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="headers-section">
|
||||
<div class="section-title">Headers</div>
|
||||
<div v-for="(value, key) in headers" :key="key" class="header-row">
|
||||
<span class="header-key">{{ key }}:</span>
|
||||
<span class="header-value">{{ value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="sendRequest"
|
||||
class="send-btn"
|
||||
:disabled="isProcessing"
|
||||
>
|
||||
{{ isProcessing ? 'Sending...' : 'Send Request' }}
|
||||
<input v-model="path" class="url-input" :disabled="loading" />
|
||||
<button @click="sendRequest" :disabled="loading" class="send-btn">
|
||||
{{ loading ? '...' : t.send }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network Visualization -->
|
||||
<div class="network-space">
|
||||
<div class="connection-line"></div>
|
||||
<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
|
||||
v-if="currentPacket"
|
||||
class="packet"
|
||||
:class="currentPacket.type"
|
||||
:style="{ left: packetPosition + '%' }"
|
||||
class="log-row"
|
||||
:class="{ active: requestSent, selected: true }"
|
||||
v-if="requestSent"
|
||||
>
|
||||
{{ currentPacket.label }}
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- Server Side -->
|
||||
<div class="panel server-panel">
|
||||
<div class="panel-header"><span class="icon">🖥️</span> Server</div>
|
||||
<!-- Response Tab -->
|
||||
<div v-if="activeTab === 'response'" class="code-view">
|
||||
<pre>{{ responseBody }}</pre>
|
||||
</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>
|
||||
<!-- 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 class="body-preview">
|
||||
{{ response.body }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="placeholder">Waiting for request...</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'
|
||||
const t = {
|
||||
send: '提交订单 (发送请求)',
|
||||
noRequests: '购物车是空的 (无请求)',
|
||||
placeholder: '点击 "提交订单" 向店员购买玩具',
|
||||
general: '订单详情 (General)',
|
||||
requestUrl: '商品地址 (URL)',
|
||||
requestMethod: '操作类型 (Method)',
|
||||
statusCode: '店员回复 (Status)',
|
||||
responseHeaders: '包裹标签 (Headers)',
|
||||
tabs: {
|
||||
headers: '订单信息',
|
||||
response: '包裹内容',
|
||||
preview: '玩具预览'
|
||||
},
|
||||
body: '<!DOCTYPE html>\n<html>\n <body>Hello World</body>\n</html>'
|
||||
},
|
||||
POST: {
|
||||
status: 201,
|
||||
statusText: 'Created',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: '{"success": true, "id": 123}'
|
||||
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>
|
||||
<div v-if="count > 5">...</div>
|
||||
</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'
|
||||
}
|
||||
showSyn.value = true
|
||||
await wait(1500)
|
||||
|
||||
const sendSynAck = () => {
|
||||
// Step 2: SYN-ACK
|
||||
step.value = 2
|
||||
serverState.value = 'SYN_RCVD'
|
||||
}
|
||||
showSynAck.value = true
|
||||
await wait(1500)
|
||||
|
||||
const sendAck = () => {
|
||||
// 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-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-state">
|
||||
Invalid URL format / 无效的 URL 格式
|
||||
</div>
|
||||
<div v-else class="error-message">无效的 URL 格式</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
<span class="icon">💡</span>
|
||||
<strong>Note:</strong>
|
||||
URL (统一资源定位符)
|
||||
是互联网资源的地址。浏览器首先需要将它拆解成不同的部分,才能知道要去哪里(域名)、用什么方式(协议)、找什么东西(路径)。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
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,34 +1,44 @@
|
||||
<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>
|
||||
<div class="stage-display">
|
||||
<div class="header">
|
||||
<h2>{{ stages[currentStage].title }}</h2>
|
||||
<p>{{ stages[currentStage].desc }}</p>
|
||||
</div>
|
||||
<div class="viz-action">
|
||||
|
||||
<div class="component-wrapper">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component
|
||||
:is="stages[currentStage].component"
|
||||
v-if="stages[currentStage].component"
|
||||
:key="currentStage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</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; }
|
||||
|
||||
@@ -94,6 +94,10 @@ import RollbackSwitchDemo from './components/appendix/deployment/RollbackSwitchD
|
||||
import ObservabilityBackupDemo from './components/appendix/deployment/ObservabilityBackupDemo.vue'
|
||||
import CssBoxModel from './components/appendix/web-basics/CssBoxModel.vue'
|
||||
import CssFlexbox from './components/appendix/web-basics/CssFlexbox.vue'
|
||||
import CssLayoutDemo from './components/appendix/web-basics/CssLayoutDemo.vue'
|
||||
import CssPlaygroundDemo from './components/appendix/web-basics/CssPlaygroundDemo.vue'
|
||||
import CssCommonProperties from './components/appendix/web-basics/CssCommonProperties.vue'
|
||||
import CssSelectorsDemo from './components/appendix/web-basics/CssSelectorsDemo.vue'
|
||||
import DomManipulator from './components/appendix/web-basics/DomManipulator.vue'
|
||||
import SemanticTagsDemo from './components/appendix/web-basics/SemanticTagsDemo.vue'
|
||||
import DnsLookupDemo from './components/appendix/web-basics/DnsLookupDemo.vue'
|
||||
@@ -101,6 +105,16 @@ import TcpHandshakeDemo from './components/appendix/web-basics/TcpHandshakeDemo.
|
||||
import UrlParserDemo from './components/appendix/web-basics/UrlParserDemo.vue'
|
||||
import HttpExchangeDemo from './components/appendix/web-basics/HttpExchangeDemo.vue'
|
||||
import BrowserRenderingDemo from './components/appendix/web-basics/BrowserRenderingDemo.vue'
|
||||
import FrontendEvolutionDemo from './components/appendix/web-basics/FrontendEvolutionDemo.vue'
|
||||
import AiEvolutionDemo from './components/appendix/ai-history/AiEvolutionDemo.vue'
|
||||
import RuleBasedVsLearningDemo from './components/appendix/ai-history/RuleBasedVsLearningDemo.vue'
|
||||
import PerceptronDemo from './components/appendix/ai-history/PerceptronDemo.vue'
|
||||
|
||||
import ImperativeVsDeclarativeDemo from './components/appendix/web-basics/ImperativeVsDeclarativeDemo.vue'
|
||||
import ComponentReusabilityDemo from './components/appendix/web-basics/ComponentReusabilityDemo.vue'
|
||||
|
||||
import BackendEvolutionDemo from './components/appendix/backend-evolution/BackendEvolutionDemo.vue'
|
||||
import MonolithVsMicroserviceDemo from './components/appendix/backend-evolution/MonolithVsMicroserviceDemo.vue'
|
||||
|
||||
// Prompt Engineering Components
|
||||
import PromptQuickStartDemo from './components/appendix/prompt-engineering/PromptQuickStartDemo.vue'
|
||||
@@ -133,6 +147,9 @@ import SqlPlaygroundDemo from './components/appendix/database-intro/SqlPlaygroun
|
||||
// IDE Intro Components
|
||||
import VirtualVSCodeDemo from './components/appendix/ide-intro/VirtualVSCodeDemo.vue'
|
||||
import IdeArchitectureDemo from './components/appendix/ide-intro/IdeArchitectureDemo.vue'
|
||||
import AiHelpDemo from './components/appendix/ide-intro/AiHelpDemo.vue'
|
||||
import BrowserDevToolsDemo from './components/appendix/browser-devtools/BrowserDevToolsDemo.vue'
|
||||
import BrowserDevToolsLiveDemo from './components/appendix/browser-devtools/BrowserDevToolsLiveDemo.vue'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
@@ -224,6 +241,10 @@ export default {
|
||||
app.component('ObservabilityBackupDemo', ObservabilityBackupDemo)
|
||||
app.component('CssBoxModel', CssBoxModel)
|
||||
app.component('CssFlexbox', CssFlexbox)
|
||||
app.component('CssLayoutDemo', CssLayoutDemo)
|
||||
app.component('CssPlaygroundDemo', CssPlaygroundDemo)
|
||||
app.component('CssCommonProperties', CssCommonProperties)
|
||||
app.component('CssSelectorsDemo', CssSelectorsDemo)
|
||||
app.component('DomManipulator', DomManipulator)
|
||||
app.component('SemanticTagsDemo', SemanticTagsDemo)
|
||||
app.component('DnsLookupDemo', DnsLookupDemo)
|
||||
@@ -231,6 +252,16 @@ export default {
|
||||
app.component('UrlParserDemo', UrlParserDemo)
|
||||
app.component('HttpExchangeDemo', HttpExchangeDemo)
|
||||
app.component('BrowserRenderingDemo', BrowserRenderingDemo)
|
||||
app.component('FrontendEvolutionDemo', FrontendEvolutionDemo)
|
||||
app.component('AiEvolutionDemo', AiEvolutionDemo)
|
||||
app.component('RuleBasedVsLearningDemo', RuleBasedVsLearningDemo)
|
||||
app.component('PerceptronDemo', PerceptronDemo)
|
||||
|
||||
app.component('ImperativeVsDeclarativeDemo', ImperativeVsDeclarativeDemo)
|
||||
app.component('ComponentReusabilityDemo', ComponentReusabilityDemo)
|
||||
|
||||
app.component('BackendEvolutionDemo', BackendEvolutionDemo)
|
||||
app.component('MonolithVsMicroserviceDemo', MonolithVsMicroserviceDemo)
|
||||
|
||||
// Prompt Engineering Components Registration
|
||||
app.component('PromptQuickStartDemo', PromptQuickStartDemo)
|
||||
@@ -264,6 +295,9 @@ export default {
|
||||
app.component('VirtualVSCodeDemo', VirtualVSCodeDemo)
|
||||
app.component('DemoIde', VirtualVSCodeDemo) // Alias
|
||||
app.component('IdeArchitectureDemo', IdeArchitectureDemo)
|
||||
app.component('AiHelpDemo', AiHelpDemo)
|
||||
app.component('BrowserDevToolsDemo', BrowserDevToolsDemo)
|
||||
app.component('BrowserDevToolsLiveDemo', BrowserDevToolsLiveDemo)
|
||||
},
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
|
||||
@@ -69,6 +69,24 @@ const base = site.value.base
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* LOGO 容器:负责上下浮动动画 */
|
||||
.VPHomeHero .image {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
/* 给 LOGO 容器设置不可侵犯的左侧边界 */
|
||||
.VPHomeHero .image {
|
||||
margin-left: 80px !important;
|
||||
flex-shrink: 0; /* 保证图片不被挤压 */
|
||||
}
|
||||
|
||||
.VPHomeHero .tagline {
|
||||
max-width: 450px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.VPHomeHero .text {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# 人工智能进化史:从 "逻辑" 到 "直觉" (Interactive Intro)
|
||||
|
||||
> 💡 **学习指南**:本章节通过交互式演示,带你梳理人工智能 70 年的发展脉络。从最早的下棋程序,到今天能写诗作画的 ChatGPT。
|
||||
|
||||
<AiEvolutionDemo />
|
||||
|
||||
## 0. 引言:机器能思考吗?
|
||||
|
||||
图灵在 1950 年提出了这个问题。
|
||||
为了回答它,人类进行了长达半个多世纪的探索。
|
||||
|
||||
我们走过弯路(试图穷举规则),也经历过寒冬(算力不足),最终在模仿人脑(神经网络)的道路上取得了突破。
|
||||
|
||||
---
|
||||
|
||||
## 1. 符号主义:教机器"守规矩" (1950s - 1980s)
|
||||
|
||||
早期的 AI 科学家认为:智慧就是**逻辑推理**。
|
||||
只要我们把世界上的所有知识都写成 `If...Then...` 的规则,机器就能像人一样聪明。
|
||||
|
||||
这被称为**专家系统 (Expert Systems)**。
|
||||
|
||||
### 1.1 什么是"基于规则"?
|
||||
就像教小孩:
|
||||
* 如果看到红灯,就停下。
|
||||
* 如果下雨,就带伞。
|
||||
|
||||
### 1.2 交互演示:规则 vs 学习
|
||||
下方的演示展示了两种方式的区别。
|
||||
* **左边 (规则)**:你必须显式地写代码 `if (size > 6)`。如果世界变了(比如苹果变小了),你的代码就失效了。
|
||||
* **右边 (学习)**:你不需要写规则。你只需要给机器看一堆苹果和樱桃的数据,点击 **Train**,它自己会"悟"出一个分界线。
|
||||
|
||||
<RuleBasedVsLearningDemo />
|
||||
|
||||
**局限性**:你能写出"识别猫"的规则吗?
|
||||
"有胡须"?老鼠也有。"有尖耳朵"?狗也有。
|
||||
现实世界太复杂,规则写不完。这就是符号主义 AI 衰落的原因。
|
||||
|
||||
---
|
||||
|
||||
## 2. 连接主义:教机器"像人脑一样思考" (2010s+)
|
||||
|
||||
既然规则写不完,不如让机器自己学?
|
||||
科学家开始模仿人脑的结构——**神经元**。
|
||||
|
||||
### 2.1 感知机 (Perceptron)
|
||||
这是最简单的神经元模型。它接收多个输入,根据**权重 (Weight)** 加权求和,如果超过某个**阈值 (Bias)**,就激活。
|
||||
|
||||
$$ Output = \begin{cases} 1 & \text{if } \sum (w_i \cdot x_i) + b > 0 \\ 0 & \text{otherwise} \end{cases} $$
|
||||
|
||||
听起来很复杂?动手试一下!
|
||||
|
||||
### 2.2 交互演示:玩转神经元
|
||||
调整下方的 **Weights (权重)** 和 **Bias (偏置)**,看看能否控制神经元的输出。
|
||||
* **Weights ($w$)**:代表输入的"重要性"。$w$ 越大,这个输入对结果影响越大。
|
||||
* **Bias ($b$)**:代表神经元的"门槛"。$b$ 越小,神经元越容易兴奋(输出 1)。
|
||||
|
||||
<PerceptronDemo />
|
||||
|
||||
当几十亿个这样的神经元连接在一起,奇迹就发生了——这就是**深度学习 (Deep Learning)**。
|
||||
|
||||
---
|
||||
|
||||
## 3. 生成式 AI:机器有了"创造力" (2020s+)
|
||||
|
||||
以前的 AI 主要是**判别式**(这是猫还是狗?)。
|
||||
现在的 AI 是**生成式**(画一只猫!)。
|
||||
|
||||
这一切的背后,是 **Transformer** 架构的诞生。它让 AI 学会了理解上下文,学会了"举一反三"。
|
||||
|
||||
> 关于大语言模型 (LLM) 的详细原理,请移步下一章:[大语言模型入门](./llm-intro.md)
|
||||
|
||||
---
|
||||
|
||||
## 4. 总结
|
||||
|
||||
| 时代 | 核心理念 | 代表产物 | 局限 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **符号主义** | 智慧 = 规则 | 深蓝 (下棋), 医疗诊断系统 | 无法处理模糊、复杂的现实世界 |
|
||||
| **连接主义** | 智慧 = 神经网络 | AlphaGo, 人脸识别 | 需要海量数据,是个"黑盒" |
|
||||
| **生成式 AI** | 智慧 = 通用理解 | ChatGPT, Midjourney | 幻觉 (一本正经胡说八道) |
|
||||
|
||||
AI 的进化,就是从"人工设定规则"到"机器自动学习数据"的过程。
|
||||
@@ -0,0 +1,89 @@
|
||||
# 后端进化史:从 "单体" 到 "无服务" (Interactive Intro)
|
||||
|
||||
> 💡 **学习指南**:本章节无需编程基础,通过交互式演示带你回顾后端架构的 30 年变迁。我们将从最早的物理服务器讲起,一直到现代的 Serverless 云计算。
|
||||
|
||||
<BackendEvolutionDemo />
|
||||
|
||||
## 0. 引言:看不见的"大后方"
|
||||
|
||||
你点的外卖、刷的视频、发的微信,背后都有一个庞大的系统在支撑。
|
||||
这个系统就是**后端 (Backend)**。
|
||||
|
||||
如果前端是"餐厅的服务员",那后端就是"后厨"。
|
||||
为了服务越来越多的客人(用户),后厨经历了一次次痛苦的扩建和重组。
|
||||
|
||||
核心变化只有一点:**如何以最低的成本,支撑最大规模的用户?**
|
||||
|
||||
---
|
||||
|
||||
## 1. 简单粗暴:单体架构 (Monolith)
|
||||
|
||||
在 2010 年以前,绝大多数应用都是**单体**的。
|
||||
就像一个小餐馆,洗菜、切菜、炒菜都在一个大厨房里完成。
|
||||
|
||||
* **优点**:开发简单,部署方便(把一个大包扔到服务器上就行)。
|
||||
* **缺点**:牵一发而动全身。
|
||||
|
||||
### 1.1 "雪崩"效应
|
||||
想象一下,如果"切菜"的师傅不小心切到了手(代码出了 Bug),整个后厨都要停下来处理伤口,导致所有客人都吃不上饭。
|
||||
|
||||
这就是单体架构最大的风险:**隔离性差**。
|
||||
|
||||
### 1.2 交互演示:单体 vs 微服务
|
||||
下方的演示展示了两种架构在面对故障时的不同表现。
|
||||
* 点击 **"Simulate Crash"** 按钮,模拟订单模块 (Order Service) 崩溃。
|
||||
* **左边 (单体)**:订单模块崩溃导致内存溢出,**整个系统**(包括用户、支付)全部瘫痪。
|
||||
* **右边 (微服务)**:订单模块挂了,但用户还能登录,还能查看历史账单。只有"下单"功能暂时不可用。
|
||||
|
||||
<MonolithVsMicroserviceDemo />
|
||||
|
||||
**关键点**:微服务架构的核心价值,在于**故障隔离**和**独立扩展**。
|
||||
|
||||
---
|
||||
|
||||
## 2. 蚂蚁雄兵:微服务 (Microservices)
|
||||
|
||||
为了解决单体的问题,我们把大厨房拆成了很多个小厨房(服务)。
|
||||
* 专门负责用户的服务
|
||||
* 专门负责订单的服务
|
||||
* 专门负责支付的服务
|
||||
|
||||
### 2.1 容器化革命 (Docker)
|
||||
怎么管理这么多小厨房?
|
||||
Docker 就像是**集装箱**。它把每个小服务连同它的锅碗瓢盆(依赖库)一起打包。
|
||||
无论运到哪里(哪台服务器),打开集装箱就能直接开工,不用再重新安装环境。
|
||||
|
||||
---
|
||||
|
||||
## 3. 云端进化:无服务 (Serverless)
|
||||
|
||||
微服务虽然好,但维护几十个小厨房还是很累。你需要担心:
|
||||
* 厨房够不够大?(服务器扩容)
|
||||
* 停电了怎么办?(高可用)
|
||||
|
||||
### 3.1 什么是 Serverless?
|
||||
Serverless 并不是"没有服务器",而是**"你不需要管理服务器"**。
|
||||
|
||||
就像你现在不想自己做饭,也不想开饭馆,而是直接叫**外卖**。
|
||||
* 你只需要写代码(下单)。
|
||||
* 云厂商(美团)负责准备机器、运行代码、自动扩容。
|
||||
* **按次付费**:代码跑了 100 毫秒,就收 100 毫秒的钱。没人访问就不收钱。
|
||||
|
||||
### 3.2 适用场景
|
||||
Serverless 特别适合:
|
||||
* **潮汐流量**:比如外卖软件,中午流量大,半夜没人。Serverless 会自动在中午为你分配 1000 台机器,半夜缩减到 0 台。
|
||||
* **事件驱动**:比如"用户上传图片后,自动压缩图片"。
|
||||
|
||||
---
|
||||
|
||||
## 4. 总结
|
||||
|
||||
后端架构的演进,本质上是在做**加法**和**减法**:
|
||||
|
||||
| 时代 | 架构 | 开发者要做的事 | 运维要做的事 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **单体时代** | 一整块 | 写所有业务逻辑 | 维护一台大服务器 |
|
||||
| **微服务时代** | 拆分 | 关注单一业务 | 维护 K8s 集群 (很累!) |
|
||||
| **Serverless** | 函数 | 只写核心函数 | 喝茶 (云厂商全包了) |
|
||||
|
||||
未来的后端开发,将越来越像"搭积木"——你只需要关注**业务逻辑**,底层的脏活累活,全部交给云。
|
||||
@@ -0,0 +1,119 @@
|
||||
# 浏览器调试器 (DevTools) 指南
|
||||
|
||||
::: tip 💡 核心作用
|
||||
浏览器开发者工具(DevTools)是前端开发的“X光机”和“手术台”。它能让你看穿网页的骨架(HTML)、皮肤(CSS)和神经系统(JavaScript),并且允许你实时地修改和调试它们。
|
||||
:::
|
||||
|
||||
## 1. 什么是 DevTools?
|
||||
|
||||
**DevTools** 是现代浏览器(Chrome, Edge, Firefox, Safari 等)内置的一套 Web 开发和调试工具。对于开发者来说,它比代码编辑器更接近“真相”,因为**它展示的是代码在浏览器中实际运行的样子**。
|
||||
|
||||
**如何打开 DevTools?**
|
||||
- **快捷键**:`F12` 或 `Ctrl + Shift + I` (Mac: `Cmd + Option + I`)
|
||||
- **鼠标**:在网页任意元素上**右键点击**,选择 **“检查 (Inspect)”**。
|
||||
- **菜单**:浏览器右上角菜单 -> 更多工具 -> 开发者工具。
|
||||
|
||||
---
|
||||
|
||||
## 2. 交互式演示:DevTools 模拟器
|
||||
|
||||
为了让你快速上手,我们制作了一个模拟的 DevTools 面板,复刻了 Chrome 浏览器的调试界面。
|
||||
**请尝试点击下方的“▶ 开始自动导览”按钮,跟随光标了解各个区域的功能。**
|
||||
|
||||
<ClientOnly>
|
||||
<BrowserDevToolsDemo />
|
||||
</ClientOnly>
|
||||
|
||||
### 2.1 进阶演示:实时修改网页 (Live Edit)
|
||||
|
||||
DevTools 最强大的功能之一就是**实时修改**。下方的演示包含了一个“虚拟网页”(上方)和一个“DevTools”(下方)。
|
||||
|
||||
**请尝试:**
|
||||
1. 在下方的 Elements 面板中,点击 DOM 树中的 `h1` 或 `button` 元素。
|
||||
2. 在右侧的 Styles 面板中,修改 `element.style` 中的属性值(例如将 `color` 改为 `red`)。
|
||||
3. 观察上方的虚拟网页如何**实时发生变化**。
|
||||
|
||||
<ClientOnly>
|
||||
<BrowserDevToolsLiveDemo />
|
||||
</ClientOnly>
|
||||
|
||||
### 2.2 实战挑战:修改真实网页文字
|
||||
|
||||
既然你已经掌握了修改样式的技巧,现在让我们来点更刺激的——**直接修改你当前看到的网页!**
|
||||
|
||||
1. **打开真实的 DevTools**:按下 `F12`(或右键点击本行文字 -> 选择“检查”)。
|
||||
2. **定位元素**:在 Elements 面板中,你会看到一行被高亮选中的代码,那正是你刚刚点击的文字。
|
||||
3. **修改内容**:**双击** 这行代码中的黑色文字部分,将其修改为“**我是黑客!**”,然后按下回车。
|
||||
4. **见证奇迹**:看!网页上的文字是不是变了?
|
||||
|
||||
::: info 🤔 为什么刷新后就没了?
|
||||
你可能会发现,当你刷新页面后,所有的修改都消失了,网页又变回了原来的样子。
|
||||
|
||||
这是因为 DevTools 的修改仅仅发生在**你的浏览器本地内存**中。
|
||||
- 当你访问网页时,浏览器从**远程服务器**下载了 HTML 代码并在本地渲染出来。
|
||||
- 你修改的只是**本地的副本**,并没有权限去修改服务器上的**源代码**。
|
||||
- 所以每次刷新,浏览器都会重新去服务器拉取最新的(未被修改的)代码,一切就复原了。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心面板详解
|
||||
|
||||
### 3.1 Elements (元素面板)
|
||||
**作用**:查看和实时编辑页面的 HTML 和 CSS。
|
||||
- **左侧 (DOM 树)**:显示网页的 HTML 结构。你可以双击标签或文本进行修改,甚至拖拽节点改变位置。
|
||||
- **右侧 (Styles)**:显示选中元素的 CSS 样式。你可以勾选/取消样式查看变化,或者直接修改数值(如颜色、边距)。
|
||||
- **应用场景**:
|
||||
- "为什么这个按钮没有对齐?" -> 检查 CSS 样式。
|
||||
- "我想试试这个标题变成红色好看吗?" -> 直接在 Styles 里修改 `color: red`。
|
||||
|
||||
### 3.2 Console (控制台面板)
|
||||
**作用**:查看日志信息,运行 JavaScript 代码。
|
||||
- **日志输出**:网页运行时的 `console.log()` 信息、警告(黄色)和报错(红色)都会显示在这里。
|
||||
- **交互环境**:你可以在这里输入任意 JS 代码并立即执行。例如输入 `alert('Hello')` 会弹窗,输入 `document.body.style.background = 'red'` 会把背景变红。
|
||||
- **应用场景**:
|
||||
- "为什么点击按钮没反应?" -> 查看是否有红色报错信息。
|
||||
- "验证一个 JS 函数的返回值。" -> 直接在控制台运行测试。
|
||||
|
||||
### 3.3 Network (网络面板)
|
||||
**作用**:监控所有网络请求。
|
||||
- **列表视图**:显示加载的所有资源(HTML, CSS, JS, 图片, 接口请求)。
|
||||
- **交互详情**:点击任意请求行,右侧会滑出详情面板:
|
||||
- **Headers (标头)**:查看请求头、响应头(如 `Content-Type`)。
|
||||
- **Response (响应)**:查看服务器返回的原始数据(JSON、HTML 代码等)。
|
||||
- **Preview (预览)**:以更易读的格式预览响应内容。
|
||||
- **关键指标**:
|
||||
- **Status**:状态码(200 成功,404 找不到,500 服务器错误)。
|
||||
- **Type**:资源类型(fetch/xhr 代表接口请求)。
|
||||
- **Time**:加载耗时。
|
||||
- **应用场景**:
|
||||
- "接口是不是挂了?" -> 看接口请求是不是红色的 500。
|
||||
- "页面加载为什么这么慢?" -> 找哪个图片或文件加载时间最长。
|
||||
|
||||
### 3.4 Sources (源代码面板)
|
||||
**作用**:查看源代码,调试 JavaScript。
|
||||
- **断点调试**:点击行号可以设置“断点 (Breakpoint)”。当代码执行到这一行时会**暂停**,让你有机会查看当前的变量值,并单步执行代码。
|
||||
- **应用场景**:
|
||||
- "代码逻辑哪里出错了?" -> 打断点,一步步看着代码跑,看变量值是否符合预期。
|
||||
|
||||
### 3.5 Application (应用面板)
|
||||
**作用**:查看和管理浏览器存储。
|
||||
- **Storage**:
|
||||
- **Local Storage**:持久化存储的数据。
|
||||
- **Session Storage**:会话级存储(关闭标签页消失)。
|
||||
- **Cookies**:用于身份验证等的小型文本数据。
|
||||
- **应用场景**:
|
||||
- "清除登录状态" -> 删除 Cookies 或 Local Storage 中的 token。
|
||||
- "查看缓存的数据" -> 检查 Local Storage 里存了什么。
|
||||
|
||||
---
|
||||
|
||||
## 4. 实战小技巧
|
||||
|
||||
1. **手机模式调试**:点击 DevTools 左上角的“手机图标” 📱,可以模拟不同型号的手机(iPhone, Pixel 等)屏幕尺寸,测试网页的响应式效果。
|
||||
2. **强制状态**:在 Elements 面板,右键点击一个元素,选择 `Force state` -> `:hover`,可以强制让元素保持悬停状态,方便调试鼠标悬停时的样式。
|
||||
3. **截图节点**:在 Elements 面板选中一个节点,按下 `Ctrl + Shift + P` (Mac: `Cmd + Shift + P`) 打开命令菜单,输入 `screenshot`,选择 `Capture node screenshot`,可以直接把这个 DOM 节点截图保存为图片。
|
||||
|
||||
::: warning ⚠️ 注意
|
||||
DevTools 中的所有修改(修改 HTML、CSS、JS)都是**临时的**,仅在当前浏览器页面生效。一旦刷新页面,所有修改都会丢失。如果想永久生效,必须修改你的源代码文件。
|
||||
:::
|
||||
@@ -0,0 +1,66 @@
|
||||
# 前端进化史:从 "切图" 到 "工程化" (Interactive Intro)
|
||||
|
||||
> 💡 **学习指南**:本章节无需编程基础,通过交互式演示带你回顾前端开发的 20 年变迁。我们将从最基础的 HTML 讲起,一直到现代的 Vue/React 组件化开发。
|
||||
|
||||
<FrontendEvolutionDemo />
|
||||
|
||||
## 0. 引言:网页的"身份危机"
|
||||
|
||||
最早的网页,只是**电子海报**。
|
||||
现在的网页,是**桌面级应用** (如 VS Code, Figma)。
|
||||
|
||||
为了支撑这种转变,前端技术经历了一场从 "手工作坊" 到 "工业化生产" 的革命。
|
||||
|
||||
核心变化只有一点:**如何更高效地管理日益复杂的页面状态?**
|
||||
|
||||
---
|
||||
|
||||
## 1. 痛苦的根源:命令式操作 (Imperative)
|
||||
|
||||
在 jQuery 时代(2005+),我们操作网页就像是在**发号施令**。
|
||||
"嘿,浏览器!找到那个 ID 是 `msg` 的 div,把它隐藏起来!然后把那个按钮变红!"
|
||||
|
||||
这种方式直观,但随着页面变复杂,命令会打结。
|
||||
|
||||
### 1.1 什么是"命令式"?
|
||||
这就好比你要画一幅画:
|
||||
* **命令式**:你告诉画家“拿起笔,蘸红颜料,在坐标(10,10)画一个圈”。
|
||||
* **声明式**:你直接给画家一张照片,“给我画成这样”。
|
||||
|
||||
### 1.2 交互演示:命令式 vs 声明式
|
||||
下方的演示展示了两种思维的巨大差异。
|
||||
* **左边 (jQuery)**:你需要手动关注每一步 DOM 操作。忘了更新 DOM?界面就不对了。
|
||||
* **右边 (Vue)**:你只管修改数据 `count`,界面自动变。
|
||||
|
||||
<ImperativeVsDeclarativeDemo />
|
||||
|
||||
**关键点**:现代框架(Vue/React)的核心价值,就是把我们从繁琐的 DOM 操作中解放出来,专注于**数据(State)**。
|
||||
|
||||
---
|
||||
|
||||
## 2. 工业化革命:组件化 (Component)
|
||||
|
||||
解决了"怎么更新页面"的问题,接下来要解决"怎么组织代码"的问题。
|
||||
以前写网页,是一个巨大的 HTML 文件,混杂着几千行 JS 代码。改一个按钮,可能弄坏整个页面。
|
||||
|
||||
### 2.1 乐高积木思维
|
||||
现代前端把页面拆成了**组件**。
|
||||
一个按钮、一个导航栏、一个商品卡片,都是独立的积木。
|
||||
|
||||
### 2.2 组件的复用
|
||||
定义好一个"商品卡片"组件后,你可以由它生成 100 个实例。每个实例都有自己独立的状态(比如点赞状态),互不干扰。
|
||||
|
||||
<ComponentReusabilityDemo />
|
||||
|
||||
**思考**:上面的演示中,点击生成的每一个 "Counter" 或 "Card",它们的数据是独立的吗?
|
||||
是的。这就是组件化的魔力:**封装**与**复用**。
|
||||
|
||||
---
|
||||
|
||||
## 3. 总结
|
||||
|
||||
前端技术的进化,本质上是在解决两个问题:
|
||||
1. **效率**:从 手动操作 DOM -> 数据驱动 (MVVM)。
|
||||
2. **规模**:从 巨型面条代码 -> 组件化架构。
|
||||
|
||||
现在的你,正站在巨人的肩膀上,使用着最先进的生产力工具。
|
||||
@@ -1,565 +1,91 @@
|
||||
# Git 版本控制入门
|
||||
# Git 版本控制:时间的后悔药
|
||||
|
||||
> 💡 **学习指南**:本章节通过**8个交互式演示**带你从零掌握 Git。无需编程基础,我们将从日常文件备份的痛点出发,通过可视化操作一步步探索 Git 的核心概念和工作原理。
|
||||
> 💡 **一句话解释**:Git 就是代码世界的**游戏存档管理器**。它能让你随时“读档”重来,也能让你和队友在各自的“平行宇宙”里互不干扰地开发。
|
||||
|
||||
## 0. 引言:从"手动备份"到"智能版本管理"
|
||||
## 1. 为什么我们需要它?
|
||||
|
||||
### 0.1 你是否遇到过这样的困境?
|
||||
你是否经历过这种绝望?
|
||||
|
||||
想象一下,你在写一份重要的论文:
|
||||
|
||||
```
|
||||
论文_最终版.docx
|
||||
论文_最终版_v2.docx
|
||||
论文_最终版_真的最终版.docx
|
||||
论文_最终版_打死不改版.docx
|
||||
论文_最终版_最后一次修改.docx
|
||||
论文_最终版_老板说要改版.docx
|
||||
```text
|
||||
论文_初稿.doc
|
||||
论文_修改版.doc
|
||||
论文_最终版.doc
|
||||
论文_最终版_打死不改版.doc
|
||||
论文_绝对是最后一次修改版.doc
|
||||
```
|
||||
|
||||
**核心问题**:
|
||||
|
||||
1. **混乱的版本号**:你根本分不清哪个是最新的版本
|
||||
2. **无法回退**:不小心删了一段重要文字,想找回三天前的版本,但发现覆盖了
|
||||
3. **协作灾难**:和同学一起写报告,各自修改后合并成噩梦
|
||||
4. **不知道改了什么**:打开两个版本的文件对比,眼睛都看花了
|
||||
|
||||
### 0.2 Git 的解决方案
|
||||
|
||||
Git 的核心任务只有一个:**记录每一次改动,让你随时回到任何版本**。
|
||||
|
||||
本教程将通过**8个交互式演示**,让你直观理解 Git 的核心机制:
|
||||
|
||||
1. **Git工作流** - 完整的提交流程
|
||||
2. **三个区域** - 工作区、暂存区、仓库
|
||||
3. **存储机制** - 增量存储 vs 完整备份
|
||||
4. **命令操作** - 交互式命令行
|
||||
5. **分支管理** - 并行开发的魔法
|
||||
6. **冲突解决** - 协作中的挑战
|
||||
7. **Stash工作流** - 任务切换利器
|
||||
8. **远程协作** - 团队协作基础
|
||||
**Git 完美解决了三个问题**:
|
||||
1. **版本混乱**:不需要复制副本,一个文件夹搞定所有历史版本。
|
||||
2. **无法后悔**:删错了代码?一秒钟找回三天前的状态。
|
||||
3. **协作冲突**:你改了 A 文件,我改了 B 文件,Git 帮我们自动合并。
|
||||
|
||||
---
|
||||
|
||||
## 1. 快速体验:Git 是如何工作的?
|
||||
## 2. 核心概念:三个箱子
|
||||
|
||||
让我们通过第一个交互式演示,直观感受 Git 的核心工作流程:
|
||||
|
||||
<GitWorkflowDemo />
|
||||
|
||||
**动手试试**:
|
||||
|
||||
1. 点击"初始化仓库",创建你的版本库
|
||||
2. 点击"提交",记录当前的代码状态
|
||||
3. 点击"创建分支",开发新功能而不影响主线
|
||||
4. 点击"合并分支",将新功能整合到一起
|
||||
|
||||
> 💡 **观察**:每个提交都像给代码拍了一张"照片",Git 记住每一次变化,让你随时回退。
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心概念:Git 的三个区域
|
||||
|
||||
### 2.1 为什么需要三个区域?
|
||||
|
||||
传统的备份工具只有一个区域(你的文件夹),你修改文件后就直接覆盖原文件。但这样有个大问题:**你无法区分"已保存的版本"和"正在修改的版本"**。
|
||||
|
||||
Git 的聪明之处在于:它引入了**三个区域**来管理文件:
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ 工作区 │ ──▶ │ 暂存区 │ ──▶ │ 仓库 │
|
||||
│ (Working) │ add │ (Staging) │ commit│ (Repository)│
|
||||
│ │ │ │ │ │
|
||||
│ 你实际看到的 │ │ 准备提交的 │ │ 永久保存的 │
|
||||
│ 文件 │ │ 文件清单 │ │ 历史版本 │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
**类比理解**:
|
||||
|
||||
- **工作区** = 你的办公桌,你可以随意摆放、修改文件
|
||||
- **暂存区** = 准备归档的文件筐,你把要保存的文件放进去
|
||||
- **仓库** = 档案馆,永久保存所有历史版本
|
||||
|
||||
### 2.2 文件的生命周期
|
||||
|
||||
```
|
||||
未跟踪 (Untracked) → 已修改 (Modified) → 已暂存 (Staged) → 已提交 (Committed)
|
||||
↑ ↓ ↓ ↓
|
||||
新建文件 文件内容改变 添加到暂存区 保存到仓库
|
||||
```
|
||||
|
||||
### 2.3 交互式演示:三个区域的文件流动
|
||||
Git 的设计哲学其实很像**寄快递**。
|
||||
|
||||
<GitThreeAreasDemo />
|
||||
|
||||
**动手试试**:
|
||||
* **工作区 (Working Dir)**:你的**书桌**。你正在这里写代码,想怎么乱改都行。
|
||||
* **暂存区 (Staging Area)**:**快递盒**。你把写好的文件放进去(`git add`),准备打包。
|
||||
* **仓库 (Repository)**:**快递柜**。一旦你封箱寄出(`git commit`),这个版本就被永久记录下来了。
|
||||
|
||||
1. 点击"新建文件"创建一个文件到工作区
|
||||
2. 选择文件并点击"添加到暂存区"(git add)
|
||||
3. 点击"提交"将暂存区的文件保存到仓库(git commit)
|
||||
4. 观察文件状态的变化和提交历史的生成
|
||||
|
||||
> 💡 **观察**:暂存区就像一个"中间站",让你可以选择性地提交某些文件,而不是全部。这给了你更精细的控制权。
|
||||
> 🔑 **关键点**:只有提交(Commit)到仓库的内容,才是安全的。工作区里没提交的内容,丢了就真丢了。
|
||||
|
||||
---
|
||||
|
||||
## 3. 为什么不用简单的"复制粘贴"?
|
||||
## 3. 基础工作流:存档三步走
|
||||
|
||||
### 3.1 传统备份方案的问题
|
||||
日常开发中,你 90% 的时间都在重复这三个动作。
|
||||
|
||||
**问题 1:空间浪费**
|
||||
<GitWorkflowDemo />
|
||||
|
||||
```
|
||||
版本 1: 一个 100MB 的项目文件
|
||||
版本 2: 只改了 1 行代码,又要保存 100MB
|
||||
版本 3: 又改了 2 行代码,再保存 100MB
|
||||
...
|
||||
结果:10 个版本 = 1GB 空间,但实际改动只有几行代码
|
||||
```
|
||||
|
||||
**问题 2:无法快速对比**
|
||||
|
||||
你有 10 个版本的文件,想知道版本 5 和版本 8 之间改了什么:
|
||||
❌ 只能打开两个文件,肉眼逐行对比
|
||||
❌ 容易遗漏,耗时耗力
|
||||
|
||||
### 3.2 Git 的解决方案:增量存储 + 快照
|
||||
|
||||
<GitStorageDemo />
|
||||
|
||||
**动手试试**:
|
||||
|
||||
1. 切换到"完整备份"模式,观察存储空间的增长
|
||||
2. 切换到"Git 增量存储"模式,对比存储效率
|
||||
3. 点击"添加版本",模拟多次文件修改
|
||||
4. 观察存储统计和空间节省
|
||||
|
||||
> 💡 **观察**:Git 通过增量存储和内容寻址,只保存文件的变更部分。随着版本增加,空间节省越明显。
|
||||
1. **修改代码**:在工作区写写画画。
|
||||
2. **`git add`**:挑选你要保存的文件,放入暂存区。
|
||||
3. **`git commit`**:给这次修改起个名字(比如“修复了登录 Bug”),永久存档。
|
||||
|
||||
---
|
||||
|
||||
## 4. 第一次提交:Git 基础命令
|
||||
## 4. 平行宇宙:分支 (Branch)
|
||||
|
||||
### 4.1 初始化仓库
|
||||
这是 Git 最强大的功能。想象你在玩游戏,前面有个大 Boss(上线新功能),你怕打不过导致游戏结束(系统崩溃)。
|
||||
|
||||
```bash
|
||||
# 在当前目录创建 Git 仓库
|
||||
git init
|
||||
|
||||
# 输出:
|
||||
# Initialized empty Git repository in /your/project/.git/
|
||||
```
|
||||
|
||||
**发生了什么?**
|
||||
|
||||
Git 在你的项目目录下创建了一个隐藏的 `.git` 文件夹:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .git/ # Git 的所有数据都在这里
|
||||
│ ├── objects/ # 存储所有文件对象
|
||||
│ ├── refs/ # 存储分支和标签引用
|
||||
│ ├── HEAD # 指向当前分支
|
||||
│ └── config # 仓库配置
|
||||
└── your-files/ # 你的项目文件
|
||||
```
|
||||
|
||||
### 4.2 查看状态
|
||||
|
||||
```bash
|
||||
# 查看当前仓库状态
|
||||
git status
|
||||
```
|
||||
|
||||
### 4.3 添加文件到暂存区
|
||||
|
||||
```bash
|
||||
# 添加单个文件
|
||||
git add hello.txt
|
||||
|
||||
# 添加所有文件
|
||||
git add .
|
||||
```
|
||||
|
||||
### 4.4 提交到仓库
|
||||
|
||||
```bash
|
||||
# 提交并添加说明
|
||||
git commit -m "第一次提交:添加问候文件"
|
||||
```
|
||||
|
||||
### 4.5 交互式演示:命令行实战
|
||||
|
||||
<GitCommandDemo />
|
||||
|
||||
**动手试试**:
|
||||
|
||||
1. 查看当前状态:`git status`
|
||||
2. 添加文件到暂存区:`git add hello.txt`
|
||||
3. 提交更改:`git commit -m "添加问候文件"`
|
||||
4. 查看提交历史:`git log --oneline`
|
||||
|
||||
> 💡 **观察**:通过真实的命令行操作,理解 Git 的工作流程。记住:工作区 → add → 暂存区 → commit → 仓库
|
||||
|
||||
---
|
||||
|
||||
## 5. 分支管理:并行开发的魔法
|
||||
|
||||
### 5.1 什么是分支?
|
||||
|
||||
**传统开发的问题**:
|
||||
|
||||
你想开发一个新功能,但不想影响当前稳定的代码,怎么办?
|
||||
|
||||
**传统方案**:
|
||||
|
||||
```
|
||||
❌ 复制整个项目文件夹
|
||||
project/ # 当前版本
|
||||
project-feature/ # 开发新功能
|
||||
```
|
||||
|
||||
**问题**:
|
||||
|
||||
- 浪费空间(两个完整副本)
|
||||
- 两个版本容易混淆
|
||||
- 合并时困难
|
||||
|
||||
**Git 的解决方案:分支**
|
||||
|
||||
Git 的分支**不是复制整个项目**,而是:
|
||||
|
||||
- 只保存当前版本的"指针"
|
||||
- 分支创建是瞬间完成的
|
||||
- 分支间切换极其快速
|
||||
|
||||
**类比**:
|
||||
|
||||
> 📚 **分支就像书的"草稿"**:
|
||||
>
|
||||
> > - 正式内容在 main 分支(可以出版)
|
||||
> > - 新想法在 feature 分支(自由修改)
|
||||
> > - 修改完成后,合并到 main 分支
|
||||
|
||||
### 5.2 分支的基本操作
|
||||
|
||||
**创建和切换分支**:
|
||||
|
||||
```bash
|
||||
# 查看所有分支
|
||||
git branch
|
||||
|
||||
# 创建新分支
|
||||
git branch feature-login
|
||||
|
||||
# 切换到新分支
|
||||
git checkout feature-login
|
||||
|
||||
# 创建并切换(一步完成)
|
||||
git checkout -b feature-login
|
||||
|
||||
# 或使用新的 switch 命令(推荐)
|
||||
git switch -c feature-login
|
||||
```
|
||||
|
||||
### 5.3 合并分支:整合你的工作
|
||||
这时候,你可以开一个**分支 (Branch)**,相当于**复制了一个平行世界**。
|
||||
|
||||
<GitBranchMergeDemo />
|
||||
|
||||
**动手试试**:
|
||||
* **主分支 (Main/Master)**:稳定的线上版本,只有测试通过的代码才能进来。
|
||||
* **开发分支 (Feature)**:你的试验田。你在这里炸了地球也没关系,不会影响主分支。
|
||||
* **合并 (Merge)**:你在试验田里测试成功了,就把改动“合并”回主分支。
|
||||
|
||||
1. 点击"初始化仓库"创建一个 Git 仓库
|
||||
2. 在 main 分支上提交几次
|
||||
3. 点击"创建分支"开发新功能
|
||||
4. 在 feature 分支上提交几次
|
||||
5. 点击"切换分支"回到 main 分支
|
||||
6. 点击"合并分支"将 feature 合并到 main
|
||||
---
|
||||
|
||||
> 💡 **观察**:注意观察分支历史图,每个分支都有独立的提交线,合并时会创建一个新的合并提交。
|
||||
## 5. 常用命令速查
|
||||
|
||||
### 5.4 冲突解决:协作的挑战
|
||||
| 命令 | 作用 | 人话解释 |
|
||||
| :--- | :--- | :--- |
|
||||
| `git init` | 初始化 | "我要在这里建个新仓库" |
|
||||
| `git status` | 查看状态 | "现在书桌上乱不乱?有没有东西没装箱?" |
|
||||
| `git add .` | 添加所有 | "把桌上所有文件都扔进快递盒" |
|
||||
| `git commit -m "..."` | 提交 | "封箱!贴上标签,写上这次改了啥" |
|
||||
| `git log` | 查看历史 | "翻翻以前的日记" |
|
||||
| `git checkout -b dev` |以此创建新分支 | "我要去平行宇宙 dev 探险了" |
|
||||
| `git checkout main` | 切换分支 | "回地球(主分支)看看" |
|
||||
| `git merge dev` | 合并分支 | "把平行宇宙的成果带回地球" |
|
||||
|
||||
**什么是冲突?**
|
||||
---
|
||||
|
||||
当两个分支修改了同一文件的同一行,Git 无法自动决定保留哪个版本。
|
||||
## 6. 进阶:解决冲突与远程协作
|
||||
|
||||
当你和队友同时修改了同一个文件的同一行代码,Git 就会懵逼:“我该听谁的?” 这就是**冲突 (Conflict)**。
|
||||
|
||||
<GitConflictDemo />
|
||||
|
||||
**动手试试**:
|
||||
此时你需要手动打开文件,保留需要的代码,删除 Git 自动生成的 `<<<<<<<` 标记,然后重新提交。
|
||||
|
||||
1. 查看"冲突场景",理解冲突产生的原因
|
||||
2. 点击"模拟产生冲突",查看冲突提示
|
||||
3. 在"冲突解决编辑器"中选择解决方案
|
||||
4. 应用解决方案,完成合并
|
||||
|
||||
> 💡 **观察**:冲突并不可怕,它是协作的正常现象。关键是要仔细查看冲突内容,与团队沟通后决定保留哪个版本。
|
||||
|
||||
---
|
||||
|
||||
## 6. 实用技巧:Git 的高级用法
|
||||
|
||||
### 6.1 撤销操作:时间旅行
|
||||
|
||||
```bash
|
||||
# 恢复文件到最近一次提交的状态
|
||||
git restore file.txt
|
||||
|
||||
# 撤销最后一次提交(保留修改)
|
||||
git reset --soft HEAD~1
|
||||
|
||||
# 回到某个历史版本
|
||||
git reset --hard <commit-hash>
|
||||
```
|
||||
|
||||
### 6.2 暂存工作:切换任务的利器
|
||||
|
||||
**场景**:你正在开发功能 A,突然需要紧急修复 bug B,但功能 A 还没完成。
|
||||
|
||||
**解决方案**:使用 `git stash`
|
||||
|
||||
<GitStashDemo />
|
||||
|
||||
**动手试试**:
|
||||
|
||||
1. 在功能分支上做些修改(点击"做些修改")
|
||||
2. 突然需要修复bug(场景会自动提示)
|
||||
3. 点击"保存工作现场"(git stash)
|
||||
4. 切换分支修复bug并提交
|
||||
5. 切回功能分支,点击"恢复工作现场"
|
||||
|
||||
> 💡 **观察**:stash 让你在不同任务间快速切换,而不需要提交未完成的工作。它使用栈结构,后进先出。
|
||||
|
||||
### 6.3 查看差异:代码审计
|
||||
|
||||
```bash
|
||||
# 查看工作区修改(未暂存)
|
||||
git diff
|
||||
|
||||
# 查看暂存区修改(已暂存)
|
||||
git diff --staged
|
||||
|
||||
# 查看两次提交之间的差异
|
||||
git diff <commit1> <commit2>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 远程仓库:团队协作的基础
|
||||
|
||||
### 7.1 什么是远程仓库?
|
||||
|
||||
**本地仓库** = 你电脑上的 `.git` 文件夹
|
||||
**远程仓库** = 存储在服务器上的仓库副本(如 GitHub、GitLab)
|
||||
|
||||
**为什么需要远程仓库?**
|
||||
|
||||
- 🔄 **备份**:代码保存在云端,不怕硬盘损坏
|
||||
- 👥 **协作**:团队成员可以共享代码
|
||||
- 🏠 **部署**:自动部署到生产环境
|
||||
|
||||
### 7.2 推送到远程
|
||||
|
||||
```bash
|
||||
# 首次推送(设置上游分支)
|
||||
git push -u origin main
|
||||
|
||||
# 后续推送
|
||||
git push
|
||||
```
|
||||
|
||||
### 7.3 从远程拉取
|
||||
|
||||
```bash
|
||||
# 拉取远程更新并合并
|
||||
git pull origin main
|
||||
|
||||
# 等价于:
|
||||
git fetch origin main # 从远程获取更新
|
||||
git merge origin/main # 合并到本地
|
||||
```
|
||||
|
||||
### 7.4 交互式演示:远程仓库协作
|
||||
至于**远程仓库 (Remote)**(比如 GitHub/GitLab),它就是云端的备份中心。
|
||||
* `git push`:把本地存档上传到云端。
|
||||
* `git pull`:把云端最新的存档拉取到本地。
|
||||
|
||||
<GitRemoteDemo />
|
||||
|
||||
**动手试试**:
|
||||
|
||||
1. 点击"本地提交",在本地创建一些提交
|
||||
2. 点击"推送到远程",将本地提交同步到远程
|
||||
3. 点击"模拟团队更新",让团队成员推送代码
|
||||
4. 点击"拉取远程更新",获取团队的新代码
|
||||
|
||||
> 💡 **观察**:远程协作的核心是"先拉取,再推送",确保你的代码基于最新版本,避免冲突。
|
||||
|
||||
---
|
||||
|
||||
## 8. 常见问题与解决方案
|
||||
|
||||
### 8.1 忘记推送某文件
|
||||
|
||||
**问题**:提交后发现忘了添加某个文件。
|
||||
|
||||
```bash
|
||||
# 添加遗漏的文件
|
||||
git add forgotten-file.txt
|
||||
|
||||
# 修改最后一次提交
|
||||
git commit --amend
|
||||
|
||||
# 如果已推送,需要强制推送(谨慎!)
|
||||
git push -f origin branch
|
||||
```
|
||||
|
||||
### 8.2 提交信息写错了
|
||||
|
||||
```bash
|
||||
# 修改最后一次提交信息
|
||||
git commit --amend -m "正确的提交信息"
|
||||
```
|
||||
|
||||
### 8.3 推送被拒绝
|
||||
|
||||
**问题**:本地和远程历史不一致。
|
||||
|
||||
```bash
|
||||
# 方法 1:先拉取再推送(推荐)
|
||||
git pull --rebase origin main
|
||||
git push origin main
|
||||
|
||||
# 方法 2:强制推送(会覆盖远程,危险!)
|
||||
git push -f origin main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Git 配置与最佳实践
|
||||
|
||||
### 9.1 基本配置
|
||||
|
||||
```bash
|
||||
# 设置用户名(必填)
|
||||
git config --global user.name "Your Name"
|
||||
|
||||
# 设置邮箱(必填)
|
||||
git config --global user.email "your.email@example.com"
|
||||
|
||||
# 设置默认分支名
|
||||
git config --global init.defaultBranch main
|
||||
```
|
||||
|
||||
### 9.2 忽略文件
|
||||
|
||||
创建 `.gitignore` 文件:
|
||||
|
||||
```bash
|
||||
# 忽略文件
|
||||
*.log
|
||||
*.tmp
|
||||
.env
|
||||
.DS_Store
|
||||
|
||||
# 忽略文件夹
|
||||
node_modules/
|
||||
dist/
|
||||
.cache/
|
||||
```
|
||||
|
||||
### 9.3 提交规范
|
||||
|
||||
**好的提交信息**:
|
||||
|
||||
```bash
|
||||
# 格式:
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
# 示例:
|
||||
feat(auth): 添加用户登录功能
|
||||
fix(login): 修复密码验证错误
|
||||
docs(readme): 更新安装说明
|
||||
```
|
||||
|
||||
**类型(type)**:
|
||||
|
||||
- `feat`:新功能
|
||||
- `fix`:修复 bug
|
||||
- `docs`:文档更新
|
||||
- `style`:格式调整
|
||||
- `refactor`:重构
|
||||
- `test`:添加测试
|
||||
- `chore`:构建/工具
|
||||
|
||||
---
|
||||
|
||||
## 10. 学习资源与工具
|
||||
|
||||
### 10.1 可视化工具
|
||||
|
||||
**图形化界面(GUI)**:
|
||||
|
||||
- **GitHub Desktop**:免费,简洁易用
|
||||
- **SourceTree**:免费,功能强大
|
||||
- **GitKraken**:免费,跨平台
|
||||
- **TortoiseGit**:Windows 集成
|
||||
|
||||
**在线学习**:
|
||||
|
||||
- **Learn Git Branching**:https://learngitbranching.js.org/(强烈推荐!)
|
||||
- **Git Immersion**:http://gitimmersion.com/
|
||||
|
||||
### 10.2 高级主题(进阶学习)
|
||||
|
||||
- **Git 内部原理**:对象模型、引用、包文件
|
||||
- **Rebase 交互**:整理提交历史
|
||||
- **Git Hooks**:自动化工作流
|
||||
- **Submodule**:子模块管理
|
||||
|
||||
---
|
||||
|
||||
## 11. 总结
|
||||
|
||||
### Git 核心要点
|
||||
|
||||
通过**8个交互式演示**,我们学习了:
|
||||
|
||||
1. ✅ **三个区域**:工作区 → 暂存区 → 仓库
|
||||
2. ✅ **存储机制**:增量存储节省空间
|
||||
3. ✅ **命令操作**:add、commit、log
|
||||
4. ✅ **分支管理**:并行开发,互不干扰
|
||||
5. ✅ **冲突解决**:协作中的挑战与应对
|
||||
6. ✅ **Stash工作流**:任务切换利器
|
||||
7. ✅ **远程协作**:push、pull、团队同步
|
||||
8. ✅ **工作流程**:完整的 Git 使用流程
|
||||
|
||||
### 学习建议
|
||||
|
||||
- ✅ **多动手实践**:通过交互式演示熟悉操作
|
||||
- ✅ **理解原理**:Git 的三个区域、快照机制
|
||||
- ✅ **查看历史**:`git log` 了解项目演进
|
||||
- ✅ **不要害怕冲突**:冲突是协作的常态
|
||||
- ✅ **使用工具**:GUI 工具可以降低学习曲线
|
||||
|
||||
### 进阶路径
|
||||
|
||||
```
|
||||
初级: add、commit、pull、push
|
||||
↓
|
||||
中级:分支、合并、冲突解决、stash
|
||||
↓
|
||||
高级:rebase、交互式 rebase、cherry-pick
|
||||
↓
|
||||
专家:Git 内部原理、自定义脚本、性能优化
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**下一步行动**:
|
||||
|
||||
1. ✅ 通过本章节的**8个交互式演示**熟悉 Git 操作
|
||||
2. ✅ 创建一个 Git 仓库:`git init`
|
||||
3. ✅ 练习基础命令:add、commit、log
|
||||
4. ✅ 尝试分支管理:创建、切换、合并
|
||||
5. ✅ 注册 GitHub 账号,推送你的第一个远程仓库
|
||||
|
||||
掌握 Git,你就掌握了软件开发的基础设施。现在就开始使用 Git 管理你的代码吧!💪
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
# 集成开发环境 (IDE) 基础
|
||||
|
||||
> 💡 **学习指南**:本章节将带你深入了解程序员的核心生产力工具——**集成开发环境 (IDE)**。我们将从 IDE 的设计理念出发,逐一解析其核心组件,并通过虚拟 IDE 演示其工作原理。
|
||||
::: tip 💡 学习指南
|
||||
本章节将带你深入了解程序员的核心生产力工具——**集成开发环境 (IDE)**。我们将从 IDE 的设计理念出发,逐一解析其核心组件,并通过虚拟 IDE 演示其工作原理。
|
||||
:::
|
||||
|
||||
## 遇到不懂的怎么办?(How to solve problems)
|
||||
|
||||
在学习和使用 IDE 的过程中,你可能会遇到各种看不懂的按钮、菜单或者代码报错。这时候,**不要慌张,利用 AI 助手是最高效的解决办法**。
|
||||
|
||||
**推荐做法:截图问 AI**
|
||||
|
||||
现在的 AI(如 ChatGPT、Claude、DeepSeek 等)都具备强大的识图能力。当你遇到不认识的界面元素或复杂的代码片段时:
|
||||
|
||||
1. **截图**:截取你不懂的那一部分(比如某个奇怪的图标,或者一段复杂的配置代码)。
|
||||
2. **提问**:把图片发给 AI,并问它:“这个是什么?有什么用?”或者“这段代码里的 xxx 是干嘛的?”。
|
||||
3. **追问**:如果 AI 的回答太专业看不懂,继续问:“请用大白话解释一下,最好举个生活中的例子。”
|
||||
|
||||
<AiHelpDemo />
|
||||
|
||||
---
|
||||
|
||||
## 0. 引言:为什么需要 IDE?
|
||||
|
||||
@@ -121,43 +139,46 @@ Python 解释器(或编译器)执行完代码,将结果(或错误信息
|
||||
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
onMounted(() => {
|
||||
const openTarget = () => {
|
||||
const hash = window.location.hash
|
||||
if (hash) {
|
||||
try {
|
||||
// Handle encoded Chinese characters in hash
|
||||
const target = document.querySelector(decodeURIComponent(hash))
|
||||
// If the target is a details element, open it
|
||||
if (target && target.tagName === 'DETAILS') {
|
||||
target.setAttribute('open', '')
|
||||
}
|
||||
// If the target is inside a details element, open the parent details
|
||||
const parentDetails = target?.closest('details')
|
||||
if (parentDetails) {
|
||||
parentDetails.setAttribute('open', '')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
openTarget()
|
||||
window.addEventListener('hashchange', openTarget)
|
||||
})
|
||||
</script>
|
||||
|
||||
# 附录: Visual Studio Code 菜单栏解析
|
||||
|
||||
<el-card id="appendix-2-map" shadow="hover" style="margin-top: 40px; margin-bottom: 20px; border-left: 5px solid #67C23A;">
|
||||
<div style="font-weight: bold; margin-bottom: 10px;">🧭 界面导航:VS Code 核心区域</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
|
||||
<a href="#vscode-title-bar" style="text-decoration: none;"><el-tag effect="plain" style="cursor: pointer;">Title Bar (标题栏)</el-tag></a>
|
||||
<a href="#vscode-activity-bar" style="text-decoration: none;"><el-tag effect="plain" style="cursor: pointer;">Activity Bar (活动栏)</el-tag></a>
|
||||
<a href="#vscode-side-bar" style="text-decoration: none;"><el-tag effect="plain" style="cursor: pointer;">Side Bar (侧边栏)</el-tag></a>
|
||||
<a href="#vscode-editor" style="text-decoration: none;"><el-tag effect="plain" style="cursor: pointer;">Editor (编辑区)</el-tag></a>
|
||||
<a href="#vscode-panel" style="text-decoration: none;"><el-tag effect="plain" style="cursor: pointer;">Panel (底部面板)</el-tag></a>
|
||||
<a href="#vscode-status-bar" style="text-decoration: none;"><el-tag effect="plain" style="cursor: pointer;">Status Bar (状态栏)</el-tag></a>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
## <span id="vscode-title-bar">[Title Bar(标题栏):窗口信息与全局入口](#appendix-2-map)</span>
|
||||
|
||||
标题栏位于窗口最上方,主要用于展示当前窗口信息并提供窗口级控制。常见细节包括:
|
||||
|
||||
- 应用与窗口信息
|
||||
通常显示应用名称(如 Visual Studio Code)以及当前工作区(workspace)或当前打开文件的名称,便于在多窗口并行时识别不同项目。
|
||||
|
||||
- 菜单入口(部分系统/布局可见)
|
||||
在 Windows/Linux 的常见布局中,`File / Edit / Selection / View / Go / Run / Terminal / Help` 等菜单可能与标题栏同一行或紧邻显示,是功能的传统入口。
|
||||
|
||||
- 窗口控制按钮
|
||||
最小化、最大化/还原、关闭按钮属于操作系统窗口控件,用于调整窗口显示方式或关闭窗口。
|
||||
|
||||
- AI 侧边栏开启按钮
|
||||
一般而言,在右上角可以控制是否开启 AI 侧边栏或者其他侧边栏。我们默认右侧的侧边栏为 AI 侧边栏。
|
||||
|
||||
- 环境与状态提示或系统更新提示(可能会出现提醒你重启更新的提示,建议每次看到主动进行点击更新)
|
||||
部分情况下会显示远程连接状态(SSH/容器/WSL)、信任提示(Workspace Trust)等,具体取决于当前环境与设置。
|
||||
|
||||
为了方便大家理解每个选项的含义,在这里我们对菜单栏进行深入解析:
|
||||
|
||||

|
||||
|
||||
### File(文件):项目与文件的打开/保存/工作区管理
|
||||

|
||||
|
||||
<details class="custom-block details" id="vscode-file-menu">
|
||||
<summary>File(文件):项目与文件的打开/保存/工作区管理</summary>
|
||||
|
||||
本菜单主要负责:**创建/打开文件**、**打开项目文件夹(Folder)**、**管理工作区(Workspace)**、**保存与关闭**。
|
||||
|
||||
@@ -185,7 +206,10 @@ Python 解释器(或编译器)执行完代码,将结果(或错误信息
|
||||
- **Close Folder(关闭文件夹)**:关闭当前项目文件夹(工作区变为空)。
|
||||
- **Close Window(关闭窗口)**:关闭当前 VS Code 窗口。
|
||||
|
||||
### Edit(编辑):基础编辑、查找替换、注释与快速编辑动作
|
||||
</details>
|
||||
|
||||
<details class="custom-block details" id="vscode-edit-menu">
|
||||
<summary>Edit(编辑):基础编辑、查找替换、注释与快速编辑动作</summary>
|
||||
|
||||
本菜单主要负责:**撤销/重做**、**剪切复制粘贴**、**查找替换**、**注释与编辑器动作**(提升编辑效率)。
|
||||
|
||||
@@ -197,7 +221,10 @@ Python 解释器(或编译器)执行完代码,将结果(或错误信息
|
||||
- **Toggle Block Comment(切换块注释)**:`Shift + Alt + A`,快速注释/取消注释选区。
|
||||
- **Emmet: Expand Abbreviation(Emmet 展开)**:HTML/CSS 开发神器,输入简写按 Tab 展开代码。
|
||||
|
||||
### Selection(选择):多光标与智能选区
|
||||
</details>
|
||||
|
||||
<details class="custom-block details" id="vscode-selection-menu">
|
||||
<summary>Selection(选择):多光标与智能选区</summary>
|
||||
|
||||
本菜单主要负责:**光标控制**、**多行编辑**、**扩大/缩小选区**。这是 VS Code 提升效率的杀手锏。
|
||||
|
||||
@@ -208,7 +235,10 @@ Python 解释器(或编译器)执行完代码,将结果(或错误信息
|
||||
- **Add Cursor Above / Below(在上方 / 下方添加光标)**:`Ctrl + Alt + ↑ / ↓`,开启多光标模式,同时编辑多行。
|
||||
- **Add Cursor to Line Ends(在行尾添加光标)**:选中多行文本后,在每一行末尾添加光标。
|
||||
|
||||
### View(查看):界面布局与面板控制
|
||||
</details>
|
||||
|
||||
<details class="custom-block details" id="vscode-view-menu">
|
||||
<summary>View(查看):界面布局与面板控制</summary>
|
||||
|
||||
本菜单主要负责:**开关侧边栏/面板**、**调整布局**、**命令面板**、**输出与调试控制台**。
|
||||
|
||||
@@ -220,7 +250,10 @@ Python 解释器(或编译器)执行完代码,将结果(或错误信息
|
||||
- **Problems / Output / Debug Console / Terminal**:直接控制底部面板(Panel)的显示内容。
|
||||
- **Word Wrap(自动换行)**:`Alt + Z`,控制长行代码是否自动换行显示(不影响实际文件内容)。
|
||||
|
||||
### Go(转到):代码导航与跳转
|
||||
</details>
|
||||
|
||||
<details class="custom-block details" id="vscode-go-menu">
|
||||
<summary>Go(转到):代码导航与跳转</summary>
|
||||
|
||||
本菜单主要负责:**在文件间跳转**、**在符号(函数/变量)间跳转**。
|
||||
|
||||
@@ -232,7 +265,10 @@ Python 解释器(或编译器)执行完代码,将结果(或错误信息
|
||||
- **Go to References(转到引用)**:`Shift + F12`,查看该变量或函数在哪些地方被使用了。
|
||||
- **Go to Line/Column…(转到行/列…)**:`Ctrl + G`,跳转到指定行号。
|
||||
|
||||
### Run(运行):调试与执行
|
||||
</details>
|
||||
|
||||
<details class="custom-block details" id="vscode-run-menu">
|
||||
<summary>Run(运行):调试与执行</summary>
|
||||
|
||||
本菜单主要负责:**启动调试**、**断点管理**。
|
||||
|
||||
@@ -243,7 +279,10 @@ Python 解释器(或编译器)执行完代码,将结果(或错误信息
|
||||
- **Toggle Breakpoint(切换断点)**:`F9`,在当前行打上或取消红点(断点)。
|
||||
- **New Breakpoint(新建断点)**:支持条件断点、日志断点等高级功能。
|
||||
|
||||
### Terminal(终端):集成命令行
|
||||
</details>
|
||||
|
||||
<details class="custom-block details" id="vscode-terminal-menu">
|
||||
<summary>Terminal(终端):集成命令行</summary>
|
||||
|
||||
本菜单主要负责:**新建终端**、**管理终端窗口**。
|
||||
|
||||
@@ -251,7 +290,10 @@ Python 解释器(或编译器)执行完代码,将结果(或错误信息
|
||||
- **Split Terminal(拆分终端)**:在同一个终端面板中左右/上下拆分,同时运行多个命令。
|
||||
- **Run Task…(运行任务…)**:运行 `tasks.json` 中定义的构建/测试任务。
|
||||
|
||||
### Help(帮助):文档与反馈
|
||||
</details>
|
||||
|
||||
<details class="custom-block details" id="vscode-help-menu">
|
||||
<summary>Help(帮助):文档与反馈</summary>
|
||||
|
||||
- **Welcome(欢迎)**:打开欢迎页(包含入门引导、最近项目)。
|
||||
- **Show All Commands(显示所有命令)**:同命令面板。
|
||||
@@ -259,3 +301,5 @@ Python 解释器(或编译器)执行完代码,将结果(或错误信息
|
||||
- **Editor Playground(编辑器演练场)**:交互式教程,学习编辑技巧。
|
||||
- **Check for Updates…(检查更新…)**:手动检查更新。
|
||||
- **About(关于)**:查看版本号、构建时间、Electron/Node 版本信息。
|
||||
|
||||
</details>
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
# 从 URL 输入到浏览器显示 (From URL to Browser)
|
||||
|
||||
> 💡 **学习指南**:本章节无需网络工程基础,通过交互式演示带你深入了解浏览器如何将一行网址变成丰富多彩的页面。我们将从输入 URL 开始,一步步拆解背后的网络请求、连接建立和页面渲染过程。
|
||||
> 💡 **学习指南**:本章节通过交互式演示,带你深入了解浏览器如何将一行网址变成丰富多彩的页面。我们将从输入 URL 开始,一步步拆解背后的网络请求、连接建立和页面渲染过程。
|
||||
|
||||
## 0. 引言:一次神奇的旅行
|
||||
## 0. 全景图:一次神奇的旅行
|
||||
|
||||
当你在浏览器地址栏输入一个网址并按下回车,短短几秒钟内,背后发生了一系列复杂而精妙的过程。这就像是一次跨越万水千山的旅行。
|
||||
|
||||
它的核心任务只有一个:**将你想要的资源(网页)准确无误地从世界的另一端搬运到你的屏幕上**。
|
||||
|
||||
为了实现这个目标,我们需要解决五个核心挑战:
|
||||
|
||||
1. **寻址 (Addressing)**:如何告诉浏览器我要去哪里?(URL)
|
||||
2. **定位 (Discovery)**:服务器的真实地址是什么?(DNS)
|
||||
3. **连接 (Connection)**:如何建立一条可靠的通路?(TCP/TLS)
|
||||
4. **交流 (Exchange)**:如何用对方听得懂的语言沟通?(HTTP)
|
||||
5. **展示 (Rendering)**:如何将枯燥的代码变成漂亮的画面?(Rendering)
|
||||
我们可以把这个过程分为五个关键阶段,点击下方的步骤来预览整个流程:
|
||||
|
||||
<UrlToBrowserDemo />
|
||||
|
||||
@@ -22,75 +16,63 @@
|
||||
|
||||
## 1. 第一步:寻址 (URL Parsing)
|
||||
|
||||
### 1.1 计算机不知道"去哪里"
|
||||
### 1.1 计算机的"导航地址"
|
||||
|
||||
如果你告诉出租车司机"去那个有很多好吃的商场",他可能会一头雾水。计算机也是一样,它需要一个精确的地址格式。
|
||||
计算机需要一个精确的地址格式才能找到资源。这就是 **URL (统一资源定位符)** 的作用。它不仅告诉浏览器**去哪里**(域名),还告诉它**怎么去**(协议),以及**找什么**(路径)。
|
||||
|
||||
### 1.2 解决方案:URL (统一资源定位符)
|
||||
|
||||
**URL (Uniform Resource Locator)** 是互联网上的标准地址格式。它不仅告诉浏览器**去哪里**(域名),还告诉它**怎么去**(协议),以及**找什么**(路径)。
|
||||
|
||||
**URL 的解剖学**:
|
||||
试着在下方的模拟地址栏中输入不同的 URL,看看它是如何被拆解的:
|
||||
|
||||
<UrlParserDemo />
|
||||
|
||||
**关键部分解析**:
|
||||
|
||||
- **协议 (Protocol)**:`https` 或 `http`。就像告诉司机是"坐飞机"还是"坐火车"。
|
||||
- **域名 (Host)**:`www.example.com`。目的地的名字,方便人类记忆。
|
||||
- **路径 (Path)**:`/path/to/page`。资源在服务器上的具体位置,就像"3楼302室"。
|
||||
- **参数 (Query)**:`?q=vue`。给服务器的附加指令,就像"我要微辣"。
|
||||
- **Protocol (协议)**:通常是 `https` (安全) 或 `http`。就像告诉司机是"坐飞机"还是"坐火车"。
|
||||
- **Host (域名)**:`www.example.com`。目的地的名字,方便人类记忆。
|
||||
- **Port (端口)**:服务器的"门牌号"。Web 服务默认是 80 (HTTP) 或 443 (HTTPS),通常省略不写。
|
||||
- **Path (路径)**:`/path/to/page`。资源在服务器文件系统中的具体位置。
|
||||
- **Query (参数)**:`?q=vue`。给服务器的附加指令,就像点餐时的备注"不要香菜"。
|
||||
|
||||
---
|
||||
|
||||
## 2. 第二步:定位 (DNS Lookup)
|
||||
|
||||
### 2.1 为什么不用 IP 地址?
|
||||
### 2.1 互联网的"电话簿"
|
||||
|
||||
计算机之间通信真正识别的是 **IP 地址**(如 `142.250.185.238`),而不是域名(如 `google.com`)。
|
||||
虽然我们记住了 `google.com` 这样的域名,但计算机之间通信真正识别的是 **IP 地址**(如 `142.250.185.238`)。
|
||||
|
||||
- **缺点1**:IP 地址是一串枯燥的数字,人类很难记忆。
|
||||
- **缺点2**:服务器搬家后 IP 会变,但我们希望域名保持不变。
|
||||
**DNS (Domain Name System)** 就是互联网的"电话簿"或"导航系统"。当你输入域名时,浏览器需要先通过 DNS 找到它对应的 IP 地址。
|
||||
|
||||
### 2.2 解决方案:DNS (域名系统)
|
||||
|
||||
**DNS (Domain Name System)** 就像互联网的"电话簿"。当你呼叫 `google.com` 时,DNS 会帮你查找它对应的电话号码(IP 地址)。
|
||||
|
||||
这个查找过程是**分级**且**递归**的,就像你问村长,村长问镇长,镇长问市长...
|
||||
点击下方的 **"Go"** 按钮,观察 DNS 的**递归查询**过程:
|
||||
|
||||
<DnsLookupDemo />
|
||||
|
||||
**查询流程**:
|
||||
**查询流程解析**:
|
||||
|
||||
1. **浏览器缓存**:先看看口袋里有没有小纸条(最快)。
|
||||
2. **系统缓存**:看看操作系统的记事本 (`/etc/hosts`)。
|
||||
3. **递归查询**:如果都没有,就让 DNS 服务器去问根域名服务器、顶级域名服务器,直到找到答案。
|
||||
1. **浏览器缓存/Hosts**:先看看自己是否最近去过(缓存),或者本地小本本上有没有记(Hosts 文件)。
|
||||
2. **递归解析器 (Recursive Resolver)**:通常由你的 ISP (运营商) 提供。它像个尽职的跑腿员,负责帮你跑完剩下的路。
|
||||
3. **根域名服务器 (Root Server)**:它是 DNS 层级的顶端(`.`)。它不知道具体地址,但知道谁管 `.com`。
|
||||
4. **顶级域名服务器 (TLD Server)**:管理 `.com`、`.org` 等后缀的服务器。它会告诉你 `google.com` 归谁管。
|
||||
5. **权威域名服务器 (Authoritative Server)**:最终的管理者,它知道 `www.google.com` 的确切 IP 地址。
|
||||
|
||||
---
|
||||
|
||||
## 3. 第三步:连接 (TCP Connection)
|
||||
## 3. 第三步:连接 (TCP Handshake)
|
||||
|
||||
### 3.1 为什么不能直接发数据?
|
||||
### 3.1 建立可靠的通路
|
||||
|
||||
互联网是一个充满不确定性的环境。如果你直接扔出一个数据包:
|
||||
拿到 IP 地址后,浏览器找到了服务器。但在传输数据之前,它们必须建立一条可靠的连接,确保双方都能"听得到"且"说得出"。
|
||||
|
||||
- ❌ 它可能在路上丢了。
|
||||
- ❌ 它可能顺序乱了。
|
||||
- ❌ 它可能坏了。
|
||||
这就是著名的 **TCP 三次握手 (Three-Way Handshake)**。
|
||||
|
||||
### 3.2 解决方案:TCP 三次握手
|
||||
|
||||
**TCP (Transmission Control Protocol)** 是一种**可靠**的传输协议。在传输数据之前,通信双方必须先建立连接,确认彼此都能"听得到"且"说得出"。
|
||||
|
||||
这被称为**三次握手 (Three-Way Handshake)**:
|
||||
点击 **"Connect"** 亲自完成这次握手:
|
||||
|
||||
<TcpHandshakeDemo />
|
||||
|
||||
**握手三部曲**:
|
||||
|
||||
1. **SYN**:客户端说"你好,我想和你建立连接"(SYN)。
|
||||
2. **SYN-ACK**:服务器说"收到,我也想和你建立连接"(SYN-ACK)。
|
||||
3. **ACK**:客户端说"好的,那我们开始吧"(ACK)。
|
||||
1. **SYN** (Synchronize):客户端发送一个包,说"你好,我想和你建立连接,我的序列号是 X"。
|
||||
2. **SYN-ACK** (Synchronize-Acknowledge):服务器收到后回复,"好的,收到了 X。我也想和你建立连接,我的序列号是 Y"。
|
||||
3. **ACK** (Acknowledge):客户端最后回复,"好的,收到了 Y。那我们开始传输数据吧"。
|
||||
|
||||
> 🔒 **关于 HTTPS (TLS)**:
|
||||
> 如果使用 HTTPS,在 TCP 握手之后,还会进行 **TLS 握手**。双方会协商加密算法并交换证书,确保后续传输的数据像装在保险箱里一样安全。
|
||||
@@ -99,27 +81,27 @@
|
||||
|
||||
## 4. 第四步:交流 (HTTP Exchange)
|
||||
|
||||
### 4.1 如何索取资源?
|
||||
### 4.1 索取与交付
|
||||
|
||||
连接建立好了,浏览器需要告诉服务器它想要什么。这就像在餐厅点餐,你需要一种服务员能听懂的格式。
|
||||
连接建立好了,浏览器终于可以发出它的请求了:"请给我首页的 HTML 代码"。这就像在餐厅点餐。
|
||||
|
||||
### 4.2 解决方案:HTTP 请求与响应
|
||||
**HTTP (HyperText Transfer Protocol)** 定义了这种对话的格式。
|
||||
|
||||
**HTTP (HyperText Transfer Protocol)** 定义了客户端和服务器之间的对话格式。
|
||||
在下方的模拟器中尝试发送不同的请求(GET/POST),观察网络日志:
|
||||
|
||||
<HttpExchangeDemo />
|
||||
|
||||
**对话过程**:
|
||||
|
||||
1. **请求 (Request)**:浏览器发送一个请求报文。
|
||||
- **Method**:`GET`(获取)、`POST`(提交)等。
|
||||
1. **请求 (Request)**:
|
||||
- **Method**:`GET`(获取)、`POST`(提交数据)等。
|
||||
- **Path**:我要什么资源。
|
||||
- **Headers**:我是谁(User-Agent)、我想要什么格式(Accept)。
|
||||
- **Headers**:我是谁(User-Agent)、我想要什么格式(Accept)等元数据。
|
||||
|
||||
2. **响应 (Response)**:服务器返回一个响应报文。
|
||||
- **Status Code**:`200 OK`(成功)、`404 Not Found`(没找到)。
|
||||
- **Headers**:内容类型(Content-Type)、服务器信息。
|
||||
- **Body**:具体的 HTML 代码、图片数据等。
|
||||
2. **响应 (Response)**:
|
||||
- **Status Code**:`200 OK`(成功)、`404 Not Found`(没找到)、`500 Error`(服务器出错了)。
|
||||
- **Headers**:内容类型(Content-Type)、服务器信息等。
|
||||
- **Body**:具体的 HTML 代码、JSON 数据或图片二进制流。
|
||||
|
||||
---
|
||||
|
||||
@@ -127,29 +109,30 @@
|
||||
|
||||
### 5.1 代码如何变成画面?
|
||||
|
||||
浏览器收到的是一堆枯燥的 HTML 代码,它是如何变成我们在屏幕上看到的精美网页的呢?
|
||||
浏览器收到的是一堆枯燥的 HTML 代码,它是如何变成我们在屏幕上看到的精美网页的呢?这个过程叫做**渲染 (Rendering)**。
|
||||
|
||||
### 5.2 解决方案:渲染流水线 (Rendering Pipeline)
|
||||
浏览器就像一个精密的工厂,将原材料(HTML/CSS)加工成最终产品(屏幕上的像素)。
|
||||
|
||||
浏览器像一个精密的工厂,将原材料(HTML/CSS)加工成产品(像素)。
|
||||
点击下方的步骤,查看渲染流水线的每个阶段:
|
||||
|
||||
<BrowserRenderingDemo />
|
||||
|
||||
**流水线步骤**:
|
||||
**关键渲染路径 (Critical Rendering Path)**:
|
||||
|
||||
1. **构建 DOM 树**:解析 HTML,建立文档结构树(就像房屋的框架)。
|
||||
2. **构建渲染树 (Render Tree)**:结合 CSS 样式,确定哪些节点需要显示以及长什么样(就像装修设计图)。
|
||||
3. **布局 (Layout)**:计算每个元素在屏幕上的确切坐标和大小(就像丈量尺寸)。
|
||||
4. **绘制 (Paint)**:将元素画到屏幕的像素点上(就像刷漆)。
|
||||
2. **构建渲染树 (Render Tree)**:结合 CSS 样式,计算出所有**可见**元素的样式规则。
|
||||
3. **布局 (Layout/Reflow)**:计算每个元素在屏幕上的确切坐标和大小(就像丈量尺寸)。
|
||||
4. **绘制 (Paint)**:填充像素,包括颜色、图片、边框等。
|
||||
5. **合成 (Composite)**:将不同的图层(Layer)在 GPU 中合成,最终显示在屏幕上。
|
||||
|
||||
---
|
||||
|
||||
## 6. 总结
|
||||
|
||||
从 URL 输入到页面显示,这短短的几秒钟内,凝聚了计算机网络几十年的智慧结晶:
|
||||
从 URL 输入到页面显示,这短短的几秒钟内,凝聚了计算机网络几十年的智慧结晶。
|
||||
|
||||
| 阶段 | 核心任务 | 关键技术 | 类比 |
|
||||
| :---------- | :------- | :-------- | :------------- |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **1. 寻址** | 解析目标 | URL | 确定目的地地址 |
|
||||
| **2. 定位** | 查找 IP | DNS | 查电话簿 |
|
||||
| **3. 连接** | 建立通路 | TCP/TLS | 打电话确认通畅 |
|
||||
|
||||
+110
-163
@@ -4,243 +4,190 @@
|
||||
|
||||
<VlmQuickStartDemo />
|
||||
|
||||
## 0. 引言:从“读万卷书”到“行万里路”
|
||||
## 0. 引言:给大脑装上眼睛
|
||||
|
||||
在 [大语言模型入门](./llm-intro) 章节中,我们学习了计算机如何通过 Tokenization(分词)和 Transformer 理解文字。
|
||||
但真实世界不仅有文字,还有图像、视频和声音。
|
||||
在 [大语言模型入门](./llm-intro) 中,我们知道 LLM 本质上是一个被关在黑盒子里、只能通过**文字**来了解世界的“大脑”。
|
||||
|
||||
**多模态大模型 (VLM, Vision-Language Model)** 的核心任务,就是打破感官的界限,让 AI 不仅能“读”,还能“看”。
|
||||
**多模态大模型 (VLM)** 的出现,相当于给这个大脑装上了一双**眼睛**。
|
||||
|
||||
它的本质工作可以总结为一句话:**把图像信号“翻译”成大模型能听懂的语言信号。**
|
||||
但这并不容易。因为:
|
||||
- **大脑 (LLM)** 只懂**文字**(准确说是 Token ID)。
|
||||
- **眼睛 (摄像头)** 看到的是**像素**(RGB 颜色数值)。
|
||||
|
||||
VLM 的核心任务,就是**把“像素信号”翻译成“文字信号”**,让 LLM 觉得看图就像读文章一样简单。
|
||||
|
||||
---
|
||||
|
||||
## 1. 第一步:视觉翻译 (Visual Tokenization)
|
||||
## 1. 第一步:把图片变成“单词” (Visual Tokenization)
|
||||
|
||||
大模型(LLM)本质上是一个“文字接龙”机器,它只认识数字(Token ID)。要让它看懂图片,我们必须把图片也变成它能理解的数字序列。
|
||||
想象一下,你正在电话里给朋友描述一副拼图。你不可能一口气说完,你得一块一块地描述。
|
||||
计算机看图也是一样的道理。
|
||||
|
||||
这个过程主要由 **Vision Transformer (ViT)** 完成。**请注意:ViT 本身就是一个独立的、强大的深度学习模型**,你可以把它想象成 AI 的“视网膜”。
|
||||
### 1.1 切块 (Patchify) —— 制作视觉单词
|
||||
|
||||
### 1.1 为什么是 Transformer?(ViT 详解)
|
||||
LLM 习惯读单词。为了配合它,我们得把一张完整的图片切成一个个小方块(Patch)。
|
||||
|
||||
在 ViT 出现之前,计算机看图主要靠 **CNN (卷积神经网络)**,它像一个放大镜,一点一点地扫描图片提取特征。但 CNN 有个局限:它很难理解“全局关系”(比如左上角的鸟和右下角的树有什么关系)。
|
||||
- **图片** = 一篇文章
|
||||
- **方块 (Patch)** = 一个单词
|
||||
|
||||
**Transformer** 的核心优势在于**全局注意力 (Global Attention)**。它能同时看到整张图,并理解各个部分之间的关联。
|
||||
通常,我们会把图片切成 $16 \times 16$ 像素的小方块。一张 $224 \times 224$ 的图片就会变成 $14 \times 14 = 196$ 个方块。
|
||||
|
||||
但 Transformer 本来是处理文本(一维序列)的,怎么处理图片(二维矩阵)呢?
|
||||
> 🕹️ **交互演示**:点击下方按钮,看图片是如何被“切”成单词的。
|
||||
|
||||
| NLP (文本处理) | CV (图像处理) |
|
||||
| :--------------------- | :--------------------------- |
|
||||
| **句子** (Sentence) | **图片** (Image) |
|
||||
| **单词** (Word) | **图片块** (Patch) |
|
||||
| **词向量** (Embedding) | **特征向量** (Patch Feature) |
|
||||
<PatchifyDemo />
|
||||
|
||||
**ViT 的核心思想**:把一张图切成很多小块,然后把这些小块当成一个个单词,排成一句话喂给 Transformer。
|
||||
### 1.2 序列化 (Flatten) —— 排成一句话
|
||||
|
||||
#### 核心步骤拆解:
|
||||
切完后,我们得到的是一个 $14 \times 14$ 的方阵。但 LLM 只能读**一行字**(序列)。
|
||||
所以,我们必须把这个方阵**拍扁**,变成长长的一串。
|
||||
|
||||
1. **切块 (Patchify)**:
|
||||
就像把一张完整的拼图拆散。假设输入图片是 `224x224` 像素,我们设定每个 Patch 大小为 `16x16`。
|
||||
那么这张图就被切成了 (224/16) × (224/16) = 14 × 14 = 196 个小块。
|
||||
每个小块就是一个基础的视觉单词。
|
||||
- **原本**:二维矩阵(有行有列)。
|
||||
- **现在**:一维长条(只有前后)。
|
||||
|
||||
<PatchifyDemo />
|
||||
这样,图片就变成了一串“视觉单词序列”。
|
||||
|
||||
2. **拉平与映射 (Linear Projection)**:
|
||||
每个 `16x16` 的彩色小块包含 16 × 16 × 3 (RGB) = 768 个像素点。
|
||||
我们把这 768 个点拉成一条直线(向量),然后通过一个线性层(矩阵乘法)把它压缩成固定长度的特征向量(比如 768 维或 1024 维)。
|
||||
_现在的状态:196 个向量。_
|
||||
下面的演示展示了:**一个 Patch** 是如何被拍扁,并变成一个**向量**(计算机能读懂的数字列表)的。
|
||||
|
||||
<LinearProjectionDemo />
|
||||
|
||||
3. **加上位置编码 (Positional Embedding)**:
|
||||
Transformer 是“无序”的。如果你把拼图打乱,它就不知道哪块是头,哪块是脚。
|
||||
所以,我们必须给每个向量“贴上号码牌”:这是第1行第1列,那是第3行第5列。
|
||||
这样模型就能记住图片的空间结构。
|
||||
|
||||
<PositionalEmbeddingDemo />
|
||||
|
||||
4. **自注意力机制 (Self-Attention)**:
|
||||
这是最神奇的一步。这 196 个 Patch 开始“开会”互相交流。
|
||||
- **Patch A (猫耳朵)** 问:我是毛茸茸的三角形,谁跟我有关?
|
||||
- **Patch B (猫脸)** 回答:我是圆圆的脸,我们可以拼成一只猫头!
|
||||
- **Patch C (背景树)** 回答:我是绿色的,跟你们关系不大。
|
||||
通过层层计算,模型不仅识别出了孤立的特征,还理解了物体之间的**语义关系**。
|
||||
|
||||
<AttentionDemo />
|
||||
|
||||
5. **输出 (Output)**:
|
||||
最终,ViT 输出的是一串**富含语义的特征向量序列**。这串向量就是 LLM 后续要阅读的“图像文章”。
|
||||
|
||||
<ViTOutputDemo />
|
||||
<LinearProjectionDemo />
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心难题:跨界沟通 (Projection)
|
||||
## 2. 第二步:跨物种翻译 (Projection)
|
||||
|
||||
ViT 输出的向量虽然包含了图像信息,但它说的是“视觉方言”,LLM 的大脑只能听懂“文本方言”。
|
||||
**Projector (投射器)** 就是这个翻译官,负责对齐这两种语言的维度和语义。
|
||||
现在我们有了一串“视觉单词”,但 LLM 还是读不懂。
|
||||
因为这些“视觉单词”是**像素特征**(比如“这里是红色”、“那里有条线”),而 LLM 懂的是**语义特征**(比如“这是猫”、“那是树”)。
|
||||
|
||||
### 架构对比:三种流派
|
||||
这就需要一个翻译官:**Projector (投射器)**。
|
||||
|
||||
### 2.1 翻译官的作用
|
||||
|
||||
Projector 的工作就是把**视觉特征向量**(ViT 的输出)转换成**文本特征向量**(LLM 的输入)。
|
||||
|
||||
你可以把它理解为**外语翻译器**:
|
||||
- **输入**:视觉语言(ViT output)
|
||||
- **处理**:翻译(矩阵变换)
|
||||
- **输出**:LLM 语言(LLM embedding)
|
||||
|
||||
<ProjectorDemo />
|
||||
|
||||
#### 1. 简单粗暴派:Linear Projector (如 LLaVA)
|
||||
### 2.2 不同的翻译流派
|
||||
|
||||
- **结构**:一个简单的全连接层 (MLP)。
|
||||
- **数学原理**:$Y = WX + b$。其中 $X$ 是视觉向量,$W$ 是训练好的权重矩阵。
|
||||
- **比喻**:**直译**。把视觉向量强行“拉伸”或“压缩”到和文本向量一样的维度。
|
||||
- **优点**:保留了最多的原始视觉信息,几乎没有信息损失。
|
||||
- **缺点**:Token 数量多。一张图可能产生 576 个 Token,LLM 处理起来比较累。
|
||||
为了翻译得更好,科学家们发明了不同的翻译工具:
|
||||
|
||||
#### 2. 精细提取派:Q-Former (如 BLIP-2)
|
||||
1. **直译派 (Linear)**:
|
||||
- 做法:简单粗暴,通过一个矩阵乘法直接转换。
|
||||
- 特点:**保留原汁原味**,但废话多(Token 数量多)。
|
||||
- 代表:LLaVA。
|
||||
|
||||
- **结构**:一个小型的 Transformer,带有两组输入:一组是固定的“查询向量 (Queries)”,一组是图片特征。
|
||||
- **原理**:
|
||||
- 预设 32 个 Query(就像 32 个带着问题的记者)。
|
||||
- 这些记者进入图片特征的海洋里,寻找自己感兴趣的信息。
|
||||
- 最后只输出这 32 个记者采集到的精华摘要。
|
||||
- **比喻**:**意译/摘要**。不管原图多复杂,我都只给你总结出 32 句话。
|
||||
- **优点**:Token 数量极少(32个),LLM 跑得飞快。
|
||||
- **缺点**:信息压缩太狠,容易丢失细节(比如图片角落里的小字)。
|
||||
2. **意译派 (Q-Former/Resampler)**:
|
||||
- 做法:用一个小模型先读一遍图片,总结出几十个核心要点。
|
||||
- 特点:**精简**,Token 少,但可能会漏掉细节。
|
||||
- 代表:BLIP-2, Gemini。
|
||||
|
||||
#### 3. 注意力压缩派:C-Abstractor (如 Qwen-VL)
|
||||
|
||||
- **结构**:在 Linear 和 Q-Former 之间取平衡。利用卷积或注意力机制将相邻的 Patch 合并。
|
||||
- **原理**:比如把 $2\times2$ 的 4 个 Patch 合并成 1 个。
|
||||
- **优点**:既减少了 Token 数量(降低计算量),又保留了足够的空间细节。
|
||||
3. **折中派 (C-Abstractor)**:
|
||||
- 做法:把相邻的几个方块合并成一个,既压缩了长度,又保留了空间感。
|
||||
- 代表:Qwen-VL。
|
||||
|
||||
---
|
||||
|
||||
## 3. 进化之路:ViT + LLM
|
||||
## 3. 第三步:合体 (The Architecture)
|
||||
|
||||
现在的多模态大模型(M-LLM)本质上就是:**给 LLM 装了一副眼镜**。
|
||||
现在,零件都准备好了,我们把它们组装起来,就成了一个标准的 VLM。
|
||||
|
||||
### 模型架构对比
|
||||
|
||||
让我们直观地对比一下传统 LLM 和 VLM 在架构上的区别。
|
||||
### 3.1 VLM 的身体结构
|
||||
|
||||
<ModelArchitectureComparisonDemo />
|
||||
|
||||
### 模型解剖
|
||||
一个典型的 VLM(如 LLaVA)由三个部分组成:
|
||||
|
||||
一个标准的 LLaVA 架构模型由三部分物理连接而成:
|
||||
1. **眼睛 (Vision Encoder)**:
|
||||
- 负责看图。
|
||||
- 通常直接借用现成的、训练好的视觉模型(如 CLIP, SigLIP)。
|
||||
- *它就像视网膜,负责感光。*
|
||||
|
||||
1. **Vision Encoder (ViT)**
|
||||
- _来源_:通常借用已经训练好的模型(如 CLIP-ViT-L/14, SigLIP)。
|
||||
- _状态_:在训练初期通常是**冻结 (Frozen)** 的,因为它们已经很会看图了。
|
||||
2. **Projector (Adapter)**
|
||||
- _来源_:从零初始化。
|
||||
- _状态_:**全程训练**。它是连接视觉和语言的关键枢纽。
|
||||
3. **LLM (Backbone)**
|
||||
- _来源_:开源大模型(如 Vicuna, Qwen, Llama-3)。
|
||||
- _状态_:在预训练阶段冻结,在微调阶段解冻。
|
||||
2. **视神经 (Projector)**:
|
||||
- 负责传输和翻译信号。
|
||||
- 这是 VLM 训练的重点。
|
||||
- *它连接眼睛和大脑。*
|
||||
|
||||
### 视频也能看吗?
|
||||
|
||||
是的。对于模型来说,视频就是**一连串连续的图片**。
|
||||
|
||||
- **抽帧**:每秒抽取 1 帧或 2 帧。
|
||||
- **堆叠**:把这 10 张图片的 Token 串起来,告诉 LLM:“这是第一帧,这是第二帧...”。
|
||||
- **时间编码**:有些高级模型会加上“时间戳 Token”,让 LLM 理解动作的先后顺序。
|
||||
3. **大脑 (LLM)**:
|
||||
- 负责思考和回答。
|
||||
- 借用现成的强大 LLM(如 Vicuna, Qwen)。
|
||||
- *它负责理解看到了什么,并组织语言回答。*
|
||||
|
||||
---
|
||||
|
||||
## 4. 训练揭秘:从对齐到对话 (Training Pipeline)
|
||||
## 4. 它是怎么学会看图的?(Training)
|
||||
|
||||
要把这三个零件(ViT, Projector, LLM)磨合好,通常需要两阶段训练。
|
||||
刚组装好的 VLM 其实是“瞎”的,因为视神经(Projector)还没连通。我们需要分两步教它。
|
||||
|
||||
### 阶段一:特征对齐 (Feature Alignment / Pre-training)
|
||||
### 阶段一:认物 (Feature Alignment)
|
||||
|
||||
- **目标**:让 Projector 学会翻译。此时 LLM 还不参与学习,只是充当裁判。
|
||||
- **做法**:
|
||||
- **冻结**:ViT 和 LLM。
|
||||
- **只训练**:Projector。
|
||||
- **数据**:558K 对简单的 `<图片, 标题>` 数据 (CC3M, LAION)。
|
||||
- **过程**:
|
||||
输入一张“猫”的图 -> ViT -> Projector -> 得到向量 V。
|
||||
输入文字“一只猫” -> LLM -> 得到向量 T。
|
||||
**Loss**:强迫向量 V 和向量 T 尽可能相似。
|
||||
- **结果**:Projector 能够把图像特征转换成 LLM 能够理解的 Embedding 空间。
|
||||
这一阶段就像教婴儿认卡片。
|
||||
|
||||
- **给它看**:一张“猫”的照片。
|
||||
- **告诉它**:这是“猫”。
|
||||
- **目标**:让 Projector 学会把“猫的照片特征”翻译成“猫这个字的向量”。
|
||||
- **状态**:冻结眼睛和大脑,**只训练视神经 (Projector)**。
|
||||
|
||||
<FeatureAlignmentDemo />
|
||||
|
||||
### 阶段二:视觉指令微调 (Visual Instruction Tuning / SFT)
|
||||
### 阶段二:对话 (Visual Instruction Tuning)
|
||||
|
||||
- **目标**:让模型学会听指令,进行复杂对话。
|
||||
- **做法**:
|
||||
- **冻结**:ViT (通常保持冻结,有些激进的训练会解冻)。
|
||||
- **全量微调**:Projector + LLM。
|
||||
- **数据**:150K+ 高质量的对话数据 (LLaVA-Instruct)。
|
||||
- _User_: `<image>` 图中的男人穿什么颜色的衣服?
|
||||
- _Assistant_: 他穿着一件蓝色的衬衫。
|
||||
- **结果**:LLM 学会了结合图片信息来回答用户的问题,而不仅仅是补全文字。
|
||||
这一阶段是教它根据图片回答复杂问题。
|
||||
|
||||
- **用户问**:`<图片>` 图里的猫在干什么?
|
||||
- **教它答**:它在睡觉。
|
||||
- **目标**:让大脑 (LLM) 学会处理视觉信息,并结合常识进行推理。
|
||||
- **状态**:通常会同时微调 **Projector** 和 **LLM**。
|
||||
|
||||
<VLMInferenceDemo />
|
||||
|
||||
---
|
||||
|
||||
## 5. 进阶:新模型的视觉 Trick (Advanced Tricks)
|
||||
## 5. 进阶:看得更清 (Advanced Tricks)
|
||||
|
||||
### 5.1 Qwen-VL 的创新:像人眼一样看 (Naive Dynamic Resolution)
|
||||
基础的 VLM 有个大问题:**视力不好**。
|
||||
传统的 ViT 只能看 $224 \times 224$ 或 $336 \times 336$ 分辨率的图。这就像透过一个低清摄像头看世界,小字根本看不清。
|
||||
|
||||
传统的 ViT (如 CLIP) 有个大毛病:**强制缩放**。
|
||||
不管你给它一张长长的手机截图,还是一张扁扁的全景照,它都会粗暴地把图片拉伸成 `224x224` 的正方形。
|
||||
现在的模型(如 Qwen-VL, LLaVA-NeXT)用了一些聪明的方法来解决这个问题:
|
||||
|
||||
- **后果**:文字变形看不清,小物体丢失。
|
||||
### 5.1 动态分辨率 (Dynamic Resolution)
|
||||
|
||||
**Qwen-VL** 引入了 **Naive Dynamic Resolution(动态分辨率)** 机制:
|
||||
简单说,就是**“拼图法”**。
|
||||
|
||||
1. **保持原比例**:图片是长条的,就按长条的切。
|
||||
2. **智能分块**:将大图切成多个 `224x224` 的子图(就像用手机拍全景时移动镜头)。
|
||||
3. **全局视角**:除了看局部细节,还会生成一张缩略图看整体布局。
|
||||
这就好比人眼看东西:既能眯着眼看全貌,也能凑近了看细节,保证了高清图像的信息不丢失。
|
||||
如果图片很大(比如 $1000 \times 1000$),模型不会强行把它缩小,而是:
|
||||
1. 把它切成好几张 $336 \times 336$ 的小图。
|
||||
2. 分别看这些小图(看细节)。
|
||||
3. 再把全图缩小看一遍(看全貌)。
|
||||
4. 最后把所有信息拼起来。
|
||||
|
||||
### 5.2 LLaVA-NeXT (LLaVA-1.6): AnyRes 技术
|
||||
这就好比你用手机拍全景照片,分段扫描,最后合成一张高清大图。
|
||||
|
||||
**LLaVA-NeXT** 采用了 **AnyRes (Any Resolution)** 技术,这是一种灵活的分辨率处理策略。
|
||||
### 5.2 换个大眼睛
|
||||
|
||||
- **网格切分**:它构建了一个包含不同长宽比的网格配置集合(如 1:1, 1:2, 2:1 等)。给定一张输入图像,模型会从集合中选择最匹配的网格配置。
|
||||
- **避免变形**:通过这种方式,尽可能减少因缩放导致的图像变形。
|
||||
- **全局与局部结合**:它也会同时输入一张调整大小后的全图(用于看整体)和切分后的局部图块(用于看细节),让 LLM 综合判断。
|
||||
|
||||
### 5.3 InternVL: 让眼睛变大 (Scaling Vision Encoder)
|
||||
|
||||
传统的 VLM 往往使用 CLIP-ViT-Large (约 300M 参数) 作为视觉编码器。
|
||||
**InternVL (书生·万象)** 的思路很直接:**如果视力不好,那就换个更大的眼睛!**
|
||||
|
||||
- 它使用了一个高达 **60亿参数 (6B)** 的超大视觉编码器 (InternViT-6B)。
|
||||
- 这使得模型在无需任何微调的情况下,光靠“眼睛”就能看懂非常复杂的视觉细节,甚至能做语义分割。
|
||||
|
||||
### 5.4 DeepSeek-VL & MiniCPM-V: 细节为王 (High-Res Tiling)
|
||||
|
||||
对于需要看清密集文字(OCR)或微小物体(如仪表盘读数)的场景,**DeepSeek-VL** 和 **MiniCPM-V** 采用了更激进的高清切片策略。
|
||||
|
||||
- **混合视觉编码**:DeepSeek-VL 混合使用了负责语义理解的 SigLIP 和负责细节捕捉的 SAM (Segment Anything Model) 编码器,兼顾了“看得懂”和“看得清”。
|
||||
- **自适应切片**:MiniCPM-V 针对端侧设备优化,能够智能地将高清大图切分为多个小图输入,即使是 800万像素的图片也能在手机上被精准识别。
|
||||
还有一种暴力美学:直接换一个更强的视觉模型。
|
||||
比如 **InternVL**,直接用了一个 60 亿参数的超大视觉模型(InternViT-6B)。
|
||||
这相当于从“手机摄像头”升级到了“哈勃望远镜”,不用切图也能看得一清二楚。
|
||||
|
||||
---
|
||||
|
||||
## 6. 总结
|
||||
|
||||
VLM 的奇迹在于它证明了**语义的统一性**。无论是像素(图像)还是字符(文本),在深度神经网络的高维空间里,最终都可以汇聚为统一的数学表示。
|
||||
多模态大模型 (VLM) 并没有什么魔法。它只是做了一件事:
|
||||
|
||||
当你给 AI 发一张照片时,你其实是在发送一串它能“读懂”的数字诗篇。
|
||||
**把“图像”这种外语,翻译成了“文本”这种母语,然后喂给了 LLM。**
|
||||
|
||||
只要理解了这一点,你就理解了 VLM 的一切。
|
||||
|
||||
---
|
||||
|
||||
## 7. 名词速查表 (Glossary)
|
||||
|
||||
| 名词 | 全称 | 解释 |
|
||||
| :---------------------------- | :---------------------------- | :--------------------------------------------------------------------------------------------- |
|
||||
| **VLM** | Vision-Language Model | 多模态大模型。既能理解文本,又能理解图像(甚至视频)的 AI 模型。 |
|
||||
| **ViT** | Vision Transformer | 视觉 Transformer。将图像切分为 Patch 并通过 Self-Attention 提取特征的模型,是 VLM 的“眼睛”。 |
|
||||
| **Patch** | - | **图像块**。ViT 将图像切分成的固定大小的小方块(如 16x16 像素),相当于文本中的单词。 |
|
||||
| **Projector** | - | **投射器/对齐层**。连接 ViT 和 LLM 的桥梁,负责将视觉特征向量转换为 LLM 能理解的文本向量维度。 |
|
||||
| **Linear Projection** | - | **线性映射**。最简单的 Projector,通过一个矩阵乘法改变向量维度。 |
|
||||
| **Q-Former** | Querying Transformer | 一种复杂的 Projector,使用可学习的 Query 向量从图像特征中提取关键信息。 |
|
||||
| **Feature Alignment** | - | **特征对齐**。VLM 训练的第一阶段,目的是让 Projector 学会将图像特征映射到文本空间。 |
|
||||
| **Visual Instruction Tuning** | - | **视觉指令微调**。VLM 训练的第二阶段,使用多模态对话数据让模型学会根据图像回答问题。 |
|
||||
| **Resolution** | - | **分辨率**。图像的像素尺寸(如 224x224)。分辨率越高,看得越清,但计算量越大。 |
|
||||
| **AnyRes** | Any Resolution | **任意分辨率**。一种能够灵活处理不同尺寸和长宽比图像的技术,避免图像变形。 |
|
||||
| **OCR** | Optical Character Recognition | **光学字符识别**。从图像中提取文字的技术。现代 VLM 通常具备强大的 OCR 能力。 |
|
||||
| :--- | :--- | :--- |
|
||||
| **VLM** | Vision-Language Model | **多模态大模型**。能看懂图的 GPT。 |
|
||||
| **ViT** | Vision Transformer | **视觉模型**。VLM 的“眼睛”,负责把像素变成向量。 |
|
||||
| **Patch** | - | **图像块**。图片被切成的小方块,相当于“视觉单词”。 |
|
||||
| **Projector** | - | **投射器/翻译官**。连接眼睛和大脑的桥梁。 |
|
||||
| **Alignment** | - | **对齐**。让图像特征和文本特征在同一个空间里“互相听得懂”。 |
|
||||
|
||||
@@ -98,7 +98,21 @@ h1 {
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
```
|
||||
|
||||
### 2.3 盒模型:为什么宽度算不准?
|
||||
### 2.3 核心机制:CSS 怎么找到 HTML?
|
||||
|
||||
新手最容易晕的就是:CSS 里写的 `p`、`.card`、`#btn` 到底是怎么跟 HTML 对应上的?
|
||||
|
||||
这就好比老师在班级里点名,有三种点法:
|
||||
|
||||
1. **喊“所有人” (标签选择器)**:喊 "男生",所有男生都要站起来。
|
||||
2. **喊“小组名” (类选择器)**:喊 "英语课代表",可能有好几个。
|
||||
3. **喊“学号” (ID 选择器)**:喊 "2024001",全班只有一个。
|
||||
|
||||
**互动演示:把鼠标移到左边的 CSS 规则上,看看右边谁会亮起来。**
|
||||
|
||||
<CssSelectorsDemo />
|
||||
|
||||
### 2.4 盒模型:为什么宽度算不准?
|
||||
|
||||
每个元素都是一个盒子,由 **内容 → 内边距 → 边框 → 外边距** 组成。
|
||||
|
||||
@@ -106,9 +120,81 @@ h1 {
|
||||
|
||||
记忆公式:**总宽度 = margin + border + padding + width + padding + border + margin**。
|
||||
|
||||
### 2.4 Flexbox:为什么对齐和分布这么简单?
|
||||
### 2.5 怎么知道有哪些 CSS 属性?
|
||||
|
||||
<CssFlexbox />
|
||||
新手常问:“我怎么知道要改颜色是 `color` 还是 `font-color`?” CSS 属性多到记不住,是因为网页要面对的情况太复杂了(各种屏幕尺寸、各种设计需求)。
|
||||
|
||||
**但好消息是:日常开发中,90% 的时间你只需要用到下面这 20% 的核心属性。**
|
||||
|
||||
<CssCommonProperties />
|
||||
|
||||
#### 遇到不认识的属性怎么办?
|
||||
|
||||
**在 AI 原生时代,解决这个问题有更聪明的方法:**
|
||||
|
||||
1. **直接问 AI (首选方案)**:
|
||||
* 现在的 AI 编程助手(如 Cursor、Trae、GitHub Copilot)已经非常强大。
|
||||
* 你根本不需要背诵属性,直接用自然语言描述你的需求。
|
||||
* **例子**:你对 AI 说 "我要一个带有阴影的蓝色圆形按钮",它会直接给你写出包含 `background-color`, `border-radius`, `box-shadow` 的完整代码。
|
||||
* **为什么这是首选?** 因为它不仅告诉你“属性名”,还帮你把“值”都调好了。
|
||||
|
||||
2. **查文档 (MDN)**:
|
||||
* MDN Web Docs 是 Web 开发的权威字典。
|
||||
* 搜 "MDN CSS text color",它会告诉你正确属性是 `color`。
|
||||
* 搜 "MDN CSS background",它会列出 `background-color`, `background-image` 等家族成员。
|
||||
|
||||
3. **用浏览器“偷看” (DevTools)**:
|
||||
* 在喜欢的网页上**右键 -> 检查 (Inspect)**。
|
||||
* 在 **Styles** 面板里,你可以看到人家用了什么属性。
|
||||
* 你甚至可以直接在那里面试着改改数值,实时看效果(刷新就没了,很安全)。
|
||||
|
||||
4. **CSS 游乐场**:
|
||||
* 下面的演示列出了一些最常用的“装修参数”。
|
||||
* 试着拖动滑块、修改颜色,看看它们分别控制什么。
|
||||
|
||||
<CssPlaygroundDemo />
|
||||
|
||||
### 2.6 现代 CSS 开发:Tailwind CSS 简介
|
||||
|
||||
以前写 CSS,我们要给每个东西起个名字(比如 `.my-button`, `.header-title`),然后在 CSS 文件里写一堆属性。这叫“语义化 CSS”。
|
||||
|
||||
现在流行一种**原子化 CSS (Utility-first CSS)**,代表作是 **Tailwind CSS**。
|
||||
|
||||
**它的核心思想**:
|
||||
不要写 CSS 代码,直接在 HTML 标签上写“代号”。
|
||||
|
||||
- **传统写法**:
|
||||
```html
|
||||
<button class="btn-primary">按钮</button>
|
||||
<style>
|
||||
.btn-primary {
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
- **Tailwind 写法**:
|
||||
```html
|
||||
<!-- bg-blue-500: 蓝色背景 -->
|
||||
<!-- text-white: 白字 -->
|
||||
<!-- px-4 py-2: 左右间距4,上下间距2 -->
|
||||
<!-- rounded: 圆角 -->
|
||||
<button class="bg-blue-500 text-white px-4 py-2 rounded">按钮</button>
|
||||
```
|
||||
|
||||
**为什么它这么火?**
|
||||
1. **不用起名**:最头疼的“起类名”环节没了。
|
||||
2. **不切文件**:不用在 HTML 和 CSS 文件之间切来切去。
|
||||
3. **不怕删**:删掉 HTML 标签时,样式自动就没了,不会留下堆积如山的无用 CSS 代码。
|
||||
|
||||
> 💡 **提示**:现在的 AI 编程工具(如 Cursor, v0)非常擅长写 Tailwind。你只要说“给我一个蓝色的圆角按钮”,它大概率直接给你生成带 Tailwind 类的代码。
|
||||
|
||||
### 2.7 Flexbox:为什么对齐和分布这么简单?
|
||||
|
||||
<CssLayoutDemo />
|
||||
|
||||
核心属性速记:
|
||||
|
||||
@@ -174,9 +260,72 @@ document.getElementById('title').textContent = '新标题'
|
||||
|
||||
---
|
||||
|
||||
## 4. 协作实战:三者如何“分工又配合”?
|
||||
## 4. 深入理解 DOM:网页的“族谱”
|
||||
|
||||
### 4.1 分工对比表
|
||||
你可能经常听到 **DOM (Document Object Model)** 这个词。别被这个专业术语吓到,它其实就是一张**网页的族谱**。
|
||||
|
||||
### 4.1 什么是 DOM 树?
|
||||
|
||||
浏览器读取 HTML 代码后,不会把它们当成一堆字符串,而是会在内存里把它们画成一棵树。
|
||||
|
||||
* `<html>` 是祖先。
|
||||
* `<body>` 是 `<html>` 的孩子。
|
||||
* `div`、`p`、`button` 又是 `<body>` 的孩子。
|
||||
|
||||
这棵树就叫 **DOM 树**。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Document[Document] --> HTML[html]
|
||||
HTML --> Head[head]
|
||||
HTML --> Body[body]
|
||||
Head --> Title[title: "我的网页"]
|
||||
Body --> H1[h1: "欢迎"]
|
||||
Body --> Div[div.card]
|
||||
Div --> Img[img]
|
||||
Div --> P[p: "一段文字"]
|
||||
Div --> Button[button: "点击"]
|
||||
```
|
||||
|
||||
### 4.2 为什么叫“对象模型” (Object Model)?
|
||||
|
||||
因为在 JS 眼里,HTML 标签不仅仅是标签,而是**对象 (Object)**。它们有属性,有方法。
|
||||
|
||||
* **属性 (Property)**:
|
||||
* `img.src` = "photo.jpg"
|
||||
* `div.className` = "box"
|
||||
* `input.value` = "123"
|
||||
* **方法 (Method)**:
|
||||
* `button.click()` (假装被点了一下)
|
||||
* `div.remove()` (自杀)
|
||||
* `body.appendChild(newDiv)` (生个孩子)
|
||||
|
||||
### 4.3 怎么找节点?(CRUD)
|
||||
|
||||
就像在族谱里找人一样,JS 提供了很多方法:
|
||||
|
||||
1. **按身份证找 (ID)**:
|
||||
* `document.getElementById('header')` —— 全局唯一,最快。
|
||||
2. **按特征找 (Selector)**:
|
||||
* `document.querySelector('.card h2')` —— 就像写 CSS 一样找,很灵活。
|
||||
3. **按关系找**:
|
||||
* `element.parentNode` (找爸爸)
|
||||
* `element.children` (找孩子)
|
||||
|
||||
### 4.4 性能警告:不要频繁“拆家”
|
||||
|
||||
操作 DOM 是很贵的。每次你修改 DOM(比如改大小、改位置),浏览器都要重新计算排版(**Reflow**)和重新绘制(**Repaint**)。
|
||||
|
||||
* ❌ **低效**:循环 1000 次,每次往 `body` 里插入一个 `div`。
|
||||
* ✅ **高效**:先把 1000 个 `div` 拼好(DocumentFragment),一次性塞进 `body` 里。
|
||||
|
||||
这也正是 **Vue / React** 诞生的原因:它们在内存里玩“虚拟 DOM”,计算好最小修改量,最后才去动真正的 DOM,从而保护了性能。
|
||||
|
||||
---
|
||||
|
||||
## 5. 协作实战:三者如何“分工又配合”?
|
||||
|
||||
### 5.1 分工对比表
|
||||
|
||||
| 角色 | 负责什么 | 不做什么 | 典型示例 |
|
||||
| :------------- | :--------------- | :-------------- | :--------------------------------- |
|
||||
@@ -184,7 +333,7 @@ document.getElementById('title').textContent = '新标题'
|
||||
| **CSS** | 定义表现与布局 | 不存放业务逻辑 | `.card { border-radius: 8px; }` |
|
||||
| **JavaScript** | 定义行为与数据流 | 不承担视觉表现 | `button.onclick = changeTitle` |
|
||||
|
||||
### 4.2 组合示例:点击改变标题
|
||||
### 5.2 组合示例:点击改变标题
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
|
||||
@@ -69,6 +69,29 @@ const base = site.value.base
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* LOGO 容器:负责上下浮动动画 */
|
||||
.VPHomeHero .image {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 鼠标悬停时暂停浮动 */
|
||||
.VPHomeHero .image:hover {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
/* 给 LOGO 容器设置不可侵犯的左侧边界 */
|
||||
.VPHomeHero .image {
|
||||
margin-left: 80px !important;
|
||||
flex-shrink: 0; /* 保证图片不被挤压 */
|
||||
}
|
||||
|
||||
.VPHomeHero .tagline {
|
||||
max-width: 450px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.VPHomeHero .text {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ Trae 分为国际版和中国版。国际版需要能够访问海外网络,但
|
||||
|
||||

|
||||
|
||||
如图所示,这里我们正在创建模板,但不知道如何操作,我们可以截图该部分对大模型进行新闻:
|
||||
如图所示,这里我们正在创建模板,但不知道如何操作,我们可以截图该部分对大模型进行询问:
|
||||
|
||||

|
||||
|
||||
|
||||
Reference in New Issue
Block a user