Compare commits
11 Commits
8c02ad46e7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 44b9e6d8dc | |||
| 1f47eff479 | |||
| 8be95b4c85 | |||
| 19b2e2a507 | |||
| 3e0dd76a1e | |||
| 315729d7e8 | |||
| cbce188028 | |||
| 52d25f8b41 | |||
| b82840c692 | |||
| 5e07d3311b | |||
| 50c9ce3555 |
@@ -5,3 +5,4 @@ PUBLIC_CODE_URL=https://code.xuyue.cc
|
|||||||
PUBLIC_JUDGE0_URL=https://judge0api.xuyue.cc
|
PUBLIC_JUDGE0_URL=https://judge0api.xuyue.cc
|
||||||
PUBLIC_SIGNALING_URL=wss://signaling.xuyue.cc
|
PUBLIC_SIGNALING_URL=wss://signaling.xuyue.cc
|
||||||
PUBLIC_WS_URL=wss://oj.xuyue.cc/ws
|
PUBLIC_WS_URL=wss://oj.xuyue.cc/ws
|
||||||
|
PUBLIC_ICONIFY_URL=https://icon.xuyue.cc
|
||||||
@@ -41,10 +41,10 @@ Each feature module (under `oj/` or `admin/`) typically has:
|
|||||||
- `api.ts` — API calls specific to the feature
|
- `api.ts` — API calls specific to the feature
|
||||||
|
|
||||||
Shared logic lives in `shared/`:
|
Shared logic lives in `shared/`:
|
||||||
- `store/` — Pinia stores (user, config, authModal, screenMode, loginSummary)
|
- `store/` — Pinia stores: `user` (auth/roles), `config` (site-wide settings), `authModal` (login/signup form state), `screenMode` (problem split-screen layout), `loginSummary` (AI activity summary)
|
||||||
- `composables/` — reusable composables (pagination, WebSocket, sync, etc.)
|
- `composables/` — `pagination` (URL-synced), `websocket` (reconnect + heartbeat), `sync` (Yjs/y-webrtc for collaborative editing), `configUpdate` (WS-pushed config sync), `useMermaid` (lazy Mermaid render), `breakpoints`, `maxkb`
|
||||||
- `layout/` — `default.vue` and `admin.vue` layout wrappers
|
- `layout/` — `default.vue` and `admin.vue` layout wrappers
|
||||||
- `api.ts` — shared API calls
|
- `api.ts` — shared API calls (auth, profile, tags, captcha)
|
||||||
|
|
||||||
### Auto-Imports
|
### Auto-Imports
|
||||||
|
|
||||||
@@ -55,6 +55,7 @@ Configured via `unplugin-auto-import` and `unplugin-vue-components`. You do **no
|
|||||||
- VueUse composables
|
- VueUse composables
|
||||||
- Naive UI composables (`useDialog`, `useMessage`, `useNotification`, `useLoadingBar`)
|
- Naive UI composables (`useDialog`, `useMessage`, `useNotification`, `useLoadingBar`)
|
||||||
- Naive UI components (all `N*` components)
|
- Naive UI components (all `N*` components)
|
||||||
|
- Naive UI types (`DataTableColumn`, `FormRules`, `FormItemRule`, `SelectOption`, `UploadCustomRequestOptions`, `UploadFileInfo`, `MenuOption`, `DropdownOption`)
|
||||||
|
|
||||||
Generated type declaration files: `src/auto-imports.d.ts`, `src/components.d.ts`.
|
Generated type declaration files: `src/auto-imports.d.ts`, `src/components.d.ts`.
|
||||||
|
|
||||||
@@ -110,4 +111,4 @@ Routes are defined in `src/routes.ts` with two root routes: `ojs` (user-facing)
|
|||||||
|
|
||||||
## Related Repository
|
## Related Repository
|
||||||
|
|
||||||
The backend is at `D:\Projects\OnlineJudge` — a Django 5 + DRF project. See its CLAUDE.md for backend details.
|
The backend is at `../OnlineJudge` — a Django 5 + DRF project. See its CLAUDE.md for backend details.
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
z-index: 100 !important;
|
z-index: 100 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md-editor-preview img {
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
.md-editor-preview h1 {
|
.md-editor-preview h1 {
|
||||||
font-size: 1.6rem !important;
|
font-size: 1.6rem !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,6 +318,7 @@ export function createProblemSet(data: {
|
|||||||
description: string
|
description: string
|
||||||
difficulty: string
|
difficulty: string
|
||||||
status: string
|
status: string
|
||||||
|
end_time?: Date | null
|
||||||
}) {
|
}) {
|
||||||
return http.post("admin/problemset", data)
|
return http.post("admin/problemset", data)
|
||||||
}
|
}
|
||||||
@@ -328,6 +329,7 @@ export function editProblemSet(data: {
|
|||||||
description?: string
|
description?: string
|
||||||
difficulty?: string
|
difficulty?: string
|
||||||
status?: string
|
status?: string
|
||||||
|
end_time?: Date | null
|
||||||
visible?: boolean
|
visible?: boolean
|
||||||
}) {
|
}) {
|
||||||
return http.put("admin/problemset", data)
|
return http.put("admin/problemset", data)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h2 style="margin-top: 0">学生卡点分析</h2>
|
<h2 style="margin-top: 0">学生卡点分析(只分析前40道题目)</h2>
|
||||||
<n-data-table
|
<n-data-table
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
|
|||||||
@@ -140,6 +140,12 @@ watch(() => [query.page, query.limit, query.author], listProblems)
|
|||||||
>
|
>
|
||||||
新建
|
新建
|
||||||
</n-button>
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
v-if="!isContestProblemList"
|
||||||
|
@click="$router.push({ name: 'admin stuck problems' })"
|
||||||
|
>
|
||||||
|
卡点分析
|
||||||
|
</n-button>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<n-flex>
|
<n-flex>
|
||||||
<n-button v-if="isContestProblemList" @click="createContestProblem">
|
<n-button v-if="isContestProblemList" @click="createContestProblem">
|
||||||
|
|||||||
@@ -15,6 +15,17 @@ const formData = ref<CreateProblemSetData & Partial<EditProblemSetData>>({
|
|||||||
difficulty: "Easy",
|
difficulty: "Easy",
|
||||||
status: "draft",
|
status: "draft",
|
||||||
visible: false,
|
visible: false,
|
||||||
|
end_time: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const endTimeTimestamp = computed({
|
||||||
|
get: () =>
|
||||||
|
formData.value.end_time
|
||||||
|
? new Date(formData.value.end_time).getTime()
|
||||||
|
: null,
|
||||||
|
set: (val: number | null) => {
|
||||||
|
formData.value.end_time = val ? new Date(val) : null
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const difficultyOptions = [
|
const difficultyOptions = [
|
||||||
@@ -44,6 +55,7 @@ async function loadProblemSetDetail() {
|
|||||||
difficulty: data.difficulty,
|
difficulty: data.difficulty,
|
||||||
status: data.status,
|
status: data.status,
|
||||||
visible: data.visible,
|
visible: data.visible,
|
||||||
|
end_time: data.end_time ? new Date(data.end_time) : null,
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
message.error("加载题单详情失败:" + (err.data || "未知错误"))
|
message.error("加载题单详情失败:" + (err.data || "未知错误"))
|
||||||
@@ -120,6 +132,14 @@ onMounted(() => {
|
|||||||
placeholder="选择状态"
|
placeholder="选择状态"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
<n-form-item label="截止时间">
|
||||||
|
<n-date-picker
|
||||||
|
v-model:value="endTimeTimestamp"
|
||||||
|
type="datetime"
|
||||||
|
clearable
|
||||||
|
placeholder="不设置则无截止时间"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
<n-form-item v-if="isEdit" label="是否可见">
|
<n-form-item v-if="isEdit" label="是否可见">
|
||||||
<n-switch v-model:value="formData.visible" />
|
<n-switch v-model:value="formData.visible" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|||||||
@@ -47,10 +47,19 @@ async function loadSimilarProblems() {
|
|||||||
similarLoaded.value = true
|
similarLoaded.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换题目时重置相似推荐状态
|
||||||
|
watch(
|
||||||
|
() => problem.value?._id,
|
||||||
|
() => {
|
||||||
|
similarProblems.value = []
|
||||||
|
similarLoaded.value = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// AC 或失败次数 >= 3 时加载推荐
|
// AC 或失败次数 >= 3 时加载推荐
|
||||||
watch(
|
watch(
|
||||||
() => [problem.value?.my_status, problemStore.totalFailCount],
|
() => [problem.value?._id, problem.value?.my_status, problemStore.totalFailCount],
|
||||||
([status, failCount]) => {
|
([, status, failCount]) => {
|
||||||
if (status === 0 || (failCount as number) >= 3) {
|
if (status === 0 || (failCount as number) >= 3) {
|
||||||
loadSimilarProblems()
|
loadSimilarProblems()
|
||||||
}
|
}
|
||||||
@@ -267,7 +276,7 @@ function type(status: ProblemStatus) {
|
|||||||
<n-divider />
|
<n-divider />
|
||||||
<p class="title" :style="style">
|
<p class="title" :style="style">
|
||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
<Icon icon="streamline-emojis:light-bulb"></Icon>
|
<Icon icon="fluent-emoji-flat:light-bulb"></Icon>
|
||||||
相似题目推荐
|
相似题目推荐
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</p>
|
</p>
|
||||||
@@ -323,7 +332,7 @@ function type(status: ProblemStatus) {
|
|||||||
.testcase {
|
.testcase {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
font-family: "Monaco", monospace;
|
font-family: "Monaco";
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-alert {
|
.status-alert {
|
||||||
|
|||||||
@@ -38,14 +38,18 @@ const editorHeight = computed(() =>
|
|||||||
isDesktop.value ? "calc(100vh - 133px)" : "calc(100vh - 172px)",
|
isDesktop.value ? "calc(100vh - 133px)" : "calc(100vh - 172px)",
|
||||||
)
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
function loadCode() {
|
||||||
const savedCode = storage.get(storageKey.value)
|
const savedCode = storage.get(storageKey.value)
|
||||||
codeStore.setCode(
|
codeStore.setCode(
|
||||||
savedCode ||
|
savedCode ||
|
||||||
problem.value!.template[codeStore.code.language] ||
|
problem.value!.template[codeStore.code.language] ||
|
||||||
SOURCES[codeStore.code.language],
|
SOURCES[codeStore.code.language],
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
onMounted(loadCode)
|
||||||
|
|
||||||
|
watch(() => problem.value?._id, loadCode)
|
||||||
|
|
||||||
const changeCode = (v: string) => {
|
const changeCode = (v: string) => {
|
||||||
storage.set(storageKey.value, v)
|
storage.set(storageKey.value, v)
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ async function init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onMounted(init)
|
onMounted(init)
|
||||||
|
watch(() => props.problemID, init)
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
problem.value = null
|
problem.value = null
|
||||||
errMsg.value = "无数据"
|
errMsg.value = "无数据"
|
||||||
@@ -116,9 +117,17 @@ watch(isMobile, (value) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-grid v-if="problem" x-gap="16" :cols="screenModeStore.isBothMode ? 7 : 1">
|
<template v-if="problem">
|
||||||
<n-gi :span="isDesktop ? 3 : 7" v-if="shouldShowProblem">
|
<n-split
|
||||||
<n-scrollbar v-if="isDesktop" style="max-height: calc(100vh - 92px)">
|
v-if="isDesktop && screenModeStore.isBothMode"
|
||||||
|
direction="horizontal"
|
||||||
|
:default-size="0.43"
|
||||||
|
:min="0.2"
|
||||||
|
:max="0.8"
|
||||||
|
style="height: calc(100vh - 92px)"
|
||||||
|
>
|
||||||
|
<template #1>
|
||||||
|
<n-scrollbar style="height: 100%">
|
||||||
<n-tabs v-model:value="currentTab" type="segment">
|
<n-tabs v-model:value="currentTab" type="segment">
|
||||||
<n-tab-pane name="content" tab="题目描述">
|
<n-tab-pane name="content" tab="题目描述">
|
||||||
<ProblemContent />
|
<ProblemContent />
|
||||||
@@ -154,6 +163,58 @@ watch(isMobile, (value) => {
|
|||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
</n-scrollbar>
|
</n-scrollbar>
|
||||||
|
</template>
|
||||||
|
<template #2>
|
||||||
|
<component :is="inProblem ? ProblemEditor : ContestEditor" />
|
||||||
|
</template>
|
||||||
|
</n-split>
|
||||||
|
|
||||||
|
<!-- Desktop: code only mode -->
|
||||||
|
<template v-else-if="isDesktop && screenModeStore.isCodeOnlyMode">
|
||||||
|
<EditorForTest />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Desktop: problem only mode -->
|
||||||
|
<template v-else-if="isDesktop && shouldShowProblem">
|
||||||
|
<n-scrollbar style="max-height: calc(100vh - 92px)">
|
||||||
|
<n-tabs v-model:value="currentTab" type="segment">
|
||||||
|
<n-tab-pane name="content" tab="题目描述">
|
||||||
|
<ProblemContent />
|
||||||
|
</n-tab-pane>
|
||||||
|
<n-tab-pane
|
||||||
|
v-if="problem.show_flowchart && problem.mermaid_code"
|
||||||
|
name="flowchart"
|
||||||
|
tab="流程图表"
|
||||||
|
>
|
||||||
|
<ProblemFlowchart />
|
||||||
|
</n-tab-pane>
|
||||||
|
<n-tab-pane
|
||||||
|
name="info"
|
||||||
|
tab="题目统计"
|
||||||
|
:disabled="!!props.problemSetId"
|
||||||
|
>
|
||||||
|
<ProblemInfo />
|
||||||
|
</n-tab-pane>
|
||||||
|
<n-tab-pane
|
||||||
|
v-if="!props.contestID"
|
||||||
|
name="comment"
|
||||||
|
tab="题目点评"
|
||||||
|
:disabled="!!props.problemSetId"
|
||||||
|
>
|
||||||
|
<ProblemComment />
|
||||||
|
</n-tab-pane>
|
||||||
|
<n-tab-pane
|
||||||
|
name="submission"
|
||||||
|
tab="我的提交"
|
||||||
|
:disabled="!!props.problemSetId"
|
||||||
|
>
|
||||||
|
<ProblemSubmission />
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
</n-scrollbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Mobile -->
|
||||||
<n-tabs v-else v-model:value="currentTab" type="segment">
|
<n-tabs v-else v-model:value="currentTab" type="segment">
|
||||||
<n-tab-pane name="content" tab="描述">
|
<n-tab-pane name="content" tab="描述">
|
||||||
<ProblemContent />
|
<ProblemContent />
|
||||||
@@ -183,13 +244,6 @@ watch(isMobile, (value) => {
|
|||||||
<ProblemSubmission />
|
<ProblemSubmission />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
</n-gi>
|
</template>
|
||||||
<n-gi :span="4" v-if="isDesktop && screenModeStore.isBothMode">
|
|
||||||
<component :is="inProblem ? ProblemEditor : ContestEditor" />
|
|
||||||
</n-gi>
|
|
||||||
<n-gi v-if="isDesktop && screenModeStore.isCodeOnlyMode">
|
|
||||||
<EditorForTest />
|
|
||||||
</n-gi>
|
|
||||||
</n-grid>
|
|
||||||
<n-empty v-else :description="errMsg"></n-empty>
|
<n-empty v-else :description="errMsg"></n-empty>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -99,15 +99,6 @@ const options = computed<MenuOption[]>(() => {
|
|||||||
),
|
),
|
||||||
key: "admin tutorial list",
|
key: "admin tutorial list",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: () =>
|
|
||||||
h(
|
|
||||||
RouterLink,
|
|
||||||
{ to: "/admin/problem/stuck" },
|
|
||||||
{ default: () => "卡点" },
|
|
||||||
),
|
|
||||||
key: "admin stuck problems",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -239,9 +239,9 @@ export const CODE_TEMPLATES = {
|
|||||||
} as const
|
} as const
|
||||||
|
|
||||||
export enum ScreenMode {
|
export enum ScreenMode {
|
||||||
both = "题目 | 自测",
|
both = "双栏",
|
||||||
code = "仅自测",
|
code = "自测",
|
||||||
problem = "仅题目",
|
problem = "题目",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ChartType {
|
export enum ChartType {
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ export interface ProblemSet {
|
|||||||
create_time: Date
|
create_time: Date
|
||||||
difficulty: "Easy" | "Medium" | "Hard"
|
difficulty: "Easy" | "Medium" | "Hard"
|
||||||
status: "active" | "archived" | "draft"
|
status: "active" | "archived" | "draft"
|
||||||
|
end_time: Date | null
|
||||||
visible: boolean
|
visible: boolean
|
||||||
problems_count: number
|
problems_count: number
|
||||||
completed_count: number
|
completed_count: number
|
||||||
@@ -213,6 +214,7 @@ export interface ProblemSetList {
|
|||||||
create_time: Date
|
create_time: Date
|
||||||
difficulty: "Easy" | "Medium" | "Hard"
|
difficulty: "Easy" | "Medium" | "Hard"
|
||||||
status: "active" | "archived" | "draft"
|
status: "active" | "archived" | "draft"
|
||||||
|
end_time: Date | null
|
||||||
problems_count: number
|
problems_count: number
|
||||||
visible: boolean
|
visible: boolean
|
||||||
user_progress: {
|
user_progress: {
|
||||||
@@ -277,6 +279,7 @@ export interface CreateProblemSetData {
|
|||||||
description: string
|
description: string
|
||||||
difficulty: "Easy" | "Medium" | "Hard"
|
difficulty: "Easy" | "Medium" | "Hard"
|
||||||
status: "active" | "archived" | "draft"
|
status: "active" | "archived" | "draft"
|
||||||
|
end_time?: Date | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditProblemSetData {
|
export interface EditProblemSetData {
|
||||||
@@ -285,6 +288,7 @@ export interface EditProblemSetData {
|
|||||||
description?: string
|
description?: string
|
||||||
difficulty?: "Easy" | "Medium" | "Hard"
|
difficulty?: "Easy" | "Medium" | "Hard"
|
||||||
status?: "active" | "archived" | "draft"
|
status?: "active" | "archived" | "draft"
|
||||||
|
end_time?: Date | null
|
||||||
visible?: boolean
|
visible?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user