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")
: "从未登录",
},
{ 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 },
{
key: "actions",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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