This commit is contained in:
2026-06-15 09:00:38 +08:00
parent fec66377d5
commit 4640c5e02b
191 changed files with 6046 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
<template>
<div class="code-viewer">
<div class="code-toolbar">
<div class="file-info">
<span class="file-icon">📄</span>
<span class="file-name">{{ fileName }}</span>
<span v-if="fileLabel" class="file-label">{{ fileLabel }}</span>
</div>
<div class="toolbar-actions">
<span v-if="description" class="file-desc-tip" :title="description"></span>
<button class="copy-btn" @click="copyCode" :title="copied ? '已复制!' : '复制代码'">
{{ copied ? '✅' : '📋' }}
</button>
</div>
</div>
<div class="code-content" ref="codeRef">
<pre><code v-html="highlightedCode"></code></pre>
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner">加载中...</div>
</div>
<div v-if="error" class="error-overlay">
<p> {{ error }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, computed } from 'vue'
import { loadCodeFile, highlightC } from '../utils/codeUtils.js'
const props = defineProps({
filePath: String,
fileName: String,
fileLabel: String,
description: String
})
const codeContent = ref('')
const loading = ref(false)
const error = ref('')
const copied = ref(false)
const highlightedCode = computed(() => {
if (!codeContent.value) return ''
return highlightC(codeContent.value)
})
async function loadCode() {
if (!props.filePath) return
loading.value = true
error.value = ''
codeContent.value = ''
try {
codeContent.value = await loadCodeFile(props.filePath)
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
watch(() => props.filePath, loadCode, { immediate: true })
async function copyCode() {
try {
await navigator.clipboard.writeText(codeContent.value)
copied.value = true
setTimeout(() => { copied.value = false }, 2000)
} catch {
// fallback
const textarea = document.createElement('textarea')
textarea.value = codeContent.value
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
copied.value = true
setTimeout(() => { copied.value = false }, 2000)
}
}
</script>
<style scoped>
.code-viewer {
background: var(--code-bg);
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--border-color);
margin-bottom: 20px;
}
.code-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
background: var(--code-header-bg);
border-bottom: 1px solid var(--border-color);
}
.file-info {
display: flex;
align-items: center;
gap: 8px;
}
.file-icon {
font-size: 16px;
}
.file-name {
font-family: 'Fira Code', 'JetBrains Mono', 'Consolas', monospace;
font-size: 14px;
font-weight: 600;
color: var(--primary-color);
}
.file-label {
font-size: 12px;
color: var(--text-tertiary);
background: var(--tag-bg);
padding: 2px 8px;
border-radius: 4px;
}
.toolbar-actions {
display: flex;
align-items: center;
gap: 8px;
}
.file-desc-tip {
cursor: help;
font-size: 16px;
}
.copy-btn {
background: var(--hover-bg);
border: 1px solid var(--border-color);
padding: 4px 10px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.copy-btn:hover {
background: var(--active-bg);
}
.code-content {
position: relative;
overflow-x: auto;
}
.code-content pre {
padding: 16px;
margin: 0;
font-family: 'Fira Code', 'JetBrains Mono', 'Consolas', monospace;
font-size: 13px;
line-height: 1.6;
color: var(--code-text);
}
.code-content code {
font-family: inherit;
}
.loading-overlay,
.error-overlay {
padding: 20px;
text-align: center;
color: var(--text-tertiary);
}
.error-overlay {
color: #e74c3c;
}
</style>
+95
View File
@@ -0,0 +1,95 @@
<script setup>
import { ref } from 'vue'
import viteLogo from '../assets/vite.svg'
import heroImg from '../assets/hero.png'
import vueLogo from '../assets/vue.svg'
const count = ref(0)
</script>
<template>
<section id="center">
<div class="hero">
<img :src="heroImg" class="base" width="170" height="179" alt="" />
<img :src="vueLogo" class="framework" alt="Vue logo" />
<img :src="viteLogo" class="vite" alt="Vite logo" />
</div>
<div>
<h1>Get started</h1>
<p>Edit <code>src/App.vue</code> and save to test <code>HMR</code></p>
</div>
<button type="button" class="counter" @click="count++">
Count is {{ count }}
</button>
</section>
<div class="ticks"></div>
<section id="next-steps">
<div id="docs">
<svg class="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#documentation-icon"></use>
</svg>
<h2>Documentation</h2>
<p>Your questions, answered</p>
<ul>
<li>
<a href="https://vite.dev/" target="_blank">
<img class="logo" :src="viteLogo" alt="" />
Explore Vite
</a>
</li>
<li>
<a href="https://vuejs.org/" target="_blank">
<img class="button-icon" :src="vueLogo" alt="" />
Learn more
</a>
</li>
</ul>
</div>
<div id="social">
<svg class="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#social-icon"></use>
</svg>
<h2>Connect with us</h2>
<p>Join the Vite community</p>
<ul>
<li>
<a href="https://github.com/vitejs/vite" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#github-icon"></use>
</svg>
GitHub
</a>
</li>
<li>
<a href="https://chat.vite.dev/" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#discord-icon"></use>
</svg>
Discord
</a>
</li>
<li>
<a href="https://x.com/vite_js" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#x-icon"></use>
</svg>
X.com
</a>
</li>
<li>
<a href="https://bsky.app/profile/vite.dev" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#bluesky-icon"></use>
</svg>
Bluesky
</a>
</li>
</ul>
</div>
</section>
<div class="ticks"></div>
<section id="spacer"></section>
</template>
+166
View File
@@ -0,0 +1,166 @@
<template>
<aside class="sidebar" :class="{ collapsed }">
<div class="sidebar-header">
<h2 class="sidebar-title" @click="$router.push('/')">
<span class="logo">📚</span>
<span v-show="!collapsed">算法分析教学</span>
</h2>
<button class="toggle-btn" @click="$emit('toggle')">
{{ collapsed ? '' : '' }}
</button>
</div>
<nav class="sidebar-nav">
<router-link
v-for="ch in chapters"
:key="ch.id"
:to="`/chapter/${ch.id}`"
class="nav-item"
:class="{ active: currentId === ch.id }"
>
<span class="nav-icon">{{ ch.icon }}</span>
<span class="nav-text" v-show="!collapsed">
<span class="nav-title">{{ ch.title.split('—')[0].trim() }}</span>
<span class="nav-subtitle">{{ ch.subtitle }}</span>
</span>
</router-link>
</nav>
<div class="sidebar-footer" v-show="!collapsed">
<p>算法分析与设计</p>
<p class="footer-sub">教学辅助平台</p>
</div>
</aside>
</template>
<script setup>
import { chapters } from '../data/chapters.js'
defineProps({
collapsed: Boolean,
currentId: String
})
defineEmits(['toggle'])
</script>
<style scoped>
.sidebar {
width: 280px;
min-width: 280px;
background: var(--sidebar-bg);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
transition: all 0.3s ease;
overflow: hidden;
}
.sidebar.collapsed {
width: 60px;
min-width: 60px;
}
.sidebar-header {
padding: 16px;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
}
.sidebar-title {
font-size: 16px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
color: var(--text-primary);
white-space: nowrap;
}
.logo {
font-size: 20px;
}
.toggle-btn {
background: none;
border: none;
font-size: 18px;
cursor: pointer;
color: var(--text-secondary);
padding: 4px 8px;
border-radius: 4px;
}
.toggle-btn:hover {
background: var(--hover-bg);
}
.sidebar-nav {
flex: 1;
padding: 12px 8px;
overflow-y: auto;
}
.nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 12px;
margin-bottom: 4px;
border-radius: 8px;
color: var(--text-secondary);
text-decoration: none;
transition: all 0.2s ease;
cursor: pointer;
}
.nav-item:hover {
background: var(--hover-bg);
color: var(--text-primary);
}
.nav-item.active {
background: var(--active-bg);
color: var(--primary-color);
font-weight: 600;
}
.nav-icon {
font-size: 20px;
flex-shrink: 0;
}
.nav-text {
display: flex;
flex-direction: column;
overflow: hidden;
}
.nav-title {
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.nav-subtitle {
font-size: 11px;
color: var(--text-tertiary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-footer {
padding: 16px;
border-top: 1px solid var(--border-color);
text-align: center;
font-size: 12px;
color: var(--text-tertiary);
}
.footer-sub {
font-size: 10px;
margin-top: 2px;
}
</style>
+463
View File
@@ -0,0 +1,463 @@
<template>
<div class="sort-visualizer">
<div class="viz-header">
<h3 class="viz-title">{{ title }}</h3>
<div class="viz-controls">
<button class="ctrl-btn" @click="randomize" title="随机生成新数组">🎲</button>
<button class="ctrl-btn" @click="reset" title="重置"></button>
<button class="ctrl-btn" @click="prevStep" :disabled="currentStep <= 0" title="上一步"></button>
<button class="ctrl-btn play-btn" @click="togglePlay" :title="isPlaying ? '暂停' : '播放'">
{{ isPlaying ? '⏸' : '▶' }}
</button>
<button class="ctrl-btn" @click="nextStep" :disabled="currentStep >= totalSteps - 1" title="下一步"></button>
<button class="ctrl-btn" @click="goToEnd" :disabled="currentStep >= totalSteps - 1" title="跳到结束"></button>
<span class="step-info">{{ currentStep + 1 }} / {{ totalSteps }}</span>
</div>
<div class="speed-control">
<label>速度</label>
<input type="range" min="1" max="10" v-model.number="speed" class="speed-slider" />
</div>
</div>
<div class="viz-canvas-wrapper">
<div class="viz-canvas" ref="canvasRef">
<div
v-for="(val, idx) in currentState.array"
:key="idx"
class="bar-wrapper"
:style="{ height: barHeight(val) + 'px' }"
>
<div
class="bar"
:class="barClass(idx)"
:style="{ height: '100%' }"
>
<span class="bar-value" v-if="showValues">{{ val }}</span>
</div>
</div>
</div>
</div>
<div class="viz-description">
<span class="desc-text">{{ currentState.description }}</span>
<div class="legend">
<span class="legend-item"><span class="legend-color legend-default"></span>未处理</span>
<span class="legend-item"><span class="legend-color legend-comparing"></span>比较中</span>
<span class="legend-item"><span class="legend-color legend-swapping"></span>交换</span>
<span class="legend-item"><span class="legend-color legend-pivot"></span>基准</span>
<span class="legend-item"><span class="legend-color legend-sorted"></span>已排序</span>
<span class="legend-item"><span class="legend-color legend-merging"></span>归并中</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, watch, onBeforeUnmount } from 'vue'
import { generateMergeSortSteps, generateQuickSortSteps, generateRandomArray } from '../utils/sortAnimations.js'
const props = defineProps({
title: { type: String, default: '排序可视化' },
algorithm: { type: String, default: 'merge' }, // 'merge' | 'quick'
arraySize: { type: Number, default: 12 },
showValues: { type: Boolean, default: true }
})
const canvasRef = ref(null)
const currentStep = ref(0)
const isPlaying = ref(false)
const speed = ref(5)
let timer = null
const rawArray = ref(generateRandomArray(props.arraySize))
const steps = ref([])
const totalSteps = computed(() => steps.value.length)
const currentState = computed(() => {
if (steps.value.length === 0) {
return {
array: rawArray.value,
highlights: {},
description: '点击 ▶ 开始演示'
}
}
return steps.value[currentStep.value] || steps.value[0]
})
// 计算最大值用于柱状图高度
const maxVal = computed(() => {
return Math.max(...currentState.value.array, 1)
})
function barHeight(val) {
// 最大高度约 280px
return Math.max(20, (val / maxVal.value) * 260)
}
function barClass(idx) {
const h = currentState.value.highlights || {}
const classes = []
// 已排序(最终位置)
if (h.sorted && h.sorted.includes(idx)) {
classes.push('bar-sorted')
return classes
}
if (h.sortedIndices && h.sortedIndices.includes(idx)) {
classes.push('bar-sorted')
return classes
}
// 基准元素
if (h.pivot === idx) {
classes.push('bar-pivot')
return classes
}
// 交换中
if (h.swapping && h.swapping.includes(idx)) {
classes.push('bar-swapping')
return classes
}
// 比较中
if (h.comparing && h.comparing.includes(idx)) {
classes.push('bar-comparing')
return classes
}
// 归并范围
if (h.merging) {
const l = h.merging.left
const r = h.merging.right
if (idx >= l[0] && idx <= l[1]) classes.push('bar-merging-left')
else if (idx >= r[0] && idx <= r[1]) classes.push('bar-merging-right')
}
// 范围高亮
if (h.range && !classes.length) {
if (idx >= h.range[0] && idx <= h.range[1]) {
classes.push('bar-in-range')
}
}
return classes
}
// 生成动画步骤
function generateSteps() {
if (props.algorithm === 'merge') {
steps.value = generateMergeSortSteps(rawArray.value)
} else {
steps.value = generateQuickSortSteps(rawArray.value)
}
currentStep.value = 0
}
function randomize() {
stopPlay()
rawArray.value = generateRandomArray(props.arraySize)
generateSteps()
}
function reset() {
stopPlay()
rawArray.value = generateRandomArray(props.arraySize)
generateSteps()
}
function nextStep() {
if (currentStep.value < steps.value.length - 1) {
currentStep.value++
} else {
stopPlay()
}
}
function prevStep() {
if (currentStep.value > 0) {
currentStep.value--
}
}
function goToEnd() {
stopPlay()
currentStep.value = steps.value.length - 1
}
function togglePlay() {
if (isPlaying.value) {
stopPlay()
} else {
startPlay()
}
}
function startPlay() {
if (currentStep.value >= steps.value.length - 1) {
currentStep.value = 0
}
isPlaying.value = true
const interval = Math.max(50, 600 - speed.value * 50)
timer = setInterval(() => {
if (currentStep.value < steps.value.length - 1) {
currentStep.value++
} else {
stopPlay()
}
}, interval)
}
function stopPlay() {
isPlaying.value = false
if (timer) {
clearInterval(timer)
timer = null
}
}
onBeforeUnmount(() => {
stopPlay()
})
// 初始化
generateSteps()
</script>
<style scoped>
.sort-visualizer {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 16px;
overflow: hidden;
margin-bottom: 24px;
box-shadow: var(--shadow-sm);
}
.viz-header {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
padding: 14px 20px;
background: var(--hover-bg);
border-bottom: 1px solid var(--border-color);
}
.viz-title {
font-size: 16px;
font-weight: 700;
color: var(--text-primary);
margin-right: auto;
}
.viz-controls {
display: flex;
align-items: center;
gap: 6px;
}
.ctrl-btn {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 6px 10px;
font-size: 16px;
cursor: pointer;
transition: all 0.15s;
line-height: 1;
}
.ctrl-btn:hover:not(:disabled) {
background: var(--active-bg);
border-color: var(--primary-color);
}
.ctrl-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.play-btn {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.play-btn:hover:not(:disabled) {
opacity: 0.9;
}
.step-info {
font-size: 12px;
color: var(--text-tertiary);
font-family: var(--mono);
min-width: 60px;
text-align: center;
}
.speed-control {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--text-tertiary);
}
.speed-slider {
width: 60px;
accent-color: var(--primary-color);
}
/* 画布 */
.viz-canvas-wrapper {
padding: 24px 20px 12px;
overflow-x: auto;
}
.viz-canvas {
display: flex;
align-items: flex-end;
justify-content: center;
gap: 3px;
min-height: 300px;
padding: 0 10px;
}
.bar-wrapper {
flex: 1;
min-width: 18px;
max-width: 48px;
display: flex;
align-items: flex-end;
transition: height 0.2s ease;
}
.bar {
width: 100%;
border-radius: 4px 4px 0 0;
background: var(--primary-color);
opacity: 0.75;
display: flex;
align-items: flex-end;
justify-content: center;
transition: all 0.2s ease;
min-height: 4px;
position: relative;
}
.bar-value {
font-size: 10px;
color: white;
font-weight: 700;
position: absolute;
top: -18px;
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
font-family: var(--mono);
}
/* 状态颜色 */
.bar-comparing {
background: #f59e0b;
opacity: 1;
transform: scaleY(1.02);
box-shadow: 0 0 8px rgba(245, 158, 11, 0.5);
}
.bar-swapping {
background: #ef4444;
opacity: 1;
transform: scaleY(1.05);
box-shadow: 0 0 12px rgba(239, 68, 68, 0.5);
animation: pulse 0.3s ease;
}
.bar-pivot {
background: #8b5cf6;
opacity: 1;
transform: scaleY(1.05);
box-shadow: 0 0 12px rgba(139, 92, 246, 0.5);
}
.bar-sorted {
background: #22c55e;
opacity: 0.9;
}
.bar-in-range {
opacity: 0.85;
}
.bar-merging-left {
background: #3b82f6;
opacity: 0.9;
}
.bar-merging-right {
background: #06b6d4;
opacity: 0.9;
}
@keyframes pulse {
0%, 100% { transform: scaleY(1.05); }
50% { transform: scaleY(1.15); }
}
/* 描述和图例 */
.viz-description {
padding: 10px 20px 14px;
border-top: 1px solid var(--border-color);
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
.desc-text {
font-size: 14px;
color: var(--text-primary);
font-weight: 500;
flex: 1;
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 10px;
font-size: 11px;
color: var(--text-tertiary);
}
.legend-item {
display: flex;
align-items: center;
gap: 4px;
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 3px;
display: inline-block;
}
.legend-default {
background: var(--primary-color);
opacity: 0.75;
}
.legend-comparing {
background: #f59e0b;
}
.legend-swapping {
background: #ef4444;
}
.legend-pivot {
background: #8b5cf6;
}
.legend-sorted {
background: #22c55e;
}
.legend-merging {
background: #06b6d4;
}
</style>