Compare commits

...

34 Commits

Author SHA1 Message Date
5a77902750 fix style
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-12-20 22:28:09 +08:00
ec46928689 update
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-12-20 22:24:14 +08:00
727dc0a8e8 update
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-10-27 19:27:21 +08:00
475a09298d remove url
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-10-22 18:46:04 +08:00
9b1abd7a25 fix
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-10-22 13:21:58 +08:00
953ca3720f update
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-10-22 12:41:06 +08:00
9470614588 fix
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-10-22 12:29:27 +08:00
bc2db54575 update
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-10-22 11:17:12 +08:00
4306e555bb update
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-10-22 02:33:08 +08:00
0d600382d3 update
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-10-22 00:23:05 +08:00
9164fff6c2 update
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-10-22 00:18:27 +08:00
64eeffd041 fix
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-10-21 23:03:25 +08:00
cfeac2cdaa markdown style
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-10-21 22:38:16 +08:00
53ae1a8ef8 fix
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-10-21 21:54:59 +08:00
e96611c62b disabled maxkb
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-10-21 19:00:53 +08:00
9758322f27 fix
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-09-28 15:30:48 +08:00
c6d2e17476 添加AI分析
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-09-28 10:51:04 +08:00
0f0312529b fix
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-09-26 10:25:50 +08:00
b33b0ee110 update
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-09-24 19:14:30 +08:00
dea523cb15 update
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-09-16 18:08:46 +08:00
4b59d1cf17 add turtle
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-09-16 17:37:43 +08:00
d9bcb81109 update
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-09-10 13:47:13 +08:00
80d279365c add cpp
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-09-10 13:46:12 +08:00
044b33f39c update
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-09-05 13:26:59 +08:00
b763eb60cd update
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2025-09-05 09:18:32 +08:00
07847d351d maxkb 2025-08-22 23:46:01 +08:00
e0944e50d1 fix 2025-08-22 22:57:41 +08:00
bb93d717b8 fix 2025-08-22 22:54:04 +08:00
e188cca3af update 2025-08-22 22:29:21 +08:00
0d824026a5 update 2025-08-22 20:37:39 +08:00
09328a3147 update 2025-07-16 09:22:03 +08:00
5240e029c5 update 2025-05-13 12:01:14 +08:00
c09323cc8f update 2025-05-09 15:30:03 +08:00
b91d7405fc update 2025-05-09 10:47:29 +08:00
33 changed files with 2990 additions and 2879 deletions

5
.env
View File

@@ -1,4 +1,3 @@
PUBLIC_JUDGE0API_URL=https://judge0api.xuyue.cc
PUBLIC_MAXKB_URL=https://maxkb.xuyue.cc/api/application/embed?protocol=https&host=maxkb.xuyue.cc&token=1b7cd529423b3f36
PUBLIC_CODEAPI_URL=https://codeapi.xuyue.cc
PUBLIC_PYVIZ_URL=https://pyviz.xuyue.cc
PUBLIC_MAXKB_URL=https://maxkb.xuyue.cc/chat/api/embed?protocol=https&host=maxkb.xuyue.cc&token=2e801f7d6efdcc99
PUBLIC_CODEAPI_URL=http://localhost:8080

View File

@@ -1,4 +1,3 @@
PUBLIC_JUDGE0API_URL=https://judge0api.xuyue.cc
PUBLIC_MAXKB_URL=https://maxkb.xuyue.cc/api/application/embed?protocol=https&host=maxkb.xuyue.cc&token=1b7cd529423b3f36
PUBLIC_CODEAPI_URL=https://codeapi.xuyue.cc
PUBLIC_PYVIZ_URL=https://pyviz.xuyue.cc
PUBLIC_MAXKB_URL=https://maxkb.xuyue.cc/chat/api/embed?protocol=https&host=maxkb.xuyue.cc&token=2e801f7d6efdcc99
PUBLIC_CODEAPI_URL=https://code.xuyue.cc/api

View File

@@ -1,5 +1,4 @@
PUBLIC_JUDGE0API_URL=http://10.13.114.214:8082
PUBLIC_MAXKB_URL=
PUBLIC_CODEAPI_URL=http://10.13.114.214:8092
PUBLIC_PYVIZ_URL=http://10.13.114.214:9000
PUBLIC_ICONIFY=http://10.13.114.214:8098
PUBLIC_JUDGE0API_URL=http://10.13.114.114:8082
PUBLIC_MAXKB_URL=http://10.13.114.114:92/chat/api/embed?protocol=http&host=10.13.114.114:92&token=dd37457027c40b39
PUBLIC_CODEAPI_URL=http://10.13.114.114:82/api
PUBLIC_ICONIFY_URL=http://10.13.114.114:8098

View File

@@ -1,2 +1 @@
semi=false
plugins=["prettier-plugin-organize-imports"]

View File

@@ -2,18 +2,18 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="shortcut icon" href="/noto--cat-face.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>自测猫</title>
<link rel="stylesheet" href="/style.css" />
<script>
window.localStorage.setItem("maxkbMaskTip", true)
</script>
<script
<!-- <script
async
defer
src="<%= import.meta.env.PUBLIC_MAXKB_URL %>"
></script>
></script> -->
</head>
<body>
<div id="app"></div>

3909
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "code-next",
"private": true,
"version": "1.2.0",
"version": "1.3.1",
"type": "module",
"scripts": {
"start": "rsbuild dev",
@@ -10,30 +10,29 @@
"fmt": "prettier --write src"
},
"dependencies": {
"@codemirror/lang-cpp": "^6.0.2",
"@codemirror/lang-python": "^6.2.0",
"@vueuse/core": "^13.1.0",
"axios": "^1.9.0",
"client-zip": "1.7.0",
"codemirror": "^6.0.1",
"copy-text-to-clipboard": "^3.2.0",
"@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-python": "^6.2.1",
"@vueuse/core": "^14.1.0",
"axios": "^1.13.2",
"client-zip": "2.5.0",
"codemirror": "^6.0.2",
"copy-text-to-clipboard": "^3.2.2",
"fflate": "^0.8.2",
"file-saver": "^2.0.5",
"naive-ui": "^2.41.0",
"marked": "^17.0.1",
"naive-ui": "^2.43.2",
"normalize.css": "^8.0.1",
"query-string": "^9.1.2",
"vue": "^3.5.13",
"query-string": "^9.3.1",
"skulpt": "^1.2.0",
"vue": "^3.5.26",
"vue-codemirror": "^6.1.1"
},
"devDependencies": {
"@iconify/vue": "^5.0.0",
"@rsbuild/core": "^1.3.17",
"@rsbuild/plugin-vue": "^1.0.7",
"@rsbuild/core": "^1.6.15",
"@rsbuild/plugin-vue": "^1.2.2",
"@types/file-saver": "^2.0.7",
"@vitejs/plugin-vue": "^5.2.3",
"prettier": "^3.5.3",
"prettier-plugin-organize-imports": "^4.1.0",
"typescript": "^5.8.3",
"vite": "^6.2.2"
"prettier": "^3.7.4",
"typescript": "^5.9.3"
}
}

