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,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>