Compare commits

..

11 Commits

Author SHA1 Message Date
44b9e6d8dc fix
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled
2026-03-17 19:09:39 +08:00
1f47eff479 update
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled
2026-03-17 08:13:04 +08:00
8be95b4c85 add end_time
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled
2026-03-16 23:44:41 +08:00
19b2e2a507 add icon url
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled
2026-03-10 14:15:45 +08:00
3e0dd76a1e fix
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled
2026-03-09 21:13:54 +08:00
315729d7e8 fix
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled
2026-03-09 21:07:59 +08:00
cbce188028 fix
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled
2026-03-09 20:43:59 +08:00
52d25f8b41 fix
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled
2026-03-09 19:55:46 +08:00
b82840c692 fix
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled
2026-03-09 19:50:09 +08:00
5e07d3311b update
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled
2026-03-08 21:40:30 +08:00
50c9ce3555 update
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled
2026-03-08 21:30:55 +08:00
13 changed files with 160 additions and 64 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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;
} }

View File

@@ -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)

View File

@@ -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"

View File

@@ -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">

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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>

View File

@@ -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",
},
) )
} }

View File

@@ -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 {

View File

@@ -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
} }