Compare commits

...

9 Commits

Author SHA1 Message Date
06633c351f update
Some checks are pending
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Waiting to run
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Waiting to run
2026-06-22 18:27:27 -06:00
5ca488616b update
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled
2026-06-22 05:45:07 -06:00
df45b8f545 fix
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled
2026-06-14 09:53:04 -06:00
be0bc87d47 add state for submitting button
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled
2026-06-14 08:52:47 -06:00
43a5c923b4 Plan submit formatting button state 2026-06-14 08:46:35 -06:00
62d75b6e06 Document submit formatting button state 2026-06-14 08:43:19 -06:00
bd4461d2bc fix 2026-06-14 08:36:32 -06:00
12342f7f79 feat(problem): auto-format Python3/C/C++ code before submit 2026-06-14 08:12:31 -06:00
dad65c4bef feat(api): add formatCode endpoint for pre-submit formatting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 08:09:38 -06:00
11 changed files with 1023 additions and 415 deletions

View File

@@ -0,0 +1,259 @@
# Submit Formatting Button State Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Show `格式化中` on the submit button during automatic formatting, then show `正在提交` continuously while the submission request is pending.
**Architecture:** Extract the button presentation rules into a small pure TypeScript function so the state priority can be tested without adding a frontend test framework. Keep formatter and submission-request flags local to `SubmitCode.vue`, with `finally` blocks ensuring both flags clear on every outcome.
**Tech Stack:** Vue 3 Composition API, TypeScript, Node.js built-in test runner, Rsbuild
---
### Task 1: Define and test submit button presentation rules
**Files:**
- Create: `tests/submitButtonState.test.ts`
- Create: `src/oj/problem/components/submitButtonState.ts`
- [ ] **Step 1: Write the failing test**
Create `tests/submitButtonState.test.ts`:
```ts
import assert from "node:assert/strict"
import test from "node:test"
import { getSubmitButtonState } from "../src/oj/problem/components/submitButtonState.ts"
const idleInput = {
isAuthed: true,
hasCode: true,
isFormatting: false,
isSubmitting: false,
isJudging: false,
isCooldown: false,
}
test("shows a disabled loading state while formatting", () => {
assert.deepEqual(
getSubmitButtonState({ ...idleInput, isFormatting: true }),
{
disabled: true,
label: "格式化中",
icon: "eos-icons:loading",
},
)
})
test("shows submitting immediately after formatting", () => {
assert.deepEqual(
getSubmitButtonState({ ...idleInput, isSubmitting: true }),
{
disabled: true,
label: "正在提交",
icon: "eos-icons:loading",
},
)
})
test("preserves existing login, judging, cooldown, and idle states", () => {
assert.deepEqual(
getSubmitButtonState({ ...idleInput, isAuthed: false }),
{
disabled: true,
label: "请先登录",
icon: "ph:play-fill",
},
)
assert.deepEqual(getSubmitButtonState({ ...idleInput, isJudging: true }), {
disabled: true,
label: "正在评分",
icon: "eos-icons:loading",
})
assert.deepEqual(getSubmitButtonState({ ...idleInput, isCooldown: true }), {
disabled: true,
label: "正在冷却",
icon: "ph:lightbulb-fill",
})
assert.deepEqual(getSubmitButtonState(idleInput), {
disabled: false,
label: "提交代码",
icon: "ph:play-fill",
})
})
```
- [ ] **Step 2: Run the test to verify it fails**
Run:
```bash
node --test tests/submitButtonState.test.ts
```
Expected: FAIL with `ERR_MODULE_NOT_FOUND` for `submitButtonState.ts`.
- [ ] **Step 3: Implement the pure state function**
Create `src/oj/problem/components/submitButtonState.ts`:
```ts
export interface SubmitButtonStateInput {
isAuthed: boolean
hasCode: boolean
isFormatting: boolean
isSubmitting: boolean
isJudging: boolean
isCooldown: boolean
}
export interface SubmitButtonState {
disabled: boolean
label: string
icon: string
}
export function getSubmitButtonState({
isAuthed,
hasCode,
isFormatting,
isSubmitting,
isJudging,
isCooldown,
}: SubmitButtonStateInput): SubmitButtonState {
const disabled =
!isAuthed ||
!hasCode ||
isFormatting ||
isSubmitting ||
isJudging ||
isCooldown
let label = "提交代码"
if (!isAuthed) {
label = "请先登录"
} else if (isFormatting) {
label = "格式化中"
} else if (isSubmitting) {
label = "正在提交"
} else if (isJudging) {
label = "正在评分"
} else if (isCooldown) {
label = "正在冷却"
}
const icon =
isFormatting || isSubmitting || isJudging
? "eos-icons:loading"
: isCooldown
? "ph:lightbulb-fill"
: "ph:play-fill"
return { disabled, label, icon }
}
```
- [ ] **Step 4: Run the test to verify it passes**
Run:
```bash
node --test tests/submitButtonState.test.ts
```
Expected: 3 tests pass.
### Task 2: Connect formatting and submission request lifecycle to the button
**Files:**
- Modify: `src/oj/problem/components/SubmitCode.vue`
- [ ] **Step 1: Add local request states and computed presentation**
Import `getSubmitButtonState`, add `isFormatting` and `isSubmittingRequest` refs, and replace the three existing button computed properties with:
```ts
const buttonState = computed(() =>
getSubmitButtonState({
isAuthed: userStore.isAuthed,
hasCode: codeStore.code.value.trim() !== "",
isFormatting: isFormatting.value,
isSubmitting: isSubmittingRequest.value || submitting.value,
isJudging: judging.value || pending.value,
isCooldown: isCooldown.value,
}),
)
```
Use `buttonState.disabled`, `buttonState.icon`, and `buttonState.label` in the template.
- [ ] **Step 2: Guard and track the formatting request**
At the start of `submit`, return when `buttonState.value.disabled` is true. Around `formatCode`, set `isFormatting.value = true` before the request and clear it in `finally`:
```ts
isFormatting.value = true
try {
const res = await formatCode({
code: codeStore.code.value,
language: formatLang,
})
codeStore.setCode(res.data.code)
} catch (e: any) {
if (e?.error === "format-error") {
message.warning(`代码格式化失败:${e.data},请检查代码后重试`)
return
}
} finally {
isFormatting.value = false
}
```
- [ ] **Step 3: Track the submission API request**
Set `isSubmittingRequest.value = true` immediately before `submitCode`, keep the existing success flow inside the `try`, and clear the request state in `finally`:
```ts
isSubmittingRequest.value = true
try {
const res = await submitCode(data)
console.log(`[Submit] 代码已提交: ID=${res.data.submission_id}`)
startCooldown()
startMonitoring(res.data.submission_id)
showResult.value = true
} finally {
isSubmittingRequest.value = false
}
```
- [ ] **Step 4: Run focused tests**
Run:
```bash
node --test tests/submitButtonState.test.ts
```
Expected: 3 tests pass.
- [ ] **Step 5: Run the production build**
Run:
```bash
npm run build
```
Expected: Rsbuild exits with status 0.
- [ ] **Step 6: Check the final diff**
Run:
```bash
git diff --check
git diff -- src/oj/problem/components/SubmitCode.vue src/oj/problem/components/submitButtonState.ts tests/submitButtonState.test.ts
```
Expected: no whitespace errors; diff is limited to the button state feature and its test.

