feat(docs): add performance overview demo component and update content structure

- Create PerformanceOverviewDemo.vue with interactive performance dimension visualization
- Update config.mjs to support new component registration
- Add new frontend evolution components to theme/index.js
- Consolidate stage-0 intro pages into index.md across all locales
- Enhance LLM intro documentation with tokenization details
This commit is contained in:
sanbuphy
2026-02-05 01:33:28 +08:00
parent 3c4a5c0e0b
commit e8bba6f7c0
27 changed files with 4375 additions and 873 deletions
@@ -0,0 +1,454 @@
<template>
<div class="be-quickstart-container">
<div class="be-stage-tabs">
<button
v-for="(stage, idx) in stages"
:key="idx"
:class="['be-stage-btn', { active: currentStage === idx }]"
@click="currentStage = idx"
>
<span class="be-stage-icon">{{ stage.icon }}</span>
<span class="be-stage-name">{{ stage.name }}</span>
<span class="be-stage-year">{{ stage.year }}</span>
</button>
</div>
<div class="be-stage-content">
<Transition name="be-fade" mode="out-in">
<div :key="currentStage" class="be-stage-panel">
<div class="be-visual-section">
<div class="be-arch-diagram">
<div
v-for="(node, idx) in currentStageData.nodes"
:key="idx"
:class="['be-arch-node', node.type]"
:style="node.style"
>
<div class="be-node-icon">{{ node.icon }}</div>
<div class="be-node-label">{{ node.label }}</div>
</div>
<svg class="be-connections" viewBox="0 0 600 300">
<path
v-for="(conn, idx) in currentStageData.connections"
:key="idx"
:d="conn.path"
:class="['be-conn-line', conn.type]"
/>
</svg>
</div>
</div>
<div class="be-info-section">
<h3 class="be-section-title">💡 核心特点</h3>
<ul class="be-feature-list">
<li
v-for="(feature, idx) in currentStageData.features"
:key="idx"
:class="['be-feature-item', feature.type]"
>
<span class="be-feature-icon">{{ feature.icon }}</span>
<span class="be-feature-text">{{ feature.text }}</span>
</li>
</ul>
<div class="be-analogy-box">
<h4>🏪 餐厅类比</h4>
<p>{{ currentStageData.analogy }}</p>
</div>
</div>
</div>
</Transition>
</div>
<div class="be-progress-bar">
<div
class="be-progress-fill"
:style="{ width: ((currentStage + 1) / stages.length) * 100 + '%' }"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentStage = ref(0)
const stages = [
{ name: '物理时代', year: '1990s', icon: '🖥️' },
{ name: '单体架构', year: '2000s', icon: '🏢' },
{ name: '微服务', year: '2010s', icon: '🐜' },
{ name: 'Serverless', year: '2020s', icon: '☁️' }
]
const stageData = [
{
nodes: [
{ icon: '🌐', label: '用户请求', type: 'user', style: { left: '20px', top: '120px' } },
{ icon: '🖥️', label: '物理服务器', type: 'server', style: { left: '220px', top: '80px' } },
{ icon: '📁', label: '静态文件', type: 'file', style: { left: '420px', top: '60px' } },
{ icon: '⚙️', label: 'CGI脚本', type: 'script', style: { left: '420px', top: '160px' } }
],
connections: [
{ path: 'M 80 140 Q 150 140 220 120', type: 'http' },
{ path: 'M 320 100 Q 370 80 420 80', type: 'read' },
{ path: 'M 320 130 Q 370 160 420 180', type: 'exec' }
],
features: [
{ icon: '🐢', text: '手动部署,更新慢', type: 'con' },
{ icon: '💰', text: '扩容只能买更大的机器', type: 'con' },
{ icon: '🔧', text: 'FTP上传,配置复杂', type: 'con' }
],
analogy: '像一家小餐馆,只有一个大厨。所有活都要他自己干:洗菜、切菜、炒菜。客人多了就忙不过来,只能买更大的厨房。'
},
{
nodes: [
{ icon: '🌐', label: '用户请求', type: 'user', style: { left: '20px', top: '120px' } },
{ icon: '🏢', label: '单体应用', type: 'app', style: { left: '200px', top: '100px', width: '140px', height: '100px' } },
{ icon: '👤', label: '用户模块', type: 'module', style: { left: '220px', top: '115px', transform: 'scale(0.7)' } },
{ icon: '🛒', label: '订单模块', type: 'module', style: { left: '270px', top: '115px', transform: 'scale(0.7)' } },
{ icon: '💳', label: '支付模块', type: 'module', style: { left: '245px', top: '155px', transform: 'scale(0.7)' } },
{ icon: '🗄️', label: '数据库', type: 'db', style: { left: '420px', top: '120px' } }
],
connections: [
{ path: 'M 80 140 Q 140 140 200 150', type: 'http' },
{ path: 'M 340 150 Q 380 150 420 150', type: 'sql' }
],
features: [
{ icon: '✅', text: '开发简单,部署方便', type: 'pro' },
{ icon: '❌', text: '牵一发而动全身', type: 'con' },
{ icon: '🐌', text: '代码膨胀,启动慢', type: 'con' }
],
analogy: '像一个大型中央厨房,所有工序都在一个地方完成。好处是管理简单,坏处是如果洗菜区水管爆了,整个厨房都得停工。'
},
{
nodes: [
{ icon: '🌐', label: '用户请求', type: 'user', style: { left: '10px', top: '130px' } },
{ icon: '⚖️', label: '网关/负载均衡', type: 'gateway', style: { left: '120px', top: '130px' } },
{ icon: '👤', label: '用户服务', type: 'service', style: { left: '260px', top: '50px' } },
{ icon: '🛒', label: '订单服务', type: 'service', style: { left: '380px', top: '50px' } },
{ icon: '💳', label: '支付服务', type: 'service', style: { left: '320px', top: '130px' } },
{ icon: '📦', label: '库存服务', type: 'service', style: { left: '440px', top: '130px' } },
{ icon: '📊', label: '消息队列', type: 'mq', style: { left: '320px', top: '210px' } },
{ icon: '🗄️', label: '数据库集群', type: 'db-cluster', style: { left: '440px', top: '210px' } }
],
connections: [
{ path: 'M 70 150 L 120 150', type: 'http' },
{ path: 'M 190 140 Q 225 95 260 70', type: 'rpc' },
{ path: 'M 320 70 L 380 70', type: 'rpc' },
{ path: 'M 420 90 Q 400 110 380 130', type: 'rpc' },
{ path: 'M 220 160 Q 270 145 320 150', type: 'rpc' },
{ path: 'M 400 150 L 440 150', type: 'rpc' },
{ path: 'M 360 170 Q 360 190 360 210', type: 'async' },
{ path: 'M 480 170 Q 480 190 480 210', type: 'sql' }
],
features: [
{ icon: '✅', text: '故障隔离,独立部署', type: 'pro' },
{ icon: '✅', text: '团队自治,技术异构', type: 'pro' },
{ icon: '❌', text: '分布式复杂度,治理难', type: 'con' }
],
analogy: '像一条流水线,每个环节都是一个独立的工作站。一个工作站坏了,其他还能继续工作。但要协调这么多工作站,需要复杂的管理系统(Kubernetes)。'
},
{
nodes: [
{ icon: '🌐', label: '用户请求', type: 'user', style: { left: '20px', top: '130px' } },
{ icon: '🔀', label: 'API 网关', type: 'gateway', style: { left: '150px', top: '130px' } },
{ icon: '⚡', label: '函数1\n验证', type: 'function', style: { left: '300px', top: '60px' } },
{ icon: '⚡', label: '函数2\n处理', type: 'function', style: { left: '420px', top: '60px' } },
{ icon: '⚡', label: '函数3\n存储', type: 'function', style: { left: '360px', top: '160px' } },
{ icon: '☁️', label: '托管服务', type: 'managed', style: { left: '520px', top: '100px', width: '70px', height: '80px' } },
{ icon: '🗄️', label: '云数据库', type: 'cloud-db', style: { left: '480px', top: '210px' } }
],
connections: [
{ path: 'M 80 150 L 150 150', type: 'http' },
{ path: 'M 220 140 Q 260 100 300 80', type: 'invoke' },
{ path: 'M 360 80 L 420 80', type: 'chain' },
{ path: 'M 350 110 Q 360 135 360 160', type: 'invoke' },
{ path: 'M 480 80 L 520 110', type: 'baas' },
{ path: 'M 440 190 Q 460 200 480 220', type: 'db' }
],
features: [
{ icon: '✅', text: '零运维,自动扩缩容', type: 'pro' },
{ icon: '✅', text: '按量付费,成本优化', type: 'pro' },
{ icon: '❌', text: '冷启动延迟,vendor锁定', type: 'con' }
],
analogy: '像外卖平台。你不用自己开餐厅(维护服务器),只需要提供菜谱(写函数)。平台负责找厨师、准备食材、送餐。有人点餐就现做,没人点餐就不花钱。'
}
]
const currentStageData = computed(() => stageData[currentStage.value])
</script>
<style scoped>
.be-quickstart-container {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 16px;
padding: 24px;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.be-stage-tabs {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 24px;
}
.be-stage-btn {
background: rgba(255, 255, 255, 0.05);
border: 2px solid transparent;
border-radius: 12px;
padding: 16px 12px;
color: #a0a0b0;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.be-stage-btn:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateY(-2px);
}
.be-stage-btn.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-color: rgba(255, 255, 255, 0.3);
color: #fff;
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.4);
}
.be-stage-icon {
font-size: 28px;
margin-bottom: 4px;
}
.be-stage-name {
font-size: 14px;
font-weight: 600;
}
.be-stage-year {
font-size: 11px;
opacity: 0.7;
}
.be-stage-content {
min-height: 400px;
}
.be-stage-panel {
display: grid;
grid-template-columns: 1.2fr 1fr;
gap: 24px;
}
.be-visual-section {
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
padding: 16px;
}
.be-arch-diagram {
position: relative;
height: 300px;
width: 100%;
}
.be-arch-node {
position: absolute;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
padding: 8px 12px;
text-align: center;
font-size: 11px;
font-weight: 600;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
min-width: 60px;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.be-arch-node:hover {
transform: scale(1.05);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
}
.be-arch-node.user {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.be-arch-node.service,
.be-arch-node.function {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.be-arch-node.db,
.be-arch-node.cloud-db {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.be-arch-node.gateway {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
}
.be-arch-node.mq {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
}
.be-arch-node.managed {
background: linear-gradient(135deg, #d299c2 0%, #fef9d7 100%);
}
.be-node-icon {
font-size: 16px;
}
.be-node-label {
font-size: 9px;
line-height: 1.2;
white-space: pre-line;
}
.be-connections {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.be-conn-line {
fill: none;
stroke: rgba(102, 126, 234, 0.4);
stroke-width: 2;
stroke-dasharray: 5, 5;
animation: be-flow 2s linear infinite;
}
@keyframes be-flow {
to {
stroke-dashoffset: -20;
}
}
.be-info-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.be-section-title {
font-size: 16px;
font-weight: 600;
color: #667eea;
margin: 0;
}
.be-feature-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.be-feature-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
font-size: 13px;
}
.be-feature-item.pro {
border-left: 3px solid #38ef7d;
}
.be-feature-item.con {
border-left: 3px solid #f5576c;
}
.be-feature-icon {
font-size: 16px;
}
.be-feature-text {
color: #c0c0d0;
}
.be-analogy-box {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 12px;
padding: 16px;
}
.be-analogy-box h4 {
font-size: 14px;
font-weight: 600;
color: #667eea;
margin: 0 0 8px 0;
}
.be-analogy-box p {
font-size: 13px;
color: #a0a0b0;
line-height: 1.6;
margin: 0;
}
.be-progress-bar {
height: 4px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
margin-top: 20px;
overflow: hidden;
}
.be-progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
border-radius: 2px;
transition: width 0.5s ease;
}
.be-fade-enter-active,
.be-fade-leave-active {
transition: all 0.4s ease;
}
.be-fade-enter-from {
opacity: 0;
transform: translateX(20px);
}
.be-fade-leave-to {
opacity: 0;
transform: translateX(-20px);
}
@media (max-width: 768px) {
.be-stage-tabs {
grid-template-columns: repeat(2, 1fr);
}
.be-stage-panel {
grid-template-columns: 1fr;
}
.be-arch-diagram {
height: 250px;
}
}
</style>
@@ -0,0 +1,490 @@
<!--
ImperativeVsDeclarativeDemo.vue
命令式 vs 声明式编程对比演示
用途
通过并排的交互式计数器直观展示 ImperativejQuery DeclarativeVue
在代码量和心智负担上的差异
交互功能
- 两个可交互的计数器
- 切换展示背后的代码实现
- 高亮显示 jQuery 需要手动更新的多个 DOM 节点 vs Vue 的自动绑定
-->
<template>
<div class="imperative-declarative-demo">
<div class="demo-header">
<div class="toggle-group">
<button
v-for="view in views"
:key="view.id"
:class="['toggle-btn', { active: currentView === view.id }]"
@click="currentView = view.id"
>
{{ view.label }}
</button>
</div>
</div>
<div class="comparison-container">
<!-- Imperative Side (jQuery) -->
<div class="side imperative-side">
<div class="side-header">
<span class="badge imperative">jQuery / Imperative</span>
<h4>"Tell me HOW"</h4>
</div>
<div class="demo-area">
<!-- The UI -->
<div class="counter-ui">
<div class="display-value" id="jq-display">{{ jqCount }}</div>
<div class="meters">
<div class="meter-label">Progress:</div>
<div class="meter-bar">
<div
class="meter-fill"
id="jq-meter"
:style="{ width: jqProgress + '%' }"
></div>
</div>
<div class="status-text" id="jq-status">
{{ jqStatus }}
</div>
</div>
<div class="controls">
<button class="btn-decrement" @click="updateJq(-1)">-</button>
<button class="btn-increment" @click="updateJq(1)">+</button>
</div>
</div>
<!-- The Code -->
<div v-show="currentView === 'code'" class="code-panel">
<div class="code-block imperative-code">
<pre><code>function updateCounter(change) {
// 1. Get current value
var count = parseInt($('#counter').text());
// 2. Calculate new value
var newCount = count + change;
// 3. Update DOM element 1
$('#counter').text(newCount);
// 4. Update DOM element 2
var progress = (newCount / 10) * 100;
$('#progress-bar').css('width', progress + '%');
// 5. Update DOM element 3
if (newCount > 5) {
$('#status').text('High!').addClass('warning');
} else {
$('#status').text('Normal').removeClass('warning');
}
// 6. Update DOM element 4...
// Oops! Forgot to update the color indicator!
}</code></pre>
</div>
</div>
</div>
<div class="pain-points" v-if="showAnalysis">
<div class="pain-point">
<span class="icon"></span>
<span>需要手动操作多个 DOM 元素</span>
</div>
<div class="pain-point">
<span class="icon">🐛</span>
<span>容易遗漏更新导致界面不一致</span>
</div>
<div class="pain-point">
<span class="icon">🍝</span>
<span>逻辑分散代码难以维护</span>
</div>
</div>
</div>
<!-- VS Divider -->
<div class="vs-divider">
<div class="vs-badge">VS</div>
</div>
<!-- Declarative Side (Vue) -->
<div class="side declarative-side">
<div class="side-header">
<span class="badge declarative">Vue / Declarative</span>
<h4>"Tell me WHAT"</h4>
</div>
<div class="demo-area">
<!-- The UI -->
<div class="counter-ui">
<div class="display-value">{{ vueCount }}</div>
<div class="meters">
<div class="meter-label">Progress:</div>
<div class="meter-bar">
<div
class="meter-fill"
:style="{ width: vueProgress + '%' }"
></div>
</div>
<div class="status-text" :class="{ warning: vueCount > 5 }">
{{ vueStatus }}
</div>
</div>
<div class="controls">
<button class="btn-decrement" @click="vueCount--">-</button>
<button class="btn-increment" @click="vueCount++">+</button>
</div>
</div>
<!-- The Code -->
<div v-show="currentView === 'code'" class="code-panel">
<div class="code-block declarative-code">
<pre><code>export default {
data() {
return {
count: 0
}
},
computed: {
// Automatically updates when count changes
progress() {
return (this.count / 10) * 100;
},
status() {
return this.count > 5 ? 'High!' : 'Normal';
},
isWarning() {
return this.count > 5;
}
}
}
// Template - just declare what the UI should look like
&lt;template&gt;
&lt;div class="status" :class="{ warning: isWarning }"&gt;
{{ status }}
&lt;/div&gt;
&lt;/template&gt;</code></pre>
</div>
</div>
</div>
<div class="benefits" v-if="showAnalysis">
<div class="benefit">
<span class="icon"></span>
<span>只关注数据不用手动操作 DOM</span>
</div>
<div class="benefit">
<span class="icon">🔄</span>
<span>数据变化自动同步到所有相关视图</span>
</div>
<div class="benefit">
<span class="icon">🧩</span>
<span>代码结构清晰易于维护</span>
</div>
</div>
</div>
</div>
<div class="demo-controls">
<button class="toggle-btn" @click="showAnalysis = !showAnalysis">
{{ showAnalysis ? '隐藏' : '显示' }}对比分析
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentView = ref('ui')
const showAnalysis = ref(false)
const jqCount = ref(0)
const vueCount = ref(0)
const views = [
{ id: 'ui', label: '仅显示界面' },
{ id: 'code', label: '显示代码' }
]
const jqProgress = computed(() => Math.min((jqCount.value / 10) * 100, 100))
const vueProgress = computed(() => Math.min((vueCount.value / 10) * 100, 100))
const jqStatus = computed(() => (jqCount.value > 5 ? 'High!' : 'Normal'))
const vueStatus = computed(() => (vueCount.value > 5 ? 'High!' : 'Normal'))
function updateJq(change) {
jqCount.value += change
}
</script>
<style scoped>
.imperative-declarative-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.demo-header {
margin-bottom: 1.5rem;
}
.toggle-group {
display: flex;
gap: 0.5rem;
justify-content: center;
}
.toggle-btn {
padding: 0.5rem 1rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg);
color: var(--vp-c-text-2);
cursor: pointer;
font-size: 0.875rem;
transition: all 0.2s;
}
.toggle-btn:hover {
border-color: var(--vp-c-brand);
}
.toggle-btn.active {
background-color: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.comparison-container {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 1.5rem;
align-items: stretch;
}
.side {
display: flex;
flex-direction: column;
gap: 1rem;
}
.side-header {
text-align: center;
}
.badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
}
.badge.imperative {
background-color: rgba(7, 105, 173, 0.2);
color: #0769ad;
}
.badge.declarative {
background-color: rgba(66, 184, 131, 0.2);
color: #2c8a5e;
}
.side-header h4 {
margin: 0.5rem 0 0;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-area {
flex: 1;
display: flex;
flex-direction: column;
gap: 1rem;
}
.counter-ui {
background-color: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.display-value {
font-size: 2rem;
font-weight: bold;
text-align: center;
color: var(--vp-c-brand);
}
.meters {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.meter-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.meter-bar {
height: 8px;
background-color: var(--vp-c-bg-alt);
border-radius: 4px;
overflow: hidden;
}
.meter-fill {
height: 100%;
background-color: var(--vp-c-brand);
transition: width 0.3s ease;
}
.status-text {
font-size: 0.75rem;
text-align: center;
color: var(--vp-c-text-2);
}
.status-text.warning {
color: #f87171;
font-weight: 600;
}
.controls {
display: flex;
gap: 0.5rem;
justify-content: center;
}
.controls button {
width: 36px;
height: 36px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg);
cursor: pointer;
font-size: 1rem;
transition: all 0.2s;
}
.controls button:hover:not(:disabled) {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.controls button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.code-panel {
background-color: var(--vp-c-bg-alt);
border-radius: 6px;
overflow: hidden;
}
.code-block {
margin: 0;
padding: 0.75rem;
overflow-x: auto;
}
.code-block pre {
margin: 0;
font-size: 0.75rem;
line-height: 1.5;
}
.imperative-code {
background-color: #1e1e2e;
color: #a6accd;
}
.imperative-code code {
font-family: 'Fira Code', 'Menlo', monospace;
}
.declarative-code {
background-color: #1e1e2e;
color: #a6accd;
}
.declarative-code code {
font-family: 'Fira Code', 'Menlo', monospace;
}
.vs-divider {
display: flex;
align-items: center;
justify-content: center;
}
.vs-badge {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #0769ad, #42b883);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.875rem;
}
.pain-points,
.benefits {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.pain-point,
.benefit {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border-radius: 6px;
font-size: 0.8125rem;
}
.pain-point {
background-color: rgba(248, 113, 113, 0.1);
color: #dc2626;
}
.benefit {
background-color: rgba(74, 222, 128, 0.1);
color: #16a34a;
}
.demo-controls {
display: flex;
justify-content: center;
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid var(--vp-c-divider);
}
@media (max-width: 768px) {
.comparison-container {
grid-template-columns: 1fr;
gap: 1rem;
}
.vs-divider {
display: none;
}
}
</style>
@@ -0,0 +1,804 @@
<!--
JQueryVsStateDemo.vue
jQuery vs 数据驱动对比演示 - 重构版
用途
"餐厅服务员"的比喻让零基础用户理解命令式 vs 声明式的区别
通过并排的交互式计数器直观展示两种编程范式的差异
-->
<template>
<div class="jquery-state-demo">
<div class="scenario-intro">
<div class="emoji-scene">🍽 👨🍳 📝</div>
<h4>餐厅服务员模拟器</h4>
<p>想象一下你在餐厅当服务员有两种工作方式你会选哪种</p>
</div>
<div class="comparison-container">
<!-- 左边jQuery 模式 -->
<div class="side-panel jquery-panel">
<div class="panel-header">
<div class="mode-badge jquery">
<span class="badge-icon">🏃</span>
<span class="badge-text">跑腿王模式</span>
</div>
<div class="mode-subtitle">命令式jQuery</div>
</div>
<div class="scenario-visual">
<div class="visual-label">后厨 吧台 收银台</div>
<div class="runner-path">
<div class="station kitchen" :class="{ active: jqActiveStation === 'kitchen' }">
<span class="station-icon">🍳</span>
<span class="station-name">后厨</span>
</div>
<div class="path-arrow" :class="{ active: jqActiveStation === 'bar' }"></div>
<div class="station bar" :class="{ active: jqActiveStation === 'bar' }">
<span class="station-icon">🥤</span>
<span class="station-name">吧台</span>
</div>
<div class="path-arrow" :class="{ active: jqActiveStation === 'cashier' }"></div>
<div class="station cashier" :class="{ active: jqActiveStation === 'cashier' }">
<span class="station-icon">💰</span>
<span class="station-name">收银</span>
</div>
</div>
</div>
<div class="demo-counter">
<div class="counter-display">
<div class="display-label">当前计数</div>
<div class="display-value">{{ jqCount }}</div>
</div>
<div class="counter-controls">
<button class="ctrl-btn decrement" @click="updateJq(-1)" :disabled="jqCount <= 0">
<span class="btn-icon"></span>
<span class="btn-label"> 1</span>
</button>
<button class="ctrl-btn increment" @click="updateJq(1)">
<span class="btn-icon"></span>
<span class="btn-label"> 1</span>
</button>
</div>
<div class="status-bars">
<div class="status-item">
<span class="status-label">进度条</span>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: jqProgress + '%' }"></div>
</div>
<span class="status-value">{{ jqProgress }}%</span>
</div>
<div class="status-item">
<span class="status-label">状态</span>
<span class="status-badge" :class="jqStatusClass">{{ jqStatus }}</span>
</div>
</div>
</div>
<div class="code-snippet">
<div class="snippet-header">
<span class="snippet-title">💻 代码实现</span>
<span class="snippet-lang">jQuery</span>
</div>
<pre class="snippet-code"><code>// 需要手动更新每个元素
function updateCounter(change) {
var count = parseInt($('#counter').text());
var newCount = count + change;
// 更新计数显示
$('#counter').text(newCount);
// 更新进度条
var progress = (newCount / 10) * 100;
$('#progress').css('width', progress + '%');
// 更新状态文字
if (newCount > 5) {
$('#status').text('高!').addClass('warning');
} else {
$('#status').text('正常').removeClass('warning');
}
// 如果忘了更新某个地方...
// 界面就会不一致!😱
}</code></pre>
</div>
<div class="pain-points">
<div class="pain-title">😫 痛点</div>
<ul class="pain-list">
<li>每次都要亲自跑三个地方更新</li>
<li>漏改一个地方界面就不一致</li>
<li>代码分散难以维护</li>
<li>累得半死还容易出错</li>
</ul>
</div>
</div>
<!-- VS 标识 -->
<div class="vs-divider">
<div class="vs-badge">VS</div>
</div>
<!-- 右边Vue 模式 -->
<div class="side-panel vue-panel">
<div class="panel-header">
<div class="mode-badge vue">
<span class="badge-icon">👔</span>
<span class="badge-text">指挥家模式</span>
</div>
<div class="mode-subtitle">声明式Vue</div>
</div>
<div class="scenario-visual">
<div class="visual-label">我只管改单子其他自动同步</div>
<div class="conductor-scene">
<div class="conductor">🎩</div>
<div class="orchestra">
<div class="musician" :class="{ playing: vueCount > 0 }">
<span class="musician-icon">🎸</span>
<span class="musician-role">计数</span>
</div>
<div class="musician" :class="{ playing: vueProgress > 0 }">
<span class="musician-icon">📊</span>
<span class="musician-role">进度</span>
</div>
<div class="musician" :class="{ playing: vueCount > 5 }">
<span class="musician-icon">🚦</span>
<span class="musician-role">状态</span>
</div>
</div>
</div>
</div>
<div class="demo-counter">
<div class="counter-display">
<div class="display-label">当前计数</div>
<div class="display-value">{{ vueCount }}</div>
</div>
<div class="counter-controls">
<button class="ctrl-btn decrement" @click="vueCount--" :disabled="vueCount <= 0">
<span class="btn-icon"></span>
<span class="btn-label"> 1</span>
</button>
<button class="ctrl-btn increment" @click="vueCount++">
<span class="btn-icon"></span>
<span class="btn-label"> 1</span>
</button>
</div>
<div class="status-bars">
<div class="status-item">
<span class="status-label">进度条</span>
<div class="progress-bar">
<div class="progress-fill vue" :style="{ width: vueProgress + '%' }"></div>
</div>
<span class="status-value">{{ vueProgress }}%</span>
</div>
<div class="status-item">
<span class="status-label">状态</span>
<span class="status-badge" :class="vueStatusClass">{{ vueStatus }}</span>
</div>
</div>
</div>
<div class="code-snippet">
<div class="snippet-header">
<span class="snippet-title">💻 代码实现</span>
<span class="snippet-lang">Vue</span>
</div>
<pre class="snippet-code"><code>// 只需要定义数据和规则
data() {
return {
count: 0
}
},
computed: {
// 进度自动计算
progress() {
return (this.count / 10) * 100;
},
// 状态自动判断
status() {
return this.count > 5 ? '高!' : '正常';
},
isWarning() {
return this.count > 5;
}
}
// 模板里只需要声明关系
&lt;template&gt;
&lt;div class="status" :class="{ warning: isWarning }"&gt;
{{ status }}
&lt;/div&gt;
&lt;/template&gt;</code></pre>
</div>
<div class="benefits">
<div class="benefit-title"> 优势</div>
<ul class="benefit-list">
<li>只需改数据不用手动更新每个地方</li>
<li>界面自动同步永远保持一致</li>
<li>代码结构清晰容易维护</li>
<li>轻松优雅不易出错</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// jQuery 模式的状态
const jqCount = ref(0)
const jqActiveStation = ref('')
// Vue 模式的状态
const vueCount = ref(0)
// jQuery 计算属性
const jqProgress = computed(() => Math.min((jqCount.value / 10) * 100, 100))
const jqStatus = computed(() => {
if (jqCount.value > 5) return '高!'
if (jqCount.value > 0) return '正常'
return '初始'
})
const jqStatusClass = computed(() => {
if (jqCount.value > 5) return 'warning'
if (jqCount.value > 0) return 'normal'
return 'initial'
})
// Vue 计算属性
const vueProgress = computed(() => Math.min((vueCount.value / 10) * 100, 100))
const vueStatus = computed(() => {
if (vueCount.value > 5) return '高!'
if (vueCount.value > 0) return '正常'
return '初始'
})
const vueStatusClass = computed(() => {
if (vueCount.value > 5) return 'warning'
if (vueCount.value > 0) return 'normal'
return 'initial'
})
// jQuery 更新函数(模拟需要手动更新多个地方)
const updateJq = async (change) => {
const newCount = jqCount.value + change
if (newCount < 0) return
// 模拟需要跑三个地方更新
// 第一站:后厨(计数)
jqActiveStation.value = 'kitchen'
await sleep(300)
jqCount.value = newCount
// 第二站:吧台(进度条)
jqActiveStation.value = 'bar'
await sleep(300)
// 第三站:收银台(状态)
jqActiveStation.value = 'cashier'
await sleep(300)
jqActiveStation.value = ''
}
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
</script>
<style scoped>
.jquery-state-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: linear-gradient(135deg, var(--vp-c-bg-soft) 0%, var(--vp-c-bg) 100%);
padding: 1.5rem;
margin: 1rem 0;
}
.scenario-intro {
text-align: center;
margin-bottom: 1.5rem;
padding: 1rem;
background: linear-gradient(135deg, rgba(255, 183, 77, 0.2), rgba(255, 138, 101, 0.2));
border-radius: 12px;
}
.emoji-scene {
font-size: 3rem;
margin-bottom: 0.5rem;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.scenario-intro h4 {
margin: 0.5rem 0;
color: var(--vp-c-text-1);
font-size: 1.2rem;
}
.scenario-intro p {
margin: 0;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.comparison-container {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 1rem;
align-items: stretch;
}
.side-panel {
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
overflow: hidden;
background: var(--vp-c-bg);
display: flex;
flex-direction: column;
}
.jquery-panel {
border-color: #ff7043;
}
.vue-panel {
border-color: #42b883;
}
.panel-header {
padding: 1rem;
text-align: center;
border-bottom: 1px solid var(--vp-c-divider);
}
.mode-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 9999px;
font-weight: 600;
margin-bottom: 0.5rem;
}
.mode-badge.jquery {
background: linear-gradient(135deg, #ff7043, #f4511e);
color: white;
}
.mode-badge.vue {
background: linear-gradient(135deg, #42b883, #35495e);
color: white;
}
.badge-icon {
font-size: 1.25rem;
}
.mode-subtitle {
font-size: 0.875rem;
color: var(--vp-c-text-2);
}
.scenario-visual {
padding: 1rem;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.visual-label {
text-align: center;
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.runner-path {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.25rem;
}
.station {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.5rem;
border-radius: 8px;
background: #f5f5f5;
transition: all 0.3s;
min-width: 60px;
}
.station.active {
background: #ff7043;
color: white;
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(255, 112, 67, 0.4);
}
.station-icon {
font-size: 1.5rem;
}
.station-name {
font-size: 0.625rem;
margin-top: 0.25rem;
}
.path-arrow {
font-size: 1.5rem;
color: #ccc;
transition: all 0.3s;
}
.path-arrow.active {
color: #ff7043;
transform: translateX(5px);
}
.conductor-scene {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.conductor {
font-size: 3rem;
animation: conduct 2s ease-in-out infinite;
}
@keyframes conduct {
0%, 100% { transform: rotate(-10deg); }
50% { transform: rotate(10deg); }
}
.orchestra {
display: flex;
gap: 0.5rem;
justify-content: center;
}
.musician {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.5rem;
background: #f5f5f5;
border-radius: 8px;
transition: all 0.3s;
min-width: 60px;
}
.musician.playing {
background: #42b883;
color: white;
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(66, 184, 131, 0.4);
}
.musician-icon {
font-size: 1.5rem;
}
.musician-role {
font-size: 0.625rem;
margin-top: 0.25rem;
}
.demo-counter {
padding: 1rem;
flex: 1;
}
.counter-display {
text-align: center;
margin-bottom: 1rem;
}
.display-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.25rem;
}
.display-value {
font-size: 3rem;
font-weight: 700;
color: var(--vp-c-brand);
line-height: 1;
}
.counter-controls {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.ctrl-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
padding: 0.75rem;
border: none;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.ctrl-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.jquery-panel .ctrl-btn.decrement {
background: #ffccbc;
color: #bf360c;
}
.jquery-panel .ctrl-btn.increment {
background: #ff7043;
color: white;
}
.vue-panel .ctrl-btn.decrement {
background: #c8e6c9;
color: #2e7d32;
}
.vue-panel .ctrl-btn.increment {
background: #42b883;
color: white;
}
.ctrl-btn:not(:disabled):hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.btn-icon {
font-size: 1rem;
}
.status-bars {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.status-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.status-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
min-width: 50px;
}
.progress-bar {
flex: 1;
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
}
.jquery-panel .progress-fill {
background: linear-gradient(90deg, #ff7043, #f4511e);
}
.vue-panel .progress-fill {
background: linear-gradient(90deg, #42b883, #35495e);
}
.status-value {
font-size: 0.75rem;
color: var(--vp-c-text-2);
min-width: 35px;
text-align: right;
}
.status-badge {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
}
.status-badge.initial {
background: #f5f5f5;
color: #999;
}
.status-badge.normal {
background: #c8e6c9;
color: #2e7d32;
}
.status-badge.warning {
background: #ffccbc;
color: #bf360c;
}
.code-snippet {
margin: 1rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
}
.snippet-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0.75rem;
background: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
}
.snippet-title {
font-size: 0.875rem;
font-weight: 500;
color: var(--vp-c-text-1);
}
.snippet-lang {
font-size: 0.75rem;
padding: 0.125rem 0.5rem;
border-radius: 4px;
font-weight: 600;
}
.jquery-panel .snippet-lang {
background: #ff7043;
color: white;
}
.vue-panel .snippet-lang {
background: #42b883;
color: white;
}
.snippet-code {
margin: 0;
padding: 0.75rem;
background: #1e1e2e;
color: #a6accd;
font-size: 0.75rem;
line-height: 1.5;
overflow-x: auto;
}
.pain-points,
.benefits {
margin: 1rem;
padding: 1rem;
border-radius: 8px;
}
.pain-points {
background: #fff3e0;
border-left: 4px solid #ff7043;
}
.benefits {
background: #e8f5e9;
border-left: 4px solid #42b883;
}
.pain-title,
.benefit-title {
font-weight: 700;
margin-bottom: 0.5rem;
}
.pain-title {
color: #e65100;
}
.benefit-title {
color: #2e7d32;
}
.pain-list,
.benefit-list {
margin: 0;
padding-left: 1.25rem;
font-size: 0.875rem;
line-height: 1.6;
}
.pain-list li {
color: #bf360c;
margin-bottom: 0.25rem;
}
.benefit-list li {
color: #1b5e20;
margin-bottom: 0.25rem;
}
.vs-divider {
display: flex;
align-items: center;
justify-content: center;
}
.vs-badge {
width: 50px;
height: 50px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 1rem;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
@media (max-width: 1024px) {
.comparison-container {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.vs-divider {
order: -1;
}
.vs-badge {
width: 40px;
height: 40px;
font-size: 0.875rem;
}
}
@media (max-width: 640px) {
.runner-path,
.orchestra {
flex-wrap: wrap;
gap: 0.5rem;
}
.counter-controls {
flex-direction: column;
}
.status-item {
flex-wrap: wrap;
}
}
</style>
@@ -0,0 +1,532 @@
<!--
ResponsiveGridDemo.vue
响应式布局演示 - 重构版
用途
"智能变形相框"的比喻让零基础用户理解响应式设计
通过可拖动的滑块直观展示同一套代码如何适配不同屏幕尺寸
-->
<template>
<div class="responsive-demo">
<div class="scenario-intro">
<div class="emoji-scene">🖼 📱 💻</div>
<h4>智能变形相框</h4>
<p>想象你有一张照片它会自动调整大小和布局在任何相框里都好看</p>
</div>
<div class="device-presets">
<button
v-for="device in devices"
:key="device.id"
:class="['device-btn', { active: screenWidth === device.width }]"
@click="setDevice(device.width)"
>
<span class="device-icon">{{ device.icon }}</span>
<span class="device-name">{{ device.name }}</span>
<span class="device-size">{{ device.width }}px</span>
</button>
</div>
<div class="slider-control">
<div class="slider-labels">
<span>📱 手机</span>
<span>
<input
type="range"
v-model="screenWidth"
:min="320"
:max="1400"
step="10"
class="width-slider"
/>
</span>
<span>💻 电脑</span>
</div>
<div class="current-width">
当前宽度: <strong>{{ screenWidth }}px</strong> - {{ currentBreakpoint.name }}
</div>
</div>
<div class="viewport-preview">
<div class="viewport-header">
<span class="viewport-device">{{ currentDevice }}</span>
<span class="viewport-dots">
<span></span>
<span></span>
<span></span>
</span>
</div>
<div class="viewport-content" :style="{ width: screenWidth + 'px' }">
<div class="demo-grid" :class="gridClass">
<div
v-for="(item, index) in gridItems"
:key="index"
class="grid-item"
:style="{ animationDelay: (index * 0.1) + 's' }"
>
<div class="item-icon">{{ item.icon }}</div>
<div class="item-title">{{ item.title }}</div>
<div class="item-desc">{{ item.description }}</div>
</div>
</div>
</div>
</div>
<div class="breakpoint-indicator">
<div class="bp-track">
<div
v-for="bp in breakpoints"
:key="bp.name"
class="bp-point"
:class="{ active: screenWidth >= bp.min && screenWidth < (bp.max || 9999) }"
:style="{ left: ((bp.min - 320) / (1400 - 320)) * 100 + '%' }"
>
<span class="bp-label">{{ bp.name }}</span>
<span class="bp-range">{{ bp.min }}px</span>
</div>
</div>
</div>
<div class="code-preview">
<div class="code-header">
<span class="code-title">💻 响应式 CSS 代码</span>
<span class="code-lang">CSS</span>
</div>
<pre class="code-block"><code>/* 默认:手机端(单列) */
.grid {
display: grid;
gap: 1rem;
grid-template-columns: 1fr;
}
/* 平板端:双列(640px 以上) */
@media (min-width: 640px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* 电脑端:三列(1024px 以上) */
@media (min-width: 1024px) {
.grid {
grid-template-columns: repeat(3, 1fr);
}
}</code></pre>
</div>
<div class="tips-box">
<div class="tips-icon">🎯</div>
<div class="tips-content">
<strong>关键点</strong>
响应式布局通过 CSS 媒体查询Media Query自动适配不同屏幕
就像智能相框无论大屏小屏内容都能完美展示
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const screenWidth = ref(375)
const devices = [
{ id: 'iphone-se', name: 'iPhone SE', width: 375, icon: '📱' },
{ id: 'iphone-14', name: 'iPhone 14', width: 390, icon: '📱' },
{ id: 'ipad-mini', name: 'iPad Mini', width: 768, icon: '📲' },
{ id: 'macbook', name: 'MacBook', width: 1280, icon: '💻' },
{ id: 'desktop', name: '桌面显示器', width: 1400, icon: '🖥️' }
]
const breakpoints = [
{ name: 'sm', min: 320, max: 640 },
{ name: 'md', min: 640, max: 1024 },
{ name: 'lg', min: 1024, max: 9999 }
]
const currentBreakpoint = computed(() => {
if (screenWidth.value < 640) return { name: '手机端(小屏)', cols: 1 }
if (screenWidth.value < 1024) return { name: '平板端(中屏)', cols: 2 }
return { name: '电脑端(大屏)', cols: 3 }
})
const currentDevice = computed(() => {
const device = devices.find(d => d.width === screenWidth.value)
return device ? device.name : `${screenWidth.value}px 视口`
})
const gridClass = computed(() => {
if (screenWidth.value < 640) return 'grid-cols-1'
if (screenWidth.value < 1024) return 'grid-cols-2'
return 'grid-cols-3'
})
const setDevice = (width) => {
screenWidth.value = width
}
const gridItems = [
{ icon: '📷', title: '摄影', description: '捕捉精彩瞬间' },
{ icon: '🎨', title: '设计', description: '创造视觉美感' },
{ icon: '💻', title: '编程', description: '构建数字世界' },
{ icon: '🎵', title: '音乐', description: '谱写动人旋律' },
{ icon: '📚', title: '阅读', description: '探索知识海洋' },
{ icon: '✈️', title: '旅行', description: '发现世界之美' }
]
</script>
<style scoped>
.responsive-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: linear-gradient(135deg, var(--vp-c-bg-soft) 0%, var(--vp-c-bg) 100%);
padding: 1.5rem;
margin: 1rem 0;
}
.scenario-intro {
text-align: center;
margin-bottom: 1.5rem;
padding: 1rem;
background: linear-gradient(135deg, rgba(77, 208, 225, 0.2), rgba(126, 87, 194, 0.2));
border-radius: 12px;
}
.emoji-scene {
font-size: 3rem;
margin-bottom: 0.5rem;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.scenario-intro h4 {
margin: 0.5rem 0;
color: var(--vp-c-text-1);
font-size: 1.2rem;
}
.scenario-intro p {
margin: 0;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.device-presets {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.device-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s;
min-width: 80px;
}
.device-btn:hover {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
}
.device-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.device-icon {
font-size: 1.5rem;
}
.device-name {
font-size: 0.75rem;
font-weight: 500;
color: var(--vp-c-text-1);
}
.device-size {
font-size: 0.625rem;
color: var(--vp-c-text-2);
}
.slider-control {
margin-bottom: 1rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.slider-labels {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
.width-slider {
width: 100%;
height: 8px;
-webkit-appearance: none;
appearance: none;
background: linear-gradient(90deg, #4dd0e1, #7e57c2);
border-radius: 4px;
outline: none;
}
.width-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 24px;
height: 24px;
background: white;
border: 3px solid #7e57c2;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
.current-width {
text-align: center;
margin-top: 0.5rem;
font-size: 0.875rem;
color: var(--vp-c-text-2);
}
.viewport-preview {
border: 2px solid var(--vp-c-brand);
border-radius: 12px;
overflow: hidden;
background: white;
margin-bottom: 1rem;
}
.viewport-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0.75rem;
background: var(--vp-c-brand-soft);
border-bottom: 1px solid var(--vp-c-brand);
}
.viewport-device {
font-size: 0.75rem;
font-weight: 600;
color: var(--vp-c-brand-dark);
}
.viewport-dots {
display: flex;
gap: 4px;
}
.viewport-dots span {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--vp-c-brand);
}
.viewport-content {
margin: 0 auto;
transition: width 0.3s ease;
overflow: hidden;
}
.demo-grid {
display: grid;
gap: 0.75rem;
padding: 1rem;
}
.grid-cols-1 {
grid-template-columns: 1fr;
}
.grid-cols-2 {
grid-template-columns: repeat(2, 1fr);
}
.grid-cols-3 {
grid-template-columns: repeat(3, 1fr);
}
.grid-item {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
padding: 1rem;
color: white;
text-align: center;
transition: all 0.3s ease;
animation: fadeInUp 0.5s ease both;
}
.grid-item:hover {
transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.item-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.item-title {
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.item-desc {
font-size: 0.75rem;
opacity: 0.9;
}
.breakpoint-indicator {
margin-bottom: 1rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.bp-track {
position: relative;
height: 40px;
background: linear-gradient(90deg, #4dd0e1 0%, #7e57c2 50%, #ab47bc 100%);
border-radius: 20px;
}
.bp-point {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.bp-point.active .bp-label {
background: white;
color: #333;
font-weight: 700;
transform: scale(1.1);
}
.bp-label {
padding: 4px 8px;
background: rgba(255, 255, 255, 0.8);
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
color: #555;
transition: all 0.3s;
}
.bp-range {
font-size: 0.625rem;
color: white;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.code-preview {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
margin-bottom: 1rem;
}
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0.75rem;
background: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
}
.code-title {
font-size: 0.875rem;
font-weight: 500;
color: var(--vp-c-text-1);
}
.code-lang {
font-size: 0.75rem;
padding: 0.125rem 0.5rem;
background: var(--vp-c-brand);
color: white;
border-radius: 4px;
}
.code-block {
margin: 0;
padding: 1rem;
background: #1e1e2e;
color: #a6accd;
font-size: 0.8125rem;
line-height: 1.6;
overflow-x: auto;
}
.tips-box {
display: flex;
gap: 0.75rem;
padding: 1rem;
background: linear-gradient(135deg, #e3f2fd, #f3e5f5);
border-radius: 8px;
border-left: 4px solid #2196f3;
}
.tips-icon {
font-size: 1.5rem;
}
.tips-content {
flex: 1;
font-size: 0.9rem;
color: #444;
line-height: 1.6;
}
@media (max-width: 768px) {
.scene-body {
grid-template-columns: 1fr;
}
.device-presets {
flex-direction: column;
}
.bp-track {
height: 60px;
}
}
</style>
@@ -0,0 +1,717 @@
<!--
SliceRequestDemo.vue
切图时代请求次数演示 - 重构版
用途
用外卖点餐的比喻让零基础用户理解 HTTP 请求的概念
通过可视化的外卖小哥动画展示切图时代 vs 雪碧图的性能差异
-->
<template>
<div class="slice-demo">
<div class="scenario-intro">
<div class="emoji-scene">🍕 📱 🛵</div>
<h4>外卖点餐模拟器</h4>
<p>想象一下你在点披萨外卖每次下单外卖小哥就要跑一趟</p>
</div>
<div class="mode-tabs">
<button
v-for="mode in modes"
:key="mode.id"
:class="['mode-tab', { active: currentMode === mode.id }]"
@click="switchMode(mode.id)"
>
<span class="tab-icon">{{ mode.icon }}</span>
<span class="tab-label">{{ mode.label }}</span>
<span class="tab-desc">{{ mode.desc }}</span>
</button>
</div>
<div class="restaurant-scene">
<div class="scene-header">
<div class="restaurant-info">
<span class="restaurant-emoji">🏪</span>
<span class="restaurant-name">前端披萨店</span>
</div>
<div class="delivery-stats">
<div class="stat">
<span class="stat-label">外卖小哥跑了:</span>
<span class="stat-value deliveries">{{ deliveryCount }}</span>
<span class="stat-unit"></span>
</div>
<div class="stat time-stat">
<span class="stat-label">总耗时:</span>
<span class="stat-value time">{{ totalTime }}</span>
<span class="stat-unit"></span>
</div>
</div>
</div>
<div class="scene-body">
<div class="kitchen-area">
<div class="kitchen-label">🍳 后厨服务器</div>
<div class="food-items">
<div
v-for="(item, index) in foodItems"
:key="index"
class="food-item"
:class="{ preparing: item.status === 'preparing', ready: item.status === 'ready' }"
>
<span class="food-emoji">{{ item.emoji }}</span>
<span class="food-name">{{ item.name }}</span>
<span class="food-status">{{ getStatusText(item.status) }}</span>
</div>
</div>
</div>
<div class="delivery-lane">
<div class="lane-label">🛵 配送路线网络</div>
<div class="delivery-runway">
<div
v-for="(rider, index) in activeRiders"
:key="rider.id"
class="rider"
:style="{ left: rider.position + '%' }"
>
<div class="rider-emoji">{{ rider.mode === 'sprite' ? '🚚' : '🛵' }}</div>
<div class="rider-package">
<span v-for="emoji in rider.packages" :key="emoji">{{ emoji }}</span>
</div>
</div>
<div v-if="activeRiders.length === 0" class="empty-lane">
等待下单...
</div>
</div>
</div>
<div class="customer-area">
<div class="customer-label">🏠 你家浏览器</div>
<div class="received-items">
<div v-if="receivedItems.length === 0" class="empty-plate">
🍽 等待美食送达...
</div>
<div v-else class="food-on-table">
<div
v-for="(item, index) in receivedItems"
:key="index"
class="received-item"
:class="{ fresh: item.isNew }"
>
<span class="item-emoji">{{ item.emoji }}</span>
<span class="item-name">{{ item.name }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="control-panel">
<button class="order-btn" @click="placeOrder" :disabled="isOrdering">
<span class="btn-icon">{{ isOrdering ? '⏳' : '🛒' }}</span>
<span class="btn-text">{{ isOrdering ? '配 送 中...' : '下 单 点 餐' }}</span>
</button>
<button class="reset-btn" @click="resetScene">
<span class="btn-icon">🔄</span>
<span class="btn-text">重新开始</span>
</button>
</div>
<div class="explanation-box">
<div class="explanation-icon">💡</div>
<div class="explanation-content">
<strong>{{ currentMode === 'slice' ? '切图时代' : '雪碧图时代' }}</strong>
{{ currentExplanation }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentMode = ref('slice')
const isOrdering = ref(false)
const deliveryCount = ref(0)
const totalTime = ref(0)
const activeRiders = ref([])
const receivedItems = ref([])
const modes = [
{
id: 'slice',
label: '切图时代',
icon: '🛵',
desc: '每次只送一道菜'
},
{
id: 'sprite',
label: '雪碧图时代',
icon: '🚚',
desc: '一次送完整桌菜'
}
]
const foodItems = [
{ emoji: '🍕', name: '披萨底', status: 'ready' },
{ emoji: '🧀', name: '芝士', status: 'ready' },
{ emoji: '🍄', name: '蘑菇', status: 'ready' },
{ emoji: '🥓', name: '培根', status: 'ready' },
{ emoji: '🫑', name: '青椒', status: 'ready' },
{ emoji: '🍅', name: '番茄酱', status: 'ready' }
]
const currentExplanation = computed(() => {
return currentMode.value === 'slice'
? '每张小图都单独发一个 HTTP 请求。就像点外卖时,每道菜都单独叫一个外卖小哥,跑 6 趟才能送齐!'
: '把所有小图合并成一张大图。就像把一桌菜装进一个保温箱,一个外卖小哥一趟就全送来了!'
})
const getStatusText = (status) => {
const map = { ready: '✓ 就绪', preparing: '⏳ 制作中', delivering: '🛵 配送中' }
return map[status] || status
}
let riderIdCounter = 0
const switchMode = (mode) => {
currentMode.value = mode
resetScene()
}
const resetScene = () => {
isOrdering.value = false
deliveryCount.value = 0
totalTime.value = 0
activeRiders.value = []
receivedItems.value = []
riderIdCounter = 0
}
const placeOrder = async () => {
if (isOrdering.value) return
isOrdering.value = true
receivedItems.value = []
const items = [...foodItems]
if (currentMode.value === 'slice') {
// 切图模式:每个食材单独配送
for (let i = 0; i < items.length; i++) {
const item = items[i]
deliveryCount.value++
// 创建骑手
const rider = {
id: riderIdCounter++,
position: 0,
mode: 'slice',
packages: [item.emoji]
}
activeRiders.value = [rider]
// 动画:去程 - 使用响应式方式更新
await animateRiderReactive(rider, 100, 800)
// 送达
receivedItems.value.push({ ...item, isNew: true })
setTimeout(() => { if (receivedItems.value[i]) receivedItems.value[i].isNew = false }, 500)
// 动画:返程 - 使用响应式方式更新
await animateRiderReactive(rider, 0, 600)
totalTime.value += 1.4
activeRiders.value = []
}
} else {
// 雪碧图模式:一次送全部
deliveryCount.value = 1
const rider = {
id: riderIdCounter++,
position: 0,
mode: 'sprite',
packages: items.map(i => i.emoji)
}
activeRiders.value = [rider]
// 动画:去程
await animateRider(rider, 100, 1500)
// 全部送达
items.forEach((item, idx) => {
setTimeout(() => {
receivedItems.value.push({ ...item, isNew: true })
setTimeout(() => {
const found = receivedItems.value.find(r => r.name === item.name && r.isNew)
if (found) found.isNew = false
}, 500)
}, idx * 100)
})
totalTime.value = 2.5
// 动画:返程
await animateRider(rider, 0, 1000)
activeRiders.value = []
}
isOrdering.value = false
}
// 响应式动画函数 - 使用 Vue 的响希方式更新位置
const animateRiderReactive = (rider, targetPosition, duration) => {
return new Promise(resolve => {
const startPosition = rider.position
const startTime = performance.now()
let isActive = true
const animate = (currentTime) => {
if (!isActive) return
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / duration, 1)
// 缓动函数
const easeProgress = 1 - Math.pow(1 - progress, 3)
// 使用 Vue 的方式触发更新 - 直接修改对象属性
const newPosition = startPosition + (targetPosition - startPosition) * easeProgress
// 通过强制触发 Vue 响应的方式更新
rider.position = newPosition
// 手动触发 Vue 的更新(通过操作数组)
const riders = activeRiders.value
const index = riders.indexOf(rider)
if (index !== -1) {
// 通过替换对象强制触发响应
riders[index] = { ...rider, position: newPosition }
}
if (progress < 1) {
requestAnimationFrame(animate)
} else {
isActive = false
resolve()
}
}
requestAnimationFrame(animate)
})
}
</script>
<style scoped>
.slice-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: linear-gradient(135deg, var(--vp-c-bg-soft) 0%, var(--vp-c-bg) 100%);
padding: 1.5rem;
margin: 1rem 0;
}
.scenario-intro {
text-align: center;
margin-bottom: 1.5rem;
padding: 1rem;
background: linear-gradient(135deg, rgba(255, 183, 77, 0.2), rgba(255, 138, 101, 0.2));
border-radius: 12px;
}
.emoji-scene {
font-size: 3rem;
margin-bottom: 0.5rem;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.scenario-intro h4 {
margin: 0.5rem 0;
color: var(--vp-c-text-1);
font-size: 1.2rem;
}
.scenario-intro p {
margin: 0;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.mode-tabs {
display: flex;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.mode-tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
padding: 1rem;
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.3s ease;
}
.mode-tab:hover {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
}
.mode-tab.active {
border-color: var(--vp-c-brand);
background: linear-gradient(135deg, var(--vp-c-brand-soft), var(--vp-c-bg));
}
.tab-icon {
font-size: 2rem;
}
.tab-label {
font-weight: 600;
color: var(--vp-c-text-1);
}
.tab-desc {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.restaurant-scene {
background: linear-gradient(180deg, #e3f2fd 0%, #f5f5f5 100%);
border-radius: 12px;
overflow: hidden;
margin-bottom: 1rem;
}
.scene-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: rgba(255, 255, 255, 0.9);
border-bottom: 1px solid #e0e0e0;
}
.restaurant-info {
display: flex;
align-items: center;
gap: 0.5rem;
}
.restaurant-emoji {
font-size: 1.5rem;
}
.restaurant-name {
font-weight: 600;
color: #333;
}
.delivery-stats {
display: flex;
gap: 1rem;
}
.stat {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.875rem;
}
.stat-label {
color: #666;
}
.stat-value {
font-weight: 700;
font-size: 1.1rem;
}
.stat-value.deliveries {
color: #ff6b6b;
}
.stat-value.time {
color: #4ecdc4;
}
.scene-body {
display: grid;
grid-template-columns: 1fr 1.5fr 1fr;
gap: 1rem;
padding: 1rem;
min-height: 250px;
}
.kitchen-area,
.delivery-lane,
.customer-area {
background: rgba(255, 255, 255, 0.8);
border-radius: 8px;
padding: 0.75rem;
}
.kitchen-label,
.lane-label,
.customer-label {
font-size: 0.75rem;
font-weight: 600;
color: #666;
margin-bottom: 0.5rem;
text-align: center;
}
.food-items {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.food-item {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem;
background: #f5f5f5;
border-radius: 4px;
font-size: 0.75rem;
transition: all 0.3s;
}
.food-item.preparing {
background: #fff3e0;
}
.food-item.delivering {
background: #e3f2fd;
}
.food-item.ready {
background: #e8f5e9;
}
.food-emoji {
font-size: 1rem;
}
.food-name {
flex: 1;
font-weight: 500;
}
.food-status {
font-size: 0.625rem;
color: #999;
}
.delivery-runway {
position: relative;
height: 120px;
background: linear-gradient(90deg, #e8eaf6 0%, #c5cae9 50%, #e8eaf6 100%);
border-radius: 8px;
overflow: hidden;
}
.delivery-runway::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 2px;
background: repeating-linear-gradient(
90deg,
#9fa8da 0px,
#9fa8da 20px,
transparent 20px,
transparent 40px
);
}
.rider {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
transition: left 0.1s linear;
}
.rider-emoji {
font-size: 2rem;
animation: rider-bounce 0.5s infinite alternate;
}
@keyframes rider-bounce {
from { transform: translateY(0); }
to { transform: translateY(-3px); }
}
.rider-package {
display: flex;
gap: 2px;
margin-top: 2px;
padding: 2px 4px;
background: rgba(255, 255, 255, 0.9);
border-radius: 10px;
font-size: 0.75rem;
}
.empty-lane {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #9fa8da;
font-size: 0.875rem;
}
.received-items {
min-height: 150px;
}
.empty-plate {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 150px;
color: #999;
font-size: 0.875rem;
}
.food-on-table {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
}
.received-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.5rem;
background: #f5f5f5;
border-radius: 8px;
transition: all 0.3s;
}
.received-item.fresh {
animation: item-arrive 0.5s ease;
background: #e8f5e9;
}
@keyframes item-arrive {
0% { transform: scale(0.5); opacity: 0; }
50% { transform: scale(1.1); }
100% { transform: scale(1); opacity: 1; }
}
.item-emoji {
font-size: 1.5rem;
}
.item-name {
font-size: 0.625rem;
color: #666;
margin-top: 0.25rem;
}
.control-panel {
display: flex;
gap: 1rem;
justify-content: center;
margin-top: 1rem;
}
.order-btn,
.reset-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.875rem 1.5rem;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.order-btn {
background: linear-gradient(135deg, #ff6b6b, #ee5a5a);
color: white;
}
.order-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4);
}
.order-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.reset-btn {
background: #f5f5f5;
color: #666;
}
.reset-btn:hover {
background: #e0e0e0;
}
.explanation-box {
display: flex;
gap: 0.75rem;
margin-top: 1rem;
padding: 1rem;
background: linear-gradient(135deg, #e3f2fd, #f3e5f5);
border-radius: 8px;
border-left: 4px solid #2196f3;
}
.explanation-icon {
font-size: 1.5rem;
}
.explanation-content {
flex: 1;
font-size: 0.9rem;
color: #444;
line-height: 1.6;
}
@media (max-width: 768px) {
.scene-body {
grid-template-columns: 1fr;
gap: 0.75rem;
}
.delivery-stats {
flex-direction: column;
gap: 0.5rem;
}
.mode-tabs {
flex-direction: column;
}
.control-panel {
flex-direction: column;
}
}
</style>
@@ -0,0 +1,320 @@
<!--
PerformanceOverviewDemo.vue
前端性能优化全景图展示瓶颈与优化手段的对应关系
交互功能
- 点击不同维度传输渲染执行查看对应的瓶颈和方案
- 动态展示瓶颈对用户体验的影响
-->
<template>
<div class="performance-overview">
<div class="header">
<div class="title">前端性能优化全景图</div>
<div class="subtitle">点击下方维度探索性能瓶颈与优化方案的对应关系</div>
</div>
<!-- 维度切换 -->
<div class="dimension-tabs">
<button
v-for="dim in dimensions"
:key="dim.id"
class="tab-btn"
:class="{ active: currentDim.id === dim.id }"
@click="currentDim = dim"
>
<span class="icon">{{ dim.icon }}</span>
<span class="text">{{ dim.name }}</span>
</button>
</div>
<!-- 内容展示区 -->
<div class="content-area" :class="currentDim.id">
<div class="panel bottlenecks">
<h3>
<span class="icon"></span>
常见瓶颈 (Bottlenecks)
</h3>
<ul class="list">
<li v-for="(item, index) in currentDim.bottlenecks" :key="index">
<div class="item-title">{{ item.title }}</div>
<div class="item-desc">{{ item.desc }}</div>
</li>
</ul>
</div>
<div class="arrow">
<div class="arrow-line"></div>
<div class="arrow-text">如何解决</div>
</div>
<div class="panel solutions">
<h3>
<span class="icon">🚀</span>
优化方案 (Solutions)
</h3>
<ul class="list">
<li v-for="(item, index) in currentDim.solutions" :key="index">
<div class="item-title">{{ item.title }}</div>
<div class="item-desc">{{ item.desc }}</div>
<div class="tags">
<span v-for="tag in item.tags" :key="tag" class="tag">{{ tag }}</span>
</div>
</li>
</ul>
</div>
</div>
<!-- 总结栏 -->
<div class="summary-bar">
<p>
<strong>核心目标</strong>
{{ currentDim.goal }}
</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const dimensions = [
{
id: 'network',
name: '传输层 (Network)',
icon: '📡',
goal: '让资源更快到达浏览器 (减体积、减次数、缩短距离)',
bottlenecks: [
{ title: '体积过大', desc: '图片、JS bundle 未压缩,下载耗时久' },
{ title: '请求过多', desc: 'HTTP/1.1 队头阻塞,资源排队下载' },
{ title: '网络延迟', desc: '服务器物理距离远,RTT 时间长' }
],
solutions: [
{ title: '资源压缩', desc: 'Gzip/Brotli, 图片格式转换 (WebP)', tags: ['减体积'] },
{ title: '懒加载', desc: '只加载当前视口可见的资源', tags: ['减体积', '减次数'] },
{ title: 'CDN 加速', desc: '将资源分发到离用户最近的节点', tags: ['缩短距离'] },
{ title: 'HTTP 缓存', desc: '利用浏览器缓存,避免重复请求', tags: ['减次数'] }
]
},
{
id: 'rendering',
name: '渲染层 (Rendering)',
icon: '🎨',
goal: '让页面更快画出来 (减少重排重绘、利用 GPU)',
bottlenecks: [
{ title: '关键路径阻塞', desc: 'CSS/JS 阻塞了 DOM 树构建' },
{ title: '频繁重排 (Reflow)', desc: '修改布局属性导致全量重新计算' },
{ title: '动画卡顿', desc: '使用 CPU 绘制动画,帧率低于 60fps' }
],
solutions: [
{ title: '关键 CSS 内联', desc: '首屏样式直接写在 HTML 中', tags: ['关键路径'] },
{ title: 'GPU 加速', desc: '使用 transform/opacity 触发合成层', tags: ['动画'] },
{ title: '虚拟列表', desc: '只渲染可见 DOM,处理海量数据', tags: ['DOM 优化'] },
{ title: '防抖节流', desc: '减少高频事件触发渲染的频率', tags: ['逻辑优化'] }
]
},
{
id: 'execution',
name: '执行层 (Scripting)',
icon: '⚙️',
goal: '让主线程不卡顿 (减少长任务、并行计算)',
bottlenecks: [
{ title: '主线程阻塞', desc: '长任务 (Long Tasks) 导致无法响应交互' },
{ title: '无效计算', desc: 'React/Vue 中不必要的组件重渲染' },
{ title: '内存泄漏', desc: '未清理的监听器导致页面越来越卡' }
],
solutions: [
{ title: 'Web Workers', desc: '将复杂计算移到后台线程', tags: ['并行'] },
{ title: '代码分割', desc: '按需加载 JS,减少主线程解析压力', tags: ['减负'] },
{ title: '时间切片', desc: '将大任务拆分为多个小任务', tags: ['响应'] },
{ title: '算法优化', desc: '降低时间复杂度 (如 O(n²) -> O(n))', tags: ['效率'] }
]
}
]
const currentDim = ref(dimensions[0])
</script>
<style scoped>
.performance-overview {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
font-family: var(--vp-font-family-sans);
}
.header {
text-align: center;
margin-bottom: 1.5rem;
}
.title {
font-size: 1.2rem;
font-weight: 700;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.9rem;
color: var(--vp-c-text-2);
margin-top: 0.5rem;
}
.dimension-tabs {
display: flex;
justify-content: center;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.tab-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 1.2rem;
border-radius: 20px;
background-color: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
cursor: pointer;
transition: all 0.2s;
color: var(--vp-c-text-2);
}
.tab-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.tab-btn.active {
background-color: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
box-shadow: 0 4px 12px rgba(var(--vp-c-brand-rgb), 0.2);
}
.content-area {
display: flex;
gap: 2rem;
align-items: stretch;
background-color: var(--vp-c-bg);
padding: 1.5rem;
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
@media (max-width: 768px) {
.content-area {
flex-direction: column;
}
}
.panel {
flex: 1;
}
.panel h3 {
font-size: 1.1rem;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--vp-c-text-1);
}
.list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.list li {
padding: 0.8rem;
border-radius: 6px;
background-color: var(--vp-c-bg-soft);
border: 1px solid transparent;
transition: all 0.2s;
}
.bottlenecks .list li {
border-left: 3px solid var(--vp-c-danger);
}
.solutions .list li {
border-left: 3px solid var(--vp-c-brand);
}
.item-title {
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.2rem;
}
.item-desc {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.4;
}
.tags {
margin-top: 0.5rem;
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}
.tag {
font-size: 0.75rem;
padding: 0.1rem 0.4rem;
border-radius: 4px;
background-color: var(--vp-c-bg-mute);
color: var(--vp-c-text-2);
}
.arrow {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--vp-c-text-3);
font-size: 0.9rem;
width: 80px;
}
@media (max-width: 768px) {
.arrow {
width: 100%;
height: 40px;
flex-direction: row;
gap: 0.5rem;
}
}
.arrow-line {
flex: 1;
width: 2px;
background-color: var(--vp-c-divider);
}
@media (max-width: 768px) {
.arrow-line {
width: 100%;
height: 2px;
flex: 1;
}
}
.summary-bar {
margin-top: 1.5rem;
padding: 1rem;
background-color: var(--vp-c-brand-dimm);
border-radius: 6px;
text-align: center;
color: var(--vp-c-brand-dark);
font-size: 0.95rem;
}
</style>
@@ -15,8 +15,8 @@
<div class="mode-desc">
{{
architecture === 'dense'
? '全能天才:每个问题都动用整个大脑 (100% 激活)'
: '专家团队:根据问题指派专人处理 (稀疏激活)'
? '全能天才:每个 Token 都激活所有神经元 (100% 激活)'
: '专家团队:每个 Token 路由给特定专家 (Token-Level Routing)'
}}
</div>
</div>
@@ -25,7 +25,7 @@
<div class="visual-stage">
<!-- Step 1: Input Selection -->
<div class="stage-section input-section">
<div class="section-label">1. 输入指令 (Input)</div>
<div class="section-label">1. 选择输入 (Select Input)</div>
<div class="task-selector">
<button
v-for="(task, idx) in tasks"
@@ -39,105 +39,116 @@
<span class="task-text">{{ task.label }}</span>
</button>
</div>
<div
class="token-stream"
:class="{ flowing: processing && currentStep >= 1 }"
>
<div class="token-particle">{{ selectedTask.icon }}</div>
</div>
</div>
<!-- Arrow -->
<div class="flow-arrow"></div>
<!-- Step 2: Processing Unit (Dense or MoE) -->
<div class="stage-section process-section">
<div class="section-label">
2. 模型处理 (Processing)
<span v-if="processing" class="status-badge">计算中...</span>
</div>
<!-- Dense Visualization -->
<div v-if="architecture === 'dense'" class="dense-visualization">
<div
class="dense-block"
:class="{ activating: processing && currentStep === 2 }"
>
<div class="dense-label">前馈神经网络 (FFN)</div>
<div class="neuron-grid">
<div v-for="n in 32" :key="n" class="neuron"></div>
</div>
<div class="activation-info" v-if="processing && currentStep === 2">
🔥 激活率: 100% (全员过载)
</div>
<!-- Processing Pipeline -->
<div class="pipeline-container">
<!-- Token Flow Animation -->
<div class="token-flow-viz" v-if="processing">
<div class="current-token-display">
<span class="token-label">Current Token:</span>
<span class="token-badge" :style="{ borderColor: getExpertColor(currentToken?.expert) }">
{{ currentToken?.text || '...' }}
</span>
</div>
</div>
<!-- MoE Visualization -->
<div v-else class="moe-visualization">
<!-- Router -->
<div
class="router-node"
:class="{ active: processing && currentStep === 1 }"
>
<div class="router-label">门控路由 (Router)</div>
<div class="router-action" v-if="processing && currentStep >= 1">
🔍 识别意图: "{{ selectedTask.type }}"
</div>
<!-- Step 2: Processing Unit (Dense or MoE) -->
<div class="stage-section process-section">
<div class="section-label">
2. 模型处理 (Processing)
<span v-if="processing" class="status-badge">生成中...</span>
</div>
<!-- Connections -->
<div class="connections">
<!-- Dense Visualization -->
<div v-if="architecture === 'dense'" class="dense-visualization">
<div
v-for="(expert, idx) in experts"
:key="idx"
class="connection-line"
:class="{
active: processing && currentStep >= 2 && isExpertSelected(idx),
inactive:
processing && currentStep >= 2 && !isExpertSelected(idx)
}"
></div>
</div>
<!-- Experts -->
<div class="experts-grid">
<div
v-for="(expert, idx) in experts"
:key="idx"
class="expert-card"
:class="{
active: processing && currentStep >= 2 && isExpertSelected(idx),
inactive:
processing && currentStep >= 2 && !isExpertSelected(idx)
}"
class="dense-block"
:class="{ activating: processing && currentStep === 'expert' }"
>
<div class="expert-icon">{{ expert.icon }}</div>
<div class="expert-name">{{ expert.name }}</div>
<div class="expert-role">{{ expert.role }}</div>
<div class="dense-label">Dense FFN Layers</div>
<div class="neuron-grid">
<div v-for="n in 32" :key="n" class="neuron"></div>
</div>
<div class="activation-info" v-if="processing">
🔥 激活率: 100% (All Parameters)
</div>
</div>
</div>
<!-- MoE Visualization -->
<div v-else class="moe-visualization">
<!-- Router -->
<div
class="router-node"
:class="{ active: processing && currentStep === 'router' }"
>
<div class="router-label">Router (Token 分发)</div>
<div class="router-action" v-if="processing && currentToken">
Routing "{{ currentToken.text.trim() }}" {{ experts[currentToken.expert].name }}
</div>
</div>
<!-- Connections -->
<div class="connections">
<div
class="expert-status"
v-if="processing && currentStep >= 2 && isExpertSelected(idx)"
v-for="(expert, idx) in experts"
:key="idx"
class="connection-line"
:class="{
active: processing && currentStep === 'expert' && currentToken?.expert === idx,
inactive: processing && currentStep === 'expert' && currentToken?.expert !== idx
}"
:style="{
borderColor: processing && currentStep === 'expert' && currentToken?.expert === idx ? expert.color : ''
}"
></div>
</div>
<!-- Experts -->
<div class="experts-grid">
<div
v-for="(expert, idx) in experts"
:key="idx"
class="expert-card"
:class="{
active: processing && currentStep === 'expert' && currentToken?.expert === idx,
inactive: processing && currentStep === 'expert' && currentToken?.expert !== idx
}"
:style="{
borderColor: processing && currentStep === 'expert' && currentToken?.expert === idx ? expert.color : ''
}"
>
激活
<div class="expert-icon">{{ expert.icon }}</div>
<div class="expert-name">{{ expert.name }}</div>
<div
class="expert-status"
v-if="processing && currentStep === 'expert' && currentToken?.expert === idx"
:style="{ color: expert.color }"
>
Active
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Arrow -->
<div class="flow-arrow"></div>
<!-- Step 3: Output -->
<div class="stage-section output-section">
<div class="section-label">3. 生成结果 (Output)</div>
<div class="output-box" :class="{ revealed: currentStep === 3 }">
<div v-if="currentStep === 3" class="output-content">
<span class="output-icon">{{ selectedTask.icon }}</span>
<span class="typing-effect">{{ selectedTask.output }}</span>
</div>
<div v-else class="placeholder">等待处理...</div>
<div class="section-label">3. 逐步生成 (Output Stream)</div>
<div class="output-box">
<span class="output-content">
<span
v-for="(token, idx) in generatedTokens"
:key="idx"
class="generated-token"
:style="{ color: architecture === 'moe' ? experts[token.expert].color : 'inherit' }"
:title="architecture === 'moe' ? `Expert: ${experts[token.expert].name}` : ''"
>{{ token.text }}</span>
<span v-if="processing" class="cursor">|</span>
</span>
<div v-if="generatedTokens.length === 0 && !processing" class="placeholder">点击运行查看生成过程...</div>
</div>
</div>
</div>
@@ -145,7 +156,7 @@
<!-- Controls -->
<div class="demo-controls">
<button class="run-btn" @click="runDemo" :disabled="processing">
{{ processing ? '正在推理...' : '▶️ 开始生成 (Run Inference)' }}
{{ processing ? '正在生成 (Generating)...' : '▶️ 开始生成 (Run Generation)' }}
</button>
</div>
</div>
@@ -156,43 +167,54 @@ import { ref, computed } from 'vue'
const architecture = ref('moe')
const processing = ref(false)
const currentStep = ref(0) // 0: idle, 1: router, 2: experts, 3: output
const currentStep = ref('idle') // idle, router, expert
const currentToken = ref(null)
const generatedTokens = ref([])
const experts = [
{ icon: '💻', name: '代码专家', role: 'Python/JS/Rust' },
{ icon: '🎨', name: '创意专家', role: '诗歌/小说/绘画' },
{ icon: '📐', name: '逻辑专家', role: '数学/推理/证明' },
{ icon: '🌍', name: '语言专家', role: '翻译/润色/摘要' }
{ icon: '💻', name: 'Code', color: '#059669' }, // Green
{ icon: '📐', name: 'Math', color: '#2563eb' }, // Blue
{ icon: '🎨', name: 'Creative', color: '#d97706' }, // Amber
{ icon: '📝', name: 'Grammar', color: '#7c3aed' } // Purple
]
const tasks = [
{
label: 'Python 脚本',
type: '编程',
label: 'Python 代码示例',
icon: '🐍',
expertIdx: 0,
output: 'def fib(n): return n if n < 2 else...'
tokens: [
{ text: 'def', expert: 0 },
{ text: ' calc', expert: 3 },
{ text: '_area', expert: 0 },
{ text: '(', expert: 3 },
{ text: 'r', expert: 0 },
{ text: '):', expert: 0 },
{ text: '\n ', expert: 3 },
{ text: 'return', expert: 0 },
{ text: ' 3.14', expert: 1 }, // Math
{ text: ' *', expert: 1 },
{ text: ' r', expert: 0 },
{ text: ' **', expert: 1 },
{ text: ' 2', expert: 1 }
]
},
{
label: '写七言绝句',
type: '文学',
icon: '🌸',
expertIdx: 1,
output: '窗含西岭千秋雪,门泊东吴万里船...'
},
{
label: '解二元方程',
type: '数学',
icon: '✖️',
expertIdx: 2,
output: 'x = 5, y = -2 (过程略)'
},
{
label: '翻译成英文',
type: '翻译',
icon: '🔤',
expertIdx: 3,
output: 'To be, or not to be, that is the question.'
label: '科幻小说片段',
icon: '🚀',
tokens: [
{ text: 'The', expert: 3 },
{ text: ' spaceship', expert: 2 },
{ text: ' warped', expert: 2 },
{ text: ' into', expert: 3 },
{ text: ' dimension', expert: 1 }, // Logic/Math concept
{ text: ' X', expert: 2 },
{ text: '.', expert: 3 },
{ text: ' Coordinates', expert: 1 },
{ text: ':', expert: 3 },
{ text: ' 42', expert: 1 },
{ text: '.', expert: 3 },
{ text: '00', expert: 1 }
]
}
]
@@ -211,33 +233,39 @@ const selectTask = (task) => {
}
const resetDemo = () => {
currentStep.value = 0
currentStep.value = 'idle'
generatedTokens.value = []
currentToken.value = null
}
const isExpertSelected = (idx) => {
if (architecture.value === 'dense') return true // All active in dense
return idx === selectedTask.value.expertIdx
const getExpertColor = (expertIdx) => {
if (expertIdx === undefined || architecture.value === 'dense') return 'var(--vp-c-text-1)'
return experts[expertIdx].color
}
const runDemo = async () => {
if (processing.value) return
processing.value = true
currentStep.value = 0
resetDemo()
// Step 1: Input -> Router
await wait(300)
currentStep.value = 1
for (const token of selectedTask.value.tokens) {
currentToken.value = token
// Step 1: Router (MoE only) or Prep (Dense)
currentStep.value = 'router'
await wait(architecture.value === 'moe' ? 400 : 200)
// Step 2: Router -> Expert / Dense Processing
await wait(800)
currentStep.value = 2
// Step 2: Expert Processing
currentStep.value = 'expert'
await wait(architecture.value === 'moe' ? 600 : 400) // Dense might be slower in reality, but for demo keep it brisk
// Step 3: Expert -> Output
await wait(1200)
currentStep.value = 3
// Step 3: Output
generatedTokens.value.push(token)
await wait(200)
}
// Finish
await wait(500)
currentStep.value = 'idle'
currentToken.value = null
processing.value = false
}
@@ -246,8 +274,7 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
<style scoped>
.moe-demo-container {
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-family: monospace, system-ui;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
@@ -278,7 +305,6 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
font-weight: 600;
color: var(--vp-c-text-2);
cursor: pointer;
transition: all 0.2s;
border: none;
background: transparent;
}
@@ -298,8 +324,7 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
.visual-stage {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
gap: 16px;
}
.stage-section {
@@ -309,7 +334,6 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
border-radius: 8px;
padding: 16px;
position: relative;
transition: all 0.3s;
}
.section-label {
@@ -343,42 +367,42 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
border-radius: 6px;
background: var(--vp-c-bg-mute);
cursor: pointer;
transition: all 0.2s;
font-size: 13px;
}
.task-btn:hover {
background: var(--vp-c-bg-soft);
}
.task-btn.selected {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-dimm);
color: var(--vp-c-brand);
}
.token-stream {
height: 4px;
background: var(--vp-c-divider);
margin-top: 12px;
border-radius: 2px;
position: relative;
overflow: hidden;
/* Token Flow */
.token-flow-viz {
display: flex;
justify-content: center;
margin-bottom: 8px;
height: 30px;
}
.token-particle {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: all 0.3s;
.current-token-display {
display: flex;
align-items: center;
gap: 8px;
animation: slideIn 0.3s ease-out;
}
.token-stream.flowing .token-particle {
opacity: 1;
top: 0;
animation: slideDown 0.5s forwards;
.token-label {
font-size: 12px;
color: var(--vp-c-text-3);
}
.token-badge {
background: var(--vp-c-bg-mute);
border: 1px solid;
padding: 2px 8px;
border-radius: 4px;
font-weight: bold;
font-size: 14px;
}
/* Process Section */
@@ -393,12 +417,12 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
background: var(--vp-c-bg-mute);
border-radius: 8px;
padding: 12px;
transition: all 0.3s;
transition: all 0.2s;
}
.dense-block.activating {
background: var(--vp-c-brand);
box-shadow: 0 0 20px var(--vp-c-brand-dimm);
box-shadow: 0 0 15px var(--vp-c-brand-dimm);
}
.dense-block.activating .neuron {
@@ -429,7 +453,7 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
padding-bottom: 100%;
background: var(--vp-c-divider);
border-radius: 50%;
transition: all 0.3s;
transition: all 0.2s;
}
.activation-info {
@@ -445,15 +469,16 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
background: var(--vp-c-bg-mute);
border: 2px dashed var(--vp-c-text-3);
border-radius: 8px;
padding: 10px;
padding: 8px;
text-align: center;
margin-bottom: 12px;
transition: all 0.3s;
transition: all 0.2s;
}
.router-node.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-dimm);
transform: scale(1.02);
}
.router-label {
@@ -464,14 +489,14 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
.router-action {
font-size: 12px;
color: var(--vp-c-brand);
margin-top: 4px;
margin-top: 2px;
}
.connections {
display: flex;
justify-content: space-around;
height: 20px;
margin-bottom: -10px; /* Overlap slightly */
margin-bottom: -10px;
z-index: 0;
}
@@ -479,12 +504,14 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
width: 2px;
height: 100%;
background: var(--vp-c-divider);
transition: all 0.3s;
transition: all 0.2s;
opacity: 0.3;
}
.connection-line.active {
background: var(--vp-c-brand);
box-shadow: 0 0 8px var(--vp-c-brand);
background: currentColor; /* Use inline style color */
box-shadow: 0 0 6px currentColor;
opacity: 1;
}
.experts-grid {
@@ -501,65 +528,44 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
border-radius: 6px;
padding: 8px 4px;
text-align: center;
transition: all 0.3s;
opacity: 0.7;
transition: all 0.2s;
opacity: 0.5;
}
.expert-card.active {
opacity: 1;
border-color: var(--vp-c-brand);
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.expert-card.inactive {
opacity: 0.3;
transform: scale(0.95);
}
.expert-icon {
font-size: 20px;
margin-bottom: 4px;
}
.expert-name {
font-size: 11px;
font-size: 10px;
font-weight: bold;
margin-bottom: 2px;
}
.expert-role {
font-size: 9px;
color: var(--vp-c-text-3);
}
.expert-status {
font-size: 9px;
color: var(--vp-c-brand);
margin-top: 4px;
font-weight: bold;
}
/* Output Section */
.output-box {
min-height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-bg-mute);
border-radius: 6px;
padding: 10px;
transition: all 0.3s;
}
.output-box.revealed {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-brand);
}
.output-content {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
font-family: monospace;
font-size: 13px;
white-space: pre-wrap;
line-height: 1.5;
}
.generated-token {
display: inline-block;
transition: all 0.3s;
}
.placeholder {
@@ -568,6 +574,13 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
font-style: italic;
}
.cursor {
display: inline-block;
width: 2px;
background: var(--vp-c-text-1);
animation: blink 1s infinite;
}
/* Controls */
.demo-controls {
margin-top: 20px;
@@ -596,20 +609,13 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
cursor: not-allowed;
}
/* Animations */
.flow-arrow {
text-align: center;
color: var(--vp-c-divider);
font-size: 18px;
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
</style>
@@ -238,8 +238,8 @@
>
{{
isPredictionCorrect
? '✅ Parameters Good'
: '❌ Update Weights'
? '✅ Good Prediction'
: '🔧 Adjusting Weights'
}}
</div>
</div>
+13 -1
View File
@@ -144,6 +144,11 @@ import GPTEvolutionDemo from './components/appendix/ai-history/GPTEvolutionDemo.
import ImperativeVsDeclarativeDemo from './components/appendix/web-basics/ImperativeVsDeclarativeDemo.vue'
import ComponentReusabilityDemo from './components/appendix/web-basics/ComponentReusabilityDemo.vue'
// Frontend Evolution Components
import EvolutionSliceRequestDemo from './components/appendix/frontend-evolution/SliceRequestDemo.vue'
import EvolutionResponsiveGridDemo from './components/appendix/frontend-evolution/ResponsiveGridDemo.vue'
import EvolutionJQueryVsStateDemo from './components/appendix/frontend-evolution/JQueryVsStateDemo.vue'
import BackendEvolutionDemo from './components/appendix/backend-evolution/BackendEvolutionDemo.vue'
import MonolithVsMicroserviceDemo from './components/appendix/backend-evolution/MonolithVsMicroserviceDemo.vue'
import CgiQueueDemo from './components/appendix/backend-evolution/CgiQueueDemo.vue'
@@ -154,6 +159,7 @@ import ServerlessCostAutoScaleDemo from './components/appendix/backend-evolution
// Frontend Performance Components
import PerformanceMetricsDemo from './components/appendix/frontend-performance/PerformanceMetricsDemo.vue'
import PerformanceOverviewDemo from './components/appendix/frontend-performance/PerformanceOverviewDemo.vue'
import ReflowRepaintDemo from './components/appendix/frontend-performance/ReflowRepaintDemo.vue'
import ImageOptimizationDemo from './components/appendix/frontend-performance/ImageOptimizationDemo.vue'
import LazyLoadingDemo from './components/appendix/frontend-performance/LazyLoadingDemo.vue'
@@ -433,6 +439,11 @@ export default {
app.component('ImperativeVsDeclarativeDemo', ImperativeVsDeclarativeDemo)
app.component('ComponentReusabilityDemo', ComponentReusabilityDemo)
// Frontend Evolution Components Registration
app.component('EvolutionSliceRequestDemo', EvolutionSliceRequestDemo)
app.component('EvolutionResponsiveGridDemo', EvolutionResponsiveGridDemo)
app.component('EvolutionJQueryVsStateDemo', EvolutionJQueryVsStateDemo)
app.component('BackendEvolutionDemo', BackendEvolutionDemo)
app.component('MonolithVsMicroserviceDemo', MonolithVsMicroserviceDemo)
app.component('CgiQueueDemo', CgiQueueDemo)
@@ -441,8 +452,9 @@ export default {
app.component('CacheHitRatioDemo', CacheHitRatioDemo)
app.component('ServerlessCostAutoScaleDemo', ServerlessCostAutoScaleDemo)
// Frontend Performance Components Registration
// Frontend Performance Components
app.component('PerformanceMetricsDemo', PerformanceMetricsDemo)
app.component('PerformanceOverviewDemo', PerformanceOverviewDemo)
app.component('ReflowRepaintDemo', ReflowRepaintDemo)
app.component('ImageOptimizationDemo', ImageOptimizationDemo)
app.component('LazyLoadingDemo', LazyLoadingDemo)