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:
@@ -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>
|
||||
Reference in New Issue
Block a user