feat: update docs and components, fix DLQ demo bug
This commit is contained in:
@@ -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"><!DOCTYPE html></div>
|
||||
<div class="line" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'html' }" @mouseenter="hoveredPart = 'html'" @mouseleave="hoveredPart = null"><html></div>
|
||||
<div class="line indent" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'body' }" @mouseenter="hoveredPart = 'body'" @mouseleave="hoveredPart = null"><body></div>
|
||||
<div class="line indent-2" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'card' }" @mouseenter="hoveredPart = 'card'" @mouseleave="hoveredPart = null"><div class="card"></div>
|
||||
<div class="line indent-3" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'img' }" @mouseenter="hoveredPart = 'img'" @mouseleave="hoveredPart = null"><img class="icon" src="castle.png" /></div>
|
||||
<div class="line indent-3" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'title' }" @mouseenter="hoveredPart = 'title'" @mouseleave="hoveredPart = null"><h2 class="title">乐高城堡</h2></div>
|
||||
<div class="line indent-3" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'btn' }" @mouseenter="hoveredPart = 'btn'" @mouseleave="hoveredPart = null"><button class="btn">购买</button></div>
|
||||
<div class="line indent-2" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'card' }" @mouseenter="hoveredPart = 'card'" @mouseleave="hoveredPart = null"></div></div>
|
||||
<div class="line indent" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'body' }" @mouseenter="hoveredPart = 'body'" @mouseleave="hoveredPart = null"></body></div>
|
||||
<div class="line" :class="{ active: currentStep >= 0, hovered: hoveredPart === 'html' }" @mouseenter="hoveredPart = 'html'" @mouseleave="hoveredPart = null"></html></div>
|
||||
|
||||
<div
|
||||
class="line"
|
||||
:class="{
|
||||
active: currentStep >= 0,
|
||||
hovered: hoveredPart === 'html'
|
||||
}"
|
||||
@mouseenter="hoveredPart = 'html'"
|
||||
@mouseleave="hoveredPart = null"
|
||||
>
|
||||
<!DOCTYPE html>
|
||||
</div>
|
||||
<div
|
||||
class="line"
|
||||
:class="{
|
||||
active: currentStep >= 0,
|
||||
hovered: hoveredPart === 'html'
|
||||
}"
|
||||
@mouseenter="hoveredPart = 'html'"
|
||||
@mouseleave="hoveredPart = null"
|
||||
>
|
||||
<html>
|
||||
</div>
|
||||
<div
|
||||
class="line indent"
|
||||
:class="{
|
||||
active: currentStep >= 0,
|
||||
hovered: hoveredPart === 'body'
|
||||
}"
|
||||
@mouseenter="hoveredPart = 'body'"
|
||||
@mouseleave="hoveredPart = null"
|
||||
>
|
||||
<body>
|
||||
</div>
|
||||
<div
|
||||
class="line indent-2"
|
||||
:class="{
|
||||
active: currentStep >= 0,
|
||||
hovered: hoveredPart === 'card'
|
||||
}"
|
||||
@mouseenter="hoveredPart = 'card'"
|
||||
@mouseleave="hoveredPart = null"
|
||||
>
|
||||
<div class="card">
|
||||
</div>
|
||||
<div
|
||||
class="line indent-3"
|
||||
:class="{
|
||||
active: currentStep >= 0,
|
||||
hovered: hoveredPart === 'img'
|
||||
}"
|
||||
@mouseenter="hoveredPart = 'img'"
|
||||
@mouseleave="hoveredPart = null"
|
||||
>
|
||||
<img class="icon" src="castle.png" />
|
||||
</div>
|
||||
<div
|
||||
class="line indent-3"
|
||||
:class="{
|
||||
active: currentStep >= 0,
|
||||
hovered: hoveredPart === 'title'
|
||||
}"
|
||||
@mouseenter="hoveredPart = 'title'"
|
||||
@mouseleave="hoveredPart = null"
|
||||
>
|
||||
<h2 class="title">乐高城堡</h2>
|
||||
</div>
|
||||
<div
|
||||
class="line indent-3"
|
||||
:class="{
|
||||
active: currentStep >= 0,
|
||||
hovered: hoveredPart === 'btn'
|
||||
}"
|
||||
@mouseenter="hoveredPart = 'btn'"
|
||||
@mouseleave="hoveredPart = null"
|
||||
>
|
||||
<button class="btn">购买</button>
|
||||
</div>
|
||||
<div
|
||||
class="line indent-2"
|
||||
:class="{
|
||||
active: currentStep >= 0,
|
||||
hovered: hoveredPart === 'card'
|
||||
}"
|
||||
@mouseenter="hoveredPart = 'card'"
|
||||
@mouseleave="hoveredPart = null"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="line indent"
|
||||
:class="{
|
||||
active: currentStep >= 0,
|
||||
hovered: hoveredPart === 'body'
|
||||
}"
|
||||
@mouseenter="hoveredPart = 'body'"
|
||||
@mouseleave="hoveredPart = null"
|
||||
>
|
||||
</body>
|
||||
</div>
|
||||
<div
|
||||
class="line"
|
||||
:class="{
|
||||
active: currentStep >= 0,
|
||||
hovered: hoveredPart === 'html'
|
||||
}"
|
||||
@mouseenter="hoveredPart = 'html'"
|
||||
@mouseleave="hoveredPart = null"
|
||||
>
|
||||
</html>
|
||||
</div>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
|
||||
<!-- CSS Highlighted precisely based on step usage -->
|
||||
<!-- Layout properties -->
|
||||
<div class="line" :class="{ active: currentStep === 2, hovered: hoveredPart === 'card' }" @mouseenter="hoveredPart = 'card'" @mouseleave="hoveredPart = null">.card { display: flex; padding: 10px; }</div>
|
||||
<div class="line" :class="{ active: currentStep === 2, hovered: hoveredPart === 'img' }" @mouseenter="hoveredPart = 'img'" @mouseleave="hoveredPart = null">.icon { width: 50px; height: 50px; }</div>
|
||||
<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' }">
|
||||
<p>我是普通段落</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="html-line"
|
||||
:class="{ highlight: activeType === 'class' }"
|
||||
>
|
||||
|
||||
<div class="html-line" :class="{ highlight: activeType === 'class' }">
|
||||
<div <span class="attr">class="card"</span>>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="html-line indent"
|
||||
:class="{ highlight: activeType === 'tag' || activeType === 'class' }"
|
||||
|
||||
<div
|
||||
class="html-line indent"
|
||||
:class="{
|
||||
highlight: activeType === 'tag' || activeType === 'class'
|
||||
}"
|
||||
>
|
||||
<p>我是卡片里的段落</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="html-line"
|
||||
:class="{ highlight: activeType === 'class' }"
|
||||
>
|
||||
|
||||
<div class="html-line" :class="{ highlight: activeType === 'class' }">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="html-line"
|
||||
:class="{ highlight: activeType === 'id' }"
|
||||
>
|
||||
<button <span class="attr">id="submit-btn"</span>>提交</button>
|
||||
<div class="html-line" :class="{ highlight: activeType === 'id' }">
|
||||
<button
|
||||
<span class="attr">id="submit-btn"</span>>提交</button>
|
||||
</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>
|
||||
|
||||
+55
-19
@@ -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 />
|
||||
<div v-if="count > 5">...</div>
|
||||
</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 `// React:state 变了 -> 组件函数重新执行(重新渲染)
|
||||
// 然后 React 决定哪些 DOM 需要更新`
|
||||
})
|
||||
|
||||
const steps = computed(() => {
|
||||
if (currentTab.value === 'syntax') {
|
||||
return [
|
||||
'你写 UI 的方式:Vue 常用 Template;React 常用 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>
|
||||
|
||||
Reference in New Issue
Block a user