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:
+10
-10
@@ -314,7 +314,7 @@ export default defineConfig({
|
||||
{ text: '首页', link: '/zh-cn/' },
|
||||
{
|
||||
text: '新手与产品原型',
|
||||
link: '/zh-cn/stage-0/intro'
|
||||
link: '/zh-cn/stage-0/'
|
||||
},
|
||||
{
|
||||
text: '初中级开发',
|
||||
@@ -672,7 +672,7 @@ export default defineConfig({
|
||||
{ text: 'Home', link: '/en-us/' },
|
||||
{
|
||||
text: 'Novice & PM',
|
||||
link: '/en-us/stage-0/intro'
|
||||
link: '/en-us/stage-0/'
|
||||
},
|
||||
{
|
||||
text: 'Full-Stack Development',
|
||||
@@ -712,7 +712,7 @@ export default defineConfig({
|
||||
{ text: 'ホーム', link: '/ja-jp/' },
|
||||
{
|
||||
text: '初心者とPM',
|
||||
link: '/ja-jp/stage-0/intro'
|
||||
link: '/ja-jp/stage-0/'
|
||||
},
|
||||
{
|
||||
text: 'フルスタック開発',
|
||||
@@ -750,7 +750,7 @@ export default defineConfig({
|
||||
{ text: '首頁', link: '/zh-tw/' },
|
||||
{
|
||||
text: '新手與產品原型',
|
||||
link: '/zh-tw/stage-0/intro'
|
||||
link: '/zh-tw/stage-0/'
|
||||
},
|
||||
{
|
||||
text: '初中級開發',
|
||||
@@ -785,7 +785,7 @@ export default defineConfig({
|
||||
},
|
||||
nav: [
|
||||
{ text: '홈', link: '/ko-kr/' },
|
||||
{ text: '초보자 & PM', link: '/ko-kr/stage-0/intro' },
|
||||
{ text: '초보자 & PM', link: '/ko-kr/stage-0/' },
|
||||
{
|
||||
text: '풀스택 개발',
|
||||
link: '/ko-kr/stage-2/intro'
|
||||
@@ -821,7 +821,7 @@ export default defineConfig({
|
||||
{ text: 'Inicio', link: '/es-es/' },
|
||||
{
|
||||
text: 'Principiante y PM',
|
||||
link: '/es-es/stage-0/intro'
|
||||
link: '/es-es/stage-0/'
|
||||
},
|
||||
{
|
||||
text: 'Desarrollo Full Stack',
|
||||
@@ -856,7 +856,7 @@ export default defineConfig({
|
||||
},
|
||||
nav: [
|
||||
{ text: 'Accueil', link: '/fr-fr/' },
|
||||
{ text: 'Débutant & PM', link: '/fr-fr/stage-0/intro' },
|
||||
{ text: 'Débutant & PM', link: '/fr-fr/stage-0/' },
|
||||
{
|
||||
text: 'Développement Full Stack',
|
||||
link: '/fr-fr/stage-2/intro'
|
||||
@@ -890,7 +890,7 @@ export default defineConfig({
|
||||
},
|
||||
nav: [
|
||||
{ text: 'Start', link: '/de-de/' },
|
||||
{ text: 'Anfänger & PM', link: '/de-de/stage-0/intro' },
|
||||
{ text: 'Anfänger & PM', link: '/de-de/stage-0/' },
|
||||
{
|
||||
text: 'Full Stack Entwicklung',
|
||||
link: '/de-de/stage-2/intro'
|
||||
@@ -926,7 +926,7 @@ export default defineConfig({
|
||||
{ text: 'الرئيسية', link: '/ar-sa/' },
|
||||
{
|
||||
text: 'مبتدأ & PM',
|
||||
link: '/ar-sa/stage-0/intro'
|
||||
link: '/ar-sa/stage-0/'
|
||||
},
|
||||
{
|
||||
text: 'تطوير Full Stack',
|
||||
@@ -963,7 +963,7 @@ export default defineConfig({
|
||||
{ text: 'Trang chủ', link: '/vi-vn/' },
|
||||
{
|
||||
text: 'Người mới & PM',
|
||||
link: '/vi-vn/stage-0/intro'
|
||||
link: '/vi-vn/stage-0/'
|
||||
},
|
||||
{
|
||||
text: 'Phát triển Full Stack',
|
||||
|
||||
+454
@@ -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>
|
||||
+490
@@ -0,0 +1,490 @@
|
||||
<!--
|
||||
ImperativeVsDeclarativeDemo.vue
|
||||
命令式 vs 声明式编程对比演示
|
||||
|
||||
用途:
|
||||
通过并排的交互式计数器,直观展示 Imperative(jQuery)和 Declarative(Vue)
|
||||
在代码量和心智负担上的差异。
|
||||
|
||||
交互功能:
|
||||
- 两个可交互的计数器。
|
||||
- 切换展示背后的代码实现。
|
||||
- 高亮显示 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
|
||||
<template>
|
||||
<div class="status" :class="{ warning: isWarning }">
|
||||
{{ status }}
|
||||
</div>
|
||||
</template></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;
|
||||
}
|
||||
}
|
||||
|
||||
// 模板里只需要声明关系
|
||||
<template>
|
||||
<div class="status" :class="{ warning: isWarning }">
|
||||
{{ status }}
|
||||
</div>
|
||||
</template></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>
|
||||
+320
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user