View File

@@ -0,0 +1,34 @@
# Submit Formatting Button State
## Goal
Make the code submission button reflect the automatic formatting request that runs before submission.
## Behavior
- For Python3, C, and C++, the button displays `格式化中` while the formatting API request is pending.
- During formatting, the button uses the existing loading icon and is disabled to prevent duplicate submissions.
- After formatting succeeds, the existing submission flow continues and the button can display `正在提交`.
- A formatting error stops submission and clears the formatting state before showing the existing warning.
- A formatter server or network failure keeps the existing fallback behavior: clear the formatting state and submit the original code.
- Languages without automatic formatting skip this state and submit directly.
- Existing button labels and judging/cooldown behavior remain unchanged.
## Implementation
Add a component-local `isFormatting` ref in `SubmitCode.vue`.
- Include it in `submitDisabled`.
- Give it priority in `submitLabel`, using `格式化中`.
- Include it in the loading-icon condition.
- Set it immediately before `formatCode`.
- Clear it in a `finally` block so every formatter outcome restores the button state.
The state remains local because it is transient UI state owned only by the submission button.
## Verification
The frontend currently has no automated test suite. Verify with:
- TypeScript production build.
- Manual inspection of the state transitions for successful formatting, formatting errors, formatter infrastructure failures, and languages that do not format.

