Compare commits
34 Commits
aa6854b6ab
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a77902750 | |||
| ec46928689 | |||
| 727dc0a8e8 | |||
| 475a09298d | |||
| 9b1abd7a25 | |||
| 953ca3720f | |||
| 9470614588 | |||
| bc2db54575 | |||
| 4306e555bb | |||
| 0d600382d3 | |||
| 9164fff6c2 | |||
| 64eeffd041 | |||
| cfeac2cdaa | |||
| 53ae1a8ef8 | |||
| e96611c62b | |||
| 9758322f27 | |||
| c6d2e17476 | |||
| 0f0312529b | |||
| b33b0ee110 | |||
| dea523cb15 | |||
| 4b59d1cf17 | |||
| d9bcb81109 | |||
| 80d279365c | |||
| 044b33f39c | |||
| b763eb60cd | |||
| 07847d351d | |||
| e0944e50d1 | |||
| bb93d717b8 | |||
| e188cca3af | |||
| 0d824026a5 | |||
| 09328a3147 | |||
| 5240e029c5 | |||
| c09323cc8f | |||
| b91d7405fc |
5
.env
5
.env
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,2 +1 @@
|
||||
semi=false
|
||||
plugins=["prettier-plugin-organize-imports"]
|
||||
@@ -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>
|
||||
|
||||
3911
package-lock.json
generated
3911
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
@@ -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
1
public/cpp.svg
Normal 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
BIN
public/noto--cat-face.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 190 KiB |
1
public/turtle.svg
Normal file
1
public/turtle.svg
Normal 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 |
11
src/api.ts
11
src/api.ts
@@ -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
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ watch(
|
||||
)
|
||||
|
||||
const lang = computed(() => {
|
||||
if (props.language === "python") {
|
||||
if (props.language === "python" || props.language === "turtle") {
|
||||
return python()
|
||||
}
|
||||
return cpp()
|
||||
|
||||
265
src/components/DebugEditor.vue
Normal file
265
src/components/DebugEditor.vue
Normal 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>
|
||||
188
src/components/DebugPanel.vue
Normal file
188
src/components/DebugPanel.vue
Normal 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>
|
||||
@@ -1,28 +1,46 @@
|
||||
<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 = [
|
||||
["python", "Python"],
|
||||
["c", "C 语言"],
|
||||
]
|
||||
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) => ({
|
||||
value: it[0],
|
||||
label: () => [
|
||||
h("img", {
|
||||
src: `/${it[0]}.svg`,
|
||||
style: {
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
marginRight: "8px",
|
||||
transform: "translateY(3px)",
|
||||
},
|
||||
}),
|
||||
it[1],
|
||||
],
|
||||
}))
|
||||
// 如果当前在移动端且语言是海龟绘图,自动切换到 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", {
|
||||
src: `/${it[0]}.svg`,
|
||||
style: {
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
marginRight: "8px",
|
||||
transform: "translateY(3px)",
|
||||
},
|
||||
}),
|
||||
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>
|
||||
|
||||
@@ -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
153
src/composables/analysis.ts
Normal 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),
|
||||
)
|
||||
@@ -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,15 +106,22 @@ export function reset() {
|
||||
export async function run() {
|
||||
loading.value = true
|
||||
const cleanCode = code.value.trim()
|
||||
if (!cleanCode) return
|
||||
output.value = ""
|
||||
status.value = Status.NotStarted
|
||||
const result = await submit(
|
||||
{ value: cleanCode, language: code.language },
|
||||
input.value.trim(),
|
||||
)
|
||||
output.value = result.output || ""
|
||||
status.value = result.status
|
||||
if (!cleanCode) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
if (code.language === "turtle") {
|
||||
turtleRunId.value++
|
||||
} else {
|
||||
output.value = ""
|
||||
status.value = Status.NotStarted
|
||||
const result = await submit(
|
||||
{ value: cleanCode, language: code.language },
|
||||
input.value.trim(),
|
||||
)
|
||||
output.value = result.output || ""
|
||||
status.value = result.status
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
|
||||
@@ -21,14 +21,22 @@ export function reset() {
|
||||
}))
|
||||
}
|
||||
|
||||
export function addFive() {
|
||||
files.value.push(
|
||||
...Array.from({ length: 5 }).map(() => ({
|
||||
export function add(len = 1) {
|
||||
if (len == 1) {
|
||||
files.value.push({
|
||||
in: "",
|
||||
out: "",
|
||||
error: false,
|
||||
})),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
files.value.push(
|
||||
...Array.from({ length: len }).map(() => ({
|
||||
in: "",
|
||||
out: "",
|
||||
error: false,
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function remove(index: number) {
|
||||
|
||||
172
src/desktop/AnalysisPanel.vue
Normal file
172
src/desktop/AnalysisPanel.vue
Normal 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>
|
||||
35
src/desktop/CodeSection.vue
Normal file
35
src/desktop/CodeSection.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
536
src/desktop/DebugSection.vue
Normal file
536
src/desktop/DebugSection.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
27
src/desktop/InputSection.vue
Normal file
27
src/desktop/InputSection.vue
Normal 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>
|
||||
35
src/desktop/OutputSection.vue
Normal file
35
src/desktop/OutputSection.vue
Normal 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>
|
||||
56
src/desktop/TurtleSection.vue
Normal file
56
src/desktop/TurtleSection.vue
Normal 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>
|
||||
@@ -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"
|
||||
|
||||
22
src/main.ts
22
src/main.ts
@@ -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
10
src/rsbuild-env.d.ts
vendored
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user