2
.env
2
.env
@@ -4,4 +4,4 @@ PUBLIC_OJ_URL=http://localhost:8000
|
|||||||
PUBLIC_CODE_URL=http://localhost:3000
|
PUBLIC_CODE_URL=http://localhost:3000
|
||||||
PUBLIC_JUDGE0_URL=https://judge0api.xuyue.cc
|
PUBLIC_JUDGE0_URL=https://judge0api.xuyue.cc
|
||||||
PUBLIC_SIGNALING_URL=ws://10.13.114.114:8085
|
PUBLIC_SIGNALING_URL=ws://10.13.114.114:8085
|
||||||
PUBLIC_WS_URL=ws://10.13.114.114:81/ws
|
PUBLIC_WS_URL=ws://localhost:8001/ws
|
||||||
@@ -130,10 +130,13 @@ async function saveWebsiteConfig() {
|
|||||||
message.success("网站配置保存成功")
|
message.success("网站配置保存成功")
|
||||||
getWebsiteConfig()
|
getWebsiteConfig()
|
||||||
configStore.getConfig()
|
configStore.getConfig()
|
||||||
|
|
||||||
// 通过 WebSocket 广播配置变化,实现实时切换
|
// 通过 WebSocket 广播配置变化,实现实时切换
|
||||||
updateConfig('enable_maxkb', websiteConfig.enable_maxkb)
|
updateConfig("enable_maxkb", websiteConfig.enable_maxkb)
|
||||||
updateConfig('submission_list_show_all', websiteConfig.submission_list_show_all)
|
updateConfig(
|
||||||
|
"submission_list_show_all",
|
||||||
|
websiteConfig.submission_list_show_all,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteTestcase(id?: string) {
|
async function deleteTestcase(id?: string) {
|
||||||
|
|||||||
@@ -1,26 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div align="center" style="display: inline-flex; margin: 0 10px;">
|
<div align="center" style="display: inline-flex; margin: 0 10px">
|
||||||
<img src="/S.png" alt="S Grade" v-if="props.grade === 'S'" />
|
<img src="/S.png" alt="S Grade" v-if="props.grade === 'S'" />
|
||||||
<img src="/A.png" alt="A Grade" v-if="props.grade === 'A'" />
|
<img src="/A.png" alt="A Grade" v-if="props.grade === 'A'" />
|
||||||
<img src="/B.png" alt="B Grade" v-if="props.grade === 'B'" />
|
<img src="/B.png" alt="B Grade" v-if="props.grade === 'B'" />
|
||||||
<img src="/C.png" alt="C Grade" v-if="props.grade === 'C'" />
|
<img src="/C.png" alt="C Grade" v-if="props.grade === 'C'" />
|
||||||
<n-tooltip trigger="hover">
|
<n-tooltip trigger="hover">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-icon size="16" style="cursor: help;">
|
<n-icon size="16" style="cursor: help">
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"/>
|
<path
|
||||||
|
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
<div style="max-width: 300px; line-height: 1.4;">
|
<div style="max-width: 300px; line-height: 1.4">
|
||||||
<div style="font-weight: bold; margin-bottom: 8px;">等级计算说明</div>
|
<div style="font-weight: bold; margin-bottom: 8px">等级计算说明</div>
|
||||||
<div>使用加权平均方法计算综合等级:</div>
|
<div>使用加权平均方法计算综合等级:</div>
|
||||||
<div>• S级 = 4分,A级 = 3分,B级 = 2分,C级 = 1分</div>
|
<div>• S级 = 4分,A级 = 3分,B级 = 2分,C级 = 1分</div>
|
||||||
<div>• 根据平均分数确定最终等级:</div>
|
<div>• 根据平均分数确定最终等级:</div>
|
||||||
<div> - S级:≥3.5分</div>
|
<div>- S级:≥3.5分</div>
|
||||||
<div> - A级:2.5-3.5分</div>
|
<div>- A级:2.5-3.5分</div>
|
||||||
<div> - B级:1.5-2.5分</div>
|
<div>- B级:1.5-2.5分</div>
|
||||||
<div> - C级:<1.5分</div>
|
<div>- C级:<1.5分</div>
|
||||||
</div>
|
</div>
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -108,11 +108,7 @@ watch(isMobile, (value) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-grid
|
<n-grid v-if="problem" x-gap="16" :cols="screenModeStore.isBothMode ? 2 : 1">
|
||||||
v-if="problem"
|
|
||||||
x-gap="16"
|
|
||||||
:cols="screenModeStore.isBothMode ? 2 : 1"
|
|
||||||
>
|
|
||||||
<n-gi :span="isDesktop ? 1 : 2" v-if="shouldShowProblem">
|
<n-gi :span="isDesktop ? 1 : 2" v-if="shouldShowProblem">
|
||||||
<n-scrollbar v-if="isDesktop" style="max-height: calc(100vh - 92px)">
|
<n-scrollbar v-if="isDesktop" style="max-height: calc(100vh - 92px)">
|
||||||
<n-tabs v-model:value="currentTab" type="segment">
|
<n-tabs v-model:value="currentTab" type="segment">
|
||||||
|
|||||||
@@ -1,44 +1,28 @@
|
|||||||
import { useConfigStore } from "shared/store/config"
|
import { useConfigStore } from "shared/store/config"
|
||||||
import { useConfigWebSocket, type ConfigUpdate } from "shared/composables/websocket"
|
import {
|
||||||
|
useConfigWebSocket,
|
||||||
|
type ConfigUpdate,
|
||||||
|
} from "shared/composables/websocket"
|
||||||
|
|
||||||
export function useConfigUpdate() {
|
export function useConfigUpdate() {
|
||||||
const configStore = useConfigStore()
|
const configStore = useConfigStore()
|
||||||
|
|
||||||
// 处理 WebSocket 配置更新
|
// 处理 WebSocket 配置更新
|
||||||
const handleConfigUpdate = (data: ConfigUpdate) => {
|
const handleConfigUpdate = (data: ConfigUpdate) => {
|
||||||
console.log("收到配置更新:", data)
|
|
||||||
|
|
||||||
// 更新全局配置 - 使用响应式方式
|
// 更新全局配置 - 使用响应式方式
|
||||||
if (data.key in configStore.config) {
|
if (data.key in configStore.config) {
|
||||||
// 直接修改 ref 的值来触发响应式更新
|
// 直接修改 ref 的值来触发响应式更新
|
||||||
(configStore.config as any)[data.key] = data.value
|
;(configStore.config as any)[data.key] = data.value
|
||||||
console.log(`配置 ${data.key} 已更新为:`, data.value)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化 WebSocket - handler 会在 onMounted 时自动添加
|
|
||||||
const { connect, status } = useConfigWebSocket(handleConfigUpdate)
|
|
||||||
|
|
||||||
// 连接 WebSocket
|
|
||||||
onMounted(() => {
|
|
||||||
console.log("配置更新 WebSocket 正在连接...")
|
|
||||||
connect()
|
|
||||||
|
|
||||||
// 测试连接 - 发送心跳包
|
|
||||||
setTimeout(() => {
|
|
||||||
if (status.value === "connected") {
|
|
||||||
console.log("配置更新 WebSocket 连接成功,发送测试消息...")
|
|
||||||
// 这里可以发送一个测试消息
|
|
||||||
}
|
|
||||||
}, 2000)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听连接状态
|
|
||||||
watch(status, (newStatus) => {
|
|
||||||
console.log("配置更新 WebSocket 状态:", newStatus)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
// 初始化 WebSocket - handler 会在 onMounted 时自动添加
|
||||||
|
const { connect } = useConfigWebSocket(handleConfigUpdate)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
connect()
|
||||||
|
})
|
||||||
return {
|
return {
|
||||||
connect
|
connect,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { useConfigStore } from "shared/store/config"
|
import { useConfigStore } from "shared/store/config"
|
||||||
import { useConfigWebSocket, type ConfigUpdate } from "shared/composables/websocket"
|
import {
|
||||||
|
useConfigWebSocket,
|
||||||
|
type ConfigUpdate,
|
||||||
|
} from "shared/composables/websocket"
|
||||||
|
|
||||||
export function useMaxKB() {
|
export function useMaxKB() {
|
||||||
const configStore = useConfigStore()
|
const configStore = useConfigStore()
|
||||||
const isLoaded = ref(false)
|
const isLoaded = ref(false)
|
||||||
|
|
||||||
// 处理 WebSocket 配置更新 - 只处理 MaxKB 相关
|
// 处理 WebSocket 配置更新 - 只处理 MaxKB 相关
|
||||||
const handleConfigUpdate = (data: ConfigUpdate) => {
|
const handleConfigUpdate = (data: ConfigUpdate) => {
|
||||||
console.log("MaxKB 收到配置更新:", data)
|
if (data.key === "enable_maxkb") {
|
||||||
if (data.key === 'enable_maxkb') {
|
|
||||||
if (data.value) {
|
if (data.value) {
|
||||||
loadMaxKBScript()
|
loadMaxKBScript()
|
||||||
} else {
|
} else {
|
||||||
@@ -16,7 +18,7 @@ export function useMaxKB() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化 WebSocket
|
// 初始化 WebSocket
|
||||||
const { connect } = useConfigWebSocket(handleConfigUpdate)
|
const { connect } = useConfigWebSocket(handleConfigUpdate)
|
||||||
|
|
||||||
@@ -27,7 +29,9 @@ export function useMaxKB() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingScript = document.querySelector(`script[src="${import.meta.env.PUBLIC_MAXKB_URL}"]`)
|
const existingScript = document.querySelector(
|
||||||
|
`script[src="${import.meta.env.PUBLIC_MAXKB_URL}"]`,
|
||||||
|
)
|
||||||
if (existingScript) {
|
if (existingScript) {
|
||||||
isLoaded.value = true
|
isLoaded.value = true
|
||||||
return
|
return
|
||||||
@@ -38,12 +42,11 @@ export function useMaxKB() {
|
|||||||
script.src = import.meta.env.PUBLIC_MAXKB_URL
|
script.src = import.meta.env.PUBLIC_MAXKB_URL
|
||||||
script.async = true
|
script.async = true
|
||||||
script.defer = true
|
script.defer = true
|
||||||
|
|
||||||
script.onload = () => {
|
script.onload = () => {
|
||||||
isLoaded.value = true
|
isLoaded.value = true
|
||||||
console.log("MaxKB script loaded successfully")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
script.onerror = () => {
|
script.onerror = () => {
|
||||||
console.error("Failed to load MaxKB script")
|
console.error("Failed to load MaxKB script")
|
||||||
}
|
}
|
||||||
@@ -52,15 +55,41 @@ export function useMaxKB() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const removeMaxKBScript = () => {
|
const removeMaxKBScript = () => {
|
||||||
|
// 把 script 也删除
|
||||||
|
const script = document.querySelector(
|
||||||
|
`script[src="${import.meta.env.PUBLIC_MAXKB_URL}"]`,
|
||||||
|
)
|
||||||
|
if (script) {
|
||||||
|
script.remove()
|
||||||
|
}
|
||||||
|
// 等待DOM加载完成后删除所有id以"maxkb-"开头的元素
|
||||||
|
const removeMaxKBElements = () => {
|
||||||
|
// 查找所有id以"maxkb-"开头的元素
|
||||||
|
const elements = document.querySelectorAll('[id^="maxkb-"]')
|
||||||
|
|
||||||
|
elements.forEach((element) => {
|
||||||
|
element.remove()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果DOM已经加载完成,直接执行删除
|
||||||
|
if (document.readyState === "complete") {
|
||||||
|
removeMaxKBElements()
|
||||||
|
} else {
|
||||||
|
// 等待DOM加载完成
|
||||||
|
window.addEventListener("load", removeMaxKBElements, { once: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除MaxKB脚本标签
|
||||||
|
const existingScript = document.querySelector(
|
||||||
|
`script[src="${import.meta.env.PUBLIC_MAXKB_URL}"]`,
|
||||||
|
)
|
||||||
|
if (existingScript) {
|
||||||
|
existingScript.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置加载状态
|
||||||
isLoaded.value = false
|
isLoaded.value = false
|
||||||
const maxkbChatButton = document.getElementById("maxkb-chat-button")
|
|
||||||
const maxkbChatContainer = document.getElementById("maxkb-chat-container")
|
|
||||||
if (maxkbChatButton) {
|
|
||||||
maxkbChatButton.remove()
|
|
||||||
}
|
|
||||||
if (maxkbChatContainer) {
|
|
||||||
maxkbChatContainer.remove()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连接 WebSocket
|
// 连接 WebSocket
|
||||||
@@ -70,20 +99,19 @@ export function useMaxKB() {
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => configStore.config.enable_maxkb,
|
() => configStore.config.enable_maxkb,
|
||||||
(newValue) => {
|
(enabled) => {
|
||||||
if (newValue) {
|
if (enabled) {
|
||||||
loadMaxKBScript()
|
loadMaxKBScript()
|
||||||
} else {
|
} else {
|
||||||
console.log("removeMaxKBScript")
|
|
||||||
removeMaxKBScript()
|
removeMaxKBScript()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loadMaxKBScript,
|
loadMaxKBScript,
|
||||||
removeMaxKBScript,
|
removeMaxKBScript,
|
||||||
isLoaded: readonly(isLoaded)
|
isLoaded: readonly(isLoaded),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ export const SYNC_MESSAGES = {
|
|||||||
SYNC_ON: "断开同步",
|
SYNC_ON: "断开同步",
|
||||||
SYNC_OFF: "开启同步",
|
SYNC_OFF: "开启同步",
|
||||||
SYNCING_WITH: (name: string) => `🔗 与 ${name} 同步中`,
|
SYNCING_WITH: (name: string) => `🔗 与 ${name} 同步中`,
|
||||||
STUDENT_LEFT: (name?: string) => name ? `💡 ${name}已离开` : "💡 可以关闭同步",
|
STUDENT_LEFT: (name?: string) =>
|
||||||
|
name ? `💡 ${name}已离开` : "💡 可以关闭同步",
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// 类型定义
|
// 类型定义
|
||||||
|
|||||||
@@ -313,7 +313,6 @@ class SubmissionWebSocket extends BaseWebSocket<SubmissionUpdate> {
|
|||||||
* 订阅特定提交的更新
|
* 订阅特定提交的更新
|
||||||
*/
|
*/
|
||||||
subscribe(submissionId: string) {
|
subscribe(submissionId: string) {
|
||||||
console.log(`[WebSocket] 发送订阅请求: submission_id=${submissionId}`)
|
|
||||||
const success = this.send({
|
const success = this.send({
|
||||||
type: "subscribe",
|
type: "subscribe",
|
||||||
submission_id: submissionId,
|
submission_id: submissionId,
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
import { defineStore } from "pinia"
|
import { defineStore } from "pinia"
|
||||||
import { ScreenMode } from "utils/constants"
|
import { ScreenMode } from "utils/constants"
|
||||||
|
|
||||||
export const useScreenModeStore = defineStore("screenMode", () => {
|
export const useScreenModeStore = defineStore("screenMode", () => {
|
||||||
const { state: screenMode, next: switchScreenMode } = useCycleList(
|
const { state: screenMode, next: switchScreenMode } = useCycleList(
|
||||||
Object.values(ScreenMode),
|
Object.values(ScreenMode),
|
||||||
{
|
{
|
||||||
initialValue: ScreenMode.both,
|
initialValue: ScreenMode.both,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const isBothMode = computed(() => screenMode.value === ScreenMode.both)
|
const isBothMode = computed(() => screenMode.value === ScreenMode.both)
|
||||||
const isCodeOnlyMode = computed(() => screenMode.value === ScreenMode.code)
|
const isCodeOnlyMode = computed(() => screenMode.value === ScreenMode.code)
|
||||||
|
|
||||||
const shouldShowProblem = computed(
|
const shouldShowProblem = computed(
|
||||||
() =>
|
() =>
|
||||||
screenMode.value === ScreenMode.both ||
|
screenMode.value === ScreenMode.both ||
|
||||||
screenMode.value === ScreenMode.problem,
|
screenMode.value === ScreenMode.problem,
|
||||||
)
|
)
|
||||||
|
|
||||||
function resetScreenMode() {
|
function resetScreenMode() {
|
||||||
screenMode.value = ScreenMode.both
|
screenMode.value = ScreenMode.both
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
screenMode,
|
screenMode,
|
||||||
isBothMode,
|
isBothMode,
|
||||||
isCodeOnlyMode,
|
isCodeOnlyMode,
|
||||||
shouldShowProblem,
|
shouldShowProblem,
|
||||||
switchScreenMode,
|
switchScreenMode,
|
||||||
resetScreenMode,
|
resetScreenMode,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user