Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
530e1a3d5d
commit
195069450f
480 files changed
+15507
-24650
No files matched your search
@@ -7,18 +7,18 @@ export function useChallengeTone() {
|
||||
return computed(() =>
|
||||
isDark.value
|
||||
? {
|
||||
solid: "#b49cf5",
|
||||
onSolid: "#17112b",
|
||||
text: "#c9b8ff",
|
||||
soft: "rgba(180, 156, 245, 0.14)",
|
||||
border: "rgba(180, 156, 245, 0.35)",
|
||||
solid: '#b49cf5',
|
||||
onSolid: '#17112b',
|
||||
text: '#c9b8ff',
|
||||
soft: 'rgba(180, 156, 245, 0.14)',
|
||||
border: 'rgba(180, 156, 245, 0.35)',
|
||||
}
|
||||
: {
|
||||
solid: "#5b34c4",
|
||||
onSolid: "#ffffff",
|
||||
text: "#2c1a66",
|
||||
soft: "#f4f0fe",
|
||||
border: "#ddd3f6",
|
||||
solid: '#5b34c4',
|
||||
onSolid: '#ffffff',
|
||||
text: '#2c1a66',
|
||||
soft: '#f4f0fe',
|
||||
border: '#ddd3f6',
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCodeStore } from "oj/store/code"
|
||||
import { useProblemStore } from "oj/store/problem"
|
||||
import { useCodeStore } from 'oj/store/code'
|
||||
import { useProblemStore } from 'oj/store/problem'
|
||||
|
||||
/**
|
||||
* 这道题、这门语言的本地代码草稿存在哪个键下。编辑器(存、读草稿)和「重置代码」
|
||||
@@ -16,7 +16,7 @@ export function useDraftKey() {
|
||||
const problemStore = useProblemStore()
|
||||
return computed(() => {
|
||||
const id = problemStore.problem?._id
|
||||
if (!id) return ""
|
||||
if (!id) return ''
|
||||
const language = codeStore.code.language
|
||||
const problemSetId = route.params.problemSetId
|
||||
if (problemSetId) return `problem_${id}_problemset_${problemSetId}_lang_${language}`
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { storeToRefs } from "pinia"
|
||||
import type { Ref } from "vue"
|
||||
import { useCodeStore } from "oj/store/code"
|
||||
import { useProblemStore } from "oj/store/problem"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import { LANGUAGE_FORMAT_VALUE, SOURCES } from "utils/constants"
|
||||
import { compressToBase64, copyToClipboard } from "utils/functions"
|
||||
import storage from "utils/storage"
|
||||
import { useDraftKey } from "./draftKey"
|
||||
import { useTeacherCollab } from "./teacherCollab"
|
||||
import { useFlowchartStore } from "oj/store/flowchart"
|
||||
import { useProblemPageContext } from "./problemPageContext"
|
||||
import { storeToRefs } from 'pinia'
|
||||
import type { Ref } from 'vue'
|
||||
import { useCodeStore } from 'oj/store/code'
|
||||
import { useProblemStore } from 'oj/store/problem'
|
||||
import { useBreakpoints } from 'shared/composables/breakpoints'
|
||||
import { useUserStore } from 'shared/store/user'
|
||||
import { LANGUAGE_FORMAT_VALUE, SOURCES } from 'utils/constants'
|
||||
import { compressToBase64, copyToClipboard } from 'utils/functions'
|
||||
import storage from 'utils/storage'
|
||||
import { useDraftKey } from './draftKey'
|
||||
import { useTeacherCollab } from './teacherCollab'
|
||||
import { useFlowchartStore } from 'oj/store/flowchart'
|
||||
import { useProblemPageContext } from './problemPageContext'
|
||||
|
||||
/**
|
||||
* 「课堂统计」:新标签打开数据统计页,带上这道题(原来是题目页上的弹框)。
|
||||
* 工具栏按钮和手机页签行的「⋯」都走这一个。新标签是因为老师多半正开着这道题在看
|
||||
*/
|
||||
export function openStatistics(problemDisplayId: string) {
|
||||
window.open(`/statistics?problem=${encodeURIComponent(problemDisplayId)}`, "_blank")
|
||||
window.open(`/statistics?problem=${encodeURIComponent(problemDisplayId)}`, '_blank')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,42 +44,41 @@ export function useEditorMenu(statisticsInline: Ref<boolean>) {
|
||||
const items: DropdownOption[] = []
|
||||
// 「本题提交」挪到了「我的提交」抽屉的底部;题单入口没有抽屉,给管理员角色留在这里
|
||||
if (ctx.value.allSubmissionsInMenu && userStore.isAdminRole) {
|
||||
items.push({ label: "本题提交", key: "allSubmissions" })
|
||||
items.push({ label: '本题提交', key: 'allSubmissions' })
|
||||
}
|
||||
if (ctx.value.classStats && !statisticsInline.value && userStore.isTeacherOrAbove) {
|
||||
items.push({ label: "课堂统计", key: "statistics" })
|
||||
items.push({ label: '课堂统计', key: 'statistics' })
|
||||
}
|
||||
if (codeStore.code.language === "Flowchart") {
|
||||
items.push({ label: "清空画布", key: "clearFlowchart" })
|
||||
if (codeStore.code.language === 'Flowchart') {
|
||||
items.push({ label: '清空画布', key: 'clearFlowchart' })
|
||||
} else {
|
||||
if (codeStore.code.language !== "SQL") items.push({ label: "去自测猫", key: "testcat" })
|
||||
items.push({ label: "复制代码", key: "copy" })
|
||||
if (codeStore.code.language !== 'SQL') items.push({ label: '去自测猫', key: 'testcat' })
|
||||
items.push({ label: '复制代码', key: 'copy' })
|
||||
// 协作中的教师不给「重置代码」:那会儿编辑器里是**学生的**代码,而 v-model
|
||||
// 一写回去就顺着 Yjs 同步过去,等于一键清空学生的作业,他还没法撤回
|
||||
if (!teacherCollab.value) items.push({ label: "重置代码", key: "reset" })
|
||||
if (!teacherCollab.value) items.push({ label: '重置代码', key: 'reset' })
|
||||
}
|
||||
if (isDesktop.value && problem.value && userStore.canEditProblem(problem.value))
|
||||
items.push({ label: "编辑题目", key: "edit" })
|
||||
if (isDesktop.value && problem.value && userStore.canEditProblem(problem.value)) items.push({ label: '编辑题目', key: 'edit' })
|
||||
return items
|
||||
})
|
||||
|
||||
async function copy() {
|
||||
const success = await copyToClipboard(codeStore.code.value)
|
||||
message[success ? "success" : "error"](`代码复制${success ? "成功" : "失败"}`)
|
||||
message[success ? 'success' : 'error'](`代码复制${success ? '成功' : '失败'}`)
|
||||
}
|
||||
|
||||
// 重置会把编辑器里的代码换成模板、连本地草稿一起删掉,点错一下写了半节课的代码就没了,先问一句
|
||||
function reset() {
|
||||
dialog.warning({
|
||||
title: "重置代码",
|
||||
content: "编辑器里的代码会换回题目给的模板,存着的草稿也会删掉。确定吗?",
|
||||
positiveText: "重置",
|
||||
negativeText: "再想想",
|
||||
title: '重置代码',
|
||||
content: '编辑器里的代码会换回题目给的模板,存着的草稿也会删掉。确定吗?',
|
||||
positiveText: '重置',
|
||||
negativeText: '再想想',
|
||||
onPositiveClick: () => {
|
||||
const language = codeStore.code.language
|
||||
codeStore.setCode(problem.value!.template[language] || SOURCES[language])
|
||||
storage.remove(draftKey.value)
|
||||
message.success("已换回模板,按 Ctrl+Z 可以撤回")
|
||||
message.success('已换回模板,按 Ctrl+Z 可以撤回')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -90,13 +89,13 @@ export function useEditorMenu(statisticsInline: Ref<boolean>) {
|
||||
const editor = flowchartStore.editor
|
||||
if (!editor) return
|
||||
dialog.warning({
|
||||
title: "清空画布",
|
||||
content: "画布上的图会全部清掉。确定吗?",
|
||||
positiveText: "清空",
|
||||
negativeText: "再想想",
|
||||
title: '清空画布',
|
||||
content: '画布上的图会全部清掉。确定吗?',
|
||||
positiveText: '清空',
|
||||
negativeText: '再想想',
|
||||
onPositiveClick: () => {
|
||||
editor.clear()
|
||||
message.success("已清空,按 Ctrl+Z 可以撤回")
|
||||
message.success('已清空,按 Ctrl+Z 可以撤回')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -106,52 +105,50 @@ export function useEditorMenu(statisticsInline: Ref<boolean>) {
|
||||
lang: LANGUAGE_FORMAT_VALUE[codeStore.code.language],
|
||||
code: codeStore.code.value,
|
||||
// 没有例子的题原来在这里抛 TypeError,点了没反应
|
||||
input: problem.value?.samples[0]?.input ?? "",
|
||||
input: problem.value?.samples[0]?.input ?? '',
|
||||
}
|
||||
const base64 = compressToBase64(JSON.stringify(data))
|
||||
window.open(`${import.meta.env.PUBLIC_CODE_URL}?share=${encodeURIComponent(base64)}`, "_blank")
|
||||
window.open(`${import.meta.env.PUBLIC_CODE_URL}?share=${encodeURIComponent(base64)}`, '_blank')
|
||||
}
|
||||
|
||||
function goEdit() {
|
||||
const p = problem.value!
|
||||
const url = p.contestId
|
||||
? `/admin/contest/${p.contestId}/problem/edit/${p.id}`
|
||||
: `/admin/problem/edit/${p.id}`
|
||||
window.open(router.resolve(url).href, "_blank")
|
||||
const url = p.contestId ? `/admin/contest/${p.contestId}/problem/edit/${p.id}` : `/admin/problem/edit/${p.id}`
|
||||
window.open(router.resolve(url).href, '_blank')
|
||||
}
|
||||
|
||||
function goAllSubmissions() {
|
||||
const href = router.resolve({
|
||||
name: "submissions",
|
||||
name: 'submissions',
|
||||
query: { problem: problem.value!._id },
|
||||
}).href
|
||||
// 协作中走新标签,跳走这一页协作就断了(设计文档第 9 节)
|
||||
if (teacherCollab.value) window.open(href, "_blank")
|
||||
if (teacherCollab.value) window.open(href, '_blank')
|
||||
else router.push(href)
|
||||
}
|
||||
|
||||
/** 菜单项被点了;不是这张菜单里的 key 返回 false,调用方自己处理(页签行的抽屉) */
|
||||
function select(key: string) {
|
||||
switch (key) {
|
||||
case "statistics":
|
||||
case 'statistics':
|
||||
openStatistics(problem.value!._id)
|
||||
return true
|
||||
case "testcat":
|
||||
case 'testcat':
|
||||
goTestCat()
|
||||
return true
|
||||
case "copy":
|
||||
case 'copy':
|
||||
copy()
|
||||
return true
|
||||
case "reset":
|
||||
case 'reset':
|
||||
reset()
|
||||
return true
|
||||
case "clearFlowchart":
|
||||
case 'clearFlowchart':
|
||||
clearFlowchart()
|
||||
return true
|
||||
case "edit":
|
||||
case 'edit':
|
||||
goEdit()
|
||||
return true
|
||||
case "allSubmissions":
|
||||
case 'allSubmissions':
|
||||
goAllSubmissions()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { RouteLocationNormalizedLoaded } from "vue-router"
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
/**
|
||||
* 题目页的三种入口:题库(含课堂)、题单、比赛。同一个 detail.vue 挂在三条路由上。
|
||||
@@ -7,14 +7,14 @@ import type { RouteLocationNormalizedLoaded } from "vue-router"
|
||||
* 在题单里是「禁用」、在比赛里是「隐藏」,加一处功能就得把每个组件翻一遍
|
||||
* (见 docs/specs/2026-09-28-problem-page-redesign-design.md 第 8 节)。
|
||||
*/
|
||||
export type ProblemEntry = "problem" | "problemset" | "contest"
|
||||
export type ProblemEntry = 'problem' | 'problemset' | 'contest'
|
||||
|
||||
export type ProblemDrawer = "info" | "reaction" | "submission"
|
||||
export type ProblemDrawer = 'info' | 'reaction' | 'submission'
|
||||
|
||||
export const DRAWER_TITLE: Record<ProblemDrawer, string> = {
|
||||
info: "统计",
|
||||
reaction: "点评",
|
||||
submission: "我的提交",
|
||||
info: '统计',
|
||||
reaction: '点评',
|
||||
submission: '我的提交',
|
||||
}
|
||||
|
||||
interface EntryRules {
|
||||
@@ -58,7 +58,7 @@ interface EntryRules {
|
||||
|
||||
const RULES: Record<ProblemEntry, EntryRules> = {
|
||||
problem: {
|
||||
drawers: ["info", "reaction", "submission"],
|
||||
drawers: ['info', 'reaction', 'submission'],
|
||||
statusTag: true,
|
||||
similar: true,
|
||||
lessonNext: true,
|
||||
@@ -87,7 +87,7 @@ const RULES: Record<ProblemEntry, EntryRules> = {
|
||||
allSubmissionsInMenu: true,
|
||||
},
|
||||
contest: {
|
||||
drawers: ["info", "submission"],
|
||||
drawers: ['info', 'submission'],
|
||||
statusTag: true,
|
||||
// 相似题接口按 displayId 在公开题库里找,比赛题的编号默认是 1/2/3,
|
||||
// 撞上同号公开题时反而会在比赛中把题库列给学生
|
||||
@@ -104,9 +104,9 @@ const RULES: Record<ProblemEntry, EntryRules> = {
|
||||
}
|
||||
|
||||
export function problemEntryOf(route: RouteLocationNormalizedLoaded): ProblemEntry {
|
||||
if (route.name === "contest problem") return "contest"
|
||||
if (route.name === "problemset problem") return "problemset"
|
||||
return "problem"
|
||||
if (route.name === 'contest problem') return 'contest'
|
||||
if (route.name === 'problemset problem') return 'problemset'
|
||||
return 'problem'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,8 +118,8 @@ export function useProblemPageContext() {
|
||||
const entry = problemEntryOf(route)
|
||||
return {
|
||||
entry,
|
||||
contestId: entry === "contest" ? String(route.params.contestID ?? "") : "",
|
||||
problemSetId: entry === "problemset" ? String(route.params.problemSetId ?? "") : "",
|
||||
contestId: entry === 'contest' ? String(route.params.contestID ?? '') : '',
|
||||
problemSetId: entry === 'problemset' ? String(route.params.problemSetId ?? '') : '',
|
||||
...RULES[entry],
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useProblemStore } from "oj/store/problem"
|
||||
import { useCollabStore } from "shared/store/collab"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import { useProblemPageContext } from "./problemPageContext"
|
||||
import { useProblemStore } from 'oj/store/problem'
|
||||
import { useCollabStore } from 'shared/store/collab'
|
||||
import { useUserStore } from 'shared/store/user'
|
||||
import { useProblemPageContext } from './problemPageContext'
|
||||
|
||||
/**
|
||||
* 老师正在这道题上协作:编辑器里是**学生的**代码,「我的提交」看的是学生的,
|
||||
@@ -21,10 +21,5 @@ export function useCollabHere() {
|
||||
const collabStore = useCollabStore()
|
||||
const problemStore = useProblemStore()
|
||||
const ctx = useProblemPageContext()
|
||||
return computed(
|
||||
() =>
|
||||
ctx.value.collab &&
|
||||
collabStore.room !== null &&
|
||||
collabStore.room.problemId === problemStore.problem?._id,
|
||||
)
|
||||
return computed(() => ctx.value.collab && collabStore.room !== null && collabStore.room.problemId === problemStore.problem?._id)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import confetti from "canvas-confetti"
|
||||
import { getCurrentScope, onScopeDispose } from "vue"
|
||||
import confetti from 'canvas-confetti'
|
||||
import { getCurrentScope, onScopeDispose } from 'vue'
|
||||
|
||||
// 表情图标取自 @iconify 的 streamline-emojis 图标集,与项目内 <Icon icon="streamline-emojis:..."> 用法保持一致
|
||||
const EMOJI_SVGS = [
|
||||
@@ -16,16 +16,16 @@ let emojiShapesPromise: Promise<confetti.Shape[]> | null = null
|
||||
|
||||
function svgToBitmapShape(svg: string): Promise<confetti.Shape> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const blob = new Blob([svg], { type: "image/svg+xml" })
|
||||
const blob = new Blob([svg], { type: 'image/svg+xml' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const img = new Image()
|
||||
img.onload = async () => {
|
||||
const renderSize = 64
|
||||
const baseUnit = 10
|
||||
const canvas = document.createElement("canvas")
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = renderSize
|
||||
canvas.height = renderSize
|
||||
const ctx = canvas.getContext("2d")!
|
||||
const ctx = canvas.getContext('2d')!
|
||||
ctx.drawImage(img, 0, 0, renderSize, renderSize)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
@@ -33,7 +33,7 @@ function svgToBitmapShape(svg: string): Promise<confetti.Shape> {
|
||||
const bitmap = await createImageBitmap(canvas)
|
||||
const scale = baseUnit / renderSize
|
||||
resolve({
|
||||
type: "bitmap",
|
||||
type: 'bitmap',
|
||||
bitmap,
|
||||
matrix: [scale, 0, 0, scale, (-renderSize * scale) / 2, (-renderSize * scale) / 2],
|
||||
} as unknown as confetti.Shape)
|
||||
@@ -54,16 +54,16 @@ function loadEmojiShapes(): Promise<confetti.Shape[]> {
|
||||
}
|
||||
|
||||
const COLORS = {
|
||||
green: "#18a058",
|
||||
mint: "#2cc27a",
|
||||
gold: "#f0a020",
|
||||
lightGold: "#ffd36b",
|
||||
orange: "#f06a1a",
|
||||
pink: "#e8457f",
|
||||
lightPink: "#ff7aa8",
|
||||
blue: "#2080f0",
|
||||
purple: "#7d50e8",
|
||||
silver: "#8f99aa",
|
||||
green: '#18a058',
|
||||
mint: '#2cc27a',
|
||||
gold: '#f0a020',
|
||||
lightGold: '#ffd36b',
|
||||
orange: '#f06a1a',
|
||||
pink: '#e8457f',
|
||||
lightPink: '#ff7aa8',
|
||||
blue: '#2080f0',
|
||||
purple: '#7d50e8',
|
||||
silver: '#8f99aa',
|
||||
}
|
||||
const PALETTES = [
|
||||
[COLORS.green, COLORS.gold, COLORS.blue, COLORS.pink],
|
||||
@@ -92,9 +92,9 @@ function loadPathShapes() {
|
||||
if (!pathShapes) {
|
||||
const matrix = [0.5, 0, 0, 0.5, -5, -5] as unknown as DOMMatrix
|
||||
pathShapes = {
|
||||
check: confetti.shapeFromPath({ path: "M1 11 L4 8 L8 12 L16 3 L19 6 L8 17 Z", matrix }),
|
||||
check: confetti.shapeFromPath({ path: 'M1 11 L4 8 L8 12 L16 3 L19 6 L8 17 Z', matrix }),
|
||||
heart: confetti.shapeFromPath({
|
||||
path: "M10 18 C2 12 0 7 3 4 C6 1 9 3 10 5 C11 3 14 1 17 4 C20 7 18 12 10 18 Z",
|
||||
path: 'M10 18 C2 12 0 7 3 4 C6 1 9 3 10 5 C11 3 14 1 17 4 C20 7 18 12 10 18 Z',
|
||||
matrix,
|
||||
}),
|
||||
}
|
||||
@@ -185,7 +185,7 @@ export function useFireworks() {
|
||||
ticks: 26,
|
||||
scalar: 0.65,
|
||||
flat: true,
|
||||
shapes: ["circle"],
|
||||
shapes: ['circle'],
|
||||
colors: [COLORS.gold, COLORS.lightGold],
|
||||
origin: { x: x0 + (x - x0) * eased, y: 1.02 - (1.02 - y) * eased },
|
||||
})
|
||||
@@ -201,7 +201,7 @@ export function useFireworks() {
|
||||
decay: 0.92,
|
||||
gravity: 0.55,
|
||||
flat: true,
|
||||
shapes: ["circle"],
|
||||
shapes: ['circle'],
|
||||
colors,
|
||||
origin: { x, y },
|
||||
}
|
||||
@@ -236,12 +236,7 @@ export function useFireworks() {
|
||||
for (let i = 0; i < 7; i++) {
|
||||
runLater(() => {
|
||||
const left = i % 2 === 0
|
||||
shell(
|
||||
left ? 0.08 + Math.random() * 0.3 : 0.62 + Math.random() * 0.3,
|
||||
0.14 + Math.random() * 0.26,
|
||||
pick(PALETTES),
|
||||
0.9 + Math.random() * 0.3,
|
||||
)
|
||||
shell(left ? 0.08 + Math.random() * 0.3 : 0.62 + Math.random() * 0.3, 0.14 + Math.random() * 0.26, pick(PALETTES), 0.9 + Math.random() * 0.3)
|
||||
}, i * 240)
|
||||
}
|
||||
},
|
||||
@@ -258,7 +253,7 @@ export function useFireworks() {
|
||||
decay: 0.97,
|
||||
ticks: 120,
|
||||
scalar: 1.1,
|
||||
shapes: ["star"],
|
||||
shapes: ['star'],
|
||||
colors: [COLORS.gold, COLORS.gold, COLORS.pink, COLORS.blue, COLORS.silver],
|
||||
origin: { x: Math.random(), y: -0.05 },
|
||||
})
|
||||
@@ -269,13 +264,7 @@ export function useFireworks() {
|
||||
() => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
runLater(() => {
|
||||
shell(
|
||||
0.14 + 0.18 * i,
|
||||
0.38 - 0.2 * Math.sin((Math.PI * i) / 4),
|
||||
[RAINBOW[i], COLORS.silver],
|
||||
1,
|
||||
0.5,
|
||||
)
|
||||
shell(0.14 + 0.18 * i, 0.38 - 0.2 * Math.sin((Math.PI * i) / 4), [RAINBOW[i], COLORS.silver], 1, 0.5)
|
||||
}, i * 170)
|
||||
}
|
||||
},
|
||||
@@ -294,7 +283,7 @@ export function useFireworks() {
|
||||
ticks: 95,
|
||||
scalar: 0.8,
|
||||
flat: true,
|
||||
shapes: ["circle"],
|
||||
shapes: ['circle'],
|
||||
colors: [color],
|
||||
}
|
||||
fire({ ...base, angle: 62, origin: { x: 0, y: 1 } })
|
||||
@@ -318,7 +307,7 @@ export function useFireworks() {
|
||||
ticks: 30,
|
||||
scalar: 0.6,
|
||||
flat: true,
|
||||
shapes: ["circle"],
|
||||
shapes: ['circle'],
|
||||
colors: [COLORS.silver, COLORS.gold],
|
||||
origin: {
|
||||
x: 0.5 + (Math.random() - 0.5) * 0.3,
|
||||
@@ -349,7 +338,7 @@ export function useFireworks() {
|
||||
ticks: 40,
|
||||
scalar: 0.8,
|
||||
flat: true,
|
||||
shapes: ["circle"],
|
||||
shapes: ['circle'],
|
||||
colors: [arm ? COLORS.blue : COLORS.pink],
|
||||
origin: { x: 0.5 + Math.sin(phase) * 0.05 * (1 - progress * 0.6), y },
|
||||
})
|
||||
@@ -365,7 +354,7 @@ export function useFireworks() {
|
||||
const list = [
|
||||
{ x: 0.22, y: 0.36, shape: shapes.check, colors: [COLORS.green, COLORS.mint] },
|
||||
{ x: 0.5, y: 0.3, shape: shapes.heart, colors: [COLORS.pink, COLORS.lightPink] },
|
||||
{ x: 0.78, y: 0.36, shape: "star" as const, colors: [COLORS.gold, COLORS.orange] },
|
||||
{ x: 0.78, y: 0.36, shape: 'star' as const, colors: [COLORS.gold, COLORS.orange] },
|
||||
]
|
||||
list.forEach((item, i) => {
|
||||
runLater(() => {
|
||||
@@ -406,7 +395,7 @@ export function useFireworks() {
|
||||
ticks: 50,
|
||||
scalar: 0.9,
|
||||
flat: true,
|
||||
shapes: ["circle"],
|
||||
shapes: ['circle'],
|
||||
colors: [colors[arm]],
|
||||
origin: { x: 0.5, y: 0.42 },
|
||||
})
|
||||
@@ -430,7 +419,7 @@ export function useFireworks() {
|
||||
ticks: 110,
|
||||
scalar: 0.8,
|
||||
flat: true,
|
||||
shapes: ["circle"],
|
||||
shapes: ['circle'],
|
||||
colors: [COLORS.gold, COLORS.gold, COLORS.silver, COLORS.orange],
|
||||
origin: { x: 0.06 + Math.random() * 0.88, y: 0.06 },
|
||||
})
|
||||
@@ -470,10 +459,7 @@ export function useFireworks() {
|
||||
*/
|
||||
function celebrate(): Promise<void> {
|
||||
pick(fireworkTypes)()
|
||||
return Promise.race([
|
||||
finished(),
|
||||
new Promise<void>((resolve) => setTimeout(resolve, MAX_WAIT_MS)),
|
||||
])
|
||||
return Promise.race([finished(), new Promise<void>((resolve) => setTimeout(resolve, MAX_WAIT_MS))])
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AiHintDone } from "@oj2/contract"
|
||||
import { submitHintFeedback } from "oj/api"
|
||||
import { useAIStream } from "shared/composables/aiStream"
|
||||
import type { AiHintDone } from '@oj2/contract'
|
||||
import { submitHintFeedback } from 'oj/api'
|
||||
import { useAIStream } from 'shared/composables/aiStream'
|
||||
|
||||
/**
|
||||
* 一次提交的 AI 提示。
|
||||
@@ -14,12 +14,12 @@ import { useAIStream } from "shared/composables/aiStream"
|
||||
* 逐字显示改在这边模拟。
|
||||
*/
|
||||
export function useSubmissionHint() {
|
||||
const hintTarget = ref("")
|
||||
const hintContent = ref("")
|
||||
const hintTarget = ref('')
|
||||
const hintContent = ref('')
|
||||
const hintStream = useAIStream()
|
||||
// 整条流跑完才算结束:提示是一次推全文,没有「第一个字到了」的中间态
|
||||
const hintLoading = hintStream.running
|
||||
const hintError = ref("")
|
||||
const hintError = ref('')
|
||||
// 这条提示在 ai_hint 里的 id,生成完由 done 事件带回来;后端落库失败时没有,就不出评价按钮
|
||||
const hintId = ref<number | null>(null)
|
||||
// 这条提示是哪一级(-1 = 编译错误那一档,不在阶梯上),以及还能不能再往上要一级。
|
||||
@@ -55,9 +55,9 @@ export function useSubmissionHint() {
|
||||
|
||||
function resetHint() {
|
||||
stopTyping()
|
||||
hintTarget.value = ""
|
||||
hintContent.value = ""
|
||||
hintError.value = ""
|
||||
hintTarget.value = ''
|
||||
hintContent.value = ''
|
||||
hintError.value = ''
|
||||
hintId.value = null
|
||||
hintLevel.value = null
|
||||
hintCanEscalate.value = false
|
||||
@@ -76,7 +76,7 @@ export function useSubmissionHint() {
|
||||
resetHint()
|
||||
try {
|
||||
await hintStream.run<AiHintDone>(
|
||||
"ai/hint",
|
||||
'ai/hint',
|
||||
{ submissionId, more },
|
||||
{
|
||||
onDelta(content) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ref, computed } from "vue"
|
||||
import { getSubmission } from "oj/api"
|
||||
import { SubmissionStatus } from "utils/constants"
|
||||
import type { PendingAchievement, Submission } from "utils/types"
|
||||
import { useAchievementStore } from "shared/store/achievement"
|
||||
import { useSubmissionWebSocket, type SubmissionUpdate } from "shared/composables/websocket"
|
||||
import { useFallbackPoll } from "shared/composables/fallbackPoll"
|
||||
import { ref, computed } from 'vue'
|
||||
import { getSubmission } from 'oj/api'
|
||||
import { SubmissionStatus } from 'utils/constants'
|
||||
import type { PendingAchievement, Submission } from 'utils/types'
|
||||
import { useAchievementStore } from 'shared/store/achievement'
|
||||
import { useSubmissionWebSocket, type SubmissionUpdate } from 'shared/composables/websocket'
|
||||
import { useFallbackPoll } from 'shared/composables/fallbackPoll'
|
||||
|
||||
/**
|
||||
* 判题监控 Composable
|
||||
@@ -12,7 +12,7 @@ import { useFallbackPoll } from "shared/composables/fallbackPoll"
|
||||
*/
|
||||
export function useSubmissionMonitor() {
|
||||
// ==================== 状态 ====================
|
||||
const submissionId = ref("")
|
||||
const submissionId = ref('')
|
||||
const submission = ref<Submission>()
|
||||
|
||||
// ==================== WebSocket 处理 ====================
|
||||
@@ -23,18 +23,18 @@ export function useSubmissionMonitor() {
|
||||
type: string
|
||||
achievements?: PendingAchievement[]
|
||||
}
|
||||
if (frame.type === "achievement_unlocked") {
|
||||
if (frame.type === 'achievement_unlocked') {
|
||||
useAchievementStore().enqueue(frame.achievements ?? [])
|
||||
return
|
||||
}
|
||||
if (frame.type !== "submission_update") {
|
||||
if (frame.type !== 'submission_update') {
|
||||
return
|
||||
}
|
||||
|
||||
console.log("[SubmissionMonitor] 收到WebSocket更新:", data)
|
||||
console.log('[SubmissionMonitor] 收到WebSocket更新:', data)
|
||||
|
||||
if (data.submissionId !== submissionId.value) {
|
||||
console.log("[SubmissionMonitor] 提交ID不匹配,忽略")
|
||||
console.log('[SubmissionMonitor] 提交ID不匹配,忽略')
|
||||
return
|
||||
}
|
||||
// 订阅后服务端立刻回的那一帧(还在排队也回)就算:WebSocket 是通的,轮询退到慢档
|
||||
@@ -44,11 +44,11 @@ export function useSubmissionMonitor() {
|
||||
submission.value = {} as Submission
|
||||
}
|
||||
|
||||
submission.value.result = data.result as Submission["result"]
|
||||
submission.value.result = data.result as Submission['result']
|
||||
|
||||
// 判题完成或出错,获取完整详情
|
||||
if (data.status === "finished" || data.status === "error") {
|
||||
console.log(`[SubmissionMonitor] 判题${data.status === "finished" ? "完成" : "出错"}`)
|
||||
if (data.status === 'finished' || data.status === 'error') {
|
||||
console.log(`[SubmissionMonitor] 判题${data.status === 'finished' ? '完成' : '出错'}`)
|
||||
|
||||
// 停止轮询(WebSocket已成功)
|
||||
fallback.stop()
|
||||
@@ -66,8 +66,7 @@ export function useSubmissionMonitor() {
|
||||
}
|
||||
|
||||
// 初始化 WebSocket
|
||||
const { connect, subscribe, unsubscribe, scheduleDisconnect, cancelScheduledDisconnect, status } =
|
||||
useSubmissionWebSocket(handleSubmissionUpdate)
|
||||
const { connect, subscribe, unsubscribe, scheduleDisconnect, cancelScheduledDisconnect, status } = useSubmissionWebSocket(handleSubmissionUpdate)
|
||||
|
||||
// ==================== 轮询兜底 ====================
|
||||
const fallback = useFallbackPoll({
|
||||
@@ -119,7 +118,7 @@ export function useSubmissionMonitor() {
|
||||
// 连接由 store 持有,离开题目页组件卸了它也还在。和判完之后一样,空闲 15 分钟断开;
|
||||
// 不在这里立刻断,是因为刚交的那条判完之后还可能推成就过来
|
||||
scheduleDisconnect(15 * 60 * 1000)
|
||||
submissionId.value = ""
|
||||
submissionId.value = ''
|
||||
submission.value = undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { StatisticInfo, TrialRunResponse } from "@oj2/contract"
|
||||
import { errorMessage } from "utils/api"
|
||||
import { SubmissionStatus } from "utils/constants"
|
||||
import { isRunnableLanguage, trialRun } from "utils/judge"
|
||||
import type { Code } from "utils/types"
|
||||
import type { StatisticInfo, TrialRunResponse } from '@oj2/contract'
|
||||
import { errorMessage } from 'utils/api'
|
||||
import { SubmissionStatus } from 'utils/constants'
|
||||
import { isRunnableLanguage, trialRun } from 'utils/judge'
|
||||
import type { Code } from 'utils/types'
|
||||
|
||||
/**
|
||||
* 试跑:「运行例子」和「自己输入」。走本站判题机(utils/judge.ts → POST /trial-runs),
|
||||
@@ -26,27 +26,26 @@ interface TrialOutcome {
|
||||
/** 没跑成、或者要换一种说法时给学生看的中文,有它就不用 TrialErrorNote 的默认说明 */
|
||||
note?: string
|
||||
/** 运行时错误的诊断(第几行、什么异常),和提交的同一套,交给 RuntimeErrorExplain */
|
||||
runtimeError?: NonNullable<StatisticInfo["runtime_error"]>
|
||||
runtimeError?: NonNullable<StatisticInfo['runtime_error']>
|
||||
}
|
||||
|
||||
const TOO_MUCH_OUTPUT =
|
||||
"程序输出太多了,一直停不下来:多半是循环没有结束的条件(条件一直成立、循环变量忘了改),在不停地打印。"
|
||||
const TOO_MUCH_OUTPUT = '程序输出太多了,一直停不下来:多半是循环没有结束的条件(条件一直成立、循环变量忘了改),在不停地打印。'
|
||||
|
||||
/**
|
||||
* 把一次试跑的回包摊成每组一条。编译没过就每组都是编译失败、带同一份报错;
|
||||
* 输出太多的时候判题机回包被掐断了,分不出是哪一组,每组都挂上同一句说明。
|
||||
*/
|
||||
function outcomes(response: TrialRunResponse, count: number): TrialOutcome[] {
|
||||
if (response.status === "compile-error") {
|
||||
if (response.status === 'compile-error') {
|
||||
return Array.from({ length: count }, () => ({
|
||||
result: SubmissionStatus.compile_error,
|
||||
output: response.message,
|
||||
}))
|
||||
}
|
||||
if (response.status === "too-much-output") {
|
||||
if (response.status === 'too-much-output') {
|
||||
return Array.from({ length: count }, () => ({
|
||||
result: SubmissionStatus.runtime_error,
|
||||
output: "",
|
||||
output: '',
|
||||
note: TOO_MUCH_OUTPUT,
|
||||
}))
|
||||
}
|
||||
@@ -54,8 +53,7 @@ function outcomes(response: TrialRunResponse, count: number): TrialOutcome[] {
|
||||
// 开头的空格是输出的一部分(打印菱形、三角形),不能动
|
||||
return response.cases.map((item) => {
|
||||
const result = item.result as SubmissionStatus
|
||||
const finished =
|
||||
result === SubmissionStatus.accepted || result === SubmissionStatus.wrong_answer
|
||||
const finished = result === SubmissionStatus.accepted || result === SubmissionStatus.wrong_answer
|
||||
return {
|
||||
result,
|
||||
output: finished ? item.output.trimEnd() : item.output,
|
||||
@@ -64,15 +62,11 @@ function outcomes(response: TrialRunResponse, count: number): TrialOutcome[] {
|
||||
})
|
||||
}
|
||||
|
||||
async function runCases(
|
||||
code: Code,
|
||||
target: TrialTarget,
|
||||
cases: { input: string; output?: string }[],
|
||||
): Promise<TrialOutcome[]> {
|
||||
async function runCases(code: Code, target: TrialTarget, cases: { input: string; output?: string }[]): Promise<TrialOutcome[]> {
|
||||
if (!isRunnableLanguage(code.language)) {
|
||||
return cases.map(() => ({
|
||||
result: SubmissionStatus.system_error,
|
||||
output: "",
|
||||
output: '',
|
||||
note: `${code.language} 不能试运行`,
|
||||
}))
|
||||
}
|
||||
@@ -87,8 +81,8 @@ async function runCases(
|
||||
return outcomes(response, cases.length)
|
||||
} catch (error) {
|
||||
// 后端的文案是给学生看的(「上一次还没跑完」「试运行的人太多了」)
|
||||
const note = errorMessage(error, "试跑的服务暂时连不上,稍后再试一次。")
|
||||
return cases.map(() => ({ result: SubmissionStatus.system_error, output: "", note }))
|
||||
const note = errorMessage(error, '试跑的服务暂时连不上,稍后再试一次。')
|
||||
return cases.map(() => ({ result: SubmissionStatus.system_error, output: '', note }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +94,7 @@ export interface SampleRun {
|
||||
output: string
|
||||
result: SubmissionStatus
|
||||
note?: string
|
||||
runtimeError?: TrialOutcome["runtimeError"]
|
||||
runtimeError?: TrialOutcome['runtimeError']
|
||||
}
|
||||
|
||||
export function useTrialRun() {
|
||||
@@ -111,13 +105,13 @@ export function useTrialRun() {
|
||||
/** 跑例子时用的那份代码:错因说明要看代码(比如有没有 input("提示文字")),编辑器里可能已经改了 */
|
||||
const samplesCode = ref<Code | null>(null)
|
||||
|
||||
const customInput = ref("")
|
||||
const customOutput = ref("")
|
||||
const customInput = ref('')
|
||||
const customOutput = ref('')
|
||||
const customResult = ref<SubmissionStatus | null>(null)
|
||||
const customNote = ref("")
|
||||
const customRuntimeError = ref<TrialOutcome["runtimeError"] | null>(null)
|
||||
const customNote = ref('')
|
||||
const customRuntimeError = ref<TrialOutcome['runtimeError'] | null>(null)
|
||||
/** 跑「自己输入」时用的那份代码:出错那一行要从它里面摆出来,编辑器里可能已经改了 */
|
||||
const customCode = ref("")
|
||||
const customCode = ref('')
|
||||
const customRunning = ref(false)
|
||||
|
||||
/**
|
||||
@@ -127,11 +121,7 @@ export function useTrialRun() {
|
||||
let samplesGeneration = 0
|
||||
let customGeneration = 0
|
||||
|
||||
async function runSamples(
|
||||
code: Code,
|
||||
target: TrialTarget,
|
||||
samples: { input: string; output: string }[],
|
||||
) {
|
||||
async function runSamples(code: Code, target: TrialTarget, samples: { input: string; output: string }[]) {
|
||||
const mine = ++samplesGeneration
|
||||
samplesRunning.value = true
|
||||
const runs = await runCases(code, target, samples)
|
||||
@@ -155,7 +145,7 @@ export function useTrialRun() {
|
||||
if (mine !== customGeneration) return
|
||||
customOutput.value = run!.output
|
||||
customResult.value = run!.result
|
||||
customNote.value = run!.note ?? ""
|
||||
customNote.value = run!.note ?? ''
|
||||
customRuntimeError.value = run!.runtimeError ?? null
|
||||
customCode.value = code.value
|
||||
customRunning.value = false
|
||||
@@ -169,12 +159,12 @@ export function useTrialRun() {
|
||||
samplesRunning.value = false
|
||||
samplesRunAt.value = null
|
||||
samplesCode.value = null
|
||||
customInput.value = ""
|
||||
customOutput.value = ""
|
||||
customInput.value = ''
|
||||
customOutput.value = ''
|
||||
customResult.value = null
|
||||
customNote.value = ""
|
||||
customNote.value = ''
|
||||
customRuntimeError.value = null
|
||||
customCode.value = ""
|
||||
customCode.value = ''
|
||||
customRunning.value = false
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user