Files
suanfa/src/views/ChapterView.vue
T
2026-06-16 10:23:48 +08:00

543 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
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="chapter-view" v-if="chapter">
<header class="chapter-header">
<div class="chapter-icon">{{ chapter.icon }}</div>
<div>
<h1 class="chapter-title">{{ chapter.title }}</h1>
<p class="chapter-subtitle">{{ chapter.subtitle }}</p>
<p class="chapter-desc">{{ chapter.description }}</p>
</div>
</header>
<section class="topics-section">
<h2 class="section-label">📌 本章知识点</h2>
<div class="topics-list">
<span v-for="topic in chapter.topics" :key="topic" class="topic-pill">
{{ topic }}
</span>
</div>
</section>
<!-- 子文件夹分组显示 -->
<section v-for="folder in chapter.subfolders" :key="folder.name" class="folder-section">
<h2 class="folder-title">📂 {{ folder.label }}</h2>
<p class="folder-hint" v-if="getFolderDescription(folder.name)">{{ getFolderDescription(folder.name) }}</p>
<!-- 动态演示入口 -->
<div v-if="folder.demo" class="demo-entry">
<router-link :to="folder.demo.path" class="demo-link">
<span class="demo-link-icon">{{ folder.demo.label.split(' ')[0] }}</span>
<span class="demo-link-text">{{ folder.demo.label }}</span>
<span class="demo-link-desc">{{ folder.demo.description }}</span>
<span class="demo-link-arrow"></span>
</router-link>
</div>
<div class="file-list">
<div
v-for="file in folder.files"
:key="file.path"
class="file-item"
:class="{ active: currentFile?.path === file.path }"
@click="selectFile(file)"
>
<div class="file-item-header">
<span class="file-item-icon">📄</span>
<span class="file-item-name">{{ file.name }}</span>
<span class="file-item-label">{{ file.label }}</span>
</div>
<p class="file-item-desc">{{ file.description }}</p>
</div>
</div>
</section>
<!-- 根级文件没有子文件夹的文件 -->
<section v-if="chapter.files && chapter.files.length" class="folder-section">
<h2 class="folder-title">📂 其他示例</h2>
<div class="file-list">
<div
v-for="file in chapter.files"
:key="file.path"
class="file-item"
:class="{ active: currentFile?.path === file.path }"
@click="selectFile(file)"
>
<div class="file-item-header">
<span class="file-item-icon">📄</span>
<span class="file-item-name">{{ file.name }}</span>
<span class="file-item-label">{{ file.label }}</span>
</div>
<p class="file-item-desc">{{ file.description }}</p>
</div>
</div>
</section>
<!-- 代码查看 Drawer -->
<Teleport to="body">
<Transition name="drawer">
<div v-if="currentFile" class="drawer-overlay" @click.self="closeDrawer">
<div class="drawer-panel">
<div class="drawer-header">
<span class="drawer-title">💻 代码查看</span>
<div class="drawer-lang-tabs">
<button
class="lang-tab"
:class="{ active: language === 'c' }"
@click="switchLanguage('c')"
>C</button>
<button
class="lang-tab"
:class="{ active: language === 'python' }"
@click="switchLanguage('python')"
>Python</button>
</div>
<button class="drawer-close-btn" @click="closeDrawer"></button>
</div>
<div class="drawer-body">
<CodeViewer
:key="currentFilePath"
:filePath="currentFilePath"
:fileName="currentFileName"
:fileLabel="currentFile.label"
:description="currentFile.description"
:language="language"
/>
</div>
</div>
</div>
</Transition>
</Teleport>
</div>
<div v-else class="not-found">
<h2>章节未找到</h2>
<p>请返回首页重新选择</p>
<router-link to="/" class="back-link"> 返回首页</router-link>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useRoute } from 'vue-router'
import { getChapterById } from '../data/chapters.js'
import CodeViewer from '../components/CodeViewer.vue'
const props = defineProps({
id: String
})
const route = useRoute()
const chapterId = computed(() => props.id || route.params.id)
const currentFile = ref(null)
const language = ref('c')
// 根据语言生成文件路径
const currentFilePath = computed(() => {
if (!currentFile.value) return null
if (language.value === 'python') {
return currentFile.value.pyPath || currentFile.value.path.replace(/^c\//, 'py/').replace(/\.c$/, '.py')
}
return currentFile.value.path
})
const currentFileName = computed(() => {
if (!currentFile.value) return ''
if (language.value === 'python') {
return currentFile.value.pyPath
? currentFile.value.pyPath.split('/').pop()
: currentFile.value.name.replace(/\.c$/, '.py')
}
return currentFile.value.name
})
function switchLanguage(lang) {
language.value = lang
}
const chapter = computed(() => getChapterById(chapterId.value))
// Reset current file when switching chapters
watch(chapterId, () => {
currentFile.value = null
})
function selectFile(file) {
currentFile.value = file
}
function closeDrawer() {
currentFile.value = null
language.value = 'c'
}
function getFolderDescription(folderName) {
const descriptions = {
'complexity-demo': '通过交互式动画,直观体验不同时间复杂度算法的执行过程与操作次数差异。',
'code-examples': '本章相关的 C 语言和 Python 代码示例。',
mergesort: '归并排序是分治法的经典应用:将数组一分为二,分别排序后合并。时间复杂度 O(n log n)。',
quicksort: '快速排序通过选择一个基准元素,将数组分为左右两部分,递归排序。平均时间复杂度 O(n log n)。',
halfsearch: '二分查找在有序数组中通过每次将搜索范围缩小一半来查找目标值,时间复杂度 O(log n)。',
bigcheng: '大整数乘法通过分治策略将 n 位数乘法分解为更小规模的计算,降低时间复杂度。',
matrix: '矩阵乘法有多种实现方式:朴素算法 O(n³),Strassen 分治算法可优化至 O(n^2.81)。',
digui: '递归是分治法的基石,通过这些示例理解递归的基本结构与调用栈。',
maopao: '冒泡排序通过相邻元素比较交换将最大元素"冒泡"到末尾,时间复杂度 O(n²)。',
allpai: '全排列生成所有 n! 种排列方式,是回溯思想和分治策略的典型应用。',
richeng: '循环赛日程表使用分治策略为 2^k 个选手安排比赛日程。',
bag01: '0/1背包问题是动态规划的经典问题:给定容量和物品,求最大价值。',
lcs: '最长公共子序列(LCS)问题是比较两个序列相似度的经典DP问题。',
image: '图像压缩问题使用动态规划确定最优的像素段划分,平衡压缩比与存储空间。',
homework: '课后练习中的算法实现示例。',
huodong: '活动选择问题是贪心算法的经典案例:选择最多的不重叠活动。',
huffman: 'Huffman编码是数据压缩经典算法,通过构造最优前缀码实现无损压缩。',
bag: '贪心背包(部分背包)问题允许取物品的一部分,按单位价值贪心选择。',
bestload: '最优装载问题:在载重限制下尽可能多地装载物品。',
money: '找零问题:使用最少硬币凑出指定金额。',
shortest: '最短路径问题:寻找图中两点之间的最短路径。',
hui01bag: '0/1背包的回溯解法:通过深度优先搜索和剪枝寻找最优解。',
loading: '装载问题的回溯解法:搜索最优的装载方案。',
queue: 'N皇后问题的回溯解法:在 N×N 棋盘上放置 N 个皇后使其互不攻击。',
xianbag01: '0/1背包的分支限界解法:使用队列或优先队列进行广度优先搜索。',
tsp: '旅行商问题(TSP):寻找最短的环游所有城市的路径。',
graph: '图的两种基本遍历方式:广度优先(BFS)和深度优先(DFS)。'
}
return descriptions[folderName] || ''
}
</script>
<style scoped>
.chapter-view {
max-width: 1000px;
margin: 0 auto;
padding: 0 24px;
}
.chapter-header {
display: flex;
gap: 16px;
align-items: flex-start;
padding: 32px 0 20px;
border-bottom: 1px solid var(--border-color);
margin-bottom: 24px;
}
.chapter-icon {
font-size: 48px;
flex-shrink: 0;
}
.chapter-title {
font-size: 26px;
font-weight: 800;
color: var(--text-primary);
margin-bottom: 4px;
}
.chapter-subtitle {
font-size: 15px;
color: var(--primary-color);
font-weight: 500;
margin-bottom: 6px;
}
.chapter-desc {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.6;
}
.section-label {
font-size: 18px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 12px;
}
.topics-section {
margin-bottom: 28px;
}
.topics-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.topic-pill {
font-size: 13px;
background: var(--active-bg);
color: var(--primary-color);
padding: 6px 16px;
border-radius: 20px;
font-weight: 500;
border: 1px solid var(--border-color);
}
.folder-section {
margin-bottom: 28px;
}
.folder-title {
font-size: 17px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 6px;
padding-bottom: 8px;
border-bottom: 2px solid var(--border-color);
}
.folder-hint {
font-size: 13px;
color: var(--text-tertiary);
margin-bottom: 12px;
line-height: 1.5;
}
/* 动态演示入口 */
.demo-entry {
margin-bottom: 12px;
}
.demo-link {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.08), rgba(139, 92, 246, 0.08));
border: 1px solid rgba(59, 130, 246, 0.25);
border-radius: 10px;
text-decoration: none;
transition: all 0.2s ease;
}
.demo-link:hover {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(139, 92, 246, 0.15));
border-color: var(--primary-color);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
text-decoration: none;
}
.demo-link-icon {
font-size: 24px;
flex-shrink: 0;
}
.demo-link-text {
font-size: 15px;
font-weight: 700;
color: var(--primary-color);
white-space: nowrap;
}
.demo-link-desc {
font-size: 13px;
color: var(--text-secondary);
flex: 1;
}
.demo-link-arrow {
font-size: 18px;
color: var(--primary-color);
font-weight: 700;
flex-shrink: 0;
}
.file-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 10px;
}
.file-item {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.file-item:hover {
border-color: var(--primary-color);
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
.file-item.active {
border-color: var(--primary-color);
background: var(--active-bg);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
}
.file-item-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
}
.file-item-icon {
font-size: 14px;
}
.file-item-name {
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 13px;
font-weight: 600;
color: var(--primary-color);
}
.file-item-label {
font-size: 11px;
color: var(--text-tertiary);
background: var(--tag-bg);
padding: 1px 6px;
border-radius: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-item-desc {
font-size: 12px;
color: var(--text-tertiary);
line-height: 1.4;
margin-left: 20px;
}
/* ========== Drawer ========== */
.drawer-overlay {
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.45);
display: flex;
justify-content: flex-end;
}
.drawer-panel {
width: min(720px, 90vw);
height: 100%;
background: var(--bg);
display: flex;
flex-direction: column;
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);
}
.drawer-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.drawer-title {
font-size: 17px;
font-weight: 700;
color: var(--text-primary);
margin-right: auto;
}
.drawer-lang-tabs {
display: flex;
gap: 4px;
background: var(--hover-bg);
padding: 3px;
border-radius: 8px;
}
.lang-tab {
background: none;
border: none;
padding: 6px 16px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
color: var(--text-secondary);
transition: all 0.15s;
font-family: var(--mono);
}
.lang-tab:hover {
color: var(--text-primary);
}
.lang-tab.active {
background: var(--bg);
color: var(--primary-color);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.drawer-close-btn {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: var(--text-secondary);
padding: 4px 10px;
border-radius: 6px;
transition: all 0.15s;
}
.drawer-close-btn:hover {
background: var(--hover-bg);
color: var(--text-primary);
}
.drawer-body {
flex: 1;
overflow-y: auto;
padding: 0;
}
.drawer-body .code-viewer {
border-radius: 0;
border: none;
min-height: 100%;
}
/* 抽屉过渡动画 */
.drawer-enter-active,
.drawer-leave-active {
transition: opacity 0.25s ease;
}
.drawer-enter-active .drawer-panel,
.drawer-leave-active .drawer-panel {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.drawer-enter-from,
.drawer-leave-to {
opacity: 0;
}
.drawer-enter-from .drawer-panel,
.drawer-leave-to .drawer-panel {
transform: translateX(100%);
}
.not-found {
text-align: center;
padding: 80px 20px;
}
.not-found h2 {
font-size: 24px;
margin-bottom: 8px;
color: var(--text-primary);
}
.back-link {
display: inline-block;
margin-top: 16px;
color: var(--primary-color);
text-decoration: none;
font-weight: 600;
}
.back-link:hover {
text-decoration: underline;
}
</style>