add python code

This commit is contained in:
2026-06-16 09:35:51 +08:00
parent daecbf4603
commit daf9e50938
17 changed files with 763 additions and 19 deletions
+8 -4
View File
@@ -27,7 +27,7 @@
class="tool-btn run-btn"
@click="runCode"
:disabled="running"
:title="running ? '正在编译运行...' : '编译并运行 C 代码'"
:title="running ? '正在运行...' : `运行${language === 'python' ? ' Python' : ' C'}代码`"
>
{{ running ? '⏳' : '▶' }} 运行
</button>
@@ -66,13 +66,14 @@
<script setup>
import { ref, watch, computed } from 'vue'
import { loadCodeFile, highlightC } from '../utils/codeUtils.js'
import { loadCodeFile, highlightC, highlightPython } from '../utils/codeUtils.js'
const props = defineProps({
filePath: String,
fileName: String,
fileLabel: String,
description: String
description: String,
language: { type: String, default: 'c' } // 'c' | 'python'
})
const codeContent = ref('')
@@ -90,6 +91,7 @@ const FONT_STEP = 2
const highlightedCode = computed(() => {
if (!codeContent.value) return ''
if (props.language === 'python') return highlightPython(codeContent.value)
return highlightC(codeContent.value)
})
@@ -166,8 +168,10 @@ async function runCode() {
runOutput.value = null
outputCollapsed.value = false
const apiEndpoint = props.language === 'python' ? '/api/run-py' : '/api/run-c'
try {
const res = await fetch('/api/run-c', {
const res = await fetch(apiEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: codeContent.value })
+12 -12
View File
@@ -17,8 +17,8 @@ export const chapters = [
'实验对比分析方法'
],
files: [
{ path: 'c/ch1/first.c', name: 'first.c', label: '递归打印图案', description: '递归打印矩形/三角形图案,演示递归基本结构' },
{ path: 'c/ch1/compare.c', name: 'compare.c', label: '性能对比(累加 vs 高斯公式)', description: '比较循环累加与高斯求和公式的耗时差异,直观感受算法效率' },
{ path: 'c/ch1/first.c', name: 'first.c', label: '递归打印图案', description: '递归打印矩形/三角形图案,演示递归基本结构', pyPath: 'py/ch1/first.py' },
{ path: 'c/ch1/compare.c', name: 'compare.c', label: '性能对比(累加 vs 高斯公式)', description: '比较循环累加与高斯求和公式的耗时差异,直观感受算法效率', pyPath: 'py/ch1/compare.py' },
{ path: 'c/ch1/compareall.c', name: 'compareall.c', label: '多项性能对比', description: '更多累加与高斯公式的计时对比实验' }
]
},
@@ -43,7 +43,7 @@ export const chapters = [
name: 'mergesort',
label: '归并排序',
files: [
{ path: 'c/ch2/mergesort/guibing.c', name: 'guibing.c', label: '归并排序完整实现', description: '分治归并排序,包含 merge 与 mergeSort 函数' },
{ path: 'c/ch2/mergesort/guibing.c', name: 'guibing.c', label: '归并排序完整实现', description: '分治归并排序,包含 merge 与 mergeSort 函数', pyPath: 'py/ch2/mergesort/mergesort.py' },
{ path: 'c/ch2/mergesort/merge.c', name: 'merge.c', label: '归并操作', description: '合并两个有序子数组的核心 merge 操作' }
],
demo: { path: '/sort-demo#merge', label: '🎬 动态演示', description: '观看归并排序的完整动画过程' }
@@ -52,7 +52,7 @@ export const chapters = [
name: 'quicksort',
label: '快速排序',
files: [
{ path: 'c/ch2/quicksort/danppt.c', name: 'danppt.c', label: '快速排序(单侧指针)', description: '单侧指针遍历划分的快速排序实现' },
{ path: 'c/ch2/quicksort/danppt.c', name: 'danppt.c', label: '快速排序(单侧指针)', description: '单侧指针遍历划分的快速排序实现', pyPath: 'py/ch2/quicksort/quicksort.py' },
{ path: 'c/ch2/quicksort/danleft.c', name: 'danleft.c', label: '快速排序(左指针法)', description: '基于左右指针移动的快速排序变体' },
{ path: 'c/ch2/quicksort/dantwo.c', name: 'dantwo.c', label: '快速排序(双指针法)', description: '另一种双指针快速排序实现' },
{ path: 'c/ch2/quicksort/shuangbian.c', name: 'shuangbian.c', label: '快速排序(双边扫描)', description: '经典的双边扫描分区快速排序' }
@@ -63,7 +63,7 @@ export const chapters = [
name: 'halfsearch',
label: '二分查找',
files: [
{ path: 'c/ch2/student/halfsearch.c', name: 'halfsearch.c', label: '二分查找(递归版)', description: '递归实现的二分查找算法' },
{ path: 'c/ch2/student/halfsearch.c', name: 'halfsearch.c', label: '二分查找(递归版)', description: '递归实现的二分查找算法', pyPath: 'py/ch2/halfsearch/binary_search.py' },
{ path: 'c/ch2/student/halfsearchnew.c', name: 'halfsearchnew.c', label: '二分查找(迭代版)', description: '迭代实现的二分查找算法' },
{ path: 'c/ch2/halfsearch/fenzhi.c', name: 'fenzhi.c', label: '分治分割示例', description: '二分分治分割示例' },
{ path: 'c/ch2/halfsearch/fenzhirec.c', name: 'fenzhirec.c', label: '递归查找', description: '递归结构的查找实现' }
@@ -90,7 +90,7 @@ export const chapters = [
name: 'digui',
label: '递归示例',
files: [
{ path: 'c/ch2/digui/printnumber.c', name: 'printnumber.c', label: '递归打印整数', description: '递归按位打印整数(高位到低位)' },
{ path: 'c/ch2/digui/printnumber.c', name: 'printnumber.c', label: '递归打印整数', description: '递归按位打印整数(高位到低位)', pyPath: 'py/ch2/digui/print_number.py' },
{ path: 'c/ch2/digui/tuxing.c', name: 'tuxing.c', label: '递归图形', description: '递归绘制图形示例' },
{ path: 'c/ch2/shangji/findarrmax.c', name: 'findarrmax.c', label: '递归求最大值', description: '递归查找数组最大值' },
{ path: 'c/ch2/shangji/sanjiao.c', name: 'sanjiao.c', label: '递归三角形', description: '递归打印星号三角形' }
@@ -107,7 +107,7 @@ export const chapters = [
name: 'allpai',
label: '全排列',
files: [
{ path: 'c/ch2/ch2.allpai/allpai.c', name: 'allpai.c', label: '全排列生成', description: '递归交换法全排列生成' },
{ path: 'c/ch2/ch2.allpai/allpai.c', name: 'allpai.c', label: '全排列生成', description: '递归交换法全排列生成', pyPath: 'py/ch2/allpai/permutations.py' },
{ path: 'c/ch2/ch2.allpai/allpaichong.c', name: 'allpaichong.c', label: '全排列(去重)', description: '带去重逻辑的全排列生成' },
{ path: 'c/ch2/ch2.allpai/arrpaichong.c', name: 'arrpaichong.c', label: '数组排列去重', description: '数组排列生成与去重' },
{ path: 'c/ch2/student/quanpai.c', name: 'quanpai.c', label: '全排列(学生版)', description: '全排列/排列打印程序' },
@@ -145,7 +145,7 @@ export const chapters = [
name: 'bag01',
label: '0/1背包问题',
files: [
{ path: 'c/ch3/bag01/bag01.c', name: 'bag01.c', label: '0/1背包 DP 实现', description: '经典二维DP矩阵解法' },
{ path: 'c/ch3/bag01/bag01.c', name: 'bag01.c', label: '0/1背包 DP 实现', description: '经典二维DP矩阵解法', pyPath: 'py/ch3/bag01/knapsack01.py' },
{ path: 'c/ch3/bag01/bagbag.c', name: 'bagbag.c', label: '背包状态追踪', description: '多种背包实现/状态追踪' },
{ path: 'c/ch3/bag01/bagevery.c', name: 'bagevery.c', label: '背包枚举', description: '背包问题枚举/遍历示例' },
{ path: 'c/ch3/bag01/yanghui.c', name: 'yanghui.c', label: '杨辉三角(二维数组)', description: '使用二维数组打印杨辉三角' },
@@ -156,7 +156,7 @@ export const chapters = [
name: 'lcs',
label: '最长公共子序列(LCS)',
files: [
{ path: 'c/ch3/lcs/lcs1.c', name: 'lcs1.c', label: 'LCS 实现(一)', description: '构建LCS DP表' },
{ path: 'c/ch3/lcs/lcs1.c', name: 'lcs1.c', label: 'LCS 实现(一)', description: '构建LCS DP表', pyPath: 'py/ch3/lcs/lcs.py' },
{ path: 'c/ch3/lcs/lcs2.c', name: 'lcs2.c', label: 'LCS 实现(二)', description: 'LCS动态规划实现,返回长度' },
{ path: 'c/ch3/lcs/printtable.c', name: 'printtable.c', label: 'LCS 路径回溯', description: '打印LCS路径表并回溯输出LCS' }
]
@@ -210,14 +210,14 @@ export const chapters = [
name: 'huodong',
label: '活动选择',
files: [
{ path: 'c/ch4/huodong/fenpei.c', name: 'fenpei.c', label: '活动分配', description: '活动选择问题的贪心实现' }
{ path: 'c/ch4/huodong/fenpei.c', name: 'fenpei.c', label: '活动分配', description: '活动选择问题的贪心实现', pyPath: 'py/ch4/huodong/activity_selection.py' }
]
},
{
name: 'huffman',
label: 'Huffman编码',
files: [
{ path: 'c/ch4/huffman/halfall.c', name: 'halfall.c', label: 'Huffman编码主流程', description: '构建Huffman树并生成编码' },
{ path: 'c/ch4/huffman/halfall.c', name: 'halfall.c', label: 'Huffman编码主流程', description: '构建Huffman树并生成编码', pyPath: 'py/ch4/huffman/huffman.py' },
{ path: 'c/ch4/huffman/halfcode.c', name: 'halfcode.c', label: 'Huffman编码生成', description: '从Huffman树回溯得到编码' },
{ path: 'c/ch4/huffman/halftree.c', name: 'halftree.c', label: 'Huffman树构建', description: '节点创建、合并、排序构建Huffman树' },
{ path: 'c/ch4/huffman/treelist.c', name: 'treelist.c', label: 'Huffman节点管理', description: '树节点创建和列表操作' }
@@ -243,7 +243,7 @@ export const chapters = [
name: 'money',
label: '找零问题',
files: [
{ path: 'c/ch4/money/getmoney.c', name: 'getmoney.c', label: '贪心找零', description: '按面额计算所需最少张数' },
{ path: 'c/ch4/money/getmoney.c', name: 'getmoney.c', label: '贪心找零', description: '按面额计算所需最少张数', pyPath: 'py/ch4/money/coin_change.py' },
{ path: 'c/ch4/money/moneytwowei.c', name: 'moneytwowei.c', label: '找零(二维)', description: '面额分解的另一版本' }
]
},
+146
View File
@@ -140,6 +140,152 @@ export function highlightC(code) {
return result
}
/**
* Python 语法高亮
*/
export function highlightPython(code) {
const keywords = new Set([
'False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await',
'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except',
'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is',
'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return',
'try', 'while', 'with', 'yield', 'print', 'range', 'len', 'int',
'float', 'str', 'list', 'dict', 'set', 'tuple', 'bool', 'input',
'enumerate', 'zip', 'map', 'filter', 'sorted', 'reversed', 'open',
'self', 'cls', 'super', 'property', 'staticmethod', 'classmethod',
'TypeVar', 'Optional', 'List', 'Dict', 'Tuple', 'Any', 'Union'
])
const builtins = new Set([
'print', 'range', 'len', 'int', 'float', 'str', 'list', 'dict',
'set', 'tuple', 'bool', 'input', 'enumerate', 'zip', 'map',
'filter', 'sorted', 'reversed', 'open', 'type', 'isinstance',
'hasattr', 'getattr', 'setattr', 'abs', 'max', 'min', 'sum',
'all', 'any', 'ord', 'chr', 'repr', 'format', 'id', 'hash'
])
let result = ''
let i = 0
const len = code.length
while (i < len) {
// 单行注释 #
if (code[i] === '#') {
let end = i + 1
while (end < len && code[end] !== '\n') end++
result += '<span class="hl-comment">' + escapeHtml(code.slice(i, end)) + '</span>'
i = end
continue
}
// 多行注释/文档字符串 """...""" 或 '''...'''
if ((code[i] === '"' && i + 2 < len && code[i + 1] === '"' && code[i + 2] === '"') ||
(code[i] === "'" && i + 2 < len && code[i + 1] === "'" && code[i + 2] === "'")) {
const quote = code.slice(i, i + 3)
let end = i + 3
while (end + 2 < len && code.slice(end, end + 3) !== quote) {
if (code[end] === '\\') end++
end++
}
if (end + 2 < len) end += 3
result += '<span class="hl-comment">' + escapeHtml(code.slice(i, end)) + '</span>'
i = end
continue
}
// 双引号字符串 "..."
if (code[i] === '"') {
let end = i + 1
while (end < len && code[end] !== '"') {
if (code[end] === '\\') end++
end++
}
if (end < len) end++
result += '<span class="hl-string">' + escapeHtml(code.slice(i, end)) + '</span>'
i = end
continue
}
// 单引号字符串 '...'
if (code[i] === "'") {
let end = i + 1
while (end < len && code[end] !== "'") {
if (code[end] === '\\') end++
end++
}
if (end < len) end++
result += '<span class="hl-string">' + escapeHtml(code.slice(i, end)) + '</span>'
i = end
continue
}
// f-string f"..."
if (code[i] === 'f' && i + 1 < len && code[i + 1] === '"') {
let end = i + 2
const stack = []
while (end < len && code[end] !== '"') {
if (code[end] === '{') stack.push(end)
else if (code[end] === '}' && stack.length) stack.pop()
if (code[end] === '\\') end++
end++
}
if (end < len) end++
result += '<span class="hl-string">' + escapeHtml(code.slice(i, end)) + '</span>'
i = end
continue
}
// 数字
if (isDigit(code[i]) || (code[i] === '.' && i + 1 < len && isDigit(code[i + 1]))) {
let start = i
while (i < len && (isDigit(code[i]) || code[i] === '.' || code[i] === 'e' || code[i] === 'E' ||
code[i] === 'x' || code[i] === 'X' || code[i] === 'o' || code[i] === 'O' ||
code[i] === 'b' || code[i] === 'B' || code[i] === '+' || code[i] === '-' ||
(code[i] >= 'a' && code[i] <= 'f') || (code[i] >= 'A' && code[i] <= 'F'))) {
// 简单数字检测,遇到非数字字符停止
if (isWordChar(code[i]) && !isDigit(code[i]) &&
!(code[i] >= 'a' && code[i] <= 'f') && !(code[i] >= 'A' && code[i] <= 'F') &&
code[i] !== 'x' && code[i] !== 'X' && code[i] !== 'o' && code[i] !== 'O' &&
code[i] !== 'b' && code[i] !== 'B' && code[i] !== 'e' && code[i] !== 'E' &&
code[i] !== '.' && code[i] !== '+' && code[i] !== '-') break
i++
}
result += '<span class="hl-number">' + escapeHtml(code.slice(start, i)) + '</span>'
continue
}
// @decorator
if (code[i] === '@' && i + 1 < len && isWordStart(code[i + 1])) {
let end = i + 1
while (end < len && isWordChar(code[end])) end++
result += '<span class="hl-preprocessor">' + escapeHtml(code.slice(i, end)) + '</span>'
i = end
continue
}
// 标识符/关键字
if (isWordStart(code[i])) {
let start = i
while (i < len && isWordChar(code[i])) i++
const word = code.slice(start, i)
if (keywords.has(word)) {
result += '<span class="hl-keyword">' + escapeHtml(word) + '</span>'
} else if (builtins.has(word)) {
result += '<span class="hl-preprocessor">' + escapeHtml(word) + '</span>'
} else {
result += escapeHtml(word)
}
continue
}
// 其他字符
result += escapeHtml(code[i])
i++
}
return result
}
function escapeHtml(str) {
return str
.replace(/&/g, '&amp;')
+74 -3
View File
@@ -77,14 +77,28 @@
<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
:filePath="currentFile.path"
:fileName="currentFile.name"
:key="currentFilePath"
:filePath="currentFilePath"
:fileName="currentFileName"
:fileLabel="currentFile.label"
:description="currentFile.description"
:language="language"
/>
</div>
</div>
@@ -113,6 +127,30 @@ const props = defineProps({
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))
@@ -127,6 +165,7 @@ function selectFile(file) {
function closeDrawer() {
currentFile.value = null
language.value = 'c'
}
function getFolderDescription(folderName) {
@@ -383,7 +422,7 @@ function getFolderDescription(folderName) {
.drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 20px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
@@ -393,6 +432,38 @@ function getFolderDescription(folderName) {
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 {