Files
test-repo/docs/.vitepress/theme/components/appendix/engineering-excellence/PatternPlaygroundDemo.vue
T
sanbuphy f35cddeb8b feat(appendix): 重构工程实践章节,添加交互式演示组件
## 新增组件 (14个)
- CodeSmellDemo.vue: 代码异味识别演示
- DecisionMatrixDemo.vue: 决策矩阵工具
- DesignPatternCatalogDemo.vue: 设计模式目录
- DocStructureDemo.vue: 文档结构示例
- LicenseComparisonDemo.vue: 开源许可证对比
- OpenSourceWorkflowDemo.vue: 开源协作流程
- PatternPlaygroundDemo.vue: 设计模式演练场
- RefactoringDemo.vue: 重构实战演示
- SecurityChecklistDemo.vue: 安全检查清单
- TDDCycleDemo.vue: TDD 循环演示
- TechRadarDemo.vue: 技术雷达图
- TechWritingPracticeDemo.vue: 技术写作实践
- TestPyramidDemo.vue: 测试金字塔
- WebSecurityDemo.vue: Web 安全演示

## 文档更新 (7篇)
- code-quality-refactoring.md: 代码质量与重构
- design-patterns.md: 设计模式
- open-source-collaboration.md: 开源协作
- security-thinking.md: 安全思维
- technical-writing.md: 技术写作
- technology-selection.md: 技术选型
- testing-strategies.md: 测试策略

## 其他变更
- 将 browser-as-os.md 内容合并到 computer-networks.md
- 更新 .gitignore 和 theme/index.js
2026-02-24 13:03:21 +08:00

