Compare commits
7 Commits
d16ee709b2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| df45b8f545 | |||
| be0bc87d47 | |||
| 43a5c923b4 | |||
| 62d75b6e06 | |||
| bd4461d2bc | |||
| 12342f7f79 | |||
| dad65c4bef |
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -62,6 +62,10 @@ export function submitCode(data: SubmitCodePayload) {
|
|||||||
return http.post("submission", data)
|
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>) {
|
export function getSubmissions(params: Partial<SubmissionListPayload>) {
|
||||||
const endpoint = !!params.contest_id ? "contest_submissions" : "submissions"
|
const endpoint = !!params.contest_id ? "contest_submissions" : "submissions"
|
||||||
return http.get(endpoint, { params })
|
return http.get(endpoint, { params })
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { SOURCES } from "utils/constants"
|
|||||||
import SyncCodeEditor from "shared/components/SyncCodeEditor.vue"
|
import SyncCodeEditor from "shared/components/SyncCodeEditor.vue"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
import storage from "utils/storage"
|
import storage from "utils/storage"
|
||||||
import { LANGUAGE } from "utils/types"
|
import type { LANGUAGE } from "utils/types"
|
||||||
import Form from "./Form.vue"
|
import Form from "./Form.vue"
|
||||||
|
|
||||||
const FlowchartEditor = defineAsyncComponent(
|
const FlowchartEditor = defineAsyncComponent(
|
||||||
@@ -51,6 +51,13 @@ onMounted(loadCode)
|
|||||||
|
|
||||||
watch(() => problem.value?._id, loadCode)
|
watch(() => problem.value?._id, loadCode)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => codeStore.code.value,
|
||||||
|
(v) => {
|
||||||
|
storage.set(storageKey.value, v)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const changeCode = (v: string) => {
|
const changeCode = (v: string) => {
|
||||||
storage.set(storageKey.value, v)
|
storage.set(storageKey.value, v)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from "@iconify/vue"
|
import { Icon } from "@iconify/vue"
|
||||||
import { storeToRefs } from "pinia"
|
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 { useCodeStore } from "oj/store/code"
|
||||||
import { useProblemStore } from "oj/store/problem"
|
import { useProblemStore } from "oj/store/problem"
|
||||||
import { useFireworks } from "oj/problem/composables/useFireworks"
|
import { useFireworks } from "oj/problem/composables/useFireworks"
|
||||||
import { useSubmissionMonitor } from "oj/problem/composables/useSubmissionMonitor"
|
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 type { SubmitCodePayload } from "utils/types"
|
||||||
import SubmissionResult from "./SubmissionResult.vue"
|
import SubmissionResult from "./SubmissionResult.vue"
|
||||||
|
import { getSubmitButtonState } from "./submitButtonState"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
import { checkPythonSyntax } from "oj/problem/utils/pythonSyntaxCheck"
|
import { checkPythonSyntax } from "oj/problem/utils/pythonSyntaxCheck"
|
||||||
@@ -37,16 +43,12 @@ const { isDesktop } = useBreakpoints()
|
|||||||
const { celebrate } = useFireworks()
|
const { celebrate } = useFireworks()
|
||||||
|
|
||||||
// ==================== 判题监控 ====================
|
// ==================== 判题监控 ====================
|
||||||
const {
|
const { submission, judging, pending, submitting, startMonitoring } =
|
||||||
submission,
|
useSubmissionMonitor()
|
||||||
judging,
|
|
||||||
pending,
|
|
||||||
submitting,
|
|
||||||
isProcessing,
|
|
||||||
startMonitoring,
|
|
||||||
} = useSubmissionMonitor()
|
|
||||||
|
|
||||||
const showResult = ref(false)
|
const showResult = ref(false)
|
||||||
|
const isFormatting = ref(false)
|
||||||
|
const isSubmittingRequest = ref(false)
|
||||||
|
|
||||||
// ==================== 提交冷却 ====================
|
// ==================== 提交冷却 ====================
|
||||||
const { start: startCooldown, isPending: isCooldown } = useTimeout(5000, {
|
const { start: startCooldown, isPending: isCooldown } = useTimeout(5000, {
|
||||||
@@ -80,35 +82,20 @@ const { start: goToProblemSetDelayed } = useTimeoutFn(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ==================== 计算属性 ====================
|
// ==================== 计算属性 ====================
|
||||||
// 按钮禁用逻辑
|
const buttonState = computed(() =>
|
||||||
const submitDisabled = computed(() => {
|
getSubmitButtonState({
|
||||||
return (
|
isAuthed: userStore.isAuthed,
|
||||||
!userStore.isAuthed ||
|
hasCode: codeStore.code.value.trim() !== "",
|
||||||
codeStore.code.value.trim() === "" ||
|
isFormatting: isFormatting.value,
|
||||||
isProcessing.value ||
|
isSubmitting: isSubmittingRequest.value || submitting.value,
|
||||||
isCooldown.value
|
isJudging: judging.value || pending.value,
|
||||||
|
isCooldown: 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"
|
|
||||||
})
|
|
||||||
|
|
||||||
// ==================== 提交函数 ====================
|
// ==================== 提交函数 ====================
|
||||||
async function submit() {
|
async function submit() {
|
||||||
if (!userStore.isAuthed) return
|
if (buttonState.value.disabled) return
|
||||||
|
|
||||||
// 0. Python3 语法检测
|
// 0. Python3 语法检测
|
||||||
if (codeStore.code.language === "Python3") {
|
if (codeStore.code.language === "Python3") {
|
||||||
@@ -119,6 +106,28 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 0.5 提交前自动格式化(Python3 用 ruff,C/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. 构建提交数据
|
// 1. 构建提交数据
|
||||||
const data: SubmitCodePayload = {
|
const data: SubmitCodePayload = {
|
||||||
problem_id: problem.value!.id,
|
problem_id: problem.value!.id,
|
||||||
@@ -129,6 +138,8 @@ async function submit() {
|
|||||||
data.contest_id = parseInt(contestID)
|
data.contest_id = parseInt(contestID)
|
||||||
}
|
}
|
||||||
// 2. 提交代码到后端
|
// 2. 提交代码到后端
|
||||||
|
isSubmittingRequest.value = true
|
||||||
|
try {
|
||||||
const res = await submitCode(data)
|
const res = await submitCode(data)
|
||||||
console.log(`[Submit] 代码已提交: ID=${res.data.submission_id}`)
|
console.log(`[Submit] 代码已提交: ID=${res.data.submission_id}`)
|
||||||
|
|
||||||
@@ -136,6 +147,9 @@ async function submit() {
|
|||||||
startCooldown()
|
startCooldown()
|
||||||
startMonitoring(res.data.submission_id)
|
startMonitoring(res.data.submission_id)
|
||||||
showResult.value = true
|
showResult.value = true
|
||||||
|
} finally {
|
||||||
|
isSubmittingRequest.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 失败计数 ====================
|
// ==================== 失败计数 ====================
|
||||||
@@ -213,15 +227,15 @@ watch(
|
|||||||
<n-button
|
<n-button
|
||||||
:size="isDesktop ? 'medium' : 'small'"
|
:size="isDesktop ? 'medium' : 'small'"
|
||||||
type="primary"
|
type="primary"
|
||||||
:disabled="submitDisabled"
|
:disabled="buttonState.disabled"
|
||||||
@click="submit"
|
@click="submit"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon>
|
<n-icon>
|
||||||
<Icon :icon="submitIcon" />
|
<Icon :icon="buttonState.icon" />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
{{ submitLabel }}
|
{{ buttonState.label }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
53
src/oj/problem/components/submitButtonState.ts
Normal file
53
src/oj/problem/components/submitButtonState.ts
Normal 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 }
|
||||||
|
}
|
||||||
@@ -218,6 +218,15 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 登录状态变化后刷新提交列表,更新提交编号列的可点击状态
|
||||||
|
watch(
|
||||||
|
() => userStore.isAuthed,
|
||||||
|
() => {
|
||||||
|
listSubmissions()
|
||||||
|
if (route.name === "submissions") getTodayCount()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const columns = computed(() => {
|
const columns = computed(() => {
|
||||||
const res: DataTableColumn<SubmissionListItem>[] = [
|
const res: DataTableColumn<SubmissionListItem>[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
51
tests/submitButtonState.test.ts
Normal file
51
tests/submitButtonState.test.ts
Normal 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",
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user