This commit is contained in:
2025-10-05 10:04:36 +08:00
parent ece84e15c7
commit 94d2066a99
9 changed files with 341 additions and 331 deletions

View File

@@ -75,7 +75,12 @@ const columns: DataTableColumn<User>[] = [
? parseTime(row.last_login, "YYYY-MM-DD HH:mm:ss") ? parseTime(row.last_login, "YYYY-MM-DD HH:mm:ss")
: "从未登录", : "从未登录",
}, },
{ title: "真名", key: "real_name", width: 100, render: (row) => h(TextCopy, () => row.real_name) }, {
title: "真名",
key: "real_name",
width: 100,
render: (row) => h(TextCopy, () => row.real_name),
},
{ title: "邮箱", key: "email", width: 200 }, { title: "邮箱", key: "email", width: 200 },
{ {
key: "actions", key: "actions",

View File

@@ -1,57 +1,54 @@
<script lang="ts" setup> <script lang="ts" setup>
import { code } from "oj/composables/code" import { code } from "oj/composables/code"
import { problem } from "oj/composables/problem" import { problem } from "oj/composables/problem"
import { SOURCES } from "utils/constants" import { SOURCES } from "utils/constants"
import CodeEditor from "~/shared/components/CodeEditor.vue" import CodeEditor from "~/shared/components/CodeEditor.vue"
import { isDesktop } from "~/shared/composables/breakpoints" import { isDesktop } from "~/shared/composables/breakpoints"
import storage from "~/utils/storage" import storage from "~/utils/storage"
import { LANGUAGE } from "~/utils/types" import { LANGUAGE } from "~/utils/types"
import Form from "./Form.vue" import Form from "./Form.vue"
const route = useRoute() const route = useRoute()
const contestID = route.params.contestID || null const contestID = route.params.contestID || null
const storageKey = computed( const storageKey = computed(
() => () =>
`problem_${problem.value!._id}_contest_${contestID}_lang_${code.language}`, `problem_${problem.value!._id}_contest_${contestID}_lang_${code.language}`,
) )
const editorHeight = computed(() => const editorHeight = computed(() =>
isDesktop.value ? "calc(100vh - 133px)" : "calc(100vh - 172px)", isDesktop.value ? "calc(100vh - 133px)" : "calc(100vh - 172px)",
) )
onMounted(() => { onMounted(() => {
const savedCode = storage.get(storageKey.value) const savedCode = storage.get(storageKey.value)
code.value = code.value =
savedCode || savedCode ||
problem.value!.template[code.language] || problem.value!.template[code.language] ||
SOURCES[code.language] SOURCES[code.language]
}) })
const changeCode = (v: string) => { const changeCode = (v: string) => {
storage.set(storageKey.value, v) storage.set(storageKey.value, v)
} }
const changeLanguage = (v: LANGUAGE) => { const changeLanguage = (v: LANGUAGE) => {
const savedCode = storage.get(storageKey.value) const savedCode = storage.get(storageKey.value)
code.value = code.value =
savedCode && storageKey.value.split("_").pop() === v savedCode && storageKey.value.split("_").pop() === v
? savedCode ? savedCode
: problem.value!.template[code.language] || SOURCES[code.language] : problem.value!.template[code.language] || SOURCES[code.language]
} }
</script> </script>
<template> <template>
<n-flex vertical> <n-flex vertical>
<Form <Form :storage-key="storageKey" @change-language="changeLanguage" />
:storage-key="storageKey" <CodeEditor
@change-language="changeLanguage" v-model:value="code.value"
/> :language="code.language"
<CodeEditor :height="editorHeight"
v-model:value="code.value" @update:model-value="changeCode"
:language="code.language" />
:height="editorHeight" </n-flex>
@update:model-value="changeCode" </template>
/>
</n-flex>
</template>

View File

@@ -1,94 +1,94 @@
<script lang="ts" setup> <script lang="ts" setup>
import { code } from "oj/composables/code" import { code } from "oj/composables/code"
import { problem } from "oj/composables/problem" import { problem } from "oj/composables/problem"
import { SOURCES } from "utils/constants" import { SOURCES } from "utils/constants"
import SyncCodeEditor from "~/shared/components/SyncCodeEditor.vue" import SyncCodeEditor from "~/shared/components/SyncCodeEditor.vue"
import { isDesktop } from "~/shared/composables/breakpoints" import { isDesktop } from "~/shared/composables/breakpoints"
import storage from "~/utils/storage" import storage from "~/utils/storage"
import { LANGUAGE } from "~/utils/types" import { LANGUAGE } from "~/utils/types"
import Form from "./Form.vue" import Form from "./Form.vue"
const route = useRoute() const route = useRoute()
const formRef = useTemplateRef<InstanceType<typeof Form>>("formRef") const formRef = useTemplateRef<InstanceType<typeof Form>>("formRef")
const sync = ref(false) const sync = ref(false)
const otherUserInfo = ref<{ name: string; isSuperAdmin: boolean }>() const otherUserInfo = ref<{ name: string; isSuperAdmin: boolean }>()
const hadConnection = ref(false) const hadConnection = ref(false)
const contestID = route.params.contestID || null const contestID = route.params.contestID || null
const storageKey = computed( const storageKey = computed(
() => () =>
`problem_${problem.value!._id}_contest_${contestID}_lang_${code.language}`, `problem_${problem.value!._id}_contest_${contestID}_lang_${code.language}`,
) )
const editorHeight = computed(() => const editorHeight = computed(() =>
isDesktop.value ? "calc(100vh - 133px)" : "calc(100vh - 172px)", isDesktop.value ? "calc(100vh - 133px)" : "calc(100vh - 172px)",
) )
onMounted(() => { onMounted(() => {
const savedCode = storage.get(storageKey.value) const savedCode = storage.get(storageKey.value)
code.value = code.value =
savedCode || savedCode ||
problem.value!.template[code.language] || problem.value!.template[code.language] ||
SOURCES[code.language] SOURCES[code.language]
}) })
const changeCode = (v: string) => { const changeCode = (v: string) => {
storage.set(storageKey.value, v) storage.set(storageKey.value, v)
} }
const changeLanguage = (v: LANGUAGE) => { const changeLanguage = (v: LANGUAGE) => {
const savedCode = storage.get(storageKey.value) const savedCode = storage.get(storageKey.value)
code.value = code.value =
savedCode && storageKey.value.split("_").pop() === v savedCode && storageKey.value.split("_").pop() === v
? savedCode ? savedCode
: problem.value!.template[code.language] || SOURCES[code.language] : problem.value!.template[code.language] || SOURCES[code.language]
} }
const toggleSync = (value: boolean) => { const toggleSync = (value: boolean) => {
sync.value = value sync.value = value
if (!value) { if (!value) {
hadConnection.value = false hadConnection.value = false
} }
} }
const handleSyncClosed = () => { const handleSyncClosed = () => {
sync.value = false sync.value = false
otherUserInfo.value = undefined otherUserInfo.value = undefined
hadConnection.value = false hadConnection.value = false
formRef.value?.resetSyncStatus() formRef.value?.resetSyncStatus()
} }
const handleSyncStatusChange = (status: { const handleSyncStatusChange = (status: {
otherUser?: { name: string; isSuperAdmin: boolean } otherUser?: { name: string; isSuperAdmin: boolean }
}) => { }) => {
otherUserInfo.value = status.otherUser otherUserInfo.value = status.otherUser
if (status.otherUser) { if (status.otherUser) {
hadConnection.value = true hadConnection.value = true
} }
} }
</script> </script>
<template> <template>
<n-flex vertical> <n-flex vertical>
<Form <Form
ref="formRef" ref="formRef"
:storage-key="storageKey" :storage-key="storageKey"
:other-user-info="otherUserInfo" :other-user-info="otherUserInfo"
:is-connected="sync" :is-connected="sync"
:had-connection="hadConnection" :had-connection="hadConnection"
@change-language="changeLanguage" @change-language="changeLanguage"
@toggle-sync="toggleSync" @toggle-sync="toggleSync"
/> />
<SyncCodeEditor <SyncCodeEditor
v-model:value="code.value" v-model:value="code.value"
:sync="sync" :sync="sync"
:problem="problem!._id" :problem="problem!._id"
:language="code.language" :language="code.language"
:height="editorHeight" :height="editorHeight"
@update:model-value="changeCode" @update:model-value="changeCode"
@sync-closed="handleSyncClosed" @sync-closed="handleSyncClosed"
@sync-status-change="handleSyncStatusChange" @sync-status-change="handleSyncStatusChange"
/> />
</n-flex> </n-flex>
</template> </template>

View File

@@ -158,7 +158,8 @@ watch(query, listSubmissions)
<template #header> <template #header>
<n-flex align="center"> <n-flex align="center">
<span> <span>
本道题你还没有解决你们班共有 <b>{{ class_ac_count }}</b> 人答案正确 本道题你还没有解决你们班共有
<b>{{ class_ac_count }}</b> 人答案正确
</span> </span>
<n-button <n-button
v-if="userStore.showSubmissions" v-if="userStore.showSubmissions"
@@ -245,7 +246,12 @@ watch(query, listSubmissions)
</template> </template>
<template v-if="userStore.showSubmissions && userStore.isAuthed"> <template v-if="userStore.showSubmissions && userStore.isAuthed">
<n-data-table v-if="submissions.length > 0" striped :columns="columns" :data="submissions" /> <n-data-table
v-if="submissions.length > 0"
striped
:columns="columns"
:data="submissions"
/>
<Pagination <Pagination
:total="total" :total="total"
v-model:limit="query.limit" v-model:limit="query.limit"

View File

@@ -9,8 +9,12 @@ import {
} from "~/shared/composables/switchScreen" } from "~/shared/composables/switchScreen"
import { problem } from "../composables/problem" import { problem } from "../composables/problem"
const ProblemEditor = defineAsyncComponent(() => import("./components/ProblemEditor.vue")) const ProblemEditor = defineAsyncComponent(
const ContestEditor = defineAsyncComponent(() => import("./components/ContestEditor.vue")) () => import("./components/ProblemEditor.vue"),
)
const ContestEditor = defineAsyncComponent(
() => import("./components/ContestEditor.vue"),
)
const EditorWithTest = defineAsyncComponent( const EditorWithTest = defineAsyncComponent(
() => import("./components/EditorWithTest.vue"), () => import("./components/EditorWithTest.vue"),
) )

View File

@@ -58,4 +58,4 @@ const extensions = computed(() => [
:placeholder="placeholder" :placeholder="placeholder"
:style="{ height, fontSize: `${fontSize}px` }" :style="{ height, fontSize: `${fontSize}px` }"
/> />
</template> </template>

View File

@@ -1,138 +1,138 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cpp } from "@codemirror/lang-cpp" import { cpp } from "@codemirror/lang-cpp"
import { python } from "@codemirror/lang-python" import { python } from "@codemirror/lang-python"
import { EditorView } from "@codemirror/view" import { EditorView } from "@codemirror/view"
import { Codemirror } from "vue-codemirror" import { Codemirror } from "vue-codemirror"
import type { Extension } from "@codemirror/state" import type { Extension } from "@codemirror/state"
import { LANGUAGE } from "~/utils/types" import { LANGUAGE } from "~/utils/types"
import { oneDark } from "../themes/oneDark" import { oneDark } from "../themes/oneDark"
import { smoothy } from "../themes/smoothy" import { smoothy } from "../themes/smoothy"
import { useCodeSync } from "../composables/sync" import { useCodeSync } from "../composables/sync"
import { isDesktop } from "../composables/breakpoints" import { isDesktop } from "../composables/breakpoints"
interface EditorReadyPayload { interface EditorReadyPayload {
view: EditorView view: EditorView
state: any state: any
container: HTMLElement container: HTMLElement
} }
interface Props { interface Props {
sync: boolean sync: boolean
problem: string problem: string
language?: LANGUAGE language?: LANGUAGE
fontSize?: number fontSize?: number
height?: string height?: string
readonly?: boolean readonly?: boolean
placeholder?: string placeholder?: string
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
language: "Python3", language: "Python3",
fontSize: 20, fontSize: 20,
height: "100%", height: "100%",
readonly: false, readonly: false,
placeholder: "", placeholder: "",
}) })
const { readonly, placeholder, height, fontSize } = toRefs(props) const { readonly, placeholder, height, fontSize } = toRefs(props)
const code = defineModel<string>("value") const code = defineModel<string>("value")
const emit = defineEmits<{ const emit = defineEmits<{
syncClosed: [] syncClosed: []
syncStatusChange: [ syncStatusChange: [
status: { otherUser?: { name: string; isSuperAdmin: boolean } }, status: { otherUser?: { name: string; isSuperAdmin: boolean } },
] ]
}>() }>()
const isDark = useDark() const isDark = useDark()
const styleTheme = EditorView.baseTheme({ const styleTheme = EditorView.baseTheme({
"& .cm-scroller": { "font-family": "Monaco" }, "& .cm-scroller": { "font-family": "Monaco" },
"&.cm-editor.cm-focused": { outline: "none" }, "&.cm-editor.cm-focused": { outline: "none" },
"&.cm-editor .cm-tooltip.cm-tooltip-autocomplete ul": { "&.cm-editor .cm-tooltip.cm-tooltip-autocomplete ul": {
"font-family": "Monaco", "font-family": "Monaco",
}, },
}) })
const lang = computed((): Extension => { const lang = computed((): Extension => {
return ["Python2", "Python3"].includes(props.language) ? python() : cpp() return ["Python2", "Python3"].includes(props.language) ? python() : cpp()
}) })
const extensions = computed(() => [ const extensions = computed(() => [
styleTheme, styleTheme,
lang.value, lang.value,
isDark.value ? oneDark : smoothy, isDark.value ? oneDark : smoothy,
getInitialExtension(), getInitialExtension(),
]) ])
const { startSync, stopSync, getInitialExtension } = useCodeSync() const { startSync, stopSync, getInitialExtension } = useCodeSync()
const editorView = ref<EditorView | null>(null) const editorView = ref<EditorView | null>(null)
let cleanupSync: (() => void) | null = null let cleanupSync: (() => void) | null = null
const cleanupSyncResources = () => { const cleanupSyncResources = () => {
if (cleanupSync) { if (cleanupSync) {
cleanupSync() cleanupSync()
cleanupSync = null cleanupSync = null
} }
stopSync() stopSync()
} }
const initSync = async () => { const initSync = async () => {
if (!editorView.value || !props.problem || !isDesktop.value) return if (!editorView.value || !props.problem || !isDesktop.value) return
cleanupSyncResources() cleanupSyncResources()
cleanupSync = await startSync({ cleanupSync = await startSync({
problemId: props.problem, problemId: props.problem,
editorView: editorView.value as EditorView, editorView: editorView.value as EditorView,
onStatusChange: (status) => { onStatusChange: (status) => {
if (status.error === "超管已离开" && !status.connected) { if (status.error === "超管已离开" && !status.connected) {
emit("syncClosed") emit("syncClosed")
} }
emit("syncStatusChange", { otherUser: status.otherUser }) emit("syncStatusChange", { otherUser: status.otherUser })
}, },
}) })
} }
const handleEditorReady = (payload: EditorReadyPayload) => { const handleEditorReady = (payload: EditorReadyPayload) => {
editorView.value = payload.view as EditorView editorView.value = payload.view as EditorView
if (props.sync) { if (props.sync) {
initSync() initSync()
} }
} }
watch( watch(
() => props.sync, () => props.sync,
(shouldSync) => { (shouldSync) => {
if (shouldSync) { if (shouldSync) {
initSync() initSync()
} else { } else {
cleanupSyncResources() cleanupSyncResources()
} }
}, },
) )
watch( watch(
() => props.problem, () => props.problem,
(newProblem, oldProblem) => { (newProblem, oldProblem) => {
if (newProblem !== oldProblem && props.sync) { if (newProblem !== oldProblem && props.sync) {
initSync() initSync()
} }
}, },
) )
onUnmounted(cleanupSyncResources) onUnmounted(cleanupSyncResources)
</script> </script>
<template> <template>
<Codemirror <Codemirror
v-model="code" v-model="code"
indentWithTab indentWithTab
:extensions="extensions" :extensions="extensions"
:disabled="readonly" :disabled="readonly"
:tab-size="4" :tab-size="4"
:placeholder="placeholder" :placeholder="placeholder"
:style="{ height, fontSize: `${fontSize}px` }" :style="{ height, fontSize: `${fontSize}px` }"
@ready="handleEditorReady" @ready="handleEditorReady"
/> />
</template> </template>

