@@ -16,6 +16,14 @@ export default defineConfig(({ envMode }) => {
|
|||||||
headers: { Referer: url },
|
headers: { Referer: url },
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebSocket 代理配置(开发环境 Daphne 在 8001 端口)
|
||||||
|
const wsProxyConfig = {
|
||||||
|
target: url.replace(':8000', ':8001').replace('http:', 'ws:'), // http://localhost:8001 → ws://localhost:8001
|
||||||
|
ws: true, // 启用 WebSocket 代理
|
||||||
|
changeOrigin: true,
|
||||||
|
logLevel: 'debug' as const, // 显示代理日志
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
plugins: [pluginVue()],
|
plugins: [pluginVue()],
|
||||||
tools: {
|
tools: {
|
||||||
@@ -141,6 +149,7 @@ export default defineConfig(({ envMode }) => {
|
|||||||
proxy: {
|
proxy: {
|
||||||
"/api": proxyConfig,
|
"/api": proxyConfig,
|
||||||
"/public": proxyConfig,
|
"/public": proxyConfig,
|
||||||
|
"/ws": wsProxyConfig, // WebSocket 使用单独的代理配置
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import { submissionMemoryFormat, submissionTimeFormat } from "utils/functions"
|
|||||||
import { Submission, SubmitCodePayload } from "utils/types"
|
import { Submission, SubmitCodePayload } from "utils/types"
|
||||||
import SubmissionResultTag from "shared/components/SubmissionResultTag.vue"
|
import SubmissionResultTag from "shared/components/SubmissionResultTag.vue"
|
||||||
import { isDesktop } from "shared/composables/breakpoints"
|
import { isDesktop } from "shared/composables/breakpoints"
|
||||||
|
import {
|
||||||
|
useSubmissionWebSocket,
|
||||||
|
type SubmissionUpdate,
|
||||||
|
} from "shared/composables/websocket"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
|
|
||||||
const ProblemComment = defineAsyncComponent(
|
const ProblemComment = defineAsyncComponent(
|
||||||
@@ -39,7 +43,7 @@ const { start: showCommentPanel } = useTimeoutFn(
|
|||||||
{ immediate: false },
|
{ immediate: false },
|
||||||
)
|
)
|
||||||
|
|
||||||
const { start: fetchSubmission } = useTimeoutFn(
|
const { start: fetchSubmission, stop: stopFetchSubmission } = useTimeoutFn(
|
||||||
async () => {
|
async () => {
|
||||||
const res = await getSubmission(submissionId.value)
|
const res = await getSubmission(submissionId.value)
|
||||||
submission.value = res.data
|
submission.value = res.data
|
||||||
@@ -57,45 +61,76 @@ const { start: fetchSubmission } = useTimeoutFn(
|
|||||||
{ immediate: false },
|
{ immediate: false },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// WebSocket 消息处理器
|
||||||
|
const handleSubmissionUpdate = (data: SubmissionUpdate) => {
|
||||||
|
console.log("[Submit] 收到提交更新:", data)
|
||||||
|
|
||||||
|
if (data.submission_id !== submissionId.value) {
|
||||||
|
console.log(`[Submit] 提交ID不匹配: 期望=${submissionId.value}, 实际=${data.submission_id}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!submission.value) {
|
||||||
|
submission.value = {} as Submission
|
||||||
|
}
|
||||||
|
|
||||||
|
submission.value.result = data.result as Submission["result"]
|
||||||
|
|
||||||
|
// 判题完成,获取完整提交详情
|
||||||
|
if (data.status === "finished") {
|
||||||
|
console.log("[Submit] 判题完成,获取详细信息")
|
||||||
|
getSubmission(submissionId.value).then((res) => {
|
||||||
|
submission.value = res.data
|
||||||
|
submitted.value = false
|
||||||
|
|
||||||
|
// 判题完成后,15 分钟无新提交则断开 WebSocket 连接
|
||||||
|
// 考虑到学生换题间隔约 10 分钟,15 分钟可覆盖大部分场景
|
||||||
|
scheduleDisconnect(15 * 60 * 1000)
|
||||||
|
})
|
||||||
|
} else if (data.status === "error") {
|
||||||
|
console.log("[Submit] 判题出错")
|
||||||
|
submitted.value = false
|
||||||
|
// 判题出错后,15 分钟无新提交则断开 WebSocket 连接
|
||||||
|
scheduleDisconnect(15 * 60 * 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 WebSocket(按需连接模式)
|
||||||
|
const {
|
||||||
|
connect,
|
||||||
|
subscribe,
|
||||||
|
scheduleDisconnect,
|
||||||
|
cancelScheduledDisconnect,
|
||||||
|
status: wsStatus,
|
||||||
|
} = useSubmissionWebSocket(handleSubmissionUpdate)
|
||||||
|
|
||||||
|
// 组件卸载时停止轮询
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopFetchSubmission()
|
||||||
|
})
|
||||||
|
|
||||||
const judging = computed(
|
const judging = computed(
|
||||||
() =>
|
() => submission.value?.result === SubmissionStatus.judging,
|
||||||
!!(
|
|
||||||
submission.value && submission.value.result === SubmissionStatus.judging
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const pending = computed(
|
const pending = computed(
|
||||||
() =>
|
() => submission.value?.result === SubmissionStatus.pending,
|
||||||
!!(
|
|
||||||
submission.value && submission.value.result === SubmissionStatus.pending
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const submitting = computed(
|
const submitting = computed(
|
||||||
() =>
|
() => submission.value?.result === SubmissionStatus.submitting,
|
||||||
!!(
|
|
||||||
submission.value &&
|
|
||||||
submission.value.result === SubmissionStatus.submitting
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const submitDisabled = computed(() => {
|
const submitDisabled = computed(() => {
|
||||||
if (!userStore.isAuthed) {
|
return (
|
||||||
return true
|
!userStore.isAuthed ||
|
||||||
}
|
code.value.trim() === "" ||
|
||||||
if (code.value.trim() === "") {
|
judging.value ||
|
||||||
return true
|
pending.value ||
|
||||||
}
|
submitting.value ||
|
||||||
if (judging.value || pending.value || submitting.value) {
|
submitted.value ||
|
||||||
return true
|
isPending.value
|
||||||
}
|
)
|
||||||
if (submitted.value) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (isPending.value) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const submitLabel = computed(() => {
|
const submitLabel = computed(() => {
|
||||||
@@ -115,43 +150,39 @@ const submitLabel = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const msg = computed(() => {
|
const msg = computed(() => {
|
||||||
|
if (!submission.value) return ""
|
||||||
|
|
||||||
let msg = ""
|
let msg = ""
|
||||||
const result = submission.value && submission.value.result
|
const result = submission.value.result
|
||||||
|
|
||||||
if (
|
if (
|
||||||
result === SubmissionStatus.compile_error ||
|
result === SubmissionStatus.compile_error ||
|
||||||
result === SubmissionStatus.runtime_error
|
result === SubmissionStatus.runtime_error
|
||||||
) {
|
) {
|
||||||
msg += "请仔细检查,看看代码的格式是不是写错了!\n\n"
|
msg += "请仔细检查,看看代码的格式是不是写错了!\n\n"
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
submission.value &&
|
if (submission.value.statistic_info?.err_info) {
|
||||||
submission.value.statistic_info &&
|
|
||||||
submission.value.statistic_info.err_info
|
|
||||||
) {
|
|
||||||
msg += submission.value.statistic_info.err_info
|
msg += submission.value.statistic_info.err_info
|
||||||
}
|
}
|
||||||
|
|
||||||
return msg
|
return msg
|
||||||
})
|
})
|
||||||
|
|
||||||
const infoTable = computed(() => {
|
const infoTable = computed(() => {
|
||||||
|
if (!submission.value?.info?.data?.length) return []
|
||||||
|
|
||||||
|
const result = submission.value.result
|
||||||
if (
|
if (
|
||||||
submission.value &&
|
result === SubmissionStatus.accepted ||
|
||||||
submission.value.result !== SubmissionStatus.accepted &&
|
result === SubmissionStatus.compile_error ||
|
||||||
submission.value.result !== SubmissionStatus.compile_error &&
|
result === SubmissionStatus.runtime_error
|
||||||
submission.value.result !== SubmissionStatus.runtime_error &&
|
|
||||||
submission.value.info &&
|
|
||||||
submission.value.info.data &&
|
|
||||||
submission.value.info.data.length
|
|
||||||
) {
|
) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const data = submission.value.info.data
|
const data = submission.value.info.data
|
||||||
if (data.some((item) => item.result === 0)) {
|
return data.some((item) => item.result === 0) ? data : []
|
||||||
return submission.value.info.data
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const columns: DataTableColumn<Submission["info"]["data"][number]>[] = [
|
const columns: DataTableColumn<Submission["info"]["data"][number]>[] = [
|
||||||
@@ -175,9 +206,8 @@ const columns: DataTableColumn<Submission["info"]["data"][number]>[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
if (!userStore.isAuthed) {
|
if (!userStore.isAuthed) return
|
||||||
return
|
|
||||||
}
|
|
||||||
const data: SubmitCodePayload = {
|
const data: SubmitCodePayload = {
|
||||||
problem_id: problem.value!.id,
|
problem_id: problem.value!.id,
|
||||||
language: code.language,
|
language: code.language,
|
||||||
@@ -186,20 +216,61 @@ async function submit() {
|
|||||||
if (contestID) {
|
if (contestID) {
|
||||||
data.contest_id = parseInt(contestID)
|
data.contest_id = parseInt(contestID)
|
||||||
}
|
}
|
||||||
|
|
||||||
submission.value = { result: 9 } as Submission
|
submission.value = { result: 9 } as Submission
|
||||||
const res = await submitCode(data)
|
const res = await submitCode(data)
|
||||||
submissionId.value = res.data.submission_id
|
submissionId.value = res.data.submission_id
|
||||||
// 防止重复提交
|
console.log(`[Submit] 代码已提交: ID=${submissionId.value}`)
|
||||||
submitPending()
|
submitPending()
|
||||||
submitted.value = true
|
submitted.value = true
|
||||||
// 查询结果
|
|
||||||
|
// 取消之前安排的断开倒计时(如果有新提交)
|
||||||
|
cancelScheduledDisconnect()
|
||||||
|
|
||||||
|
// 按需连接 WebSocket
|
||||||
|
if (wsStatus.value !== "connected") {
|
||||||
|
console.log(`[Submit] WebSocket 未连接,正在连接... 当前状态: ${wsStatus.value}`)
|
||||||
|
connect()
|
||||||
|
} else {
|
||||||
|
console.log("[Submit] WebSocket 已连接,直接订阅")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先使用 WebSocket 实时更新
|
||||||
|
if (wsStatus.value === "connected" || wsStatus.value === "connecting") {
|
||||||
|
// 等待连接完成后订阅
|
||||||
|
const checkConnection = setInterval(() => {
|
||||||
|
if (wsStatus.value === "connected") {
|
||||||
|
clearInterval(checkConnection)
|
||||||
|
console.log(`[Submit] 订阅提交更新: ID=${submissionId.value}`)
|
||||||
|
subscribe(submissionId.value)
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
// 5 秒后如果还在判题中,降级到轮询作为保险
|
||||||
|
// 考虑到判题一般 1-3 秒完成,5 秒足以覆盖绝大部分情况
|
||||||
|
setTimeout(() => {
|
||||||
|
clearInterval(checkConnection)
|
||||||
|
if (
|
||||||
|
submission.value &&
|
||||||
|
(submission.value.result === SubmissionStatus.judging ||
|
||||||
|
submission.value.result === SubmissionStatus.pending ||
|
||||||
|
submission.value.result === 9)
|
||||||
|
) {
|
||||||
|
console.log("WebSocket 未及时响应,降级到轮询模式")
|
||||||
fetchSubmission()
|
fetchSubmission()
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
} else {
|
||||||
|
// WebSocket 连接失败,直接使用轮询
|
||||||
|
fetchSubmission()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => submission?.value?.result,
|
() => submission.value?.result,
|
||||||
(result) => {
|
(result) => {
|
||||||
if (result === SubmissionStatus.accepted) {
|
if (result !== SubmissionStatus.accepted) return
|
||||||
|
|
||||||
// 刷新题目状态
|
// 刷新题目状态
|
||||||
problem.value!.my_status = 0
|
problem.value!.my_status = 0
|
||||||
// 放烟花
|
// 放烟花
|
||||||
@@ -211,10 +282,7 @@ watch(
|
|||||||
origin: { x: 0.5, y: 0.4 },
|
origin: { x: 0.5, y: 0.4 },
|
||||||
})
|
})
|
||||||
// 题目在第一次完成之后,弹出点评框
|
// 题目在第一次完成之后,弹出点评框
|
||||||
if (!contestID) {
|
if (!contestID) showCommentPanel()
|
||||||
showCommentPanel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
425
src/shared/composables/websocket.ts
Normal file
425
src/shared/composables/websocket.ts
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
import { ref, onUnmounted, type Ref } from "vue"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket 连接状态
|
||||||
|
*/
|
||||||
|
export type ConnectionStatus =
|
||||||
|
| "disconnected"
|
||||||
|
| "connecting"
|
||||||
|
| "connected"
|
||||||
|
| "error"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket 消息类型
|
||||||
|
*/
|
||||||
|
export interface WebSocketMessage {
|
||||||
|
type: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket 配置
|
||||||
|
*/
|
||||||
|
export interface WebSocketConfig {
|
||||||
|
/** WebSocket 路径(如 '/ws/submission/') */
|
||||||
|
path: string
|
||||||
|
/** 最大重连次数,默认 5 */
|
||||||
|
maxReconnectAttempts?: number
|
||||||
|
/** 重连延迟(毫秒),默认 1000 */
|
||||||
|
reconnectDelay?: number
|
||||||
|
/** 心跳间隔(毫秒),默认 30000(30秒) */
|
||||||
|
heartbeatTime?: number
|
||||||
|
/** 是否启用心跳,默认 true */
|
||||||
|
enableHeartbeat?: boolean
|
||||||
|
/** 是否启用自动重连,默认 true */
|
||||||
|
enableAutoReconnect?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket 消息处理器
|
||||||
|
*/
|
||||||
|
export type MessageHandler<T extends WebSocketMessage = WebSocketMessage> = (
|
||||||
|
data: T,
|
||||||
|
) => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket 基础连接管理类
|
||||||
|
* 提供连接、重连、心跳等通用功能
|
||||||
|
*/
|
||||||
|
export class BaseWebSocket<T extends WebSocketMessage = WebSocketMessage> {
|
||||||
|
protected ws: WebSocket | null = null
|
||||||
|
protected url: string
|
||||||
|
protected handlers: Set<MessageHandler<T>> = new Set()
|
||||||
|
protected reconnectAttempts = 0
|
||||||
|
protected maxReconnectAttempts: number
|
||||||
|
protected reconnectDelay: number
|
||||||
|
protected heartbeatInterval: number | null = null
|
||||||
|
protected heartbeatTime: number
|
||||||
|
protected enableHeartbeat: boolean
|
||||||
|
protected enableAutoReconnect: boolean
|
||||||
|
protected disconnectTimer: number | null = null
|
||||||
|
|
||||||
|
public status: Ref<ConnectionStatus> = ref<ConnectionStatus>("disconnected")
|
||||||
|
|
||||||
|
constructor(config: WebSocketConfig) {
|
||||||
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
|
||||||
|
const host = window.location.host
|
||||||
|
this.url = `${protocol}//${host}/ws/${config.path}/`
|
||||||
|
|
||||||
|
this.maxReconnectAttempts = config.maxReconnectAttempts ?? 5
|
||||||
|
this.reconnectDelay = config.reconnectDelay ?? 1000
|
||||||
|
this.heartbeatTime = config.heartbeatTime ?? 30000
|
||||||
|
this.enableHeartbeat = config.enableHeartbeat ?? true
|
||||||
|
this.enableAutoReconnect = config.enableAutoReconnect ?? true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接 WebSocket
|
||||||
|
*/
|
||||||
|
connect() {
|
||||||
|
if (
|
||||||
|
this.ws &&
|
||||||
|
(this.ws.readyState === WebSocket.OPEN ||
|
||||||
|
this.ws.readyState === WebSocket.CONNECTING)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.status.value = "connecting"
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.ws = new WebSocket(this.url)
|
||||||
|
|
||||||
|
this.ws.onopen = () => {
|
||||||
|
this.status.value = "connected"
|
||||||
|
this.reconnectAttempts = 0
|
||||||
|
console.log(`[WebSocket] 连接成功: ${this.url}`)
|
||||||
|
if (this.enableHeartbeat) {
|
||||||
|
this.startHeartbeat()
|
||||||
|
}
|
||||||
|
this.onConnected()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data) as T
|
||||||
|
console.log(`[WebSocket] 收到消息:`, data)
|
||||||
|
|
||||||
|
// 处理心跳响应
|
||||||
|
if (data.type === "pong") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用消息处理钩子
|
||||||
|
this.onMessage(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[WebSocket] 解析消息失败:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onerror = (error) => {
|
||||||
|
console.error("[WebSocket] 连接错误:", error)
|
||||||
|
this.status.value = "error"
|
||||||
|
this.onError(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onclose = (event) => {
|
||||||
|
console.log(
|
||||||
|
`[WebSocket] 连接关闭: code=${event.code}, reason=${event.reason}`,
|
||||||
|
)
|
||||||
|
this.status.value = "disconnected"
|
||||||
|
this.stopHeartbeat()
|
||||||
|
this.onDisconnected(event)
|
||||||
|
|
||||||
|
// 自动重连
|
||||||
|
if (
|
||||||
|
this.enableAutoReconnect &&
|
||||||
|
this.reconnectAttempts < this.maxReconnectAttempts
|
||||||
|
) {
|
||||||
|
this.reconnectAttempts++
|
||||||
|
const delay = this.reconnectDelay * this.reconnectAttempts
|
||||||
|
console.log(
|
||||||
|
`[WebSocket] 将在 ${delay}ms 后重连 (尝试 ${this.reconnectAttempts}/${this.maxReconnectAttempts})`,
|
||||||
|
)
|
||||||
|
setTimeout(() => this.connect(), delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create WebSocket connection:", error)
|
||||||
|
this.status.value = "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断开连接
|
||||||
|
*/
|
||||||
|
disconnect() {
|
||||||
|
this.cancelScheduledDisconnect()
|
||||||
|
this.stopHeartbeat()
|
||||||
|
this.enableAutoReconnect = false // 停止自动重连
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close()
|
||||||
|
this.ws = null
|
||||||
|
}
|
||||||
|
this.status.value = "disconnected"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安排延迟断开连接
|
||||||
|
* @param delay 延迟时间(毫秒),默认 900000(15分钟)
|
||||||
|
*/
|
||||||
|
scheduleDisconnect(delay: number = 15 * 60 * 1000) {
|
||||||
|
// 取消之前的定时器
|
||||||
|
this.cancelScheduledDisconnect()
|
||||||
|
|
||||||
|
// 设置新的定时器
|
||||||
|
this.disconnectTimer = window.setTimeout(() => {
|
||||||
|
const minutes = Math.floor(delay / 60000)
|
||||||
|
console.log(`WebSocket idle for ${minutes} minutes, disconnecting...`)
|
||||||
|
this.disconnect()
|
||||||
|
// 断开后需要重新允许自动重连
|
||||||
|
this.enableAutoReconnect = true
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消已安排的断开连接
|
||||||
|
*/
|
||||||
|
cancelScheduledDisconnect() {
|
||||||
|
if (this.disconnectTimer !== null) {
|
||||||
|
clearTimeout(this.disconnectTimer)
|
||||||
|
this.disconnectTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息
|
||||||
|
*/
|
||||||
|
send(data: any) {
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.send(JSON.stringify(data))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加消息处理器
|
||||||
|
*/
|
||||||
|
addHandler(handler: MessageHandler<T>) {
|
||||||
|
this.handlers.add(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除消息处理器
|
||||||
|
*/
|
||||||
|
removeHandler(handler: MessageHandler<T>) {
|
||||||
|
this.handlers.delete(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有处理器
|
||||||
|
*/
|
||||||
|
clearHandlers() {
|
||||||
|
this.handlers.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送心跳包
|
||||||
|
*/
|
||||||
|
protected sendHeartbeat() {
|
||||||
|
this.send({ type: "ping", timestamp: Date.now() })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始心跳
|
||||||
|
*/
|
||||||
|
protected startHeartbeat() {
|
||||||
|
this.stopHeartbeat()
|
||||||
|
this.heartbeatInterval = window.setInterval(() => {
|
||||||
|
this.sendHeartbeat()
|
||||||
|
}, this.heartbeatTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止心跳
|
||||||
|
*/
|
||||||
|
protected stopHeartbeat() {
|
||||||
|
if (this.heartbeatInterval) {
|
||||||
|
clearInterval(this.heartbeatInterval)
|
||||||
|
this.heartbeatInterval = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接成功钩子(子类可重写)
|
||||||
|
*/
|
||||||
|
protected onConnected() {
|
||||||
|
// 子类实现
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断开连接钩子(子类可重写)
|
||||||
|
*/
|
||||||
|
protected onDisconnected(event: CloseEvent) {
|
||||||
|
// 子类实现
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误钩子(子类可重写)
|
||||||
|
*/
|
||||||
|
protected onError(error: Event) {
|
||||||
|
// 子类实现
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息处理钩子(子类可重写)
|
||||||
|
*/
|
||||||
|
protected onMessage(data: T) {
|
||||||
|
// 通知所有处理器
|
||||||
|
this.handlers.forEach((handler) => {
|
||||||
|
try {
|
||||||
|
handler(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in message handler:", error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交状态更新的数据类型
|
||||||
|
*/
|
||||||
|
export interface SubmissionUpdate extends WebSocketMessage {
|
||||||
|
type: "submission_update"
|
||||||
|
submission_id: string
|
||||||
|
result: number
|
||||||
|
status: "pending" | "judging" | "finished" | "error"
|
||||||
|
time_cost?: number
|
||||||
|
memory_cost?: number
|
||||||
|
score?: number
|
||||||
|
err_info?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交 WebSocket 连接管理类
|
||||||
|
*/
|
||||||
|
class SubmissionWebSocket extends BaseWebSocket<SubmissionUpdate> {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
path: "submission",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅特定提交的更新
|
||||||
|
*/
|
||||||
|
subscribe(submissionId: string) {
|
||||||
|
console.log(`[WebSocket] 发送订阅请求: submission_id=${submissionId}`)
|
||||||
|
const success = this.send({
|
||||||
|
type: "subscribe",
|
||||||
|
submission_id: submissionId,
|
||||||
|
})
|
||||||
|
if (!success) {
|
||||||
|
console.error("[WebSocket] 订阅失败: 连接未就绪")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局单例
|
||||||
|
let wsInstance: SubmissionWebSocket | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 WebSocket 实例
|
||||||
|
*/
|
||||||
|
export function getWebSocketInstance(): SubmissionWebSocket {
|
||||||
|
if (!wsInstance) {
|
||||||
|
wsInstance = new SubmissionWebSocket()
|
||||||
|
}
|
||||||
|
return wsInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于组件中使用 WebSocket 的 Composable
|
||||||
|
*/
|
||||||
|
export function useSubmissionWebSocket(
|
||||||
|
handler?: MessageHandler<SubmissionUpdate>,
|
||||||
|
) {
|
||||||
|
const ws = getWebSocketInstance()
|
||||||
|
|
||||||
|
// 如果提供了处理器,添加到实例中
|
||||||
|
if (handler) {
|
||||||
|
ws.addHandler(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件卸载时移除处理器
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (handler) {
|
||||||
|
ws.removeHandler(handler)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
connect: () => ws.connect(),
|
||||||
|
disconnect: () => ws.disconnect(),
|
||||||
|
subscribe: (submissionId: string) => ws.subscribe(submissionId),
|
||||||
|
scheduleDisconnect: (delay?: number) => ws.scheduleDisconnect(delay),
|
||||||
|
cancelScheduledDisconnect: () => ws.cancelScheduledDisconnect(),
|
||||||
|
status: ws.status,
|
||||||
|
addHandler: (h: MessageHandler<SubmissionUpdate>) => ws.addHandler(h),
|
||||||
|
removeHandler: (h: MessageHandler<SubmissionUpdate>) => ws.removeHandler(h),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用 WebSocket Composable 工厂函数
|
||||||
|
* 用于创建自定义的 WebSocket composable
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // 创建通知 WebSocket
|
||||||
|
* interface NotificationMessage extends WebSocketMessage {
|
||||||
|
* type: 'notification'
|
||||||
|
* title: string
|
||||||
|
* content: string
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* class NotificationWebSocket extends BaseWebSocket<NotificationMessage> {
|
||||||
|
* constructor() {
|
||||||
|
* super({ path: 'notification' })
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* let notificationWs: NotificationWebSocket | null = null
|
||||||
|
*
|
||||||
|
* export function useNotificationWebSocket(handler?: MessageHandler<NotificationMessage>) {
|
||||||
|
* if (!notificationWs) {
|
||||||
|
* notificationWs = new NotificationWebSocket()
|
||||||
|
* }
|
||||||
|
* return createWebSocketComposable(notificationWs, handler)
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function createWebSocketComposable<T extends WebSocketMessage>(
|
||||||
|
ws: BaseWebSocket<T>,
|
||||||
|
handler?: MessageHandler<T>,
|
||||||
|
) {
|
||||||
|
if (handler) {
|
||||||
|
ws.addHandler(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (handler) {
|
||||||
|
ws.removeHandler(handler)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
connect: () => ws.connect(),
|
||||||
|
disconnect: () => ws.disconnect(),
|
||||||
|
send: (data: any) => ws.send(data),
|
||||||
|
status: ws.status,
|
||||||
|
addHandler: (h: MessageHandler<T>) => ws.addHandler(h),
|
||||||
|
removeHandler: (h: MessageHandler<T>) => ws.removeHandler(h),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user