214 lines
6.0 KiB
Vue
214 lines
6.0 KiB
Vue
<script setup lang="ts">
|
|
import { downloadZip } from "client-zip"
|
|
import type { LANGUAGE, Testcase } from "utils/types"
|
|
import { createTestSubmission } from "utils/judge"
|
|
import { uploadTestcases } from "../../api"
|
|
|
|
interface FileEntry {
|
|
id: number
|
|
in: string
|
|
out: string
|
|
error: boolean
|
|
}
|
|
|
|
interface Props {
|
|
answers: { language: LANGUAGE; code: string }[]
|
|
samples?: { input: string; output: string }[]
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
const emit = defineEmits<{
|
|
uploaded: [testCaseId: string, testCaseScore: Testcase[]]
|
|
}>()
|
|
|
|
const message = useMessage()
|
|
|
|
let nextId = 0
|
|
|
|
function makeInitialFiles(): FileEntry[] {
|
|
const fromSamples = (props.samples ?? []).map((s) => ({ id: nextId++, in: s.input, out: s.output, error: false }))
|
|
const extra = Math.max(0, 5 - fromSamples.length)
|
|
return [...fromSamples, ...Array.from({ length: extra }, () => ({ id: nextId++, in: "", out: "", error: false }))]
|
|
}
|
|
|
|
const files = ref<FileEntry[]>(makeInitialFiles())
|
|
|
|
const selectedLanguage = ref<LANGUAGE>("Python3")
|
|
|
|
// 始终显示所有语言,不管有没有答案代码
|
|
const availableLanguages = computed(() =>
|
|
props.answers.map((a) => ({ label: a.language, value: a.language })),
|
|
)
|
|
|
|
// 当前选中语言是否有答案代码(用于控制"先运行"按钮)
|
|
const hasAnswerCode = computed(() => {
|
|
const answer = props.answers.find((a) => a.language === selectedLanguage.value)
|
|
return !!answer?.code.trim()
|
|
})
|
|
|
|
// 当语言列表变化时,确保 selectedLanguage 始终指向一个有效值
|
|
watch(
|
|
availableLanguages,
|
|
(langs) => {
|
|
if (langs.length && !langs.find((l) => l.value === selectedLanguage.value)) {
|
|
selectedLanguage.value = langs[0].value
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
const isRunning = ref(false)
|
|
const isUploading = ref(false)
|
|
|
|
const hasAnyInput = computed(() => files.value.some((f) => f.in.trim()))
|
|
|
|
const canUpload = computed(
|
|
() =>
|
|
!isRunning.value &&
|
|
hasAnyInput.value &&
|
|
files.value.filter((f) => f.in.trim()).every((f) => f.out && !f.error),
|
|
)
|
|
|
|
function reset() {
|
|
files.value = Array.from({ length: 5 }, () => ({ id: nextId++, in: "", out: "", error: false }))
|
|
}
|
|
|
|
function add(n: number) {
|
|
files.value.push(...Array.from({ length: n }, () => ({ id: nextId++, in: "", out: "", error: false })))
|
|
}
|
|
|
|
function remove(index: number) {
|
|
files.value.splice(index, 1)
|
|
}
|
|
|
|
async function run() {
|
|
const answer = props.answers.find((a) => a.language === selectedLanguage.value)
|
|
if (!answer?.code.trim()) return
|
|
|
|
// 过滤空行,去重(按输入内容)
|
|
const seen = new Set<string>()
|
|
files.value = files.value.filter((f) => {
|
|
if (!f.in.trim()) return false
|
|
if (seen.has(f.in)) return false
|
|
seen.add(f.in)
|
|
return true
|
|
})
|
|
|
|
// 清空旧输出
|
|
files.value = files.value.map((f) => ({ ...f, out: "", error: false }))
|
|
|
|
isRunning.value = true
|
|
await Promise.all(
|
|
files.value.map(async (_, i) => {
|
|
try {
|
|
const result = await createTestSubmission(
|
|
{ language: selectedLanguage.value, value: answer.code },
|
|
files.value[i].in,
|
|
)
|
|
files.value[i] = { ...files.value[i], out: result.output, error: result.status !== 3 }
|
|
} catch {
|
|
files.value[i] = { ...files.value[i], out: "", error: true }
|
|
}
|
|
}),
|
|
)
|
|
isRunning.value = false
|
|
}
|
|
|
|
async function upload() {
|
|
isUploading.value = true
|
|
try {
|
|
const now = new Date()
|
|
const data = files.value
|
|
.filter((f) => f.in.trim() && f.out && !f.error)
|
|
.flatMap((f, i) => [
|
|
{ name: `${i + 1}.in`, input: f.in, lastModified: now },
|
|
{ name: `${i + 1}.out`, input: f.out, lastModified: now },
|
|
])
|
|
|
|
const blob = await downloadZip(data).blob()
|
|
const file = new File([blob], "testcase.zip", { type: "application/zip" })
|
|
|
|
const res = await uploadTestcases(file)
|
|
const testcases: Testcase[] = res.data.info
|
|
const baseScore = Math.floor(100 / testcases.length)
|
|
const remainder = 100 - baseScore * testcases.length
|
|
testcases.forEach((tc, i) => {
|
|
tc.score = String(i === testcases.length - 1 ? baseScore + remainder : baseScore)
|
|
})
|
|
|
|
emit("uploaded", res.data.id, testcases)
|
|
message.success("上传成功")
|
|
} catch {
|
|
message.error("上传失败")
|
|
} finally {
|
|
isUploading.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<n-flex vertical>
|
|
<n-flex align="center" wrap>
|
|
<n-select
|
|
style="width: 120px"
|
|
:options="availableLanguages"
|
|
v-model:value="selectedLanguage"
|
|
/>
|
|
<n-button :disabled="isRunning" @click="reset">清空</n-button>
|
|
<n-button :disabled="isRunning" @click="add(1)">+1</n-button>
|
|
<n-button :disabled="isRunning" @click="add(5)">+5</n-button>
|
|
<n-tooltip :disabled="hasAnswerCode && hasAnyInput">
|
|
<template #trigger>
|
|
<span>
|
|
<n-button
|
|
type="success"
|
|
:loading="isRunning"
|
|
:disabled="!hasAnswerCode || !hasAnyInput"
|
|
@click="run"
|
|
>
|
|
先运行
|
|
</n-button>
|
|
</span>
|
|
</template>
|
|
{{ !hasAnswerCode ? "请先在题目中填写答案代码" : "请先填写输入" }}
|
|
</n-tooltip>
|
|
<n-button
|
|
type="primary"
|
|
:loading="isUploading"
|
|
:disabled="!canUpload"
|
|
@click="upload"
|
|
>
|
|
上传
|
|
</n-button>
|
|
</n-flex>
|
|
|
|
<n-flex
|
|
v-for="(file, index) in files"
|
|
:key="file.id"
|
|
align="start"
|
|
style="gap: 8px"
|
|
>
|
|
<n-flex vertical style="flex: 1">
|
|
<span>{{ index + 1 }}.in</span>
|
|
<n-input type="textarea" v-model:value="file.in" :rows="3" />
|
|
</n-flex>
|
|
<n-flex vertical style="flex: 1">
|
|
<span>{{ index + 1 }}.out</span>
|
|
<n-input
|
|
type="textarea"
|
|
v-model:value="file.out"
|
|
:rows="3"
|
|
:status="file.out ? (file.error ? 'error' : 'success') : undefined"
|
|
/>
|
|
</n-flex>
|
|
<n-button
|
|
:disabled="files.length === 1 || isRunning"
|
|
style="margin-top: 22px"
|
|
@click="remove(index)"
|
|
>
|
|
删除
|
|
</n-button>
|
|
</n-flex>
|
|
</n-flex>
|
|
</template>
|