1
public/cpp.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="#659ad2" d="M29 10.232a2.4 2.4 0 0 0-.318-1.244a2.45 2.45 0 0 0-.936-.879q-5.194-2.868-10.393-5.733a2.64 2.64 0 0 0-2.763.024c-1.378.779-8.275 4.565-10.331 5.706A2.29 2.29 0 0 0 3 10.231V21.77a2.4 2.4 0 0 0 .3 1.22a2.43 2.43 0 0 0 .954.9c2.056 1.141 8.954 4.927 10.332 5.706a2.64 2.64 0 0 0 2.763.026q5.19-2.871 10.386-5.733a2.44 2.44 0 0 0 .955-.9a2.4 2.4 0 0 0 .3-1.22V10.232"/><path fill="#00599c" d="M28.549 23.171a2 2 0 0 0 .147-.182a2.4 2.4 0 0 0 .3-1.22V10.232a2.4 2.4 0 0 0-.318-1.244c-.036-.059-.089-.105-.13-.16L16 16Z"/><path fill="#004482" d="M28.549 23.171L16 16L3.451 23.171a2.4 2.4 0 0 0 .809.72c2.056 1.141 8.954 4.927 10.332 5.706a2.64 2.64 0 0 0 2.763.026q5.19-2.871 10.386-5.733a2.4 2.4 0 0 0 .808-.719"/><path fill="#fff" d="M19.6 18.02a4.121 4.121 0 1 1-.027-4.087l3.615-2.073A8.309 8.309 0 0 0 7.7 16a8.2 8.2 0 0 0 1.1 4.117a8.319 8.319 0 0 0 14.411-.017z"/><path fill="#fff" d="M24.076 15.538h-.926v-.921h-.925v.921h-.926v.923h.926v.92h.925v-.92h.926zm3.473 0h-.926v-.921h-.926v.921h-.926v.923h.926v.92h.926v-.92h.926z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/noto--cat-face.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

1
public/turtle.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"><path fill="#bdcf47" d="M112.7 59.21s3.94-2.21 4.93-2.77s4.6-2.82 5.91-.84c.77 1.16-.7 4.44-3.05 7.86c-2.14 3.13-7.12 9.56-7.4 10.83s1.11 6.36 1.53 8.33s1.74 6.71 1.17 8.54s-3.43 6.85-10.75 6.76c-5.82-.07-7.51-1.78-7.7-2.82c-.14-.75-.56-3.24-.56-3.24s-4.79 2.96-7.04 4.08s-8.31 4.22-8.31 4.22s1.17 5.35 1.36 7.51s.86 5.25-.28 7.32c-1.03 1.88-4.25 5.02-11.83 4.97c-5.92-.04-7.41-1.88-8.35-3c-.94-1.13-1.13-6.48-1.13-7.6s-.19-5.07-.19-5.07s-8.02-.4-12.86-.75c-4.38-.32-10.16-.99-10.16-.99s.21 2.33.42 4.01c.19 1.5.23 4.64-1.34 6.17c-2.11 2.06-7.56 2.21-10.56 1.92c-3-.28-7.18-1.83-8.4-4.55s.38-6.29 1.03-8.35c.58-1.81 1.6-4.41 1.22-5.16s-4.04-1.69-9.29-6.95c-5.26-5.26-12.13-23.52 3.28-36.23c15.49-12.76 43.81 1.1 45.31 2.04c1.54.96 53.04 3.76 53.04 3.76"/><path fill="#6e823a" d="M66.25 25.28c-13.93.62-24.38 7.52-29.57 15.06c-3.1 4.5-4.65 7.74-4.65 7.74s4.81.14 9.15 2.46c5 2.67 10.8 5.56 14.61 18.13c2.87 9.5 3.98 18.53 11.44 20.52c8.45 2.25 28.16 1.13 37.59-8.02s11.26-16.05 8.87-25.06s-13.17-25.05-28.16-29.28C79.06 25 72.58 25 66.25 25.28"/><path fill="#484e23" d="M111.93 51.32c-.42-.99-1.3-2.5-1.3-2.5s-.07 2.05-.25 3.13c-.28 1.76-1.25 5.42-1.81 4.88c-1-.97-5.73-6.92-7.98-10.23c-1.71-2.52-7.6-9.11-7.74-11.26c-.07-1.06 1.27-4.65 1.27-4.65s-1.22-.7-2.35-1.34c-.88-.49-2.16-1.03-2.16-1.03s-.77 4.9-1.62 5.82c-.75.81-5.32 2.6-8.87 3.94c-4.29 1.62-8.45 3.73-10 4.01c-1.36.25-9.09-1.41-12-1.97c-3.66-.7-9.18-2.26-10.45-3.17c-1.48-1.06-3.07-3.78-3.07-3.78s-.89.61-1.78 1.31c-.88.69-2.02 2.06-2.02 2.06s2.31 2.32 2.44 3.18c.18 1.2-1.27 2.83-2.46 4.38c-.72.93-2.75 4.85-2.75 4.85s.97.09 2.15.63c1.23.57 2.38 1.16 2.38 1.16s2.97-6.9 4.9-7.53c1.65-.54 6.3.99 9.68 1.69c4.79.99 9.64 1.87 10.66 3.17c1.06 1.34 2.06 6.68 3.03 11.19C70.89 64.2 73.64 77.02 73 78c-.63.99-5.7.63-8.59.28c-2.45-.3-6.41-1.76-6.41-1.76s.58 2.11.77 2.67c.28.81 1.16 3.06 1.16 3.06s5.67 2.5 22.42.95s25.03-12.96 27.38-18.02c3.14-6.78 3.54-10.39 3.54-10.39s-.92-2.48-1.34-3.47M96.65 73.21c-4.24 2.67-15.2 5.49-17.18 4.43c-1.58-.85-3.94-13.94-5.07-19.78c-.72-3.74-2.45-9.42-1.41-11.19c.7-1.2 4.79-2.99 7.81-4.4c2.87-1.33 6.97-3.13 8.17-2.99c1.7.2 5.35 6.12 9.01 11.19s7.67 10.35 7.74 12.18c.09 1.84-4.7 7.82-9.07 10.56"/><path fill="#2a2b28" d="M41.18 65.86c.5 2.83-.95 5.75-4.07 6.02c-2.56.22-4.59-1.57-5.09-4.4s1.14-5.49 3.68-5.94c2.52-.45 4.98 1.48 5.48 4.32m-18.36.25c.07 2.84-2.42 5.69-5.5 5.11c-2.53-.48-3.99-2.73-3.71-5.55c.29-2.82 2.59-4.9 5.15-4.65s3.99 2.13 4.06 5.09m7.95 10.48c1.16-.79 3.1-2.67 4.36-1.06c1.27 1.62-.92 3.1-2.18 4.01c-1.27.92-4.08 3.17-6.12 3.17c-1.9 0-4.79-2.32-6.62-3.87c-1.49-1.26-2.18-2.89-1.34-3.87s2.14-.62 3.24.35c1.27 1.13 3.72 3.38 4.72 3.38c.98.01 2.39-1.05 3.94-2.11"/></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -68,6 +68,13 @@ export async function createCode(data: { code: string; query: string }) {
}
export async function removeCode(id: number) {
const res = await api.delete(`/${id}`)
console.log(res.data)
await api.delete(`/${id}`)
}
export async function debug(code: string, inputs: string[]) {
const res = await api.post("/debug", {
code,
inputs,
})
return res.data
}

View File