View File

@@ -1,33 +1,33 @@
<template> <template>
<n-tooltip> <n-tooltip>
<template #trigger> <template #trigger>
<n-button text @click="handleClick"> <n-button text @click="handleClick">
<slot /> <slot />
</n-button> </n-button>
</template> </template>
点击复制 点击复制
</n-tooltip> </n-tooltip>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { copyToClipboard } from "~/utils/functions" import { copyToClipboard } from "~/utils/functions"
const message = useMessage() const message = useMessage()
const slots = useSlots() const slots = useSlots()
async function handleClick() { async function handleClick() {
const textToCopy = getTextFromSlot() const textToCopy = getTextFromSlot()
const success = await copyToClipboard(textToCopy) const success = await copyToClipboard(textToCopy)
if (success) { if (success) {
message.success("已复制") message.success("已复制")
} else { } else {
message.error("复制失败") message.error("复制失败")
} }
} }
function getTextFromSlot() { function getTextFromSlot() {
const vnodes = slots.default?.() const vnodes = slots.default?.()
if (!vnodes) return "" if (!vnodes) return ""
return vnodes.map((vnode) => vnode.children).join("") return vnodes.map((vnode) => vnode.children).join("")
} }
</script> </script>

View File

@@ -175,9 +175,7 @@ export function useCodeSync() {
roomUsers, roomUsers,
canSync: false, canSync: false,
message: message:
roomUsers === 1 roomUsers === 1 ? "正在等待小伙伴加入..." : "等待超级管理员加入...",
? "正在等待小伙伴加入..."
: "等待超级管理员加入...",
otherUser, otherUser,
}, },
onStatusChange, onStatusChange,