diff --git a/frontend/src/components/CodeEditor.vue b/frontend/src/components/CodeEditor.vue new file mode 100644 index 0000000..ab88a09 --- /dev/null +++ b/frontend/src/components/CodeEditor.vue @@ -0,0 +1,84 @@ + + + diff --git a/frontend/src/composables/useCodeDraft.ts b/frontend/src/composables/useCodeDraft.ts new file mode 100644 index 0000000..f1ac72a --- /dev/null +++ b/frontend/src/composables/useCodeDraft.ts @@ -0,0 +1,77 @@ +import { computed, onScopeDispose, ref, watch } from 'vue' +export type Language = 'cpp' | 'python' | 'rust' +export const languageLabels: Record = { + cpp: 'C++', + python: 'Python', + rust: 'Rust', +} +export const codeTemplates: Record = { + cpp: '#include \nusing namespace std;\n\nint main() {\n // 在这里编写你的解法\n return 0;\n}\n', + python: '# 在这里编写你的解法\n\n', + rust: 'use std::io::{self, Read};\n\nfn main() {\n let mut input = String::new();\n io::stdin().read_to_string(&mut input).unwrap();\n // 在这里编写你的解法\n}\n', +} +export function useCodeDraft(problemId: string) { + const key = 'kf-oj:code-draft:' + problemId + const language = ref('cpp') + const drafts = ref({ ...codeTemplates }) + const state = ref<'initial' | 'saved' | 'changed' | 'unavailable'>('initial') + let timer: ReturnType | undefined + try { + const raw = localStorage.getItem(key) + if (raw) { + const saved: unknown = JSON.parse(raw) + if ( + saved && + typeof saved === 'object' && + 'version' in saved && + saved.version === 1 && + 'drafts' in saved && + saved.drafts && + typeof saved.drafts === 'object' + ) { + const storedDrafts = saved.drafts as Record + for (const lang of ['cpp', 'python', 'rust'] as const) { + const value = storedDrafts[lang] + if (typeof value === 'string') drafts.value[lang] = value + } + if ('language' in saved && ['cpp', 'python', 'rust'].includes(String(saved.language))) + language.value = saved.language as Language + state.value = 'saved' + } + } + } catch { + state.value = 'unavailable' + } + const code = computed({ + get: () => drafts.value[language.value], + set: (value) => { + drafts.value[language.value] = value + }, + }) + function save() { + clearTimeout(timer) + try { + localStorage.setItem( + key, + JSON.stringify({ version: 1, language: language.value, drafts: drafts.value }), + ) + state.value = 'saved' + } catch { + state.value = 'unavailable' + } + } + watch( + [drafts, language], + () => { + state.value = 'changed' + clearTimeout(timer) + timer = setTimeout(save, 400) + }, + { deep: true }, + ) + onScopeDispose(() => { + if (state.value === 'changed') save() + clearTimeout(timer) + }) + return { language, code, state, save } +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 79b992a..f4524b4 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -1,13 +1,67 @@ import { createRouter, createWebHistory } from 'vue-router' - export const router = createRouter({ history: createWebHistory(), + scrollBehavior: (to, _from, saved) => saved ?? (to.hash ? { el: to.hash } : { top: 0 }), routes: [ - { path: '/', name: 'home', component: () => import('../views/HomeView.vue') }, + { + path: '/', + name: 'home', + meta: { title: '首页' }, + component: () => import('../views/HomeView.vue'), + }, + { + path: '/problems', + meta: { title: '题库' }, + component: () => import('../views/ProblemsView.vue'), + }, + { + path: '/problems/:id', + meta: { title: '题目详情' }, + component: () => import('../views/ProblemDetailView.vue'), + }, + { + path: '/trainings', + meta: { title: '训练' }, + component: () => import('../views/TrainingsView.vue'), + }, + { + path: '/trainings/:id', + meta: { title: '训练详情' }, + component: () => import('../views/TrainingDetailView.vue'), + }, + { + path: '/contests', + meta: { title: '比赛' }, + component: () => import('../views/ContestsView.vue'), + }, + { + path: '/contests/:id', + meta: { title: '比赛详情' }, + component: () => import('../views/ContestDetailView.vue'), + }, + { + path: '/submissions', + meta: { title: '评测' }, + component: () => import('../views/SubmissionsView.vue'), + }, + { + path: '/submissions/:id', + meta: { title: '评测详情' }, + component: () => import('../views/SubmissionDetailView.vue'), + }, + { + path: '/rankings', + meta: { title: '排名' }, + component: () => import('../views/RankingsView.vue'), + }, { path: '/:pathMatch(.*)*', name: 'not-found', + meta: { title: '页面不存在' }, component: () => import('../views/NotFoundView.vue'), }, ], }) +router.afterEach((to) => { + document.title = `${String(to.meta.title ?? '首页')} · KF OnlineJudge` +}) diff --git a/frontend/src/views/ContestDetailView.vue b/frontend/src/views/ContestDetailView.vue new file mode 100644 index 0000000..ef8712b --- /dev/null +++ b/frontend/src/views/ContestDetailView.vue @@ -0,0 +1,77 @@ + + diff --git a/frontend/src/views/ContestsView.vue b/frontend/src/views/ContestsView.vue new file mode 100644 index 0000000..a7315fe --- /dev/null +++ b/frontend/src/views/ContestsView.vue @@ -0,0 +1,73 @@ + + diff --git a/frontend/src/views/ProblemDetailView.vue b/frontend/src/views/ProblemDetailView.vue new file mode 100644 index 0000000..6683560 --- /dev/null +++ b/frontend/src/views/ProblemDetailView.vue @@ -0,0 +1,520 @@ + + + diff --git a/frontend/src/views/ProblemsView.vue b/frontend/src/views/ProblemsView.vue new file mode 100644 index 0000000..51ae338 --- /dev/null +++ b/frontend/src/views/ProblemsView.vue @@ -0,0 +1,62 @@ + + diff --git a/frontend/src/views/RankingsView.vue b/frontend/src/views/RankingsView.vue new file mode 100644 index 0000000..bdae973 --- /dev/null +++ b/frontend/src/views/RankingsView.vue @@ -0,0 +1,67 @@ + + diff --git a/frontend/src/views/SubmissionDetailView.vue b/frontend/src/views/SubmissionDetailView.vue new file mode 100644 index 0000000..a2e9f1e --- /dev/null +++ b/frontend/src/views/SubmissionDetailView.vue @@ -0,0 +1,63 @@ + + diff --git a/frontend/src/views/SubmissionsView.vue b/frontend/src/views/SubmissionsView.vue new file mode 100644 index 0000000..9b39f10 --- /dev/null +++ b/frontend/src/views/SubmissionsView.vue @@ -0,0 +1,75 @@ + + diff --git a/frontend/src/views/TrainingDetailView.vue b/frontend/src/views/TrainingDetailView.vue new file mode 100644 index 0000000..b77c9be --- /dev/null +++ b/frontend/src/views/TrainingDetailView.vue @@ -0,0 +1,59 @@ + + diff --git a/frontend/src/views/TrainingsView.vue b/frontend/src/views/TrainingsView.vue new file mode 100644 index 0000000..482765e --- /dev/null +++ b/frontend/src/views/TrainingsView.vue @@ -0,0 +1,55 @@ + +