772
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@
"fmt": "prettier --write src *.ts"
},
"dependencies": {
"@codemirror/autocomplete": "^6.20.2",
"@codemirror/autocomplete": "^6.20.3",
"@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-python": "^6.2.1",
"@vue-flow/background": "^1.3.2",
@@ -21,44 +21,44 @@
"@vue-flow/node-toolbar": "^1.1.1",
"@vueuse/core": "^14.3.0",
"@vueuse/router": "^14.3.0",
"@wangeditor-next/editor": "^5.7.0",
"@wangeditor-next/editor": "^5.7.12",
"@wangeditor-next/editor-for-vue": "^5.1.14",
"axios": "^1.16.1",
"axios": "^1.18.0",
"canvas-confetti": "^1.9.4",
"chart.js": "^4.5.1",
"chartjs-chart-wordcloud": "^4.4.5",
"client-zip": "^2.5.0",
"codemirror": "^6.0.2",
"copy-text-to-clipboard": "^3.2.2",
"date-fns": "^4.1.0",
"fflate": "^0.8.2",
"date-fns": "^4.4.0",
"fflate": "^0.8.3",
"highlight.js": "^11.11.1",
"md-editor-v3": "^6.5.0",
"md-editor-v3": "^6.5.1",
"mermaid": "^11.15.0",
"mermaid-legacy": "npm:mermaid@^9.1.7",
"naive-ui": "^2.44.1",
"nanoid": "^5.1.11",
"nanoid": "^5.1.15",
"normalize.css": "^8.0.1",
"pinia": "^3.0.4",
"skulpt": "^1.2.0",
"vue": "^3.5.34",
"vue": "^3.5.38",
"vue-chartjs": "^5.3.3",
"vue-codemirror": "^6.1.1",
"vue-router": "^5.0.6",
"vue-router": "^5.1.0",
"y-codemirror.next": "^0.3.5",
"y-webrtc": "^10.3.0",
"yjs": "^13.6.30"
"yjs": "^13.6.31"
},
"devDependencies": {
"@iconify/vue": "^5.0.1",
"@rsbuild/core": "^1.7.5",
"@rsbuild/plugin-vue": "^1.2.7",
"@rsbuild/plugin-vue": "^1.2.9",
"@types/canvas-confetti": "^1.9.0",
"@types/node": "^25.6.0",
"@types/node": "^26.0.0",
"@vue/tsconfig": "^0.9.1",
"prettier": "^3.8.3",
"prettier": "^3.8.4",
"typescript": "^6.0.3",
"unplugin-auto-import": "^21.0.0",
"unplugin-vue-components": "^32.0.0"
"unplugin-vue-components": "^32.1.0"
}
}

View File

