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_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_MAXKB_URL=https://maxkb.xuyue.cc/chat/api/embed?protocol=https&host=maxkb.xuyue.cc&token=2e801f7d6efdcc99
|
||||||
PUBLIC_CODEAPI_URL=https://codeapi.xuyue.cc
|
PUBLIC_CODEAPI_URL=http://localhost:8080
|
||||||
PUBLIC_PYVIZ_URL=https://pyviz.xuyue.cc
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
PUBLIC_JUDGE0API_URL=https://judge0api.xuyue.cc
|
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_MAXKB_URL=https://maxkb.xuyue.cc/chat/api/embed?protocol=https&host=maxkb.xuyue.cc&token=2e801f7d6efdcc99
|
||||||
PUBLIC_CODEAPI_URL=https://codeapi.xuyue.cc
|
PUBLIC_CODEAPI_URL=https://code.xuyue.cc/api
|
||||||
PUBLIC_PYVIZ_URL=https://pyviz.xuyue.cc
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
PUBLIC_JUDGE0API_URL=http://10.13.114.214:8082
|
PUBLIC_JUDGE0API_URL=http://10.13.114.114:8082
|
||||||
PUBLIC_MAXKB_URL=
|
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.214:8092
|
PUBLIC_CODEAPI_URL=http://10.13.114.114:82/api
|
||||||
PUBLIC_PYVIZ_URL=http://10.13.114.214:9000
|
PUBLIC_ICONIFY_URL=http://10.13.114.114:8098
|
||||||
PUBLIC_ICONIFY=http://10.13.114.214:8098
|
|
||||||
@@ -1,2 +1 @@
|
|||||||
semi=false
|
semi=false
|
||||||
plugins=["prettier-plugin-organize-imports"]
|
|
||||||
@@ -2,18 +2,18 @@
|
|||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>自测猫</title>
|
<title>自测猫</title>
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css" />
|
||||||
<script>
|
<script>
|
||||||
window.localStorage.setItem("maxkbMaskTip", true)
|
window.localStorage.setItem("maxkbMaskTip", true)
|
||||||
</script>
|
</script>
|
||||||
<script
|
<!-- <script
|
||||||
async
|
async
|
||||||
defer
|
defer
|
||||||
src="<%= import.meta.env.PUBLIC_MAXKB_URL %>"
|
src="<%= import.meta.env.PUBLIC_MAXKB_URL %>"
|
||||||
></script>
|
></script> -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<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",
|
"name": "code-next",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.2.0",
|
"version": "1.3.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "rsbuild dev",
|
"start": "rsbuild dev",
|
||||||
@@ -10,30 +10,29 @@
|
|||||||
"fmt": "prettier --write src"
|
"fmt": "prettier --write src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-cpp": "^6.0.2",
|
"@codemirror/lang-cpp": "^6.0.3",
|
||||||
"@codemirror/lang-python": "^6.2.0",
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
"@vueuse/core": "^13.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.13.2",
|
||||||
"client-zip": "1.7.0",
|
"client-zip": "2.5.0",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.2",
|
||||||
"copy-text-to-clipboard": "^3.2.0",
|
"copy-text-to-clipboard": "^3.2.2",
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"naive-ui": "^2.41.0",
|
"marked": "^17.0.1",
|
||||||
|
"naive-ui": "^2.43.2",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"query-string": "^9.1.2",
|
"query-string": "^9.3.1",
|
||||||
"vue": "^3.5.13",
|
"skulpt": "^1.2.0",
|
||||||
|
"vue": "^3.5.26",
|
||||||
"vue-codemirror": "^6.1.1"
|
"vue-codemirror": "^6.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify/vue": "^5.0.0",
|
"@iconify/vue": "^5.0.0",
|
||||||
"@rsbuild/core": "^1.3.17",
|
"@rsbuild/core": "^1.6.15",
|
||||||
"@rsbuild/plugin-vue": "^1.0.7",
|
"@rsbuild/plugin-vue": "^1.2.2",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@vitejs/plugin-vue": "^5.2.3",
|
"prettier": "^3.7.4",
|
||||||
"prettier": "^3.5.3",
|
"typescript": "^5.9.3"
|
||||||
"prettier-plugin-organize-imports": "^4.1.0",
|
|
||||||
"typescript": "^5.8.3",
|
|
||||||
"vite": "^6.2.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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) {
|
export async function removeCode(id: number) {
|
||||||
const res = await api.delete(`/${id}`)
|
await api.delete(`/${id}`)
|
||||||
console.log(res.data)
|
}
|
||||||
|
|
||||||
|
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(() => {
|
const lang = computed(() => {
|
||||||
if (props.language === "python") {
|
if (props.language === "python" || props.language === "turtle") {
|
||||||
return python()
|
return python()
|
||||||
}
|
}
|
||||||
return cpp()
|
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>
|
<script lang="ts" setup>
|
||||||
import type { SelectOption } from "naive-ui"
|
import type { SelectOption } from "naive-ui"
|
||||||
import { h } from "vue"
|
import { h, computed, watch } from "vue"
|
||||||
import { code } from "../composables/code"
|
import { code } from "../composables/code"
|
||||||
|
import { isMobile } from "../composables/breakpoints"
|
||||||
|
|
||||||
const LANGS = [
|
const LANGS = computed(() => {
|
||||||
["python", "Python"],
|
const allLangs = [
|
||||||
["c", "C 语言"],
|
["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
|
||||||
value: it[0],
|
watch(isMobile, (mobile) => {
|
||||||
label: () => [
|
if (mobile && code.language === "turtle") {
|
||||||
h("img", {
|
code.language = "python"
|
||||||
src: `/${it[0]}.svg`,
|
}
|
||||||
style: {
|
})
|
||||||
width: "16px",
|
|
||||||
height: "16px",
|
const languages = computed<SelectOption[]>(() =>
|
||||||
marginRight: "8px",
|
LANGS.value.map((it) => ({
|
||||||
transform: "translateY(3px)",
|
value: it[0],
|
||||||
},
|
label: () => [
|
||||||
}),
|
h("img", {
|
||||||
it[1],
|
src: `/${it[0]}.svg`,
|
||||||
],
|
style: {
|
||||||
}))
|
width: "16px",
|
||||||
|
height: "16px",
|
||||||
|
marginRight: "8px",
|
||||||
|
transform: "translateY(3px)",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
it[1],
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<n-select
|
<n-select
|
||||||
@@ -34,6 +52,6 @@ const languages: SelectOption[] = LANGS.map((it) => ({
|
|||||||
</template>
|
</template>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.select {
|
.select {
|
||||||
width: 120px;
|
width: 125px;
|
||||||
}
|
}
|
||||||
</style>
|
</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: {
|
code: {
|
||||||
python: useStorage("code_python", sources["python"]),
|
python: useStorage("code_python", sources["python"]),
|
||||||
c: useStorage("code_c", sources["c"]),
|
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 output = ref("")
|
||||||
export const status = ref(Status.NotStarted)
|
export const status = ref(Status.NotStarted)
|
||||||
export const loading = ref(false)
|
export const loading = ref(false)
|
||||||
|
export const turtleRunId = ref(0)
|
||||||
export const size = ref(0)
|
export const size = ref(0)
|
||||||
export const debug = ref(false)
|
|
||||||
|
|
||||||
watch(size, (value: number) => {
|
watch(size, (value: number) => {
|
||||||
cache.fontsize.value = value
|
cache.fontsize.value = value
|
||||||
@@ -69,9 +71,14 @@ export async function init() {
|
|||||||
if (base64) {
|
if (base64) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(atou(base64))
|
const data = JSON.parse(atou(base64))
|
||||||
code.language = data.lang
|
const lang = ["python", "c", "cpp", "turtle"].includes(data.lang)
|
||||||
code.value = data.code
|
? (data.lang as LANGUAGE)
|
||||||
input.value = data.input
|
: 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) {}
|
} catch (err) {}
|
||||||
}
|
}
|
||||||
const preset = parsed.query as string
|
const preset = parsed.query as string
|
||||||
@@ -99,15 +106,22 @@ export function reset() {
|
|||||||
export async function run() {
|
export async function run() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const cleanCode = code.value.trim()
|
const cleanCode = code.value.trim()
|
||||||
if (!cleanCode) return
|
if (!cleanCode) {
|
||||||
output.value = ""
|
loading.value = false
|
||||||
status.value = Status.NotStarted
|
return
|
||||||
const result = await submit(
|
}
|
||||||
{ value: cleanCode, language: code.language },
|
if (code.language === "turtle") {
|
||||||
input.value.trim(),
|
turtleRunId.value++
|
||||||
)
|
} else {
|
||||||
output.value = result.output || ""
|
output.value = ""
|
||||||
status.value = result.status
|
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
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,14 +21,22 @@ export function reset() {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addFive() {
|
export function add(len = 1) {
|
||||||
files.value.push(
|
if (len == 1) {
|
||||||
...Array.from({ length: 5 }).map(() => ({
|
files.value.push({
|
||||||
in: "",
|
in: "",
|
||||||
out: "",
|
out: "",
|
||||||
error: false,
|
error: false,
|
||||||
})),
|
})
|
||||||
)
|
} else {
|
||||||
|
files.value.push(
|
||||||
|
...Array.from({ length: len }).map(() => ({
|
||||||
|
in: "",
|
||||||
|
out: "",
|
||||||
|
error: false,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function remove(index: number) {
|
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>
|
<script lang="ts" setup>
|
||||||
import copyTextToClipboard from "copy-text-to-clipboard"
|
import { code } from "../composables/code"
|
||||||
import { useMessage } from "naive-ui"
|
import CodeSection from "./CodeSection.vue"
|
||||||
import { computed } from "vue"
|
import DebugSection from "./DebugSection.vue"
|
||||||
import CodeEditor from "../components/CodeEditor.vue"
|
import InputSection from "./InputSection.vue"
|
||||||
import { analyse, analyzeError, showAnalyse } from "../composables/analyse"
|
import OutputSection from "./OutputSection.vue"
|
||||||
import {
|
import TurtleSection from "./TurtleSection.vue"
|
||||||
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
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-layout-content class="container">
|
<n-layout-content class="container">
|
||||||
<n-split direction="horizontal" :min="1 / 3" :max="4 / 5">
|
<n-split direction="horizontal" :min="1 / 3" :max="4 / 5">
|
||||||
<template #1>
|
<template #1>
|
||||||
<CodeEditor
|
<component
|
||||||
label="代码区"
|
:is="code.language === 'python' ? DebugSection : CodeSection"
|
||||||
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>
|
|
||||||
</template>
|
</template>
|
||||||
<template #2>
|
<template #2>
|
||||||
<n-split
|
<n-split
|
||||||
|
v-if="code.language !== 'turtle'"
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
:default-size="1 / 3"
|
:default-size="1 / 3"
|
||||||
:min="1 / 5"
|
:min="1 / 5"
|
||||||
:max="3 / 5"
|
:max="3 / 5"
|
||||||
>
|
>
|
||||||
<template #1>
|
<template #1>
|
||||||
<CodeEditor
|
<InputSection />
|
||||||
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>
|
</template>
|
||||||
<template #2>
|
<template #2>
|
||||||
<CodeEditor
|
<OutputSection />
|
||||||
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>
|
|
||||||
</template>
|
</template>
|
||||||
</n-split>
|
</n-split>
|
||||||
|
<TurtleSection v-else />
|
||||||
</template>
|
</template>
|
||||||
</n-split>
|
</n-split>
|
||||||
</n-layout-content>
|
</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>
|
<script lang="ts" setup>
|
||||||
import {
|
import {
|
||||||
addFive,
|
add,
|
||||||
download,
|
download,
|
||||||
files,
|
files,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -13,7 +13,8 @@ import {
|
|||||||
<n-flex vertical>
|
<n-flex vertical>
|
||||||
<n-flex>
|
<n-flex>
|
||||||
<n-button @click="reset">清空</n-button>
|
<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="run">先运行</n-button>
|
||||||
<n-button @click="download">再下载</n-button>
|
<n-button @click="download">再下载</n-button>
|
||||||
</n-flex>
|
</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 />
|
<Query />
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-modal
|
|
||||||
v-model:show="debug"
|
|
||||||
preset="card"
|
|
||||||
style="width: 700px"
|
|
||||||
:mask-closable="false"
|
|
||||||
title="可视化调试(测试版)"
|
|
||||||
>
|
|
||||||
<Debug />
|
|
||||||
</n-modal>
|
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useMagicKeys, whenever } from "@vueuse/core"
|
import { useMagicKeys, whenever } from "@vueuse/core"
|
||||||
import { ref } from "vue"
|
import { ref } from "vue"
|
||||||
import { debug, run } from "../composables/code"
|
import { run } from "../composables/code"
|
||||||
import Content from "./Content.vue"
|
import Content from "./Content.vue"
|
||||||
import Debug from "./Debug.vue"
|
|
||||||
import File from "./File.vue"
|
import File from "./File.vue"
|
||||||
import Header from "./Header.vue"
|
import Header from "./Header.vue"
|
||||||
import Query from "./Query.vue"
|
import Query from "./Query.vue"
|
||||||
|
|||||||
22
src/main.ts
22
src/main.ts
@@ -1,6 +1,9 @@
|
|||||||
import { addAPIProvider } from "@iconify/vue"
|
import { addAPIProvider } from "@iconify/vue"
|
||||||
import {
|
import {
|
||||||
NButton,
|
NButton,
|
||||||
|
NCard,
|
||||||
|
NCollapse,
|
||||||
|
NCollapseItem,
|
||||||
NConfigProvider,
|
NConfigProvider,
|
||||||
NDropdown,
|
NDropdown,
|
||||||
NFlex,
|
NFlex,
|
||||||
@@ -14,10 +17,16 @@ import {
|
|||||||
NModal,
|
NModal,
|
||||||
NPopover,
|
NPopover,
|
||||||
NSelect,
|
NSelect,
|
||||||
|
NSpace,
|
||||||
NSplit,
|
NSplit,
|
||||||
NTabPane,
|
NTabPane,
|
||||||
NTabs,
|
NTabs,
|
||||||
NTag,
|
NTag,
|
||||||
|
NSpin,
|
||||||
|
NText,
|
||||||
|
NTooltip,
|
||||||
|
NSlider,
|
||||||
|
NScrollbar,
|
||||||
create,
|
create,
|
||||||
} from "naive-ui"
|
} from "naive-ui"
|
||||||
import "normalize.css"
|
import "normalize.css"
|
||||||
@@ -27,6 +36,9 @@ import App from "./App.vue"
|
|||||||
const naive = create({
|
const naive = create({
|
||||||
components: [
|
components: [
|
||||||
NButton,
|
NButton,
|
||||||
|
NCard,
|
||||||
|
NCollapse,
|
||||||
|
NCollapseItem,
|
||||||
NConfigProvider,
|
NConfigProvider,
|
||||||
NMessageProvider,
|
NMessageProvider,
|
||||||
NLayout,
|
NLayout,
|
||||||
@@ -45,6 +57,12 @@ const naive = create({
|
|||||||
NTabs,
|
NTabs,
|
||||||
NTabPane,
|
NTabPane,
|
||||||
NDropdown,
|
NDropdown,
|
||||||
|
NSpin,
|
||||||
|
NSpace,
|
||||||
|
NText,
|
||||||
|
NTooltip,
|
||||||
|
NSlider,
|
||||||
|
NScrollbar,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -52,8 +70,8 @@ const app = createApp(App)
|
|||||||
app.use(naive)
|
app.use(naive)
|
||||||
app.mount("#app")
|
app.mount("#app")
|
||||||
|
|
||||||
if (!!import.meta.env.PUBLIC_ICONIFY) {
|
if (!!import.meta.env.PUBLIC_ICONIFY_URL) {
|
||||||
addAPIProvider("", {
|
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 =
|
const javaSource =
|
||||||
"public class Main {\r\n public static void main(String[] args) {\r\n \r\n }\r\n}"
|
"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,
|
c: 50,
|
||||||
cpp: 54,
|
cpp: 54,
|
||||||
java: 62,
|
java: 62,
|
||||||
@@ -18,4 +29,5 @@ export const sources = {
|
|||||||
cpp: cppSource,
|
cpp: cppSource,
|
||||||
java: javaSource,
|
java: javaSource,
|
||||||
python: pythonSource,
|
python: pythonSource,
|
||||||
|
turtle: turtleSource,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { RemovableRef } from "@vueuse/core"
|
import { RemovableRef } from "@vueuse/core"
|
||||||
|
|
||||||
export type LANGUAGE = "c" | "python"
|
export type LANGUAGE = "c" | "python" | "cpp" | "turtle"
|
||||||
|
|
||||||
export interface Code {
|
export interface Code {
|
||||||
value: string
|
value: string
|
||||||
|
|||||||
Reference in New Issue
Block a user