feat: update docs and components, fix DLQ demo bug

This commit is contained in:
sanbuphy
2026-01-18 12:21:49 +08:00
parent 26ed39e1eb
commit e41063a1cd
159 changed files with 54236 additions and 2525 deletions
@@ -0,0 +1,221 @@
<!--
BigFrontendScopeDemo.vue
前端 vs 大前端跨端范围演示
-->
<template>
<div class="bigfe-demo">
<div class="header">
<div class="title">前端 vs 大前端到底前端都包含什么</div>
<div class="subtitle">点一下不同立刻看到它跑在哪里怎么发布</div>
</div>
<div class="platforms">
<button
v-for="p in platforms"
:key="p.key"
class="platform"
:class="{ active: current === p.key }"
@click="current = p.key"
>
<span class="icon">{{ p.icon }}</span>
<span>{{ p.label }}</span>
</button>
</div>
<div class="cards">
<div class="card">
<div class="label">运行环境</div>
<div class="value">{{ currentData.runtime }}</div>
</div>
<div class="card">
<div class="label">主要技术</div>
<div class="value">{{ currentData.stack }}</div>
</div>
<div class="card">
<div class="label">发布方式</div>
<div class="value">{{ currentData.release }}</div>
</div>
</div>
<div class="skills">
<div class="skills-title">哪些能力是共通的</div>
<div class="tags">
<span v-for="t in commonSkills" :key="t" class="tag">{{ t }}</span>
</div>
<div class="skills-note">
大前端的核心不是会更多框架而是<strong>用同一套工程能力把体验交付到不同平台</strong>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const platforms = [
{ key: 'web', label: 'Web 网站', icon: '🌐' },
{ key: 'h5', label: 'H5 活动页', icon: '📱' },
{ key: 'miniapp', label: '小程序', icon: '🧩' },
{ key: 'native', label: 'App(原生)', icon: '📲' },
{ key: 'cross', label: '跨端 App', icon: '🧱' },
{ key: 'desktop', label: '桌面应用', icon: '🖥️' }
]
const current = ref('web')
const data = {
web: {
runtime: '浏览器 (Chrome/Safari/Edge)',
stack: 'HTML + CSS + JavaScript / Vue / React',
release: '部署到服务器/静态托管,用户刷新即可更新'
},
h5: {
runtime: '手机浏览器 / App 内的 WebView',
stack: '同 Web,但更关注性能与兼容',
release: '发链接/扫码即用,迭代很快'
},
miniapp: {
runtime: '小程序运行时(微信/支付宝等)',
stack: '小程序框架 + JS/TS + 组件',
release: '需要审核/发布(比网页慢一些)'
},
native: {
runtime: 'iOS/Android 原生系统',
stack: 'Swift/Objective-C / Kotlin/Java',
release: '应用商店上架(流程最慢,但能力最强)'
},
cross: {
runtime: '原生壳 + 跨端引擎',
stack: 'React Native / Flutter(用一套代码做多端)',
release: '仍走商店流程,但研发复用更高'
},
desktop: {
runtime: 'Windows/macOS/Linux',
stack: 'Electron / Tauri(用 Web 技术做桌面)',
release: '打包成安装包/自动更新'
}
}
const currentData = computed(() => data[current.value] || data.web)
const commonSkills = [
'HTTP/网络',
'性能优化',
'工程化与构建',
'组件化',
'状态管理',
'调试与排错',
'用户体验'
]
</script>
<style scoped>
.bigfe-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.platforms {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0.75rem 0 1rem;
}
.platform {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
padding: 0.45rem 0.75rem;
border-radius: 999px;
font-size: 0.85rem;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.platform.active {
border-color: #3b82f6;
color: #1d4ed8;
background: rgba(59, 130, 246, 0.12);
}
.icon {
font-size: 1rem;
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 0.75rem;
}
.card {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 10px;
padding: 0.85rem;
}
.label {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.value {
margin-top: 0.25rem;
font-size: 0.95rem;
font-weight: 600;
line-height: 1.35;
}
.skills {
margin-top: 1rem;
border-top: 1px dashed var(--vp-c-divider);
padding-top: 1rem;
}
.skills-title {
font-weight: 600;
margin-bottom: 0.5rem;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
}
.tag {
font-size: 0.8rem;
padding: 0.2rem 0.55rem;
border-radius: 999px;
background: rgba(34, 197, 94, 0.12);
color: #15803d;
border: 1px solid rgba(34, 197, 94, 0.2);
}
.skills-note {
margin-top: 0.75rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
</style>
@@ -1,11 +1,14 @@
<template>
<div class="browser-rendering-demo">
<div class="stepper">
<button
v-for="(step, index) in steps"
<button
v-for="(step, index) in steps"
:key="index"
class="step-btn"
:class="{ active: currentStep === index, completed: currentStep > index }"
:class="{
active: currentStep === index,
completed: currentStep > index
}"
@click="currentStep = index"
>
<span class="step-num">{{ index + 1 }}</span>
@@ -25,26 +28,166 @@
<div class="window-title">积木说明书 (HTML/CSS)</div>
<div class="code-content">
<!-- HTML Highlighted always after Step 0 -->
<div class="line" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'html' }" @mouseenter="hoveredPart = 'html'" @mouseleave="hoveredPart = null">&lt;!DOCTYPE html&gt;</div>
<div class="line" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'html' }" @mouseenter="hoveredPart = 'html'" @mouseleave="hoveredPart = null">&lt;html&gt;</div>
<div class="line indent" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'body' }" @mouseenter="hoveredPart = 'body'" @mouseleave="hoveredPart = null">&lt;body&gt;</div>
<div class="line indent-2" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'card' }" @mouseenter="hoveredPart = 'card'" @mouseleave="hoveredPart = null">&lt;div class="card"&gt;</div>
<div class="line indent-3" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'img' }" @mouseenter="hoveredPart = 'img'" @mouseleave="hoveredPart = null">&lt;img class="icon" src="castle.png" /&gt;</div>
<div class="line indent-3" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'title' }" @mouseenter="hoveredPart = 'title'" @mouseleave="hoveredPart = null">&lt;h2 class="title"&gt;乐高城堡&lt;/h2&gt;</div>
<div class="line indent-3" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'btn' }" @mouseenter="hoveredPart = 'btn'" @mouseleave="hoveredPart = null">&lt;button class="btn"&gt;购买&lt;/button&gt;</div>
<div class="line indent-2" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'card' }" @mouseenter="hoveredPart = 'card'" @mouseleave="hoveredPart = null">&lt;/div&gt;</div>
<div class="line indent" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'body' }" @mouseenter="hoveredPart = 'body'" @mouseleave="hoveredPart = null">&lt;/body&gt;</div>
<div class="line" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'html' }" @mouseenter="hoveredPart = 'html'" @mouseleave="hoveredPart = null">&lt;/html&gt;</div>
<div
class="line"
:class="{
active: currentStep >= 0,
hovered: hoveredPart === 'html'
}"
@mouseenter="hoveredPart = 'html'"
@mouseleave="hoveredPart = null"
>
&lt;!DOCTYPE html&gt;
</div>
<div
class="line"
:class="{
active: currentStep >= 0,
hovered: hoveredPart === 'html'
}"
@mouseenter="hoveredPart = 'html'"
@mouseleave="hoveredPart = null"
>
&lt;html&gt;
</div>
<div
class="line indent"
:class="{
active: currentStep >= 0,
hovered: hoveredPart === 'body'
}"
@mouseenter="hoveredPart = 'body'"
@mouseleave="hoveredPart = null"
>
&lt;body&gt;
</div>
<div
class="line indent-2"
:class="{
active: currentStep >= 0,
hovered: hoveredPart === 'card'
}"
@mouseenter="hoveredPart = 'card'"
@mouseleave="hoveredPart = null"
>
&lt;div class="card"&gt;
</div>
<div
class="line indent-3"
:class="{
active: currentStep >= 0,
hovered: hoveredPart === 'img'
}"
@mouseenter="hoveredPart = 'img'"
@mouseleave="hoveredPart = null"
>
&lt;img class="icon" src="castle.png" /&gt;
</div>
<div
class="line indent-3"
:class="{
active: currentStep >= 0,
hovered: hoveredPart === 'title'
}"
@mouseenter="hoveredPart = 'title'"
@mouseleave="hoveredPart = null"
>
&lt;h2 class="title"&gt;乐高城堡&lt;/h2&gt;
</div>
<div
class="line indent-3"
:class="{
active: currentStep >= 0,
hovered: hoveredPart === 'btn'
}"
@mouseenter="hoveredPart = 'btn'"
@mouseleave="hoveredPart = null"
>
&lt;button class="btn"&gt;购买&lt;/button&gt;
</div>
<div
class="line indent-2"
:class="{
active: currentStep >= 0,
hovered: hoveredPart === 'card'
}"
@mouseenter="hoveredPart = 'card'"
@mouseleave="hoveredPart = null"
>
&lt;/div&gt;
</div>
<div
class="line indent"
:class="{
active: currentStep >= 0,
hovered: hoveredPart === 'body'
}"
@mouseenter="hoveredPart = 'body'"
@mouseleave="hoveredPart = null"
>
&lt;/body&gt;
</div>
<div
class="line"
:class="{
active: currentStep >= 0,
hovered: hoveredPart === 'html'
}"
@mouseenter="hoveredPart = 'html'"
@mouseleave="hoveredPart = null"
>
&lt;/html&gt;
</div>
<div class="spacer"></div>
<!-- CSS Highlighted precisely based on step usage -->
<!-- Layout properties -->
<div class="line" :class="{ active: currentStep === 2, hovered: hoveredPart === 'card' }" @mouseenter="hoveredPart = 'card'" @mouseleave="hoveredPart = null">.card { display: flex; padding: 10px; }</div>
<div class="line" :class="{ active: currentStep === 2, hovered: hoveredPart === 'img' }" @mouseenter="hoveredPart = 'img'" @mouseleave="hoveredPart = null">.icon { width: 50px; height: 50px; }</div>
<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
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>
@@ -53,38 +196,89 @@
<!-- 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">
<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">
<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">
<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">
<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>
<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">
<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>
<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">
<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>
<span v-if="currentStep >= 3" class="content-btn"
>购买</span
>
</div>
</div>
</div>
</div>
</transition-group>
@@ -98,7 +292,7 @@
<div class="ruler">📏 正在排版 (Layout)...</div>
</div>
<div v-if="currentStep === 3" class="overlay-info paint-info">
<div v-if="currentStep === 3" class="overlay-info paint-info">
<div class="paint"> 绘制完成 (Paint)!</div>
</div>
</div>
@@ -238,7 +432,8 @@ const hoveredPart = ref(null)
min-height: 400px;
}
.source-view, .result-view {
.source-view,
.result-view {
flex: 1;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
@@ -279,10 +474,18 @@ const hoveredPart = ref(null)
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; }
.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;
@@ -321,9 +524,21 @@ const hoveredPart = ref(null)
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-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;
@@ -357,7 +572,7 @@ const hoveredPart = ref(null)
padding: 15px;
background: white;
border: 1px solid #ccc;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.block-box.img.layout {
@@ -381,15 +596,21 @@ const hoveredPart = ref(null)
}
/* Content visibility for Paint step */
.content, .content-img, .content-btn {
.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; }
.content-img {
font-size: 2rem;
}
.content-btn {
font-size: 0.8rem;
}
/* Overlay Info */
.overlay-info {
@@ -402,14 +623,16 @@ const hoveredPart = ref(null)
pointer-events: none;
}
.brush, .ruler, .paint {
.brush,
.ruler,
.paint {
display: inline-block;
background: rgba(0,0,0,0.8);
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);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Vue Transitions */
@@ -424,14 +647,26 @@ const hoveredPart = ref(null)
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
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); }
0% {
transform: scale(0.8);
opacity: 0;
}
60% {
transform: scale(1.1);
opacity: 1;
}
100% {
transform: scale(1);
}
}
/* Hover Interactions */
@@ -0,0 +1,151 @@
<!--
BundlerSizeDemo.vue
打包体积与构建时间演示
-->
<template>
<div class="bundler-demo">
<div class="header">
<div class="title">工程化打包体积与构建时间</div>
<div class="subtitle">勾选功能观察体积变化</div>
</div>
<div class="options">
<label v-for="item in features" :key="item.key" class="option">
<input type="checkbox" v-model="item.enabled" />
{{ item.label }} (+{{ item.size }} KB)
</label>
</div>
<label class="toggle">
<input type="checkbox" v-model="treeShaking" />
开启 Tree Shaking (移除未使用代码)
</label>
<div class="stats">
<div class="stat-card">
<div class="label">Bundle Size</div>
<div class="value">{{ bundleSize }} KB</div>
</div>
<div class="stat-card">
<div class="label">Build Time</div>
<div class="value">{{ buildTime }} s</div>
</div>
</div>
<div class="bar">
<div class="progress" :style="{ width: barWidth + '%' }"></div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const features = ref([
{ key: 'chart', label: '图表库', size: 180, enabled: true },
{ key: 'editor', label: '富文本编辑器', size: 220, enabled: false },
{ key: 'i18n', label: '国际化', size: 60, enabled: true },
{ key: 'analytics', label: '埋点分析', size: 80, enabled: false }
])
const treeShaking = ref(true)
const rawSize = computed(() =>
features.value.reduce(
(sum, item) => (item.enabled ? sum + item.size : sum),
120
)
)
const bundleSize = computed(() => {
const size = treeShaking.value ? rawSize.value * 0.82 : rawSize.value
return Math.round(size)
})
const buildTime = computed(() => Math.round(bundleSize.value / 90))
const barWidth = computed(() => Math.min(100, Math.round(bundleSize.value / 6)))
</script>
<style scoped>
.bundler-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.option {
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
align-items: center;
gap: 0.4rem;
}
.toggle {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 1rem;
margin-top: 0.75rem;
}
.stat-card {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 10px;
padding: 0.8rem;
}
.label {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.value {
font-size: 1.2rem;
font-weight: 700;
margin-top: 0.2rem;
}
.bar {
margin-top: 0.75rem;
height: 10px;
background: var(--vp-c-bg);
border-radius: 999px;
overflow: hidden;
border: 1px solid var(--vp-c-divider);
}
.progress {
height: 100%;
background: linear-gradient(90deg, #6366f1, #8b5cf6);
}
</style>
@@ -2,7 +2,9 @@
<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('counter')">
New Counter
</button>
<button class="spawn-btn" @click="spawn('card')"> New Card</button>
</div>
@@ -10,9 +12,9 @@
<div class="workspace-label">App Workspace</div>
<div class="instances-container">
<transition-group name="list">
<div
v-for="item in instances"
:key="item.id"
<div
v-for="item in instances"
:key="item.id"
class="instance-wrapper"
>
<!-- Counter Component -->
@@ -36,8 +38,8 @@
<div class="comp-body">
<div class="skeleton-img"></div>
<div class="skeleton-text"></div>
<button
class="like-btn"
<button
class="like-btn"
:class="{ liked: item.data.liked }"
@click="item.data.liked = !item.data.liked"
>
@@ -49,7 +51,7 @@
</transition-group>
<div v-if="instances.length === 0" class="empty-hint">
Click buttons above to add components.
<br>
<br />
Notice how each one works independently!
</div>
</div>
@@ -80,7 +82,7 @@ const spawn = (type) => {
}
const remove = (id) => {
instances.value = instances.value.filter(i => i.id !== id)
instances.value = instances.value.filter((i) => i.id !== id)
}
</script>
@@ -119,7 +121,7 @@ const remove = (id) => {
cursor: pointer;
font-size: 0.8rem;
transition: all 0.2s;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.spawn-btn:hover {
border-color: var(--vp-c-brand);
@@ -168,7 +170,7 @@ const remove = (id) => {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
width: 140px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
@@ -191,7 +193,9 @@ const remove = (id) => {
font-size: 1rem;
line-height: 1;
}
.close-btn:hover { color: #ef4444; }
.close-btn:hover {
color: #ef4444;
}
.comp-body {
padding: 0.8rem;
@@ -214,7 +218,9 @@ const remove = (id) => {
border-radius: 4px;
cursor: pointer;
}
.mini-btn:hover { background: #e2e8f0; }
.mini-btn:hover {
background: #e2e8f0;
}
/* Card Style */
.skeleton-img {
@@ -257,4 +263,4 @@ const remove = (id) => {
opacity: 0;
transform: scale(0.8);
}
</style>
</style>
@@ -38,7 +38,7 @@
<span class="layer-label" v-if="padding >= 15">Padding</span>
<div class="content">
<div class="content-inner">
内容区<br>
内容区<br />
{{ contentW }} × {{ contentH }}
</div>
</div>
@@ -60,7 +60,9 @@
</div>
<div class="detail-item">
<span class="detail-label">计算公式</span>
<span class="detail-text">Margin(×2) + Border(×2) + Padding(×2) + 内容宽</span>
<span class="detail-text"
>Margin(×2) + Border(×2) + Padding(×2) + 内容宽</span
>
</div>
</div>
</div>
@@ -69,11 +71,11 @@
<div class="code-title">CSS 代码片段</div>
<div class="code-content">
<div class="line">.box {</div>
<div class="line"> width: {{ contentW }}px;</div>
<div class="line"> height: {{ contentH }}px;</div>
<div class="line"> padding: {{ padding }}px;</div>
<div class="line"> border: {{ border }}px solid #0ea5e9;</div>
<div class="line"> margin: {{ margin }}px;</div>
<div class="line">width: {{ contentW }}px;</div>
<div class="line">height: {{ contentH }}px;</div>
<div class="line">padding: {{ padding }}px;</div>
<div class="line">border: {{ border }}px solid #0ea5e9;</div>
<div class="line">margin: {{ margin }}px;</div>
<div class="line">}</div>
</div>
</div>
@@ -89,7 +91,9 @@ const padding = ref(20)
const border = ref(10)
const margin = ref(20)
const total = computed(() => margin.value * 2 + border.value * 2 + padding.value * 2 + contentW)
const total = computed(
() => margin.value * 2 + border.value * 2 + padding.value * 2 + contentW
)
</script>
<style scoped>
@@ -127,8 +131,15 @@ const total = computed(() => margin.value * 2 + border.value * 2 + padding.value
font-size: 13px;
}
label { font-weight: 700; color: var(--vp-c-text-1); }
.val { font-family: var(--vp-font-family-mono); color: var(--vp-c-brand); font-weight: 600; }
label {
font-weight: 700;
color: var(--vp-c-text-1);
}
.val {
font-family: var(--vp-font-family-mono);
color: var(--vp-c-brand);
font-weight: 600;
}
input[type='range'] {
width: 100%;
@@ -179,7 +190,9 @@ input[type='range'] {
border: 1px dashed #d1d5db; /* gray-300 */
color: #6b7280;
}
.margin > .layer-label { color: #6b7280; }
.margin > .layer-label {
color: #6b7280;
}
/* Border Layer */
.border {
@@ -188,7 +201,9 @@ input[type='range'] {
border-color: #7dd3fc; /* sky-300 */
color: #0284c7;
}
.border > .layer-label { color: #0284c7; }
.border > .layer-label {
color: #0284c7;
}
/* Padding Layer */
.padding {
@@ -196,7 +211,9 @@ input[type='range'] {
border: 1px dashed #93c5fd; /* blue-300 */
color: #2563eb;
}
.padding > .layer-label { color: #2563eb; }
.padding > .layer-label {
color: #2563eb;
}
/* Content Box */
.content {
@@ -237,7 +254,9 @@ input[type='range'] {
font-weight: 600;
}
.meta-value { color: var(--vp-c-brand); }
.meta-value {
color: var(--vp-c-brand);
}
.meta-detail {
display: flex;
@@ -1,19 +1,16 @@
<template>
<div class="css-props-ref">
<div class="intro">
CSS 属性就像装修队的施工指令常用的其实只有几十个这里有一份装修菜单供你参考
CSS
属性就像装修队的施工指令常用的其实只有几十个这里有一份装修菜单供你参考
</div>
<div class="categories">
<div
v-for="(cat, index) in categories"
:key="index"
class="category"
>
<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"
<div
v-for="prop in cat.props"
:key="prop.name"
class="prop-item"
@click="activeProp = prop"
@@ -37,9 +34,7 @@
<pre><code>{{ activeProp.example }}</code></pre>
</div>
</div>
<div v-else class="prop-detail empty">
点击上面的属性看看它能做什么 👆
</div>
<div v-else class="prop-detail empty">点击上面的属性看看它能做什么 👆</div>
</div>
</template>
@@ -52,38 +47,145 @@ 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;' },
{
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;' },
{
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;' },
{
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;' },
{
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;'
}
]
}
]
@@ -137,7 +239,7 @@ const categories = [
.prop-item:hover {
border-color: var(--vp-c-brand);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.prop-item.active {
@@ -231,7 +333,13 @@ code {
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
@@ -10,7 +10,14 @@
<label>主轴方向 (flex-direction)</label>
</div>
<div class="chips">
<button v-for="d in directions" :key="d.id" :class="['chip', { active: dir === d.id }]" @click="dir = d.id">{{ d.label }}</button>
<button
v-for="d in directions"
:key="d.id"
:class="['chip', { active: dir === d.id }]"
@click="dir = d.id"
>
{{ d.label }}
</button>
</div>
</div>
<div class="control-item">
@@ -18,7 +25,14 @@
<label>主轴对齐 (justify-content)</label>
</div>
<div class="chips">
<button v-for="j in justifies" :key="j.id" :class="['chip', { active: justify === j.id }]" @click="justify = j.id">{{ j.label }}</button>
<button
v-for="j in justifies"
:key="j.id"
:class="['chip', { active: justify === j.id }]"
@click="justify = j.id"
>
{{ j.label }}
</button>
</div>
</div>
<div class="control-item">
@@ -26,7 +40,14 @@
<label>是否换行 (flex-wrap)</label>
</div>
<div class="chips">
<button v-for="w in wraps" :key="w.id" :class="['chip', { active: wrap === w.id }]" @click="wrap = w.id">{{ w.label }}</button>
<button
v-for="w in wraps"
:key="w.id"
:class="['chip', { active: wrap === w.id }]"
@click="wrap = w.id"
>
{{ w.label }}
</button>
</div>
</div>
</div>
@@ -41,10 +62,10 @@
<div class="code-title">CSS 代码片段</div>
<div class="code-content">
<div class="line">.container {</div>
<div class="line"> display: flex;</div>
<div class="line"> flex-direction: {{ dir }};</div>
<div class="line"> justify-content: {{ justify }};</div>
<div class="line"> flex-wrap: {{ wrap }};</div>
<div class="line">display: flex;</div>
<div class="line">flex-direction: {{ dir }};</div>
<div class="line">justify-content: {{ justify }};</div>
<div class="line">flex-wrap: {{ wrap }};</div>
<div class="line">}</div>
</div>
</div>
@@ -169,7 +190,9 @@ const boxStyle = computed(() => ({
display: grid;
place-items: center;
font-size: 18px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
flex-shrink: 0;
}
@@ -8,8 +8,8 @@
<div class="control-group">
<label>排列方向 (flex-direction)</label>
<div class="btn-group">
<button
v-for="val in ['row', 'column']"
<button
v-for="val in ['row', 'column']"
:key="val"
:class="{ active: direction === val }"
@click="direction = val"
@@ -22,8 +22,13 @@
<div class="control-group">
<label>主轴对齐 (justify-content)</label>
<div class="btn-group">
<button
v-for="val in ['flex-start', 'center', 'space-between', 'space-around']"
<button
v-for="val in [
'flex-start',
'center',
'space-between',
'space-around'
]"
:key="val"
:class="{ active: justify === val }"
@click="justify = val"
@@ -36,8 +41,8 @@
<div class="control-group">
<label>交叉轴对齐 (align-items)</label>
<div class="btn-group">
<button
v-for="val in ['stretch', 'center', 'flex-start', 'flex-end']"
<button
v-for="val in ['stretch', 'center', 'flex-start', 'flex-end']"
:key="val"
:class="{ active: align === val }"
@click="align = val"
@@ -50,8 +55,8 @@
<div class="control-group">
<label>换行 (flex-wrap)</label>
<div class="btn-group">
<button
v-for="val in ['nowrap', 'wrap']"
<button
v-for="val in ['nowrap', 'wrap']"
:key="val"
:class="{ active: wrap === val }"
@click="wrap = val"
@@ -64,10 +69,10 @@
<div class="preview-area">
<div class="container" :style="containerStyle">
<div
v-for="n in itemCount"
<div
v-for="n in itemCount"
:key="n"
class="item"
class="item"
:style="[itemStyle, getItemColor(n)]"
>
{{ n }}
@@ -156,9 +161,18 @@ const itemStyle = computed(() => {
const itemCount = computed(() => (wrap.value === 'wrap' ? 12 : 5))
const colors = [
'#3b82f6', '#8b5cf6', '#ec4899', '#f59e0b', '#10b981',
'#6366f1', '#14b8a6', '#f97316', '#ef4444', '#84cc16',
'#06b6d4', '#d946ef'
'#3b82f6',
'#8b5cf6',
'#ec4899',
'#f59e0b',
'#10b981',
'#6366f1',
'#14b8a6',
'#f97316',
'#ef4444',
'#84cc16',
'#06b6d4',
'#d946ef'
]
const getItemColor = (n) => {
@@ -247,7 +261,7 @@ const getItemColor = (n) => {
color: white;
font-weight: bold;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
flex-shrink: 0;
}
@@ -290,6 +304,11 @@ const getItemColor = (n) => {
font-weight: normal;
}
pre { margin: 0; }
.val { color: #f472b6; font-weight: bold; }
pre {
margin: 0;
}
.val {
color: #f472b6;
font-weight: bold;
}
</style>
@@ -1,7 +1,7 @@
<template>
<div class="css-playground">
<div class="demo-box">
<div
<div
class="target-element"
:style="{
backgroundColor: bgColor,
@@ -52,7 +52,7 @@
<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" />
@@ -151,12 +151,12 @@ const borderColor = ref('#000000')
text-align: right;
}
input[type="range"] {
input[type='range'] {
flex: 1;
cursor: pointer;
}
input[type="color"] {
input[type='color'] {
width: 30px;
height: 30px;
padding: 0;
@@ -190,4 +190,4 @@ pre {
color: #9cdcfe;
font-weight: bold;
}
</style>
</style>
@@ -1,13 +1,15 @@
<template>
<div class="selectors-demo">
<div class="hint">👇 鼠标悬停在左侧 CSS 代码上看看右侧 HTML 谁会被选中</div>
<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
<div
class="rule-item"
:class="{ active: activeType === 'tag' }"
@mouseenter="activeType = 'tag'"
@@ -21,7 +23,7 @@
</div>
</div>
<div
<div
class="rule-item"
:class="{ active: activeType === 'class' }"
@mouseenter="activeType = 'class'"
@@ -35,7 +37,7 @@
</div>
</div>
<div
<div
class="rule-item"
:class="{ active: activeType === 'id' }"
@mouseenter="activeType = 'id'"
@@ -61,39 +63,30 @@
<div class="column html-col">
<div class="col-title">HTML (结构)</div>
<div class="code-view">
<div
class="html-line"
:class="{ highlight: activeType === 'tag' }"
>
<div class="html-line" :class="{ highlight: activeType === 'tag' }">
&lt;p&gt;我是普通段落&lt;/p&gt;
</div>
<div
class="html-line"
:class="{ highlight: activeType === 'class' }"
>
<div class="html-line" :class="{ highlight: activeType === 'class' }">
&lt;div <span class="attr">class="card"</span>&gt;
</div>
<div
class="html-line indent"
:class="{ highlight: activeType === 'tag' || activeType === 'class' }"
<div
class="html-line indent"
:class="{
highlight: activeType === 'tag' || activeType === 'class'
}"
>
&lt;p&gt;我是卡片里的段落&lt;/p&gt;
</div>
<div
class="html-line"
:class="{ highlight: activeType === 'class' }"
>
<div class="html-line" :class="{ highlight: activeType === 'class' }">
&lt;/div&gt;
</div>
<div
class="html-line"
:class="{ highlight: activeType === 'id' }"
>
&lt;button <span class="attr">id="submit-btn"</span>&gt;提交&lt;/button&gt;
<div class="html-line" :class="{ highlight: activeType === 'id' }">
&lt;button
<span class="attr">id="submit-btn"</span>&gt;提交&lt;/button&gt;
</div>
</div>
</div>
@@ -157,7 +150,8 @@ const activeType = ref(null)
position: relative;
}
.rule-item:hover, .rule-item.active {
.rule-item:hover,
.rule-item.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-dimm);
transform: translateX(5px);
@@ -167,8 +161,12 @@ const activeType = ref(null)
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 */
.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;
@@ -185,9 +183,15 @@ const activeType = ref(null)
border-radius: 4px;
color: white;
}
.badge.tag { background: #d73a49; }
.badge.class { background: #6f42c1; }
.badge.id { background: #005cc5; }
.badge.tag {
background: #d73a49;
}
.badge.class {
background: #6f42c1;
}
.badge.id {
background: #005cc5;
}
/* HTML Column */
.code-view {
@@ -216,7 +220,7 @@ const activeType = ref(null)
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);
text-shadow: 0 0 5px rgba(255, 255, 255, 0.5);
}
.attr {
@@ -242,7 +246,8 @@ const activeType = ref(null)
.comparison {
flex-direction: column;
}
.rule-item:hover, .rule-item.active {
.rule-item:hover,
.rule-item.active {
transform: translateY(2px);
}
.connector {
@@ -5,8 +5,9 @@
<strong>为什么需要 DNS(查导航)</strong>
</p>
<p class="why-desc-zh">
你知道店铺名字叫 "Shop.com"但快递员需要知道具体的经纬度坐标 (IP 地址) 才能送达
<br>
你知道店铺名字叫 "Shop.com"但快递员需要知道具体的经纬度坐标 (IP 地址)
才能送达
<br />
DNS 就像是<strong>地图导航</strong>输入店名它告诉你具体的坐标
</p>
</div>
@@ -86,7 +87,8 @@ defineProps({
gap: 1rem;
}
.input-area, .output-area {
.input-area,
.output-area {
text-align: center;
width: 100%;
}
@@ -98,7 +100,8 @@ defineProps({
margin-bottom: 0.5rem;
}
.fake-input, .fake-output {
.fake-input,
.fake-output {
background: var(--vp-c-bg-alt);
padding: 0.8rem;
border-radius: 8px;
@@ -161,7 +164,12 @@ defineProps({
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(5px); }
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(5px);
}
}
</style>
@@ -10,7 +10,10 @@
<input v-model="title" placeholder="输入新标题" />
</div>
<div class="field checkbox">
<label><input type="checkbox" v-model="highlight" /> 高亮模式 (class="highlight")</label>
<label
><input type="checkbox" v-model="highlight" /> 高亮模式
(class="highlight")</label
>
</div>
</div>
@@ -43,17 +46,75 @@ const toggleText = () => {
</script>
<style scoped>
.dom-demo { border: 1px solid var(--vp-c-divider); border-radius: 12px; background: var(--vp-c-bg-soft); padding: 16px; margin: 20px 0; display: flex; flex-direction: column; gap: 12px; }
.controls { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px; }
.field { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-radius: 10px; padding: 10px; display: flex; flex-direction: column; gap: 6px; }
.checkbox { flex-direction: row; align-items: center; gap: 8px; }
input[type='text'], input[type='checkbox'] { accent-color: var(--vp-c-brand); }
.dom-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 16px;
margin: 20px 0;
display: flex;
flex-direction: column;
gap: 12px;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 10px;
}
.field {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 10px;
display: flex;
flex-direction: column;
gap: 6px;
}
.checkbox {
flex-direction: row;
align-items: center;
gap: 8px;
}
input[type='text'],
input[type='checkbox'] {
accent-color: var(--vp-c-brand);
}
.card { border: 1px solid var(--vp-c-divider); border-radius: 12px; padding: 16px; background: var(--vp-c-bg); transition: all 0.2s; }
.card.highlight { border-color: #f59e0b; box-shadow: 0 8px 18px rgba(245, 158, 11, 0.2); background: #fff7ed; }
.card h2 { margin: 0 0 8px 0; }
.card p { margin: 0 0 12px 0; color: var(--vp-c-text-2); }
.card button { background: var(--vp-c-brand); color: #fff; border: none; border-radius: 8px; padding: 8px 12px; cursor: pointer; }
.card {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 16px;
background: var(--vp-c-bg);
transition: all 0.2s;
}
.card.highlight {
border-color: #f59e0b;
box-shadow: 0 8px 18px rgba(245, 158, 11, 0.2);
background: #fff7ed;
}
.card h2 {
margin: 0 0 8px 0;
}
.card p {
margin: 0 0 12px 0;
color: var(--vp-c-text-2);
}
.card button {
background: var(--vp-c-brand);
color: #fff;
border: none;
border-radius: 8px;
padding: 8px 12px;
cursor: pointer;
}
.code { background: #0b1221; color: #e5e7eb; border-radius: 10px; padding: 12px; font-family: var(--vp-font-family-mono); font-size: 13px; overflow-x: auto; }
.code {
background: #0b1221;
color: #e5e7eb;
border-radius: 10px;
padding: 12px;
font-family: var(--vp-font-family-mono);
font-size: 13px;
overflow-x: auto;
}
</style>
@@ -3,11 +3,14 @@
<!-- Modern Timeline -->
<div class="timeline-container">
<div class="timeline-track"></div>
<button
v-for="(stage, index) in stages"
<button
v-for="(stage, index) in stages"
:key="index"
class="timeline-node"
:class="{ active: currentStage === index, passed: currentStage > index }"
:class="{
active: currentStage === index,
passed: currentStage > index
}"
@click="currentStage = index"
>
<div class="node-dot">
@@ -25,7 +28,9 @@
<div :key="currentStage" class="stage-content">
<div class="header-section">
<h3>
<span class="stage-index">{{ indexToRoman(currentStage + 1) }}.</span>
<span class="stage-index"
>{{ indexToRoman(currentStage + 1) }}.</span
>
{{ stages[currentStage].title }}
</h3>
<p>{{ stages[currentStage].desc }}</p>
@@ -40,7 +45,9 @@
<span class="light yellow"></span>
<span class="light green"></span>
</div>
<div class="window-title">{{ stages[currentStage].codeTitle }}</div>
<div class="window-title">
{{ stages[currentStage].codeTitle }}
</div>
</div>
<div class="editor-content">
<pre><code>{{ stages[currentStage].code }}</code></pre>
@@ -53,7 +60,6 @@
<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">
@@ -75,9 +81,21 @@
</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)"/>
<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>
@@ -85,11 +103,18 @@
<div class="concept-box js">
<span class="icon">🍝</span> jQuery / JS
</div>
<!-- SVG Marker Definition -->
<svg style="position: absolute; width: 0; height: 0;">
<svg style="position: absolute; width: 0; height: 0">
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<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>
@@ -102,7 +127,7 @@
<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>
@@ -128,11 +153,8 @@
</div>
</div>
</div>
<div class="flow-pill">
State UI = f(State)
</div>
<div class="flow-pill">State UI = f(State)</div>
</div>
</div>
</div>
</div>
@@ -224,11 +246,13 @@ export default {
.frontend-evolution-demo {
border-radius: 16px;
background: var(--vp-c-bg);
box-shadow: 0 8px 30px rgba(0,0,0,0.05);
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;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial,
sans-serif;
}
/* --- Timeline --- */
@@ -270,7 +294,8 @@ export default {
opacity: 0.9;
}
.timeline-node.active, .timeline-node.passed {
.timeline-node.active,
.timeline-node.passed {
opacity: 1;
}
@@ -385,8 +410,8 @@ export default {
.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);
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;
@@ -408,8 +433,8 @@ export default {
.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);
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;
@@ -417,7 +442,7 @@ export default {
.diagram-window .window-bar {
background: white;
border-bottom: 1px solid rgba(0,0,0,0.05);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.traffic-lights {
@@ -431,9 +456,15 @@ export default {
border-radius: 50%;
}
.light.red { background: #ff5f56; }
.light.yellow { background: #ffbd2e; }
.light.green { background: #27c93f; }
.light.red {
background: #ff5f56;
}
.light.yellow {
background: #ffbd2e;
}
.light.green {
background: #27c93f;
}
.window-title {
position: absolute;
@@ -482,10 +513,10 @@ export default {
/* --- Diagram Specifics --- */
.concept-box {
background: white;
border: 1px solid rgba(0,0,0,0.1);
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);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
font-weight: 600;
font-size: 0.9rem;
display: flex;
@@ -494,7 +525,9 @@ export default {
z-index: 2;
}
.icon { font-size: 1.2rem; }
.icon {
font-size: 1.2rem;
}
/* Static Diagram */
.diagram.static .flow-stack {
@@ -507,7 +540,7 @@ export default {
margin-top: 1rem;
font-size: 0.8rem;
color: var(--vp-c-text-3);
background: rgba(0,0,0,0.05);
background: rgba(0, 0, 0, 0.05);
padding: 4px 8px;
border-radius: 4px;
}
@@ -542,16 +575,21 @@ export default {
.arrow-path.dashed {
stroke-dasharray: 4;
}
.label-action, .label-event {
.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);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.label-action {
transform: translate(-20px, -10px);
}
.label-event {
transform: translate(20px, 10px);
}
.label-action { transform: translate(-20px, -10px); }
.label-event { transform: translate(20px, 10px); }
/* MVC Diagram */
.diagram.mvc {
@@ -563,9 +601,22 @@ export default {
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; }
.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;
@@ -636,9 +687,18 @@ export default {
.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; }
.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);
@@ -665,4 +725,4 @@ export default {
opacity: 0;
transform: translateY(-20px);
}
</style>
</style>
@@ -24,13 +24,15 @@
<span>{{ t.cols.type }}</span>
<span>{{ t.cols.time }}</span>
</div>
<div
class="log-row"
:class="{ active: requestSent, selected: true }"
<div
class="log-row"
:class="{ active: requestSent, selected: true }"
v-if="requestSent"
>
<span class="col-name">{{ path.split('/').pop() || 'index' }}</span>
<span class="col-status" :class="statusClass">{{ responseStatus }}</span>
<span class="col-status" :class="statusClass">{{
responseStatus
}}</span>
<span class="col-type">document</span>
<span class="col-time">{{ loading ? 'Pending' : '45ms' }}</span>
</div>
@@ -40,8 +42,8 @@
<!-- Details Panel (Right) -->
<div class="details-panel" v-if="requestSent">
<div class="tabs">
<button
v-for="tabKey in ['headers', 'response', 'preview']"
<button
v-for="tabKey in ['headers', 'response', 'preview']"
:key="tabKey"
:class="{ active: activeTab === tabKey }"
@click="activeTab = tabKey"
@@ -73,7 +75,11 @@
</div>
<div class="section">
<div class="section-title">{{ t.responseHeaders }}</div>
<div class="kv-row" v-for="(val, key) in responseHeaders" :key="key">
<div
class="kv-row"
v-for="(val, key) in responseHeaders"
:key="key"
>
<span class="key">{{ key }}:</span>
<span class="value">{{ val }}</span>
</div>
@@ -87,7 +93,11 @@
<!-- Preview Tab -->
<div v-if="activeTab === 'preview'" class="preview-view">
<div v-if="method === 'GET'" class="html-preview" v-html="responseBody"></div>
<div
v-if="method === 'GET'"
class="html-preview"
v-html="responseBody"
></div>
<div v-else class="json-preview">
JSON Data: {{ responseBody }}
</div>
@@ -149,24 +159,24 @@ const sendRequest = async () => {
loading.value = true
requestSent.value = true
responseStatus.value = '处理中...'
await new Promise(r => setTimeout(r, 800))
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': '乐高官方店'
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()
Date: new Date().toLocaleString()
}
responseBody.value = `{\n "success": true,\n "message": "Order placed"\n}`
}
@@ -185,7 +195,10 @@ const statusClass = computed(() => {
border-radius: 8px;
background: var(--vp-c-bg);
margin: 1rem 0;
font-family: system-ui, -apple-system, sans-serif;
font-family:
system-ui,
-apple-system,
sans-serif;
overflow: hidden;
}
@@ -249,7 +262,9 @@ const statusClass = computed(() => {
color: var(--vp-c-text-2);
}
.log-header span { flex: 1; }
.log-header span {
flex: 1;
}
.log-row {
display: flex;
@@ -266,10 +281,19 @@ html.dark .log-row.selected {
background: #1e3a8a;
}
.log-row span { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.log-row span {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.col-status.success { color: #10b981; }
.col-status.pending { color: #9ca3af; }
.col-status.success {
color: #10b981;
}
.col-status.pending {
color: #9ca3af;
}
.empty-state {
padding: 2rem;
@@ -358,7 +382,10 @@ html.dark .log-row.selected {
border-radius: 50%;
margin-right: 4px;
}
.status-dot.success { background: #10b981; }
.status-dot.pending { background: #9ca3af; }
.status-dot.success {
background: #10b981;
}
.status-dot.pending {
background: #9ca3af;
}
</style>
@@ -9,8 +9,8 @@
</div>
<div class="code-preview">
<code>
// 手动操作 DOM<br>
$('#count').text(val);<br>
// 手动操作 DOM<br />
$('#count').text(val);<br />
if (val > 5) $('#msg').show();
</code>
</div>
@@ -21,8 +21,16 @@
</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>
<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>
@@ -36,8 +44,8 @@
</div>
<div class="code-preview">
<code>
// 只需要绑定数据<br>
{{ '{' + '{ count }' + '}' }}<br>
// 只需要绑定数据<br />
{{ '{' + '{ count }' + '}' }}<br />
&lt;div v-if="count > 5"&gt;...&lt;/div&gt;
</code>
</div>
@@ -47,9 +55,13 @@
<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>
<button @click="decIncrement" class="btn primary">
Value++ (Auto Render)
</button>
</div>
<div class="status-log">
Framework handles DOM updates automatically.
</div>
<div class="status-log">Framework handles DOM updates automatically.</div>
</div>
</div>
</div>
@@ -68,7 +80,8 @@ 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.'
impStatus.value =
'Variable `count` is now ' + (impCount.value + 1) + '. DOM is NOT updated.'
impCount.value++
impChanged.value = true
impTextUpdated.value = false
@@ -115,7 +128,9 @@ const decIncrement = () => {
}
@media (max-width: 640px) {
.demo-grid { grid-template-columns: 1fr; }
.demo-grid {
grid-template-columns: 1fr;
}
}
.panel {
@@ -143,8 +158,12 @@ const decIncrement = () => {
border-radius: 4px;
color: white;
}
.badge.yellow { background: #f59e0b; }
.badge.green { background: #10b981; }
.badge.yellow {
background: #f59e0b;
}
.badge.green {
background: #10b981;
}
.sub-text {
font-size: 0.8rem;
@@ -203,10 +222,21 @@ const decIncrement = () => {
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; }
.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;
@@ -216,7 +246,13 @@ const decIncrement = () => {
}
@keyframes popIn {
0% { transform: scale(0.8); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
0% {
transform: scale(0.8);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
</style>
</style>
@@ -0,0 +1,383 @@
<!--
JQueryVsStateDemo.vue
用可视化方式解释jQuery = 手动改 DOM框架 = State 自动同步
-->
<template>
<div class="jq-demo">
<div class="header">
<div class="title">什么是 jQuery购物车数量秒懂</div>
<div class="subtitle">
左边 jQuery 一样手动改页面容易漏右边 Vue/React
一样只改状态
</div>
</div>
<div class="panes">
<!-- jQuery-like -->
<div class="pane">
<div class="pane-title">jQuery 思路到处改 DOM</div>
<div class="mock-app">
<div class="topbar">
<span>🛒 角标</span>
<span class="badge" :class="{ wrong: jqBadgeWrong }">{{
jqBadge
}}</span>
</div>
<div class="content">
<div class="row">
购物车页数量
<span class="num" :class="{ wrong: jqPageWrong }">{{
jqPage
}}</span>
</div>
<div class="row">
结算按钮
<button class="checkout">去结算 ({{ jqButtonLabel }})</button>
</div>
</div>
</div>
<div class="controls">
<div class="control-title">模拟你写的命令</div>
<div class="btns">
<button @click="jqIncreaseData">数据 +1但还没改页面</button>
<button @click="jqUpdateBadge">改角标</button>
<button @click="jqUpdateCartPage">改购物车页</button>
<button @click="jqUpdateCheckoutButton">改结算按钮</button>
</div>
<div class="hint" :class="{ danger: jqInconsistent }">
{{ jqHint }}
</div>
<div class="log">
<div class="log-title">命令日志</div>
<div v-if="jqLogs.length === 0" class="log-empty">
还没有操作
</div>
<div v-else class="log-list">
<div v-for="(l, idx) in jqLogs" :key="idx" class="log-item">
{{ l }}
</div>
</div>
</div>
</div>
</div>
<!-- State-driven -->
<div class="pane">
<div class="pane-title">Vue/React 思路只改 State</div>
<div class="mock-app">
<div class="topbar">
<span>🛒 角标</span>
<span class="badge">{{ state }}</span>
</div>
<div class="content">
<div class="row">
购物车页数量 <span class="num">{{ state }}</span>
</div>
<div class="row">
结算按钮
<button class="checkout">去结算 ({{ state }} )</button>
</div>
</div>
</div>
<div class="controls">
<div class="control-title">你只需要做一件事</div>
<div class="btns">
<button class="primary" @click="state = state + 1">state +1</button>
<button class="secondary" @click="resetAll">重置</button>
</div>
<div class="hint ok">
State 变了界面三处会自动同步不需要你手动找 DOM 去改
</div>
<div class="mini">
<div class="mini-title">这里的两个新词</div>
<div class="mini-item">
<strong>DOM</strong>浏览器里的页面结构按钮/文字/图片都在里面
</div>
<div class="mini-item">
<strong>State</strong>页面的数据比如购物车数量
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const state = ref(1)
// jQuery side: "real data" + "DOM" values displayed at multiple places
const jqData = ref(1)
const jqBadge = ref(1)
const jqPage = ref(1)
const jqButtonLabel = ref('1 件')
const jqLogs = ref([])
const log = (txt) => {
jqLogs.value.unshift(
`${new Date().toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})} - ${txt}`
)
jqLogs.value = jqLogs.value.slice(0, 8)
}
const jqIncreaseData = () => {
jqData.value += 1
log(`数据 +1(现在真实数据 = ${jqData.value}`)
}
const jqUpdateBadge = () => {
jqBadge.value = jqData.value
log(`更新角标 DOM = ${jqBadge.value}`)
}
const jqUpdateCartPage = () => {
jqPage.value = jqData.value
log(`更新购物车页 DOM = ${jqPage.value}`)
}
const jqUpdateCheckoutButton = () => {
jqButtonLabel.value = `${jqData.value}`
log(`更新结算按钮 DOM = ${jqButtonLabel.value}`)
}
const jqInconsistent = computed(() => {
return (
jqBadge.value !== jqData.value ||
jqPage.value !== jqData.value ||
jqButtonLabel.value !== `${jqData.value}`
)
})
const jqBadgeWrong = computed(() => jqBadge.value !== jqData.value)
const jqPageWrong = computed(() => jqPage.value !== jqData.value)
const jqHint = computed(() => {
if (!jqInconsistent.value) return '✅ 三处显示一致(恭喜你都改对了)'
return '⚠️ 数据和页面不一致:你可能漏更新了某一处 DOM(真实项目里这就是 bug)'
})
const resetAll = () => {
state.value = 1
jqData.value = 1
jqBadge.value = 1
jqPage.value = 1
jqButtonLabel.value = '1 件'
jqLogs.value = []
}
</script>
<style scoped>
.jq-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.panes {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1rem;
}
.pane {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 12px;
padding: 1rem;
}
.pane-title {
font-weight: 700;
font-size: 0.95rem;
margin-bottom: 0.75rem;
}
.mock-app {
border: 1px dashed var(--vp-c-divider);
border-radius: 12px;
overflow: hidden;
}
.topbar {
padding: 0.6rem 0.75rem;
background: var(--vp-c-bg-soft);
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
}
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2ch;
padding: 0.1rem 0.45rem;
border-radius: 999px;
background: rgba(59, 130, 246, 0.15);
color: #1d4ed8;
font-weight: 700;
}
.content {
padding: 0.75rem;
}
.row {
margin-bottom: 0.6rem;
font-size: 0.92rem;
}
.num {
font-weight: 800;
padding: 0.05rem 0.25rem;
border-radius: 6px;
background: rgba(34, 197, 94, 0.12);
color: #15803d;
}
.checkout {
border: none;
background: var(--vp-c-brand);
color: #fff;
padding: 0.4rem 0.8rem;
border-radius: 10px;
font-size: 0.85rem;
}
.controls {
margin-top: 0.9rem;
}
.control-title {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.btns {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.btns button {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
padding: 0.35rem 0.65rem;
border-radius: 10px;
cursor: pointer;
font-size: 0.85rem;
}
.btns button.primary {
border: none;
background: #22c55e;
color: #fff;
}
.btns button.secondary {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
}
.hint {
margin-top: 0.65rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
border: 1px dashed var(--vp-c-divider);
background: var(--vp-c-bg-soft);
padding: 0.6rem 0.7rem;
border-radius: 10px;
}
.hint.danger {
color: #b91c1c;
border-color: rgba(239, 68, 68, 0.4);
background: rgba(239, 68, 68, 0.08);
}
.hint.ok {
color: #166534;
border-color: rgba(34, 197, 94, 0.35);
background: rgba(34, 197, 94, 0.08);
}
.wrong {
background: rgba(239, 68, 68, 0.12) !important;
color: #b91c1c !important;
}
.log {
margin-top: 0.75rem;
}
.log-title {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.35rem;
}
.log-empty {
color: var(--vp-c-text-3);
font-size: 0.85rem;
}
.log-list {
display: grid;
gap: 0.25rem;
}
.log-item {
font-family: var(--vp-font-family-mono);
font-size: 0.78rem;
color: var(--vp-c-text-2);
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 0.35rem 0.5rem;
}
.mini {
margin-top: 0.75rem;
border-top: 1px dashed var(--vp-c-divider);
padding-top: 0.75rem;
}
.mini-title {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.4rem;
}
.mini-item {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.25rem;
}
</style>
@@ -0,0 +1,144 @@
<!--
RenderingStrategyDemo.vue
CSR / SSR / SSG 对比演示
-->
<template>
<div class="render-demo">
<div class="header">
<div class="title">渲染策略CSR / SSR / SSG</div>
<div class="subtitle">选择策略观察首屏表现</div>
</div>
<div class="options">
<button
v-for="item in strategies"
:key="item.key"
class="option"
:class="{ active: current === item.key }"
@click="current = item.key"
>
{{ item.label }}
</button>
</div>
<div class="cards">
<div class="card">
<div class="label">TTFB</div>
<div class="value">{{ metrics.ttfb }} ms</div>
</div>
<div class="card">
<div class="label">可交互时间</div>
<div class="value">{{ metrics.tti }} ms</div>
</div>
<div class="card">
<div class="label">SEO 友好</div>
<div class="value">{{ metrics.seo }}</div>
</div>
</div>
<div class="hint">{{ metrics.note }}</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const strategies = [
{ key: 'csr', label: 'CSR' },
{ key: 'ssr', label: 'SSR' },
{ key: 'ssg', label: 'SSG' }
]
const current = ref('csr')
const metrics = computed(() => {
if (current.value === 'csr') {
return { ttfb: 450, tti: 1600, seo: '一般', note: 'JS 拉取完成后才渲染' }
}
if (current.value === 'ssr') {
return {
ttfb: 220,
tti: 1100,
seo: '好',
note: '首屏更快,但服务器压力更大'
}
}
return { ttfb: 120, tti: 700, seo: '很好', note: '静态预渲染,适合内容站点' }
})
</script>
<style scoped>
.render-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.options {
display: flex;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.option {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
padding: 0.35rem 0.8rem;
border-radius: 999px;
font-size: 0.85rem;
cursor: pointer;
}
.option.active {
border-color: #22c55e;
color: #15803d;
background: rgba(34, 197, 94, 0.12);
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 0.75rem;
}
.card {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 10px;
padding: 0.75rem;
}
.label {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.value {
font-size: 1.1rem;
font-weight: 700;
margin-top: 0.2rem;
}
.hint {
margin-top: 0.8rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
</style>
@@ -0,0 +1,116 @@
<!--
ResponsiveGridDemo.vue
响应式布局断点演示
-->
<template>
<div class="responsive-demo">
<div class="header">
<div class="title">响应式布局一套代码多种屏幕</div>
<div class="subtitle">拖动宽度观察列数变化</div>
</div>
<div class="controls">
<label>
视口宽度<strong>{{ viewportWidth }}</strong> px
</label>
<input
v-model="viewportWidth"
type="range"
min="320"
max="1200"
step="10"
/>
</div>
<div class="preview" :style="{ width: viewportWidth + 'px' }">
<div class="grid" :style="gridStyle">
<div v-for="n in 6" :key="n" class="card">Card {{ n }}</div>
</div>
</div>
<div class="note">
当前列数<strong>{{ columns }}</strong>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const viewportWidth = ref(860)
const columns = computed(() => {
if (viewportWidth.value < 640) return 1
if (viewportWidth.value < 900) return 2
return 3
})
const gridStyle = computed(() => ({
gridTemplateColumns: `repeat(${columns.value}, minmax(0, 1fr))`
}))
</script>
<style scoped>
.responsive-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.controls label {
display: block;
margin-bottom: 0.4rem;
font-size: 0.9rem;
}
.controls input[type='range'] {
width: 100%;
}
.preview {
border: 1px dashed var(--vp-c-divider);
margin-top: 1rem;
padding: 0.75rem;
border-radius: 10px;
background: var(--vp-c-bg);
overflow: hidden;
max-width: 100%;
}
.grid {
display: grid;
gap: 0.6rem;
}
.card {
background: rgba(59, 130, 246, 0.12);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: 8px;
padding: 0.75rem;
font-size: 0.9rem;
text-align: center;
}
.note {
margin-top: 0.75rem;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
</style>
@@ -0,0 +1,159 @@
<!--
RoutingModeDemo.vue
MPA vs SPA 路由体验对比
-->
<template>
<div class="routing-demo">
<div class="header">
<div class="title">路由方式整页刷新 vs 局部切换</div>
<div class="subtitle">点击导航感受体验差异</div>
</div>
<div class="mode-switch">
<button
class="mode"
:class="{ active: mode === 'mpa' }"
@click="mode = 'mpa'"
>
传统多页 (MPA)
</button>
<button
class="mode"
:class="{ active: mode === 'spa' }"
@click="mode = 'spa'"
>
单页应用 (SPA)
</button>
</div>
<div class="nav">
<button v-for="page in pages" :key="page" @click="navigate(page)">
{{ page }}
</button>
</div>
<div class="screen">
<div v-if="loading" class="loading">页面加载中...</div>
<div v-else class="content">
当前页面<strong>{{ currentPage }}</strong>
</div>
</div>
<div class="hint">
{{ hintText }}
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const mode = ref('mpa')
const pages = ['首页', '商品', '购物车', '个人中心']
const currentPage = ref('首页')
const loading = ref(false)
const hintText = computed(() =>
mode.value === 'mpa'
? 'MPA:每次切换都要整页刷新'
: 'SPA:只更新内容区域,状态可保留'
)
const navigate = (page) => {
loading.value = true
const delay = mode.value === 'mpa' ? 700 : 160
setTimeout(() => {
currentPage.value = page
loading.value = false
}, delay)
}
</script>
<style scoped>
.routing-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.mode-switch {
display: flex;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.mode {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
padding: 0.4rem 0.8rem;
border-radius: 999px;
font-size: 0.85rem;
cursor: pointer;
}
.mode.active {
border-color: #3b82f6;
color: #1d4ed8;
background: rgba(59, 130, 246, 0.12);
}
.nav {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.nav button {
border: none;
background: var(--vp-c-brand);
color: white;
padding: 0.35rem 0.7rem;
border-radius: 8px;
font-size: 0.85rem;
cursor: pointer;
}
.screen {
margin-top: 1rem;
border: 1px dashed var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
background: var(--vp-c-bg);
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.loading {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.content {
font-size: 0.95rem;
}
.hint {
margin-top: 0.75rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
</style>
@@ -16,9 +16,15 @@
</div>
<div class="panel">
<div class="row"><span class="label">用途</span><span>{{ current.purpose }}</span></div>
<div class="row"><span class="label">类型</span><span>{{ current.display }}</span></div>
<div class="row"><span class="label">常见位置</span><span>{{ current.scene }}</span></div>
<div class="row">
<span class="label">用途</span><span>{{ current.purpose }}</span>
</div>
<div class="row">
<span class="label">类型</span><span>{{ current.display }}</span>
</div>
<div class="row">
<span class="label">常见位置</span><span>{{ current.scene }}</span>
</div>
<div class="row code-title">示例</div>
<pre><code>{{ current.example }}</code></pre>
<div class="row code-title">渲染效果</div>
@@ -102,21 +108,80 @@ const current = ref(tags[0])
</script>
<style scoped>
.semantic { border: 1px solid var(--vp-c-divider); border-radius: 12px; background: var(--vp-c-bg-soft); padding: 16px; margin: 20px 0; display: grid; grid-template-columns: 1fr 2fr; gap: 12px; }
@media (max-width: 720px) { .semantic { grid-template-columns: 1fr; } }
.tags { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; }
.tag-btn { padding: 10px 12px; border-radius: 10px; border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg); cursor: pointer; text-align: left; }
.tag-btn.active { border-color: var(--vp-c-brand); color: var(--vp-c-brand); box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
.panel { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-radius: 10px; padding: 12px; display: flex; flex-direction: column; gap: 8px; }
.row { display: flex; justify-content: space-between; gap: 8px; font-size: 14px; }
.label { color: var(--vp-c-text-2); font-weight: 700; }
.code-title { font-weight: 700; margin-top: 4px; }
pre { margin: 0; background: #0b1221; color: #e5e7eb; border-radius: 8px; padding: 10px; font-family: var(--vp-font-family-mono); font-size: 13px; white-space: pre-wrap; }
.semantic {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 16px;
margin: 20px 0;
display: grid;
grid-template-columns: 1fr 2fr;
gap: 12px;
}
@media (max-width: 720px) {
.semantic {
grid-template-columns: 1fr;
}
}
.tags {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 8px;
}
.tag-btn {
padding: 10px 12px;
border-radius: 10px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
cursor: pointer;
text-align: left;
}
.tag-btn.active {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.panel {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.row {
display: flex;
justify-content: space-between;
gap: 8px;
font-size: 14px;
}
.label {
color: var(--vp-c-text-2);
font-weight: 700;
}
.code-title {
font-weight: 700;
margin-top: 4px;
}
pre {
margin: 0;
background: #0b1221;
color: #e5e7eb;
border-radius: 8px;
padding: 10px;
font-family: var(--vp-font-family-mono);
font-size: 13px;
white-space: pre-wrap;
}
.preview-box {
border: 1px dashed var(--vp-c-divider);
padding: 16px;
border-radius: 8px;
background: var(--vp-c-bg);
}
.tip { color: var(--vp-c-text-2); font-size: 13px; }
.tip {
color: var(--vp-c-text-2);
font-size: 13px;
}
</style>
@@ -0,0 +1,131 @@
<!--
SliceRequestDemo.vue
切图时代的请求数与加载时间演示
-->
<template>
<div class="slice-demo">
<div class="header">
<div class="title">切图时代请求数越多越慢</div>
<div class="subtitle">调整切图数量观察加载时间变化</div>
</div>
<div class="controls">
<label>
切图数量<strong>{{ slices }}</strong>
</label>
<input v-model="slices" type="range" min="1" max="30" step="1" />
<label class="toggle">
<input v-model="useSprite" type="checkbox" />
合并雪碧图 (Sprite)
</label>
</div>
<div class="metrics">
<div class="metric">
<div class="label">总请求数</div>
<div class="value">{{ totalRequests }}</div>
</div>
<div class="metric">
<div class="label">预计加载时间</div>
<div class="value">{{ loadTime }} ms</div>
</div>
</div>
<div class="bar">
<div class="progress" :style="{ width: barWidth + '%' }"></div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const slices = ref(12)
const useSprite = ref(false)
const totalRequests = computed(() => {
const sliceRequests = useSprite.value ? 1 : slices.value
return sliceRequests + 2
})
const loadTime = computed(() => {
const base = 120
const perRequest = 45
return Math.round(base + totalRequests.value * perRequest)
})
const barWidth = computed(() => Math.min(100, Math.round(loadTime.value / 20)))
</script>
<style scoped>
.slice-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.controls label {
display: block;
margin-bottom: 0.4rem;
font-size: 0.9rem;
}
.controls input[type='range'] {
width: 100%;
margin-bottom: 0.6rem;
}
.toggle {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.metric .label {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.metric .value {
font-size: 1.2rem;
font-weight: 700;
margin-top: 0.2rem;
}
.bar {
height: 10px;
margin-top: 1rem;
background: var(--vp-c-bg);
border-radius: 999px;
overflow: hidden;
border: 1px solid var(--vp-c-divider);
}
.progress {
height: 100%;
background: linear-gradient(90deg, #f97316, #ef4444);
}
</style>
@@ -0,0 +1,309 @@
<!--
SpaStatePreservationDemo.vue
SPA vs MPA页面切换时状态是否保留的演示
-->
<template>
<div class="spa-state-demo">
<div class="header">
<div class="title">页面切换时输入会不会丢</div>
<div class="subtitle">
同样点击切换页面MPA 会像刷新一样清空SPA 会保留状态
</div>
</div>
<div class="mode-switch">
<button
class="mode"
:class="{ active: mode === 'mpa' }"
@click="switchMode('mpa')"
>
MPA整页刷新
</button>
<button
class="mode"
:class="{ active: mode === 'spa' }"
@click="switchMode('spa')"
>
SPA局部切换
</button>
<button class="reset" @click="resetAll">重置</button>
</div>
<div class="app">
<div class="nav">
<button
v-for="p in pages"
:key="p"
class="nav-btn"
:class="{ active: page === p }"
@click="go(p)"
>
{{ p }}
</button>
</div>
<div class="screen">
<div v-if="loading" class="loading">加载中...</div>
<div v-else class="content">
<div class="row">
当前页面<strong>{{ page }}</strong>
</div>
<div class="form">
<label>
备注模拟表单输入
<input v-model="note" type="text" placeholder="输入点东西试试" />
</label>
<div class="help">
提示切到别的页面再回来看看这段文字还在不在
</div>
</div>
<div class="row">
购物车数量模拟状态
<button class="small" @click="cart = Math.max(0, cart - 1)">
-
</button>
<strong class="num">{{ cart }}</strong>
<button class="small" @click="cart = cart + 1">+</button>
</div>
</div>
</div>
<div class="explain">
<div class="card">
<div class="label">你现在看到的现象</div>
<div class="value">{{ explainText }}</div>
</div>
<div class="card">
<div class="label">背后的原因一句话</div>
<div class="value">{{ reasonText }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const pages = ['首页', '商品', '购物车']
const mode = ref('mpa')
const page = ref('首页')
const loading = ref(false)
// 模拟用户输入/页面状态
const note = ref('我想买两杯奶茶')
const cart = ref(1)
const switchMode = (next) => {
mode.value = next
// 切模式时也模拟一次“回到首页”
go('首页')
}
const resetAll = () => {
mode.value = 'mpa'
page.value = '首页'
note.value = '我想买两杯奶茶'
cart.value = 1
loading.value = false
}
const go = (nextPage) => {
loading.value = true
// MPA:切换 = 类似刷新,状态丢失
if (mode.value === 'mpa') {
note.value = ''
cart.value = 0
}
const delay = mode.value === 'mpa' ? 650 : 150
setTimeout(() => {
page.value = nextPage
loading.value = false
}, delay)
}
const explainText = computed(() =>
mode.value === 'mpa'
? 'MPA:切换页面时像刷新,输入和状态经常会丢'
: 'SPA:切换页面只换内容区域,输入和状态更容易保留'
)
const reasonText = computed(() =>
mode.value === 'mpa'
? '因为浏览器加载了“新的页面”,旧页面的内存状态会被清掉'
: '因为还是“同一个页面”,只是 JavaScript 把内容换了一下'
)
</script>
<style scoped>
.spa-state-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.mode-switch {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.mode {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
padding: 0.4rem 0.8rem;
border-radius: 999px;
font-size: 0.85rem;
cursor: pointer;
}
.mode.active {
border-color: #3b82f6;
color: #1d4ed8;
background: rgba(59, 130, 246, 0.12);
}
.reset {
border: none;
background: var(--vp-c-brand);
color: #fff;
padding: 0.4rem 0.8rem;
border-radius: 999px;
font-size: 0.85rem;
cursor: pointer;
}
.app {
border: 1px dashed var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg);
padding: 1rem;
}
.nav {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 0.75rem;
}
.nav-btn {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
padding: 0.35rem 0.7rem;
border-radius: 8px;
font-size: 0.85rem;
cursor: pointer;
}
.nav-btn.active {
border-color: #22c55e;
background: rgba(34, 197, 94, 0.12);
color: #15803d;
}
.screen {
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 0.9rem;
min-height: 120px;
}
.loading {
color: var(--vp-c-text-2);
display: flex;
align-items: center;
justify-content: center;
min-height: 100px;
}
.content .row {
margin-bottom: 0.75rem;
font-size: 0.95rem;
}
.form label {
display: flex;
flex-direction: column;
gap: 0.35rem;
font-size: 0.9rem;
}
.form input {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 0.45rem 0.6rem;
font-size: 0.9rem;
}
.help {
margin-top: 0.35rem;
color: var(--vp-c-text-2);
font-size: 0.85rem;
}
.small {
border: none;
background: rgba(99, 102, 241, 0.15);
color: #4338ca;
padding: 0.2rem 0.55rem;
border-radius: 8px;
cursor: pointer;
margin: 0 0.35rem;
}
.num {
display: inline-block;
min-width: 2ch;
text-align: center;
}
.explain {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.75rem;
margin-top: 0.9rem;
}
.card {
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
}
.label {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.value {
margin-top: 0.25rem;
font-size: 0.9rem;
font-weight: 600;
line-height: 1.35;
}
</style>
@@ -2,10 +2,13 @@
<div class="tcp-handshake-demo">
<div class="controls">
<div class="status-indicator">
{{ t.statusLabel }}: <span :class="connectionStatus.toLowerCase()">{{ statusText }}</span>
{{ 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-if="step === 0" @click="startHandshake" class="action-btn">
{{ t.connect }}
</button>
<button v-else @click="reset" class="reset-btn">{{ t.reset }}</button>
</div>
</div>
@@ -19,7 +22,9 @@
</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 class="state-marker" :class="{ active: step >= 3 }">
ESTABLISHED
</div>
</div>
<!-- Interaction Area -->
@@ -63,7 +68,9 @@
</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 class="state-marker" :class="{ active: step >= 3 }">
ESTABLISHED
</div>
</div>
</div>
@@ -123,21 +130,21 @@ const currentDescription = computed(() => {
return t.steps[step.value] || ''
})
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const startHandshake = async () => {
if (step.value > 0) return
// Step 1: SYN
step.value = 1
showSyn.value = true
await wait(1500)
// Step 2: SYN-ACK
step.value = 2
showSynAck.value = true
await wait(1500)
// Step 3: ACK
step.value = 3
showAck.value = true
@@ -173,9 +180,15 @@ const reset = () => {
.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; }
.status-indicator span.closed {
color: var(--vp-c-text-3);
}
.status-indicator span.handshaking {
color: #f59e0b;
}
.status-indicator span.established {
color: #10b981;
}
.action-btn {
background: #3b82f6;
@@ -269,15 +282,24 @@ const reset = () => {
flex-direction: column;
align-items: center;
min-width: 120px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 10;
}
.packet.syn-ack { background: #f59e0b; }
.packet.ack { background: #10b981; }
.packet.syn-ack {
background: #f59e0b;
}
.packet.ack {
background: #10b981;
}
.packet-body { font-weight: bold; }
.packet-detail { font-size: 0.7rem; opacity: 0.9; }
.packet-body {
font-weight: bold;
}
.packet-detail {
font-size: 0.7rem;
opacity: 0.9;
}
/* Animations */
.slide-right-enter-active {
@@ -288,10 +310,20 @@ const reset = () => {
}
@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 */
0% {
transform: translateX(0);
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
transform: translateX(100%);
opacity: 1;
} /* Not quite right, need to stick */
}
/*
@@ -333,13 +365,25 @@ Here I want it to appear and move.
}
@keyframes moveRight {
from { transform: translateX(-100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
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; }
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.description-box {
@@ -10,46 +10,52 @@
<span class="lock-icon">🔒</span>
<!-- Segmented URL Display -->
<div class="segmented-url" v-if="parsedUrl">
<span
class="url-part protocol"
<span
class="url-part protocol"
:class="{ active: highlightedPart === 'protocol' }"
@mouseover="highlightedPart = 'protocol'"
@mouseleave="highlightedPart = null"
>{{ parts.protocol }}:</span>
>{{ parts.protocol }}:</span
>
<span class="divider">//</span>
<span
<span
class="url-part host"
:class="{ active: highlightedPart === 'host' }"
@mouseover="highlightedPart = 'host'"
@mouseleave="highlightedPart = null"
>{{ parts.host }}</span>
<span
>{{ 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
>:{{ parts.port }}</span
>
<span
class="url-part pathname"
:class="{ active: highlightedPart === 'pathname' }"
@mouseover="highlightedPart = 'pathname'"
@mouseleave="highlightedPart = null"
>{{ parts.pathname }}</span>
<span
>{{ 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
>{{ 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>
>{{ parts.hash }}</span
>
</div>
<input
v-else
@@ -63,8 +69,8 @@
<div class="visualization-area">
<div v-if="parsedUrl" class="url-breakdown">
<div
v-for="(part, key) in parts"
<div
v-for="(part, key) in parts"
:key="key"
class="url-segment"
:class="[key, { active: highlightedPart === key }]"
@@ -79,9 +85,7 @@
<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-state">Invalid URL format / 无效的 URL 格式</div>
</div>
</div>
</template>
@@ -139,7 +143,9 @@ 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
@@ -183,7 +189,7 @@ const parts = computed(() => {
display: flex;
align-items: center;
gap: 0.5rem;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
font-size: 0.9rem;
overflow: hidden;
}
@@ -207,23 +213,48 @@ const parts = computed(() => {
font-weight: bold;
}
.url-part:hover, .url-part.active {
.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.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; }
.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);
@@ -254,29 +285,53 @@ const parts = computed(() => {
.url-segment.active {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
/* 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.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; }
.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;
align-items: center;
gap: 0.5rem;
border-bottom: 1px solid rgba(0,0,0,0.05);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
padding-bottom: 0.5rem;
}
@@ -5,7 +5,10 @@
v-for="(stage, index) in stages"
:key="index"
class="tracker-node"
:class="{ active: currentStage === index, visited: currentStage > index }"
:class="{
active: currentStage === index,
visited: currentStage > index
}"
@click="currentStage = index"
>
<div class="node-circle">
@@ -14,7 +17,10 @@
<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
class="line-fill"
:style="{ width: (currentStage / (stages.length - 1)) * 100 + '%' }"
></div>
</div>
</div>
@@ -23,20 +29,15 @@
<h2>{{ stages[currentStage].title }}</h2>
<p>{{ stages[currentStage].desc }}</p>
</div>
<div class="component-wrapper">
<transition name="fade" mode="out-in">
<component
:is="stages[currentStage].component"
:key="currentStage"
/>
<component :is="stages[currentStage].component" :key="currentStage" />
</transition>
</div>
<div class="action-footer" v-if="currentStage < stages.length - 1">
<button class="next-btn" @click="nextStage">
下一步
</button>
<button class="next-btn" @click="nextStage">下一步 </button>
</div>
</div>
</div>
@@ -99,7 +100,7 @@ const nextStage = () => {
background-color: var(--vp-c-bg-soft);
overflow: hidden;
margin: 2rem 0;
box-shadow: 0 4px 24px rgba(0,0,0,0.05);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.05);
}
.stage-tracker {
@@ -222,13 +223,13 @@ const nextStage = () => {
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.next-btn:hover {
background: var(--vp-c-brand-dark);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0,0,0,0.15);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
}
.fade-enter-active,
@@ -0,0 +1,352 @@
<!--
VueReactComparisonDemo.vue
用可视化方式对比 Vue vs React语法状态更新渲染心智模型
-->
<template>
<div class="vr-demo">
<div class="header">
<div class="title">Vue vs React它们哪里像哪里不一样</div>
<div class="subtitle">
选一个标签页然后点+1看看背后发生了什么示意
</div>
</div>
<div class="tabs">
<button
v-for="t in tabs"
:key="t.key"
class="tab"
:class="{ active: currentTab === t.key }"
@click="currentTab = t.key"
>
{{ t.label }}
</button>
</div>
<div class="grid">
<div class="panel">
<div class="panel-title">Vue</div>
<div class="preview">
<div class="row">
count: <strong>{{ count }}</strong>
</div>
<button class="btn vue" @click="inc('vue')">+1</button>
</div>
<div class="code">
<div class="code-title">典型写法示意</div>
<pre><code class="language-vue">{{ vueCode }}</code></pre>
</div>
</div>
<div class="panel">
<div class="panel-title">React</div>
<div class="preview">
<div class="row">
count: <strong>{{ count }}</strong>
</div>
<button class="btn react" @click="inc('react')">+1</button>
</div>
<div class="code">
<div class="code-title">典型写法示意</div>
<pre><code class="language-jsx">{{ reactCode }}</code></pre>
</div>
</div>
</div>
<div class="what">
<div class="what-title">点击 +1 时发生了什么</div>
<div class="steps">
<div
v-for="(s, idx) in steps"
:key="idx"
class="step"
:class="{ highlight: idx === lastStepIndex }"
>
<span class="num">{{ idx + 1 }}</span>
<span class="text">{{ s }}</span>
</div>
</div>
<div class="note">
说明这是为了建立心智模型的<strong>简化示意</strong>真实框架内部更复杂
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const tabs = [
{ key: 'syntax', label: '语法(Template vs JSX' },
{ key: 'state', label: '状态更新(ref vs useState' },
{ key: 'render', label: '渲染心智模型' }
]
const currentTab = ref('syntax')
const count = ref(1)
const lastClicked = ref('vue')
const lastStepIndex = ref(-1)
const inc = (who) => {
lastClicked.value = who
count.value += 1
// 简单动画:把最后一步高亮一下
lastStepIndex.value = 2
setTimeout(() => (lastStepIndex.value = -1), 600)
}
const vueCode = computed(() => {
if (currentTab.value === 'syntax') {
// NOTE: Avoid literal closing script tag inside a script block (HTML parser would terminate early).
return [
`<template>`,
` <button @click="count++">+1</button>`,
` <div>count: {{ count }}</div>`,
`</template>`,
``,
`<script setup>`,
`import { ref } from 'vue'`,
`const count = ref(1)`,
`</scr` + `ipt>`
].join('\n')
}
if (currentTab.value === 'state') {
return `import { ref } from 'vue'
const count = ref(1)
function inc() {
count.value++
}`
}
return `// Vue:响应式系统会“追踪依赖”
// count 变了 -> 用到 count 的地方自动更新`
})
const reactCode = computed(() => {
if (currentTab.value === 'syntax') {
return `function App() {
const [count, setCount] = useState(1)
return (
<>
<button onClick={() => setCount(count + 1)}>+1</button>
<div>count: {count}</div>
</>
)
}`
}
if (currentTab.value === 'state') {
return `const [count, setCount] = useState(1)
function inc() {
setCount(count + 1)
}`
}
return `// Reactstate 变了 -> 组件函数重新执行(重新渲染)
// 然后 React 决定哪些 DOM 需要更新`
})
const steps = computed(() => {
if (currentTab.value === 'syntax') {
return [
'你写 UI 的方式:Vue 常用 TemplateReact 常用 JSX',
'点击按钮触发事件处理函数',
'count 更新后,界面显示跟着变'
]
}
if (currentTab.value === 'state') {
return [
'Vue:用 ref/ reactive 保存状态;React:用 useState 保存状态',
lastClicked.value === 'vue'
? '你修改了 count.value'
: '你调用 setCount(...)',
'框架把变化反映到界面'
]
}
return [
'Vue:更偏“依赖追踪”,谁用到了 count,就更新谁',
'React:更偏“重新执行组件函数”,得到新的 UI 描述',
'最终都会只更新需要变化的 DOM(避免全量重画)'
]
})
</script>
<style scoped>
.vr-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.tabs {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.tab {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
padding: 0.35rem 0.75rem;
border-radius: 999px;
font-size: 0.85rem;
cursor: pointer;
}
.tab.active {
border-color: #3b82f6;
color: #1d4ed8;
background: rgba(59, 130, 246, 0.12);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1rem;
}
.panel {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 12px;
padding: 1rem;
}
.panel-title {
font-weight: 800;
margin-bottom: 0.75rem;
}
.preview {
border: 1px dashed var(--vp-c-divider);
border-radius: 10px;
padding: 0.9rem;
background: var(--vp-c-bg-soft);
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.row {
font-size: 0.95rem;
}
.btn {
border: none;
padding: 0.45rem 0.8rem;
border-radius: 10px;
color: #fff;
cursor: pointer;
font-weight: 700;
font-size: 0.85rem;
}
.btn.vue {
background: #22c55e;
}
.btn.react {
background: #0ea5e9;
}
.code {
margin-top: 0.9rem;
}
.code-title {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.35rem;
}
pre {
margin: 0;
padding: 0.75rem;
border-radius: 10px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
overflow: auto;
}
code {
font-family: var(--vp-font-family-mono);
font-size: 0.8rem;
color: var(--vp-c-text-1);
}
.what {
margin-top: 1rem;
border-top: 1px dashed var(--vp-c-divider);
padding-top: 1rem;
}
.what-title {
font-weight: 700;
margin-bottom: 0.6rem;
}
.steps {
display: grid;
gap: 0.5rem;
}
.step {
display: flex;
gap: 0.6rem;
align-items: flex-start;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 10px;
padding: 0.55rem 0.65rem;
}
.step.highlight {
border-color: rgba(34, 197, 94, 0.5);
background: rgba(34, 197, 94, 0.08);
}
.num {
width: 1.6rem;
height: 1.6rem;
border-radius: 999px;
background: rgba(99, 102, 241, 0.15);
color: #4338ca;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-size: 0.85rem;
flex: 0 0 auto;
}
.text {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.35;
}
.note {
margin-top: 0.7rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
</style>
@@ -34,7 +34,11 @@
<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" />
<input
type="text"
v-model="customColors.primary"
class="hex-input"
/>
</div>
</div>
<div class="config-item">
@@ -48,14 +52,20 @@
<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" />
<input
type="text"
v-model="customColors.btnText"
class="hex-input"
/>
</div>
</div>
</div>
</div>
<div class="preview" :class="current">
<div class="hint">点一下标题/段落/按钮我会在下面的代码里高亮对应行</div>
<div class="hint">
点一下标题/段落/按钮我会在下面的代码里高亮对应行
</div>
<h1
class="hero"
:class="{ selected: selectedPart === 'h1' }"
@@ -75,7 +85,7 @@
<button
class="cta"
:class="{ selected: selectedPart === 'btn' }"
@click="selectedPart = 'btn'; increment()"
@click="handleBtnClick"
>
<span class="badge"></span>
点我试试看 ({{ clicks }})
@@ -88,9 +98,9 @@
<div class="code-block">
<div class="code-title">{{ codeTitle }}</div>
<div class="code-content">
<div
v-for="(line, i) in codeLines"
:key="i"
<div
v-for="(line, i) in codeLines"
:key="i"
:class="['line', { hl: line.key === selectedPart }]"
>
{{ line.text }}
@@ -198,9 +208,15 @@ const codeLines = computed(() => {
}
if (current.value === 'css') {
return [
{ key: 'h1', text: `.hero { color: ${DEMO_CONFIG.value.colors.primary}; font-size: 24px; }` },
{
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; }` }
{
key: 'btn',
text: `.cta { background: ${DEMO_CONFIG.value.colors.primary}; color: ${DEMO_CONFIG.value.colors.btnText}; border-radius: 10px; }`
}
]
}
return [
@@ -259,7 +275,8 @@ const steps = computed(() => {
const oneLine = computed(() => {
if (current.value === 'html') return '先把“有哪些东西、是什么东西”说清楚。'
if (current.value === 'css') return '在不改结构的前提下,把外观调到你想要的样子。'
if (current.value === 'css')
return '在不改结构的前提下,把外观调到你想要的样子。'
return '把“点击/输入”等行为接上逻辑,让页面能互动。'
})
@@ -269,6 +286,11 @@ const increment = () => {
if (current.value !== 'js') return
clicks.value++
}
const handleBtnClick = () => {
selectedPart.value = 'btn'
increment()
}
</script>
<style scoped>
@@ -333,7 +355,7 @@ const increment = () => {
border: 1px solid var(--vp-c-divider);
}
input[type="color"] {
input[type='color'] {
width: 28px;
height: 28px;
border: none;
@@ -368,11 +390,28 @@ input[type="color"] {
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; }
.top {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.title {
font-weight: 800;
font-size: 16px;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 13px;
margin-top: 4px;
}
.modes { display: flex; gap: 8px; flex-wrap: wrap; }
.modes {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.mode {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
@@ -382,10 +421,12 @@ input[type="color"] {
font-size: 13px;
transition: all 0.2s;
}
.mode:hover { background: var(--vp-c-bg-soft); }
.mode.active {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
.mode:hover {
background: var(--vp-c-bg-soft);
}
.mode.active {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
background: var(--vp-c-brand-dimm);
}
@@ -400,7 +441,11 @@ input[type="color"] {
transition: all 0.2s;
}
.hint { color: var(--vp-c-text-2); font-size: 13px; margin-bottom: 8px; }
.hint {
color: var(--vp-c-text-2);
font-size: 13px;
margin-bottom: 8px;
}
.badge {
display: inline-flex;
align-items: center;
@@ -416,8 +461,21 @@ input[type="color"] {
flex-shrink: 0;
}
.hero { margin: 0; cursor: pointer; display: flex; align-items: center; line-height: 1.4; }
.desc { margin: 0; color: var(--vp-c-text-2); cursor: pointer; display: flex; align-items: center; line-height: 1.5; }
.hero {
margin: 0;
cursor: pointer;
display: flex;
align-items: center;
line-height: 1.4;
}
.desc {
margin: 0;
color: var(--vp-c-text-2);
cursor: pointer;
display: flex;
align-items: center;
line-height: 1.5;
}
.cta {
width: fit-content;
border: 1px solid var(--vp-c-divider);
@@ -437,22 +495,48 @@ input[type="color"] {
border-radius: 4px;
}
.click-tip { margin-top: 6px; color: var(--vp-c-text-2); font-size: 13px; }
.click-tip {
margin-top: 6px;
color: var(--vp-c-text-2);
font-size: 13px;
}
.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');
.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); }
.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); }
.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);
}
.code-content {
background: #0b1221;
color: #e5e7eb;
@@ -464,8 +548,10 @@ input[type="color"] {
line-height: 1.6;
}
.line { min-height: 1.6em; }
.hl {
.line {
min-height: 1.6em;
}
.hl {
background: var(--vp-c-brand-dimm);
border-left: 3px solid var(--vp-c-brand);
border-radius: 4px;
@@ -477,12 +563,33 @@ input[type="color"] {
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; }
.card-title { font-weight: 700; margin-bottom: 4px; }
.card-body { color: var(--vp-c-text-2); font-size: 13px; line-height: 1.5; }
.explain {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
}
.card {
background: var(--vp-c-bg);
border: 1px dashed var(--vp-c-divider);
border-radius: 10px;
padding: 10px;
}
.card-title {
font-weight: 700;
margin-bottom: 4px;
}
.card-body {
color: var(--vp-c-text-2);
font-size: 13px;
line-height: 1.5;
}
.map { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }
.map {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 8px;
}
.map-row {
display: flex;
justify-content: space-between;
@@ -498,9 +605,17 @@ input[type="color"] {
border-color: var(--vp-c-brand);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.left { font-weight: 800; }
.right { color: var(--vp-c-text-2); }
.steps { margin: 8px 0 0 18px; color: var(--vp-c-text-2); line-height: 1.6; }
.left {
font-weight: 800;
}
.right {
color: var(--vp-c-text-2);
}
.steps {
margin: 8px 0 0 18px;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.one-line {
background: var(--vp-c-bg);
@@ -509,6 +624,10 @@ input[type="color"] {
padding: 10px 12px;
font-size: 14px;
}
.one-line-title { font-weight: 800; }
.one-line-body { color: var(--vp-c-text-2); }
.one-line-title {
font-weight: 800;
}
.one-line-body {
color: var(--vp-c-text-2);
}
</style>