@@ -62,6 +62,10 @@ export function submitCode(data: SubmitCodePayload) {
return http.post("submission", data)
}
export function formatCode(data: { code: string; language: string }) {
return http.post<{ code: string }>("format_code", data)
}
export function getSubmissions(params: Partial<SubmissionListPayload>) {
const endpoint = !!params.contest_id ? "contest_submissions" : "submissions"
return http.get(endpoint, { params })

View File

@@ -7,7 +7,7 @@ import { SOURCES } from "utils/constants"
import SyncCodeEditor from "shared/components/SyncCodeEditor.vue"
import { useBreakpoints } from "shared/composables/breakpoints"
import storage from "utils/storage"
import { LANGUAGE } from "utils/types"
import type { LANGUAGE } from "utils/types"
import Form from "./Form.vue"
const FlowchartEditor = defineAsyncComponent(
@@ -51,6 +51,13 @@ onMounted(loadCode)
watch(() => problem.value?._id, loadCode)
watch(
() => codeStore.code.value,
(v) => {
storage.set(storageKey.value, v)
},
)
const changeCode = (v: string) => {
storage.set(storageKey.value, v)
}

View File

@@ -1,14 +1,20 @@
<script setup lang="ts">
import { Icon } from "@iconify/vue"
import { storeToRefs } from "pinia"
import { getComment, submitCode, updateProblemSetProgress } from "oj/api"
import {
formatCode,
getComment,
submitCode,
updateProblemSetProgress,
} from "oj/api"
import { useCodeStore } from "oj/store/code"
import { useProblemStore } from "oj/store/problem"
import { useFireworks } from "oj/problem/composables/useFireworks"
import { useSubmissionMonitor } from "oj/problem/composables/useSubmissionMonitor"
import { SubmissionStatus } from "utils/constants"
import { LANGUAGE_FORMAT_VALUE, SubmissionStatus } from "utils/constants"
import type { SubmitCodePayload } from "utils/types"
import SubmissionResult from "./SubmissionResult.vue"
import { getSubmitButtonState } from "./submitButtonState"
import { useBreakpoints } from "shared/composables/breakpoints"
import { useUserStore } from "shared/store/user"
import { checkPythonSyntax } from "oj/problem/utils/pythonSyntaxCheck"
@@ -37,16 +43,12 @@ const { isDesktop } = useBreakpoints()
const { celebrate } = useFireworks()
// ==================== 判题监控 ====================
const {
submission,
judging,
pending,
submitting,
isProcessing,
startMonitoring,
} = useSubmissionMonitor()
const { submission, judging, pending, submitting, startMonitoring } =
useSubmissionMonitor()
const showResult = ref(false)
const isFormatting = ref(false)
const isSubmittingRequest = ref(false)
// ==================== 提交冷却 ====================
const { start: startCooldown, isPending: isCooldown } = useTimeout(5000, {
@@ -80,35 +82,20 @@ const { start: goToProblemSetDelayed } = useTimeoutFn(
)
// ==================== 计算属性 ====================
// 按钮禁用逻辑
const submitDisabled = computed(() => {
return (
!userStore.isAuthed ||
codeStore.code.value.trim() === "" ||
isProcessing.value ||
isCooldown.value
)
})
// 按钮文案
const submitLabel = computed(() => {
if (!userStore.isAuthed) return "请先登录"
if (submitting.value) return "正在提交"
if (judging.value || pending.value) return "正在评分"
if (isCooldown.value) return "正在冷却"
return "提交代码"
})
// 按钮图标
const submitIcon = computed(() => {
if (isProcessing.value) return "eos-icons:loading"
if (isCooldown.value) return "ph:lightbulb-fill"
return "ph:play-fill"
})
const buttonState = computed(() =>
getSubmitButtonState({
isAuthed: userStore.isAuthed,
hasCode: codeStore.code.value.trim() !== "",
isFormatting: isFormatting.value,
isSubmitting: isSubmittingRequest.value || submitting.value,
isJudging: judging.value || pending.value,
isCooldown: isCooldown.value,
}),
)
// ==================== 提交函数 ====================
async function submit() {
if (!userStore.isAuthed) return
if (buttonState.value.disabled) return
// 0. Python3 语法检测
if (codeStore.code.language === "Python3") {
@@ -119,6 +106,28 @@ async function submit() {
}
}
// 0.5 提交前自动格式化Python3 用 ruffC/C++ 用 clang-format
const formatLang = LANGUAGE_FORMAT_VALUE[codeStore.code.language]
if (["python", "c", "cpp"].includes(formatLang)) {
isFormatting.value = true
try {
const res = await formatCode({
code: codeStore.code.value,
language: formatLang,
})
codeStore.setCode(res.data.code)
} catch (e: any) {
if (e?.error === "format-error") {
// 仅 Python3 会出现:代码本身存在语法错误
message.warning(`代码格式化失败:${e.data},请检查代码后重试`)
return
}
// server-error / 网络异常:格式化工具问题,静默降级,提交原代码
} finally {
isFormatting.value = false
}
}
// 1. 构建提交数据
const data: SubmitCodePayload = {
problem_id: problem.value!.id,
@@ -129,6 +138,8 @@ async function submit() {
data.contest_id = parseInt(contestID)
}
// 2. 提交代码到后端
isSubmittingRequest.value = true
try {
const res = await submitCode(data)
console.log(`[Submit] 代码已提交: ID=${res.data.submission_id}`)
@@ -136,6 +147,9 @@ async function submit() {
startCooldown()
startMonitoring(res.data.submission_id)
showResult.value = true
} finally {
isSubmittingRequest.value = false
}
}
// ==================== 失败计数 ====================
@@ -213,15 +227,15 @@ watch(
<n-button
:size="isDesktop ? 'medium' : 'small'"
type="primary"
:disabled="submitDisabled"
:disabled="buttonState.disabled"
@click="submit"
>
<template #icon>
<n-icon>
<Icon :icon="submitIcon" />
<Icon :icon="buttonState.icon" />
</n-icon>
</template>
{{ submitLabel }}
{{ buttonState.label }}
</n-button>
</template>

View File

@@ -0,0 +1,53 @@
export interface SubmitButtonStateInput {
isAuthed: boolean
hasCode: boolean
isFormatting: boolean
isSubmitting: boolean
isJudging: boolean
isCooldown: boolean
}
export interface SubmitButtonState {
disabled: boolean
label: string
icon: string
}
export function getSubmitButtonState({
isAuthed,
hasCode,
isFormatting,
isSubmitting,
isJudging,
isCooldown,
}: SubmitButtonStateInput): SubmitButtonState {
const disabled =
!isAuthed ||
!hasCode ||
isFormatting ||
isSubmitting ||
isJudging ||
isCooldown
let label = "提交代码"
if (!isAuthed) {
label = "请先登录"
} else if (isFormatting) {
label = "格式化中"
} else if (isSubmitting) {
label = "正在提交"
} else if (isJudging) {
label = "正在评分"
} else if (isCooldown) {
label = "正在冷却"
}
const icon =
isFormatting || isSubmitting || isJudging
? "eos-icons:loading"
: isCooldown
? "ph:lightbulb-fill"
: "ph:play-fill"
return { disabled, label, icon }
}

File diff suppressed because one or more lines are too long

View File

@@ -218,6 +218,15 @@ watch(
},
)
// 登录状态变化后刷新提交列表,更新提交编号列的可点击状态
watch(
() => userStore.isAuthed,
() => {
listSubmissions()
if (route.name === "submissions") getTodayCount()
},
)
const columns = computed(() => {
const res: DataTableColumn<SubmissionListItem>[] = [
{

View File

@@ -0,0 +1,51 @@
import assert from "node:assert/strict"
import test from "node:test"
import { getSubmitButtonState } from "../src/oj/problem/components/submitButtonState.ts"
const idleInput = {
isAuthed: true,
hasCode: true,
isFormatting: false,
isSubmitting: false,
isJudging: false,
isCooldown: false,
}
test("shows a disabled loading state while formatting", () => {
assert.deepEqual(getSubmitButtonState({ ...idleInput, isFormatting: true }), {
disabled: true,
label: "格式化中",
icon: "eos-icons:loading",
})
})
test("shows submitting immediately after formatting", () => {
assert.deepEqual(getSubmitButtonState({ ...idleInput, isSubmitting: true }), {
disabled: true,
label: "正在提交",
icon: "eos-icons:loading",
})
})
test("preserves existing login, judging, cooldown, and idle states", () => {
assert.deepEqual(getSubmitButtonState({ ...idleInput, isAuthed: false }), {
disabled: true,
label: "请先登录",
icon: "ph:play-fill",
})
assert.deepEqual(getSubmitButtonState({ ...idleInput, isJudging: true }), {
disabled: true,
label: "正在评分",
icon: "eos-icons:loading",
})
assert.deepEqual(getSubmitButtonState({ ...idleInput, isCooldown: true }), {
disabled: true,
label: "正在冷却",
icon: "ph:lightbulb-fill",
})
assert.deepEqual(getSubmitButtonState(idleInput), {
disabled: false,
label: "提交代码",
icon: "ph:play-fill",
})
})