Build Phase 2 judge vertical slice
This commit is contained in:
@@ -3,8 +3,6 @@ import { darkTheme, dateZhCN, zhCN } from "naive-ui"
|
||||
import "normalize.css"
|
||||
import "./index.css"
|
||||
import { useConfigStore } from "shared/store/config"
|
||||
import { useConfigUpdate } from "shared/composables/configUpdate"
|
||||
import { useMaxKB } from "shared/composables/maxkb"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
|
||||
const isDark = useDark()
|
||||
@@ -17,9 +15,7 @@ onMounted(() => {
|
||||
userStore.getMyProfile()
|
||||
})
|
||||
|
||||
// 使用配置更新和 MaxKB 功能
|
||||
useConfigUpdate()
|
||||
useMaxKB()
|
||||
// 配置推送和 MaxKB 仍属于 Phase 3;在它们迁入前不连接旧 WebSocket。
|
||||
|
||||
// 延迟加载 highlight.js,避免阻塞首屏
|
||||
const hljsInstance = ref<any>(null)
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
createSubmissionResponseSchema,
|
||||
problemDetailSchema,
|
||||
submissionDetailSchema,
|
||||
} from "@oj2/contract"
|
||||
import api2 from "utils/api2"
|
||||
import http from "utils/http"
|
||||
import { filterResult } from "oj/transforms"
|
||||
import type {
|
||||
@@ -8,10 +14,23 @@ import type {
|
||||
Submission,
|
||||
SubmissionListPayload,
|
||||
SubmitCodePayload,
|
||||
WebsiteConfig,
|
||||
} from "utils/types"
|
||||
|
||||
export function getWebsiteConfig() {
|
||||
return http.get("website")
|
||||
return Promise.resolve({
|
||||
error: null,
|
||||
data: {
|
||||
website_base_url: "",
|
||||
website_name: "判题狗",
|
||||
website_name_shortcut: "判题狗",
|
||||
website_footer: "",
|
||||
submission_list_show_all: true,
|
||||
allow_register: false,
|
||||
class_list: [],
|
||||
enable_maxkb: false,
|
||||
} as WebsiteConfig,
|
||||
})
|
||||
}
|
||||
|
||||
export async function getProblemList(
|
||||
@@ -41,6 +60,7 @@ export function getRandomProblemID() {
|
||||
}
|
||||
|
||||
export function getProblem(problemID: string, contestID: string) {
|
||||
if (!contestID) return getPhase2Problem(problemID)
|
||||
const endpoint = !!contestID ? "contest/problem" : "problem"
|
||||
return http.get(endpoint, {
|
||||
params: {
|
||||
@@ -50,22 +70,105 @@ export function getProblem(problemID: string, contestID: string) {
|
||||
})
|
||||
}
|
||||
|
||||
async function getPhase2Problem(problemID: string) {
|
||||
const response = await api2.get<unknown>(
|
||||
`problems/${encodeURIComponent(problemID)}`,
|
||||
)
|
||||
const problem = problemDetailSchema.parse(response.data)
|
||||
return {
|
||||
error: null,
|
||||
data: {
|
||||
id: problem.id,
|
||||
_id: problem._id,
|
||||
title: problem.title,
|
||||
description: problem.description,
|
||||
input_description: problem.inputDescription,
|
||||
output_description: problem.outputDescription,
|
||||
samples: problem.samples,
|
||||
hint: problem.hint ?? "",
|
||||
languages: problem.languages,
|
||||
template: problem.template,
|
||||
create_time: problem.createTime,
|
||||
last_update_time: problem.lastUpdateTime,
|
||||
time_limit: problem.timeLimit,
|
||||
memory_limit: problem.memoryLimit,
|
||||
difficulty: problem.difficulty,
|
||||
source: problem.source ?? "",
|
||||
prompt: problem.prompt ?? "",
|
||||
answers: [],
|
||||
submission_number: problem.submissionNumber,
|
||||
accepted_number: problem.acceptedNumber,
|
||||
statistic_info: problem.statisticInfo,
|
||||
share_submission: problem.shareSubmission,
|
||||
contest: problem.contestId,
|
||||
tags: problem.tags,
|
||||
created_by: {
|
||||
id: problem.createdBy.id,
|
||||
username: problem.createdBy.username,
|
||||
real_name: problem.createdBy.realName,
|
||||
},
|
||||
my_status: problem.myStatus,
|
||||
my_failed_count: problem.myFailedCount,
|
||||
visible: true,
|
||||
allow_flowchart: problem.allowFlowchart,
|
||||
show_flowchart: problem.showFlowchart,
|
||||
mermaid_code: problem.mermaidCode,
|
||||
flowchart_data: problem.flowchartData,
|
||||
flowchart_hint: problem.flowchartHint,
|
||||
sql_config: problem.sqlConfig,
|
||||
sql_display: problem.sqlDisplay,
|
||||
} as Problem,
|
||||
}
|
||||
}
|
||||
|
||||
export function getProblemBeatRate(problemID: number) {
|
||||
return http.get("problem/beat_count", { params: { problem_id: problemID } })
|
||||
}
|
||||
|
||||
export function getSubmission(id: string) {
|
||||
return http.get<Submission>("submission", {
|
||||
params: { id },
|
||||
})
|
||||
export async function getSubmission(id: string) {
|
||||
const response = await api2.get<unknown>(
|
||||
`submissions/${encodeURIComponent(id)}`,
|
||||
)
|
||||
const submission = submissionDetailSchema.parse(response.data)
|
||||
return {
|
||||
error: null,
|
||||
data: {
|
||||
id: submission.id,
|
||||
create_time: submission.createTime,
|
||||
user_id: submission.userId,
|
||||
username: submission.username,
|
||||
code: submission.code,
|
||||
result: submission.result,
|
||||
info: submission.info,
|
||||
language: submission.language,
|
||||
shared: submission.shared,
|
||||
show_link: submission.showLink,
|
||||
statistic_info: submission.statisticInfo,
|
||||
ip: submission.ip,
|
||||
contest: submission.contestId,
|
||||
problem: submission.problemId,
|
||||
can_unshare: submission.canUnshare,
|
||||
} as Submission,
|
||||
}
|
||||
}
|
||||
|
||||
export function submitCode(data: SubmitCodePayload) {
|
||||
return http.post("submission", data)
|
||||
export async function submitCode(data: SubmitCodePayload) {
|
||||
const response = await api2.post<unknown>("submissions", {
|
||||
problemId: data.problem_id,
|
||||
language: data.language,
|
||||
code: data.code,
|
||||
contestId: data.contest_id,
|
||||
})
|
||||
const created = createSubmissionResponseSchema.parse(response.data)
|
||||
return {
|
||||
error: null,
|
||||
data: { submission_id: created.submissionId },
|
||||
}
|
||||
}
|
||||
|
||||
export function formatCode(data: { code: string; language: string }) {
|
||||
return http.post<{ code: string }>("format_code", data)
|
||||
// 格式化端点在 Phase 3 迁移;Phase 2 保留原代码继续提交。
|
||||
return Promise.resolve({ error: null, data: { code: data.code } })
|
||||
}
|
||||
|
||||
export function getSubmissions(params: Partial<SubmissionListPayload>) {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Icon } from "@iconify/vue"
|
||||
import { storeToRefs } from "pinia"
|
||||
import {
|
||||
formatCode,
|
||||
getReaction,
|
||||
submitCode,
|
||||
updateProblemSetProgress,
|
||||
} from "oj/api"
|
||||
@@ -73,18 +72,6 @@ const { start: startCooldown, isPending: isCooldown } = useTimeout(5000, {
|
||||
immediate: false,
|
||||
})
|
||||
|
||||
// ==================== AC后显示评论框 ====================
|
||||
const { start: showCommentPanelDelayed } = useTimeoutFn(
|
||||
async () => {
|
||||
const res = await getReaction(problem.value!.id)
|
||||
if (res.data.mine === null) {
|
||||
commentPanel.value = true
|
||||
}
|
||||
},
|
||||
1500,
|
||||
{ immediate: false },
|
||||
)
|
||||
|
||||
const { start: goToProblemSetDelayed } = useTimeoutFn(
|
||||
() => {
|
||||
router.push({
|
||||
@@ -216,11 +203,6 @@ watch(
|
||||
// 3. 放烟花
|
||||
celebrate()
|
||||
|
||||
// 4. 显示评价框
|
||||
if (!contestID && !problemSetId) {
|
||||
showCommentPanelDelayed()
|
||||
}
|
||||
|
||||
if (problemSetId) {
|
||||
// 延迟回到题单页面
|
||||
goToProblemSetDelayed()
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { userProfileSchema } from "@oj2/contract"
|
||||
import api2 from "utils/api2"
|
||||
import http from "utils/http"
|
||||
import type { ApiResponse } from "utils/http"
|
||||
import type { Profile, Tag } from "utils/types"
|
||||
|
||||
export function login(data: { username: string; password: string }) {
|
||||
return http.post("login", data)
|
||||
return api2.post("auth/login", data)
|
||||
}
|
||||
|
||||
export function signup(data: {
|
||||
@@ -14,11 +17,47 @@ export function signup(data: {
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return http.get("logout")
|
||||
return api2.delete("auth/session")
|
||||
}
|
||||
|
||||
export function getProfile(username: string = "") {
|
||||
return http.get<Profile>("profile", { params: { username } })
|
||||
export async function getProfile(
|
||||
username: string = "",
|
||||
): Promise<ApiResponse<Profile | null>> {
|
||||
if (username) return http.get<Profile>("profile", { params: { username } })
|
||||
|
||||
const response = await api2.get<unknown>("me")
|
||||
if (response.data === null) return { error: null, data: null }
|
||||
const profile = userProfileSchema.parse(response.data)
|
||||
return {
|
||||
error: null,
|
||||
data: {
|
||||
id: profile.id,
|
||||
user: {
|
||||
id: profile.user.id,
|
||||
username: profile.user.username,
|
||||
real_name: profile.realName ?? "",
|
||||
email: profile.user.email ?? "",
|
||||
admin_type: profile.user.adminType as Profile["user"]["admin_type"],
|
||||
problem_permission: profile.user.problemPermission,
|
||||
create_time: profile.user.createTime as unknown as Date,
|
||||
last_login: profile.user.lastLogin as unknown as Date,
|
||||
open_api: profile.user.openApi,
|
||||
is_disabled: profile.user.isDisabled,
|
||||
class_name: profile.user.className,
|
||||
},
|
||||
real_name: profile.realName ?? "",
|
||||
acm_problems_status: profile.acmProblemsStatus as Profile["acm_problems_status"],
|
||||
avatar: profile.avatar,
|
||||
blog: profile.blog as null,
|
||||
mood: profile.mood ?? "",
|
||||
github: profile.github ?? "",
|
||||
school: profile.school ?? "",
|
||||
major: profile.major ?? "",
|
||||
language: profile.language ?? "",
|
||||
accepted_number: profile.acceptedNumber,
|
||||
submission_number: profile.submissionNumber,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function getProblemTagList() {
|
||||
|
||||
@@ -4,12 +4,10 @@ import { storeToRefs } from "pinia"
|
||||
import { useAuthModalStore } from "../store/authModal"
|
||||
import { useConfigStore } from "../store/config"
|
||||
import { useUserStore } from "../store/user"
|
||||
import { useLoginSummaryStore } from "../store/loginSummary"
|
||||
|
||||
const userStore = useUserStore()
|
||||
const configStore = useConfigStore()
|
||||
const authStore = useAuthModalStore()
|
||||
const loginSummaryStore = useLoginSummaryStore()
|
||||
|
||||
const {
|
||||
loginModalOpen,
|
||||
@@ -68,7 +66,6 @@ async function submit() {
|
||||
if (!msg.value) {
|
||||
authStore.closeLoginModal()
|
||||
await userStore.getMyProfile()
|
||||
loginSummaryStore.open()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,6 +20,8 @@ export interface WebSocketMessage {
|
||||
export interface WebSocketConfig {
|
||||
/** WebSocket 路径(如 '/ws/submission/') */
|
||||
path: string
|
||||
/** 完整 URL;提供后覆盖 PUBLIC_WS_URL + path */
|
||||
url?: string
|
||||
/** 最大重连次数,默认 5 */
|
||||
maxReconnectAttempts?: number
|
||||
/** 重连延迟(毫秒),默认 1000 */
|
||||
@@ -59,7 +61,8 @@ export class BaseWebSocket<T extends WebSocketMessage = WebSocketMessage> {
|
||||
public status: Ref<ConnectionStatus> = ref<ConnectionStatus>("disconnected")
|
||||
|
||||
constructor(config: WebSocketConfig) {
|
||||
this.url = `${import.meta.env.PUBLIC_WS_URL}/${config.path}/`
|
||||
this.url =
|
||||
config.url ?? `${import.meta.env.PUBLIC_WS_URL}/${config.path}/`
|
||||
|
||||
this.maxReconnectAttempts = config.maxReconnectAttempts ?? 5
|
||||
this.reconnectDelay = config.reconnectDelay ?? 1000
|
||||
@@ -300,9 +303,13 @@ export interface SubmissionUpdate extends WebSocketMessage {
|
||||
* 提交 WebSocket 连接管理类
|
||||
*/
|
||||
class SubmissionWebSocket extends BaseWebSocket<SubmissionUpdate> {
|
||||
private pendingSubmissionId = ""
|
||||
|
||||
constructor() {
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
|
||||
super({
|
||||
path: "submission",
|
||||
url: `${protocol}//${window.location.host}/ws2/submissions`,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -310,12 +317,24 @@ class SubmissionWebSocket extends BaseWebSocket<SubmissionUpdate> {
|
||||
* 订阅特定提交的更新
|
||||
*/
|
||||
subscribe(submissionId: string) {
|
||||
this.pendingSubmissionId = submissionId
|
||||
const success = this.send({
|
||||
type: "subscribe",
|
||||
submission_id: submissionId,
|
||||
})
|
||||
if (!success) {
|
||||
console.error("[WebSocket] 订阅失败: 连接未就绪")
|
||||
if (success) this.pendingSubmissionId = ""
|
||||
}
|
||||
|
||||
protected onConnected() {
|
||||
if (!this.pendingSubmissionId) return
|
||||
const submissionId = this.pendingSubmissionId
|
||||
if (
|
||||
this.send({
|
||||
type: "subscribe",
|
||||
submission_id: submissionId,
|
||||
})
|
||||
) {
|
||||
this.pendingSubmissionId = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
42
apps/web/src/utils/api2.ts
Normal file
42
apps/web/src/utils/api2.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import axios, { type AxiosRequestConfig } from "axios"
|
||||
import type { ApiResponse } from "./http"
|
||||
|
||||
interface Api2Error {
|
||||
error?: {
|
||||
code?: string
|
||||
message?: string
|
||||
}
|
||||
}
|
||||
|
||||
interface Api2Client {
|
||||
get<T>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>>
|
||||
post<T>(
|
||||
url: string,
|
||||
data?: unknown,
|
||||
config?: AxiosRequestConfig,
|
||||
): Promise<ApiResponse<T>>
|
||||
delete<T>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>>
|
||||
}
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL: "/api2",
|
||||
withCredentials: true,
|
||||
})
|
||||
|
||||
instance.interceptors.response.use(
|
||||
(response) => Promise.resolve({ error: null, data: response.data.data }),
|
||||
(error) => {
|
||||
const payload = error.response?.data as Api2Error | undefined
|
||||
const code = payload?.error?.code ?? "network-error"
|
||||
const message = payload?.error?.message ?? "Request failed"
|
||||
const legacyMessage =
|
||||
code === "invalid-credentials"
|
||||
? "Invalid username or password"
|
||||
: code === "account-disabled"
|
||||
? "Your account has been disabled"
|
||||
: message
|
||||
return Promise.reject({ error: code, data: legacyMessage })
|
||||
},
|
||||
)
|
||||
|
||||
export default instance as unknown as Api2Client
|
||||
@@ -171,6 +171,12 @@ export default defineConfig(({ mode }) => {
|
||||
changeOrigin: true,
|
||||
rewrite: (path: string) => path.replace(/^\/api2/, "/api"),
|
||||
},
|
||||
"/ws2": {
|
||||
target: "ws://localhost:3000",
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
rewrite: (path: string) => path.replace(/^\/ws2/, "/ws"),
|
||||
},
|
||||
"/api": proxyConfig,
|
||||
"/public": proxyConfig,
|
||||
"/ws": wsProxyConfig,
|
||||
|
||||
Reference in New Issue
Block a user