@@ -52,7 +52,7 @@ watch(
)
const lang = computed(() => {
if (props.language === "python") {
if (props.language === "python" || props.language === "turtle") {
return python()
}
return cpp()

View File

@@ -0,0 +1,265 @@
<script lang="ts" setup>
import { cpp } from "@codemirror/lang-cpp"
import { python } from "@codemirror/lang-python"
import { EditorState } from "@codemirror/state"
import { EditorView, Decoration, DecorationSet } from "@codemirror/view"
import { StateField, StateEffect } from "@codemirror/state"
import { useDark } from "@vueuse/core"
import { computed, ref, watch } from "vue"
import { Codemirror } from "vue-codemirror"
import { oneDark } from "../themes/oneDark"
import { smoothy } from "../themes/smoothy"
import { LANGUAGE } from "../types"
interface Props {
modelValue: string
language?: LANGUAGE
fontSize?: number
currentLine?: number
nextLine?: number
currentLineText?: string
nextLineText?: string
}
const props = withDefaults(defineProps<Props>(), {
language: "python",
fontSize: 24,
})
const code = ref(props.modelValue)
const isDark = useDark()
const editorView = ref<EditorView>()
// 定义高亮效果
const setHighlight = StateEffect.define<{
currentLine?: number
nextLine?: number
currentLineText?: string
nextLineText?: string
}>()
// 高亮状态字段
const highlightField = StateField.define<DecorationSet>({
create() {
return Decoration.none
},
update(decorations, tr) {
decorations = decorations.map(tr.changes)
for (let effect of tr.effects) {
if (effect.is(setHighlight)) {
decorations = Decoration.none
if (effect.value.currentLine || effect.value.nextLine) {
const decorations_array: any[] = []
// 当前行高亮(绿色)
if (effect.value.currentLine) {
try {
const line = tr.state.doc.line(effect.value.currentLine)
decorations_array.push(
Decoration.line({
class: "cm-current-line",
}).range(line.from),
)
// 在当前行添加文字 - 使用行装饰而不是Widget
if (effect.value.currentLineText) {
decorations_array.push(
Decoration.line({
class: "cm-current-line-with-text",
attributes: {
"data-text": effect.value.currentLineText,
},
}).range(line.from),
)
}
} catch (e) {
console.warn(
"Invalid line number for current line:",
effect.value.currentLine,
)
}
}
// 下一步行高亮(红色)
if (effect.value.nextLine) {
try {
const line = tr.state.doc.line(effect.value.nextLine)
decorations_array.push(
Decoration.line({
class: "cm-next-line",
}).range(line.from),
)
// 在下一步行添加文字
if (effect.value.nextLineText) {
decorations_array.push(
Decoration.line({
class: "cm-next-line-with-text",
attributes: {
"data-text": effect.value.nextLineText,
},
}).range(line.from),
)
}
} catch (e) {
console.warn(
"Invalid line number for next line:",
effect.value.nextLine,
)
}
}
// 确保装饰按位置排序,避免重复
decorations_array.sort((a, b) => a.from - b.from)
decorations = Decoration.set(decorations_array)
}
}
}
return decorations
},
provide: (f) => EditorView.decorations.from(f),
})
const styleTheme = EditorView.baseTheme({
"& .cm-scroller": {
"font-family": "Monaco",
height: "calc(100vh - 120px)",
},
"&.cm-editor.cm-focused": {
outline: "none",
},
"&.cm-editor .cm-tooltip.cm-tooltip-autocomplete ul": {
"font-family": "Monaco",
},
// 当前行高亮样式(绿色)
"& .cm-current-line": {
"background-color": "rgba(0, 255, 0, 0.2)",
"border-left": "3px solid #00ff00",
},
// 下一步行高亮样式(红色)
"& .cm-next-line": {
"background-color": "rgba(255, 0, 0, 0.2)",
"border-left": "3px solid #ff0000",
},
// 当前行带文字样式
"& .cm-current-line-with-text": {
position: "relative",
"&::after": {
content: "attr(data-text)",
position: "absolute",
right: "8px",
top: "50%",
transform: "translateY(-50%)",
"background-color": "rgba(0, 255, 0, 0.8)",
color: "#000",
padding: "2px 6px",
"border-radius": "3px",
"font-size": "12px",
"font-weight": "bold",
"white-space": "nowrap",
"z-index": "10",
},
},
// 下一步行带文字样式
"& .cm-next-line-with-text": {
position: "relative",
"&::after": {
content: "attr(data-text)",
position: "absolute",
right: "8px",
top: "50%",
transform: "translateY(-50%)",
"background-color": "rgba(255, 0, 0, 0.8)",
color: "#fff",
padding: "2px 6px",
"border-radius": "3px",
"font-size": "12px",
"font-weight": "bold",
"white-space": "nowrap",
"z-index": "10",
},
},
})
const emit = defineEmits(["update:modelValue", "ready"])
watch(
() => props.modelValue,
(v) => {
code.value = v
},
)
const lang = computed(() => {
if (props.language === "python" || props.language === "turtle") {
return python()
}
return cpp()
})
function onChange(v: string) {
emit("update:modelValue", v)
}
function onReady(payload: {
view: EditorView
state: EditorState
container: HTMLDivElement
}) {
editorView.value = payload.view
emit("ready", payload.view)
// Editor 准备好后立即设置高亮
updateHighlight()
}
// 更新高亮的函数
function updateHighlight() {
if (editorView.value) {
console.log(
"Updating highlight - currentLine:",
props.currentLine,
"nextLine:",
props.nextLine,
)
// 如果当前行和下一步相同,只高亮当前行,不显示下一步
const nextLine =
props.currentLine === props.nextLine ? undefined : props.nextLine
editorView.value.dispatch({
effects: setHighlight.of({
currentLine: props.currentLine,
nextLine: nextLine,
currentLineText: props.currentLineText,
nextLineText: props.nextLineText,
}),
})
}
}
// 监听 props 变化并更新高亮
watch(
() => [
props.currentLine,
props.nextLine,
props.currentLineText,
props.nextLineText,
],
() => {
updateHighlight()
},
)
</script>
<template>
<Codemirror
v-model="code"
indentWithTab
:extensions="[styleTheme, lang, highlightField, isDark ? oneDark : smoothy]"
:tabSize="4"
:style="{
fontSize: props.fontSize + 'px',
}"
@change="onChange"
@ready="onReady"
/>
</template>

View File

@@ -0,0 +1,188 @@
<script lang="ts" setup>
import { computed } from "vue"
import { Icon } from "@iconify/vue"
interface Props {
visible: boolean
variables?: Record<string, any>
output?: string
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
variables: () => ({}),
output: "",
})
const emit = defineEmits(["close"])
// 格式化变量显示
const formattedVariables = computed(() => {
if (!props.variables || Object.keys(props.variables).length === 0) {
return []
}
return Object.entries(props.variables).map(([key, value]) => {
// 处理特殊类型
let displayValue = ""
let displayType = typeof value
if (
Array.isArray(value) &&
value.length === 2 &&
value[0] === "IMPORTED_FAUX_PRIMITIVE" &&
value[1] === "imported object"
) {
displayValue = ""
displayType = "function"
} else if (typeof value === "object" && value !== null) {
displayValue = JSON.stringify(value, null, 2)
} else {
displayValue = String(value)
}
return {
name: key,
value: displayValue,
type: displayType,
}
})
})
// 格式化输出显示
const formattedOutput = computed(() => {
if (!props.output) return ""
return props.output
})
// 计算输出行数
const outputLines = computed(() => {
if (!props.output) return 0
return props.output.split("\n").filter((line) => line !== "").length
})
function closePanel() {
emit("close")
}
</script>
<template>
<n-card v-if="visible" class="floating-panel" :bordered="true" size="small">
<template #header>
<n-flex justify="space-between" align="center">
<n-flex align="center">
<n-icon>
<Icon icon="mdi:bug" :width="16" :height="16" />
</n-icon>
<n-text strong>调试信息</n-text>
</n-flex>
<n-button quaternary circle size="small" @click="closePanel">
<template #icon>
<n-icon>
<Icon icon="mdi:close" :width="16" :height="16" />
</n-icon>
</template>
</n-button>
</n-flex>
</template>
<n-space vertical :size="16" class="panel-content">
<!-- 变量部分 -->
<n-collapse :default-expanded-names="['variables']">
<n-collapse-item title="变量" name="variables">
<n-scrollbar style="max-height: 260px">
<template #header>
<n-flex align="center">
<n-icon>
<Icon icon="mdi:variable" :width="14" :height="14" />
</n-icon>
<n-text>变量</n-text>
</n-flex>
</template>
<div v-if="formattedVariables.length === 0">
<n-text type="info" class="no-variables-text">暂无变量</n-text>
</div>
<n-space v-else vertical>
<n-card
v-for="variable in formattedVariables"
:key="variable.name"
size="small"
:bordered="true"
>
<n-flex
justify="space-between"
align="center"
class="variable-header"
>
<n-text strong :type="'primary'">{{ variable.name }}</n-text>
<n-tag size="small" type="info">{{ variable.type }}</n-tag>
</n-flex>
<n-text code class="variable-value">
{{ variable.value }}
</n-text>
</n-card>
</n-space>
</n-scrollbar>
</n-collapse-item>
</n-collapse>
<!-- 输出部分 -->
<n-collapse v-if="formattedOutput" :default-expanded-names="['output']">
<n-collapse-item :title="`输出 (${outputLines} 行)`" name="output">
<template #header>
<n-flex align="center">
<n-icon>
<Icon icon="mdi:console" :width="14" :height="14" />
</n-icon>
<n-text>输出({{ outputLines }})</n-text>
</n-flex>
</template>
<n-card size="small" :bordered="true">
<n-scrollbar style="max-height: 300px">
<n-text code class="output-text">
{{ formattedOutput }}
</n-text>
</n-scrollbar>
</n-card>
</n-collapse-item>
</n-collapse>
</n-space>
</n-card>
</template>
<style scoped>
.floating-panel {
width: 300px;
position: absolute;
top: 20px;
right: 120px;
z-index: 100;
}
.output-text {
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
display: block;
}
.panel-content {
padding: 16px;
}
.no-variables-text {
text-align: center;
display: block;
padding: 20px;
}
.variable-header {
margin-bottom: 8px;
}
.variable-value {
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
}
</style>

View File

@@ -1,14 +1,31 @@
<script lang="ts" setup>
import type { SelectOption } from "naive-ui"
import { h } from "vue"
import { h, computed, watch } from "vue"
import { code } from "../composables/code"
import { isMobile } from "../composables/breakpoints"
const LANGS = [
const LANGS = computed(() => {
const allLangs = [
["python", "Python"],
["turtle", "海龟绘图"],
["c", "C 语言"],
["cpp", "C++"],
]
if (isMobile.value) {
return allLangs.filter(([lang]) => lang !== "turtle")
}
return allLangs
})
const languages: SelectOption[] = LANGS.map((it) => ({
// 如果当前在移动端且语言是海龟绘图,自动切换到 Python
watch(isMobile, (mobile) => {
if (mobile && code.language === "turtle") {
code.language = "python"
}
})
const languages = computed<SelectOption[]>(() =>
LANGS.value.map((it) => ({
value: it[0],
label: () => [
h("img", {
@@ -22,7 +39,8 @@ const languages: SelectOption[] = LANGS.map((it) => ({
}),
it[1],
],
}))
})),
)
</script>
<template>
<n-select
@@ -34,6 +52,6 @@ const languages: SelectOption[] = LANGS.map((it) => ({
</template>
<style scoped>
.select {
width: 120px;
width: 125px;
}
</style>

View File

@@ -1,53 +0,0 @@
import { computed, reactive } from "vue"
import { Status } from "../types"
import { output, status } from "./code"
export const analyse = reactive({
line: -1,
message: "",
})
export const showAnalyse = computed(
() => ![Status.Accepted, Status.NotStarted].includes(status.value),
)
function findError(line: string, language = "python") {
const python: any = {
"EOFError: EOF when reading a line": "需要在输入框填写输入信息",
"SyntaxError: invalid character in identifier":
"可能是单词拼写错误,可能是括号、引号写成中文的了",
"SyntaxError: invalid syntax": "语法错误,不合法的语法",
"SyntaxError: EOL while scanning string literal":
"可能是这一行最后一个符号是中文的,或者引号、括号不匹配",
"NameError: name '(.*?)' is not defined": (name: string) =>
`命名错误,${name} 不知道是什么东西`,
"IndentationError: expected an indented block": "缩进错误:这一行需要缩进",
'TypeError: can only concatenate str \\(not "(.*?)"\\) to str':
"文字和数字不能相加",
}
const c: any = {}
const regex = { c, python }[language]
let message = ""
for (let r in regex) {
const err = line.match(r)
if (err) {
if (typeof regex[r] === "function") {
message = regex[r](err[1])
} else {
message = regex[r]
}
break
}
}
return message
}
export function analyzeError() {
const line = output.value.match(/File "script.py", line (\d+)/)
if (line) {
analyse.line = parseInt(line[1])
}
const lines = output.value.split("\n")
const lastLine = lines[lines.length - 1]
analyse.message = findError(lastLine)
}

153
src/composables/analysis.ts Normal file
View File

@@ -0,0 +1,153 @@
import { computed, ref } from "vue"
import { Status } from "../types"
import { output, status, code } from "./code"
export const analysis = ref("")
export const loading = ref(false)
export async function getAIAnalysis() {
analysis.value = ""
// 使用 streaming 流式方式 fetch /ai 接口,传入 code 和 error_info
const baseUrl = import.meta.env.PUBLIC_CODEAPI_URL
loading.value = true
try {
const response = await fetch(`${baseUrl}/ai`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code: code.value,
language: code.language,
error_info: output.value,
}),
})
const reader = response.body?.getReader()
if (!reader) return
const decoder = new TextDecoder()
let buffer = ""
let eventLines: string[] = []
let currentEvent: string | null = null
const flushEvent = () => {
if (eventLines.length === 0) return false
const raw = eventLines.join("\n")
eventLines = []
const event = currentEvent ?? "message"
currentEvent = null
if (!raw) return false
let payload: unknown
try {
payload = JSON.parse(raw)
} catch (error) {
// eslint-disable-next-line no-console
console.error("无法解析 SSE 数据", error, raw)
return false
}
const data = (payload as { data?: string }).data ?? ""
const message = (payload as { message?: string }).message ?? ""
if (event === "chunk") {
appendContent(data)
return false
}
if (event === "error") {
if (loading.value) {
loading.value = false
}
if (message) {
appendContent(`\n[错误] ${message}`)
}
return true
}
if (event === "done") {
if (loading.value) {
loading.value = false
}
return true
}
appendContent(data || message)
return false
}
const processLine = (line: string) => {
if (line === "") {
return flushEvent()
}
if (line.startsWith("event:")) {
currentEvent = line.slice(6).trimStart()
return false
}
if (!line.startsWith("data:")) return false
let value = line.slice(5)
if (value.startsWith(" ")) {
value = value.slice(1)
}
eventLines.push(value)
return false
}
const processBuffer = (final = false) => {
const lines = buffer.split("\n")
if (!final) {
buffer = lines.pop() ?? ""
} else {
buffer = ""
}
for (const line of lines) {
const shouldStop = processLine(line)
if (shouldStop) {
return true
}
}
if (final) {
return processLine("")
}
return false
}
const appendContent = (segment: string) => {
if (!segment) return
analysis.value += segment
if (loading.value) {
loading.value = false
}
}
while (true) {
const { done, value } = (await reader.read()) as ReadableStreamReadResult<
Uint8Array<ArrayBuffer>
>
if (done) break
buffer += decoder.decode(value, { stream: true })
if (processBuffer()) {
return
}
}
if (processBuffer(true)) {
return
}
} finally {
if (loading.value) {
loading.value = false
}
}
}
export const showAnalysis = computed(
() => ![Status.Accepted, Status.NotStarted].includes(status.value),
)

View File

@@ -17,6 +17,8 @@ const cache: Cache = {
code: {
python: useStorage("code_python", sources["python"]),
c: useStorage("code_c", sources["c"]),
cpp: useStorage("code_cpp", sources["cpp"]),
turtle: useStorage("code_turtle", sources["turtle"]),
},
}
@@ -28,8 +30,8 @@ export const input = ref("")
export const output = ref("")
export const status = ref(Status.NotStarted)
export const loading = ref(false)
export const turtleRunId = ref(0)
export const size = ref(0)
export const debug = ref(false)
watch(size, (value: number) => {
cache.fontsize.value = value
@@ -69,9 +71,14 @@ export async function init() {
if (base64) {
try {
const data = JSON.parse(atou(base64))
code.language = data.lang
code.value = data.code
input.value = data.input
const lang = ["python", "c", "cpp", "turtle"].includes(data.lang)
? (data.lang as LANGUAGE)
: defaultLanguage
const sharedCode = data.code ?? sources[lang]
cache.code[lang].value = sharedCode
code.language = lang
code.value = sharedCode
input.value = typeof data.input === "string" ? data.input : ""
} catch (err) {}
}
const preset = parsed.query as string
@@ -99,7 +106,13 @@ export function reset() {
export async function run() {
loading.value = true
const cleanCode = code.value.trim()
if (!cleanCode) return
if (!cleanCode) {
loading.value = false
return
}
if (code.language === "turtle") {
turtleRunId.value++
} else {
output.value = ""
status.value = Status.NotStarted
const result = await submit(
@@ -108,6 +121,7 @@ export async function run() {
)
output.value = result.output || ""
status.value = result.status
}
loading.value = false
}

View File

@@ -21,15 +21,23 @@ export function reset() {
}))
}
export function addFive() {
export function add(len = 1) {
if (len == 1) {
files.value.push({
in: "",
out: "",
error: false,
})
} else {
files.value.push(
...Array.from({ length: 5 }).map(() => ({
...Array.from({ length: len }).map(() => ({
in: "",
out: "",
error: false,
})),
)
}
}
export function remove(index: number) {
files.value = files.value.filter((_, i) => i !== index)

View File

@@ -0,0 +1,172 @@
<script lang="ts" setup>
import { marked } from "marked"
interface Props {
analysis: string
loading: boolean
}
defineProps<Props>()
</script>
<template>
<n-spin :show="loading">
<div class="analysisPanel" v-html="marked.parse(analysis)"></div>
</n-spin>
</template>
<style scoped>
.analysisPanel {
width: 400px;
min-height: 60px;
max-height: calc(100vh - 200px);
overflow: auto;
padding: 16px;
border-radius: 8px;
line-height: 1.6;
color: #374151;
}
/* 简洁 Markdown 样式 */
.analysisPanel :deep(h1),
.analysisPanel :deep(h2),
.analysisPanel :deep(h3),
.analysisPanel :deep(h4),
.analysisPanel :deep(h5),
.analysisPanel :deep(h6) {
margin: 16px 0 8px 0;
font-weight: 600;
color: #1f2937;
}
.analysisPanel :deep(h1) {
font-size: 1.5em;
border-bottom: 2px solid #e5e7eb;
padding-bottom: 8px;
}
.analysisPanel :deep(h2) {
font-size: 1.3em;
color: #374151;
}
.analysisPanel :deep(h3) {
font-size: 1.1em;
color: #4b5563;
}
.analysisPanel :deep(p) {
margin: 12px 0;
color: #374151;
font-size: 14px;
}
.analysisPanel :deep(ul),
.analysisPanel :deep(ol) {
margin: 12px 0;
padding-left: 20px;
}
.analysisPanel :deep(li) {
margin: 4px 0;
color: #374151;
}
.analysisPanel :deep(strong) {
font-weight: 600;
color: #1f2937;
}
.analysisPanel :deep(em) {
font-style: italic;
color: #6b7280;
}
/* 简洁代码块样式 */
.analysisPanel :deep(pre) {
background: #f8f9fa;
color: #24292f;
padding: 16px;
border-radius: 6px;
margin: 12px 0;
overflow-x: auto;
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
font-size: 13px;
line-height: 1.5;
border: 1px solid #d0d7de;
}
.analysisPanel :deep(code) {
background: #f1f3f4;
color: #d63384;
padding: 2px 6px;
border-radius: 3px;
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
font-size: 0.9em;
}
.analysisPanel :deep(pre code) {
background: transparent;
color: inherit;
padding: 0;
border-radius: 0;
}
/* 简洁引用块样式 */
.analysisPanel :deep(blockquote) {
border-left: 4px solid #d0d7de;
background: #f6f8fa;
margin: 16px 0;
padding: 12px 16px;
border-radius: 0 6px 6px 0;
color: #656d76;
font-style: italic;
}
/* 简洁表格样式 */
.analysisPanel :deep(table) {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
background: #ffffff;
border-radius: 6px;
overflow: hidden;
border: 1px solid #d0d7de;
}
.analysisPanel :deep(th),
.analysisPanel :deep(td) {
padding: 12px;
text-align: left;
border-bottom: 1px solid #d0d7de;
}
.analysisPanel :deep(th) {
background: #f6f8fa;
font-weight: 600;
color: #24292f;
}
.analysisPanel :deep(td) {
color: #24292f;
}
/* 简洁链接样式 */
.analysisPanel :deep(a) {
color: #0969da;
text-decoration: none;
}
.analysisPanel :deep(a:hover) {
color: #0969da;
text-decoration: underline;
}
/* 简洁分割线 */
.analysisPanel :deep(hr) {
border: none;
height: 1px;
background: #d0d7de;
margin: 16px 0;
}
</style>

View File

@@ -0,0 +1,35 @@
<script lang="ts" setup>
import copyTextToClipboard from "copy-text-to-clipboard"
import { useMessage } from "naive-ui"
import CodeEditor from "../components/CodeEditor.vue"
import { code, input, reset, size } from "../composables/code"
import { debug } from "../api"
const message = useMessage()
function copy() {
copyTextToClipboard(code.value)
message.success("已经复制好了")
}
async function handleDebug() {
const inputs = input.value ? input.value.split("\n") : []
const res = await debug(code.value, inputs)
console.log(res.data)
}
</script>
<template>
<CodeEditor
label="代码区"
icon="streamline-emojis:lemon"
:font-size="size"
v-model="code.value"
:language="code.language"
>
<template #actions>
<n-button quaternary type="primary" @click="copy">复制</n-button>
<n-button quaternary @click="reset">清空</n-button>
</template>
</CodeEditor>
</template>

View File

@@ -1,122 +1,36 @@
<script lang="ts" setup>
import copyTextToClipboard from "copy-text-to-clipboard"
import { useMessage } from "naive-ui"
import { computed } from "vue"
import CodeEditor from "../components/CodeEditor.vue"
import { analyse, analyzeError, showAnalyse } from "../composables/analyse"
import {
clearInput,
code,
debug,
input,
output,
reset,
size,
status,
} from "../composables/code"
import { Status } from "../types"
const showInputClearBtn = computed(() => !!input.value)
const message = useMessage()
function copy() {
copyTextToClipboard(code.value)
message.success("已经复制好了")
}
function handleDebug() {
debug.value = true
}
import { code } from "../composables/code"
import CodeSection from "./CodeSection.vue"
import DebugSection from "./DebugSection.vue"
import InputSection from "./InputSection.vue"
import OutputSection from "./OutputSection.vue"
import TurtleSection from "./TurtleSection.vue"
</script>
<template>
<n-layout-content class="container">
<n-split direction="horizontal" :min="1 / 3" :max="4 / 5">
<template #1>
<CodeEditor
label="代码区"
icon="streamline-emojis:lemon"
:font-size="size"
v-model="code.value"
:language="code.language"
>
<template #actions>
<n-button
quaternary
type="error"
:disabled="!code.value"
v-if="code.language === 'python'"
@click="handleDebug"
>
调试
</n-button>
<n-button quaternary type="primary" @click="copy">复制</n-button>
<n-button quaternary @click="reset">清空</n-button>
</template>
</CodeEditor>
<component
:is="code.language === 'python' ? DebugSection : CodeSection"
/>
</template>
<template #2>
<n-split
v-if="code.language !== 'turtle'"
direction="vertical"
:default-size="1 / 3"
:min="1 / 5"
:max="3 / 5"
>
<template #1>
<CodeEditor
icon="streamline-emojis:four-leaf-clover"
label="输入框"
:font-size="size"
v-model="input"
>
<template #actions>
<n-button
quaternary
type="primary"
@click="clearInput"
v-if="showInputClearBtn"
>
清空
</n-button>
</template>
</CodeEditor>
<InputSection />
</template>
<template #2>
<CodeEditor
icon="streamline-emojis:hibiscus"
label="输出框"
v-model="output"
readonly
:font-size="size"
>
<template #actions>
<n-tag v-if="status === Status.Accepted" type="success">
运行成功
</n-tag>
<n-tag v-if="showAnalyse" type="warning">运行失败</n-tag>
<n-popover
v-if="showAnalyse && code.language === 'python'"
trigger="click"
>
<template #trigger>
<n-button quaternary type="error" @click="analyzeError">
推测原因
</n-button>
</template>
<template #header v-if="analyse.line > 0">
错误在第
<n-tag type="error">
<b>{{ analyse.line }}</b>
</n-tag>
</template>
<span v-if="analyse.message">
{{ analyse.message }}
</span>
</n-popover>
</template>
</CodeEditor>
<OutputSection />
</template>
</n-split>
<TurtleSection v-else />
</template>
</n-split>
</n-layout-content>

View File

@@ -1,65 +0,0 @@
<template>
<div class="loading" v-if="loading">正在加载中...第一次打开会有点慢</div>
<div v-if="!loading">
<p class="tip">提醒</p>
<p>1. 点击下一步开始调试也可以拖动进度条</p>
<p>
2. 点击
<n-button text type="primary" @click="close">修改代码</n-button>
完成修改后可再次调试
</p>
</div>
<iframe
width="100%"
height="360"
frameborder="0"
:src="src"
ref="main"
></iframe>
</template>
<script lang="ts" setup>
import qs from "query-string"
import { onMounted, ref, useTemplateRef } from "vue"
import { code, debug } from "../composables/code"
import { useDark } from "@vueuse/core";
const src = ref("")
const loading = ref(true)
const main = useTemplateRef("main")
const isDark = useDark()
onMounted(() => {
// const url = "http://localhost:8000"
const url = import.meta.env.PUBLIC_PYVIZ_URL
const base = url + "/iframe-embed.html"
const part1 = qs.stringify({
code: code.value,
codeDivWidth: 300,
})
const part2 =
"&cumulative=false&curInstr=0&heapPrimitives=nevernest&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=true&"
const part3 = qs.stringify({
dark: isDark.value,
})
const query = part1 + part2 + part3
src.value = base + "#" + query
main.value!.addEventListener("load", () => {
loading.value = false
})
})
function close() {
debug.value = false
}
</script>
<style scoped>
.loading {
font-size: 16px;
}
.tip {
margin-top: 0;
}
</style>

View File

@@ -0,0 +1,536 @@
<script lang="ts" setup>
// Vue 核心
import { ref, computed, watch } from "vue"
// 第三方库
import copyTextToClipboard from "copy-text-to-clipboard"
import { useMessage } from "naive-ui"
import { Icon } from "@iconify/vue"
import { useIntervalFn } from "@vueuse/core"
// 组件
import DebugEditor from "../components/DebugEditor.vue"
import DebugPanel from "../components/DebugPanel.vue"
// 组合式函数和类型
import { code, input, reset, size, output, status } from "../composables/code"
import { Status } from "../types"
import { debug } from "../api"
// ==================== 响应式状态 ====================
const message = useMessage()
// 调试状态
const showDebug = ref(false)
const debugData = ref<any>(null)
const currentStep = ref(0)
// UI 状态
const showFloatingPanel = ref(false)
// 自动运行状态
const isAutoRunning = ref(false)
const {
pause: pauseAutoRun,
resume: resumeAutoRun,
isActive: isAutoRunActive,
} = useIntervalFn(
() => {
if (currentStep.value < debugData.value.trace.length - 1) {
currentStep.value++
// 如果遇到输入步骤,暂停自动运行
if (debugData.value.trace[currentStep.value]?.event === "raw_input") {
pauseAutoRun()
isAutoRunning.value = false
message.info("程序正在等待输入,自动运行已暂停")
}
} else {
// 到达最后一步,停止自动运行
pauseAutoRun()
isAutoRunning.value = false
}
},
500,
{ immediate: false },
)
// 调试状态快照(用于检测代码/输入变化)
let debugStartCode = ""
let debugStartInput = ""
// ==================== 监听器 ====================
// 监听代码变化
watch(
() => code.value,
(newCode) => {
if (showDebug.value && newCode !== debugStartCode) {
message.warning("代码已修改,请重新点击调试按钮")
}
},
)
// 监听输入变化
watch(
() => input.value,
(newInput) => {
if (showDebug.value && newInput !== debugStartInput) {
message.warning("输入已修改,请重新点击调试按钮")
}
},
)
// ==================== 计算属性 ====================
// 调试行号相关
const currentLine = computed(() => {
if (
debugData.value &&
debugData.value.trace &&
debugData.value.trace[currentStep.value]
) {
const line = debugData.value.trace[currentStep.value].line
console.log(`Step ${currentStep.value}: currentLine = ${line}`)
return line && line > 0 ? line : undefined
}
return undefined
})
const nextLine = computed(() => {
if (
debugData.value &&
debugData.value.trace &&
debugData.value.trace[currentStep.value + 1]
) {
const line = debugData.value.trace[currentStep.value + 1].line
console.log(`Step ${currentStep.value}: nextLine = ${line}`)
return line && line > 0 && line !== currentLine.value ? line : undefined
}
console.log(`Step ${currentStep.value}: nextLine = undefined (no next step)`)
return undefined
})
// 调试信息相关
const currentVariables = computed(() => {
if (
debugData.value &&
debugData.value.trace &&
debugData.value.trace[currentStep.value]
) {
return debugData.value.trace[currentStep.value].globals || {}
}
return {}
})
const currentLineText = computed(() => {
if (
debugData.value &&
debugData.value.trace &&
debugData.value.trace[currentStep.value]
) {
const step = debugData.value.trace[currentStep.value]
const isLastStep = currentStep.value === debugData.value.trace.length - 1
const eventText = isLastStep ? "" : getEventText(step.event)
const stepText = isLastStep
? "最后一步"
: `当前第${currentStep.value + 1}`
return `${stepText}${eventText}`
}
return undefined
})
const nextLineText = computed(() => {
if (
debugData.value &&
debugData.value.trace &&
debugData.value.trace[currentStep.value + 1]
) {
const step = debugData.value.trace[currentStep.value + 1]
const isNextLastStep =
currentStep.value + 1 === debugData.value.trace.length - 1
const eventText = isNextLastStep ? "" : getEventText(step.event)
const stepText = isNextLastStep ? "最后一步" : `下一步`
return `${stepText}${eventText}`
}
return undefined
})
// ==================== 工具函数 ====================
/**
* 获取事件类型的中文描述
*/
function getEventText(event: string): string {
switch (event) {
case "step_line":
return "" // 普通执行不显示额外文字
case "call":
return "(调用函数)"
case "return":
return "(函数返回)"
case "exception":
return "(异常)"
case "uncaught_exception":
return "(异常)"
case "raw_input":
return "(等待输入)"
default:
return event || ""
}
}
// 输出相关
const currentOutput = computed(() => {
if (
debugData.value &&
debugData.value.trace &&
debugData.value.trace.length > 0
) {
let outputText = ""
for (let i = 0; i <= currentStep.value; i++) {
const step = debugData.value.trace[i]
if (step) {
if (step.event === "exception" || step.event === "uncaught_exception") {
if (step.exception_msg) {
outputText = step.exception_msg
}
} else if (step.stdout) {
outputText = step.stdout
}
}
}
outputText = outputText.trimEnd()
const hasException = debugData.value.trace.some(
(step: any) =>
step.event === "exception" || step.event === "uncaught_exception",
)
status.value = hasException ? Status.RuntimeError : Status.Accepted
output.value = outputText
return outputText
}
return output.value || ""
})
// ==================== 主要功能函数 ====================
/**
* 复制代码到剪贴板
*/
function copy() {
copyTextToClipboard(code.value)
message.success("已经复制好了")
}
/**
* 开始调试
*/
async function handleDebug() {
showDebug.value = true
showFloatingPanel.value = true
// 保存调试开始时的代码和输入状态
debugStartCode = code.value
debugStartInput = input.value
const inputs = input.value ? input.value.split("\n") : []
const res = await debug(code.value, inputs)
debugData.value = res.data
currentStep.value = 0
// 检查步骤数量并显示提醒
if (res.data.trace && res.data.trace.length > 5000) {
message.warning(`超过 5000 步,请优化代码或减少循环次数`)
}
// 检查最后一步是否为 raw_input 事件
if (res.data.trace && res.data.trace.length > 0) {
const lastStep = res.data.trace[res.data.trace.length - 1]
if (lastStep.event === "raw_input") {
message.info("程序正在等待输入,请在输入框输入内容后重新点击调试按钮")
}
}
// 显示前几个 trace 条目的行号
if (res.data.trace) {
console.log("First few trace entries:")
res.data.trace.slice(0, 5).forEach((entry: any, index: number) => {
console.log(` Step ${index}: line ${entry.line}, event: ${entry.event}`)
})
}
}
// ==================== 调试控制函数 ====================
/**
* 跳转到第一步
*/
function firstStep() {
if (debugData.value && debugData.value.trace) {
currentStep.value = 0
}
}
/**
* 上一步
*/
function prevStep() {
if (debugData.value && debugData.value.trace && currentStep.value > 0) {
currentStep.value--
}
}
/**
* 下一步
*/
function nextStep() {
if (
debugData.value &&
debugData.value.trace &&
currentStep.value < debugData.value.trace.length - 1
) {
currentStep.value++
}
}
/**
* 跳转到最后一步
*/
function lastStep() {
if (debugData.value && debugData.value.trace) {
currentStep.value = debugData.value.trace.length - 1
// 如果最后一步是 raw_input显示提醒
const lastStep = debugData.value.trace[debugData.value.trace.length - 1]
if (lastStep.event === "raw_input") {
message.info("程序正在等待输入,请在输入区域输入内容后重新调试")
}
}
}
// ==================== UI控制函数 ====================
/**
* 关闭浮动面板
*/
function closeFloatingPanel() {
showFloatingPanel.value = false
}
/**
* 关闭调试模式
*/
function closeDebug() {
showDebug.value = false
showFloatingPanel.value = false
debugData.value = null
currentStep.value = 0
// 清除保存的调试状态
debugStartCode = ""
debugStartInput = ""
// 停止自动运行
pauseAutoRun()
isAutoRunning.value = false
}
/**
* 自动运行/暂停
*/
function autoRun() {
if (!debugData.value || !debugData.value.trace) return
if (isAutoRunActive.value) {
// 停止自动运行
pauseAutoRun()
isAutoRunning.value = false
} else {
// 开始自动运行
isAutoRunning.value = true
resumeAutoRun()
}
}
</script>
<template>
<!-- 头部工具栏 -->
<n-flex align="center" class="header">
<!-- 标题和基础操作 -->
<Icon icon="streamline-emojis:lemon" :width="24" :height="24" />
<span class="title">代码区</span>
<n-button quaternary type="primary" @click="copy">复制</n-button>
<n-button quaternary @click="reset">清空</n-button>
<n-button
quaternary
type="error"
:disabled="!code.value"
@click="handleDebug"
>
调试
</n-button>
<!-- 调试控制按钮 -->
<template v-if="showDebug">
<!-- 步骤控制 -->
<n-tooltip>
<template #trigger>
<n-button text @click="firstStep">
<template #icon>
<Icon icon="material-symbols:skip-previous" />
</template>
</n-button>
</template>
第一步
</n-tooltip>
<n-tooltip>
<template #trigger>
<n-button text type="primary" @click="prevStep">
<template #icon>
<Icon icon="tabler:chevron-left" />
</template>
</n-button>
</template>
上一步
</n-tooltip>
<n-tooltip>
<template #trigger>
<n-button
:type="isAutoRunning ? 'warning' : 'info'"
text
@click="autoRun"
>
<template #icon>
<Icon
:icon="
isAutoRunning
? 'material-symbols:pause'
: 'material-symbols:play-arrow'
"
/>
</template>
</n-button>
</template>
{{ isAutoRunning ? "暂停自动运行" : "开始自动运行" }}
</n-tooltip>
<n-tooltip>
<template #trigger>
<n-button type="error" text @click="nextStep">
<template #icon>
<Icon icon="tabler:chevron-right" />
</template>
</n-button>
</template>
下一步
</n-tooltip>
<n-tooltip>
<template #trigger>
<n-button text @click="lastStep">
<template #icon>
<Icon icon="material-symbols:skip-next" />
</template>
</n-button>
</template>
最后一步
</n-tooltip>
<!-- 面板控制 -->
<n-button
quaternary
type="info"
@click="showFloatingPanel = !showFloatingPanel"
>
{{ showFloatingPanel ? "隐藏面板" : "显示面板" }}
</n-button>
</template>
<!-- 调试进度条 -->
<n-flex
v-if="showDebug && debugData && debugData.trace"
align="center"
class="progress-section"
>
<n-slider
v-model:value="currentStep"
:min="0"
:max="debugData.trace.length - 1"
:step="1"
:tooltip="false"
class="debug-progress"
/>
<span class="progress-text">
步骤: {{ currentStep + 1 }} / {{ debugData.trace.length }}
</span>
</n-flex>
<!-- 关闭调试 -->
<n-tooltip v-if="showDebug">
<template #trigger>
<n-button text type="error" @click="closeDebug">
<template #icon>
<Icon icon="tabler:x" />
</template>
</n-button>
</template>
关闭调试
</n-tooltip>
</n-flex>
<!-- 主要内容区域 -->
<div class="debug-container">
<!-- 代码编辑器 -->
<DebugEditor
v-model="code.value"
:font-size="size"
:language="code.language"
:current-line="currentLine"
:next-line="nextLine"
:current-line-text="currentLineText"
:next-line-text="nextLineText"
/>
<!-- 调试面板 -->
<DebugPanel
:visible="showFloatingPanel"
:variables="currentVariables"
:output="currentOutput"
@close="closeFloatingPanel"
/>
</div>
</template>
<style scoped>
/* ==================== 头部样式 ==================== */
.header {
padding: 12px 20px;
height: 60px;
box-sizing: border-box;
}
.title {
font-size: 16px;
}
/* ==================== 主容器样式 ==================== */
.debug-container {
position: relative;
}
/* ==================== 进度条样式 ==================== */
.progress-section {
flex: 1;
}
.debug-progress {
flex: 1;
max-width: 200px;
}
.progress-text {
font-size: 12px;
color: var(--n-text-color-disabled);
white-space: nowrap;
}
</style>

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import {
addFive,
add,
download,
files,
onChange,
@@ -13,7 +13,8 @@ import {
<n-flex vertical>
<n-flex>
<n-button @click="reset">清空</n-button>
<n-button @click="addFive">增加5</n-button>
<n-button @click="add(1)">增加1</n-button>
<n-button @click="add(5)">增加5个</n-button>
<n-button @click="run">先运行</n-button>
<n-button @click="download">再下载</n-button>
</n-flex>

View File

@@ -0,0 +1,27 @@
<script lang="ts" setup>
import { computed } from "vue"
import CodeEditor from "../components/CodeEditor.vue"
import { clearInput, input, size } from "../composables/code"
const showInputClearBtn = computed(() => !!input.value)
</script>
<template>
<CodeEditor
icon="streamline-emojis:four-leaf-clover"
label="输入框"
:font-size="size"
v-model="input"
>
<template #actions>
<n-button
quaternary
type="primary"
@click="clearInput"
v-if="showInputClearBtn"
>
清空
</n-button>
</template>
</CodeEditor>
</template>

View File

@@ -0,0 +1,35 @@
<script lang="ts" setup>
import CodeEditor from "../components/CodeEditor.vue"
import { output, size, status } from "../composables/code"
import { Status } from "../types"
import {
analysis,
loading,
getAIAnalysis,
showAnalysis,
} from "../composables/analysis"
import AnalysisPanel from "./AnalysisPanel.vue"
</script>
<template>
<CodeEditor
icon="streamline-emojis:hibiscus"
label="输出框"
v-model="output"
readonly
:font-size="size"
>
<template #actions>
<n-tag v-if="status === Status.Accepted" type="success"> 运行成功 </n-tag>
<n-tag v-if="showAnalysis" type="warning">运行失败</n-tag>
<n-popover v-if="showAnalysis" trigger="click" placement="left">
<template #trigger>
<n-button quaternary type="error" @click="getAIAnalysis">
推测原因
</n-button>
</template>
<AnalysisPanel :analysis="analysis" :loading="loading" />
</n-popover>
</template>
</CodeEditor>
</template>

View File

@@ -0,0 +1,56 @@
<script lang="ts" setup>
import { useTemplateRef, watch } from "vue"
// @ts-ignore
import * as Sk from "skulpt"
import { code, input, output, turtleRunId } from "../composables/code"
const turtleCanvas = useTemplateRef("turtle")
function builtinRead(x: any) {
if (
Sk.builtinFiles === undefined ||
Sk.builtinFiles["files"][x] === undefined
)
throw "文件没有找到:'" + x + "'"
return Sk.builtinFiles["files"][x]
}
function runSkulptTurtle() {
const canvas = turtleCanvas.value
if (!canvas) return
canvas.innerHTML = ""
Sk.configure({
output: console.log,
read: builtinRead,
inputfun: function () {
return input.value
},
__future__: Sk.python3,
})
Sk.TurtleGraphics = {
target: canvas,
width: canvas.clientWidth,
height: canvas.clientHeight,
}
Sk.misceval
.asyncToPromise(function () {
return Sk.importMainWithBody("<stdin>", false, code.value, true)
})
.catch((err: any) => {
output.value += String(err)
})
}
watch(turtleRunId, () => runSkulptTurtle())
</script>
<template>
<div ref="turtle" class="canvas"></div>
</template>
<style scoped>
.canvas {
width: 100%;
height: 100%;
}
</style>

View File

@@ -19,22 +19,12 @@
>
<Query />
</n-modal>
<n-modal
v-model:show="debug"
preset="card"
style="width: 700px"
:mask-closable="false"
title="可视化调试(测试版)"
>
<Debug />
</n-modal>
</template>
<script lang="ts" setup>
import { useMagicKeys, whenever } from "@vueuse/core"
import { ref } from "vue"
import { debug, run } from "../composables/code"
import { run } from "../composables/code"
import Content from "./Content.vue"
import Debug from "./Debug.vue"
import File from "./File.vue"
import Header from "./Header.vue"
import Query from "./Query.vue"

View File

@@ -1,6 +1,9 @@
import { addAPIProvider } from "@iconify/vue"
import {
NButton,
NCard,
NCollapse,
NCollapseItem,
NConfigProvider,
NDropdown,
NFlex,
@@ -14,10 +17,16 @@ import {
NModal,
NPopover,
NSelect,
NSpace,
NSplit,
NTabPane,
NTabs,
NTag,
NSpin,
NText,
NTooltip,
NSlider,
NScrollbar,
create,
} from "naive-ui"
import "normalize.css"
@@ -27,6 +36,9 @@ import App from "./App.vue"
const naive = create({
components: [
NButton,
NCard,
NCollapse,
NCollapseItem,
NConfigProvider,
NMessageProvider,
NLayout,
@@ -45,6 +57,12 @@ const naive = create({
NTabs,
NTabPane,
NDropdown,
NSpin,
NSpace,
NText,
NTooltip,
NSlider,
NScrollbar,
],
})
@@ -52,8 +70,8 @@ const app = createApp(App)
app.use(naive)
app.mount("#app")
if (!!import.meta.env.PUBLIC_ICONIFY) {
if (!!import.meta.env.PUBLIC_ICONIFY_URL) {
addAPIProvider("", {
resources: [import.meta.env.PUBLIC_ICONIFY],
resources: [import.meta.env.PUBLIC_ICONIFY_URL],
})
}

10
src/rsbuild-env.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
interface ImportMetaEnv {
readonly PUBLIC_JUDGE0API_URL: string
readonly PUBLIC_MAXKB_URL: string
readonly PUBLIC_CODEAPI_URL: string
readonly PUBLIC_ICONIFY_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View File

@@ -6,7 +6,18 @@ const pythonSource = ""
const javaSource =
"public class Main {\r\n public static void main(String[] args) {\r\n \r\n }\r\n}"
export const languageToId = {
const turtleSource = `import turtle
t = turtle.Turtle()
t.speed(1)
for i in range(4):
t.forward(100)
t.left(90)
turtle.done()`
export const languageToId: { [key in string]: number } = {
c: 50,
cpp: 54,
java: 62,
@@ -18,4 +29,5 @@ export const sources = {
cpp: cppSource,
java: javaSource,
python: pythonSource,
turtle: turtleSource,
}

View File

@@ -1,6 +1,6 @@
import { RemovableRef } from "@vueuse/core"
export type LANGUAGE = "c" | "python"
export type LANGUAGE = "c" | "python" | "cpp" | "turtle"
export interface Code {
value: string