评阅页直接运行学生的程序;离开页面时结束正在运行的程序
- 运行区(窗体桌面、提示框、虚拟实训台、编译错误和控制台输出)抽成 RunArea 组件,实验页和评阅页共用 - 评阅页代码区加"运行"标签页:运行学生提交的代码,可以操作窗体和实训台;换学生时自动停止并清空; 点编译错误跳到对应代码;原来的按钮改为"在新标签页运行" - 运行会话加 close():离开实验页、评阅页时断开连接,服务器随即结束程序(之前要等 60 分钟超时) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
91237b85b8
commit
8a92354fe4
4 files changed
+213
-111
No files matched your search
@@ -1,14 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 评阅一个作业:左边是全班学生(提交状态、自动评分、老师评分),右边是选中学生的代码和界面截图、自动评分结果、
|
// 评阅一个作业:左边是全班学生(提交状态、自动评分、老师评分),右边是选中学生的界面截图、运行区、代码,
|
||||||
// 规范提示、AI 建议和老师评分。"运行他的程序"在新标签页的实验页里打开学生的代码。
|
// 以及自动评分结果、规范提示、AI 建议和老师评分。"运行"标签页里直接运行学生的程序(换学生时自动停止);
|
||||||
|
// "在新标签页运行"打开完整的实验页,地方更大。
|
||||||
// AI 建议只是参考:老师采纳(或修改后保存)才成为老师评分,学生才能看到。
|
// AI 建议只是参考:老师采纳(或修改后保存)才成为老师评分,学生才能看到。
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { NButton, NEmpty, NInput, NInputNumber, NSpin, NTab, NTabs, NTag, useDialog, useMessage } from 'naive-ui'
|
import { NButton, NEmpty, NInput, NInputNumber, NSpin, NTab, NTabs, NTag, useDialog, useMessage } from 'naive-ui'
|
||||||
import type { AiReview, Assignment, Review, Screenshot, SourceFile, Submission } from '@iot/shared'
|
import type { AiReview, Assignment, Review, Screenshot, SourceFile, Submission } from '@iot/shared'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
import { currentUser, logout } from '../auth'
|
import { currentUser, logout } from '../auth'
|
||||||
import CodeEditor from '../editor/CodeEditor.vue'
|
import CodeEditor from '../editor/CodeEditor.vue'
|
||||||
|
import RunArea from '../run/RunArea.vue'
|
||||||
|
import { createRunSession } from '../run/session'
|
||||||
import GradeResultView from '../grading/GradeResultView.vue'
|
import GradeResultView from '../grading/GradeResultView.vue'
|
||||||
import { conventionHints } from '@iot/shared'
|
import { conventionHints } from '@iot/shared'
|
||||||
import { downloadCsv } from '../teacher/csv'
|
import { downloadCsv } from '../teacher/csv'
|
||||||
@@ -39,8 +42,9 @@ interface Detail {
|
|||||||
submissionCount: number
|
submissionCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 代码区"界面截图"标签页的名字(不会和文件名冲突) */
|
/** 代码区"界面截图""运行"标签页的名字(不会和 .cs 文件名冲突) */
|
||||||
const SCREENS = '界面截图'
|
const SCREENS = '界面截图'
|
||||||
|
const RUN = '运行'
|
||||||
|
|
||||||
const props = defineProps<{ assignmentId: string }>()
|
const props = defineProps<{ assignmentId: string }>()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -48,6 +52,27 @@ const router = useRouter()
|
|||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
|
||||||
|
// ---------- 运行学生的程序 ----------
|
||||||
|
const session = createRunSession()
|
||||||
|
const editorRef = ref<InstanceType<typeof CodeEditor>>()
|
||||||
|
const runState = session.state
|
||||||
|
const running = computed(() => runState.status === 'compiling' || runState.status === 'running')
|
||||||
|
const STATUS_TEXT = { idle: '未运行', compiling: '编译中…', running: '运行中', stopped: '已停止' } as const
|
||||||
|
|
||||||
|
function runProgram() {
|
||||||
|
if (!detail.value?.files || running.value) return
|
||||||
|
void session.run(detail.value.files.map((f) => ({ ...f })))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 点编译错误:切到对应文件并跳到出错的行 */
|
||||||
|
async function showDiagnostic(file: string, line: number, column: number) {
|
||||||
|
activeFile.value = file
|
||||||
|
await nextTick()
|
||||||
|
editorRef.value?.reveal(file, line, column)
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => session.close())
|
||||||
|
|
||||||
const assignment = ref<Assignment | null>(null)
|
const assignment = ref<Assignment | null>(null)
|
||||||
const rows = ref<Row[]>([])
|
const rows = ref<Row[]>([])
|
||||||
const selectedId = ref<number>()
|
const selectedId = ref<number>()
|
||||||
@@ -88,6 +113,14 @@ onMounted(async () => {
|
|||||||
|
|
||||||
async function select(userId: number | undefined) {
|
async function select(userId: number | undefined) {
|
||||||
if (userId === undefined) return
|
if (userId === undefined) return
|
||||||
|
// 换学生:停掉上一个学生的程序,清空运行区
|
||||||
|
if (running.value) await session.stop()
|
||||||
|
runState.forms = []
|
||||||
|
runState.messageBoxes = []
|
||||||
|
runState.diagnostics = []
|
||||||
|
runState.errors = []
|
||||||
|
runState.console = ''
|
||||||
|
const wasRunTab = activeFile.value === RUN
|
||||||
selectedId.value = userId
|
selectedId.value = userId
|
||||||
void router.replace({ query: { student: userId } })
|
void router.replace({ query: { student: userId } })
|
||||||
detail.value = await api<Detail>('GET', `/api/teacher/assignments/${props.assignmentId}/students/${userId}`).catch((e: Error) => {
|
detail.value = await api<Detail>('GET', `/api/teacher/assignments/${props.assignmentId}/students/${userId}`).catch((e: Error) => {
|
||||||
@@ -97,8 +130,10 @@ async function select(userId: number | undefined) {
|
|||||||
const manual = assignment.value?.scoring.manual ?? []
|
const manual = assignment.value?.scoring.manual ?? []
|
||||||
scores.value = manual.map((_, i) => detail.value?.review?.scores[i] ?? null)
|
scores.value = manual.map((_, i) => detail.value?.review?.scores[i] ?? null)
|
||||||
comment.value = detail.value?.review?.comment ?? ''
|
comment.value = detail.value?.review?.comment ?? ''
|
||||||
// 默认先看界面截图,没有截图时看第一个窗体的代码
|
// 正在看"运行"时换学生,留在"运行";否则默认先看界面截图,没有截图时看第一个窗体的代码
|
||||||
activeFile.value = detail.value?.screenshots.length
|
activeFile.value = wasRunTab
|
||||||
|
? RUN
|
||||||
|
: detail.value?.screenshots.length
|
||||||
? SCREENS
|
? SCREENS
|
||||||
: (codeFiles.value.find((f) => !f.path.endsWith('.Designer.cs') && f.path !== 'Program.cs')?.path ?? codeFiles.value[0]?.path)
|
: (codeFiles.value.find((f) => !f.path.endsWith('.Designer.cs') && f.path !== 'Program.cs')?.path ?? codeFiles.value[0]?.path)
|
||||||
pollAi()
|
pollAi()
|
||||||
@@ -191,7 +226,7 @@ async function go(step: 1 | -1) {
|
|||||||
message.info(step > 0 ? '后面没有已提交的学生了' : '前面没有已提交的学生了')
|
message.info(step > 0 ? '后面没有已提交的学生了' : '前面没有已提交的学生了')
|
||||||
}
|
}
|
||||||
|
|
||||||
function runProgram() {
|
function openRunPage() {
|
||||||
if (detail.value?.submission) window.open(router.resolve(`/teacher/submissions/${detail.value.submission.id}/run`).href, '_blank')
|
if (detail.value?.submission) window.open(router.resolve(`/teacher/submissions/${detail.value.submission.id}/run`).href, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +315,7 @@ async function signOut() {
|
|||||||
<span class="spacer" />
|
<span class="spacer" />
|
||||||
<NButton size="small" @click="go(-1)">上一个</NButton>
|
<NButton size="small" @click="go(-1)">上一个</NButton>
|
||||||
<NButton size="small" @click="go(1)">下一个</NButton>
|
<NButton size="small" @click="go(1)">下一个</NButton>
|
||||||
<NButton size="small" type="primary" :disabled="!detail.submission" @click="runProgram">运行他的程序</NButton>
|
<NButton size="small" :disabled="!detail.submission" title="在新标签页里打开完整的实验页,地方更大" @click="openRunPage">在新标签页运行</NButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NEmpty v-if="!detail.submission" description="还没有提交" class="empty" />
|
<NEmpty v-if="!detail.submission" description="还没有提交" class="empty" />
|
||||||
@@ -288,6 +323,7 @@ async function signOut() {
|
|||||||
<div class="code">
|
<div class="code">
|
||||||
<NTabs :value="activeFile" type="card" size="small" @update:value="(v: string) => (activeFile = v)">
|
<NTabs :value="activeFile" type="card" size="small" @update:value="(v: string) => (activeFile = v)">
|
||||||
<NTab v-if="detail.screenshots.length" :name="SCREENS">{{ SCREENS }}</NTab>
|
<NTab v-if="detail.screenshots.length" :name="SCREENS">{{ SCREENS }}</NTab>
|
||||||
|
<NTab :name="RUN">▶ {{ RUN }}</NTab>
|
||||||
<NTab v-for="f in codeFiles" :key="f.path" :name="f.path">{{ f.path }}</NTab>
|
<NTab v-for="f in codeFiles" :key="f.path" :name="f.path">{{ f.path }}</NTab>
|
||||||
</NTabs>
|
</NTabs>
|
||||||
<div v-if="activeFile === SCREENS" class="screens">
|
<div v-if="activeFile === SCREENS" class="screens">
|
||||||
@@ -296,7 +332,26 @@ async function signOut() {
|
|||||||
<figcaption>{{ s.form }}</figcaption>
|
<figcaption>{{ s.form }}</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
<CodeEditor v-show="activeFile !== SCREENS" class="editor" :files="codeFiles" :active-file="activeFile" :diagnostics="[]" read-only />
|
<div v-show="activeFile === RUN" class="run-pane">
|
||||||
|
<div class="run-bar">
|
||||||
|
<NButton size="small" type="primary" :disabled="running" @click="runProgram">▶ 运行</NButton>
|
||||||
|
<NButton size="small" :disabled="!running" @click="session.stop">■ 停止</NButton>
|
||||||
|
<NTag size="small" :type="runState.status === 'running' ? 'success' : runState.status === 'compiling' ? 'warning' : 'default'">
|
||||||
|
{{ STATUS_TEXT[runState.status] }}
|
||||||
|
</NTag>
|
||||||
|
<span class="muted">运行的是学生提交的代码,可以操作窗体和实训台</span>
|
||||||
|
</div>
|
||||||
|
<RunArea class="run-area-host" :session="session" @diagnostic="showDiagnostic" />
|
||||||
|
</div>
|
||||||
|
<CodeEditor
|
||||||
|
v-show="activeFile !== SCREENS && activeFile !== RUN"
|
||||||
|
ref="editorRef"
|
||||||
|
class="editor"
|
||||||
|
:files="codeFiles"
|
||||||
|
:active-file="activeFile"
|
||||||
|
:diagnostics="runState.diagnostics"
|
||||||
|
read-only
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="side">
|
<div class="side">
|
||||||
@@ -463,6 +518,28 @@ async function signOut() {
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.run-pane {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-area-host {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.screens {
|
.screens {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -13,12 +13,10 @@ import CodeEditor from '../editor/CodeEditor.vue'
|
|||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
import { currentUser, logout } from '../auth'
|
import { currentUser, logout } from '../auth'
|
||||||
import { deleteWork, loadWork, sampleWorkKey, saveWork } from '../editor/works'
|
import { deleteWork, loadWork, sampleWorkKey, saveWork } from '../editor/works'
|
||||||
|
import RunArea from '../run/RunArea.vue'
|
||||||
import { createRunSession } from '../run/session'
|
import { createRunSession } from '../run/session'
|
||||||
import GradeResultView from '../grading/GradeResultView.vue'
|
import GradeResultView from '../grading/GradeResultView.vue'
|
||||||
import { captureForms } from '../grading/screenshots'
|
import { captureForms } from '../grading/screenshots'
|
||||||
import TrainingBoard from '../scene/TrainingBoard.vue'
|
|
||||||
import FormWindow from '../winforms/FormWindow.vue'
|
|
||||||
import MessageBoxDialog from '../winforms/MessageBoxDialog.vue'
|
|
||||||
|
|
||||||
const props = defineProps<{ assignmentId?: string; submissionId?: string }>()
|
const props = defineProps<{ assignmentId?: string; submissionId?: string }>()
|
||||||
|
|
||||||
@@ -91,7 +89,10 @@ function onKeyDown(event: KeyboardEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => window.addEventListener('keydown', onKeyDown, { capture: true }))
|
onMounted(() => window.addEventListener('keydown', onKeyDown, { capture: true }))
|
||||||
onBeforeUnmount(() => window.removeEventListener('keydown', onKeyDown, { capture: true }))
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('keydown', onKeyDown, { capture: true })
|
||||||
|
session.close()
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 一打开页面就连接服务器,实训台在运行程序之前就能显示和调节
|
// 一打开页面就连接服务器,实训台在运行程序之前就能显示和调节
|
||||||
@@ -473,40 +474,7 @@ function run() {
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="run-area">
|
<RunArea :session="session" @diagnostic="showDiagnostic" />
|
||||||
<div class="desktop">
|
|
||||||
<FormWindow
|
|
||||||
v-for="(form, index) in state.forms"
|
|
||||||
:key="form.root.id"
|
|
||||||
:form="form.root"
|
|
||||||
:modal="form.modal"
|
|
||||||
:offset="index"
|
|
||||||
:act="session.act"
|
|
||||||
/>
|
|
||||||
<MessageBoxDialog
|
|
||||||
v-for="box in state.messageBoxes"
|
|
||||||
:key="box.requestId"
|
|
||||||
:box="box"
|
|
||||||
@reply="session.replyMessageBox(box.requestId, $event)"
|
|
||||||
/>
|
|
||||||
<div v-if="state.forms.length === 0" class="desktop-hint">点击"运行"后,学生程序的窗体会显示在这里</div>
|
|
||||||
</div>
|
|
||||||
<TrainingBoard :board="state.board" @set="session.setBoard" @reset="session.resetBoard" />
|
|
||||||
<div class="output">
|
|
||||||
<div
|
|
||||||
v-for="(d, i) in state.diagnostics"
|
|
||||||
:key="`d${i}`"
|
|
||||||
:class="['diag', d.severity]"
|
|
||||||
title="点击跳到出错的代码"
|
|
||||||
@click="showDiagnostic(d.file, d.line, d.column)"
|
|
||||||
>
|
|
||||||
{{ d.file }}({{ d.line }},{{ d.column }}) {{ d.code }}:{{ d.message }}
|
|
||||||
<div v-if="d.hint" class="hint">提示:{{ d.hint }}</div>
|
|
||||||
</div>
|
|
||||||
<pre v-for="(e, i) in state.errors" :key="`e${i}`" class="error">{{ e }}</pre>
|
|
||||||
<pre v-if="state.console" class="console">{{ state.console }}</pre>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -623,67 +591,4 @@ function run() {
|
|||||||
.new-form-error {
|
.new-form-error {
|
||||||
color: #dc2626;
|
color: #dc2626;
|
||||||
}
|
}
|
||||||
|
|
||||||
.run-area {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desktop {
|
|
||||||
position: relative;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 320px;
|
|
||||||
background: #2d6aa3;
|
|
||||||
border-radius: 4px;
|
|
||||||
/* 窗体比桌面区域大时可以滚动查看 */
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desktop-hint {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: rgb(255 255 255 / 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
.output {
|
|
||||||
max-height: 180px;
|
|
||||||
overflow: auto;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diag {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diag:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diag.error,
|
|
||||||
.error {
|
|
||||||
color: #b91c1c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diag.warning {
|
|
||||||
color: #a16207;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hint {
|
|
||||||
color: #2563eb;
|
|
||||||
padding-left: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.console {
|
|
||||||
background: #111827;
|
|
||||||
color: #e5e7eb;
|
|
||||||
padding: 6px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// 运行区:学生程序的窗体(在一块"桌面"上)、提示框、虚拟实训台,以及编译错误和控制台输出。
|
||||||
|
// 实验页和老师的评阅页共用。
|
||||||
|
import TrainingBoard from '../scene/TrainingBoard.vue'
|
||||||
|
import FormWindow from '../winforms/FormWindow.vue'
|
||||||
|
import MessageBoxDialog from '../winforms/MessageBoxDialog.vue'
|
||||||
|
import type { RunSession } from './session'
|
||||||
|
|
||||||
|
const props = defineProps<{ session: RunSession }>()
|
||||||
|
const emit = defineEmits<{ diagnostic: [file: string, line: number, column: number] }>()
|
||||||
|
|
||||||
|
const state = props.session.state
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="run-area">
|
||||||
|
<div class="desktop">
|
||||||
|
<FormWindow
|
||||||
|
v-for="(form, index) in state.forms"
|
||||||
|
:key="form.root.id"
|
||||||
|
:form="form.root"
|
||||||
|
:modal="form.modal"
|
||||||
|
:offset="index"
|
||||||
|
:act="session.act"
|
||||||
|
/>
|
||||||
|
<MessageBoxDialog
|
||||||
|
v-for="box in state.messageBoxes"
|
||||||
|
:key="box.requestId"
|
||||||
|
:box="box"
|
||||||
|
@reply="session.replyMessageBox(box.requestId, $event)"
|
||||||
|
/>
|
||||||
|
<div v-if="state.forms.length === 0" class="desktop-hint">点击"运行"后,学生程序的窗体会显示在这里</div>
|
||||||
|
</div>
|
||||||
|
<TrainingBoard :board="state.board" @set="session.setBoard" @reset="session.resetBoard" />
|
||||||
|
<div class="output">
|
||||||
|
<div
|
||||||
|
v-for="(d, i) in state.diagnostics"
|
||||||
|
:key="`d${i}`"
|
||||||
|
:class="['diag', d.severity]"
|
||||||
|
title="点击跳到出错的代码"
|
||||||
|
@click="emit('diagnostic', d.file, d.line, d.column)"
|
||||||
|
>
|
||||||
|
{{ d.file }}({{ d.line }},{{ d.column }}) {{ d.code }}:{{ d.message }}
|
||||||
|
<div v-if="d.hint" class="hint">提示:{{ d.hint }}</div>
|
||||||
|
</div>
|
||||||
|
<pre v-for="(e, i) in state.errors" :key="`e${i}`" class="error">{{ e }}</pre>
|
||||||
|
<pre v-if="state.console" class="console">{{ state.console }}</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.run-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 320px;
|
||||||
|
background: #2d6aa3;
|
||||||
|
border-radius: 4px;
|
||||||
|
/* 窗体比桌面区域大时可以滚动查看 */
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-hint {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: rgb(255 255 255 / 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.output {
|
||||||
|
max-height: 180px;
|
||||||
|
overflow: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diag {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diag:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diag.error,
|
||||||
|
.error {
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diag.warning {
|
||||||
|
color: #a16207;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
color: #2563eb;
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console {
|
||||||
|
background: #111827;
|
||||||
|
color: #e5e7eb;
|
||||||
|
padding: 6px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -187,7 +187,12 @@ export function createRunSession() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { state, connect, run, stop, act, replyMessageBox, setBoard, resetBoard }
|
/** 离开页面时断开连接:服务器随即结束正在运行的程序 */
|
||||||
|
function close() {
|
||||||
|
socket?.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return { state, connect, run, stop, act, replyMessageBox, setBoard, resetBoard, close }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RunSession = ReturnType<typeof createRunSession>
|
export type RunSession = ReturnType<typeof createRunSession>
|
||||||
Reference in new issue
Block a user