477 lines
12 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="pattern-playground">
<div class="demo-header">
<span class="icon">🎮</span>
<span class="title">设计模式演练场</span>
<span class="subtitle">选择模式动手体验</span>
</div>
<div class="mode-tabs">
<button
v-for="m in modes"
:key="m.key"
:class="['mode-btn', { active: activeMode === m.key }]"
@click="activeMode = m.key; resetState()"
>
{{ m.icon }} {{ m.name }}
</button>
</div>
<!-- 观察者模式演练 -->
<div v-if="activeMode === 'observer'" class="playground-area">
<div class="playground-desc">
模拟事件发布/订阅添加订阅者发布事件观察通知如何传播
</div>
<div class="observer-layout">
<div class="publisher-panel">
<div class="panel-title">📡 发布者 (Publisher)</div>
<div class="event-input">
<input
v-model="eventMessage"
placeholder="输入事件消息..."
@keyup.enter="publishEvent"
/>
<button class="action-btn publish" @click="publishEvent">发布事件</button>
</div>
</div>
<div class="subscribers-panel">
<div class="panel-title">
👥 订阅者
<button class="action-btn add" @click="addSubscriber">+ 添加</button>
</div>
<div v-if="subscribers.length === 0" class="empty-hint">暂无订阅者点击"添加"按钮</div>
<div
v-for="sub in subscribers"
:key="sub.id"
:class="['subscriber-card', { notified: sub.notified }]"
>
<span class="sub-name">{{ sub.name }}</span>
<span v-if="sub.lastMsg" class="sub-msg">收到: {{ sub.lastMsg }}</span>
<button class="remove-btn" @click="removeSubscriber(sub.id)">移除</button>
</div>
</div>
</div>
<div v-if="observerLog.length" class="event-log">
<div class="log-title">📋 事件日志</div>
<div v-for="(log, i) in observerLog" :key="i" class="log-item">{{ log }}</div>
</div>
</div>
<!-- 策略模式演练 -->
<div v-if="activeMode === 'strategy'" class="playground-area">
<div class="playground-desc">
选择不同的排序策略观察同一组数据如何被不同算法处理
</div>
<div class="strategy-layout">
<div class="data-panel">
<div class="panel-title">📊 待排序数据</div>
<div class="data-bars">
<div
v-for="(v, i) in strategyData"
:key="i"
class="bar"
:style="{ height: v * 3 + 'px', background: barColor(i) }"
>
<span class="bar-label">{{ v }}</span>
</div>
</div>
<button class="action-btn" @click="shuffleData" style="margin-top: 10px">🔀 打乱数据</button>
</div>
<div class="strategy-panel">
<div class="panel-title"> 选择策略</div>
<div class="strategy-options">
<button
v-for="s in sortStrategies"
:key="s.key"
:class="['strategy-btn', { active: activeStrategy === s.key }]"
@click="activeStrategy = s.key"
>
{{ s.name }}
<span class="strategy-complexity">{{ s.complexity }}</span>
</button>
</div>
<button class="action-btn publish" @click="executeSort" :disabled="sorting">
{{ sorting ? '排序中...' : '▶ 执行排序' }}
</button>
<div v-if="sortSteps.length" class="steps-info">
步骤数: {{ sortSteps.length }} | 策略: {{ sortStrategies.find(s => s.key === activeStrategy)?.name }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeMode = ref('observer')
const modes = [
{ key: 'observer', name: '观察者模式', icon: '📡' },
{ key: 'strategy', name: '策略模式', icon: '⚙️' }
]
// === 观察者模式状态 ===
let subIdCounter = 0
const subscribers = ref([])
const eventMessage = ref('')
const observerLog = ref([])
const subNames = ['小明', '小红', '小刚', '小美', '小李', '小王', '小张', '小赵']
function addSubscriber() {
const name = subNames[subIdCounter % subNames.length]
subscribers.value.push({
id: ++subIdCounter,
name: name + '#' + subIdCounter,
lastMsg: '',
notified: false
})
observerLog.value.unshift(`[订阅] ${name}#${subIdCounter} 加入了订阅列表`)
}
function removeSubscriber(id) {
const sub = subscribers.value.find(s => s.id === id)
subscribers.value = subscribers.value.filter(s => s.id !== id)
if (sub) observerLog.value.unshift(`[取消订阅] ${sub.name} 离开了`)
}
function publishEvent() {
const msg = eventMessage.value.trim() || '默认事件'
observerLog.value.unshift(`[发布] 事件: "${msg}" → 通知 ${subscribers.value.length} 个订阅者`)
subscribers.value.forEach((sub, i) => {
setTimeout(() => {
sub.lastMsg = msg
sub.notified = true
setTimeout(() => { sub.notified = false }, 600)
}, i * 150)
})
eventMessage.value = ''
}
// === 策略模式状态 ===
const strategyData = ref([38, 15, 72, 46, 91, 23, 64, 8, 55, 30])
const activeStrategy = ref('bubble')
const sorting = ref(false)
const sortSteps = ref([])
const highlightIdx = ref(-1)
const sortStrategies = [
{ key: 'bubble', name: '冒泡排序', complexity: 'O(n²)' },
{ key: 'selection', name: '选择排序', complexity: 'O(n²)' },
{ key: 'insertion', name: '插入排序', complexity: 'O(n²)' }
]
function shuffleData() {
const arr = [...strategyData.value]
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]]
}
strategyData.value = arr
sortSteps.value = []
}
function barColor(i) {
if (i === highlightIdx.value) return '#f59e0b'
return `hsl(${strategyData.value[i] * 2.5}, 65%, 55%)`
}
async function executeSort() {
sorting.value = true
sortSteps.value = []
const arr = [...strategyData.value]
const steps = []
if (activeStrategy.value === 'bubble') {
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
steps.push({ arr: [...arr], idx: j + 1 })
}
}
}
} else if (activeStrategy.value === 'selection') {
for (let i = 0; i < arr.length; i++) {
let min = i
for (let j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) min = j
}
if (min !== i) {
[arr[i], arr[min]] = [arr[min], arr[i]]
steps.push({ arr: [...arr], idx: i })
}
}
} else {
for (let i = 1; i < arr.length; i++) {
const key = arr[i]
let j = i - 1
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j]
j--
}
arr[j + 1] = key
steps.push({ arr: [...arr], idx: j + 1 })
}
}
sortSteps.value = steps
for (const step of steps) {
strategyData.value = step.arr
highlightIdx.value = step.idx
await new Promise(r => setTimeout(r, 200))
}
highlightIdx.value = -1
sorting.value = false
}
function resetState() {
observerLog.value = []
subscribers.value = []
subIdCounter = 0
eventMessage.value = ''
strategyData.value = [38, 15, 72, 46, 91, 23, 64, 8, 55, 30]
sortSteps.value = []
sorting.value = false
highlightIdx.value = -1
}
</script>
<style scoped>
.pattern-playground {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
background: var(--vp-c-bg-soft);
}
.demo-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
font-size: 18px;
font-weight: 600;
}
.demo-header .icon { font-size: 24px }
.demo-header .subtitle {
font-size: 13px;
color: var(--vp-c-text-2);
font-weight: 400;
}
.mode-tabs {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.mode-btn {
flex: 1;
padding: 10px;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
}
.mode-btn.active {
border-color: var(--vp-c-brand-1);
background: rgba(100, 108, 255, 0.08);
color: var(--vp-c-brand-1);
}
.playground-area { margin-top: 8px }
.playground-desc {
font-size: 13px;
color: var(--vp-c-text-2);
margin-bottom: 14px;
line-height: 1.6;
}
.panel-title {
font-weight: 600;
font-size: 14px;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.action-btn {
padding: 6px 14px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.action-btn:hover { background: var(--vp-c-bg-soft) }
.action-btn.publish {
background: var(--vp-c-brand-1);
color: #fff;
border-color: var(--vp-c-brand-1);
}
.action-btn.publish:hover { opacity: 0.85 }
.action-btn.add {
font-size: 12px;
padding: 3px 10px;
}
/* Observer */
.observer-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.publisher-panel, .subscribers-panel {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 14px;
background: var(--vp-c-bg);
}
.event-input {
display: flex;
gap: 8px;
}
.event-input input {
flex: 1;
padding: 6px 10px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 13px;
background: var(--vp-c-bg-soft);
}
.subscriber-card {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 6px;
margin-bottom: 4px;
font-size: 13px;
background: var(--vp-c-bg-soft);
transition: all 0.3s;
}
.subscriber-card.notified {
background: rgba(16, 185, 129, 0.15);
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.3);
}
.sub-name { font-weight: 500 }
.sub-msg {
flex: 1;
color: var(--vp-c-text-2);
font-size: 12px;
}
.remove-btn {
padding: 2px 8px;
border: 1px solid #f87171;
border-radius: 4px;
background: transparent;
color: #f87171;
cursor: pointer;
font-size: 11px;
}
.empty-hint {
font-size: 12px;
color: var(--vp-c-text-3);
text-align: center;
padding: 16px;
}
.event-log {
margin-top: 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 10px;
background: var(--vp-c-bg);
max-height: 150px;
overflow-y: auto;
}
.log-title {
font-size: 13px;
font-weight: 600;
margin-bottom: 6px;
}
.log-item {
font-size: 12px;
color: var(--vp-c-text-2);
padding: 2px 0;
border-bottom: 1px solid var(--vp-c-divider);
}
/* Strategy */
.strategy-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.data-panel, .strategy-panel {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 14px;
background: var(--vp-c-bg);
}
.data-bars {
display: flex;
align-items: flex-end;
gap: 4px;
height: 280px;
padding: 10px 0;
}
.bar {
flex: 1;
border-radius: 4px 4px 0 0;
display: flex;
align-items: flex-start;
justify-content: center;
min-width: 20px;
transition: height 0.2s, background 0.2s;
}
.bar-label {
font-size: 10px;
color: #fff;
font-weight: 600;
margin-top: 4px;
}
.strategy-options {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 12px;
}
.strategy-btn {
padding: 8px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-soft);
cursor: pointer;
font-size: 13px;
text-align: left;
display: flex;
justify-content: space-between;
transition: all 0.2s;
}
.strategy-btn.active {
border-color: var(--vp-c-brand-1);
background: rgba(100, 108, 255, 0.08);
}
.strategy-complexity {
font-size: 11px;
color: var(--vp-c-text-3);
}
.steps-info {
margin-top: 8px;
font-size: 12px;
color: var(--vp-c-text-2);
}
@media (max-width: 640px) {
.observer-layout, .strategy-layout {
grid-template-columns: 1fr;
}
}
</style>