docs: 重构 README 附录展示 & 新增多个附录交互组件
README 更新: - 移除顶部 header.png 横幅图片 - 新增「附录知识库」板块,以 3×3 网格展示 9 大知识领域精选内容 - 附录链接指向部署版网站 (datawhalechina.github.io) - 阶段表格新增「附录」行,突出 80+ 交互式专题 - 章节标题「新手入门 & PM」简化为「零基础入门」 - News 新增 2026-02-25 附录知识库更新条目 新增交互组件: - 异步任务队列 (async-task-queues) 演示组件 - 文件存储 (file-storage) 演示组件 - 项目架构 (project-architecture) 演示组件 - 限流与背压 (rate-limiting) 演示组件 - 搜索引擎 (search-engines) 演示组件 - 计算机基础: AppLaunch/BiosUefi/OSBoot 等启动流程演示组件 新增附录文档: - 前端项目架构 (frontend-project-architecture.md) - 后端项目架构 (backend-project-architecture.md) 内容优化: - 算法思维、数据结构、编程语言、调试艺术等多篇附录内容更新 - HTML/CSS 布局、请求旅程等前后端文档完善 - 附录索引页 (index.md) 同步更新
This commit is contained in:
@@ -613,13 +613,13 @@ export default defineConfig({
|
||||
text: '编程语言图谱',
|
||||
link: '/zh-cn/appendix/1-computer-fundamentals/programming-languages'
|
||||
},
|
||||
{
|
||||
text: '类型系统入门',
|
||||
link: '/zh-cn/appendix/1-computer-fundamentals/type-systems'
|
||||
},
|
||||
{
|
||||
text: '编译原理入门',
|
||||
link: '/zh-cn/appendix/1-computer-fundamentals/compilers'
|
||||
},
|
||||
{
|
||||
text: '类型系统入门',
|
||||
link: '/zh-cn/appendix/1-computer-fundamentals/type-systems'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -725,6 +725,10 @@ export default defineConfig({
|
||||
text: '前端工程化全貌',
|
||||
link: '/zh-cn/appendix/3-browser-and-frontend/frontend-engineering'
|
||||
},
|
||||
{
|
||||
text: '前端项目架构设计',
|
||||
link: '/zh-cn/appendix/3-browser-and-frontend/frontend-project-architecture'
|
||||
},
|
||||
{
|
||||
text: '无障碍与国际化',
|
||||
link: '/zh-cn/appendix/3-browser-and-frontend/a11n-i18n'
|
||||
@@ -807,6 +811,10 @@ export default defineConfig({
|
||||
text: '后端分层架构',
|
||||
link: '/zh-cn/appendix/4-server-and-backend/backend-layered-architecture'
|
||||
},
|
||||
{
|
||||
text: '后端项目架构设计',
|
||||
link: '/zh-cn/appendix/4-server-and-backend/backend-project-architecture'
|
||||
},
|
||||
{
|
||||
text: '领域特定语言(DSL)',
|
||||
link: '/zh-cn/appendix/4-server-and-backend/domain-specific-languages'
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<!--
|
||||
AsyncComparisonDemo.vue
|
||||
异步任务框架对比演示
|
||||
-->
|
||||
<template>
|
||||
<div class="comparison-demo">
|
||||
<div class="header">
|
||||
<div class="title">主流异步任务框架对比</div>
|
||||
<div class="subtitle">点击查看各框架详情</div>
|
||||
</div>
|
||||
|
||||
<div class="framework-grid">
|
||||
<div
|
||||
v-for="fw in frameworks"
|
||||
:key="fw.name"
|
||||
:class="['fw-card', { active: selected === fw.name }]"
|
||||
@click="selected = fw.name"
|
||||
>
|
||||
<div class="fw-name">{{ fw.name }}</div>
|
||||
<div class="fw-lang">{{ fw.lang }}</div>
|
||||
<div class="fw-stars">
|
||||
<span v-for="n in 5" :key="n" :class="n <= fw.rating ? 'star-filled' : 'star-empty'">★</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentFw" class="detail-panel">
|
||||
<div class="detail-header">
|
||||
<span class="detail-name">{{ currentFw.name }}</span>
|
||||
<span class="detail-lang-tag">{{ currentFw.lang }}</span>
|
||||
</div>
|
||||
<div class="detail-desc">{{ currentFw.desc }}</div>
|
||||
<div class="detail-features">
|
||||
<div class="feature-title">核心特性:</div>
|
||||
<div class="feature-list">
|
||||
<span v-for="f in currentFw.features" :key="f" class="feature-tag">{{ f }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-usecase">
|
||||
<div class="usecase-title">典型场景:</div>
|
||||
<div class="usecase-text">{{ currentFw.usecase }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const selected = ref('Celery')
|
||||
|
||||
const frameworks = [
|
||||
{
|
||||
name: 'Celery',
|
||||
lang: 'Python',
|
||||
rating: 5,
|
||||
desc: 'Python 生态最流行的分布式任务队列,支持多种消息中间件(RabbitMQ、Redis),功能全面且社区活跃。',
|
||||
features: ['定时任务', '任务链', '结果存储', '自动重试', '优先级队列', '任务路由'],
|
||||
usecase: '数据处理管道、邮件发送、报表生成、机器学习训练任务'
|
||||
},
|
||||
{
|
||||
name: 'Sidekiq',
|
||||
lang: 'Ruby',
|
||||
rating: 5,
|
||||
desc: 'Ruby 生态的高性能后台任务处理器,基于 Redis,使用多线程模型,内存效率极高。',
|
||||
features: ['多线程', 'Web UI', '定时任务', '批量处理', '速率限制', '唯一任务'],
|
||||
usecase: 'Rails 应用的邮件、通知、数据导入导出'
|
||||
},
|
||||
{
|
||||
name: 'Bull',
|
||||
lang: 'Node.js',
|
||||
rating: 4,
|
||||
desc: 'Node.js 生态最成熟的任务队列库,基于 Redis,支持优先级、延迟任务、重复任务等。BullMQ 是其下一代版本。',
|
||||
features: ['优先级', '延迟任务', '速率限制', '并发控制', '事件驱动', 'Dashboard'],
|
||||
usecase: 'API 后台处理、文件转换、爬虫任务、通知推送'
|
||||
},
|
||||
{
|
||||
name: 'RQ',
|
||||
lang: 'Python',
|
||||
rating: 3,
|
||||
desc: '轻量级 Python 任务队列,基于 Redis,API 简洁易用。适合不需要 Celery 全部功能的中小项目。',
|
||||
features: ['简洁 API', '任务依赖', 'Worker 管理', '失败重试', 'Dashboard'],
|
||||
usecase: '中小型 Web 应用的后台任务处理'
|
||||
},
|
||||
{
|
||||
name: 'Kafka Streams',
|
||||
lang: 'Java/JVM',
|
||||
rating: 4,
|
||||
desc: '基于 Kafka 的流处理框架,适合高吞吐量的实时数据处理场景,天然支持分布式和容错。',
|
||||
features: ['流处理', '精确一次语义', '状态存储', '窗口操作', '高吞吐', '容错'],
|
||||
usecase: '实时数据管道、事件驱动架构、日志聚合分析'
|
||||
}
|
||||
]
|
||||
|
||||
const currentFw = computed(() => frameworks.find(f => f.name === selected.value))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.comparison-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
.header { margin-bottom: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.framework-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 0.5rem; margin-bottom: 1rem;
|
||||
}
|
||||
.fw-card {
|
||||
padding: 0.75rem; border-radius: 8px; background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider); cursor: pointer; text-align: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.fw-card:hover { border-color: var(--vp-c-brand); }
|
||||
.fw-card.active { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); }
|
||||
.fw-name { font-weight: 700; font-size: 0.95rem; }
|
||||
.fw-lang { font-size: 0.8rem; color: var(--vp-c-text-2); margin: 0.25rem 0; }
|
||||
.fw-stars { font-size: 0.85rem; }
|
||||
.star-filled { color: #f59e0b; }
|
||||
.star-empty { color: var(--vp-c-divider); }
|
||||
.detail-panel {
|
||||
padding: 1rem; border-radius: 10px; background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.detail-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
|
||||
.detail-name { font-weight: 700; font-size: 1rem; }
|
||||
.detail-lang-tag {
|
||||
padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.75rem;
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.1); color: var(--vp-c-brand);
|
||||
}
|
||||
.detail-desc { font-size: 0.9rem; color: var(--vp-c-text-2); margin-bottom: 0.75rem; line-height: 1.6; }
|
||||
.feature-title, .usecase-title { font-weight: 600; font-size: 0.85rem; margin-bottom: 0.4rem; }
|
||||
.feature-list { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 0.75rem; }
|
||||
.feature-tag {
|
||||
padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.75rem;
|
||||
background: var(--vp-c-bg-soft); border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.usecase-text { font-size: 0.85rem; color: var(--vp-c-text-2); }
|
||||
</style>
|
||||
@@ -0,0 +1,197 @@
|
||||
<!--
|
||||
AsyncTaskFlowDemo.vue
|
||||
异步任务流程演示:展示同步 vs 异步处理的对比
|
||||
-->
|
||||
<template>
|
||||
<div class="async-task-demo">
|
||||
<div class="header">
|
||||
<div class="title">同步 vs 异步处理对比</div>
|
||||
<div class="subtitle">点击按钮观察两种模式的差异</div>
|
||||
</div>
|
||||
|
||||
<div class="mode-tabs">
|
||||
<button
|
||||
:class="['tab', { active: mode === 'sync' }]"
|
||||
@click="mode = 'sync'; reset()"
|
||||
>同步模式</button>
|
||||
<button
|
||||
:class="['tab', { active: mode === 'async' }]"
|
||||
@click="mode = 'async'; reset()"
|
||||
>异步模式</button>
|
||||
</div>
|
||||
|
||||
<div class="flow-area">
|
||||
<div class="user-side">
|
||||
<div class="label">用户请求</div>
|
||||
<button class="action-btn" @click="startProcess" :disabled="running">
|
||||
{{ running ? '处理中...' : '提交订单' }}
|
||||
</button>
|
||||
<div :class="['response-box', { success: responseReady }]">
|
||||
<template v-if="!running && !responseReady">等待提交</template>
|
||||
<template v-else-if="running && mode === 'sync'">
|
||||
⏳ 用户等待中... ({{ elapsed }}s)
|
||||
</template>
|
||||
<template v-else-if="running && mode === 'async' && responseReady">
|
||||
✅ 已返回 ({{ asyncResponseTime }}ms)
|
||||
</template>
|
||||
<template v-else-if="running && mode === 'async'">
|
||||
⏳ 等待响应...
|
||||
</template>
|
||||
<template v-else>
|
||||
✅ 完成 ({{ mode === 'sync' ? syncTime + 'ms' : asyncResponseTime + 'ms' }})
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow">→</div>
|
||||
|
||||
<div class="server-side">
|
||||
<div class="label">服务端处理</div>
|
||||
<div class="tasks">
|
||||
<div
|
||||
v-for="(task, i) in tasks"
|
||||
:key="i"
|
||||
:class="['task-item', task.status]"
|
||||
>
|
||||
<span class="task-icon">{{ task.status === 'done' ? '✅' : task.status === 'running' ? '⏳' : '⬜' }}</span>
|
||||
<span>{{ task.name }}</span>
|
||||
<span class="task-time">{{ task.time }}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary" v-if="!running && responseReady">
|
||||
<template v-if="mode === 'sync'">
|
||||
<div class="summary-bad">同步模式:用户等待了 {{ syncTime }}ms,所有任务串行完成后才返回响应</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="summary-good">异步模式:用户仅等待 {{ asyncResponseTime }}ms,耗时任务在后台异步处理</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const mode = ref('sync')
|
||||
const running = ref(false)
|
||||
const responseReady = ref(false)
|
||||
const elapsed = ref(0)
|
||||
const syncTime = ref(0)
|
||||
const asyncResponseTime = ref(200)
|
||||
|
||||
const tasks = ref([
|
||||
{ name: '扣减库存', time: 50, status: 'pending' },
|
||||
{ name: '创建订单', time: 100, status: 'pending' },
|
||||
{ name: '发送确认邮件', time: 800, status: 'pending' },
|
||||
{ name: '更新推荐系统', time: 600, status: 'pending' },
|
||||
{ name: '记录审计日志', time: 300, status: 'pending' }
|
||||
])
|
||||
|
||||
let timer = null
|
||||
|
||||
function reset() {
|
||||
running.value = false
|
||||
responseReady.value = false
|
||||
elapsed.value = 0
|
||||
syncTime.value = 0
|
||||
tasks.value.forEach(t => t.status = 'pending')
|
||||
if (timer) clearInterval(timer)
|
||||
}
|
||||
|
||||
async function sleep(ms) {
|
||||
return new Promise(r => setTimeout(r, Math.min(ms, 1500)))
|
||||
}
|
||||
|
||||
async function startProcess() {
|
||||
reset()
|
||||
running.value = true
|
||||
|
||||
if (mode.value === 'sync') {
|
||||
timer = setInterval(() => { elapsed.value = (elapsed.value + 0.1).toFixed(1) }, 100)
|
||||
let total = 0
|
||||
for (const task of tasks.value) {
|
||||
task.status = 'running'
|
||||
await sleep(task.time)
|
||||
task.status = 'done'
|
||||
total += task.time
|
||||
}
|
||||
syncTime.value = total
|
||||
responseReady.value = true
|
||||
running.value = false
|
||||
clearInterval(timer)
|
||||
} else {
|
||||
// 异步模式:只等核心任务
|
||||
tasks.value[0].status = 'running'
|
||||
await sleep(tasks.value[0].time)
|
||||
tasks.value[0].status = 'done'
|
||||
|
||||
tasks.value[1].status = 'running'
|
||||
await sleep(tasks.value[1].time)
|
||||
tasks.value[1].status = 'done'
|
||||
|
||||
responseReady.value = true
|
||||
|
||||
// 后台任务继续
|
||||
for (let i = 2; i < tasks.value.length; i++) {
|
||||
tasks.value[i].status = 'running'
|
||||
await sleep(tasks.value[i].time)
|
||||
tasks.value[i].status = 'done'
|
||||
}
|
||||
running.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.async-task-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
.header { margin-bottom: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.mode-tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
|
||||
.tab {
|
||||
padding: 0.5rem 1rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg); cursor: pointer; font-size: 0.9rem;
|
||||
}
|
||||
.tab.active { border-color: var(--vp-c-brand); color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); }
|
||||
.flow-area { display: flex; align-items: flex-start; gap: 1rem; margin-bottom: 1rem; }
|
||||
.arrow { font-size: 2rem; color: var(--vp-c-text-3); padding-top: 2rem; }
|
||||
.user-side, .server-side { flex: 1; }
|
||||
.label { font-weight: 600; margin-bottom: 0.5rem; font-size: 0.9rem; }
|
||||
.action-btn {
|
||||
padding: 0.5rem 1.5rem; border-radius: 6px; border: none;
|
||||
background: var(--vp-c-brand); color: #fff; cursor: pointer; font-size: 0.9rem;
|
||||
margin-bottom: 0.75rem; width: 100%;
|
||||
}
|
||||
.action-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.response-box {
|
||||
padding: 0.75rem; border-radius: 8px; background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider); font-size: 0.85rem; text-align: center;
|
||||
}
|
||||
.response-box.success { border-color: #22c55e; background: rgba(34,197,94,0.05); }
|
||||
.tasks { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.task-item {
|
||||
display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem;
|
||||
border-radius: 6px; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.task-item.running { border-color: #f59e0b; background: rgba(245,158,11,0.05); }
|
||||
.task-item.done { border-color: #22c55e; background: rgba(34,197,94,0.05); }
|
||||
.task-icon { font-size: 0.9rem; }
|
||||
.task-time { margin-left: auto; color: var(--vp-c-text-3); font-family: var(--vp-font-family-mono); font-size: 0.8rem; }
|
||||
.summary { margin-top: 0.75rem; padding: 0.75rem; border-radius: 8px; font-size: 0.9rem; }
|
||||
.summary-bad { background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.3); border-radius: 8px; padding: 0.75rem; }
|
||||
.summary-good { background: rgba(34,197,94,0.08); border: 1px solid rgba(34,197,94,0.3); border-radius: 8px; padding: 0.75rem; }
|
||||
@media (max-width: 640px) {
|
||||
.flow-area { flex-direction: column; }
|
||||
.arrow { transform: rotate(90deg); align-self: center; padding: 0; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,168 @@
|
||||
<!--
|
||||
TaskRetryDemo.vue
|
||||
任务重试机制演示:展示失败重试和退避策略
|
||||
-->
|
||||
<template>
|
||||
<div class="retry-demo">
|
||||
<div class="header">
|
||||
<div class="title">任务重试与退避策略</div>
|
||||
<div class="subtitle">模拟任务失败后的重试过程</div>
|
||||
</div>
|
||||
|
||||
<div class="strategy-tabs">
|
||||
<button
|
||||
v-for="s in strategies"
|
||||
:key="s.key"
|
||||
:class="['tab', { active: strategy === s.key }]"
|
||||
@click="strategy = s.key; reset()"
|
||||
>{{ s.label }}</button>
|
||||
</div>
|
||||
|
||||
<div class="retry-area">
|
||||
<button class="start-btn" @click="startRetry" :disabled="running">
|
||||
{{ running ? '重试中...' : '执行任务(模拟失败)' }}
|
||||
</button>
|
||||
|
||||
<div class="attempts">
|
||||
<div
|
||||
v-for="(attempt, i) in attempts"
|
||||
:key="i"
|
||||
:class="['attempt', attempt.status]"
|
||||
>
|
||||
<div class="attempt-header">
|
||||
<span class="attempt-num">第 {{ i + 1 }} 次{{ i === 0 ? '执行' : '重试' }}</span>
|
||||
<span :class="['status-badge', attempt.status]">
|
||||
{{ attempt.status === 'success' ? '成功' : attempt.status === 'fail' ? '失败' : attempt.status === 'waiting' ? '等待中' : '执行中' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="attempt-detail">
|
||||
<span v-if="attempt.delay > 0">等待 {{ attempt.delay }}s 后重试</span>
|
||||
<span v-if="attempt.error" class="error-msg">{{ attempt.error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="strategy-info">
|
||||
<div class="info-title">{{ currentStrategy.label }}</div>
|
||||
<div class="info-desc">{{ currentStrategy.desc }}</div>
|
||||
<div class="info-formula">
|
||||
延迟公式:<code>{{ currentStrategy.formula }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const strategy = ref('fixed')
|
||||
const running = ref(false)
|
||||
const attempts = ref([])
|
||||
|
||||
const strategies = [
|
||||
{ key: 'fixed', label: '固定间隔', desc: '每次重试等待相同的时间,简单但可能造成"重试风暴"', formula: 'delay = 2s' },
|
||||
{ key: 'exponential', label: '指数退避', desc: '每次重试等待时间翻倍,有效避免服务端过载', formula: 'delay = 2^n 秒 (1s, 2s, 4s, 8s...)' },
|
||||
{ key: 'jitter', label: '指数退避+抖动', desc: '在指数退避基础上加随机偏移,防止多个客户端同时重试', formula: 'delay = 2^n + random(0, 1s)' }
|
||||
]
|
||||
|
||||
const currentStrategy = computed(() => strategies.find(s => s.key === strategy.value))
|
||||
|
||||
function reset() {
|
||||
running.value = false
|
||||
attempts.value = []
|
||||
}
|
||||
|
||||
function getDelay(n) {
|
||||
if (strategy.value === 'fixed') return 2
|
||||
if (strategy.value === 'exponential') return Math.pow(2, n)
|
||||
return Math.pow(2, n) + Math.random().toFixed(1) * 1
|
||||
}
|
||||
|
||||
async function sleep(ms) {
|
||||
return new Promise(r => setTimeout(r, ms))
|
||||
}
|
||||
|
||||
async function startRetry() {
|
||||
reset()
|
||||
running.value = true
|
||||
const maxRetries = 4
|
||||
const failUntil = 2 + Math.floor(Math.random() * 2) // succeed on 3rd or 4th attempt
|
||||
|
||||
for (let i = 0; i <= maxRetries; i++) {
|
||||
const delay = i === 0 ? 0 : getDelay(i - 1)
|
||||
const attempt = { status: 'waiting', delay, error: '' }
|
||||
attempts.value.push(attempt)
|
||||
|
||||
if (delay > 0) {
|
||||
await sleep(Math.min(delay * 500, 2000))
|
||||
}
|
||||
|
||||
attempt.status = 'running'
|
||||
await sleep(500)
|
||||
|
||||
if (i < failUntil) {
|
||||
attempt.status = 'fail'
|
||||
attempt.error = ['连接超时', '服务不可用', '网络错误'][i % 3]
|
||||
} else {
|
||||
attempt.status = 'success'
|
||||
running.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
running.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.retry-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
.header { margin-bottom: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.strategy-tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
|
||||
.tab {
|
||||
padding: 0.4rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg); cursor: pointer; font-size: 0.85rem;
|
||||
}
|
||||
.tab.active { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
|
||||
.start-btn {
|
||||
padding: 0.5rem 1.5rem; border-radius: 6px; border: none;
|
||||
background: var(--vp-c-brand); color: #fff; cursor: pointer; font-size: 0.9rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.start-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.attempts { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.attempt {
|
||||
padding: 0.6rem 0.75rem; border-radius: 8px; background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.attempt.fail { border-color: rgba(239,68,68,0.4); }
|
||||
.attempt.success { border-color: #22c55e; background: rgba(34,197,94,0.05); }
|
||||
.attempt.running { border-color: var(--vp-c-brand); }
|
||||
.attempt-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.attempt-num { font-weight: 600; font-size: 0.85rem; }
|
||||
.status-badge { font-size: 0.75rem; padding: 0.15rem 0.5rem; border-radius: 4px; }
|
||||
.status-badge.fail { background: rgba(239,68,68,0.1); color: #ef4444; }
|
||||
.status-badge.success { background: rgba(34,197,94,0.1); color: #22c55e; }
|
||||
.status-badge.running { background: rgba(var(--vp-c-brand-rgb),0.1); color: var(--vp-c-brand); }
|
||||
.status-badge.waiting { background: var(--vp-c-bg-soft); color: var(--vp-c-text-3); }
|
||||
.attempt-detail { font-size: 0.8rem; color: var(--vp-c-text-2); margin-top: 0.25rem; }
|
||||
.error-msg { color: #ef4444; margin-left: 0.5rem; }
|
||||
.strategy-info {
|
||||
margin-top: 1rem; padding: 0.75rem; border-radius: 8px;
|
||||
background: rgba(var(--vp-c-brand-rgb),0.05); border: 1px solid var(--vp-c-brand);
|
||||
}
|
||||
.info-title { font-weight: 700; font-size: 0.9rem; margin-bottom: 0.25rem; }
|
||||
.info-desc { font-size: 0.85rem; color: var(--vp-c-text-2); margin-bottom: 0.5rem; }
|
||||
.info-formula { font-size: 0.85rem; }
|
||||
.info-formula code {
|
||||
padding: 0.15rem 0.4rem; background: var(--vp-c-bg); border-radius: 4px;
|
||||
font-family: var(--vp-font-family-mono); font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,190 @@
|
||||
<!--
|
||||
TaskWorkerDemo.vue
|
||||
Worker 工作池演示:展示任务分发和消费过程
|
||||
-->
|
||||
<template>
|
||||
<div class="worker-demo">
|
||||
<div class="header">
|
||||
<div class="title">Worker 工作池模型</div>
|
||||
<div class="subtitle">观察任务如何被分发到不同 Worker 处理</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button class="ctrl-btn" @click="addTask" :disabled="running">添加任务</button>
|
||||
<button class="ctrl-btn primary" @click="startProcessing" :disabled="running || queue.length === 0">开始处理</button>
|
||||
<button class="ctrl-btn" @click="resetAll">重置</button>
|
||||
<div class="worker-count">
|
||||
Worker 数量:
|
||||
<button class="small-btn" @click="workerCount = Math.max(1, workerCount - 1)" :disabled="running">-</button>
|
||||
<span>{{ workerCount }}</span>
|
||||
<button class="small-btn" @click="workerCount = Math.min(5, workerCount + 1)" :disabled="running">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pool-layout">
|
||||
<div class="queue-section">
|
||||
<div class="section-title">任务队列 ({{ queue.length }})</div>
|
||||
<div class="queue-list">
|
||||
<div v-for="task in queue" :key="task.id" class="queue-item">
|
||||
{{ task.name }}
|
||||
</div>
|
||||
<div v-if="queue.length === 0" class="empty">队列为空</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow-section">→</div>
|
||||
|
||||
<div class="workers-section">
|
||||
<div class="section-title">Workers</div>
|
||||
<div class="workers-grid">
|
||||
<div v-for="w in workers" :key="w.id" :class="['worker-card', w.status]">
|
||||
<div class="worker-name">Worker {{ w.id }}</div>
|
||||
<div class="worker-status">
|
||||
<template v-if="w.status === 'idle'">💤 空闲</template>
|
||||
<template v-else>⚙️ {{ w.currentTask }}</template>
|
||||
</div>
|
||||
<div class="worker-count-label">已完成: {{ w.completed }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow-section">→</div>
|
||||
|
||||
<div class="done-section">
|
||||
<div class="section-title">已完成 ({{ doneList.length }})</div>
|
||||
<div class="done-list">
|
||||
<div v-for="task in doneList" :key="task.id" class="done-item">
|
||||
✅ {{ task.name }}
|
||||
</div>
|
||||
<div v-if="doneList.length === 0" class="empty">暂无</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const workerCount = ref(3)
|
||||
const running = ref(false)
|
||||
let taskId = 0
|
||||
|
||||
const taskTypes = ['发送邮件', '生成报表', '图片压缩', '数据同步', '推送通知', '日志归档', 'PDF 导出', '缓存预热']
|
||||
|
||||
const queue = ref([])
|
||||
const doneList = ref([])
|
||||
|
||||
const workers = computed(() => {
|
||||
const arr = []
|
||||
for (let i = 1; i <= workerCount.value; i++) {
|
||||
arr.push(workerState.value[i] || { id: i, status: 'idle', currentTask: '', completed: 0 })
|
||||
}
|
||||
return arr
|
||||
})
|
||||
|
||||
const workerState = ref({})
|
||||
|
||||
function addTask() {
|
||||
const name = taskTypes[taskId % taskTypes.length]
|
||||
queue.value.push({ id: ++taskId, name: `${name} #${taskId}` })
|
||||
}
|
||||
|
||||
function resetAll() {
|
||||
running.value = false
|
||||
queue.value = []
|
||||
doneList.value = []
|
||||
workerState.value = {}
|
||||
taskId = 0
|
||||
}
|
||||
|
||||
async function sleep(ms) {
|
||||
return new Promise(r => setTimeout(r, ms))
|
||||
}
|
||||
|
||||
async function startProcessing() {
|
||||
running.value = true
|
||||
// Initialize worker states
|
||||
for (let i = 1; i <= workerCount.value; i++) {
|
||||
workerState.value[i] = { id: i, status: 'idle', currentTask: '', completed: 0 }
|
||||
}
|
||||
|
||||
const workerPromises = []
|
||||
for (let i = 1; i <= workerCount.value; i++) {
|
||||
workerPromises.push(runWorker(i))
|
||||
}
|
||||
await Promise.all(workerPromises)
|
||||
running.value = false
|
||||
}
|
||||
|
||||
async function runWorker(wid) {
|
||||
while (queue.value.length > 0) {
|
||||
const task = queue.value.shift()
|
||||
if (!task) break
|
||||
workerState.value = {
|
||||
...workerState.value,
|
||||
[wid]: { ...workerState.value[wid], status: 'busy', currentTask: task.name }
|
||||
}
|
||||
await sleep(600 + Math.random() * 800)
|
||||
doneList.value.push(task)
|
||||
workerState.value = {
|
||||
...workerState.value,
|
||||
[wid]: { ...workerState.value[wid], status: 'idle', currentTask: '', completed: workerState.value[wid].completed + 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.worker-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
.header { margin-bottom: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.controls { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; margin-bottom: 1rem; }
|
||||
.ctrl-btn {
|
||||
padding: 0.4rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg); cursor: pointer; font-size: 0.85rem;
|
||||
}
|
||||
.ctrl-btn.primary { background: var(--vp-c-brand); color: #fff; border-color: var(--vp-c-brand); }
|
||||
.ctrl-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.small-btn {
|
||||
width: 24px; height: 24px; border-radius: 4px; border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg); cursor: pointer; font-size: 0.85rem;
|
||||
}
|
||||
.small-btn:disabled { opacity: 0.5; }
|
||||
.worker-count { display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; margin-left: auto; }
|
||||
.pool-layout { display: flex; gap: 0.75rem; align-items: flex-start; }
|
||||
.arrow-section { font-size: 1.5rem; color: var(--vp-c-text-3); padding-top: 2rem; flex-shrink: 0; }
|
||||
.queue-section, .done-section { flex: 1; min-width: 0; }
|
||||
.workers-section { flex: 1.5; min-width: 0; }
|
||||
.section-title { font-weight: 600; font-size: 0.9rem; margin-bottom: 0.5rem; }
|
||||
.queue-list, .done-list { display: flex; flex-direction: column; gap: 0.25rem; max-height: 200px; overflow-y: auto; }
|
||||
.queue-item {
|
||||
padding: 0.4rem 0.6rem; background: rgba(245,158,11,0.1); border: 1px solid rgba(245,158,11,0.3);
|
||||
border-radius: 4px; font-size: 0.8rem;
|
||||
}
|
||||
.done-item {
|
||||
padding: 0.4rem 0.6rem; background: rgba(34,197,94,0.08); border: 1px solid rgba(34,197,94,0.2);
|
||||
border-radius: 4px; font-size: 0.8rem;
|
||||
}
|
||||
.empty { color: var(--vp-c-text-3); font-size: 0.8rem; padding: 0.5rem; }
|
||||
.workers-grid { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.worker-card {
|
||||
padding: 0.5rem 0.75rem; border-radius: 8px; background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.worker-card.busy { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); }
|
||||
.worker-name { font-weight: 600; font-size: 0.85rem; }
|
||||
.worker-status { font-size: 0.8rem; color: var(--vp-c-text-2); margin: 0.25rem 0; }
|
||||
.worker-count-label { font-size: 0.75rem; color: var(--vp-c-text-3); }
|
||||
@media (max-width: 640px) {
|
||||
.pool-layout { flex-direction: column; }
|
||||
.arrow-section { transform: rotate(90deg); align-self: center; padding: 0; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,337 @@
|
||||
<template>
|
||||
<div class="launch-demo">
|
||||
<div class="demo-header">
|
||||
<span class="demo-icon">🌐</span>
|
||||
<span class="demo-title">浏览器启动过程</span>
|
||||
<span class="demo-hint">点击每一步查看详情</span>
|
||||
</div>
|
||||
|
||||
<div class="timeline">
|
||||
<div
|
||||
v-for="(step, i) in steps"
|
||||
:key="i"
|
||||
class="timeline-item"
|
||||
:class="{ active: active === i, done: active > i }"
|
||||
@click="active = active === i ? -1 : i"
|
||||
>
|
||||
<div class="marker-col">
|
||||
<div class="dot">
|
||||
<span v-if="active > i" class="check">✓</span>
|
||||
<span v-else>{{ i + 1 }}</span>
|
||||
</div>
|
||||
<div v-if="i < steps.length - 1" class="line"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="step-icon">{{ step.icon }}</span>
|
||||
<div class="card-titles">
|
||||
<div class="step-name">{{ step.name }}</div>
|
||||
<div class="step-brief">{{ step.brief }}</div>
|
||||
</div>
|
||||
<span class="expand-icon">{{ active === i ? '▾' : '▸' }}</span>
|
||||
</div>
|
||||
|
||||
<transition name="slide">
|
||||
<div v-if="active === i" class="card-detail">
|
||||
<div class="detail-desc">{{ step.detail }}</div>
|
||||
<div class="detail-visual">
|
||||
<div
|
||||
v-for="(item, j) in step.items"
|
||||
:key="j"
|
||||
class="visual-item"
|
||||
>
|
||||
<span class="vi-icon">{{ item.icon }}</span>
|
||||
<div class="vi-text">
|
||||
<span class="vi-label">{{ item.label }}</span>
|
||||
<span class="vi-desc">{{ item.desc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="step.analogy" class="analogy">
|
||||
<span class="analogy-icon">💡</span>
|
||||
<span class="analogy-text">{{ step.analogy }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const active = ref(-1)
|
||||
|
||||
const steps = [
|
||||
{
|
||||
icon: '👆',
|
||||
name: '双击图标',
|
||||
brief: '用户触发启动请求,操作系统开始响应',
|
||||
detail: '你双击桌面上的浏览器图标时,操作系统的窗口管理器捕获这个鼠标事件,通过文件关联表查找该图标对应的可执行文件路径。',
|
||||
items: [
|
||||
{ icon: '🖱️', label: '鼠标事件捕获', desc: '窗口管理器检测到双击动作,识别点击目标' },
|
||||
{ icon: '🔗', label: '快捷方式解析', desc: '读取 .lnk(Windows)或 .desktop(Linux)文件中的目标路径' },
|
||||
{ icon: '📂', label: '文件关联查找', desc: '在注册表或 MIME 数据库中找到对应的可执行文件' }
|
||||
],
|
||||
analogy: '就像你按下遥控器的开机键,电视先要识别你按的是哪个按钮,再决定执行什么操作。'
|
||||
},
|
||||
{
|
||||
icon: '🔍',
|
||||
name: '查找可执行文件',
|
||||
brief: '根据文件关联,找到浏览器的 .exe 或可执行文件',
|
||||
detail: '操作系统根据路径在硬盘上定位浏览器的可执行文件(如 chrome.exe),验证文件完整性和权限,准备加载。',
|
||||
items: [
|
||||
{ icon: '📋', label: '路径解析', desc: '将快捷方式中的路径转换为硬盘上的实际文件位置' },
|
||||
{ icon: '🔒', label: '权限检查', desc: '验证当前用户是否有执行该文件的权限' },
|
||||
{ icon: '✅', label: '签名验证', desc: '检查数字签名确认文件未被篡改(Windows UAC)' }
|
||||
],
|
||||
analogy: '好比你要找一本书,先查图书馆目录(路径),确认你有借阅权限(权限检查),再确认书没有被损坏(签名验证)。'
|
||||
},
|
||||
{
|
||||
icon: '📋',
|
||||
name: '创建浏览器进程',
|
||||
brief: '为浏览器创建一个新的进程,分配进程 ID',
|
||||
detail: '操作系统内核调用 fork()+exec()(Linux)或 CreateProcess()(Windows),在进程表中创建新条目,分配唯一的 PID,建立进程控制块(PCB)。',
|
||||
items: [
|
||||
{ icon: '🆔', label: '分配 PID', desc: '为新进程分配唯一的进程标识符' },
|
||||
{ icon: '📊', label: '创建 PCB', desc: '记录进程状态、优先级、寄存器上下文等元信息' },
|
||||
{ icon: '🧠', label: '分配虚拟地址空间', desc: '为进程创建独立的 4GB(32位)虚拟内存空间' },
|
||||
{ icon: '📑', label: '初始化文件描述符', desc: '打开 stdin/stdout/stderr 三个标准 I/O 通道' }
|
||||
],
|
||||
analogy: '就像新生儿出生要办户口——分配身份证号(PID)、建立档案(PCB)、分配住房(内存空间)。'
|
||||
},
|
||||
{
|
||||
icon: '💾',
|
||||
name: '加载代码到内存',
|
||||
brief: '把浏览器的程序代码从硬盘读取到内存中',
|
||||
detail: '操作系统的加载器(Loader)解析可执行文件格式(PE/ELF),将代码段、数据段映射到虚拟内存,并加载所需的动态链接库(DLL/SO)。',
|
||||
items: [
|
||||
{ icon: '📦', label: '解析文件格式', desc: '读取 PE(Windows)或 ELF(Linux)文件头,确定各段位置' },
|
||||
{ icon: '🗺️', label: '内存映射', desc: '将 .text(代码)、.data(数据)、.bss 段映射到虚拟地址' },
|
||||
{ icon: '🔗', label: '动态链接', desc: '加载 DLL/SO 共享库,解析函数符号引用' },
|
||||
{ icon: '📍', label: '重定位', desc: '修正代码中的绝对地址引用,适配实际加载位置' }
|
||||
],
|
||||
analogy: '好比搬家——把家具(代码)从仓库(硬盘)搬到新房(内存),还要接通水电(链接库)。'
|
||||
},
|
||||
{
|
||||
icon: '🚀',
|
||||
name: '初始化各模块',
|
||||
brief: '启动主线程、渲染引擎、网络引擎、JS 引擎等',
|
||||
detail: '浏览器的 main() 函数开始执行,依次初始化多进程架构中的各个核心模块:Browser 主进程、GPU 进程、网络进程等。',
|
||||
items: [
|
||||
{ icon: '🧵', label: '主线程启动', desc: '初始化消息循环(Event Loop),处理 UI 事件和任务调度' },
|
||||
{ icon: '🎨', label: '渲染引擎', desc: '初始化 Blink/Gecko 引擎,准备解析 HTML/CSS' },
|
||||
{ icon: '🌐', label: '网络模块', desc: '启动网络栈,初始化 DNS 缓存、连接池、Cookie 管理' },
|
||||
{ icon: '⚡', label: 'JS 引擎', desc: '初始化 V8/SpiderMonkey,编译内置 JavaScript 代码' }
|
||||
],
|
||||
analogy: '就像一家餐厅开业前——厨房(渲染)、前台(UI)、外卖(网络)、收银(JS)各部门同时准备就绪。'
|
||||
},
|
||||
{
|
||||
icon: '🖼️',
|
||||
name: '显示浏览器窗口',
|
||||
brief: '所有模块就绪,浏览器界面呈现在屏幕上',
|
||||
detail: '浏览器向操作系统请求创建窗口,GPU 进程完成界面的合成与光栅化,最终将像素数据提交给显卡,浏览器窗口出现在屏幕上。',
|
||||
items: [
|
||||
{ icon: '🪟', label: '创建窗口', desc: '调用系统 API 创建原生窗口,设置大小和位置' },
|
||||
{ icon: '🎨', label: 'UI 绘制', desc: '渲染地址栏、标签页、工具栏等浏览器 Chrome 界面' },
|
||||
{ icon: '🖥️', label: 'GPU 合成', desc: '将各图层合成为最终画面,提交给显卡输出' },
|
||||
{ icon: '✨', label: '加载首页', desc: '打开新标签页或恢复上次会话,浏览器进入可用状态' }
|
||||
],
|
||||
analogy: '幕布拉开,灯光亮起——舞台(窗口)搭好了,演员(界面元素)就位,等待观众(你)的第一次操作。'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.launch-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1.2rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.demo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.demo-icon { font-size: 1.2rem; }
|
||||
.demo-title {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.demo-hint {
|
||||
font-size: 0.65rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.timeline { display: flex; flex-direction: column; }
|
||||
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.marker-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dot {
|
||||
width: 1.8rem;
|
||||
height: 1.8rem;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-text-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.timeline-item.active .dot {
|
||||
background: var(--vp-c-brand);
|
||||
border-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
transform: scale(1.15);
|
||||
box-shadow: 0 0 0 4px var(--vp-c-brand-soft);
|
||||
}
|
||||
.timeline-item.done .dot {
|
||||
background: #10b981;
|
||||
border-color: #10b981;
|
||||
color: white;
|
||||
}
|
||||
.check { font-size: 0.65rem; }
|
||||
.line {
|
||||
flex: 1;
|
||||
width: 2px;
|
||||
background: var(--vp-c-divider);
|
||||
min-height: 0.8rem;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.timeline-item.done .line { background: #10b981; opacity: 0.5; }
|
||||
.timeline-item.active .line { background: var(--vp-c-brand); opacity: 0.4; }
|
||||
|
||||
.card {
|
||||
flex: 1;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 0.7rem 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
transition: all 0.25s;
|
||||
}
|
||||
.timeline-item.active .card {
|
||||
border-color: var(--vp-c-brand);
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.step-icon { font-size: 1.2rem; }
|
||||
.card-titles { flex: 1; }
|
||||
.step-name {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.step-brief {
|
||||
font-size: 0.68rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-top: 0.1rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.expand-icon {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-3);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.card-detail {
|
||||
margin-top: 0.7rem;
|
||||
padding-top: 0.7rem;
|
||||
border-top: 1px dashed var(--vp-c-divider);
|
||||
}
|
||||
.detail-desc {
|
||||
font-size: 0.72rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.detail-visual {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.visual-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.4rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
padding: 0.45rem 0.55rem;
|
||||
}
|
||||
.vi-icon { font-size: 0.9rem; flex-shrink: 0; margin-top: 0.05rem; }
|
||||
.vi-text { display: flex; flex-direction: column; }
|
||||
.vi-label {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.vi-desc {
|
||||
font-size: 0.62rem;
|
||||
color: var(--vp-c-text-3);
|
||||
line-height: 1.4;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
.analogy {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.4rem;
|
||||
margin-top: 0.6rem;
|
||||
padding: 0.5rem 0.6rem;
|
||||
background: var(--vp-c-brand-soft);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.analogy-icon { font-size: 0.85rem; flex-shrink: 0; }
|
||||
.analogy-text {
|
||||
font-size: 0.66rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.slide-enter-active, .slide-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
.slide-enter-from, .slide-leave-to {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
.slide-enter-to, .slide-leave-from {
|
||||
opacity: 1;
|
||||
max-height: 30rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.detail-visual { grid-template-columns: 1fr; }
|
||||
.demo-hint { display: none; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,376 @@
|
||||
<template>
|
||||
<div class="bios-demo">
|
||||
<div class="demo-header">
|
||||
<span class="demo-icon">📟</span>
|
||||
<span class="demo-title">BIOS/UEFI 工作流程</span>
|
||||
<span class="demo-hint">点击每一步查看详情</span>
|
||||
</div>
|
||||
|
||||
<div class="timeline">
|
||||
<div
|
||||
v-for="(step, i) in steps"
|
||||
:key="i"
|
||||
class="timeline-item"
|
||||
:class="{ active: active === i, done: active > i }"
|
||||
@click="active = active === i ? -1 : i"
|
||||
>
|
||||
<div class="marker-col">
|
||||
<div class="dot">
|
||||
<span v-if="active > i" class="check">✓</span>
|
||||
<span v-else>{{ i + 1 }}</span>
|
||||
</div>
|
||||
<div v-if="i < steps.length - 1" class="line"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="step-icon">{{ step.icon }}</span>
|
||||
<div class="card-titles">
|
||||
<div class="step-name">{{ step.name }}</div>
|
||||
<div class="step-brief">{{ step.brief }}</div>
|
||||
</div>
|
||||
<span class="expand-icon">{{ active === i ? '▾' : '▸' }}</span>
|
||||
</div>
|
||||
|
||||
<transition name="slide">
|
||||
<div v-if="active === i" class="card-detail">
|
||||
<div class="detail-desc">{{ step.detail }}</div>
|
||||
<div class="detail-visual">
|
||||
<div
|
||||
v-for="(item, j) in step.items"
|
||||
:key="j"
|
||||
class="visual-item"
|
||||
:class="{ 'error-item': item.error }"
|
||||
>
|
||||
<span class="vi-icon">{{ item.icon }}</span>
|
||||
<div class="vi-text">
|
||||
<span class="vi-label">{{ item.label }}</span>
|
||||
<span class="vi-desc">{{ item.desc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="step.analogy" class="analogy">
|
||||
<span class="analogy-icon">💡</span>
|
||||
<span class="analogy-text">{{ step.analogy }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="beep-note">
|
||||
<span class="beep-icon">🔔</span>
|
||||
<div class="beep-content">
|
||||
<div class="beep-title">蜂鸣声错误码</div>
|
||||
<div class="beep-desc">如果 POST 发现问题,主板会发出蜂鸣声。不同次数代表不同错误:</div>
|
||||
<div class="beep-codes">
|
||||
<div v-for="code in beepCodes" :key="code.beeps" class="beep-code">
|
||||
<span class="beep-count">{{ code.beeps }}</span>
|
||||
<span class="beep-meaning">{{ code.meaning }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const active = ref(-1)
|
||||
|
||||
const steps = [
|
||||
{
|
||||
icon: '🔍',
|
||||
name: '硬件自检(POST)',
|
||||
brief: '检查内存、显卡、键盘等部件是否正常',
|
||||
detail: 'Power-On Self-Test 是开机后执行的第一段程序。BIOS/UEFI 固件逐一检测关键硬件,确保它们能正常工作,任何故障都会在这一步被发现。',
|
||||
items: [
|
||||
{ icon: '🧠', label: '内存检测', desc: '向内存写入测试数据并读回验证,确认每个内存条工作正常' },
|
||||
{ icon: '🎮', label: '显卡检测', desc: '初始化显卡,尝试输出画面;如果失败,屏幕会保持黑屏' },
|
||||
{ icon: '⌨️', label: '键盘/鼠标检测', desc: '扫描 PS/2 或 USB 端口,检测输入设备是否连接并响应' },
|
||||
{ icon: '💾', label: '存储设备检测', desc: '识别硬盘、SSD、光驱等存储设备,读取设备信息' },
|
||||
{ icon: '❌', label: '错误报告', desc: '检测失败时通过蜂鸣声或屏幕错误码告知用户具体问题', error: true }
|
||||
],
|
||||
analogy: '就像飞机起飞前的安全检查——机长逐项确认引擎、仪表、燃油都正常,有任何问题就不能起飞。'
|
||||
},
|
||||
{
|
||||
icon: '⚙️',
|
||||
name: '初始化硬件',
|
||||
brief: '设置硬件工作模式,配置中断向量表',
|
||||
detail: '自检通过后,BIOS/UEFI 开始配置各硬件的工作参数:设置 CPU 频率、内存时序、配置中断控制器,建立硬件与软件之间的通信桥梁。',
|
||||
items: [
|
||||
{ icon: '🔧', label: '设置工作模式', desc: '配置 CPU 运行频率、内存时序(CAS Latency)等参数' },
|
||||
{ icon: '📋', label: '中断向量表', desc: '建立中断号与处理程序的映射表,让硬件事件能被正确响应' },
|
||||
{ icon: '🔌', label: 'PCI 设备枚举', desc: '扫描 PCI/PCIe 总线,为显卡、网卡、声卡分配资源' },
|
||||
{ icon: '🕐', label: '时钟初始化', desc: '读取 CMOS 中的实时时钟(RTC),同步系统时间' }
|
||||
],
|
||||
analogy: '好比乐队演出前的调音——每件乐器(硬件)都要调到正确的音高(工作模式),指挥(中断控制器)要能指挥每个声部。'
|
||||
},
|
||||
{
|
||||
icon: '🔎',
|
||||
name: '寻找启动设备',
|
||||
brief: '按启动顺序查找可启动设备,读取启动扇区',
|
||||
detail: 'BIOS/UEFI 按照用户设定的启动顺序(Boot Order),依次检查硬盘、U 盘、网络等设备,找到第一个包含有效引导记录的设备,读取其启动扇区并将控制权交出。',
|
||||
items: [
|
||||
{ icon: '📑', label: '读取启动顺序', desc: '从 CMOS/NVRAM 中读取用户设定的设备优先级列表' },
|
||||
{ icon: '💿', label: '检查启动扇区', desc: '读取设备第一个扇区,验证末尾的 0x55AA 魔数签名' },
|
||||
{ icon: '🔀', label: '多设备尝试', desc: '第一个设备无法启动时,自动尝试下一个(硬盘→U盘→网络)' },
|
||||
{ icon: '🚀', label: '跳转执行', desc: '将启动扇区代码加载到内存 0x7C00,CPU 跳转到该地址执行' }
|
||||
],
|
||||
analogy: '就像你早上出门找交通工具——先看车库有没有车(硬盘),没有就看门口有没有共享单车(U盘),再不行就叫网约车(网络启动)。'
|
||||
}
|
||||
]
|
||||
|
||||
const beepCodes = [
|
||||
{ beeps: '1 短', meaning: '正常启动,一切 OK' },
|
||||
{ beeps: '1 长 2 短', meaning: '显卡错误或未插好' },
|
||||
{ beeps: '1 长 3 短', meaning: '内存错误或未插好' },
|
||||
{ beeps: '持续长鸣', meaning: '内存未检测到' },
|
||||
{ beeps: '持续短鸣', meaning: '电源供电异常' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bios-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1.2rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.demo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.demo-icon { font-size: 1.2rem; }
|
||||
.demo-title {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.demo-hint {
|
||||
font-size: 0.65rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.timeline { display: flex; flex-direction: column; }
|
||||
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.marker-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dot {
|
||||
width: 1.8rem;
|
||||
height: 1.8rem;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-text-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.timeline-item.active .dot {
|
||||
background: var(--vp-c-brand);
|
||||
border-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
transform: scale(1.15);
|
||||
box-shadow: 0 0 0 4px var(--vp-c-brand-soft);
|
||||
}
|
||||
.timeline-item.done .dot {
|
||||
background: #10b981;
|
||||
border-color: #10b981;
|
||||
color: white;
|
||||
}
|
||||
.check { font-size: 0.65rem; }
|
||||
.line {
|
||||
flex: 1;
|
||||
width: 2px;
|
||||
background: var(--vp-c-divider);
|
||||
min-height: 0.8rem;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.timeline-item.done .line { background: #10b981; opacity: 0.5; }
|
||||
.timeline-item.active .line { background: var(--vp-c-brand); opacity: 0.4; }
|
||||
|
||||
.card {
|
||||
flex: 1;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 0.7rem 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
transition: all 0.25s;
|
||||
}
|
||||
.timeline-item.active .card {
|
||||
border-color: var(--vp-c-brand);
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.step-icon { font-size: 1.2rem; }
|
||||
.card-titles { flex: 1; }
|
||||
.step-name {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.step-brief {
|
||||
font-size: 0.68rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-top: 0.1rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.expand-icon {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.card-detail {
|
||||
margin-top: 0.7rem;
|
||||
padding-top: 0.7rem;
|
||||
border-top: 1px dashed var(--vp-c-divider);
|
||||
}
|
||||
.detail-desc {
|
||||
font-size: 0.72rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.detail-visual {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.visual-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.4rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
padding: 0.45rem 0.55rem;
|
||||
}
|
||||
.visual-item.error-item {
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
border: 1px solid rgba(239, 68, 68, 0.15);
|
||||
}
|
||||
.vi-icon { font-size: 0.9rem; flex-shrink: 0; margin-top: 0.05rem; }
|
||||
.vi-text { display: flex; flex-direction: column; }
|
||||
.vi-label {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.vi-desc {
|
||||
font-size: 0.62rem;
|
||||
color: var(--vp-c-text-3);
|
||||
line-height: 1.4;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
.analogy {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.4rem;
|
||||
margin-top: 0.6rem;
|
||||
padding: 0.5rem 0.6rem;
|
||||
background: var(--vp-c-brand-soft);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.analogy-icon { font-size: 0.85rem; flex-shrink: 0; }
|
||||
.analogy-text {
|
||||
font-size: 0.66rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.beep-note {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
margin-top: 0.8rem;
|
||||
padding: 0.7rem 0.8rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #f59e0b;
|
||||
}
|
||||
.beep-icon { font-size: 1.1rem; flex-shrink: 0; }
|
||||
.beep-content { flex: 1; }
|
||||
.beep-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
.beep-desc {
|
||||
font-size: 0.66rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.beep-codes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.beep-code {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.beep-count {
|
||||
font-size: 0.62rem;
|
||||
font-weight: 700;
|
||||
color: #f59e0b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.beep-meaning {
|
||||
font-size: 0.62rem;
|
||||
color: var(--vp-c-text-2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.slide-enter-active, .slide-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
.slide-enter-from, .slide-leave-to {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
.slide-enter-to, .slide-leave-from {
|
||||
opacity: 1;
|
||||
max-height: 30rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.detail-visual { grid-template-columns: 1fr; }
|
||||
.demo-hint { display: none; }
|
||||
.beep-codes { flex-direction: column; }
|
||||
}
|
||||
</style>
|
||||
+599
@@ -0,0 +1,599 @@
|
||||
<template>
|
||||
<div class="bios-demo">
|
||||
<div class="demo-header">
|
||||
<span class="demo-title">BIOS/UEFI 工作流程</span>
|
||||
</div>
|
||||
|
||||
<div class="main-layout">
|
||||
<!-- 左侧:模拟屏幕 -->
|
||||
<div class="screen-panel">
|
||||
<div class="monitor">
|
||||
<div class="monitor-bezel">
|
||||
<div class="screen" :class="'stage-' + stage">
|
||||
<!-- Stage 0: 介绍 -->
|
||||
<div v-if="stage === 0" class="screen-intro">
|
||||
<div class="intro-icon">📟</div>
|
||||
<div class="intro-title">BIOS/UEFI</div>
|
||||
<div class="intro-desc">点击开始了解<br>固件启动流程</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage 1: POST 自检 -->
|
||||
<div v-if="stage === 1" class="screen-post">
|
||||
<div class="post-header">POST - Power On Self Test</div>
|
||||
<div class="post-list">
|
||||
<div v-for="(item, i) in postItems" :key="i" class="post-item" :class="{ checking: currentCheck === i, done: currentCheck > i }">
|
||||
<span class="post-icon">{{ currentCheck > i ? '✓' : (currentCheck === i ? '◐' : '○') }}</span>
|
||||
<span class="post-name">{{ item.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentCheck >= postItems.length" class="post-result">
|
||||
<span class="result-ok">✓ 所有硬件检测通过</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage 2: 初始化硬件 -->
|
||||
<div v-if="stage === 2" class="screen-init">
|
||||
<div class="init-header">初始化硬件配置</div>
|
||||
<div class="init-visual">
|
||||
<div class="hardware-grid">
|
||||
<div v-for="(hw, i) in hardwareItems" :key="i" class="hw-item" :class="{ active: activeHw === i }">
|
||||
<span class="hw-icon">{{ hw.icon }}</span>
|
||||
<span class="hw-name">{{ hw.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="init-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: hwProgress + '%' }"></div>
|
||||
</div>
|
||||
<div class="progress-text">{{ hwProgress }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage 3: 寻找启动设备 -->
|
||||
<div v-if="stage === 3" class="screen-boot">
|
||||
<div class="boot-header">寻找启动设备</div>
|
||||
<div class="boot-order">
|
||||
<div class="order-label">启动顺序:</div>
|
||||
<div class="device-list">
|
||||
<div v-for="(dev, i) in bootDevices" :key="i" class="device-item" :class="{ checking: currentDevice === i, found: foundDevice === i, skipped: foundDevice > i || (foundDevice === -1 && currentDevice > i) }">
|
||||
<span class="device-num">{{ i + 1 }}</span>
|
||||
<span class="device-icon">{{ dev.icon }}</span>
|
||||
<span class="device-name">{{ dev.name }}</span>
|
||||
<span class="device-status">{{ getDeviceStatus(i) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="foundDevice >= 0" class="boot-result">
|
||||
<span class="boot-ok">🚀 从 {{ bootDevices[foundDevice].name }} 启动</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度指示 -->
|
||||
<div class="stage-dots">
|
||||
<div
|
||||
v-for="(s, i) in stages"
|
||||
:key="i"
|
||||
class="stage-dot"
|
||||
:class="{ active: stage === i, done: stage > i }"
|
||||
>
|
||||
<span class="dot-label">{{ s.short }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制按钮 -->
|
||||
<div class="controls">
|
||||
<button class="ctrl-btn" :disabled="stage <= 0" @click="prev">← 上一步</button>
|
||||
<button class="ctrl-btn primary" v-if="stage === 0" @click="next">开始 →</button>
|
||||
<button class="ctrl-btn primary" v-else-if="stage < 3" @click="next">下一步 →</button>
|
||||
<button class="ctrl-btn" v-else @click="reset">↺ 重新开始</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:详细信息 -->
|
||||
<div class="info-panel">
|
||||
<div class="info-stage-header">
|
||||
<span class="info-stage-icon">{{ currentStage.icon }}</span>
|
||||
<div>
|
||||
<div class="info-stage-name">{{ currentStage.name }}</div>
|
||||
<div class="info-stage-desc">{{ currentStage.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-operations">
|
||||
<div
|
||||
v-for="(op, i) in currentStage.operations"
|
||||
:key="i"
|
||||
class="op-card"
|
||||
:class="{ expanded: expandedOp === i }"
|
||||
@click="expandedOp = expandedOp === i ? -1 : i"
|
||||
>
|
||||
<div class="op-header">
|
||||
<span class="op-num">{{ i + 1 }}</span>
|
||||
<span class="op-icon">{{ op.icon }}</span>
|
||||
<span class="op-name">{{ op.name }}</span>
|
||||
<span class="op-toggle">{{ expandedOp === i ? '▾' : '▸' }}</span>
|
||||
</div>
|
||||
<transition name="expand">
|
||||
<div v-if="expandedOp === i" class="op-detail">
|
||||
<div class="op-what">{{ op.what }}</div>
|
||||
<div v-if="op.details" class="op-details">
|
||||
<div v-for="(d, j) in op.details" :key="j" class="op-detail-item">
|
||||
<span class="od-dot">•</span>
|
||||
<span>{{ d }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentStage.analogy" class="info-analogy">
|
||||
<span class="analogy-icon">💡</span>
|
||||
<span>{{ currentStage.analogy }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 蜂鸣声错误码 -->
|
||||
<div v-if="stage === 1" class="beep-codes">
|
||||
<div class="beep-header">
|
||||
<span class="beep-icon">🔔</span>
|
||||
<span class="beep-title">蜂鸣声错误码</span>
|
||||
</div>
|
||||
<div class="beep-list">
|
||||
<div v-for="code in beepCodes" :key="code.beeps" class="beep-item">
|
||||
<span class="beep-count">{{ code.beeps }}</span>
|
||||
<span class="beep-meaning">{{ code.meaning }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
|
||||
const stage = ref(0)
|
||||
const expandedOp = ref(-1)
|
||||
const currentCheck = ref(0)
|
||||
const activeHw = ref(0)
|
||||
const hwProgress = ref(0)
|
||||
const currentDevice = ref(0)
|
||||
const foundDevice = ref(-1)
|
||||
|
||||
const postItems = [
|
||||
{ name: '内存检测', icon: '🧠' },
|
||||
{ name: '显卡检测', icon: '🎮' },
|
||||
{ name: '键盘/鼠标', icon: '⌨️' },
|
||||
{ name: '存储设备', icon: '💾' }
|
||||
]
|
||||
|
||||
const hardwareItems = [
|
||||
{ name: 'CPU', icon: '🧠' },
|
||||
{ name: '内存', icon: '💾' },
|
||||
{ name: '显卡', icon: '🎮' },
|
||||
{ name: '网卡', icon: '🌐' },
|
||||
{ name: '声卡', icon: '🔊' },
|
||||
{ name: 'USB', icon: '🔌' }
|
||||
]
|
||||
|
||||
const bootDevices = [
|
||||
{ name: '硬盘', icon: '💿' },
|
||||
{ name: 'U盘', icon: '🔌' },
|
||||
{ name: '网络', icon: '🌐' }
|
||||
]
|
||||
|
||||
const beepCodes = [
|
||||
{ beeps: '1 短', meaning: '正常启动' },
|
||||
{ beeps: '1 长 2 短', meaning: '显卡错误' },
|
||||
{ beeps: '1 长 3 短', meaning: '内存错误' },
|
||||
{ beeps: '持续长鸣', meaning: '内存未检测' },
|
||||
{ beeps: '持续短鸣', meaning: '电源异常' }
|
||||
]
|
||||
|
||||
const stages = [
|
||||
{
|
||||
short: '介绍',
|
||||
icon: '📟',
|
||||
name: '什么是 BIOS/UEFI?',
|
||||
desc: 'BIOS 是电脑启动后第一个运行的程序,存储在主板的只读芯片中。UEFI 是 BIOS 的升级版,更安全、更现代。',
|
||||
operations: [
|
||||
{
|
||||
icon: '💾', name: 'BIOS(传统)',
|
||||
what: 'Basic Input/Output System,1980年代开始使用的固件接口。',
|
||||
details: ['存储在主板 ROM 芯片中', '16位实模式运行', '最大支持 2.2TB 硬盘', '蓝色文本界面']
|
||||
},
|
||||
{
|
||||
icon: '✨', name: 'UEFI(现代)',
|
||||
what: 'Unified Extensible Firmware Interface,BIOS 的现代化替代品。',
|
||||
details: ['支持 32/64位模式', '支持超过 2.2TB 的大硬盘', '图形化设置界面', '安全启动(Secure Boot)']
|
||||
}
|
||||
],
|
||||
analogy: 'BIOS/UEFI 就像是电脑的"守门人"——它第一个醒来,检查一切是否正常,然后决定让谁(操作系统)进来。'
|
||||
},
|
||||
{
|
||||
short: 'POST',
|
||||
icon: '🔍',
|
||||
name: '硬件自检(POST)',
|
||||
desc: 'Power-On Self-Test,逐一检测关键硬件,确保它们能正常工作。',
|
||||
operations: [
|
||||
{
|
||||
icon: '🧠', name: '内存检测',
|
||||
what: '向内存写入测试数据并读回验证,确认每个内存条工作正常。',
|
||||
details: ['逐字节写入/读取测试', '检测内存容量和速度', '失败会发出蜂鸣声(1长3短)']
|
||||
},
|
||||
{
|
||||
icon: '🎮', name: '显卡检测',
|
||||
what: '初始化显卡,尝试输出画面。如果失败,屏幕会保持黑屏。',
|
||||
details: ['加载显卡 BIOS', '设置基本显示模式', '失败蜂鸣:1长2短']
|
||||
},
|
||||
{
|
||||
icon: '⌨️', name: '外设检测',
|
||||
what: '扫描 USB/PS2 端口,检测键盘、鼠标等输入设备。',
|
||||
details: ['枚举 USB 设备', '检测键盘响应', '非关键设备,缺失不影响启动']
|
||||
},
|
||||
{
|
||||
icon: '💾', name: '存储设备检测',
|
||||
what: '识别硬盘、SSD、光驱等存储设备,读取设备信息。',
|
||||
details: ['检测 SATA/NVMe 设备', '读取设备型号和容量', '为后续启动做准备']
|
||||
}
|
||||
],
|
||||
analogy: '就像飞机起飞前的安全检查——机长逐项确认引擎、仪表、燃油都正常,有任何问题就不能起飞。'
|
||||
},
|
||||
{
|
||||
short: '初始化',
|
||||
icon: '⚙️',
|
||||
name: '初始化硬件',
|
||||
desc: '自检通过后,配置各硬件的工作参数,建立硬件与软件之间的通信桥梁。',
|
||||
operations: [
|
||||
{
|
||||
icon: '🔧', name: '设置工作模式',
|
||||
what: '配置 CPU 运行频率、内存时序(CAS Latency)等参数。',
|
||||
details: ['读取 CMOS 中的用户设置', '应用超频配置(如果有)', '设置电源管理模式']
|
||||
},
|
||||
{
|
||||
icon: '📋', name: '中断向量表',
|
||||
what: '建立中断号与处理程序的映射表,让硬件事件能被正确响应。',
|
||||
details: ['配置中断控制器(PIC/APIC)', '分配 IRQ 中断号', '设置中断处理程序入口']
|
||||
},
|
||||
{
|
||||
icon: '🔌', name: 'PCI 设备枚举',
|
||||
what: '扫描 PCI/PCIe 总线,为显卡、网卡、声卡分配资源。',
|
||||
details: ['发现所有 PCI 设备', '分配内存映射 I/O 地址', '分配中断资源']
|
||||
},
|
||||
{
|
||||
icon: '🕐', name: '时钟初始化',
|
||||
what: '读取 CMOS 中的实时时钟(RTC),同步系统时间。',
|
||||
details: ['读取硬件时钟', '校验时间有效性', '为操作系统提供初始时间']
|
||||
}
|
||||
],
|
||||
analogy: '好比乐队演出前的调音——每件乐器(硬件)都要调到正确的音高(工作模式),指挥(中断控制器)要能指挥每个声部。'
|
||||
},
|
||||
{
|
||||
short: '启动',
|
||||
icon: '🔎',
|
||||
name: '寻找启动设备',
|
||||
desc: '按照启动顺序查找可启动设备,读取启动扇区,把控制权交给操作系统。',
|
||||
operations: [
|
||||
{
|
||||
icon: '📑', name: '读取启动顺序',
|
||||
what: '从 CMOS/NVRAM 中读取用户设定的设备优先级列表。',
|
||||
details: ['硬盘 → U盘 → 网络(默认顺序)', '用户可在 BIOS 设置中修改', '保存到非易失性存储器']
|
||||
},
|
||||
{
|
||||
icon: '💿', name: '检查启动扇区',
|
||||
what: '读取设备第一个扇区,验证末尾的 0x55AA 魔数签名。',
|
||||
details: ['读取第 0 扇区(512字节)', '检查 510-511 字节是否为 0x55AA', '验证引导代码有效性']
|
||||
},
|
||||
{
|
||||
icon: '🔀', name: '多设备尝试',
|
||||
what: '第一个设备无法启动时,自动尝试下一个。',
|
||||
details: ['硬盘无系统 → 尝试 U盘', 'U盘不存在 → 尝试网络启动', '全部失败 → 显示错误信息']
|
||||
},
|
||||
{
|
||||
icon: '🚀', name: '跳转执行',
|
||||
what: '将启动扇区代码加载到内存 0x7C00,CPU 跳转到该地址执行。',
|
||||
details: ['加载 512 字节引导代码', '跳转到 0x7C00 执行', '控制权交给引导程序']
|
||||
}
|
||||
],
|
||||
analogy: '就像你早上出门找交通工具——先看车库有没有车(硬盘),没有就看门口有没有共享单车(U盘),再不行就叫网约车(网络启动)。'
|
||||
}
|
||||
]
|
||||
|
||||
const currentStage = computed(() => stages[stage.value])
|
||||
|
||||
function getDeviceStatus(i) {
|
||||
if (foundDevice.value === i) return '✓ 可启动'
|
||||
if (foundDevice.value > i || (foundDevice.value === -1 && currentDevice.value > i)) return '✗ 跳过'
|
||||
if (currentDevice.value === i) return '检查中...'
|
||||
return '等待'
|
||||
}
|
||||
|
||||
// POST 自检动画
|
||||
watch(() => stage.value, (newStage) => {
|
||||
if (newStage === 1) {
|
||||
currentCheck.value = 0
|
||||
const interval = setInterval(() => {
|
||||
if (currentCheck.value < postItems.length) {
|
||||
currentCheck.value++
|
||||
} else {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 600)
|
||||
}
|
||||
})
|
||||
|
||||
// 硬件初始化动画
|
||||
watch(() => stage.value, (newStage) => {
|
||||
if (newStage === 2) {
|
||||
activeHw.value = 0
|
||||
hwProgress.value = 0
|
||||
const interval = setInterval(() => {
|
||||
if (hwProgress.value < 100) {
|
||||
hwProgress.value += 5
|
||||
activeHw.value = Math.floor(hwProgress.value / 20) % hardwareItems.length
|
||||
} else {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
|
||||
// 启动设备搜索动画
|
||||
watch(() => stage.value, (newStage) => {
|
||||
if (newStage === 3) {
|
||||
currentDevice.value = 0
|
||||
foundDevice.value = -1
|
||||
let device = 0
|
||||
const interval = setInterval(() => {
|
||||
if (device < bootDevices.length) {
|
||||
currentDevice.value = device
|
||||
// 假设第一个设备(硬盘)可启动
|
||||
if (device === 0) {
|
||||
setTimeout(() => {
|
||||
foundDevice.value = device
|
||||
}, 400)
|
||||
clearInterval(interval)
|
||||
}
|
||||
device++
|
||||
} else {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 800)
|
||||
}
|
||||
})
|
||||
|
||||
function next() {
|
||||
if (stage.value < 3) {
|
||||
stage.value++
|
||||
expandedOp.value = -1
|
||||
}
|
||||
}
|
||||
function prev() {
|
||||
if (stage.value > 0) {
|
||||
stage.value--
|
||||
expandedOp.value = -1
|
||||
}
|
||||
}
|
||||
function reset() {
|
||||
stage.value = 0
|
||||
expandedOp.value = -1
|
||||
currentCheck.value = 0
|
||||
activeHw.value = 0
|
||||
hwProgress.value = 0
|
||||
currentDevice.value = 0
|
||||
foundDevice.value = -1
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bios-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1.2rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.demo-header { margin-bottom: 1rem; }
|
||||
.demo-title { font-size: 0.9rem; font-weight: 700; color: var(--vp-c-text-1); }
|
||||
|
||||
/* 主布局 */
|
||||
.main-layout { display: flex; gap: 1rem; }
|
||||
|
||||
/* ===== 左侧屏幕 ===== */
|
||||
.screen-panel { flex: 0 0 280px; display: flex; flex-direction: column; gap: 0.6rem; }
|
||||
.monitor { background: #222; border-radius: 10px; padding: 3px; }
|
||||
.monitor-bezel { background: #111; border-radius: 8px; overflow: hidden; }
|
||||
.screen {
|
||||
width: 100%; aspect-ratio: 4/3; display: flex;
|
||||
align-items: center; justify-content: center;
|
||||
font-family: 'Courier New', monospace; transition: background 0.5s;
|
||||
overflow: hidden; position: relative;
|
||||
}
|
||||
|
||||
/* 介绍 */
|
||||
.stage-0 { background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); }
|
||||
.screen-intro { text-align: center; color: #fff; }
|
||||
.intro-icon { font-size: 2.5rem; margin-bottom: 0.3rem; }
|
||||
.intro-title { font-size: 0.9rem; font-weight: 700; margin-bottom: 0.3rem; }
|
||||
.intro-desc { font-size: 0.6rem; color: #94a3b8; line-height: 1.5; }
|
||||
|
||||
/* POST */
|
||||
.stage-1 { background: #000; flex-direction: column; padding: 0.6rem; align-items: flex-start; }
|
||||
.screen-post { width: 100%; }
|
||||
.post-header { color: #4ade80; font-size: 0.55rem; margin-bottom: 0.5rem; font-weight: 700; }
|
||||
.post-list { display: flex; flex-direction: column; gap: 0.3rem; }
|
||||
.post-item {
|
||||
display: flex; align-items: center; gap: 0.4rem;
|
||||
color: #64748b; font-size: 0.6rem;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.post-item.checking { color: #fbbf24; }
|
||||
.post-item.done { color: #4ade80; }
|
||||
.post-icon { width: 1rem; text-align: center; }
|
||||
.post-result { margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid #333; }
|
||||
.result-ok { color: #4ade80; font-size: 0.6rem; }
|
||||
|
||||
/* 初始化 */
|
||||
.stage-2 { background: #0f172a; flex-direction: column; padding: 0.6rem; }
|
||||
.screen-init { width: 100%; }
|
||||
.init-header { color: #60a5fa; font-size: 0.55rem; margin-bottom: 0.5rem; font-weight: 700; }
|
||||
.hardware-grid {
|
||||
display: grid; grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.4rem; margin-bottom: 0.6rem;
|
||||
}
|
||||
.hw-item {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
padding: 0.4rem; background: rgba(255,255,255,0.05);
|
||||
border-radius: 6px; transition: all 0.3s;
|
||||
}
|
||||
.hw-item.active { background: rgba(96, 165, 250, 0.3); transform: scale(1.05); }
|
||||
.hw-icon { font-size: 1.2rem; margin-bottom: 0.1rem; }
|
||||
.hw-name { font-size: 0.5rem; color: #94a3b8; }
|
||||
.init-progress { display: flex; align-items: center; gap: 0.4rem; }
|
||||
.progress-bar {
|
||||
flex: 1; height: 4px; background: #333; border-radius: 2px; overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%; background: linear-gradient(90deg, #60a5fa, #3b82f6);
|
||||
transition: width 0.1s linear;
|
||||
}
|
||||
.progress-text { color: #60a5fa; font-size: 0.55rem; width: 2rem; text-align: right; }
|
||||
|
||||
/* 启动 */
|
||||
.stage-3 { background: #1e1b4b; flex-direction: column; padding: 0.6rem; align-items: flex-start; }
|
||||
.screen-boot { width: 100%; }
|
||||
.boot-header { color: #a78bfa; font-size: 0.55rem; margin-bottom: 0.4rem; font-weight: 700; }
|
||||
.order-label { color: #94a3b8; font-size: 0.5rem; margin-bottom: 0.3rem; }
|
||||
.device-list { display: flex; flex-direction: column; gap: 0.25rem; }
|
||||
.device-item {
|
||||
display: flex; align-items: center; gap: 0.3rem;
|
||||
padding: 0.3rem 0.4rem; background: rgba(255,255,255,0.05);
|
||||
border-radius: 4px; font-size: 0.55rem; color: #64748b;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.device-item.checking { color: #fbbf24; background: rgba(251, 191, 36, 0.1); }
|
||||
.device-item.found { color: #4ade80; background: rgba(74, 222, 128, 0.1); }
|
||||
.device-item.skipped { opacity: 0.5; }
|
||||
.device-num {
|
||||
width: 1rem; height: 1rem; border-radius: 50%;
|
||||
background: rgba(255,255,255,0.1); display: flex;
|
||||
align-items: center; justify-content: center; font-size: 0.5rem;
|
||||
}
|
||||
.device-icon { font-size: 0.8rem; }
|
||||
.device-name { flex: 1; }
|
||||
.device-status { font-size: 0.5rem; }
|
||||
.boot-result { margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid rgba(167, 139, 250, 0.3); }
|
||||
.boot-ok { color: #4ade80; font-size: 0.6rem; }
|
||||
|
||||
/* 进度点 */
|
||||
.stage-dots { display: flex; justify-content: center; gap: 0.3rem; }
|
||||
.stage-dot {
|
||||
padding: 0.15rem 0.4rem; border-radius: 10px;
|
||||
font-size: 0.55rem; color: var(--vp-c-text-3);
|
||||
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.stage-dot.active {
|
||||
background: var(--vp-c-brand); color: white; border-color: var(--vp-c-brand);
|
||||
}
|
||||
.stage-dot.done { background: #10b981; color: white; border-color: #10b981; }
|
||||
.dot-label { white-space: nowrap; }
|
||||
|
||||
/* 控制按钮 */
|
||||
.controls { display: flex; gap: 0.4rem; justify-content: center; }
|
||||
.ctrl-btn {
|
||||
padding: 0.35rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg); color: var(--vp-c-text-2); font-size: 0.68rem;
|
||||
cursor: pointer; transition: all 0.2s;
|
||||
}
|
||||
.ctrl-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
|
||||
.ctrl-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.ctrl-btn.primary {
|
||||
background: var(--vp-c-brand); color: white; border-color: var(--vp-c-brand);
|
||||
}
|
||||
.ctrl-btn.primary:hover { opacity: 0.9; }
|
||||
|
||||
/* ===== 右侧信息 ===== */
|
||||
.info-panel { flex: 1; min-width: 0; }
|
||||
.info-stage-header { display: flex; align-items: flex-start; gap: 0.5rem; margin-bottom: 0.7rem; }
|
||||
.info-stage-icon { font-size: 1.4rem; }
|
||||
.info-stage-name { font-size: 0.82rem; font-weight: 700; color: var(--vp-c-text-1); }
|
||||
.info-stage-desc { font-size: 0.68rem; color: var(--vp-c-text-3); margin-top: 0.1rem; line-height: 1.4; }
|
||||
|
||||
/* 操作卡片 */
|
||||
.info-operations { display: flex; flex-direction: column; gap: 0.35rem; }
|
||||
.op-card {
|
||||
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px; padding: 0.5rem 0.6rem; cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.op-card.expanded { border-color: var(--vp-c-brand); box-shadow: 0 1px 8px rgba(0,0,0,0.05); }
|
||||
.op-header { display: flex; align-items: center; gap: 0.4rem; }
|
||||
.op-num {
|
||||
width: 1.2rem; height: 1.2rem; border-radius: 50%;
|
||||
background: var(--vp-c-brand-soft); color: var(--vp-c-brand);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 0.58rem; font-weight: 700; flex-shrink: 0;
|
||||
}
|
||||
.op-icon { font-size: 0.9rem; }
|
||||
.op-name { flex: 1; font-size: 0.72rem; font-weight: 600; color: var(--vp-c-text-1); }
|
||||
.op-toggle { font-size: 0.65rem; color: var(--vp-c-text-3); }
|
||||
|
||||
.op-detail { margin-top: 0.4rem; padding-top: 0.4rem; border-top: 1px dashed var(--vp-c-divider); }
|
||||
.op-what { font-size: 0.66rem; color: var(--vp-c-text-2); line-height: 1.6; margin-bottom: 0.3rem; }
|
||||
.op-details { display: flex; flex-direction: column; gap: 0.15rem; }
|
||||
.op-detail-item {
|
||||
display: flex; align-items: flex-start; gap: 0.3rem;
|
||||
font-size: 0.62rem; color: var(--vp-c-text-3); line-height: 1.4;
|
||||
}
|
||||
.od-dot { color: var(--vp-c-brand); flex-shrink: 0; }
|
||||
|
||||
/* 类比 */
|
||||
.info-analogy {
|
||||
display: flex; align-items: flex-start; gap: 0.4rem;
|
||||
margin-top: 0.6rem; padding: 0.5rem 0.6rem;
|
||||
background: var(--vp-c-brand-soft); border-radius: 6px;
|
||||
font-size: 0.64rem; color: var(--vp-c-text-2);
|
||||
line-height: 1.5; font-style: italic;
|
||||
}
|
||||
.analogy-icon { font-size: 0.85rem; flex-shrink: 0; }
|
||||
|
||||
/* 蜂鸣声错误码 */
|
||||
.beep-codes {
|
||||
margin-top: 0.6rem; padding: 0.5rem 0.6rem;
|
||||
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.beep-header {
|
||||
display: flex; align-items: center; gap: 0.3rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.beep-icon { font-size: 0.9rem; }
|
||||
.beep-title { font-size: 0.7rem; font-weight: 600; color: var(--vp-c-text-1); }
|
||||
.beep-list { display: flex; flex-direction: column; gap: 0.2rem; }
|
||||
.beep-item {
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
font-size: 0.62rem;
|
||||
}
|
||||
.beep-count {
|
||||
padding: 0.1rem 0.3rem; background: var(--vp-c-brand-soft);
|
||||
border-radius: 4px; color: var(--vp-c-brand); font-weight: 600;
|
||||
min-width: 3rem; text-align: center;
|
||||
}
|
||||
.beep-meaning { color: var(--vp-c-text-2); }
|
||||
|
||||
/* 展开动画 */
|
||||
.expand-enter-active, .expand-leave-active { transition: all 0.25s ease; overflow: hidden; }
|
||||
.expand-enter-from, .expand-leave-to { opacity: 0; max-height: 0; }
|
||||
.expand-enter-to, .expand-leave-from { opacity: 1; max-height: 20rem; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.main-layout { flex-direction: column; }
|
||||
.screen-panel { flex: none; width: 100%; }
|
||||
}
|
||||
</style>
|
||||
+471
-56
@@ -1,15 +1,129 @@
|
||||
<template>
|
||||
<div class="boot-demo">
|
||||
<div class="demo-title">操作系统启动流程</div>
|
||||
<div class="timeline">
|
||||
<div v-for="(step, i) in steps" :key="step.name" class="timeline-item">
|
||||
<div class="marker">
|
||||
<span class="dot">{{ i + 1 }}</span>
|
||||
<span v-if="i < steps.length - 1" class="line"></span>
|
||||
<div class="demo-header">
|
||||
<span class="demo-title">从开机到桌面</span>
|
||||
</div>
|
||||
|
||||
<div class="main-layout">
|
||||
<!-- 左侧:模拟屏幕 -->
|
||||
<div class="screen-panel">
|
||||
<div class="monitor">
|
||||
<div class="monitor-bezel">
|
||||
<div class="screen" :class="'stage-' + stage">
|
||||
<!-- Stage 0: 关机 -->
|
||||
<div v-if="stage === 0" class="screen-off">
|
||||
<div class="power-icon">⏻</div>
|
||||
<div class="off-text">按下电源键开始</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage 1: BIOS 自检 -->
|
||||
<div v-if="stage === 1" class="screen-bios">
|
||||
<div class="bios-line" v-for="(line, i) in biosLines" :key="i">{{ line }}</div>
|
||||
<div class="bios-cursor">_</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage 2: 内核加载 -->
|
||||
<div v-if="stage === 2" class="screen-kernel">
|
||||
<div class="kernel-logo">🐧</div>
|
||||
<div class="kernel-text">Loading kernel...</div>
|
||||
<div class="kernel-bar-wrap">
|
||||
<div class="kernel-bar"></div>
|
||||
</div>
|
||||
<div class="kernel-modules">
|
||||
<div v-for="m in kernelModules" :key="m">[ OK ] {{ m }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage 3: 服务启动 -->
|
||||
<div v-if="stage === 3" class="screen-services">
|
||||
<div class="svc-header">Starting system services...</div>
|
||||
<div class="svc-list">
|
||||
<div v-for="s in services" :key="s.name" class="svc-item">
|
||||
<span class="svc-status" :class="s.ok ? 'ok' : ''">{{ s.ok ? '●' : '○' }}</span>
|
||||
<span>{{ s.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage 4: 桌面 -->
|
||||
<div v-if="stage === 4" class="screen-desktop">
|
||||
<div class="desktop-icons">
|
||||
<div class="desktop-icon" v-for="ic in desktopIcons" :key="ic.label">
|
||||
<span class="icon-emoji">{{ ic.icon }}</span>
|
||||
<span class="icon-label">{{ ic.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="taskbar">
|
||||
<span class="taskbar-menu">☰</span>
|
||||
<span class="taskbar-time">09:57</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="step-name">{{ step.name }}</div>
|
||||
<div class="step-desc">{{ step.desc }}</div>
|
||||
|
||||
<!-- 进度指示 -->
|
||||
<div class="stage-dots">
|
||||
<div
|
||||
v-for="(s, i) in stages"
|
||||
:key="i"
|
||||
class="stage-dot"
|
||||
:class="{ active: stage === i, done: stage > i }"
|
||||
>
|
||||
<span class="dot-label">{{ s.short }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制按钮 -->
|
||||
<div class="controls">
|
||||
<button class="ctrl-btn" :disabled="stage <= 0" @click="prev">← 上一步</button>
|
||||
<button class="ctrl-btn primary" v-if="stage === 0" @click="next">⏻ 开机</button>
|
||||
<button class="ctrl-btn primary" v-else-if="stage < 4" @click="next">下一步 →</button>
|
||||
<button class="ctrl-btn" v-else @click="reset">↺ 重新开始</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:详细信息 -->
|
||||
<div class="info-panel">
|
||||
<div class="info-stage-header">
|
||||
<span class="info-stage-icon">{{ currentStage.icon }}</span>
|
||||
<div>
|
||||
<div class="info-stage-name">{{ currentStage.name }}</div>
|
||||
<div class="info-stage-desc">{{ currentStage.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-operations">
|
||||
<div
|
||||
v-for="(op, i) in currentStage.operations"
|
||||
:key="i"
|
||||
class="op-card"
|
||||
:class="{ expanded: expandedOp === i }"
|
||||
@click="expandedOp = expandedOp === i ? -1 : i"
|
||||
>
|
||||
<div class="op-header">
|
||||
<span class="op-num">{{ i + 1 }}</span>
|
||||
<span class="op-icon">{{ op.icon }}</span>
|
||||
<span class="op-name">{{ op.name }}</span>
|
||||
<span class="op-toggle">{{ expandedOp === i ? '▾' : '▸' }}</span>
|
||||
</div>
|
||||
<transition name="expand">
|
||||
<div v-if="expandedOp === i" class="op-detail">
|
||||
<div class="op-what">{{ op.what }}</div>
|
||||
<div v-if="op.details" class="op-details">
|
||||
<div v-for="(d, j) in op.details" :key="j" class="op-detail-item">
|
||||
<span class="od-dot">•</span>
|
||||
<span>{{ d }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentStage.analogy" class="info-analogy">
|
||||
<span class="analogy-icon">💡</span>
|
||||
<span>{{ currentStage.analogy }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -17,66 +131,367 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const steps = [
|
||||
{ name: '引导程序 Bootloader', desc: '从硬盘启动扇区读取引导代码,找到操作系统内核的位置' },
|
||||
{ name: '内核加载 Kernel', desc: '将内核载入内存,接管 CPU、内存、设备的控制权' },
|
||||
{ name: '系统服务启动', desc: '启动网络、安全、音频等后台服务(Windows 服务 / Linux systemd)' },
|
||||
{ name: '桌面环境显示', desc: '加载显卡驱动 → 启动显示服务 → 渲染桌面背景和图标' }
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const stage = ref(0)
|
||||
const expandedOp = ref(-1)
|
||||
|
||||
const biosLines = [
|
||||
'American Megatrends BIOS v2.20',
|
||||
'CPU: Intel Core i7 @ 3.60GHz ... OK',
|
||||
'Memory: 16384 MB ... OK',
|
||||
'GPU: NVIDIA GeForce RTX ... OK',
|
||||
'Keyboard ... OK',
|
||||
'Detecting drives ...',
|
||||
'SATA0: Samsung SSD 512GB',
|
||||
'Boot from Hard Disk ...'
|
||||
]
|
||||
|
||||
const kernelModules = [
|
||||
'Started Memory Manager',
|
||||
'Started Process Scheduler',
|
||||
'Loaded disk driver',
|
||||
'Mounted root filesystem'
|
||||
]
|
||||
|
||||
const services = [
|
||||
{ name: 'Network Manager', ok: true },
|
||||
{ name: 'Firewall (iptables)', ok: true },
|
||||
{ name: 'Audio Service', ok: true },
|
||||
{ name: 'SSH Server', ok: true },
|
||||
{ name: 'Display Manager', ok: true },
|
||||
{ name: 'System Logger', ok: true }
|
||||
]
|
||||
|
||||
const desktopIcons = [
|
||||
{ icon: '📁', label: '文件' },
|
||||
{ icon: '🌐', label: '浏览器' },
|
||||
{ icon: '⚙️', label: '设置' },
|
||||
{ icon: '🗑️', label: '回收站' }
|
||||
]
|
||||
|
||||
const stages = [
|
||||
{
|
||||
short: '关机',
|
||||
icon: '⏻',
|
||||
name: '准备就绪',
|
||||
desc: '电脑处于关机状态,按下电源键即可开始启动流程',
|
||||
operations: [
|
||||
{
|
||||
icon: '🔌', name: '电源供电',
|
||||
what: '按下电源键后,电源(PSU)将交流电转换为直流电,为主板、CPU、内存等供电。',
|
||||
details: ['220V 交流电 → 12V/5V/3.3V 直流电', '主板收到 Power Good 信号后开始工作']
|
||||
},
|
||||
{
|
||||
icon: '⚡', name: 'CPU 复位',
|
||||
what: 'CPU 收到复位信号,清空所有寄存器,跳转到固定地址(0xFFFFFFF0)执行第一条指令。',
|
||||
details: ['所有寄存器归零', '指令指针指向 BIOS/UEFI 固件入口']
|
||||
}
|
||||
],
|
||||
analogy: '就像你按下汽车的启动按钮——电池通电,发动机准备点火。'
|
||||
},
|
||||
{
|
||||
short: 'BIOS 自检',
|
||||
icon: '📟',
|
||||
name: 'BIOS/UEFI 自检',
|
||||
desc: '固件程序逐一检测硬件,确保一切正常后寻找启动设备',
|
||||
operations: [
|
||||
{
|
||||
icon: '🧠', name: '内存检测(POST)',
|
||||
what: '向内存写入测试数据并读回验证,确认每根内存条都能正常工作。',
|
||||
details: ['逐字节写入/读取测试', '检测内存容量和速度', '失败会发出蜂鸣声(1长3短 = 内存错误)']
|
||||
},
|
||||
{
|
||||
icon: '🎮', name: '显卡检测',
|
||||
what: '初始化显卡,尝试输出画面。如果显卡故障,屏幕会保持黑屏。',
|
||||
details: ['加载显卡 BIOS', '设置基本显示模式', '失败蜂鸣:1长2短']
|
||||
},
|
||||
{
|
||||
icon: '⌨️', name: '外设检测',
|
||||
what: '扫描 USB/PS2 端口,检测键盘、鼠标等输入设备。',
|
||||
details: ['枚举 USB 设备', '检测键盘响应', '非关键设备,缺失不影响启动']
|
||||
},
|
||||
{
|
||||
icon: '💾', name: '寻找启动设备',
|
||||
what: '按照启动顺序(Boot Order)依次检查硬盘、U盘、网络,找到可启动设备。',
|
||||
details: ['读取 CMOS 中的启动顺序设置', '检查设备第一扇区的 0x55AA 签名', '找到后将引导代码加载到内存 0x7C00']
|
||||
}
|
||||
],
|
||||
analogy: '好比飞机起飞前的安全检查——机长逐项确认引擎、仪表、燃油,有问题就不能起飞。'
|
||||
},
|
||||
{
|
||||
short: '内核加载',
|
||||
icon: '⚙️',
|
||||
name: '操作系统内核加载',
|
||||
desc: '引导程序找到内核文件,将其加载到内存,内核接管整台计算机',
|
||||
operations: [
|
||||
{
|
||||
icon: '📀', name: '引导程序(Bootloader)',
|
||||
what: '硬盘第一扇区的引导程序(如 GRUB、bootmgr)读取分区表,找到内核文件位置。',
|
||||
details: ['Windows: bootmgr → 读取 BCD 配置', 'Linux: GRUB → 显示系统选择菜单', 'macOS: boot.efi → 直接加载 XNU 内核']
|
||||
},
|
||||
{
|
||||
icon: '📦', name: '内核解压与加载',
|
||||
what: '内核通常是压缩存储的,引导程序将其解压并复制到内存的指定位置。',
|
||||
details: ['解压 vmlinuz(Linux)或加载 ntoskrnl.exe(Windows)', '内核大小通常 5-15 MB']
|
||||
},
|
||||
{
|
||||
icon: '🧠', name: '初始化内存管理',
|
||||
what: '建立虚拟内存页表,划分内核空间和用户空间,让每个程序以为自己独占内存。',
|
||||
details: ['建立页表映射', '内核空间:高地址区域', '用户空间:低地址区域,程序运行在这里']
|
||||
},
|
||||
{
|
||||
icon: '📁', name: '挂载根文件系统',
|
||||
what: '将硬盘分区挂载为根目录(/),从此系统可以读写文件。',
|
||||
details: ['识别文件系统类型(NTFS/ext4/APFS)', '挂载为 /(Linux)或 C:\\(Windows)', '加载设备驱动程序']
|
||||
}
|
||||
],
|
||||
analogy: '内核就像公司的 CEO 上任——接管所有部门(硬件),安排人事(进程)、财务(内存)、后勤(设备)各就各位。'
|
||||
},
|
||||
{
|
||||
short: '服务启动',
|
||||
icon: '🔧',
|
||||
name: '系统服务启动',
|
||||
desc: '内核拉起第一个用户进程,按依赖顺序启动各种后台服务',
|
||||
operations: [
|
||||
{
|
||||
icon: '🚀', name: '初始化进程启动',
|
||||
what: '内核启动第一个用户态进程(PID=1),它是所有其他进程的"祖先"。',
|
||||
details: ['Linux: systemd 或 init', 'Windows: smss.exe → csrss.exe → wininit.exe', '负责按配置文件拉起后续服务']
|
||||
},
|
||||
{
|
||||
icon: '🌐', name: '网络服务',
|
||||
what: '初始化网卡驱动,通过 DHCP 获取 IP 地址,启动 DNS 解析。',
|
||||
details: ['加载网卡驱动', '发送 DHCP 请求获取 IP', '配置 DNS 服务器地址']
|
||||
},
|
||||
{
|
||||
icon: '🔒', name: '安全服务',
|
||||
what: '启动防火墙、用户认证系统,确保系统安全。',
|
||||
details: ['Linux: iptables/nftables 防火墙', 'Windows: Windows Defender、安全中心', '加载登录管理器,准备用户认证']
|
||||
},
|
||||
{
|
||||
icon: '🔊', name: '多媒体与其他服务',
|
||||
what: '启动音频服务、打印服务、日志服务等,让系统功能完整。',
|
||||
details: ['音频混合器(PulseAudio/PipeWire)', '系统日志(journald/Event Log)', '定时任务(cron/Task Scheduler)']
|
||||
}
|
||||
],
|
||||
analogy: '就像商场开门营业前——保安到岗(安全)、空调开启(后台服务)、收银上线(网络),一切就绪迎接顾客。'
|
||||
},
|
||||
{
|
||||
short: '桌面就绪',
|
||||
icon: '🖥️',
|
||||
name: '桌面环境显示',
|
||||
desc: '图形界面启动完成,你熟悉的桌面出现了',
|
||||
operations: [
|
||||
{
|
||||
icon: '🎮', name: '显卡驱动加载',
|
||||
what: '初始化 GPU,设置屏幕分辨率、刷新率和色彩深度。',
|
||||
details: ['加载 NVIDIA/AMD/Intel 驱动', '设置分辨率(如 1920×1080)', '启用硬件加速']
|
||||
},
|
||||
{
|
||||
icon: '🪟', name: '显示服务器启动',
|
||||
what: '窗口管理系统启动,负责管理所有窗口的绘制、层叠和交互。',
|
||||
details: ['Windows: Desktop Window Manager (DWM)', 'Linux: X Server 或 Wayland', 'macOS: WindowServer']
|
||||
},
|
||||
{
|
||||
icon: '🎨', name: '桌面环境渲染',
|
||||
what: '绘制壁纸、桌面图标、任务栏、系统托盘等界面元素。',
|
||||
details: ['Windows: explorer.exe 渲染桌面', 'Linux: GNOME/KDE/XFCE 桌面环境', 'macOS: Finder + Dock']
|
||||
},
|
||||
{
|
||||
icon: '👆', name: '等待用户操作',
|
||||
what: '鼠标光标出现,键盘就绪,系统进入完全可交互状态。',
|
||||
details: ['加载用户配置和偏好设置', '恢复上次会话(如果设置了)', '自启动程序开始运行']
|
||||
}
|
||||
],
|
||||
analogy: '幕布拉开,灯光亮起——舞台(窗口)搭好,演员(图标)就位,等待观众(你)的第一次操作。'
|
||||
}
|
||||
]
|
||||
|
||||
const currentStage = computed(() => stages[stage.value])
|
||||
|
||||
function next() {
|
||||
if (stage.value < 4) {
|
||||
stage.value++
|
||||
expandedOp.value = -1
|
||||
}
|
||||
}
|
||||
function prev() {
|
||||
if (stage.value > 0) {
|
||||
stage.value--
|
||||
expandedOp.value = -1
|
||||
}
|
||||
}
|
||||
function reset() {
|
||||
stage.value = 0
|
||||
expandedOp.value = -1
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.boot-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem 1.2rem;
|
||||
padding: 1.2rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.demo-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.8rem;
|
||||
.demo-header { margin-bottom: 1rem; }
|
||||
.demo-title { font-size: 0.9rem; font-weight: 700; color: var(--vp-c-text-1); }
|
||||
|
||||
/* 主布局 */
|
||||
.main-layout { display: flex; gap: 1rem; }
|
||||
|
||||
/* ===== 左侧屏幕 ===== */
|
||||
.screen-panel { flex: 0 0 280px; display: flex; flex-direction: column; gap: 0.6rem; }
|
||||
.monitor { background: #222; border-radius: 10px; padding: 3px; }
|
||||
.monitor-bezel { background: #111; border-radius: 8px; overflow: hidden; }
|
||||
.screen {
|
||||
width: 100%; aspect-ratio: 4/3; display: flex;
|
||||
align-items: center; justify-content: center;
|
||||
font-family: 'Courier New', monospace; transition: background 0.5s;
|
||||
overflow: hidden; position: relative;
|
||||
}
|
||||
.timeline { display: flex; flex-direction: column; }
|
||||
.timeline-item { display: flex; gap: 0.7rem; }
|
||||
.marker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 1.6rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
/* 关机 */
|
||||
.stage-0 { background: #000; }
|
||||
.screen-off { text-align: center; color: #555; }
|
||||
.power-icon { font-size: 2.5rem; margin-bottom: 0.3rem; }
|
||||
.off-text { font-size: 0.6rem; }
|
||||
|
||||
/* BIOS */
|
||||
.stage-1 { background: #000; align-items: flex-start; justify-content: flex-start; padding: 0.5rem; flex-direction: column; }
|
||||
.screen-bios { width: 100%; }
|
||||
.bios-line { color: #aaa; font-size: 0.5rem; line-height: 1.5; }
|
||||
.bios-cursor { color: #fff; animation: blink 1s infinite; font-size: 0.55rem; }
|
||||
|
||||
/* 内核 */
|
||||
.stage-2 { background: #1a1a2e; flex-direction: column; padding: 0.6rem; }
|
||||
.screen-kernel { text-align: center; width: 100%; }
|
||||
.kernel-logo { font-size: 1.8rem; margin-bottom: 0.3rem; }
|
||||
.kernel-text { color: #ccc; font-size: 0.55rem; margin-bottom: 0.4rem; }
|
||||
.kernel-bar-wrap {
|
||||
width: 70%; height: 4px; background: #333; border-radius: 2px;
|
||||
margin: 0 auto 0.5rem; overflow: hidden;
|
||||
}
|
||||
.dot {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
.kernel-bar {
|
||||
width: 100%; height: 100%;
|
||||
background: linear-gradient(90deg, #4ade80, #22d3ee);
|
||||
animation: loading 2s ease-in-out infinite;
|
||||
}
|
||||
.line {
|
||||
flex: 1;
|
||||
width: 2px;
|
||||
background: var(--vp-c-brand);
|
||||
opacity: 0.3;
|
||||
min-height: 1.2rem;
|
||||
.kernel-modules { text-align: left; width: 100%; }
|
||||
.kernel-modules div { color: #4ade80; font-size: 0.45rem; line-height: 1.6; }
|
||||
|
||||
/* 服务 */
|
||||
.stage-3 { background: #0f172a; flex-direction: column; align-items: flex-start; padding: 0.6rem; }
|
||||
.screen-services { width: 100%; }
|
||||
.svc-header { color: #94a3b8; font-size: 0.55rem; margin-bottom: 0.4rem; }
|
||||
.svc-list { display: flex; flex-direction: column; gap: 0.15rem; }
|
||||
.svc-item { color: #cbd5e1; font-size: 0.48rem; display: flex; align-items: center; gap: 0.3rem; }
|
||||
.svc-status { font-size: 0.5rem; color: #475569; }
|
||||
.svc-status.ok { color: #4ade80; }
|
||||
|
||||
/* 桌面 */
|
||||
.stage-4 { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); flex-direction: column; justify-content: space-between; padding: 0; }
|
||||
.screen-desktop { flex: 1; display: flex; flex-direction: column; justify-content: space-between; width: 100%; }
|
||||
.desktop-icons {
|
||||
display: grid; grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.3rem; padding: 0.8rem 0.5rem; justify-items: center;
|
||||
}
|
||||
.content { flex: 1; padding-bottom: 0.8rem; }
|
||||
.step-name {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
.desktop-icon { display: flex; flex-direction: column; align-items: center; gap: 0.1rem; }
|
||||
.icon-emoji { font-size: 1.3rem; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3)); }
|
||||
.icon-label { font-size: 0.45rem; color: white; text-shadow: 0 1px 2px rgba(0,0,0,0.5); }
|
||||
.taskbar {
|
||||
background: rgba(0,0,0,0.6); backdrop-filter: blur(8px);
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.step-desc {
|
||||
font-size: 0.68rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-top: 0.15rem;
|
||||
line-height: 1.5;
|
||||
.taskbar-menu { color: white; font-size: 0.7rem; }
|
||||
.taskbar-time { color: white; font-size: 0.5rem; }
|
||||
|
||||
/* 进度点 */
|
||||
.stage-dots { display: flex; justify-content: center; gap: 0.3rem; }
|
||||
.stage-dot {
|
||||
padding: 0.15rem 0.4rem; border-radius: 10px;
|
||||
font-size: 0.55rem; color: var(--vp-c-text-3);
|
||||
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.stage-dot.active {
|
||||
background: var(--vp-c-brand); color: white; border-color: var(--vp-c-brand);
|
||||
}
|
||||
.stage-dot.done { background: #10b981; color: white; border-color: #10b981; }
|
||||
.dot-label { white-space: nowrap; }
|
||||
|
||||
/* 控制按钮 */
|
||||
.controls { display: flex; gap: 0.4rem; justify-content: center; }
|
||||
.ctrl-btn {
|
||||
padding: 0.35rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg); color: var(--vp-c-text-2); font-size: 0.68rem;
|
||||
cursor: pointer; transition: all 0.2s;
|
||||
}
|
||||
.ctrl-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
|
||||
.ctrl-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.ctrl-btn.primary {
|
||||
background: var(--vp-c-brand); color: white; border-color: var(--vp-c-brand);
|
||||
}
|
||||
.ctrl-btn.primary:hover { opacity: 0.9; }
|
||||
|
||||
/* ===== 右侧信息 ===== */
|
||||
.info-panel { flex: 1; min-width: 0; }
|
||||
.info-stage-header { display: flex; align-items: flex-start; gap: 0.5rem; margin-bottom: 0.7rem; }
|
||||
.info-stage-icon { font-size: 1.4rem; }
|
||||
.info-stage-name { font-size: 0.82rem; font-weight: 700; color: var(--vp-c-text-1); }
|
||||
.info-stage-desc { font-size: 0.68rem; color: var(--vp-c-text-3); margin-top: 0.1rem; line-height: 1.4; }
|
||||
|
||||
/* 操作卡片 */
|
||||
.info-operations { display: flex; flex-direction: column; gap: 0.35rem; }
|
||||
.op-card {
|
||||
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px; padding: 0.5rem 0.6rem; cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.op-card.expanded { border-color: var(--vp-c-brand); box-shadow: 0 1px 8px rgba(0,0,0,0.05); }
|
||||
.op-header { display: flex; align-items: center; gap: 0.4rem; }
|
||||
.op-num {
|
||||
width: 1.2rem; height: 1.2rem; border-radius: 50%;
|
||||
background: var(--vp-c-brand-soft); color: var(--vp-c-brand);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 0.58rem; font-weight: 700; flex-shrink: 0;
|
||||
}
|
||||
.op-icon { font-size: 0.9rem; }
|
||||
.op-name { flex: 1; font-size: 0.72rem; font-weight: 600; color: var(--vp-c-text-1); }
|
||||
.op-toggle { font-size: 0.65rem; color: var(--vp-c-text-3); }
|
||||
|
||||
.op-detail { margin-top: 0.4rem; padding-top: 0.4rem; border-top: 1px dashed var(--vp-c-divider); }
|
||||
.op-what { font-size: 0.66rem; color: var(--vp-c-text-2); line-height: 1.6; margin-bottom: 0.3rem; }
|
||||
.op-details { display: flex; flex-direction: column; gap: 0.15rem; }
|
||||
.op-detail-item {
|
||||
display: flex; align-items: flex-start; gap: 0.3rem;
|
||||
font-size: 0.62rem; color: var(--vp-c-text-3); line-height: 1.4;
|
||||
}
|
||||
.od-dot { color: var(--vp-c-brand); flex-shrink: 0; }
|
||||
|
||||
/* 类比 */
|
||||
.info-analogy {
|
||||
display: flex; align-items: flex-start; gap: 0.4rem;
|
||||
margin-top: 0.6rem; padding: 0.5rem 0.6rem;
|
||||
background: var(--vp-c-brand-soft); border-radius: 6px;
|
||||
font-size: 0.64rem; color: var(--vp-c-text-2);
|
||||
line-height: 1.5; font-style: italic;
|
||||
}
|
||||
.analogy-icon { font-size: 0.85rem; flex-shrink: 0; }
|
||||
|
||||
/* 展开动画 */
|
||||
.expand-enter-active, .expand-leave-active { transition: all 0.25s ease; overflow: hidden; }
|
||||
.expand-enter-from, .expand-leave-to { opacity: 0; max-height: 0; }
|
||||
.expand-enter-to, .expand-leave-from { opacity: 1; max-height: 20rem; }
|
||||
|
||||
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
|
||||
@keyframes loading { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.main-layout { flex-direction: column; }
|
||||
.screen-panel { flex: none; width: 100%; }
|
||||
}
|
||||
</style>
|
||||
|
||||
+763
@@ -0,0 +1,763 @@
|
||||
<template>
|
||||
<div class="os-boot-demo">
|
||||
<div class="demo-header">
|
||||
<span class="demo-title">操作系统启动流程</span>
|
||||
</div>
|
||||
|
||||
<div class="main-layout">
|
||||
<!-- 左侧:模拟屏幕 -->
|
||||
<div class="screen-panel">
|
||||
<div class="monitor">
|
||||
<div class="monitor-bezel">
|
||||
<div class="screen" :class="'stage-' + stage">
|
||||
<!-- Stage 0: 操作系统介绍 -->
|
||||
<div v-if="stage === 0" class="screen-intro">
|
||||
<div class="intro-icon">🖥️</div>
|
||||
<div class="intro-title">操作系统</div>
|
||||
<div class="intro-desc">管理硬件和软件资源<br>计算机的"大管家"</div>
|
||||
<div class="os-icons">
|
||||
<div v-for="os in osList" :key="os.name" class="os-item">
|
||||
<span class="os-icon">{{ os.icon }}</span>
|
||||
<span class="os-name">{{ os.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage 1: 引导程序 -->
|
||||
<div v-if="stage === 1" class="screen-bootloader">
|
||||
<div class="bl-header">Bootloader</div>
|
||||
<div class="bl-flow">
|
||||
<div v-for="(step, i) in blSteps" :key="i" class="bl-step" :class="{ active: blStep >= i }">
|
||||
<span class="bl-num">{{ i + 1 }}</span>
|
||||
<span class="bl-text">{{ step }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bl-code">
|
||||
<div class="code-line" v-for="(line, i) in blCode" :key="i" :class="{ highlight: blCodeLine === i }">
|
||||
{{ line }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage 2: 内核加载 -->
|
||||
<div v-if="stage === 2" class="screen-kernel">
|
||||
<div class="kernel-header">Kernel Loading</div>
|
||||
<div class="kernel-logo">⚙️</div>
|
||||
<div class="kernel-name">{{ kernelName }}</div>
|
||||
<div class="kernel-bar-wrap">
|
||||
<div class="kernel-bar" :style="{ width: kernelProgress + '%' }"></div>
|
||||
</div>
|
||||
<div class="kernel-modules">
|
||||
<div v-for="(m, i) in kernelModules" :key="i" class="k-module" :class="{ loaded: kernelProgress > (i + 1) * 20 }">
|
||||
<span class="k-status">{{ kernelProgress > (i + 1) * 20 ? '✓' : '○' }}</span>
|
||||
<span class="k-name">{{ m }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage 3: 系统服务 -->
|
||||
<div v-if="stage === 3" class="screen-services">
|
||||
<div class="svc-header">System Services</div>
|
||||
<div class="svc-grid">
|
||||
<div v-for="(svc, i) in services" :key="i" class="svc-item" :class="{ started: svcProgress > i * 15 }">
|
||||
<span class="svc-icon">{{ svc.icon }}</span>
|
||||
<span class="svc-name">{{ svc.name }}</span>
|
||||
<span class="svc-status">{{ svcProgress > i * 15 ? '●' : '○' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="svc-progress-bar">
|
||||
<div class="svc-progress-fill" :style="{ width: svcProgress + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage 4: 桌面显示 -->
|
||||
<div v-if="stage === 4" class="screen-desktop">
|
||||
<div class="desktop-bg">
|
||||
<div class="desktop-icons">
|
||||
<div class="desktop-icon" v-for="(ic, i) in desktopIcons" :key="i">
|
||||
<span class="d-icon">{{ ic.icon }}</span>
|
||||
<span class="d-label">{{ ic.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="taskbar">
|
||||
<span class="taskbar-start">🪟</span>
|
||||
<span class="taskbar-apps">
|
||||
<span v-for="(app, i) in taskbarApps" :key="i" class="taskbar-app">{{ app }}</span>
|
||||
</span>
|
||||
<span class="taskbar-time">{{ currentTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度指示 -->
|
||||
<div class="stage-dots">
|
||||
<div
|
||||
v-for="(s, i) in stages"
|
||||
:key="i"
|
||||
class="stage-dot"
|
||||
:class="{ active: stage === i, done: stage > i }"
|
||||
>
|
||||
<span class="dot-label">{{ s.short }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制按钮 -->
|
||||
<div class="controls">
|
||||
<button class="ctrl-btn" :disabled="stage <= 0" @click="prev">← 上一步</button>
|
||||
<button class="ctrl-btn primary" v-if="stage === 0" @click="next">开始 →</button>
|
||||
<button class="ctrl-btn primary" v-else-if="stage < 4" @click="next">下一步 →</button>
|
||||
<button class="ctrl-btn" v-else @click="reset">↺ 重新开始</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:详细信息 -->
|
||||
<div class="info-panel">
|
||||
<div class="info-stage-header">
|
||||
<span class="info-stage-icon">{{ currentStage.icon }}</span>
|
||||
<div>
|
||||
<div class="info-stage-name">{{ currentStage.name }}</div>
|
||||
<div class="info-stage-desc">{{ currentStage.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-operations">
|
||||
<div
|
||||
v-for="(op, i) in currentStage.operations"
|
||||
:key="i"
|
||||
class="op-card"
|
||||
:class="{ expanded: expandedOp === i }"
|
||||
@click="expandedOp = expandedOp === i ? -1 : i"
|
||||
>
|
||||
<div class="op-header">
|
||||
<span class="op-num">{{ i + 1 }}</span>
|
||||
<span class="op-icon">{{ op.icon }}</span>
|
||||
<span class="op-name">{{ op.name }}</span>
|
||||
<span class="op-toggle">{{ expandedOp === i ? '▾' : '▸' }}</span>
|
||||
</div>
|
||||
<transition name="expand">
|
||||
<div v-if="expandedOp === i" class="op-detail">
|
||||
<div class="op-what">{{ op.what }}</div>
|
||||
<div v-if="op.details" class="op-details">
|
||||
<div v-for="(d, j) in op.details" :key="j" class="op-detail-item">
|
||||
<span class="od-dot">•</span>
|
||||
<span>{{ d }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentStage.analogy" class="info-analogy">
|
||||
<span class="analogy-icon">💡</span>
|
||||
<span>{{ currentStage.analogy }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 操作系统对比表 -->
|
||||
<div v-if="stage === 0" class="os-comparison">
|
||||
<div class="os-comp-header">
|
||||
<span class="os-comp-icon">📊</span>
|
||||
<span class="os-comp-title">常见操作系统</span>
|
||||
</div>
|
||||
<div class="os-comp-table">
|
||||
<div class="os-comp-row os-comp-header-row">
|
||||
<span class="os-comp-cell">系统</span>
|
||||
<span class="os-comp-cell">特点</span>
|
||||
<span class="os-comp-cell">典型设备</span>
|
||||
</div>
|
||||
<div v-for="os in osList" :key="os.name" class="os-comp-row">
|
||||
<span class="os-comp-cell os-name-cell">
|
||||
<span class="os-comp-icon-small">{{ os.icon }}</span>
|
||||
{{ os.name }}
|
||||
</span>
|
||||
<span class="os-comp-cell">{{ os.feature }}</span>
|
||||
<span class="os-comp-cell">{{ os.device }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 启动流程对比 -->
|
||||
<div v-if="stage === 1" class="boot-flow-comparison">
|
||||
<div class="bf-header">
|
||||
<span class="bf-icon">🔄</span>
|
||||
<span class="bf-title">Windows vs Linux 启动流程</span>
|
||||
</div>
|
||||
<div class="bf-content">
|
||||
<div class="bf-col">
|
||||
<div class="bf-os-name">🪟 Windows</div>
|
||||
<div class="bf-flow">
|
||||
<div class="bf-step" v-for="(step, i) in windowsFlow" :key="i">
|
||||
<span class="bf-arrow" v-if="i > 0">↓</span>
|
||||
<span class="bf-step-text">{{ step }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bf-col">
|
||||
<div class="bf-os-name">🐧 Linux</div>
|
||||
<div class="bf-flow">
|
||||
<div class="bf-step" v-for="(step, i) in linuxFlow" :key="i">
|
||||
<span class="bf-arrow" v-if="i > 0">↓</span>
|
||||
<span class="bf-step-text">{{ step }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const stage = ref(0)
|
||||
const expandedOp = ref(-1)
|
||||
const blStep = ref(-1)
|
||||
const blCodeLine = ref(-1)
|
||||
const kernelProgress = ref(0)
|
||||
const svcProgress = ref(0)
|
||||
const currentTime = ref('09:41')
|
||||
|
||||
const osList = [
|
||||
{ name: 'Windows', icon: '🪟', feature: '生态丰富,兼容性好', device: '桌面电脑、笔记本' },
|
||||
{ name: 'macOS', icon: '🍎', feature: '苹果生态,流畅稳定', device: 'Mac 电脑' },
|
||||
{ name: 'Linux', icon: '🐧', feature: '开源免费,服务器首选', device: '服务器、嵌入式' },
|
||||
{ name: 'Android', icon: '🤖', feature: '移动端 Linux', device: '手机、平板' },
|
||||
{ name: 'iOS', icon: '📱', feature: '苹果移动端', device: 'iPhone、iPad' }
|
||||
]
|
||||
|
||||
const blSteps = ['读取分区表', '找到系统分区', '加载内核到内存', '跳转到内核入口']
|
||||
const blCode = [
|
||||
'mov ax, 0x07C0',
|
||||
'mov ds, ax',
|
||||
'read_sector:',
|
||||
' mov ah, 0x02',
|
||||
' int 0x13',
|
||||
'jmp 0x0000:0x7C00'
|
||||
]
|
||||
|
||||
const kernelName = ref('ntoskrnl.exe')
|
||||
const kernelModules = ['进程管理', '内存管理', '文件系统', '设备驱动']
|
||||
|
||||
const services = [
|
||||
{ name: '网络服务', icon: '🌐' },
|
||||
{ name: '音频服务', icon: '🔊' },
|
||||
{ name: '安全中心', icon: '🛡️' },
|
||||
{ name: '打印服务', icon: '🖨️' },
|
||||
{ name: '图形界面', icon: '🎨' },
|
||||
{ name: '系统日志', icon: '📝' }
|
||||
]
|
||||
|
||||
const desktopIcons = [
|
||||
{ icon: '📁', label: '文件' },
|
||||
{ icon: '🌐', label: '浏览器' },
|
||||
{ icon: '📧', label: '邮件' },
|
||||
{ icon: '⚙️', label: '设置' }
|
||||
]
|
||||
|
||||
const taskbarApps = ['🌐', '📁', '📝']
|
||||
|
||||
const windowsFlow = ['BIOS', 'MBR', 'bootmgr', 'winload.exe', 'ntoskrnl.exe', '系统服务', '桌面']
|
||||
const linuxFlow = ['BIOS', 'GRUB', 'vmlinuz', 'systemd', '系统服务', '桌面环境']
|
||||
|
||||
const stages = [
|
||||
{
|
||||
short: '介绍',
|
||||
icon: '🖥️',
|
||||
name: '什么是操作系统?',
|
||||
desc: '操作系统(OS)是管理计算机硬件和软件资源的程序集合,就像一个"大管家"。',
|
||||
operations: [
|
||||
{
|
||||
icon: '🏢', name: '资源管理',
|
||||
what: '操作系统负责管理 CPU、内存、硬盘、网络等所有硬件资源。',
|
||||
details: ['进程管理 - 调度程序运行', '内存管理 - 分配和回收内存', '文件系统 - 管理文件存储', '设备管理 - 控制硬件设备']
|
||||
},
|
||||
{
|
||||
icon: '🎮', name: '提供接口',
|
||||
what: '为应用程序提供统一的接口,让程序不需要直接操作硬件。',
|
||||
details: ['系统调用接口(API)', '图形用户界面(GUI)', '命令行界面(CLI)', '驱动程序接口']
|
||||
},
|
||||
{
|
||||
icon: '🔒', name: '安全保护',
|
||||
what: '保护系统资源不被非法访问,确保多用户环境下的隔离。',
|
||||
details: ['用户权限管理', '进程地址空间隔离', '文件访问控制', '网络安全防护']
|
||||
}
|
||||
],
|
||||
analogy: '操作系统就像一座大楼的物业管理——负责水电供应(硬件资源)、分配房间(内存)、管理仓库(文件系统)、维护安全(权限控制),让住户(应用程序)可以安心生活。'
|
||||
},
|
||||
{
|
||||
short: '引导程序',
|
||||
icon: '🚀',
|
||||
name: '引导程序(Bootloader)',
|
||||
desc: '硬盘第一个扇区存放着引导程序,它的任务是把操作系统内核加载到内存。',
|
||||
operations: [
|
||||
{
|
||||
icon: '📀', name: '读取分区表',
|
||||
what: '引导程序首先读取硬盘的分区表,找到操作系统所在的分区。',
|
||||
details: ['读取 MBR(主引导记录)', '解析分区表结构', '定位活动分区', 'Windows: bootmgr / Linux: GRUB']
|
||||
},
|
||||
{
|
||||
icon: '🔍', name: '定位内核',
|
||||
what: '在系统分区中找到操作系统内核文件的位置。',
|
||||
details: ['Windows: 读取 BCD 配置', 'Linux: 显示系统选择菜单', '支持多系统启动', '加载文件系统驱动']
|
||||
},
|
||||
{
|
||||
icon: '💾', name: '加载到内存',
|
||||
what: '将内核文件从硬盘读取到内存的指定位置。',
|
||||
details: ['解压压缩的内核镜像', '复制到内存 0x100000 以上', 'Windows: ntoskrnl.exe', 'Linux: vmlinuz']
|
||||
},
|
||||
{
|
||||
icon: '➡️', name: '跳转执行',
|
||||
what: '设置好初始环境后,跳转到内核入口点,把控制权交给内核。',
|
||||
details: ['设置 CPU 保护模式', '初始化页表', '跳转至内核入口', '内核开始执行']
|
||||
}
|
||||
],
|
||||
analogy: '引导程序就像剧场的报幕员——他先上台确认场地(检查硬件)、找到剧本(定位内核)、把道具摆好(加载到内存),然后宣布:"演出开始!"(跳转执行)'
|
||||
},
|
||||
{
|
||||
short: '内核加载',
|
||||
icon: '⚙️',
|
||||
name: '操作系统内核(Kernel)',
|
||||
desc: '内核是操作系统的核心,负责管理内存、CPU、进程等核心功能。',
|
||||
operations: [
|
||||
{
|
||||
icon: '🧠', name: '进程管理',
|
||||
what: '创建第一个用户进程,建立进程调度机制。',
|
||||
details: ['创建 init/systemd 进程', '建立进程控制块(PCB)', '初始化调度器', '设置进程优先级']
|
||||
},
|
||||
{
|
||||
icon: '💾', name: '内存管理',
|
||||
what: '建立虚拟内存系统,划分内核空间和用户空间。',
|
||||
details: ['初始化页表', '建立物理内存映射', '设置内存保护', '启用虚拟内存']
|
||||
},
|
||||
{
|
||||
icon: '📁', name: '文件系统',
|
||||
what: '挂载根文件系统,初始化 VFS 层。',
|
||||
details: ['识别文件系统类型', '挂载根分区(/)', '初始化 inode 缓存', '建立文件描述符表']
|
||||
},
|
||||
{
|
||||
icon: '🔌', name: '设备驱动',
|
||||
what: '加载核心设备驱动,初始化硬件抽象层。',
|
||||
details: ['加载磁盘驱动', '初始化显示驱动', '加载键盘鼠标驱动', '枚举 PCI 设备']
|
||||
}
|
||||
],
|
||||
analogy: '内核就像公司的 CEO 上任——接管所有部门(硬件),安排人事(进程)、财务(内存)、后勤(设备)各就各位,建立公司的基本运作框架。'
|
||||
},
|
||||
{
|
||||
short: '服务启动',
|
||||
icon: '🔧',
|
||||
name: '系统服务启动',
|
||||
desc: '内核拉起第一个用户进程,按依赖顺序启动各种后台服务。',
|
||||
operations: [
|
||||
{
|
||||
icon: '🚀', name: '初始化进程',
|
||||
what: '启动第一个用户态进程(PID=1),它是所有其他进程的"祖先"。',
|
||||
details: ['Linux: systemd 或 init', 'Windows: smss.exe → csrss.exe', '读取服务配置文件', '按依赖关系排序']
|
||||
},
|
||||
{
|
||||
icon: '🌐', name: '网络服务',
|
||||
what: '初始化网卡驱动,配置网络连接。',
|
||||
details: ['加载网卡驱动', 'DHCP 获取 IP 地址', '配置 DNS 服务器', '启动防火墙']
|
||||
},
|
||||
{
|
||||
icon: '🔒', name: '安全服务',
|
||||
what: '启动用户认证和安全监控服务。',
|
||||
details: ['启动登录管理器', '初始化权限系统', '启动杀毒软件', '配置安全策略']
|
||||
},
|
||||
{
|
||||
icon: '🔊', name: '多媒体服务',
|
||||
what: '启动音频、显示等多媒体相关服务。',
|
||||
details: ['启动音频服务', '初始化显示管理器', '加载主题和字体', '准备用户界面']
|
||||
}
|
||||
],
|
||||
analogy: '就像商场开门营业前——保安到岗(安全)、空调开启(后台服务)、收银上线(网络),一切就绪迎接顾客(用户)。'
|
||||
},
|
||||
{
|
||||
short: '桌面就绪',
|
||||
icon: '🖥️',
|
||||
name: '显示桌面',
|
||||
desc: '图形界面启动完成,用户熟悉的桌面环境呈现出来。',
|
||||
operations: [
|
||||
{
|
||||
icon: '🎮', name: '显卡驱动',
|
||||
what: '初始化 GPU,设置屏幕分辨率和色彩。',
|
||||
details: ['加载显卡驱动', '设置分辨率(如 1920×1080)', '启用硬件加速', '配置多显示器']
|
||||
},
|
||||
{
|
||||
icon: '🪟', name: '窗口系统',
|
||||
what: '启动窗口管理器,负责窗口的绘制和交互。',
|
||||
details: ['Windows: DWM', 'Linux: X11/Wayland', 'macOS: WindowServer', '管理窗口层叠关系']
|
||||
},
|
||||
{
|
||||
icon: '🎨', name: '桌面环境',
|
||||
what: '绘制壁纸、桌面图标、任务栏等界面元素。',
|
||||
details: ['加载桌面壁纸', '显示桌面图标', '渲染任务栏', '加载系统托盘']
|
||||
},
|
||||
{
|
||||
icon: '👆', name: '用户交互',
|
||||
what: '鼠标光标出现,系统进入完全可交互状态。',
|
||||
details: ['显示鼠标指针', '响应键盘输入', '加载用户配置', '启动自启动程序']
|
||||
}
|
||||
],
|
||||
analogy: '幕布拉开,灯光亮起——舞台(窗口)搭好,演员(图标)就位,等待观众(你)的第一次操作。'
|
||||
}
|
||||
]
|
||||
|
||||
const currentStage = computed(() => stages[stage.value])
|
||||
|
||||
let timeInterval = null
|
||||
|
||||
onMounted(() => {
|
||||
timeInterval = setInterval(() => {
|
||||
const now = new Date()
|
||||
currentTime.value = now.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeInterval) clearInterval(timeInterval)
|
||||
})
|
||||
|
||||
// 引导程序动画
|
||||
watch(() => stage.value, (newStage) => {
|
||||
if (newStage === 1) {
|
||||
blStep.value = -1
|
||||
blCodeLine.value = -1
|
||||
let step = 0
|
||||
const interval = setInterval(() => {
|
||||
if (step < blSteps.length) {
|
||||
blStep.value = step
|
||||
blCodeLine.value = step + 1
|
||||
step++
|
||||
} else {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 600)
|
||||
}
|
||||
})
|
||||
|
||||
// 内核加载动画
|
||||
watch(() => stage.value, (newStage) => {
|
||||
if (newStage === 2) {
|
||||
kernelProgress.value = 0
|
||||
kernelName.value = Math.random() > 0.5 ? 'ntoskrnl.exe' : 'vmlinuz'
|
||||
const interval = setInterval(() => {
|
||||
if (kernelProgress.value < 100) {
|
||||
kernelProgress.value += 4
|
||||
} else {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 80)
|
||||
}
|
||||
})
|
||||
|
||||
// 服务启动动画
|
||||
watch(() => stage.value, (newStage) => {
|
||||
if (newStage === 3) {
|
||||
svcProgress.value = 0
|
||||
const interval = setInterval(() => {
|
||||
if (svcProgress.value < 100) {
|
||||
svcProgress.value += 3
|
||||
} else {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
|
||||
function next() {
|
||||
if (stage.value < 4) {
|
||||
stage.value++
|
||||
expandedOp.value = -1
|
||||
}
|
||||
}
|
||||
function prev() {
|
||||
if (stage.value > 0) {
|
||||
stage.value--
|
||||
expandedOp.value = -1
|
||||
}
|
||||
}
|
||||
function reset() {
|
||||
stage.value = 0
|
||||
expandedOp.value = -1
|
||||
blStep.value = -1
|
||||
blCodeLine.value = -1
|
||||
kernelProgress.value = 0
|
||||
svcProgress.value = 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.os-boot-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1.2rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.demo-header { margin-bottom: 1rem; }
|
||||
.demo-title { font-size: 0.9rem; font-weight: 700; color: var(--vp-c-text-1); }
|
||||
|
||||
/* 主布局 */
|
||||
.main-layout { display: flex; gap: 1rem; }
|
||||
|
||||
/* ===== 左侧屏幕 ===== */
|
||||
.screen-panel { flex: 0 0 280px; display: flex; flex-direction: column; gap: 0.6rem; }
|
||||
.monitor { background: #222; border-radius: 10px; padding: 3px; }
|
||||
.monitor-bezel { background: #111; border-radius: 8px; overflow: hidden; }
|
||||
.screen {
|
||||
width: 100%; aspect-ratio: 4/3; display: flex;
|
||||
align-items: center; justify-content: center;
|
||||
font-family: 'Courier New', monospace; transition: background 0.5s;
|
||||
overflow: hidden; position: relative;
|
||||
}
|
||||
|
||||
/* 介绍 */
|
||||
.stage-0 { background: linear-gradient(135deg, #1e3a5f 0%, #0f172a 100%); }
|
||||
.screen-intro { text-align: center; color: #fff; width: 100%; padding: 0.5rem; }
|
||||
.intro-icon { font-size: 2rem; margin-bottom: 0.2rem; }
|
||||
.intro-title { font-size: 0.8rem; font-weight: 700; margin-bottom: 0.2rem; }
|
||||
.intro-desc { font-size: 0.55rem; color: #94a3b8; margin-bottom: 0.4rem; line-height: 1.4; }
|
||||
.os-icons {
|
||||
display: grid; grid-template-columns: repeat(5, 1fr);
|
||||
gap: 0.2rem; padding: 0 0.3rem;
|
||||
}
|
||||
.os-item { display: flex; flex-direction: column; align-items: center; }
|
||||
.os-icon { font-size: 1rem; }
|
||||
.os-name { font-size: 0.4rem; color: #94a3b8; margin-top: 0.1rem; }
|
||||
|
||||
/* Bootloader */
|
||||
.stage-1 { background: #0f172a; flex-direction: column; padding: 0.5rem; align-items: flex-start; }
|
||||
.screen-bootloader { width: 100%; }
|
||||
.bl-header { color: #fbbf24; font-size: 0.55rem; margin-bottom: 0.4rem; font-weight: 700; }
|
||||
.bl-flow { display: flex; flex-direction: column; gap: 0.2rem; margin-bottom: 0.4rem; }
|
||||
.bl-step {
|
||||
display: flex; align-items: center; gap: 0.3rem;
|
||||
color: #64748b; font-size: 0.55rem;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.bl-step.active { color: #fbbf24; }
|
||||
.bl-num {
|
||||
width: 1rem; height: 1rem; border-radius: 50%;
|
||||
background: rgba(255,255,255,0.1); display: flex;
|
||||
align-items: center; justify-content: center; font-size: 0.5rem;
|
||||
}
|
||||
.bl-step.active .bl-num { background: #fbbf24; color: #000; }
|
||||
.bl-code {
|
||||
background: rgba(0,0,0,0.5); border-radius: 4px;
|
||||
padding: 0.3rem; font-size: 0.45rem; color: #64748b;
|
||||
font-family: monospace;
|
||||
}
|
||||
.code-line { line-height: 1.4; padding: 0 0.2rem; }
|
||||
.code-line.highlight { color: #fbbf24; background: rgba(251, 191, 36, 0.1); border-radius: 2px; }
|
||||
|
||||
/* Kernel */
|
||||
.stage-2 { background: #1a1a2e; flex-direction: column; padding: 0.6rem; }
|
||||
.screen-kernel { text-align: center; width: 100%; }
|
||||
.kernel-header { color: #60a5fa; font-size: 0.55rem; margin-bottom: 0.4rem; font-weight: 700; }
|
||||
.kernel-logo { font-size: 2rem; margin-bottom: 0.2rem; }
|
||||
.kernel-name { color: #fff; font-size: 0.6rem; margin-bottom: 0.4rem; }
|
||||
.kernel-bar-wrap {
|
||||
width: 80%; height: 6px; background: #333; border-radius: 3px;
|
||||
margin: 0 auto 0.4rem; overflow: hidden;
|
||||
}
|
||||
.kernel-bar {
|
||||
height: 100%; background: linear-gradient(90deg, #60a5fa, #3b82f6);
|
||||
transition: width 0.1s linear;
|
||||
}
|
||||
.kernel-modules { display: flex; flex-direction: column; gap: 0.15rem; }
|
||||
.k-module {
|
||||
display: flex; align-items: center; gap: 0.3rem;
|
||||
color: #64748b; font-size: 0.55rem;
|
||||
}
|
||||
.k-module.loaded { color: #4ade80; }
|
||||
.k-status { width: 1rem; text-align: center; }
|
||||
|
||||
/* Services */
|
||||
.stage-3 { background: #0f172a; flex-direction: column; padding: 0.5rem; }
|
||||
.screen-services { width: 100%; }
|
||||
.svc-header { color: #a78bfa; font-size: 0.55rem; margin-bottom: 0.4rem; font-weight: 700; }
|
||||
.svc-grid {
|
||||
display: grid; grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.3rem; margin-bottom: 0.4rem;
|
||||
}
|
||||
.svc-item {
|
||||
display: flex; align-items: center; gap: 0.2rem;
|
||||
padding: 0.25rem; background: rgba(255,255,255,0.05);
|
||||
border-radius: 4px; font-size: 0.5rem; color: #64748b;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.svc-item.started { color: #a78bfa; background: rgba(167, 139, 250, 0.1); }
|
||||
.svc-icon { font-size: 0.7rem; }
|
||||
.svc-name { flex: 1; }
|
||||
.svc-status { font-size: 0.5rem; }
|
||||
.svc-progress-bar {
|
||||
height: 4px; background: #333; border-radius: 2px; overflow: hidden;
|
||||
}
|
||||
.svc-progress-fill {
|
||||
height: 100%; background: linear-gradient(90deg, #a78bfa, #8b5cf6);
|
||||
transition: width 0.1s linear;
|
||||
}
|
||||
|
||||
/* Desktop */
|
||||
.stage-4 { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 0; }
|
||||
.screen-desktop { width: 100%; height: 100%; }
|
||||
.desktop-bg {
|
||||
width: 100%; height: 100%;
|
||||
display: flex; flex-direction: column; justify-content: space-between;
|
||||
}
|
||||
.desktop-icons {
|
||||
display: grid; grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.3rem; padding: 0.6rem 0.4rem;
|
||||
}
|
||||
.desktop-icon {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
.d-icon { font-size: 1.2rem; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3)); }
|
||||
.d-label { font-size: 0.45rem; color: white; text-shadow: 0 1px 2px rgba(0,0,0,0.5); }
|
||||
.taskbar {
|
||||
background: rgba(0,0,0,0.6); backdrop-filter: blur(8px);
|
||||
display: flex; align-items: center; gap: 0.4rem;
|
||||
padding: 0.25rem 0.4rem;
|
||||
}
|
||||
.taskbar-start { font-size: 0.9rem; cursor: pointer; }
|
||||
.taskbar-apps { display: flex; gap: 0.2rem; flex: 1; }
|
||||
.taskbar-app { font-size: 0.8rem; opacity: 0.8; cursor: pointer; }
|
||||
.taskbar-time { color: white; font-size: 0.5rem; }
|
||||
|
||||
/* 进度点 */
|
||||
.stage-dots { display: flex; justify-content: center; gap: 0.3rem; }
|
||||
.stage-dot {
|
||||
padding: 0.15rem 0.4rem; border-radius: 10px;
|
||||
font-size: 0.55rem; color: var(--vp-c-text-3);
|
||||
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.stage-dot.active {
|
||||
background: var(--vp-c-brand); color: white; border-color: var(--vp-c-brand);
|
||||
}
|
||||
.stage-dot.done { background: #10b981; color: white; border-color: #10b981; }
|
||||
.dot-label { white-space: nowrap; }
|
||||
|
||||
/* 控制按钮 */
|
||||
.controls { display: flex; gap: 0.4rem; justify-content: center; }
|
||||
.ctrl-btn {
|
||||
padding: 0.35rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg); color: var(--vp-c-text-2); font-size: 0.68rem;
|
||||
cursor: pointer; transition: all 0.2s;
|
||||
}
|
||||
.ctrl-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
|
||||
.ctrl-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.ctrl-btn.primary {
|
||||
background: var(--vp-c-brand); color: white; border-color: var(--vp-c-brand);
|
||||
}
|
||||
.ctrl-btn.primary:hover { opacity: 0.9; }
|
||||
|
||||
/* ===== 右侧信息 ===== */
|
||||
.info-panel { flex: 1; min-width: 0; }
|
||||
.info-stage-header { display: flex; align-items: flex-start; gap: 0.5rem; margin-bottom: 0.7rem; }
|
||||
.info-stage-icon { font-size: 1.4rem; }
|
||||
.info-stage-name { font-size: 0.82rem; font-weight: 700; color: var(--vp-c-text-1); }
|
||||
.info-stage-desc { font-size: 0.68rem; color: var(--vp-c-text-3); margin-top: 0.1rem; line-height: 1.4; }
|
||||
|
||||
/* 操作卡片 */
|
||||
.info-operations { display: flex; flex-direction: column; gap: 0.35rem; }
|
||||
.op-card {
|
||||
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px; padding: 0.5rem 0.6rem; cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.op-card.expanded { border-color: var(--vp-c-brand); box-shadow: 0 1px 8px rgba(0,0,0,0.05); }
|
||||
.op-header { display: flex; align-items: center; gap: 0.4rem; }
|
||||
.op-num {
|
||||
width: 1.2rem; height: 1.2rem; border-radius: 50%;
|
||||
background: var(--vp-c-brand-soft); color: var(--vp-c-brand);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 0.58rem; font-weight: 700; flex-shrink: 0;
|
||||
}
|
||||
.op-icon { font-size: 0.9rem; }
|
||||
.op-name { flex: 1; font-size: 0.72rem; font-weight: 600; color: var(--vp-c-text-1); }
|
||||
.op-toggle { font-size: 0.65rem; color: var(--vp-c-text-3); }
|
||||
|
||||
.op-detail { margin-top: 0.4rem; padding-top: 0.4rem; border-top: 1px dashed var(--vp-c-divider); }
|
||||
.op-what { font-size: 0.66rem; color: var(--vp-c-text-2); line-height: 1.6; margin-bottom: 0.3rem; }
|
||||
.op-details { display: flex; flex-direction: column; gap: 0.15rem; }
|
||||
.op-detail-item {
|
||||
display: flex; align-items: flex-start; gap: 0.3rem;
|
||||
font-size: 0.62rem; color: var(--vp-c-text-3); line-height: 1.4;
|
||||
}
|
||||
.od-dot { color: var(--vp-c-brand); flex-shrink: 0; }
|
||||
|
||||
/* 类比 */
|
||||
.info-analogy {
|
||||
display: flex; align-items: flex-start; gap: 0.4rem;
|
||||
margin-top: 0.6rem; padding: 0.5rem 0.6rem;
|
||||
background: var(--vp-c-brand-soft); border-radius: 6px;
|
||||
font-size: 0.64rem; color: var(--vp-c-text-2);
|
||||
line-height: 1.5; font-style: italic;
|
||||
}
|
||||
.analogy-icon { font-size: 0.85rem; flex-shrink: 0; }
|
||||
|
||||
/* 操作系统对比表 */
|
||||
.os-comparison {
|
||||
margin-top: 0.6rem; padding: 0.5rem 0.6rem;
|
||||
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.os-comp-header {
|
||||
display: flex; align-items: center; gap: 0.3rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.os-comp-icon { font-size: 0.9rem; }
|
||||
.os-comp-title { font-size: 0.7rem; font-weight: 600; color: var(--vp-c-text-1); }
|
||||
.os-comp-table { display: flex; flex-direction: column; gap: 0.2rem; }
|
||||
.os-comp-row {
|
||||
display: grid; grid-template-columns: 1.2fr 1.5fr 1.3fr;
|
||||
gap: 0.3rem; font-size: 0.6rem; padding: 0.2rem 0;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.os-comp-row:last-child { border-bottom: none; }
|
||||
.os-comp-header-row { font-weight: 600; color: var(--vp-c-text-1); }
|
||||
.os-comp-cell { color: var(--vp-c-text-2); }
|
||||
.os-name-cell { display: flex; align-items: center; gap: 0.2rem; }
|
||||
.os-comp-icon-small { font-size: 0.7rem; }
|
||||
|
||||
/* 启动流程对比 */
|
||||
.boot-flow-comparison {
|
||||
margin-top: 0.6rem; padding: 0.5rem 0.6rem;
|
||||
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.bf-header {
|
||||
display: flex; align-items: center; gap: 0.3rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.bf-icon { font-size: 0.9rem; }
|
||||
.bf-title { font-size: 0.7rem; font-weight: 600; color: var(--vp-c-text-1); }
|
||||
.bf-content { display: flex; gap: 0.5rem; }
|
||||
.bf-col { flex: 1; }
|
||||
.bf-os-name { font-size: 0.65rem; font-weight: 600; color: var(--vp-c-text-1); margin-bottom: 0.3rem; }
|
||||
.bf-flow { display: flex; flex-direction: column; gap: 0.15rem; }
|
||||
.bf-step { display: flex; flex-direction: column; align-items: center; font-size: 0.55rem; }
|
||||
.bf-arrow { color: var(--vp-c-brand); font-size: 0.6rem; }
|
||||
.bf-step-text {
|
||||
padding: 0.15rem 0.3rem; background: var(--vp-c-bg-soft);
|
||||
border-radius: 3px; color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
/* 展开动画 */
|
||||
.expand-enter-active, .expand-leave-active { transition: all 0.25s ease; overflow: hidden; }
|
||||
.expand-enter-from, .expand-leave-to { opacity: 0; max-height: 0; }
|
||||
.expand-enter-to, .expand-leave-from { opacity: 1; max-height: 20rem; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.main-layout { flex-direction: column; }
|
||||
.screen-panel { flex: none; width: 100%; }
|
||||
.bf-content { flex-direction: column; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,123 @@
|
||||
<!--
|
||||
CDNAccelerationDemo.vue
|
||||
CDN 加速演示:展示 CDN 如何加速文件访问
|
||||
-->
|
||||
<template>
|
||||
<div class="cdn-demo">
|
||||
<div class="header">
|
||||
<div class="title">CDN 加速原理</div>
|
||||
<div class="subtitle">对比有无 CDN 时的文件访问路径</div>
|
||||
</div>
|
||||
|
||||
<div class="mode-tabs">
|
||||
<button :class="['tab', { active: !cdnEnabled }]" @click="cdnEnabled = false">无 CDN</button>
|
||||
<button :class="['tab', { active: cdnEnabled }]" @click="cdnEnabled = true">有 CDN</button>
|
||||
</div>
|
||||
|
||||
<div class="diagram">
|
||||
<div class="node user-node">
|
||||
<div class="node-icon">👤</div>
|
||||
<div class="node-label">北京用户</div>
|
||||
</div>
|
||||
|
||||
<div class="path-line" :class="{ highlight: !cdnEnabled }">
|
||||
<span class="latency">{{ cdnEnabled ? '5ms' : '200ms' }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="cdnEnabled" class="node cdn-node">
|
||||
<div class="node-icon">⚡</div>
|
||||
<div class="node-label">北京 CDN 节点</div>
|
||||
<div class="node-detail">缓存命中</div>
|
||||
</div>
|
||||
|
||||
<div v-if="cdnEnabled" class="path-line miss-line">
|
||||
<span class="latency miss">缓存未命中时回源</span>
|
||||
</div>
|
||||
|
||||
<div class="node origin-node">
|
||||
<div class="node-icon">🏢</div>
|
||||
<div class="node-label">源站(美西 S3)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metrics">
|
||||
<div class="metric">
|
||||
<div class="metric-label">首字节时间 (TTFB)</div>
|
||||
<div class="metric-bar">
|
||||
<div class="bar-fill" :style="{ width: cdnEnabled ? '15%' : '100%' }"></div>
|
||||
</div>
|
||||
<div class="metric-value">{{ cdnEnabled ? '~30ms' : '~200ms' }}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-label">下载 1MB 图片</div>
|
||||
<div class="metric-bar">
|
||||
<div class="bar-fill" :style="{ width: cdnEnabled ? '20%' : '100%' }"></div>
|
||||
</div>
|
||||
<div class="metric-value">{{ cdnEnabled ? '~50ms' : '~800ms' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
const cdnEnabled = ref(true)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cdn-demo {
|
||||
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px; padding: 1.5rem; margin: 1.5rem 0;
|
||||
}
|
||||
.header { margin-bottom: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.mode-tabs { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; }
|
||||
.tab {
|
||||
padding: 0.4rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg); cursor: pointer; font-size: 0.85rem;
|
||||
}
|
||||
.tab.active { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
|
||||
.diagram {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
gap: 0.5rem; margin-bottom: 1.5rem; flex-wrap: wrap;
|
||||
}
|
||||
.node {
|
||||
padding: 0.75rem 1rem; border-radius: 10px; text-align: center;
|
||||
border: 2px solid var(--vp-c-divider); background: var(--vp-c-bg);
|
||||
}
|
||||
.cdn-node { border-color: #22c55e; background: rgba(34,197,94,0.05); }
|
||||
.node-icon { font-size: 1.5rem; }
|
||||
.node-label { font-weight: 600; font-size: 0.85rem; margin-top: 0.25rem; }
|
||||
.node-detail { font-size: 0.75rem; color: #22c55e; }
|
||||
.path-line {
|
||||
display: flex; align-items: center; padding: 0 0.5rem;
|
||||
font-size: 0.8rem; color: var(--vp-c-text-3);
|
||||
}
|
||||
.path-line::before, .path-line::after { content: '→'; margin: 0 0.25rem; }
|
||||
.latency {
|
||||
padding: 0.15rem 0.4rem; border-radius: 4px; font-family: var(--vp-font-family-mono);
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.1); color: var(--vp-c-brand); font-size: 0.75rem;
|
||||
}
|
||||
.latency.miss { background: rgba(245,158,11,0.1); color: #f59e0b; font-family: var(--vp-font-family-base); }
|
||||
.miss-line { opacity: 0.5; }
|
||||
.metrics { display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
.metric { display: flex; align-items: center; gap: 0.75rem; }
|
||||
.metric-label { min-width: 140px; font-size: 0.85rem; font-weight: 600; }
|
||||
.metric-bar {
|
||||
flex: 1; height: 20px; background: var(--vp-c-bg); border-radius: 4px;
|
||||
border: 1px solid var(--vp-c-divider); overflow: hidden;
|
||||
}
|
||||
.bar-fill {
|
||||
height: 100%; background: var(--vp-c-brand); border-radius: 3px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
.metric-value { min-width: 80px; font-size: 0.85rem; font-family: var(--vp-font-family-mono); text-align: right; }
|
||||
@media (max-width: 640px) {
|
||||
.diagram { flex-direction: column; }
|
||||
.path-line::before, .path-line::after { content: '↓'; }
|
||||
.metric { flex-direction: column; align-items: flex-start; gap: 0.25rem; }
|
||||
.metric-label { min-width: auto; }
|
||||
.metric-value { min-width: auto; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<!--
|
||||
StorageTypeDemo.vue (file-storage)
|
||||
文件存储类型对比演示
|
||||
-->
|
||||
<template>
|
||||
<div class="storage-type-demo">
|
||||
<div class="header">
|
||||
<div class="title">存储类型对比</div>
|
||||
<div class="subtitle">点击查看不同存储方式的特点</div>
|
||||
</div>
|
||||
|
||||
<div class="type-cards">
|
||||
<div
|
||||
v-for="t in types"
|
||||
:key="t.key"
|
||||
:class="['type-card', { active: selected === t.key }]"
|
||||
@click="selected = t.key"
|
||||
>
|
||||
<div class="type-icon">{{ t.icon }}</div>
|
||||
<div class="type-name">{{ t.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="detail">
|
||||
<div class="detail-title">{{ current.name }}</div>
|
||||
<div class="detail-desc">{{ current.desc }}</div>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<div class="item-label">访问方式</div>
|
||||
<div class="item-value">{{ current.access }}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="item-label">典型场景</div>
|
||||
<div class="item-value">{{ current.scenario }}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="item-label">代表产品</div>
|
||||
<div class="item-value">{{ current.products }}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="item-label">扩展性</div>
|
||||
<div class="item-value">{{ current.scalability }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const selected = ref('object')
|
||||
|
||||
const types = [
|
||||
{
|
||||
key: 'block', icon: '🧱', name: '块存储',
|
||||
desc: '将数据切分为固定大小的"块",像硬盘一样提供原始存储空间。操作系统可以在上面创建文件系统。性能最高,但不能直接通过网络共享。',
|
||||
access: 'iSCSI / FC 协议,挂载为磁盘设备',
|
||||
scenario: '数据库存储、虚拟机磁盘',
|
||||
products: 'AWS EBS、阿里云云盘、Ceph RBD',
|
||||
scalability: '单卷有容量上限,需要手动扩容'
|
||||
},
|
||||
{
|
||||
key: 'file', icon: '📁', name: '文件存储',
|
||||
desc: '提供传统的文件系统接口(目录 + 文件),支持多台服务器同时挂载和读写。就像一个网络共享文件夹。',
|
||||
access: 'NFS / SMB / CIFS 协议,挂载为目录',
|
||||
scenario: '共享配置文件、CMS 媒体文件、日志收集',
|
||||
products: 'AWS EFS、阿里云 NAS、NFS Server',
|
||||
scalability: '容量可弹性伸缩,但性能受限于协议开销'
|
||||
},
|
||||
{
|
||||
key: 'object', icon: '☁️', name: '对象存储',
|
||||
desc: '通过 HTTP API 存取文件(对象),每个对象有唯一 Key。扁平结构,无目录层级。容量几乎无限,成本最低,是互联网应用的首选。',
|
||||
access: 'HTTP/HTTPS RESTful API(PUT/GET/DELETE)',
|
||||
scenario: '图片、视频、备份、静态网站托管、数据湖',
|
||||
products: 'AWS S3、阿里云 OSS、MinIO、Cloudflare R2',
|
||||
scalability: '近乎无限扩展,自动分布式存储'
|
||||
}
|
||||
]
|
||||
|
||||
const current = computed(() => types.find(t => t.key === selected.value))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.storage-type-demo {
|
||||
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px; padding: 1.5rem; margin: 1.5rem 0;
|
||||
}
|
||||
.header { margin-bottom: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.type-cards { display: flex; gap: 0.75rem; margin-bottom: 1rem; }
|
||||
.type-card {
|
||||
flex: 1; padding: 1rem; border-radius: 10px; background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider); cursor: pointer; text-align: center; transition: all 0.2s;
|
||||
}
|
||||
.type-card:hover { border-color: var(--vp-c-brand); }
|
||||
.type-card.active { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); }
|
||||
.type-icon { font-size: 2rem; margin-bottom: 0.5rem; }
|
||||
.type-name { font-weight: 700; font-size: 0.95rem; }
|
||||
.detail {
|
||||
padding: 1rem; border-radius: 10px; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.detail-title { font-weight: 700; font-size: 1rem; margin-bottom: 0.5rem; }
|
||||
.detail-desc { font-size: 0.9rem; color: var(--vp-c-text-2); line-height: 1.6; margin-bottom: 1rem; }
|
||||
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
|
||||
.detail-item { padding: 0.5rem 0.75rem; background: var(--vp-c-bg-soft); border-radius: 6px; }
|
||||
.item-label { font-weight: 600; font-size: 0.8rem; color: var(--vp-c-text-3); margin-bottom: 0.25rem; }
|
||||
.item-value { font-size: 0.85rem; color: var(--vp-c-text-2); line-height: 1.5; }
|
||||
@media (max-width: 640px) {
|
||||
.type-cards { flex-direction: column; }
|
||||
.detail-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,135 @@
|
||||
<!--
|
||||
FileUploadFlowDemo.vue
|
||||
文件上传流程演示:直传 vs 服务端中转
|
||||
-->
|
||||
<template>
|
||||
<div class="upload-flow-demo">
|
||||
<div class="header">
|
||||
<div class="title">文件上传方式对比</div>
|
||||
<div class="subtitle">点击切换查看两种上传方式的流程差异</div>
|
||||
</div>
|
||||
|
||||
<div class="mode-tabs">
|
||||
<button
|
||||
:class="['tab', { active: mode === 'proxy' }]"
|
||||
@click="mode = 'proxy'; reset()"
|
||||
>服务端中转</button>
|
||||
<button
|
||||
:class="['tab', { active: mode === 'direct' }]"
|
||||
@click="mode = 'direct'; reset()"
|
||||
>客户端直传</button>
|
||||
</div>
|
||||
|
||||
<div class="flow-steps">
|
||||
<div
|
||||
v-for="(step, i) in currentSteps"
|
||||
:key="i"
|
||||
:class="['step', { active: currentStep === i, done: currentStep > i }]"
|
||||
>
|
||||
<div class="step-num">{{ i + 1 }}</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">{{ step.title }}</div>
|
||||
<div class="step-desc">{{ step.desc }}</div>
|
||||
<div v-if="step.note" class="step-note">{{ step.note }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="play-btn" @click="playFlow" :disabled="playing">
|
||||
{{ playing ? '演示中...' : '播放流程' }}
|
||||
</button>
|
||||
|
||||
<div :class="['verdict', mode]" v-if="currentStep >= currentSteps.length">
|
||||
<template v-if="mode === 'proxy'">
|
||||
⚠️ 服务端中转:文件经过你的服务器,占用带宽和内存,大文件容易超时
|
||||
</template>
|
||||
<template v-else>
|
||||
✅ 客户端直传:文件直接上传到 OSS,服务器只负责签发凭证,高效且省资源
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const mode = ref('proxy')
|
||||
const currentStep = ref(-1)
|
||||
const playing = ref(false)
|
||||
|
||||
const proxySteps = [
|
||||
{ title: '客户端 → 服务器', desc: '用户选择文件,上传到你的后端服务器', note: '大文件会占用服务器带宽和内存' },
|
||||
{ title: '服务器接收文件', desc: '后端将文件暂存到本地磁盘或内存', note: '可能触发 Nginx 的 body size 限制' },
|
||||
{ title: '服务器 → OSS', desc: '后端再将文件转发到对象存储', note: '文件传输了两次,效率低' },
|
||||
{ title: 'OSS 返回 URL', desc: '对象存储返回文件的访问地址', note: '' },
|
||||
{ title: '服务器 → 客户端', desc: '后端将文件 URL 返回给前端', note: '' }
|
||||
]
|
||||
|
||||
const directSteps = [
|
||||
{ title: '客户端 → 服务器', desc: '前端请求一个临时上传凭证(Pre-signed URL)', note: '只传少量 JSON 数据,毫秒级' },
|
||||
{ title: '服务器签发凭证', desc: '后端用 OSS SDK 生成带签名的临时上传 URL', note: '凭证有效期通常 5-15 分钟' },
|
||||
{ title: '客户端 → OSS', desc: '前端直接将文件上传到对象存储', note: '文件不经过你的服务器,节省带宽' },
|
||||
{ title: 'OSS 回调通知', desc: '上传完成后 OSS 回调你的服务器确认', note: '服务器记录文件元信息到数据库' }
|
||||
]
|
||||
|
||||
const currentSteps = computed(() => mode.value === 'proxy' ? proxySteps : directSteps)
|
||||
|
||||
function reset() {
|
||||
currentStep.value = -1
|
||||
playing.value = false
|
||||
}
|
||||
|
||||
async function playFlow() {
|
||||
reset()
|
||||
playing.value = true
|
||||
for (let i = 0; i < currentSteps.value.length; i++) {
|
||||
currentStep.value = i
|
||||
await new Promise(r => setTimeout(r, 800))
|
||||
}
|
||||
currentStep.value = currentSteps.value.length
|
||||
playing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.upload-flow-demo {
|
||||
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px; padding: 1.5rem; margin: 1.5rem 0;
|
||||
}
|
||||
.header { margin-bottom: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.mode-tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
|
||||
.tab {
|
||||
padding: 0.4rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg); cursor: pointer; font-size: 0.85rem;
|
||||
}
|
||||
.tab.active { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
|
||||
.flow-steps { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; }
|
||||
.step {
|
||||
display: flex; gap: 0.75rem; padding: 0.6rem 0.75rem; border-radius: 8px;
|
||||
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); transition: all 0.3s;
|
||||
}
|
||||
.step.active { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); }
|
||||
.step.done { border-color: #22c55e; background: rgba(34,197,94,0.03); }
|
||||
.step-num {
|
||||
width: 28px; height: 28px; border-radius: 50%; background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider); display: flex; align-items: center;
|
||||
justify-content: center; font-weight: 700; font-size: 0.8rem; flex-shrink: 0;
|
||||
}
|
||||
.step.active .step-num { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
|
||||
.step.done .step-num { border-color: #22c55e; color: #22c55e; }
|
||||
.step-title { font-weight: 600; font-size: 0.9rem; }
|
||||
.step-desc { font-size: 0.8rem; color: var(--vp-c-text-2); }
|
||||
.step-note { font-size: 0.75rem; color: var(--vp-c-text-3); font-style: italic; margin-top: 0.2rem; }
|
||||
.play-btn {
|
||||
padding: 0.5rem 1.5rem; border-radius: 6px; border: none;
|
||||
background: var(--vp-c-brand); color: #fff; cursor: pointer; font-size: 0.9rem;
|
||||
}
|
||||
.play-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.verdict {
|
||||
margin-top: 1rem; padding: 0.75rem; border-radius: 8px; font-size: 0.9rem;
|
||||
}
|
||||
.verdict.proxy { background: rgba(245,158,11,0.08); border: 1px solid rgba(245,158,11,0.3); }
|
||||
.verdict.direct { background: rgba(34,197,94,0.08); border: 1px solid rgba(34,197,94,0.3); }
|
||||
</style>
|
||||
+551
@@ -0,0 +1,551 @@
|
||||
<template>
|
||||
<div class="architecture-comparison-demo">
|
||||
<div class="demo-header">
|
||||
<span class="icon">🏗️</span>
|
||||
<span class="title">前后端项目架构对比</span>
|
||||
<span class="subtitle">点击切换查看不同架构层次</span>
|
||||
</div>
|
||||
|
||||
<!-- 切换按钮 -->
|
||||
<div class="toggle-buttons">
|
||||
<button
|
||||
:class="['toggle-btn', { active: activeType === 'frontend' }]"
|
||||
@click="activeType = 'frontend'"
|
||||
>
|
||||
<span class="btn-icon">🎨</span>
|
||||
前端架构
|
||||
</button>
|
||||
<button
|
||||
:class="['toggle-btn', { active: activeType === 'backend' }]"
|
||||
@click="activeType = 'backend'"
|
||||
>
|
||||
<span class="btn-icon">⚙️</span>
|
||||
后端架构
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 架构展示 -->
|
||||
<div class="architecture-display">
|
||||
<!-- 前端架构 -->
|
||||
<div v-if="activeType === 'frontend'" class="architecture-layers">
|
||||
<div
|
||||
v-for="(layer, index) in frontendLayers"
|
||||
:key="layer.id"
|
||||
class="layer-box"
|
||||
:class="[layer.class, { active: activeLayer === layer.id }]"
|
||||
:style="{ animationDelay: `${index * 0.1}s` }"
|
||||
@click="setActiveLayer(layer.id)"
|
||||
>
|
||||
<div class="layer-header">
|
||||
<span class="layer-icon">{{ layer.icon }}</span>
|
||||
<span class="layer-name">{{ layer.name }}</span>
|
||||
<span class="layer-badge">{{ layer.badge }}</span>
|
||||
</div>
|
||||
<div class="layer-content">
|
||||
<div class="duty">{{ layer.duty }}</div>
|
||||
<div class="example">🌰 {{ layer.example }}</div>
|
||||
</div>
|
||||
<div v-if="index < frontendLayers.length - 1" class="layer-arrow">
|
||||
<span class="arrow-icon">↓</span>
|
||||
<span class="arrow-text">{{ layer.arrow }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 后端架构 -->
|
||||
<div v-else class="architecture-layers">
|
||||
<div
|
||||
v-for="(layer, index) in backendLayers"
|
||||
:key="layer.id"
|
||||
class="layer-box"
|
||||
:class="[layer.class, { active: activeLayer === layer.id }]"
|
||||
:style="{ animationDelay: `${index * 0.1}s` }"
|
||||
@click="setActiveLayer(layer.id)"
|
||||
>
|
||||
<div class="layer-header">
|
||||
<span class="layer-icon">{{ layer.icon }}</span>
|
||||
<span class="layer-name">{{ layer.name }}</span>
|
||||
<span class="layer-badge">{{ layer.badge }}</span>
|
||||
</div>
|
||||
<div class="layer-content">
|
||||
<div class="duty">{{ layer.duty }}</div>
|
||||
<div class="example">🌰 {{ layer.example }}</div>
|
||||
</div>
|
||||
<div v-if="index < backendLayers.length - 1" class="layer-arrow">
|
||||
<span class="arrow-icon">↓</span>
|
||||
<span class="arrow-text">{{ layer.arrow }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详情面板 -->
|
||||
<Transition name="slide">
|
||||
<div v-if="currentLayer" class="detail-panel">
|
||||
<div class="detail-header">
|
||||
<span class="detail-icon">{{ currentLayer.icon }}</span>
|
||||
<span class="detail-title">{{ currentLayer.name }}</span>
|
||||
</div>
|
||||
<div class="detail-content">
|
||||
<div class="detail-section">
|
||||
<div class="section-title">📁 典型文件</div>
|
||||
<div class="file-list">
|
||||
<code v-for="file in currentLayer.files" :key="file" class="file-tag">{{ file }}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<div class="section-title">✅ 设计原则</div>
|
||||
<ul class="principle-list">
|
||||
<li v-for="principle in currentLayer.principles" :key="principle">{{ principle }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>好的架构就像整理好的空间——前端像衣柜(按功能分类展示),后端像厨房(按流程分工协作)。点击上方层次查看详情!
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeType = ref('frontend')
|
||||
const activeLayer = ref(null)
|
||||
|
||||
const frontendLayers = [
|
||||
{
|
||||
id: 'views',
|
||||
name: 'Views / Pages',
|
||||
icon: '📄',
|
||||
badge: '页面层',
|
||||
class: 'views-layer',
|
||||
duty: '职责:页面组件,对应路由',
|
||||
example: 'Home.vue、UserProfile.vue',
|
||||
arrow: '组合',
|
||||
files: ['Home/index.vue', 'User/Profile.vue', 'pages/about.tsx'],
|
||||
principles: ['保持"薄",逻辑下沉到 hooks', '页面级状态管理', '路由懒加载']
|
||||
},
|
||||
{
|
||||
id: 'components',
|
||||
name: 'Components',
|
||||
icon: '🧩',
|
||||
badge: '组件层',
|
||||
class: 'components-layer',
|
||||
duty: '职责:可复用的 UI 组件',
|
||||
example: 'Button.vue、Modal.vue、UserCard.vue',
|
||||
arrow: '调用',
|
||||
files: ['common/Button/', 'business/UserCard/', 'layout/Header/'],
|
||||
principles: ['单一职责,一个组件只做一件事', 'Props 清晰可预测', '样式隔离(scoped/css-modules)']
|
||||
},
|
||||
{
|
||||
id: 'hooks',
|
||||
name: 'Hooks / Composables',
|
||||
icon: '🎣',
|
||||
badge: '逻辑层',
|
||||
class: 'hooks-layer',
|
||||
duty: '职责:可复用的业务逻辑',
|
||||
example: 'useAuth()、useLoading()、useForm()',
|
||||
arrow: '使用',
|
||||
files: ['useAuth.js', 'usePagination.ts', 'composables/useFetch.js'],
|
||||
principles: ['纯函数优先', '单一功能,便于测试', '命名以 use 开头']
|
||||
},
|
||||
{
|
||||
id: 'services',
|
||||
name: 'Services / API',
|
||||
icon: '🌐',
|
||||
badge: '服务层',
|
||||
class: 'services-layer',
|
||||
duty: '职责:API 调用,数据获取',
|
||||
example: 'userApi.getProfile()、orderApi.create()',
|
||||
arrow: '请求',
|
||||
files: ['services/user.js', 'api/request.ts', 'clients/http.js'],
|
||||
principles: ['统一错误处理', '请求/响应拦截', '接口统一管理']
|
||||
},
|
||||
{
|
||||
id: 'utils',
|
||||
name: 'Utils / Helpers',
|
||||
icon: '🛠️',
|
||||
badge: '工具层',
|
||||
class: 'utils-layer',
|
||||
duty: '职责:通用工具函数',
|
||||
example: 'formatDate()、storage.set()、validator.email()',
|
||||
arrow: '',
|
||||
files: ['utils/format.js', 'helpers/storage.ts', 'lib/validator.js'],
|
||||
principles: ['纯函数,无副作用', '单一职责', '完善的 JSDoc 注释']
|
||||
}
|
||||
]
|
||||
|
||||
const backendLayers = [
|
||||
{
|
||||
id: 'controller',
|
||||
name: 'Controller',
|
||||
icon: '🎮',
|
||||
badge: '入口层',
|
||||
class: 'controller-layer',
|
||||
duty: '职责:接收 HTTP 请求,返回响应',
|
||||
example: 'UserController.getById()、OrderController.create()',
|
||||
arrow: '调用',
|
||||
files: ['userController.js', 'routes/api.js', 'handlers/order.ts'],
|
||||
principles: ['只处理 HTTP 相关逻辑', '参数校验', '不直接操作数据库']
|
||||
},
|
||||
{
|
||||
id: 'service',
|
||||
name: 'Service',
|
||||
icon: '⚙️',
|
||||
badge: '业务层',
|
||||
class: 'service-layer',
|
||||
duty: '职责:核心业务逻辑,事务管理',
|
||||
example: 'UserService.createUser()、OrderService.process()',
|
||||
arrow: '调用',
|
||||
files: ['userService.js', 'services/order.ts', 'business/user.js'],
|
||||
principles: ['包含核心业务规则', '协调多个 Repository', '管理事务边界']
|
||||
},
|
||||
{
|
||||
id: 'repository',
|
||||
name: 'Repository',
|
||||
icon: '🗄️',
|
||||
badge: '数据层',
|
||||
class: 'repository-layer',
|
||||
duty: '职责:数据持久化,数据库操作',
|
||||
example: 'UserRepository.findById()、OrderRepository.save()',
|
||||
arrow: '查询',
|
||||
files: ['userRepository.js', 'dao/order.ts', 'models/user.js'],
|
||||
principles: ['只负责数据存取', 'ORM 封装', '不包含业务逻辑']
|
||||
},
|
||||
{
|
||||
id: 'model',
|
||||
name: 'Model / Entity',
|
||||
icon: '📊',
|
||||
badge: '模型层',
|
||||
class: 'model-layer',
|
||||
duty: '职责:数据结构和业务规则定义',
|
||||
example: 'User 类、Order 实体、DTO 定义',
|
||||
arrow: '',
|
||||
files: ['models/User.js', 'entities/order.ts', 'dto/userDto.js'],
|
||||
principles: ['定义数据结构', '字段验证规则', '与其他层解耦']
|
||||
}
|
||||
]
|
||||
|
||||
const currentLayer = computed(() => {
|
||||
const layers = activeType.value === 'frontend' ? frontendLayers : backendLayers
|
||||
return layers.find(l => l.id === activeLayer.value)
|
||||
})
|
||||
|
||||
function setActiveLayer(id) {
|
||||
activeLayer.value = activeLayer.value === id ? null : id
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.architecture-comparison-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
max-height: 700px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.demo-header .icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.demo-header .title {
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.demo-header .subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.85rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
/* 切换按钮 */
|
||||
.toggle-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.toggle-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* 架构层 */
|
||||
.architecture-layers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.layer-box {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
animation: fadeInUp 0.3s ease forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.layer-box:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.layer-box.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
/* 不同层的颜色 */
|
||||
.views-layer {
|
||||
border-left: 4px solid #3498db;
|
||||
}
|
||||
|
||||
.components-layer {
|
||||
border-left: 4px solid #2ecc71;
|
||||
}
|
||||
|
||||
.hooks-layer {
|
||||
border-left: 4px solid #9b59b6;
|
||||
}
|
||||
|
||||
.services-layer {
|
||||
border-left: 4px solid #e67e22;
|
||||
}
|
||||
|
||||
.utils-layer {
|
||||
border-left: 4px solid #95a5a6;
|
||||
}
|
||||
|
||||
.controller-layer {
|
||||
border-left: 4px solid #3498db;
|
||||
}
|
||||
|
||||
.service-layer {
|
||||
border-left: 4px solid #2ecc71;
|
||||
}
|
||||
|
||||
.repository-layer {
|
||||
border-left: 4px solid #e67e22;
|
||||
}
|
||||
|
||||
.model-layer {
|
||||
border-left: 4px solid #9b59b6;
|
||||
}
|
||||
|
||||
.layer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.layer-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.layer-name {
|
||||
font-weight: bold;
|
||||
font-size: 0.95rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.layer-badge {
|
||||
margin-left: auto;
|
||||
padding: 0.15rem 0.4rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.layer-content {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.duty {
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.example {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.layer-arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.25rem;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* 详情面板 */
|
||||
.detail-panel {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-enter-from,
|
||||
.slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.detail-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.detail-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.file-tag {
|
||||
padding: 0.2rem 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.principle-list {
|
||||
margin: 0;
|
||||
padding-left: 1.2rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.principle-list li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* 信息框 */
|
||||
.info-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-box .icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 640px) {
|
||||
.toggle-btn {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.layer-name {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.duty, .example {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,173 @@
|
||||
<!--
|
||||
BackpressureDemo.vue
|
||||
背压控制演示:展示生产者速度 > 消费者速度时的处理策略
|
||||
-->
|
||||
<template>
|
||||
<div class="backpressure-demo">
|
||||
<div class="header">
|
||||
<div class="title">背压控制 (Backpressure)</div>
|
||||
<div class="subtitle">当生产速度超过消费速度时会发生什么?</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="speed-control">
|
||||
<span class="ctrl-label">生产速率:</span>
|
||||
<input type="range" min="1" max="10" v-model.number="produceRate" />
|
||||
<span class="ctrl-value">{{ produceRate }}/s</span>
|
||||
</div>
|
||||
<div class="speed-control">
|
||||
<span class="ctrl-label">消费速率:</span>
|
||||
<input type="range" min="1" max="10" v-model.number="consumeRate" />
|
||||
<span class="ctrl-value">{{ consumeRate }}/s</span>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="ctrl-btn primary" @click="start" :disabled="running">开始</button>
|
||||
<button class="ctrl-btn" @click="stop">停止</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buffer-visual">
|
||||
<div class="producer-side">
|
||||
<div class="side-label">生产者</div>
|
||||
<div class="rate-indicator" :class="{ fast: produceRate > consumeRate }">
|
||||
{{ produceRate }}/s
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buffer-section">
|
||||
<div class="buffer-label">缓冲区 ({{ bufferSize }}/{{ maxBuffer }})</div>
|
||||
<div class="buffer-bar">
|
||||
<div
|
||||
class="buffer-fill"
|
||||
:style="{ width: (bufferSize / maxBuffer * 100) + '%' }"
|
||||
:class="bufferLevel"
|
||||
></div>
|
||||
</div>
|
||||
<div class="buffer-status" :class="bufferLevel">{{ statusText }}</div>
|
||||
</div>
|
||||
|
||||
<div class="consumer-side">
|
||||
<div class="side-label">消费者</div>
|
||||
<div class="rate-indicator">{{ consumeRate }}/s</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="strategies">
|
||||
<div class="strat-title">背压处理策略:</div>
|
||||
<div class="strat-grid">
|
||||
<div v-for="s in strategies" :key="s.name" class="strat-card">
|
||||
<div class="strat-name">{{ s.name }}</div>
|
||||
<div class="strat-desc">{{ s.desc }}</div>
|
||||
<div class="strat-example">{{ s.example }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
|
||||
const produceRate = ref(6)
|
||||
const consumeRate = ref(3)
|
||||
const bufferSize = ref(0)
|
||||
const maxBuffer = 20
|
||||
const running = ref(false)
|
||||
let timer = null
|
||||
|
||||
const bufferLevel = computed(() => {
|
||||
const ratio = bufferSize.value / maxBuffer
|
||||
if (ratio >= 0.9) return 'critical'
|
||||
if (ratio >= 0.6) return 'warning'
|
||||
return 'normal'
|
||||
})
|
||||
|
||||
const statusText = computed(() => {
|
||||
const ratio = bufferSize.value / maxBuffer
|
||||
if (ratio >= 1) return '缓冲区溢出!数据丢失'
|
||||
if (ratio >= 0.8) return '即将溢出,需要背压控制'
|
||||
if (ratio >= 0.5) return '缓冲区压力较大'
|
||||
return '正常运行'
|
||||
})
|
||||
|
||||
function start() {
|
||||
running.value = true
|
||||
timer = setInterval(() => {
|
||||
const produced = produceRate.value
|
||||
const consumed = consumeRate.value
|
||||
bufferSize.value = Math.max(0, Math.min(maxBuffer, bufferSize.value + produced - consumed))
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function stop() {
|
||||
running.value = false
|
||||
if (timer) clearInterval(timer)
|
||||
timer = null
|
||||
bufferSize.value = 0
|
||||
}
|
||||
|
||||
const strategies = [
|
||||
{ name: '丢弃策略', desc: '缓冲区满时直接丢弃新数据', example: '如:日志采集、实时监控指标' },
|
||||
{ name: '阻塞策略', desc: '缓冲区满时让生产者等待', example: '如:Go channel、Java BlockingQueue' },
|
||||
{ name: '采样策略', desc: '只处理部分数据,跳过其余', example: '如:高频传感器数据降采样' },
|
||||
{ name: '弹性扩容', desc: '动态增加消费者数量', example: '如:K8s HPA 自动扩缩容' }
|
||||
]
|
||||
|
||||
onUnmounted(() => { if (timer) clearInterval(timer) })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.backpressure-demo {
|
||||
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px; padding: 1.5rem; margin: 1.5rem 0;
|
||||
}
|
||||
.header { margin-bottom: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.controls { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; margin-bottom: 1.5rem; }
|
||||
.speed-control { display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; }
|
||||
.ctrl-label { font-weight: 600; min-width: 70px; }
|
||||
.ctrl-value { font-family: var(--vp-font-family-mono); min-width: 30px; }
|
||||
.btn-group { display: flex; gap: 0.5rem; }
|
||||
.ctrl-btn {
|
||||
padding: 0.4rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg); cursor: pointer; font-size: 0.85rem;
|
||||
}
|
||||
.ctrl-btn.primary { background: var(--vp-c-brand); color: #fff; border-color: var(--vp-c-brand); }
|
||||
.ctrl-btn:disabled { opacity: 0.5; }
|
||||
.buffer-visual { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem; }
|
||||
.producer-side, .consumer-side { text-align: center; min-width: 80px; }
|
||||
.side-label { font-weight: 600; font-size: 0.85rem; margin-bottom: 0.25rem; }
|
||||
.rate-indicator {
|
||||
padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.8rem;
|
||||
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
.rate-indicator.fast { border-color: #ef4444; color: #ef4444; }
|
||||
.buffer-section { flex: 1; }
|
||||
.buffer-label { font-size: 0.8rem; color: var(--vp-c-text-2); margin-bottom: 0.25rem; }
|
||||
.buffer-bar {
|
||||
height: 24px; background: var(--vp-c-bg); border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider); overflow: hidden;
|
||||
}
|
||||
.buffer-fill { height: 100%; border-radius: 5px; transition: width 0.5s, background 0.3s; }
|
||||
.buffer-fill.normal { background: #22c55e; }
|
||||
.buffer-fill.warning { background: #f59e0b; }
|
||||
.buffer-fill.critical { background: #ef4444; }
|
||||
.buffer-status { font-size: 0.8rem; margin-top: 0.25rem; }
|
||||
.buffer-status.normal { color: #22c55e; }
|
||||
.buffer-status.warning { color: #f59e0b; }
|
||||
.buffer-status.critical { color: #ef4444; }
|
||||
.strat-title { font-weight: 600; font-size: 0.9rem; margin-bottom: 0.5rem; }
|
||||
.strat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.5rem; }
|
||||
.strat-card {
|
||||
padding: 0.75rem; border-radius: 8px; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.strat-name { font-weight: 700; font-size: 0.85rem; margin-bottom: 0.25rem; }
|
||||
.strat-desc { font-size: 0.8rem; color: var(--vp-c-text-2); margin-bottom: 0.25rem; }
|
||||
.strat-example { font-size: 0.75rem; color: var(--vp-c-text-3); }
|
||||
@media (max-width: 640px) {
|
||||
.buffer-visual { flex-direction: column; }
|
||||
.strat-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,219 @@
|
||||
<!--
|
||||
RateLimitAlgorithmDemo.vue
|
||||
限流算法演示:令牌桶、漏桶、滑动窗口
|
||||
-->
|
||||
<template>
|
||||
<div class="rate-limit-demo">
|
||||
<div class="header">
|
||||
<div class="title">限流算法对比</div>
|
||||
<div class="subtitle">选择算法,点击"发送请求"观察效果</div>
|
||||
</div>
|
||||
|
||||
<div class="algo-tabs">
|
||||
<button
|
||||
v-for="a in algorithms"
|
||||
:key="a.key"
|
||||
:class="['tab', { active: algo === a.key }]"
|
||||
@click="algo = a.key; reset()"
|
||||
>{{ a.label }}</button>
|
||||
</div>
|
||||
|
||||
<div class="sim-area">
|
||||
<div class="controls">
|
||||
<button class="send-btn" @click="sendRequest">发送请求</button>
|
||||
<button class="burst-btn" @click="burstRequests">突发 10 个请求</button>
|
||||
<button class="reset-btn" @click="reset">重置</button>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<span class="stat-label">通过</span>
|
||||
<span class="stat-value ok">{{ passed }}</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">拒绝</span>
|
||||
<span class="stat-value reject">{{ rejected }}</span>
|
||||
</div>
|
||||
<div class="stat" v-if="algo === 'token'">
|
||||
<span class="stat-label">剩余令牌</span>
|
||||
<span class="stat-value">{{ tokens }}</span>
|
||||
</div>
|
||||
<div class="stat" v-if="algo === 'leaky'">
|
||||
<span class="stat-label">桶中排队</span>
|
||||
<span class="stat-value">{{ bucketQueue }}</span>
|
||||
</div>
|
||||
<div class="stat" v-if="algo === 'sliding'">
|
||||
<span class="stat-label">窗口内请求</span>
|
||||
<span class="stat-value">{{ windowCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="log-area">
|
||||
<div
|
||||
v-for="(log, i) in logs.slice(-8)"
|
||||
:key="i"
|
||||
:class="['log-item', log.status]"
|
||||
>
|
||||
<span class="log-time">{{ log.time }}</span>
|
||||
<span>{{ log.msg }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="algo-info">
|
||||
<div class="info-name">{{ currentAlgo.label }}</div>
|
||||
<div class="info-desc">{{ currentAlgo.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const algo = ref('token')
|
||||
const passed = ref(0)
|
||||
const rejected = ref(0)
|
||||
const tokens = ref(5)
|
||||
const bucketQueue = ref(0)
|
||||
const windowCount = ref(0)
|
||||
const logs = ref([])
|
||||
|
||||
const algorithms = [
|
||||
{ key: 'token', label: '令牌桶', desc: '以固定速率往桶里放令牌,每个请求消耗一个令牌。桶满时多余令牌丢弃。允许一定程度的突发流量(桶里有存量令牌时)。' },
|
||||
{ key: 'leaky', label: '漏桶', desc: '请求先进入桶中排队,以固定速率从桶底"漏出"处理。桶满时新请求被拒绝。输出速率恒定,完全平滑流量。' },
|
||||
{ key: 'sliding', label: '滑动窗口', desc: '统计最近 N 秒内的请求数,超过阈值则拒绝。比固定窗口更精确,避免窗口边界的突发问题。' }
|
||||
]
|
||||
|
||||
const currentAlgo = computed(() => algorithms.find(a => a.key === algo.value))
|
||||
|
||||
// Token bucket: refill 1 token per second, max 5
|
||||
let tokenTimer = null
|
||||
function startTokenRefill() {
|
||||
if (tokenTimer) clearInterval(tokenTimer)
|
||||
tokenTimer = setInterval(() => {
|
||||
if (tokens.value < 5) tokens.value++
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// Leaky bucket: drain 1 per second, max queue 5
|
||||
let leakyTimer = null
|
||||
function startLeakyDrain() {
|
||||
if (leakyTimer) clearInterval(leakyTimer)
|
||||
leakyTimer = setInterval(() => {
|
||||
if (bucketQueue.value > 0) {
|
||||
bucketQueue.value--
|
||||
passed.value++
|
||||
addLog('ok', '漏桶处理了一个排队请求')
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// Sliding window: max 5 per 3 seconds
|
||||
const windowRequests = ref([])
|
||||
|
||||
function reset() {
|
||||
passed.value = 0
|
||||
rejected.value = 0
|
||||
tokens.value = 5
|
||||
bucketQueue.value = 0
|
||||
windowCount.value = 0
|
||||
logs.value = []
|
||||
windowRequests.value = []
|
||||
if (tokenTimer) clearInterval(tokenTimer)
|
||||
if (leakyTimer) clearInterval(leakyTimer)
|
||||
if (algo.value === 'token') startTokenRefill()
|
||||
if (algo.value === 'leaky') startLeakyDrain()
|
||||
}
|
||||
|
||||
function addLog(status, msg) {
|
||||
const now = new Date()
|
||||
logs.value.push({ status, msg, time: now.toLocaleTimeString() })
|
||||
}
|
||||
|
||||
function sendRequest() {
|
||||
if (algo.value === 'token') {
|
||||
if (tokens.value > 0) {
|
||||
tokens.value--
|
||||
passed.value++
|
||||
addLog('ok', `请求通过(剩余令牌: ${tokens.value})`)
|
||||
} else {
|
||||
rejected.value++
|
||||
addLog('reject', '令牌不足,请求被拒绝 (429)')
|
||||
}
|
||||
if (!tokenTimer) startTokenRefill()
|
||||
} else if (algo.value === 'leaky') {
|
||||
if (bucketQueue.value < 5) {
|
||||
bucketQueue.value++
|
||||
addLog('ok', `请求进入排队(队列: ${bucketQueue.value}/5)`)
|
||||
} else {
|
||||
rejected.value++
|
||||
addLog('reject', '桶已满,请求被拒绝 (429)')
|
||||
}
|
||||
if (!leakyTimer) startLeakyDrain()
|
||||
} else {
|
||||
const now = Date.now()
|
||||
windowRequests.value = windowRequests.value.filter(t => now - t < 3000)
|
||||
windowCount.value = windowRequests.value.length
|
||||
if (windowCount.value < 5) {
|
||||
windowRequests.value.push(now)
|
||||
windowCount.value++
|
||||
passed.value++
|
||||
addLog('ok', `请求通过(窗口内: ${windowCount.value}/5)`)
|
||||
} else {
|
||||
rejected.value++
|
||||
addLog('reject', '窗口内请求数超限 (429)')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function burstRequests() {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
setTimeout(() => sendRequest(), i * 80)
|
||||
}
|
||||
}
|
||||
|
||||
reset()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rate-limit-demo {
|
||||
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px; padding: 1.5rem; margin: 1.5rem 0;
|
||||
}
|
||||
.header { margin-bottom: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.algo-tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
|
||||
.tab {
|
||||
padding: 0.4rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg); cursor: pointer; font-size: 0.85rem;
|
||||
}
|
||||
.tab.active { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
|
||||
.controls { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; }
|
||||
.send-btn, .burst-btn, .reset-btn {
|
||||
padding: 0.4rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg); cursor: pointer; font-size: 0.85rem;
|
||||
}
|
||||
.send-btn { background: var(--vp-c-brand); color: #fff; border-color: var(--vp-c-brand); }
|
||||
.burst-btn { background: #f59e0b; color: #fff; border-color: #f59e0b; }
|
||||
.stats { display: flex; gap: 1rem; margin-bottom: 0.75rem; }
|
||||
.stat { display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; }
|
||||
.stat-label { color: var(--vp-c-text-3); }
|
||||
.stat-value { font-weight: 700; font-family: var(--vp-font-family-mono); }
|
||||
.stat-value.ok { color: #22c55e; }
|
||||
.stat-value.reject { color: #ef4444; }
|
||||
.log-area { max-height: 180px; overflow-y: auto; display: flex; flex-direction: column; gap: 0.25rem; }
|
||||
.log-item {
|
||||
padding: 0.3rem 0.5rem; border-radius: 4px; font-size: 0.8rem;
|
||||
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.log-item.ok { border-color: rgba(34,197,94,0.3); }
|
||||
.log-item.reject { border-color: rgba(239,68,68,0.3); background: rgba(239,68,68,0.03); }
|
||||
.log-time { color: var(--vp-c-text-3); margin-right: 0.5rem; font-family: var(--vp-font-family-mono); }
|
||||
.algo-info {
|
||||
margin-top: 1rem; padding: 0.75rem; border-radius: 8px;
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.05); border: 1px solid var(--vp-c-brand);
|
||||
}
|
||||
.info-name { font-weight: 700; font-size: 0.9rem; margin-bottom: 0.25rem; }
|
||||
.info-desc { font-size: 0.85rem; color: var(--vp-c-text-2); line-height: 1.6; }
|
||||
</style>
|
||||
@@ -0,0 +1,204 @@
|
||||
<!--
|
||||
RateLimiterDemo.vue
|
||||
限流算法演示:令牌桶 vs 滑动窗口
|
||||
-->
|
||||
<template>
|
||||
<div class="rate-limiter-demo">
|
||||
<div class="header">
|
||||
<div class="title">限流算法可视化</div>
|
||||
<div class="subtitle">选择算法,点击发送请求观察限流效果</div>
|
||||
</div>
|
||||
|
||||
<div class="algo-tabs">
|
||||
<button
|
||||
v-for="a in algorithms"
|
||||
:key="a.key"
|
||||
:class="['tab', { active: algo === a.key }]"
|
||||
@click="algo = a.key; reset()"
|
||||
>{{ a.label }}</button>
|
||||
</div>
|
||||
|
||||
<div class="sim-area">
|
||||
<div class="controls">
|
||||
<button class="send-btn" @click="sendRequest">发送请求</button>
|
||||
<button class="burst-btn" @click="sendBurst">模拟突发 (10个)</button>
|
||||
<button class="reset-btn" @click="reset">重置</button>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<span class="stat-label">已发送</span>
|
||||
<span class="stat-value">{{ totalSent }}</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">通过</span>
|
||||
<span class="stat-value pass">{{ passed }}</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">拒绝</span>
|
||||
<span class="stat-value reject">{{ rejected }}</span>
|
||||
</div>
|
||||
<div class="stat" v-if="algo === 'token'">
|
||||
<span class="stat-label">剩余令牌</span>
|
||||
<span class="stat-value">{{ tokens }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="request-log">
|
||||
<div
|
||||
v-for="(req, i) in recentRequests"
|
||||
:key="i"
|
||||
:class="['req-item', req.status]"
|
||||
>
|
||||
<span>{{ req.status === 'pass' ? '✅' : '❌' }}</span>
|
||||
<span>请求 #{{ req.id }}</span>
|
||||
<span class="req-time">{{ req.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="algo-info">
|
||||
<div class="info-name">{{ currentAlgo.label }}</div>
|
||||
<div class="info-desc">{{ currentAlgo.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const algo = ref('token')
|
||||
const totalSent = ref(0)
|
||||
const passed = ref(0)
|
||||
const rejected = ref(0)
|
||||
const tokens = ref(5)
|
||||
const recentRequests = ref([])
|
||||
|
||||
const algorithms = [
|
||||
{ key: 'token', label: '令牌桶', desc: '以固定速率往桶里放令牌,每个请求消耗一个令牌。桶满时多余令牌丢弃。允许一定程度的突发流量(桶里有存量令牌时)。' },
|
||||
{ key: 'sliding', label: '滑动窗口', desc: '在一个滑动的时间窗口内统计请求数,超过阈值则拒绝。比固定窗口更平滑,避免窗口边界的突发问题。' },
|
||||
{ key: 'leaky', label: '漏桶', desc: '请求先进入桶中排队,以固定速率流出处理。无论请求多快到达,处理速率恒定。适合需要严格匀速的场景。' }
|
||||
]
|
||||
|
||||
const currentAlgo = computed(() => algorithms.find(a => a.key === algo.value))
|
||||
|
||||
// Sliding window state
|
||||
const windowRequests = ref([])
|
||||
const WINDOW_SIZE = 3000 // 3s window
|
||||
const WINDOW_LIMIT = 5
|
||||
|
||||
// Token bucket refill
|
||||
let tokenInterval = null
|
||||
|
||||
function startTokenRefill() {
|
||||
if (tokenInterval) clearInterval(tokenInterval)
|
||||
tokenInterval = setInterval(() => {
|
||||
if (tokens.value < 5) tokens.value++
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function reset() {
|
||||
totalSent.value = 0
|
||||
passed.value = 0
|
||||
rejected.value = 0
|
||||
tokens.value = 5
|
||||
recentRequests.value = []
|
||||
windowRequests.value = []
|
||||
if (tokenInterval) clearInterval(tokenInterval)
|
||||
startTokenRefill()
|
||||
}
|
||||
|
||||
function checkLimit() {
|
||||
if (algo.value === 'token') {
|
||||
if (tokens.value > 0) { tokens.value--; return true }
|
||||
return false
|
||||
}
|
||||
if (algo.value === 'sliding') {
|
||||
const now = Date.now()
|
||||
windowRequests.value = windowRequests.value.filter(t => now - t < WINDOW_SIZE)
|
||||
if (windowRequests.value.length < WINDOW_LIMIT) {
|
||||
windowRequests.value.push(now)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
// leaky bucket: simple counter-based
|
||||
if (algo.value === 'leaky') {
|
||||
const now = Date.now()
|
||||
windowRequests.value = windowRequests.value.filter(t => now - t < 2000)
|
||||
if (windowRequests.value.length < 3) {
|
||||
windowRequests.value.push(now)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function sendRequest() {
|
||||
totalSent.value++
|
||||
const allowed = checkLimit()
|
||||
if (allowed) passed.value++
|
||||
else rejected.value++
|
||||
|
||||
const now = new Date()
|
||||
recentRequests.value.unshift({
|
||||
id: totalSent.value,
|
||||
status: allowed ? 'pass' : 'reject',
|
||||
time: `${now.getHours()}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`
|
||||
})
|
||||
if (recentRequests.value.length > 10) recentRequests.value.pop()
|
||||
}
|
||||
|
||||
async function sendBurst() {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
sendRequest()
|
||||
await new Promise(r => setTimeout(r, 100))
|
||||
}
|
||||
}
|
||||
|
||||
startTokenRefill()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rate-limiter-demo {
|
||||
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px; padding: 1.5rem; margin: 1.5rem 0;
|
||||
}
|
||||
.header { margin-bottom: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.algo-tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
|
||||
.tab {
|
||||
padding: 0.4rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg); cursor: pointer; font-size: 0.85rem;
|
||||
}
|
||||
.tab.active { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
|
||||
.controls { display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; }
|
||||
.send-btn, .burst-btn, .reset-btn {
|
||||
padding: 0.4rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg); cursor: pointer; font-size: 0.85rem;
|
||||
}
|
||||
.send-btn { background: var(--vp-c-brand); color: #fff; border-color: var(--vp-c-brand); }
|
||||
.burst-btn { background: #f59e0b; color: #fff; border-color: #f59e0b; }
|
||||
.stats { display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; }
|
||||
.stat { display: flex; flex-direction: column; align-items: center; }
|
||||
.stat-label { font-size: 0.75rem; color: var(--vp-c-text-3); }
|
||||
.stat-value { font-weight: 700; font-size: 1.2rem; font-family: var(--vp-font-family-mono); }
|
||||
.stat-value.pass { color: #22c55e; }
|
||||
.stat-value.reject { color: #ef4444; }
|
||||
.request-log { display: flex; flex-direction: column; gap: 0.25rem; max-height: 200px; overflow-y: auto; margin-bottom: 1rem; }
|
||||
.req-item {
|
||||
display: flex; gap: 0.5rem; padding: 0.3rem 0.5rem; border-radius: 4px;
|
||||
font-size: 0.8rem; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.req-item.reject { border-color: rgba(239,68,68,0.3); background: rgba(239,68,68,0.03); }
|
||||
.req-item.pass { border-color: rgba(34,197,94,0.3); background: rgba(34,197,94,0.03); }
|
||||
.req-time { margin-left: auto; color: var(--vp-c-text-3); font-family: var(--vp-font-family-mono); }
|
||||
.algo-info {
|
||||
padding: 0.75rem; border-radius: 8px;
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.05); border: 1px solid var(--vp-c-brand);
|
||||
}
|
||||
.info-name { font-weight: 700; font-size: 0.9rem; margin-bottom: 0.25rem; }
|
||||
.info-desc { font-size: 0.85rem; color: var(--vp-c-text-2); line-height: 1.6; }
|
||||
</style>
|
||||
@@ -0,0 +1,145 @@
|
||||
<!--
|
||||
InvertedIndexDemo.vue
|
||||
倒排索引演示:展示搜索引擎的核心数据结构
|
||||
-->
|
||||
<template>
|
||||
<div class="inverted-index-demo">
|
||||
<div class="header">
|
||||
<div class="title">倒排索引 (Inverted Index)</div>
|
||||
<div class="subtitle">输入搜索词,观察倒排索引如何工作</div>
|
||||
</div>
|
||||
|
||||
<div class="search-box">
|
||||
<input
|
||||
v-model="query"
|
||||
placeholder="试试搜索:苹果、手机、水果..."
|
||||
class="search-input"
|
||||
@input="search"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="index-layout">
|
||||
<div class="docs-section">
|
||||
<div class="section-title">原始文档</div>
|
||||
<div
|
||||
v-for="doc in docs"
|
||||
:key="doc.id"
|
||||
:class="['doc-card', { highlight: matchedDocs.includes(doc.id) }]"
|
||||
>
|
||||
<span class="doc-id">Doc {{ doc.id }}</span>
|
||||
<span class="doc-text">{{ doc.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="index-section">
|
||||
<div class="section-title">倒排索引表</div>
|
||||
<div class="index-table">
|
||||
<div
|
||||
v-for="(entry, word) in invertedIndex"
|
||||
:key="word"
|
||||
:class="['index-row', { highlight: matchedWords.includes(word) }]"
|
||||
>
|
||||
<span class="index-word">{{ word }}</span>
|
||||
<span class="index-arrow">→</span>
|
||||
<span class="index-docs">
|
||||
<span v-for="id in entry" :key="id" class="doc-ref">[{{ id }}]</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="query && matchedDocs.length > 0" class="result">
|
||||
命中文档:{{ matchedDocs.map(id => 'Doc ' + id).join('、') }}
|
||||
</div>
|
||||
<div v-else-if="query" class="result no-match">
|
||||
未找到匹配文档
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const query = ref('')
|
||||
const matchedDocs = ref([])
|
||||
const matchedWords = ref([])
|
||||
|
||||
const docs = [
|
||||
{ id: 1, text: '苹果是一种常见的水果' },
|
||||
{ id: 2, text: '苹果公司发布了新款手机' },
|
||||
{ id: 3, text: '我喜欢吃水果和蔬菜' },
|
||||
{ id: 4, text: '这款手机的价格很实惠' },
|
||||
{ id: 5, text: '水果店里有苹果和香蕉' }
|
||||
]
|
||||
|
||||
const invertedIndex = {
|
||||
'苹果': [1, 2, 5],
|
||||
'水果': [1, 3, 5],
|
||||
'手机': [2, 4],
|
||||
'公司': [2],
|
||||
'发布': [2],
|
||||
'喜欢': [3],
|
||||
'蔬菜': [3],
|
||||
'价格': [4],
|
||||
'实惠': [4],
|
||||
'香蕉': [5],
|
||||
'常见': [1]
|
||||
}
|
||||
|
||||
function search() {
|
||||
const q = query.value.trim()
|
||||
if (!q) {
|
||||
matchedDocs.value = []
|
||||
matchedWords.value = []
|
||||
return
|
||||
}
|
||||
const words = Object.keys(invertedIndex).filter(w => q.includes(w))
|
||||
matchedWords.value = words
|
||||
const docSet = new Set()
|
||||
words.forEach(w => invertedIndex[w].forEach(id => docSet.add(id)))
|
||||
matchedDocs.value = [...docSet].sort()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.inverted-index-demo {
|
||||
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px; padding: 1.5rem; margin: 1.5rem 0;
|
||||
}
|
||||
.header { margin-bottom: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.search-box { margin-bottom: 1rem; }
|
||||
.search-input {
|
||||
width: 100%; padding: 0.6rem 0.75rem; border-radius: 8px;
|
||||
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg);
|
||||
font-size: 0.9rem; outline: none;
|
||||
}
|
||||
.search-input:focus { border-color: var(--vp-c-brand); }
|
||||
.index-layout { display: flex; gap: 1rem; margin-bottom: 1rem; }
|
||||
.docs-section, .index-section { flex: 1; }
|
||||
.section-title { font-weight: 600; font-size: 0.9rem; margin-bottom: 0.5rem; }
|
||||
.doc-card {
|
||||
display: flex; gap: 0.5rem; padding: 0.4rem 0.6rem; margin-bottom: 0.25rem;
|
||||
border-radius: 6px; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.8rem; transition: all 0.2s;
|
||||
}
|
||||
.doc-card.highlight { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); }
|
||||
.doc-id { font-weight: 700; color: var(--vp-c-brand); white-space: nowrap; }
|
||||
.index-row {
|
||||
display: flex; align-items: center; gap: 0.5rem; padding: 0.3rem 0.5rem;
|
||||
margin-bottom: 0.2rem; border-radius: 4px; font-size: 0.8rem;
|
||||
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.index-row.highlight { border-color: #22c55e; background: rgba(34,197,94,0.05); }
|
||||
.index-word { font-weight: 700; min-width: 40px; }
|
||||
.index-arrow { color: var(--vp-c-text-3); }
|
||||
.doc-ref {
|
||||
padding: 0.1rem 0.3rem; background: var(--vp-c-bg-soft); border-radius: 3px;
|
||||
font-family: var(--vp-font-family-mono); font-size: 0.75rem; margin-right: 0.2rem;
|
||||
}
|
||||
.result { padding: 0.5rem 0.75rem; border-radius: 6px; font-size: 0.85rem; background: rgba(34,197,94,0.08); border: 1px solid rgba(34,197,94,0.3); }
|
||||
.result.no-match { background: rgba(245,158,11,0.08); border-color: rgba(245,158,11,0.3); }
|
||||
@media (max-width: 640px) { .index-layout { flex-direction: column; } }
|
||||
</style>
|
||||
@@ -0,0 +1,126 @@
|
||||
<!--
|
||||
SearchRelevanceDemo.vue
|
||||
搜索相关性评分演示:展示 TF-IDF 和 BM25 评分原理
|
||||
-->
|
||||
<template>
|
||||
<div class="relevance-demo">
|
||||
<div class="header">
|
||||
<div class="title">搜索相关性评分</div>
|
||||
<div class="subtitle">输入查询词,观察不同文档的相关性得分</div>
|
||||
</div>
|
||||
|
||||
<div class="search-box">
|
||||
<input v-model="query" placeholder="输入搜索词,如:数据库" class="search-input" />
|
||||
<button class="search-btn" @click="calcScores">计算得分</button>
|
||||
</div>
|
||||
|
||||
<div v-if="results.length > 0" class="results">
|
||||
<div
|
||||
v-for="(r, i) in results"
|
||||
:key="i"
|
||||
class="result-item"
|
||||
>
|
||||
<div class="result-rank">#{{ i + 1 }}</div>
|
||||
<div class="result-content">
|
||||
<div class="result-title">{{ r.title }}</div>
|
||||
<div class="result-snippet">{{ r.snippet }}</div>
|
||||
</div>
|
||||
<div class="result-score">
|
||||
<div class="score-bar">
|
||||
<div class="score-fill" :style="{ width: r.scorePercent + '%' }"></div>
|
||||
</div>
|
||||
<div class="score-value">{{ r.score.toFixed(2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scoring-info">
|
||||
<div class="info-title">BM25 评分因子</div>
|
||||
<div class="factor-grid">
|
||||
<div class="factor">
|
||||
<div class="factor-name">词频 (TF)</div>
|
||||
<div class="factor-desc">关键词在文档中出现的次数越多,得分越高(但有上限)</div>
|
||||
</div>
|
||||
<div class="factor">
|
||||
<div class="factor-name">逆文档频率 (IDF)</div>
|
||||
<div class="factor-desc">越稀有的词权重越高,"的"这种常见词权重很低</div>
|
||||
</div>
|
||||
<div class="factor">
|
||||
<div class="factor-name">文档长度</div>
|
||||
<div class="factor-desc">较短文档中出现关键词,比长文档中出现更有意义</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const query = ref('')
|
||||
const results = ref([])
|
||||
|
||||
const documents = [
|
||||
{ title: 'MySQL 数据库入门', snippet: '数据库是存储和管理数据的系统,MySQL 是最流行的关系型数据库之一', keywords: { '数据库': 3, '数据': 2, 'MySQL': 2, '存储': 1 } },
|
||||
{ title: 'Redis 缓存设计', snippet: 'Redis 是内存数据库,常用作缓存层,提升数据读取性能', keywords: { 'Redis': 2, '缓存': 2, '数据库': 1, '数据': 1, '性能': 1 } },
|
||||
{ title: 'Python 数据分析', snippet: '使用 Python 进行数据清洗、分析和可视化', keywords: { 'Python': 2, '数据': 3, '分析': 2, '可视化': 1 } },
|
||||
{ title: '分布式数据库架构', snippet: '分布式数据库通过分片和复制实现高可用和水平扩展', keywords: { '分布式': 2, '数据库': 2, '分片': 1, '高可用': 1 } },
|
||||
{ title: 'API 接口设计', snippet: 'RESTful API 设计规范与最佳实践', keywords: { 'API': 3, '设计': 2, 'RESTful': 1 } }
|
||||
]
|
||||
|
||||
function calcScores() {
|
||||
if (!query.value.trim()) { results.value = []; return }
|
||||
const q = query.value.trim()
|
||||
const scored = documents.map(doc => {
|
||||
let score = 0
|
||||
for (const [word, tf] of Object.entries(doc.keywords)) {
|
||||
if (word.includes(q) || q.includes(word)) {
|
||||
const idf = Math.log(documents.length / (1 + documents.filter(d => d.keywords[word]).length))
|
||||
score += tf * (idf + 1)
|
||||
}
|
||||
}
|
||||
return { ...doc, score }
|
||||
}).filter(d => d.score > 0).sort((a, b) => b.score - a.score)
|
||||
|
||||
const maxScore = scored.length > 0 ? scored[0].score : 1
|
||||
results.value = scored.map(r => ({ ...r, scorePercent: (r.score / maxScore) * 100 }))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.relevance-demo {
|
||||
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px; padding: 1.5rem; margin: 1.5rem 0;
|
||||
}
|
||||
.header { margin-bottom: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.search-box { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
|
||||
.search-input {
|
||||
flex: 1; padding: 0.5rem 0.75rem; border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg); font-size: 0.9rem;
|
||||
}
|
||||
.search-btn {
|
||||
padding: 0.5rem 1rem; border-radius: 6px; border: none;
|
||||
background: var(--vp-c-brand); color: #fff; cursor: pointer; font-size: 0.85rem;
|
||||
}
|
||||
.results { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; }
|
||||
.result-item {
|
||||
display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem;
|
||||
border-radius: 8px; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.result-rank { font-weight: 700; font-size: 1rem; color: var(--vp-c-brand); min-width: 30px; }
|
||||
.result-content { flex: 1; }
|
||||
.result-title { font-weight: 600; font-size: 0.9rem; }
|
||||
.result-snippet { font-size: 0.8rem; color: var(--vp-c-text-2); }
|
||||
.result-score { min-width: 120px; }
|
||||
.score-bar { height: 8px; background: var(--vp-c-bg-soft); border-radius: 4px; overflow: hidden; }
|
||||
.score-fill { height: 100%; background: var(--vp-c-brand); border-radius: 4px; transition: width 0.3s; }
|
||||
.score-value { font-size: 0.75rem; color: var(--vp-c-text-3); text-align: right; font-family: var(--vp-font-family-mono); }
|
||||
.scoring-info { padding: 0.75rem; border-radius: 8px; background: rgba(var(--vp-c-brand-rgb),0.05); border: 1px solid var(--vp-c-brand); }
|
||||
.info-title { font-weight: 700; font-size: 0.9rem; margin-bottom: 0.5rem; }
|
||||
.factor-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.5rem; }
|
||||
.factor { padding: 0.5rem; background: var(--vp-c-bg); border-radius: 6px; }
|
||||
.factor-name { font-weight: 600; font-size: 0.85rem; margin-bottom: 0.2rem; }
|
||||
.factor-desc { font-size: 0.75rem; color: var(--vp-c-text-2); line-height: 1.5; }
|
||||
</style>
|
||||
@@ -206,7 +206,11 @@ import LearningStrategyDemo from './components/appendix/computer-fundamentals/Le
|
||||
import VibeCodingFlowDemo from './components/appendix/computer-fundamentals/VibeCodingFlowDemo.vue'
|
||||
import PowerOnDemo from './components/appendix/computer-fundamentals/PowerOnDemo.vue'
|
||||
import BootProcessDemo from './components/appendix/computer-fundamentals/BootProcessDemo.vue'
|
||||
import BiosUefiDemo from './components/appendix/computer-fundamentals/BiosUefiDemo.vue'
|
||||
import BiosUefiInteractiveDemo from './components/appendix/computer-fundamentals/BiosUefiInteractiveDemo.vue'
|
||||
import AppLaunchDemo from './components/appendix/computer-fundamentals/AppLaunchDemo.vue'
|
||||
import DesktopDemo from './components/appendix/computer-fundamentals/DesktopDemo.vue'
|
||||
import OSBootInteractiveDemo from './components/appendix/computer-fundamentals/OSBootInteractiveDemo.vue'
|
||||
import BrowserArchitectureDemo from './components/appendix/computer-fundamentals/BrowserArchitectureDemo.vue'
|
||||
import URLRequestDemo from './components/appendix/computer-fundamentals/URLRequestDemo.vue'
|
||||
import RenderingDemo from './components/appendix/computer-fundamentals/RenderingDemo.vue'
|
||||
@@ -758,6 +762,28 @@ import IncidentCommandDemo from './components/appendix/incident-response/Inciden
|
||||
import AlertEscalationDemo from './components/appendix/incident-response/AlertEscalationDemo.vue'
|
||||
import PostmortemDemo from './components/appendix/incident-response/PostmortemDemo.vue'
|
||||
|
||||
// Async Task Queues Components
|
||||
import AsyncTaskFlowDemo from './components/appendix/async-task-queues/AsyncTaskFlowDemo.vue'
|
||||
import TaskWorkerDemo from './components/appendix/async-task-queues/TaskWorkerDemo.vue'
|
||||
import TaskRetryDemo from './components/appendix/async-task-queues/TaskRetryDemo.vue'
|
||||
import AsyncComparisonDemo from './components/appendix/async-task-queues/AsyncComparisonDemo.vue'
|
||||
|
||||
// File Storage Components
|
||||
import FileStorageTypeDemo from './components/appendix/file-storage/FileStorageTypeDemo.vue'
|
||||
import FileUploadFlowDemo from './components/appendix/file-storage/FileUploadFlowDemo.vue'
|
||||
import CDNAccelerationDemo from './components/appendix/file-storage/CDNAccelerationDemo.vue'
|
||||
|
||||
// Rate Limiting Components
|
||||
import RateLimitAlgorithmDemo from './components/appendix/rate-limiting/RateLimitAlgorithmDemo.vue'
|
||||
import BackpressureDemo from './components/appendix/rate-limiting/BackpressureDemo.vue'
|
||||
|
||||
// Search Engines Components Registration
|
||||
import InvertedIndexDemo from './components/appendix/search-engines/InvertedIndexDemo.vue'
|
||||
import SearchRelevanceDemo from './components/appendix/search-engines/SearchRelevanceDemo.vue'
|
||||
|
||||
// Project Architecture Components
|
||||
import ArchitectureComparisonDemo from './components/appendix/project-architecture/ArchitectureComparisonDemo.vue'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
Layout,
|
||||
@@ -969,7 +995,11 @@ export default {
|
||||
app.component('VibeCodingFlowDemo', VibeCodingFlowDemo)
|
||||
app.component('PowerOnDemo', PowerOnDemo)
|
||||
app.component('BootProcessDemo', BootProcessDemo)
|
||||
app.component('BiosUefiDemo', BiosUefiDemo)
|
||||
app.component('BiosUefiInteractiveDemo', BiosUefiInteractiveDemo)
|
||||
app.component('AppLaunchDemo', AppLaunchDemo)
|
||||
app.component('DesktopDemo', DesktopDemo)
|
||||
app.component('OSBootInteractiveDemo', OSBootInteractiveDemo)
|
||||
app.component('BrowserArchitectureDemo', BrowserArchitectureDemo)
|
||||
app.component('URLRequestDemo', URLRequestDemo)
|
||||
app.component('RenderingDemo', RenderingDemo)
|
||||
@@ -1533,6 +1563,28 @@ export default {
|
||||
app.component('IncidentCommandDemo', IncidentCommandDemo)
|
||||
app.component('AlertEscalationDemo', AlertEscalationDemo)
|
||||
app.component('PostmortemDemo', PostmortemDemo)
|
||||
|
||||
// Async Task Queues Components Registration
|
||||
app.component('AsyncTaskFlowDemo', AsyncTaskFlowDemo)
|
||||
app.component('TaskWorkerDemo', TaskWorkerDemo)
|
||||
app.component('TaskRetryDemo', TaskRetryDemo)
|
||||
app.component('AsyncComparisonDemo', AsyncComparisonDemo)
|
||||
|
||||
// File Storage Components Registration
|
||||
app.component('FileStorageTypeDemo', FileStorageTypeDemo)
|
||||
app.component('FileUploadFlowDemo', FileUploadFlowDemo)
|
||||
app.component('CDNAccelerationDemo', CDNAccelerationDemo)
|
||||
|
||||
// Rate Limiting Components Registration
|
||||
app.component('RateLimitAlgorithmDemo', RateLimitAlgorithmDemo)
|
||||
app.component('BackpressureDemo', BackpressureDemo)
|
||||
|
||||
// Search Engines Components Registration
|
||||
app.component('InvertedIndexDemo', InvertedIndexDemo)
|
||||
app.component('SearchRelevanceDemo', SearchRelevanceDemo)
|
||||
|
||||
// Project Architecture Components Registration
|
||||
app.component('ArchitectureComparisonDemo', ArchitectureComparisonDemo)
|
||||
},
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
|
||||
Reference in New Issue
Block a user