feat(docs): add JavaScript intro demos and update content structure
refactor(docs): rename "ClaudeCode" to "Claude Code" across all language versions chore: add ESLint configuration and update build scripts style: update component organization and remove unused imports
This commit is contained in:
@@ -445,8 +445,8 @@ export default defineConfig({
|
||||
collapsed: false,
|
||||
items: [
|
||||
{
|
||||
text: '高级一:MCP 与 ClaudeCode Skills',
|
||||
link: '/zh-cn/stage-3/core-skills/3.1-mcp-claudecode-skills/'
|
||||
text: '高级一:MCP 与 Claude Code Skills',
|
||||
link: '/zh-cn/stage-3/core-skills/3.1-mcp-claude-code-skills/'
|
||||
},
|
||||
{
|
||||
text: '高级二:如何让 Coding Tools 长时间工作',
|
||||
|
||||
@@ -0,0 +1,477 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const mode = ref('sync')
|
||||
const isRunning = ref(false)
|
||||
const elapsedTime = ref(0)
|
||||
const customerA = ref({ time: 2, status: 'waiting' })
|
||||
const customerB = ref({ time: 3, status: 'waiting' })
|
||||
const customerC = ref({ time: 5, status: 'waiting' })
|
||||
|
||||
const modes = [
|
||||
{ value: 'sync', label: '同步模式 🐢' },
|
||||
{ value: 'async', label: '异步模式 ⚡' }
|
||||
]
|
||||
|
||||
const reset = () => {
|
||||
elapsedTime.value = 0
|
||||
customerA.value = { time: 2, status: 'waiting' }
|
||||
customerB.value = { time: 3, status: 'waiting' }
|
||||
customerC.value = { time: 5, status: 'waiting' }
|
||||
}
|
||||
|
||||
const start = async () => {
|
||||
if (isRunning.value) return
|
||||
isRunning.value = true
|
||||
reset()
|
||||
|
||||
if (mode.value === 'sync') {
|
||||
// 同步模式:依次执行
|
||||
await processCustomer(customerA, 2000)
|
||||
await processCustomer(customerB, 3000)
|
||||
await processCustomer(customerC, 5000)
|
||||
} else {
|
||||
// 异步模式:同时执行
|
||||
await Promise.all([
|
||||
processCustomer(customerA, 2000),
|
||||
processCustomer(customerB, 3000),
|
||||
processCustomer(customerC, 5000)
|
||||
])
|
||||
}
|
||||
|
||||
isRunning.value = false
|
||||
}
|
||||
|
||||
const processCustomer = async (customer, realTime) => {
|
||||
customer.status = 'cooking'
|
||||
await new Promise(resolve => setTimeout(resolve, realTime))
|
||||
customer.status = 'done'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="async-restaurant-demo">
|
||||
<h3>异步:同步 vs 异步</h3>
|
||||
|
||||
<div class="mode-selector">
|
||||
<button
|
||||
v-for="m in modes"
|
||||
:key="m.value"
|
||||
@click="mode = m.value"
|
||||
:class="{ 'active': mode === m.value }"
|
||||
class="mode-btn"
|
||||
:disabled="isRunning"
|
||||
>
|
||||
{{ m.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="restaurant-scene">
|
||||
<!-- 厨房 -->
|
||||
<div class="kitchen">
|
||||
<h4>厨房</h4>
|
||||
<div class="stoves">
|
||||
<div
|
||||
class="stove"
|
||||
:class="{ 'cooking': customerA.status === 'cooking', 'done': customerA.status === 'done' }"
|
||||
>
|
||||
<div class="stove-label">灶位 1</div>
|
||||
<div class="stove-content">
|
||||
<div v-if="customerA.status === 'cooking'" class="cooking-text">煮面 {{ customerA.time }}s</div>
|
||||
<div v-if="customerA.status === 'done'" class="done-text">✅ 完成</div>
|
||||
<div v-if="customerA.status === 'waiting'" class="waiting-text">空闲</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="stove"
|
||||
:class="{ 'cooking': customerB.status === 'cooking', 'done': customerB.status === 'done' }"
|
||||
>
|
||||
<div class="stove-label">灶位 2</div>
|
||||
<div class="stove-content">
|
||||
<div v-if="customerB.status === 'cooking'" class="cooking-text">炒饭 {{ customerB.time }}s</div>
|
||||
<div v-if="customerB.status === 'done'" class="done-text">✅ 完成</div>
|
||||
<div v-if="customerB.status === 'waiting'" class="waiting-text">空闲</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="stove"
|
||||
:class="{ 'cooking': customerC.status === 'cooking', 'done': customerC.status === 'done' }"
|
||||
>
|
||||
<div class="stove-label">灶位 3</div>
|
||||
<div class="stove-content">
|
||||
<div v-if="customerC.status === 'cooking'" class="cooking-text">烤鱼 {{ customerC.time }}s</div>
|
||||
<div v-if="customerC.status === 'done'" class="done-text">✅ 完成</div>
|
||||
<div v-if="customerC.status === 'waiting'" class="waiting-text">空闲</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 顾客 -->
|
||||
<div class="customers">
|
||||
<h4>顾客</h4>
|
||||
<div class="customer-list">
|
||||
<div class="customer" :class="{ 'served': customerA.status === 'done' }">
|
||||
<div class="customer-avatar">👤</div>
|
||||
<div class="customer-info">
|
||||
<div class="customer-name">顾客 A</div>
|
||||
<div class="customer-order">煮面 ({{ customerA.time }}秒)</div>
|
||||
</div>
|
||||
<div v-if="customerA.status === 'done'" class="check-mark">✅</div>
|
||||
</div>
|
||||
<div class="customer" :class="{ 'served': customerB.status === 'done' }">
|
||||
<div class="customer-avatar">👤</div>
|
||||
<div class="customer-info">
|
||||
<div class="customer-name">顾客 B</div>
|
||||
<div class="customer-order">炒饭 ({{ customerB.time }}秒)</div>
|
||||
</div>
|
||||
<div v-if="customerB.status === 'done'" class="check-mark">✅</div>
|
||||
</div>
|
||||
<div class="customer" :class="{ 'served': customerC.status === 'done' }">
|
||||
<div class="customer-avatar">👤</div>
|
||||
<div class="customer-info">
|
||||
<div class="customer-name">顾客 C</div>
|
||||
<div class="customer-order">烤鱼 ({{ customerC.time }}秒)</div>
|
||||
</div>
|
||||
<div v-if="customerC.status === 'done'" class="check-mark">✅</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button @click="start" :disabled="isRunning" class="btn-start">
|
||||
{{ isRunning ? '执行中...' : '开始' }}
|
||||
</button>
|
||||
<button @click="reset" :disabled="isRunning" class="btn-reset">
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="comparison" v-if="!isRunning && (customerA.status === 'done' || customerB.status === 'done')">
|
||||
<div class="comparison-item">
|
||||
<strong>同步模式:</strong> 10 秒(依次执行)
|
||||
</div>
|
||||
<div class="comparison-item">
|
||||
<strong>异步模式:</strong> 约 5 秒(同时执行)
|
||||
</div>
|
||||
<div class="tip">JavaScript 用的就是异步模式——遇到耗时操作(如网络请求),不会傻等,而是先去做别的事。</div>
|
||||
</div>
|
||||
|
||||
<div class="code-display">
|
||||
<h4>代码对比</h4>
|
||||
<div class="code-comparison">
|
||||
<div class="code-block">
|
||||
<h5>同步(阻塞)</h5>
|
||||
<pre><code>console.log("1")
|
||||
console.log("2") // 等上面执行完
|
||||
console.log("3")
|
||||
// 输出:1, 2, 3</code></pre>
|
||||
</div>
|
||||
<div class="code-block">
|
||||
<h5>异步(不阻塞)</h5>
|
||||
<pre><code>console.log("1")
|
||||
setTimeout(() => console.log("2"), 1000)
|
||||
console.log("3")
|
||||
// 输出:1, 3, 2</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.async-restaurant-demo {
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin: 24px 0;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.mode-selector {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
padding: 10px 20px;
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.mode-btn:hover:not(:disabled) {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.mode-btn.active {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
background: var(--vp-c-brand-1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mode-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.restaurant-scene {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.restaurant-scene {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.kitchen, .customers {
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.stoves {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stove {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stove.cooking {
|
||||
border-color: #ed8936;
|
||||
animation: pulse 1s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(237, 137, 54, 0.4); }
|
||||
50% { box-shadow: 0 0 0 8px rgba(237, 137, 54, 0); }
|
||||
}
|
||||
|
||||
.stove.done {
|
||||
border-color: #38a169;
|
||||
}
|
||||
|
||||
.stove-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.stove-content {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.cooking-text {
|
||||
color: #ed8936;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.done-text {
|
||||
color: #38a169;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.waiting-text {
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.customer-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.customer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.customer.served {
|
||||
border-color: #38a169;
|
||||
}
|
||||
|
||||
.customer-avatar {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.customer-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.customer-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.customer-order {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.check-mark {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.btn-start {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-start:hover:not(:disabled) {
|
||||
background: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.btn-start:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-reset {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.btn-reset:hover:not(:disabled) {
|
||||
background: var(--vp-c-bg-soft-hover);
|
||||
}
|
||||
|
||||
.comparison {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-left: 4px solid var(--vp-c-brand-1);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.comparison-item {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.comparison-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tip {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--vp-c-border);
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-brand-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.code-display {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-display h4 {
|
||||
color: #d4d4d4;
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.code-comparison {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.code-comparison {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.code-block h5 {
|
||||
color: #d4d4d4;
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.code-block pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-block code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,309 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const title = ref('我的网页')
|
||||
const items = ref(['项目1', '项目2'])
|
||||
const paragraphColor = ref('black')
|
||||
|
||||
const modifyTitle = () => {
|
||||
title.value = 'Hello World!'
|
||||
}
|
||||
|
||||
const addItem = () => {
|
||||
const id = items.value.length + 1
|
||||
items.value.push(`新项目${id}`)
|
||||
}
|
||||
|
||||
const changeColor = () => {
|
||||
paragraphColor.value = paragraphColor.value === 'black' ? 'red' : 'black'
|
||||
}
|
||||
|
||||
const removeItem = () => {
|
||||
if (items.value.length > 0) {
|
||||
items.value.pop()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dom-tree-demo">
|
||||
<h3>DOM 树:JavaScript 看到的网页</h3>
|
||||
|
||||
<div class="demo-container">
|
||||
<!-- 左侧:迷你网页 -->
|
||||
<div class="webpage-preview">
|
||||
<div class="browser-bar">
|
||||
<div class="dots">
|
||||
<span class="dot red"></span>
|
||||
<span class="dot yellow"></span>
|
||||
<span class="dot green"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="webpage-content">
|
||||
<h1>{{ title }}</h1>
|
||||
<p :style="{ color: paragraphColor }">欢迎光临</p>
|
||||
<ul>
|
||||
<li v-for="(item, index) in items" :key="index">{{ item }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:DOM 树 -->
|
||||
<div class="dom-tree">
|
||||
<div class="tree-node">
|
||||
<span class="tag"><html></span>
|
||||
<div class="tree-children">
|
||||
<div class="tree-node">
|
||||
<span class="tag"><body></span>
|
||||
<div class="tree-children">
|
||||
<div class="tree-node" :class="{ 'active': title === 'Hello World!' }">
|
||||
<span class="tag"><h1></span>
|
||||
<span class="text">{{ title }}</span>
|
||||
</div>
|
||||
<div class="tree-node" :class="{ 'active': paragraphColor === 'red' }">
|
||||
<span class="tag"><p></span>
|
||||
<span class="text">欢迎光临</span>
|
||||
</div>
|
||||
<div class="tree-node">
|
||||
<span class="tag"><ul></span>
|
||||
<div class="tree-children">
|
||||
<div class="tree-node" v-for="(item, index) in items" :key="index">
|
||||
<span class="tag"><li></span>
|
||||
<span class="text">{{ item }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button @click="modifyTitle" class="btn-primary">修改标题</button>
|
||||
<button @click="addItem" class="btn-secondary">添加列表项</button>
|
||||
<button @click="changeColor" class="btn-secondary">改变段落颜色</button>
|
||||
<button @click="removeItem" class="btn-danger">删除列表项</button>
|
||||
</div>
|
||||
|
||||
<div class="code-display">
|
||||
<h4>对应代码</h4>
|
||||
<pre><code v-if="title === 'Hello World!'">document.querySelector('h1').textContent = '{{ title }}'</code>
|
||||
<code v-else-if="paragraphColor === 'red'">document.querySelector('p').style.color = '{{ paragraphColor }}'</code>
|
||||
<code v-else>点击上方按钮查看对应代码</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dom-tree-demo {
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin: 24px 0;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.webpage-preview {
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.browser-bar {
|
||||
background: #f0f0f0;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--vp-c-border);
|
||||
}
|
||||
|
||||
.dots {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.dot.red {
|
||||
background: #ff5f56;
|
||||
}
|
||||
|
||||
.dot.yellow {
|
||||
background: #ffbd2e;
|
||||
}
|
||||
|
||||
.dot.green {
|
||||
background: #27c93f;
|
||||
}
|
||||
|
||||
.webpage-content {
|
||||
padding: 16px;
|
||||
background: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.webpage-content h1 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.webpage-content p {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.webpage-content ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.webpage-content li {
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dom-tree {
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.tree-children {
|
||||
margin-left: 20px;
|
||||
border-left: 2px solid var(--vp-c-border);
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
color: var(--vp-c-brand-1);
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tree-node.active {
|
||||
background: rgba(62, 175, 124, 0.1);
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
animation: highlight 1s ease;
|
||||
}
|
||||
|
||||
@keyframes highlight {
|
||||
0%, 100% { background: transparent; }
|
||||
50% { background: rgba(62, 175, 124, 0.2); }
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--vp-c-bg-soft-hover);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #f56565;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #e53e3e;
|
||||
}
|
||||
|
||||
.code-display {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-display h4 {
|
||||
color: #d4d4d4;
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.code-display pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-display code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,358 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const price = ref(100)
|
||||
const discount = ref(0.8)
|
||||
const result = ref(null)
|
||||
const isRunning = ref(false)
|
||||
const functionType = ref('arrow') // 'declaration', 'expression', 'arrow'
|
||||
|
||||
const functionTypes = [
|
||||
{ value: 'declaration', label: 'function 声明' },
|
||||
{ value: 'expression', label: '函数表达式' },
|
||||
{ value: 'arrow', label: '箭头函数' }
|
||||
]
|
||||
|
||||
const execute = async () => {
|
||||
if (isRunning.value) return
|
||||
isRunning.value = true
|
||||
result.value = null
|
||||
|
||||
// 模拟处理动画
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
result.value = price.value * discount.value
|
||||
isRunning.value = false
|
||||
}
|
||||
|
||||
const currentCode = ref(`const calculatePrice = (price, discount) => {
|
||||
return price * discount
|
||||
}`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="function-machine-demo">
|
||||
<h3>函数就像一台机器</h3>
|
||||
|
||||
<div class="pipeline">
|
||||
<!-- 输入区 -->
|
||||
<div class="pipeline-section input-section">
|
||||
<h4>参数(输入)</h4>
|
||||
<div class="input-group">
|
||||
<label>
|
||||
价格:
|
||||
<input v-model.number="price" type="number" min="0" :disabled="isRunning" />
|
||||
</label>
|
||||
<label>
|
||||
折扣:
|
||||
<select v-model.number="discount" :disabled="isRunning">
|
||||
<option :value="0.8">8 折 (0.8)</option>
|
||||
<option :value="0.5">5 折 (0.5)</option>
|
||||
<option :value="0.7">7 折 (0.7)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 机器区 -->
|
||||
<div class="pipeline-section machine-section">
|
||||
<h4>函数</h4>
|
||||
<div class="machine">
|
||||
<div class="machine-label">calculatePrice</div>
|
||||
<div class="machine-code">
|
||||
<pre v-if="functionType === 'declaration'"><code>return price * discount</code></pre>
|
||||
<pre v-else-if="functionType === 'expression'"><code>return price * discount</code></pre>
|
||||
<pre v-else><code>price * discount</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="function-type-selector">
|
||||
<button
|
||||
v-for="type in functionTypes"
|
||||
:key="type.value"
|
||||
@click="functionType = type.value"
|
||||
:class="{ active: functionType === type.value }"
|
||||
class="type-btn"
|
||||
>
|
||||
{{ type.label }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="tip" v-if="functionType !== 'arrow'">✏️ 写法不同,但做的事一模一样</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出区 -->
|
||||
<div class="pipeline-section output-section">
|
||||
<h4>返回值(输出)</h4>
|
||||
<div class="output-display" :class="{ 'processing': isRunning }">
|
||||
<div v-if="result === null" class="placeholder">?</div>
|
||||
<div v-else class="result">¥{{ result.toFixed(2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button @click="execute" :disabled="isRunning" class="btn-execute">
|
||||
{{ isRunning ? '处理中...' : '执行 ▶' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="code-display">
|
||||
<h4>当前函数定义</h4>
|
||||
<pre><code v-if="functionType === 'declaration'">function calculatePrice(price, discount) {
|
||||
return price * discount
|
||||
}</code>
|
||||
<code v-else-if="functionType === 'expression'">const calculatePrice = function(price, discount) {
|
||||
return price * discount
|
||||
}</code>
|
||||
<code v-else>const calculatePrice = (price, discount) => {
|
||||
return price * discount
|
||||
}
|
||||
|
||||
// 或者更简洁:
|
||||
const calculatePrice = (price, discount) => price * discount</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.function-machine-demo {
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin: 24px 0;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.pipeline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pipeline {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.pipeline-section {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
input, select {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
input:disabled, select:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.machine {
|
||||
background: var(--vp-c-brand-1);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.machine-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.machine-code pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.machine-code code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.function-type-selector {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.type-btn {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.type-btn:hover {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.type-btn.active {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.tip {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-brand-1);
|
||||
text-align: center;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.output-display {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
font-family: 'Courier New', monospace;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.output-display.processing {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
animation: pulse 1s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(62, 175, 124, 0.4); }
|
||||
50% { box-shadow: 0 0 0 8px rgba(62, 175, 124, 0); }
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.result {
|
||||
color: var(--vp-c-brand-1);
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn-execute {
|
||||
padding: 10px 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
background: var(--vp-c-brand-1);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-execute:hover:not(:disabled) {
|
||||
background: var(--vp-c-brand-2);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.btn-execute:active:not(:disabled) {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.btn-execute:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.code-display {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-display h4 {
|
||||
color: #d4d4d4;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.code-display pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-display code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,475 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const isPlaying = ref(false)
|
||||
const currentStep = ref(0)
|
||||
const codeQueue = ref([
|
||||
{ id: 1, code: 'console.log("1")', type: 'sync', output: '1' },
|
||||
{ id: 2, code: 'setTimeout(() => console.log("2"), 0)', type: 'async', output: '2' },
|
||||
{ id: 3, code: 'console.log("3")', type: 'sync', output: '3' },
|
||||
{ id: 4, code: 'fetch("/api").then(() => console.log("4"))', type: 'async', output: '4' },
|
||||
{ id: 5, code: 'console.log("5")', type: 'sync', output: '5' }
|
||||
])
|
||||
const taskQueue = ref([])
|
||||
const outputLog = ref([])
|
||||
|
||||
const steps = [
|
||||
{ description: '执行 console.log("1")', output: '1' },
|
||||
{ description: '遇到 setTimeout,把回调贴到便签栏', output: null },
|
||||
{ description: '执行 console.log("3")', output: '3' },
|
||||
{ description: '遇到 fetch,把回调贴到便签栏', output: null },
|
||||
{ description: '执行 console.log("5")', output: '5' },
|
||||
{ description: '执行 setTimeout 的回调', output: '2' },
|
||||
{ description: '执行 fetch 的回调', output: '4' }
|
||||
]
|
||||
|
||||
const reset = () => {
|
||||
currentStep.value = 0
|
||||
taskQueue.value = []
|
||||
outputLog.value = []
|
||||
isPlaying.value = false
|
||||
}
|
||||
|
||||
const nextStep = () => {
|
||||
if (currentStep.value >= steps.length) return
|
||||
|
||||
const step = steps[currentStep.value]
|
||||
|
||||
if (currentStep.value === 1) {
|
||||
taskQueue.value.push({ id: 2, code: 'console.log("2")', status: 'pending' })
|
||||
} else if (currentStep.value === 3) {
|
||||
taskQueue.value.push({ id: 4, code: 'console.log("4")', status: 'pending' })
|
||||
} else if (currentStep.value === 4) {
|
||||
taskQueue.value[0].status = 'ready'
|
||||
} else if (currentStep.value === 5) {
|
||||
outputLog.value.push({ output: '2', source: 'setTimeout' })
|
||||
taskQueue.value.shift()
|
||||
taskQueue.value[0].status = 'ready'
|
||||
} else if (currentStep.value === 6) {
|
||||
outputLog.value.push({ output: '4', source: 'fetch' })
|
||||
}
|
||||
|
||||
if (step.output) {
|
||||
outputLog.value.push({ output: step.output, source: '同步代码' })
|
||||
}
|
||||
|
||||
currentStep.value++
|
||||
}
|
||||
|
||||
const play = async () => {
|
||||
if (isPlaying.value) return
|
||||
isPlaying.value = true
|
||||
|
||||
while (currentStep.value < steps.length && isPlaying.value) {
|
||||
nextStep()
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
}
|
||||
|
||||
isPlaying.value = false
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
isPlaying.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="event-loop-demo">
|
||||
<h3>事件循环:JavaScript 的执行机制</h3>
|
||||
|
||||
<div class="workspace">
|
||||
<!-- 代码队列 -->
|
||||
<div class="code-queue-section">
|
||||
<h4>代码队列</h4>
|
||||
<div class="queue">
|
||||
<div
|
||||
v-for="(item, index) in codeQueue"
|
||||
:key="item.id"
|
||||
class="code-item"
|
||||
:class="{
|
||||
'active': currentStep === index,
|
||||
'processed': currentStep > index
|
||||
}"
|
||||
>
|
||||
<div class="item-index">{{ item.id }}</div>
|
||||
<div class="item-code">{{ item.code }}</div>
|
||||
<div v-if="currentStep === index" class="executing">执行中</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工位 -->
|
||||
<div class="worker-section">
|
||||
<h4>工位(单线程)</h4>
|
||||
<div class="worker">
|
||||
<div class="worker-emoji">👨💻</div>
|
||||
<div class="worker-status">
|
||||
{{ currentStep < steps.length ? '正在执行' : '执行完成' }}
|
||||
</div>
|
||||
<div v-if="currentStep < steps.length" class="current-task">
|
||||
{{ steps[currentStep]?.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 便签栏 -->
|
||||
<div class="task-queue-section">
|
||||
<h4>便签栏(任务队列)</h4>
|
||||
<div class="task-queue">
|
||||
<div
|
||||
v-for="task in taskQueue"
|
||||
:key="task.id"
|
||||
class="task-item"
|
||||
:class="{ 'ready': task.status === 'ready' }"
|
||||
>
|
||||
<div class="task-code">{{ task.code }}</div>
|
||||
<div class="task-status">
|
||||
{{ task.status === 'ready' ? '✅ 就绪' : '⏳ 等待中...' }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="taskQueue.length === 0" class="empty-queue">
|
||||
暂无待办任务
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出日志 -->
|
||||
<div class="output-section">
|
||||
<h4>输出日志</h4>
|
||||
<div class="output-log">
|
||||
<div v-if="outputLog.length === 0" class="empty-log">等待输出...</div>
|
||||
<div
|
||||
v-for="(log, index) in outputLog"
|
||||
:key="index"
|
||||
class="log-entry"
|
||||
>
|
||||
<span class="log-output">{{ log.output }}</span>
|
||||
<span class="log-source">({{ log.source }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制按钮 -->
|
||||
<div class="controls">
|
||||
<button @click="play" :disabled="isPlaying || currentStep >= steps.length" class="btn-play">
|
||||
{{ isPlaying ? '执行中...' : '▶ 自动播放' }}
|
||||
</button>
|
||||
<button @click="nextStep" :disabled="isPlaying || currentStep >= steps.length" class="btn-step">
|
||||
⏭ 单步执行
|
||||
</button>
|
||||
<button @click="stop" :disabled="!isPlaying" class="btn-stop">
|
||||
⏸ 停止
|
||||
</button>
|
||||
<button @click="reset" class="btn-reset">
|
||||
🔄 重置
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 说明 -->
|
||||
<div class="explanation">
|
||||
<p><strong>执行顺序:</strong>{{ outputLog.map(l => l.output).join(', ') || '还未开始' }}</p>
|
||||
<p><strong>代码书写顺序:</strong>1, 2, 3, 4, 5</p>
|
||||
<p class="highlight">代码从上到下写的,但执行顺序不一定从上到下——因为异步操作会被"推迟"到当前代码执行完之后。</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.event-loop-demo {
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin: 24px 0;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.workspace {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.workspace {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.code-queue-section,
|
||||
.worker-section,
|
||||
.task-queue-section {
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.queue,
|
||||
.task-queue {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.code-item,
|
||||
.task-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.code-item.active {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
box-shadow: 0 0 0 3px rgba(62, 175, 124, 0.1);
|
||||
animation: highlight 1s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes highlight {
|
||||
0%, 100% { background: var(--vp-c-bg); }
|
||||
50% { background: rgba(62, 175, 124, 0.1); }
|
||||
}
|
||||
|
||||
.code-item.processed {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.item-index {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.item-code,
|
||||
.task-code {
|
||||
flex: 1;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.executing {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand-1);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-item.ready {
|
||||
border-color: #38a169;
|
||||
animation: pulse 1s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(56, 161, 105, 0.4); }
|
||||
50% { box-shadow: 0 0 0 6px rgba(56, 161, 105, 0); }
|
||||
}
|
||||
|
||||
.task-status {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.empty-queue {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.worker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.worker-emoji {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.worker-status {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.current-task {
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
padding: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.output-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.output-log {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
min-height: 60px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.empty-log {
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
padding: 8px 12px;
|
||||
background: var(--vp-c-brand-1);
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.log-output {
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.log-source {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.btn-play {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-play:hover:not(:disabled) {
|
||||
background: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.btn-step {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.btn-step:hover:not(:disabled) {
|
||||
background: var(--vp-c-bg-soft-hover);
|
||||
}
|
||||
|
||||
.btn-stop {
|
||||
background: #ed8936;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-stop:hover:not(:disabled) {
|
||||
background: #dd6b20;
|
||||
}
|
||||
|
||||
.btn-reset {
|
||||
background: #f56565;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-reset:hover {
|
||||
background: #e53e3e;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.explanation {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-left: 4px solid var(--vp-c-brand-1);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.explanation p {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.explanation p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.explanation strong {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.explanation .highlight {
|
||||
padding: 12px;
|
||||
background: rgba(62, 175, 124, 0.1);
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,377 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
// 基本类型
|
||||
const basicA = ref(10)
|
||||
const basicB = ref(null)
|
||||
const basicStep = ref(0)
|
||||
const basicMessage = ref('')
|
||||
|
||||
// 引用类型
|
||||
const obj1Data = ref({ name: '张三', age: 25 })
|
||||
const obj2Exists = ref(false)
|
||||
const obj2Age = ref('')
|
||||
const refMessage = ref('')
|
||||
|
||||
const basicCopy = () => {
|
||||
basicB.value = basicA.value
|
||||
basicStep.value = 1
|
||||
basicMessage.value = '✅ 基本类型复制的是值本身'
|
||||
}
|
||||
|
||||
const basicModify = () => {
|
||||
if (basicB.value === null) {
|
||||
basicMessage.value = '⚠️ 请先复制'
|
||||
return
|
||||
}
|
||||
basicB.value = 20
|
||||
basicMessage.value = '✅ 修改 b 不影响 a'
|
||||
}
|
||||
|
||||
const refCopy = () => {
|
||||
obj2Exists.value = true
|
||||
obj2Age.value = obj1Data.value.age
|
||||
refMessage.value = '⚠️ 两个变量指向同一份数据'
|
||||
}
|
||||
|
||||
const refModify = () => {
|
||||
if (!obj2Exists.value) {
|
||||
refMessage.value = '⚠️ 请先复制'
|
||||
return
|
||||
}
|
||||
obj1Data.value.age = 30
|
||||
obj2Age.value = 30
|
||||
refMessage.value = '❌ 两个变量指向同一份数据,改了一个另一个也变了!'
|
||||
}
|
||||
|
||||
const refSpreadCopy = () => {
|
||||
obj2Exists.value = true
|
||||
obj2Age.value = 25
|
||||
refMessage.value = '✅ 用展开运算符创建真正的副本,现在互不影响'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="reference-demo">
|
||||
<h3>值 vs 引用</h3>
|
||||
|
||||
<div class="demo-container">
|
||||
<!-- 左侧:基本类型 -->
|
||||
<div class="demo-section basic-section">
|
||||
<h4>基本类型(复制值)</h4>
|
||||
|
||||
<div class="visualization">
|
||||
<div class="box" :class="{ 'active': basicA !== null }">
|
||||
<div class="box-label">a</div>
|
||||
<div class="box-value">{{ basicA }}</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow" v-if="basicStep >= 1">
|
||||
<div class="arrow-line"></div>
|
||||
<div class="arrow-head">→</div>
|
||||
</div>
|
||||
|
||||
<div class="box" :class="{ 'active': basicB !== null }" v-if="basicStep >= 1">
|
||||
<div class="box-label">b</div>
|
||||
<div class="box-value">{{ basicB }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message" v-if="basicMessage">{{ basicMessage }}</div>
|
||||
|
||||
<div class="controls">
|
||||
<button @click="basicCopy" class="btn-primary">let b = a(复制)</button>
|
||||
<button @click="basicModify" class="btn-secondary">b = 20</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:引用类型 -->
|
||||
<div class="demo-section reference-section">
|
||||
<h4>引用类型(复制地址)</h4>
|
||||
|
||||
<div class="visualization">
|
||||
<!-- 数据区 -->
|
||||
<div class="data-area">
|
||||
<div class="data-label">数据区</div>
|
||||
<div class="data-content">
|
||||
<div>{{ `{ name: "${obj1Data.name}", age: ${obj1Data.age} }` }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pointers">
|
||||
<div class="pointer">
|
||||
<div class="pointer-label">obj1</div>
|
||||
<div class="arrow-line-down"></div>
|
||||
</div>
|
||||
|
||||
<div class="pointer" v-if="obj2Exists">
|
||||
<div class="pointer-label">obj2</div>
|
||||
<div class="arrow-line-down"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message" v-if="refMessage">{{ refMessage }}</div>
|
||||
|
||||
<div class="controls">
|
||||
<button @click="refCopy" class="btn-primary">let obj2 = obj1(复制)</button>
|
||||
<button @click="refModify" class="btn-danger">obj2.age = 30</button>
|
||||
<button @click="refSpreadCopy" class="btn-success">用展开运算符创建副本</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="code-display">
|
||||
<h4>代码</h4>
|
||||
<pre><code>// 基本类型
|
||||
let a = {{ basicA }}
|
||||
{{ basicB !== null ? `let b = ${basicB}` : '' }}
|
||||
{{ basicB !== null ? `b = ${basicB}` : '' }}
|
||||
console.log(a) // {{ basicA }}
|
||||
|
||||
// 引用类型
|
||||
let obj1 = { name: "{{ obj1Data.name }}", age: {{ obj1Data.age }} }
|
||||
{{ obj2Exists ? 'let obj2 = obj1 // 指向同一份数据' : '' }}
|
||||
{{ obj2Exists ? `obj2.age = ${obj1Data.age}` : '' }}
|
||||
console.log(obj1.age) // {{ obj1Data.age }}
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.reference-demo {
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin: 24px 0;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
border: 1px dashed var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.visualization {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 16px;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.box {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--vp-c-bg);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.box.active {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
box-shadow: 0 0 0 3px rgba(62, 175, 124, 0.1);
|
||||
}
|
||||
|
||||
.box-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.box-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.arrow-line {
|
||||
width: 40px;
|
||||
height: 2px;
|
||||
background: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.arrow-head {
|
||||
font-size: 24px;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.data-area {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.data-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.data-content {
|
||||
border: 2px solid var(--vp-c-brand-1);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.pointers {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pointer-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.arrow-line-down {
|
||||
width: 2px;
|
||||
height: 30px;
|
||||
background: var(--vp-c-brand-1);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.message {
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--vp-c-bg-soft-hover);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #f56565;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #e53e3e;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #38a169;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #2f855a;
|
||||
}
|
||||
|
||||
.code-display {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-display h4 {
|
||||
color: #d4d4d4;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.code-display pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-display code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,284 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const activeScope = ref('block') // 'global', 'function', 'block'
|
||||
const explanation = ref('')
|
||||
|
||||
const scopes = [
|
||||
{
|
||||
id: 'global',
|
||||
name: '全局作用域',
|
||||
color: '#a0aec0',
|
||||
variables: ['appName = "Todo"'],
|
||||
canSee: ['appName']
|
||||
},
|
||||
{
|
||||
id: 'function',
|
||||
name: '函数 greet() 作用域',
|
||||
color: '#4299e1',
|
||||
variables: ['message = "你好"'],
|
||||
canSee: ['appName', 'message']
|
||||
},
|
||||
{
|
||||
id: 'block',
|
||||
name: 'if 块作用域',
|
||||
color: '#38a169',
|
||||
variables: ['greeting = message + appName'],
|
||||
canSee: ['appName', 'message', 'greeting']
|
||||
}
|
||||
]
|
||||
|
||||
const updateExplanation = () => {
|
||||
const scope = scopes.find(s => s.id === activeScope.value)
|
||||
if (scope) {
|
||||
const visible = scope.canSee.map(v => `✅ ${v}`).join('、')
|
||||
explanation.value = `在这个位置,你能使用这些变量:${visible}`
|
||||
}
|
||||
}
|
||||
|
||||
updateExplanation()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="scope-demo">
|
||||
<h3>作用域:变量的"可见范围"</h3>
|
||||
|
||||
<div class="scopes-container">
|
||||
<!-- 全局作用域 -->
|
||||
<div
|
||||
class="scope global-scope"
|
||||
:class="{ 'active': activeScope === 'global' }"
|
||||
@click="activeScope = 'global'; updateExplanation()"
|
||||
>
|
||||
<div class="scope-header">全局作用域</div>
|
||||
<div class="scope-content">
|
||||
<div class="variable" :class="{ 'visible': activeScope === 'global', 'dimmed': activeScope !== 'global' }">
|
||||
appName = "Todo"
|
||||
</div>
|
||||
|
||||
<!-- 函数作用域 -->
|
||||
<div class="nested-scope">
|
||||
<div
|
||||
class="scope function-scope"
|
||||
:class="{ 'active': activeScope === 'function' }"
|
||||
@click.stop="activeScope = 'function'; updateExplanation()"
|
||||
>
|
||||
<div class="scope-header">函数 greet() 作用域</div>
|
||||
<div class="scope-content">
|
||||
<div class="variable" :class="{ 'visible': ['global', 'function'].includes(activeScope), 'dimmed': !['global', 'function'].includes(activeScope) }">
|
||||
appName = "Todo"
|
||||
</div>
|
||||
<div class="variable" :class="{ 'visible': activeScope === 'function', 'dimmed': activeScope !== 'function' }">
|
||||
message = "你好"
|
||||
</div>
|
||||
|
||||
<!-- 块级作用域 -->
|
||||
<div class="nested-scope">
|
||||
<div
|
||||
class="scope block-scope"
|
||||
:class="{ 'active': activeScope === 'block' }"
|
||||
@click.stop="activeScope = 'block'; updateExplanation()"
|
||||
>
|
||||
<div class="scope-header">if 块作用域</div>
|
||||
<div class="scope-content">
|
||||
<div class="variable" :class="{ 'visible': true, 'dimmed': false }">
|
||||
appName = "Todo"
|
||||
</div>
|
||||
<div class="variable" :class="{ 'visible': true, 'dimmed': false }">
|
||||
message = "你好"
|
||||
</div>
|
||||
<div class="variable" :class="{ 'visible': activeScope === 'block', 'dimmed': activeScope !== 'block' }">
|
||||
greeting = message + appName
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="explanation" v-if="explanation">
|
||||
{{ explanation }}
|
||||
</div>
|
||||
|
||||
<div class="code-display">
|
||||
<h4>对应代码</h4>
|
||||
<pre><code>const appName = "Todo" // 全局作用域
|
||||
|
||||
function greet() {
|
||||
const message = "你好" // 函数作用域
|
||||
|
||||
if (true) {
|
||||
const greeting = message + appName // 块级作用域 ✅ 能看到外层的
|
||||
console.log(greeting)
|
||||
}
|
||||
|
||||
console.log(greeting) // ❌ 报错!外层看不到内层
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.scope-demo {
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin: 24px 0;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.scopes-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.scope {
|
||||
border: 3px solid var(--vp-c-border);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.scope:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.scope.active {
|
||||
border-width: 4px;
|
||||
box-shadow: 0 0 0 4px rgba(62, 175, 124, 0.1);
|
||||
}
|
||||
|
||||
.global-scope {
|
||||
border-color: #a0aec0;
|
||||
}
|
||||
|
||||
.global-scope.active {
|
||||
border-color: #a0aec0;
|
||||
box-shadow: 0 0 0 4px rgba(160, 174, 192, 0.2);
|
||||
}
|
||||
|
||||
.function-scope {
|
||||
border-color: #4299e1;
|
||||
}
|
||||
|
||||
.function-scope.active {
|
||||
border-color: #4299e1;
|
||||
box-shadow: 0 0 0 4px rgba(66, 153, 225, 0.2);
|
||||
}
|
||||
|
||||
.block-scope {
|
||||
border-color: #38a169;
|
||||
}
|
||||
|
||||
.block-scope.active {
|
||||
border-color: #38a169;
|
||||
box-shadow: 0 0 0 4px rgba(56, 161, 105, 0.2);
|
||||
}
|
||||
|
||||
.scope-header {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.scope-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.nested-scope {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.variable {
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: var(--vp-c-bg);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.variable.visible {
|
||||
color: var(--vp-c-text-1);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.variable.dimmed {
|
||||
color: var(--vp-c-text-3);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.explanation {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.code-display {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-display h4 {
|
||||
color: #d4d4d4;
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.code-display pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-display code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.scopes-container {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.scope {
|
||||
min-width: 280px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,278 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const name = ref('张三')
|
||||
const age = ref(25)
|
||||
const isStudent = ref(true)
|
||||
const showMessage = ref('')
|
||||
const showSuccess = ref(false)
|
||||
|
||||
const modifyAge = () => {
|
||||
age.value = 26
|
||||
showMessage.value = '✅ let 变量可以修改值'
|
||||
showSuccess.value = true
|
||||
setTimeout(() => {
|
||||
showMessage.value = ''
|
||||
showSuccess.value = false
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const modifyName = () => {
|
||||
showMessage.value = '❌ const 不能重新赋值'
|
||||
showSuccess.value = false
|
||||
setTimeout(() => {
|
||||
showMessage.value = ''
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
name.value = '张三'
|
||||
age.value = 25
|
||||
isStudent.value = true
|
||||
showMessage.value = ''
|
||||
}
|
||||
|
||||
const codeLines = ref([
|
||||
`const name = "张三"`,
|
||||
`let age = 25`,
|
||||
`const isStudent = true`
|
||||
])
|
||||
|
||||
const executeCode = ref([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="variable-box-demo">
|
||||
<h3>变量就像带名字的盒子</h3>
|
||||
|
||||
<div class="boxes-container">
|
||||
<!-- 盒子 1: const name -->
|
||||
<div class="variable-box const-box">
|
||||
<div class="box-label">const name</div>
|
||||
<div class="box-value">{{ name }}</div>
|
||||
<div class="box-icon">🔒</div>
|
||||
</div>
|
||||
|
||||
<!-- 盒子 2: let age -->
|
||||
<div class="variable-box let-box" :class="{ 'success': showSuccess && age === 26 }">
|
||||
<div class="box-label">let age</div>
|
||||
<div class="box-value">{{ age }}</div>
|
||||
<div class="box-icon">🔓</div>
|
||||
</div>
|
||||
|
||||
<!-- 盒子 3: const isStudent -->
|
||||
<div class="variable-box const-box">
|
||||
<div class="box-label">const isStudent</div>
|
||||
<div class="box-value">{{ isStudent }}</div>
|
||||
<div class="box-icon">🔒</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-bubble" :class="{ 'error': !showSuccess, 'success': showSuccess }" v-if="showMessage">
|
||||
{{ showMessage }}
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button @click="modifyAge" class="btn-primary">修改 age 为 26</button>
|
||||
<button @click="modifyName" class="btn-danger">修改 name 为李四</button>
|
||||
<button @click="reset" class="btn-secondary">重置</button>
|
||||
</div>
|
||||
|
||||
<div class="code-display">
|
||||
<pre><code>const name = "张三"
|
||||
let age = 25
|
||||
const isStudent = true
|
||||
|
||||
{{ executeCode.join('\n') }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.variable-box-demo {
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin: 24px 0;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.boxes-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.variable-box {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.variable-box.success {
|
||||
border-color: #3eaf7c;
|
||||
animation: pulse 0.5s ease;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.box-label {
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--vp-c-brand-1);
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.let-box .box-label {
|
||||
background: #42b983;
|
||||
}
|
||||
|
||||
.box-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.box-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.message-bubble.error {
|
||||
background: #fee;
|
||||
color: #c00;
|
||||
}
|
||||
|
||||
.message-bubble.success {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #f56565;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #e53e3e;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--vp-c-bg-soft-hover);
|
||||
}
|
||||
|
||||
.code-display {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-display pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-display code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.boxes-container {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.variable-box {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -480,9 +480,16 @@ import RequestStructureDemo from './components/appendix/api-design/RequestStruct
|
||||
import ResponseStructureDemo from './components/appendix/api-design/ResponseStructureDemo.vue'
|
||||
|
||||
// JavaScript Intro Components
|
||||
import VariableBoxDemo from './components/appendix/javascript-intro/VariableBoxDemo.vue'
|
||||
import ReferenceDemo from './components/appendix/javascript-intro/ReferenceDemo.vue'
|
||||
import FunctionMachineDemo from './components/appendix/javascript-intro/FunctionMachineDemo.vue'
|
||||
import ScopeDemo from './components/appendix/javascript-intro/ScopeDemo.vue'
|
||||
import ClosureDemo from './components/appendix/javascript-intro/ClosureDemo.vue'
|
||||
import DOMTreeDemo from './components/appendix/javascript-intro/DOMTreeDemo.vue'
|
||||
import AsyncRestaurantDemo from './components/appendix/javascript-intro/AsyncRestaurantDemo.vue'
|
||||
import JSEventLoopDemo from './components/appendix/javascript-intro/JSEventLoopDemo.vue'
|
||||
import VariableScopeDemo from './components/appendix/javascript-intro/VariableScopeDemo.vue'
|
||||
import DataTypeDemo from './components/appendix/javascript-intro/DataTypeDemo.vue'
|
||||
import ClosureDemo from './components/appendix/javascript-intro/ClosureDemo.vue'
|
||||
import ThisContextDemo from './components/appendix/javascript-intro/ThisContextDemo.vue'
|
||||
import PrototypeDemo from './components/appendix/javascript-intro/PrototypeDemo.vue'
|
||||
import AsyncDemo from './components/appendix/javascript-intro/AsyncDemo.vue'
|
||||
@@ -642,15 +649,6 @@ export default {
|
||||
app.component('ImperativeVsDeclarativeDemo', ImperativeVsDeclarativeDemo)
|
||||
app.component('ComponentReusabilityDemo', ComponentReusabilityDemo)
|
||||
|
||||
// Frontend Evolution Components Registration
|
||||
app.component('FrontendEvolutionDemo', FrontendEvolutionTimelineDemo)
|
||||
app.component('EvolutionSliceRequestDemo', EvolutionSliceRequestDemo)
|
||||
app.component('EvolutionResponsiveGridDemo', EvolutionResponsiveGridDemo)
|
||||
app.component('EvolutionJQueryVsStateDemo', EvolutionJQueryVsStateDemo)
|
||||
app.component('RoutingModeDemo', EvolutionRoutingModeDemo)
|
||||
app.component('RenderingStrategyDemo', EvolutionRenderingStrategyDemo)
|
||||
app.component('ImperativeVsDeclarativeDemo', EvolutionImperativeVsDeclarativeDemo)
|
||||
|
||||
app.component('BackendEvolutionDemo', BackendEvolutionDemo)
|
||||
app.component('BackendQuickStartDemo', BackendQuickStartDemo)
|
||||
app.component('EvolutionIntroDemo', EvolutionIntroDemo)
|
||||
@@ -975,12 +973,19 @@ export default {
|
||||
app.component('MQComparisonDemo', MQComparisonDemo)
|
||||
|
||||
// JavaScript Intro Components Registration
|
||||
app.component('VariableBoxDemo', VariableBoxDemo)
|
||||
app.component('ReferenceDemo', ReferenceDemo)
|
||||
app.component('FunctionMachineDemo', FunctionMachineDemo)
|
||||
app.component('ScopeDemo', ScopeDemo)
|
||||
app.component('VariableScopeDemo', VariableScopeDemo)
|
||||
app.component('DataTypeDemo', DataTypeDemo)
|
||||
app.component('ClosureDemo', ClosureDemo)
|
||||
app.component('ThisContextDemo', ThisContextDemo)
|
||||
app.component('PrototypeDemo', PrototypeDemo)
|
||||
app.component('AsyncDemo', AsyncDemo)
|
||||
app.component('DOMTreeDemo', DOMTreeDemo)
|
||||
app.component('AsyncRestaurantDemo', AsyncRestaurantDemo)
|
||||
app.component('JSEventLoopDemo', JSEventLoopDemo)
|
||||
},
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
|
||||
Reference in New Issue
Block a user