Compare commits

...
2 Commits
Author SHA1 Message Date
xuyueandClaude Opus 5.5 ff3fdc0b1d refactor(出题页): 编程题和 SQL 题两个出题页抄了两份的零件收到一处
Deploy / deploy (push) Canceled after 0s
admin/problem/components/problemForm.ts:判空富文本 isBlankHtml(原来三份,CodePreview 里还有
一份)、难度选项、返回列表的路由、保存失败的提示、标签超长校验、新建页的草稿
(useCreateDraft:停 0.8 秒存、什么都没填就清、编辑页不存)。

纯搬家。新建 / 编辑 × 编程 / SQL 四个页面改前改后整页截图逐像素一致;两种新建页填个
标题、刷新能恢复,「清空草稿」之后本地存储清掉。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:43:25 +08:00
xuyueandClaude Opus 5.5 e8a20d12f9 refactor(前端): shared / utils 不再 import oj 和 admin 下的任何东西
依赖方向原来是反的:顶栏、登录框、举手列表这些 shared 组件从 oj/ 里拿东西。
- 跨端的接口挪进 shared/api.ts(按 apps/web/CLAUDE.md 的分法):学生端题目列表、
  出题人列表、这节课的题、待弹的成就
- 这节课的 store → shared/store/lesson.ts;头像 RankAvatar → shared/components
- 纯函数挪进 utils/:举手列表的错法归类 errorKind、它依赖的答案错误分类 wrongAnswer
  和 Python 报错翻译 pythonError、题目列表行的 toProblemRow
- 只有后台在用的出题人下拉 AuthorSelect 挪回 admin/components

纯搬家。首页、题目列表、题目页、排名、课堂看板、后台题目列表打开无报错,
顶栏题号框敲 1001 回车直达题目页。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:11:21 +08:00
42 changed files with 204 additions and 178 deletions

No files matched your search

@@ -9,7 +9,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { getAuthors } from "oj/api" import { getAuthors } from "shared/api"
interface Props { interface Props {
all?: boolean all?: boolean
+27 -50
View File
@@ -1,8 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { EditorView } from "@codemirror/view" import { EditorView } from "@codemirror/view"
import { PROBLEM_TAG_MAX_LENGTH, TEST_CASE_MAX_CASES } from "@oj2/contract" import { TEST_CASE_MAX_CASES } from "@oj2/contract"
import Markdown from "shared/components/Markdown.vue" import Markdown from "shared/components/Markdown.vue"
import { StorageSerializers } from "@vueuse/core"
import { useThemeVars } from "naive-ui" import { useThemeVars } from "naive-ui"
import PageHeader from "admin/components/PageHeader.vue" import PageHeader from "admin/components/PageHeader.vue"
import ProblemTypeTag from "oj/problem/components/ProblemTypeTag.vue" import ProblemTypeTag from "oj/problem/components/ProblemTypeTag.vue"
@@ -24,6 +23,15 @@ import {
previewSQLTestcase, previewSQLTestcase,
uploadTestcases, uploadTestcases,
} from "../api" } from "../api"
import {
cleanTags,
difficultyOptions,
isBlankHtml,
problemListRoute,
saveErrorText,
tagTooLongText,
useCreateDraft,
} from "./components/problemForm"
import { import {
explainSqlError, explainSqlError,
groupSummary, groupSummary,
@@ -94,13 +102,6 @@ const blankForm = (): SqlForm => ({
const form = reactive<SqlForm>(blankForm()) const form = reactive<SqlForm>(blankForm())
/** 富文本编辑器清空之后留下的是一个空段落,不是空串 */
const isBlankHtml = (html: string) => !html.trim() || html === "<p><br></p>"
const difficultyOptions = (["Low", "Mid", "High"] as const).map((value) => ({
label: DIFFICULTY[value],
value,
}))
const testCase = reactive({ id: "", score: [] as Testcase[] }) const testCase = reactive({ id: "", score: [] as Testcase[] })
let nextKey = 0 let nextKey = 0
@@ -118,28 +119,17 @@ const showMore = ref(false)
const ready = ref(false) const ready = ref(false)
// 新建时存草稿:出一道 SQL 题要写好几段脚本,误关页面全没了太亏 // 新建时存草稿:出一道 SQL 题要写好几段脚本,误关页面全没了太亏
// 初值是 null 时 VueUse 推不出类型,会拿 String() 存成 "[object Object]",得显式指定 const draft = useCreateDraft<{ form: SqlForm; scripts: string[] }>(STORAGE_KEY.ADMIN_SQL_PROBLEM, {
const draft = useLocalStorage<{ form: SqlForm; scripts: string[] } | null>( source: () => [form, groups.value.map((g) => g.sql)] as const,
STORAGE_KEY.ADMIN_SQL_PROBLEM, enabled: () => ready.value && isCreate.value,
null, empty: () =>
{ serializer: StorageSerializers.object }, !form._id &&
) !form.title &&
isBlankHtml(form.description) &&
watchDebounced( !form.answer &&
() => [form, groups.value.map((g) => g.sql)] as const, !groups.value.some((g) => g.sql),
() => { take: () => ({ form: { ...form }, scripts: groups.value.map((g) => g.sql) }),
if (!ready.value || !isCreate.value) return })
const scripts = groups.value.map((g) => g.sql)
const empty =
!form._id &&
!form.title &&
isBlankHtml(form.description) &&
!form.answer &&
!scripts.some(Boolean)
draft.value = empty ? null : { form: { ...form }, scripts }
},
{ debounce: 800, deep: true },
)
function clearDraft() { function clearDraft() {
draft.value = null draft.value = null
@@ -212,12 +202,9 @@ const tagOptions = computed(() =>
) )
function onTagsChange(tags: string[]) { function onTagsChange(tags: string[]) {
const tooLong = tags.find((t) => t.length > PROBLEM_TAG_MAX_LENGTH) const cleaned = cleanTags(tags)
if (tooLong) { if ("tooLong" in cleaned) message.error(tagTooLongText(cleaned.tooLong))
message.error(`标签最多 ${PROBLEM_TAG_MAX_LENGTH} 个字:${tooLong}`) else form.tags = cleaned.tags
return
}
form.tags = unique(tags.map((t) => t.trim()).filter(Boolean))
} }
// ---------------------------------------------------------------- 测试数据:自动重跑 // ---------------------------------------------------------------- 测试数据:自动重跑
@@ -415,25 +402,15 @@ async function save() {
} else { } else {
message.success("保存好了") message.success("保存好了")
} }
router.push( router.push(problemListRoute(props.contestID))
props.contestID
? { name: "admin contest problem list", params: { contestID: props.contestID } }
: { name: "admin problem list" },
)
} catch (err) { } catch (err) {
message.error( message.error(saveErrorText(err))
errorCode(err) === "display-id-exists" ? "显示编号重复了,换一个" : errorMessage(err),
)
} finally { } finally {
saving.value = false saving.value = false
} }
} }
const backTo = computed(() => const backTo = computed(() => problemListRoute(props.contestID))
props.contestID
? { name: "admin contest problem list", params: { contestID: props.contestID } }
: { name: "admin problem list" },
)
onMounted(async () => { onMounted(async () => {
getProblemTagList().then((res) => (tagList.value = res)) getProblemTagList().then((res) => (tagList.value = res))
@@ -7,6 +7,7 @@ import { getTagColor } from "utils/functions"
import type { AstRules, BlankProblem, LANGUAGE } from "utils/types" import type { AstRules, BlankProblem, LANGUAGE } from "utils/types"
import type { AstCheckState, FlowchartMode, PreviewTab } from "./editorTypes" import type { AstCheckState, FlowchartMode, PreviewTab } from "./editorTypes"
import type { TemplateParts } from "./codeTemplate" import type { TemplateParts } from "./codeTemplate"
import { isBlankHtml } from "./problemForm"
const CodeEditor = defineAsyncComponent(() => import("shared/components/CodeEditor.vue")) const CodeEditor = defineAsyncComponent(() => import("shared/components/CodeEditor.vue"))
@@ -41,8 +42,6 @@ const emit = defineEmits<{ renderState: [ok: boolean]; collapse: [] }>()
const theme = useThemeVars() const theme = useThemeVars()
const isBlankHtml = (html: string) => !html.trim() || html === "<p><br></p>"
// ---------------------------------------------------------------- 语法要求 // ---------------------------------------------------------------- 语法要求
const KIND_TAG_TYPE = { require: "success", forbid: "error", count: "info" } as const const KIND_TAG_TYPE = { require: "success", forbid: "error", count: "info" } as const
@@ -0,0 +1,66 @@
import { PROBLEM_TAG_MAX_LENGTH } from "@oj2/contract"
import { StorageSerializers, useLocalStorage, watchDebounced } from "@vueuse/core"
import type { WatchSource } from "vue"
import { errorCode, errorMessage } from "utils/api"
import { DIFFICULTY } from "utils/constants"
import { unique } from "utils/functions"
/**
* 编程题(detail.vue)、SQL 题(SqlDetail.vue)两个出题页共用的零件。两页要填的东西几乎
* 不重合,所以没合成一页(见 SqlDetail.vue 顶部),但这几样原来是逐字抄了两三份。
*/
/** 富文本编辑器清空之后留下的是一个空段落,不是空串 */
export const isBlankHtml = (html: string) => !html.trim() || html === "<p><br></p>"
export const difficultyOptions = (["Low", "Mid", "High"] as const).map((value) => ({
label: DIFFICULTY[value],
value,
}))
/** 出题页的「返回」:比赛题回这场比赛的题目列表,公开题回题库 */
export function problemListRoute(contestID: string | undefined) {
return contestID
? { name: "admin contest problem list", params: { contestID } }
: { name: "admin problem list" }
}
/** 保存失败给老师看的话。编号撞了是最常见的一种,单独说清楚 */
export function saveErrorText(err: unknown) {
return errorCode(err) === "display-id-exists" ? "显示编号重复了,换一个" : errorMessage(err)
}
/**
* 下拉里新打的标签:去空格、去重。有超长的就返回它(调用方提示、这次改动不收),
* 后端也卡这个长度,先在这儿拦省一次往返
*/
export function cleanTags(tags: string[]): { tags: string[] } | { tooLong: string } {
const tooLong = tags.find((t) => t.length > PROBLEM_TAG_MAX_LENGTH)
if (tooLong) return { tooLong }
return { tags: unique(tags.map((t) => t.trim()).filter(Boolean)) }
}
export const tagTooLongText = (tag: string) => `标签最多 ${PROBLEM_TAG_MAX_LENGTH} 个字:${tag}`
/**
* 新建页的草稿:改动停 0.8 秒存一次,什么都没填就清掉。编辑页不存 —— 原来编辑页也往
* 同一个 key 里写,没保存就离开的话,那道题的 id 会跟着草稿漂到新建页。
*
* `enabled` 在页面数据摆好之前要返回 false,不然加载时那一下会把空表单存进去。
*/
export function useCreateDraft<T>(
key: string,
options: { source: WatchSource; enabled: () => boolean; empty: () => boolean; take: () => T },
) {
// 初值是 null 时 VueUse 推不出类型,会拿 String() 存成 "[object Object]",得显式指定
const draft = useLocalStorage<T | null>(key, null, { serializer: StorageSerializers.object })
watchDebounced(
options.source,
() => {
if (!options.enabled()) return
draft.value = options.empty() ? null : options.take()
},
{ debounce: 800, deep: true },
)
return draft
}
+32 -48
View File
@@ -1,11 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { PROBLEM_TAG_MAX_LENGTH, TEST_CASE_MAX_CASES, type RunnableLanguage } from "@oj2/contract" import { TEST_CASE_MAX_CASES, type RunnableLanguage } from "@oj2/contract"
import { StorageSerializers } from "@vueuse/core"
import { useThemeVars } from "naive-ui" import { useThemeVars } from "naive-ui"
import PageHeader from "admin/components/PageHeader.vue" import PageHeader from "admin/components/PageHeader.vue"
import { getProblemTagList } from "shared/api" import { getProblemTagList } from "shared/api"
import TextEditor from "shared/components/TextEditor.vue" import TextEditor from "shared/components/TextEditor.vue"
import { errorCode, errorMessage } from "utils/api" import { errorMessage } from "utils/api"
import { DIFFICULTY, STORAGE_KEY } from "utils/constants" import { DIFFICULTY, STORAGE_KEY } from "utils/constants"
import download from "utils/download" import download from "utils/download"
import { createZipBlob, parseTime, unique } from "utils/functions" import { createZipBlob, parseTime, unique } from "utils/functions"
@@ -24,6 +23,15 @@ import CaseTable from "./components/CaseTable.vue"
import CodePreview from "./components/CodePreview.vue" import CodePreview from "./components/CodePreview.vue"
import PreviewStrip from "./components/PreviewStrip.vue" import PreviewStrip from "./components/PreviewStrip.vue"
import { usePreviewCollapsed } from "./components/previewCollapse" import { usePreviewCollapsed } from "./components/previewCollapse"
import {
cleanTags,
difficultyOptions,
isBlankHtml,
problemListRoute,
saveErrorText,
tagTooLongText,
useCreateDraft,
} from "./components/problemForm"
import { import {
caseFiles, caseFiles,
caseStatus, caseStatus,
@@ -135,14 +143,6 @@ const blankForm = (): CodeForm => ({
const form = reactive<CodeForm>(blankForm()) const form = reactive<CodeForm>(blankForm())
/** 富文本编辑器清空之后留下的是一个空段落,不是空串 */
const isBlankHtml = (html: string) => !html.trim() || html === "<p><br></p>"
const difficultyOptions = (["Low", "Mid", "High"] as const).map((value) => ({
label: DIFFICULTY[value],
value,
}))
const NO_INPUT = "<p>没有输入</p>" const NO_INPUT = "<p>没有输入</p>"
// ---------------------------------------------------------------- 标签 // ---------------------------------------------------------------- 标签
@@ -176,13 +176,12 @@ function toggleKnowledge(name: string) {
} }
function onOtherTagsChange(tags: string[]) { function onOtherTagsChange(tags: string[]) {
const tooLong = tags.find((t) => t.length > PROBLEM_TAG_MAX_LENGTH) const cleaned = cleanTags(tags)
if (tooLong) { if ("tooLong" in cleaned) {
message.error(`标签最多 ${PROBLEM_TAG_MAX_LENGTH} 个字:${tooLong}`) message.error(tagTooLongText(cleaned.tooLong))
return return
} }
const others = unique(tags.map((t) => t.trim()).filter(Boolean)) form.tags = unique([...form.tags.filter((t) => knowledgeSet.value.has(t)), ...cleaned.tags])
form.tags = unique([...form.tags.filter((t) => knowledgeSet.value.has(t)), ...others])
} }
// ---------------------------------------------------------------- 标准答案 // ---------------------------------------------------------------- 标准答案
@@ -525,11 +524,6 @@ interface Draft {
rows: { input: string; output: string; example: boolean }[] rows: { input: string; output: string; example: boolean }[]
} }
// 新建时存草稿(编辑页不存:原来编辑页也往同一个 key 里写,没保存就离开的话,
// 那道题的 id 会跟着草稿漂到新建页)。初值 null 时 VueUse 推不出类型,得显式指定
const draft = useLocalStorage<Draft | null>(STORAGE_KEY.ADMIN_CODE_PROBLEM, null, {
serializer: StorageSerializers.object,
})
const ready = ref(false) const ready = ref(false)
const draftRestored = ref(false) const draftRestored = ref(false)
@@ -543,25 +537,21 @@ function snapshot() {
} }
let baseline = "" let baseline = ""
watchDebounced( // 新建时存草稿(编辑页不存,见 useCreateDraft)
() => [form, rows.value.map((r) => [r.input, r.output, r.example])] as const, const draft = useCreateDraft<Draft>(STORAGE_KEY.ADMIN_CODE_PROBLEM, {
() => { source: () => [form, rows.value.map((r) => [r.input, r.output, r.example])] as const,
if (!ready.value || !isCreate.value) return enabled: () => ready.value && isCreate.value,
const empty = empty: () =>
!form._id && !form._id &&
!form.title && !form.title &&
isBlankHtml(form.description) && isBlankHtml(form.description) &&
!RUNNABLE.some((l) => form.answers[l].trim()) && !RUNNABLE.some((l) => form.answers[l].trim()) &&
!rows.value.some((r) => r.input.trim() || r.output.trim()) !rows.value.some((r) => r.input.trim() || r.output.trim()),
draft.value = empty take: () => ({
? null form: JSON.parse(JSON.stringify(form)),
: { rows: rows.value.map((r) => ({ input: r.input, output: r.output, example: r.example })),
form: JSON.parse(JSON.stringify(form)), }),
rows: rows.value.map((r) => ({ input: r.input, output: r.output, example: r.example })), })
}
},
{ debounce: 800, deep: true },
)
function clearDraft() { function clearDraft() {
draft.value = null draft.value = null
@@ -841,9 +831,7 @@ async function save() {
: { name: "admin problem edit", params: { problemID: created.id } }, : { name: "admin problem edit", params: { problemID: created.id } },
) )
} catch (err) { } catch (err) {
message.error( message.error(saveErrorText(err))
errorCode(err) === "display-id-exists" ? "显示编号重复了,换一个" : errorMessage(err),
)
} finally { } finally {
saving.value = false saving.value = false
} }
@@ -866,11 +854,7 @@ onBeforeRouteLeave(() => {
}) })
}) })
const backTo = computed(() => const backTo = computed(() => problemListRoute(props.contestID))
props.contestID
? { name: "admin contest problem list", params: { contestID: props.contestID } }
: { name: "admin problem list" },
)
const authorLine = computed(() => const authorLine = computed(() =>
props.initial props.initial
+1 -1
View File
@@ -9,7 +9,7 @@ import { batchSetProblemVisible, getProblemList, toggleProblemVisible } from "..
import Actions from "./components/Actions.vue" import Actions from "./components/Actions.vue"
import AddFromLibraryModal from "./components/AddFromLibraryModal.vue" import AddFromLibraryModal from "./components/AddFromLibraryModal.vue"
import { useRouteQuery } from "@vueuse/router" import { useRouteQuery } from "@vueuse/router"
import AuthorSelect from "shared/components/AuthorSelect.vue" import AuthorSelect from "admin/components/AuthorSelect.vue"
import type { DataTableRowKey } from "naive-ui" import type { DataTableRowKey } from "naive-ui"
import BatchTagModal from "./components/BatchTagModal.vue" import BatchTagModal from "./components/BatchTagModal.vue"
import CreateProblemMenu from "./components/CreateProblemMenu.vue" import CreateProblemMenu from "./components/CreateProblemMenu.vue"
+1 -10
View File
@@ -1,17 +1,8 @@
import type { MarkAchievementsRead } from "@oj2/contract"
import api from "utils/api" import api from "utils/api"
import type { AchievementList, PendingAchievement } from "utils/types" import type { AchievementList } from "utils/types"
export function getAchievements(name?: string) { export function getAchievements(name?: string) {
return api.get<AchievementList>("achievements", { return api.get<AchievementList>("achievements", {
params: name ? { username: name } : {}, params: name ? { username: name } : {},
}) })
} }
export function getPendingAchievements() {
return api.get<PendingAchievement[]>("achievements/pending")
}
export function markAchievementsRead(ids: number[]) {
return api.post("achievements/pending/read", { ids } satisfies MarkAchievementsRead)
}
+2 -29
View File
@@ -23,7 +23,6 @@ import {
type ProfileOverview, type ProfileOverview,
type TutorialSummary, type TutorialSummary,
type Tutorial, type Tutorial,
type ClassActivity,
type ClassToday, type ClassToday,
type LastVisit, type LastVisit,
type ClassBoard, type ClassBoard,
@@ -40,10 +39,8 @@ import {
type WeeklyRank, type WeeklyRank,
type ProblemRank, type ProblemRank,
type CreateSubmissionResponse, type CreateSubmissionResponse,
type ProblemAuthor,
type ProblemListItem, type ProblemListItem,
type ProblemStats, type ProblemStats,
type ProblemList,
type ProblemProgress, type ProblemProgress,
type CreateFlowchartResponse, type CreateFlowchartResponse,
type FlowchartScores, type FlowchartScores,
@@ -64,7 +61,7 @@ import {
} from "@oj2/contract" } from "@oj2/contract"
import api from "utils/api" import api from "utils/api"
import { contract } from "utils/contract" import { contract } from "utils/contract"
import { toProblemRow } from "oj/transforms" import { toProblemRow } from "utils/transforms"
import type { import type {
Announcement, Announcement,
AnnouncementListItem, AnnouncementListItem,
@@ -82,26 +79,12 @@ import type {
/** /**
* 题目详情 / 提交详情的 schema 按需加载,和请求并行发出。静态 import 会把 zod * 题目详情 / 提交详情的 schema 按需加载,和请求并行发出。静态 import 会把 zod
* 运行时带进首页 —— 题目列表的 getProblemList 也在这个文件里。 * 运行时带进首页 —— 首页、题目列表都 import 这个文件。
* 闸门本身仍是同步的(等 schema 到了再返回),submissionDetailSchema 有 `.catch()`, * 闸门本身仍是同步的(等 schema 到了再返回),submissionDetailSchema 有 `.catch()`,
* 放行原文和解析结果不等价,所以不能像 /me 那样改成 contractDeferred。 * 放行原文和解析结果不等价,所以不能像 /me 那样改成 contractDeferred。
*/ */
const loadDetailSchemas = () => import("./detailSchemas") const loadDetailSchemas = () => import("./detailSchemas")
export async function getProblemList(
offset = 0,
limit = 10,
searchParams: Record<string, unknown> = {},
) {
const res = await api.get<ProblemList>("problems", {
params: { paging: true, offset, limit, ...searchParams },
})
return {
results: res.results.map(toProblemRow),
total: res.total,
}
}
/** 题目列表顶上那行和左栏的进度;没登录是 null */ /** 题目列表顶上那行和左栏的进度;没登录是 null */
export function getProblemProgress() { export function getProblemProgress() {
return api.get<ProblemProgress | null>("problems/progress") return api.get<ProblemProgress | null>("problems/progress")
@@ -115,12 +98,6 @@ export function getRandomProblem(params: { tag: string; type: string; language:
return api.get<string>("problems/random", { params }) return api.get<string>("problems/random", { params })
} }
export function getAuthors(all = false) {
return api.get<ProblemAuthor[]>("problem-authors", {
params: { all: all ? "1" : "0" },
})
}
export async function getProblem(problemID: string, contestID: string) { export async function getProblem(problemID: string, contestID: string) {
const endpoint = contestID const endpoint = contestID
? `contests/${encodeURIComponent(contestID)}/problems/${encodeURIComponent(problemID)}` ? `contests/${encodeURIComponent(contestID)}/problems/${encodeURIComponent(problemID)}`
@@ -316,10 +293,6 @@ export function getKnowledgeMap() {
return api.get<KnowledgeMap>("me/knowledge") return api.get<KnowledgeMap>("me/knowledge")
} }
export function getClassActivity() {
return api.get<ClassActivity>("me/class-activity")
}
/** 老师首页「今天的课」:今天在上课的每个班 */ /** 老师首页「今天的课」:今天在上课的每个班 */
export function getClassToday() { export function getClassToday() {
return api.get<ClassToday>("classroom/today") return api.get<ClassToday>("classroom/today")
+1 -1
View File
@@ -15,7 +15,7 @@ import { LANGUAGE_SHOW_VALUE } from "utils/constants"
import { parseTime } from "utils/functions" import { parseTime } from "utils/functions"
import type { ClassBoard, ClassBoardCell, ClassBoardProblem, ClassBoardStudent } from "utils/types" import type { ClassBoard, ClassBoardCell, ClassBoardProblem, ClassBoardStudent } from "utils/types"
import ExplainModal, { type ExplainPerson } from "./components/ExplainModal.vue" import ExplainModal, { type ExplainPerson } from "./components/ExplainModal.vue"
import { errorKind, type ErrorKind } from "./errorKind" import { errorKind, type ErrorKind } from "utils/errorKind"
/** /**
* 课堂看板(老师,设计稿「课堂看板重设计」A+B):这个班、今天、这节课的几道题 × 全班学生。 * 课堂看板(老师,设计稿「课堂看板重设计」A+B):这个班、今天、这节课的几道题 × 全班学生。
+1 -1
View File
@@ -3,7 +3,6 @@ import { continueStep, doneCount, lessonPath, splitTitle, type TutorialType } fr
import { Icon } from "@iconify/vue" import { Icon } from "@iconify/vue"
import { import {
getAnnouncementList, getAnnouncementList,
getClassActivity,
getKnowledgeMap, getKnowledgeMap,
getContestList, getContestList,
getLastVisit, getLastVisit,
@@ -12,6 +11,7 @@ import {
getTutorials, getTutorials,
getWeeklyRank, getWeeklyRank,
} from "oj/api" } from "oj/api"
import { getClassActivity } from "shared/api"
import SubmissionResultTag from "shared/components/SubmissionResultTag.vue" import SubmissionResultTag from "shared/components/SubmissionResultTag.vue"
import { pickHeadline, type Headline } from "oj/user/knowledge" import { pickHeadline, type Headline } from "oj/user/knowledge"
import { useUserStore } from "shared/store/user" import { useUserStore } from "shared/store/user"
@@ -5,7 +5,7 @@ import { errorMessage } from "utils/api"
import { SubmissionStatus } from "utils/constants" import { SubmissionStatus } from "utils/constants"
import { trialRun } from "utils/judge" import { trialRun } from "utils/judge"
import { explainCCompileError } from "oj/problem/utils/cError" import { explainCCompileError } from "oj/problem/utils/cError"
import { explainPythonCompileError } from "oj/problem/utils/pythonError" import { explainPythonCompileError } from "utils/pythonError"
import { explainRuntimeError } from "oj/problem/utils/runtimeError" import { explainRuntimeError } from "oj/problem/utils/runtimeError"
import { useAuthModalStore } from "shared/store/authModal" import { useAuthModalStore } from "shared/store/authModal"
import { useUserStore } from "shared/store/user" import { useUserStore } from "shared/store/user"
@@ -10,7 +10,7 @@ import {
showPunctuationMarks, showPunctuationMarks,
} from "oj/problem/utils/errorMark" } from "oj/problem/utils/errorMark"
import { explainCCompileError } from "oj/problem/utils/cError" import { explainCCompileError } from "oj/problem/utils/cError"
import { explainPythonCompileError } from "oj/problem/utils/pythonError" import { explainPythonCompileError } from "utils/pythonError"
/** /**
* 编译错误的中文说明卡片:Python 的语法错误走 pythonError.ts,C / C++ 的 gcc 报错走 * 编译错误的中文说明卡片:Python 的语法错误走 pythonError.ts,C / C++ 的 gcc 报错走
@@ -4,7 +4,7 @@ import type { RouteLocationRaw } from "vue-router"
import { getProblemSetDetail, getProblemSetProblems, joinProblemSet } from "oj/api" import { getProblemSetDetail, getProblemSetProblems, joinProblemSet } from "oj/api"
import { useContestStore } from "oj/store/contest" import { useContestStore } from "oj/store/contest"
import { isFlowchartPass, useFlowchartStore } from "oj/store/flowchart" import { isFlowchartPass, useFlowchartStore } from "oj/store/flowchart"
import { useLessonStore } from "oj/store/lesson" import { useLessonStore } from "shared/store/lesson"
import { useProblemStore } from "oj/store/problem" import { useProblemStore } from "oj/store/problem"
import { useUserStore } from "shared/store/user" import { useUserStore } from "shared/store/user"
import { useBreakpoints } from "shared/composables/breakpoints" import { useBreakpoints } from "shared/composables/breakpoints"
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useThemeVars } from "naive-ui" import { useThemeVars } from "naive-ui"
import { useLessonStore } from "oj/store/lesson" import { useLessonStore } from "shared/store/lesson"
/** /**
* 通过之后告诉学生「这节课的下一题是哪道」。 * 通过之后告诉学生「这节课的下一题是哪道」。
@@ -18,7 +18,7 @@ import { useCollabHere, useTeacherCollab } from "../composables/teacherCollab"
import { useProblemPageContext } from "../composables/problemPageContext" import { useProblemPageContext } from "../composables/problemPageContext"
import CollabBar from "./CollabBar.vue" import CollabBar from "./CollabBar.vue"
import { useFlowchartStore } from "oj/store/flowchart" import { useFlowchartStore } from "oj/store/flowchart"
import { useLessonStore } from "oj/store/lesson" import { useLessonStore } from "shared/store/lesson"
const FlowchartEditor = defineAsyncComponent( const FlowchartEditor = defineAsyncComponent(
() => import("shared/components/FlowchartEditor/index.vue"), () => import("shared/components/FlowchartEditor/index.vue"),
@@ -5,7 +5,7 @@ import { getReaction } from "oj/api"
import { useProblemStore } from "oj/store/problem" import { useProblemStore } from "oj/store/problem"
import { useSubmissionStore } from "oj/store/submission" import { useSubmissionStore } from "oj/store/submission"
import { useFireworks } from "oj/problem/composables/useFireworks" import { useFireworks } from "oj/problem/composables/useFireworks"
import { useLessonStore } from "oj/store/lesson" import { useLessonStore } from "shared/store/lesson"
import { isFlowchartPass, useFlowchartStore } from "oj/store/flowchart" import { isFlowchartPass, useFlowchartStore } from "oj/store/flowchart"
import { SubmissionStatus } from "utils/constants" import { SubmissionStatus } from "utils/constants"
import { useProblemPageContext } from "../composables/problemPageContext" import { useProblemPageContext } from "../composables/problemPageContext"
@@ -21,7 +21,7 @@ import WrongAnswerExplain from "./WrongAnswerExplain.vue"
import LessonNext from "./LessonNext.vue" import LessonNext from "./LessonNext.vue"
import ResultHeader from "./ResultHeader.vue" import ResultHeader from "./ResultHeader.vue"
import SimilarProblems from "./SimilarProblems.vue" import SimilarProblems from "./SimilarProblems.vue"
import { useLessonStore } from "oj/store/lesson" import { useLessonStore } from "shared/store/lesson"
import { useProblemPageContext } from "../composables/problemPageContext" import { useProblemPageContext } from "../composables/problemPageContext"
import Markdown from "shared/components/Markdown.vue" import Markdown from "shared/components/Markdown.vue"
@@ -5,12 +5,7 @@ import { useThemeVars } from "naive-ui"
import { useProblemStore } from "oj/store/problem" import { useProblemStore } from "oj/store/problem"
import { markdownToText } from "oj/problem/utils/plainText" import { markdownToText } from "oj/problem/utils/plainText"
import { JUDGE_STATUS, SubmissionStatus } from "utils/constants" import { JUDGE_STATUS, SubmissionStatus } from "utils/constants"
import { import { classifyDiff, explainDiff, firstDifferentLine, type SampleCheck } from "utils/wrongAnswer"
classifyDiff,
explainDiff,
firstDifferentLine,
type SampleCheck,
} from "oj/problem/utils/wrongAnswer"
/** /**
* 答案错误时的样例对比(数据见后端 judge/run.ts 的 checkSamples)。 * 答案错误时的样例对比(数据见后端 judge/run.ts 的 checkSamples)。
+2 -1
View File
@@ -3,7 +3,8 @@ import { Icon } from "@iconify/vue"
import type { ProblemProgress, ProblemTypeFilter, Tag } from "@oj2/contract" import type { ProblemProgress, ProblemTypeFilter, Tag } from "@oj2/contract"
import { useRouteQuery } from "@vueuse/router" import { useRouteQuery } from "@vueuse/router"
import { useThemeVars } from "naive-ui" import { useThemeVars } from "naive-ui"
import { getAuthors, getProblemList, getProblemProgress, getRandomProblem } from "oj/api" import { getProblemProgress, getRandomProblem } from "oj/api"
import { getAuthors, getProblemList } from "shared/api"
import { useCodeStore } from "oj/store/code" import { useCodeStore } from "oj/store/code"
import { useTone } from "shared/composables/tone" import { useTone } from "shared/composables/tone"
import { STORAGE_KEY } from "utils/constants" import { STORAGE_KEY } from "utils/constants"
+1 -1
View File
@@ -1,5 +1,5 @@
import { findChinesePunctuation } from "./chinesePunctuation" import { findChinesePunctuation } from "./chinesePunctuation"
import { PUNCTUATION_NAMES, type PythonErrorExplanation } from "./pythonError" import { PUNCTUATION_NAMES, type PythonErrorExplanation } from "utils/pythonError"
/** /**
* 把 C / C++ 的编译错误(gcc 的原文)翻成中文,和 pythonError.ts 同一个思路: * 把 C / C++ 的编译错误(gcc 的原文)翻成中文,和 pythonError.ts 同一个思路:
@@ -1,4 +1,4 @@
import { PUNCTUATION_MAP } from "./pythonError" import { PUNCTUATION_MAP } from "utils/pythonError"
export interface PunctuationFix { export interface PunctuationFix {
from: number from: number
@@ -3,7 +3,7 @@ import type { RankRow, WeeklyChampion } from "@oj2/contract"
import { useThemeVars } from "naive-ui" import { useThemeVars } from "naive-ui"
import UserName from "shared/components/UserName.vue" import UserName from "shared/components/UserName.vue"
import { parseTime } from "utils/functions" import { parseTime } from "utils/functions"
import RankAvatar from "./RankAvatar.vue" import RankAvatar from "shared/components/RankAvatar.vue"
/** 本班最近几周的每周冠军,底下再挂这张榜上「进步最大」的那个人 */ /** 本班最近几周的每周冠军,底下再挂这张榜上「进步最大」的那个人 */
defineProps<{ defineProps<{
@@ -9,7 +9,7 @@ import { useBackClose } from "shared/composables/backClose"
import { useBreakpoints } from "shared/composables/breakpoints" import { useBreakpoints } from "shared/composables/breakpoints"
import { parseTime } from "utils/functions" import { parseTime } from "utils/functions"
import { useRankPalette } from "../palette" import { useRankPalette } from "../palette"
import RankAvatar from "./RankAvatar.vue" import RankAvatar from "shared/components/RankAvatar.vue"
// Markdown 连着整套代码高亮(两百多 KB),这里要等点开 / AI 写完才用得上,别让它拖慢页面 // Markdown 连着整套代码高亮(两百多 KB),这里要等点开 / AI 写完才用得上,别让它拖慢页面
const Markdown = defineAsyncComponent(() => import("shared/components/Markdown.vue")) const Markdown = defineAsyncComponent(() => import("shared/components/Markdown.vue"))
+1 -1
View File
@@ -4,7 +4,7 @@ import { useThemeVars } from "naive-ui"
import UserName from "shared/components/UserName.vue" import UserName from "shared/components/UserName.vue"
import { gapText, toPass } from "../utils" import { gapText, toPass } from "../utils"
import MoodPopover from "./MoodPopover.vue" import MoodPopover from "./MoodPopover.vue"
import RankAvatar from "./RankAvatar.vue" import RankAvatar from "shared/components/RankAvatar.vue"
export interface MiniRank { export interface MiniRank {
label: string label: string
@@ -4,7 +4,7 @@ import { useThemeVars } from "naive-ui"
import { useBreakpoints } from "shared/composables/breakpoints" import { useBreakpoints } from "shared/composables/breakpoints"
import UserName from "shared/components/UserName.vue" import UserName from "shared/components/UserName.vue"
import { useRankPalette } from "../palette" import { useRankPalette } from "../palette"
import RankAvatar from "./RankAvatar.vue" import RankAvatar from "shared/components/RankAvatar.vue"
/** /**
* 领奖台:前三名站在台阶上,第一名戴皇冠。赛道从第 4 名接着排,前三不再出现第二遍。 * 领奖台:前三名站在台阶上,第一名戴皇冠。赛道从第 4 名接着排,前三不再出现第二遍。
@@ -5,7 +5,7 @@ import UserName from "shared/components/UserName.vue"
import { useBreakpoints } from "shared/composables/breakpoints" import { useBreakpoints } from "shared/composables/breakpoints"
import { useRankPalette } from "../palette" import { useRankPalette } from "../palette"
import MoodPopover from "./MoodPopover.vue" import MoodPopover from "./MoodPopover.vue"
import RankAvatar from "./RankAvatar.vue" import RankAvatar from "shared/components/RankAvatar.vue"
/** /**
* 赛道:每人一根条,条越长做对越多,一样长的先做到的排前面(设计稿 G1 / G2)。 * 赛道:每人一根条,条越长做对越多,一样长的先做到的排前面(设计稿 G1 / G2)。
+1 -1
View File
@@ -3,7 +3,7 @@ import type { RankRow } from "@oj2/contract"
import { useThemeVars } from "naive-ui" import { useThemeVars } from "naive-ui"
import UserName from "shared/components/UserName.vue" import UserName from "shared/components/UserName.vue"
import { useRankPalette } from "../palette" import { useRankPalette } from "../palette"
import RankAvatar from "./RankAvatar.vue" import RankAvatar from "shared/components/RankAvatar.vue"
/** 这周本班前 5 名(做对过新题的),每周一清零 */ /** 这周本班前 5 名(做对过新题的),每周一清零 */
const props = defineProps<{ rows: RankRow[]; meId?: number; loaded: boolean }>() const props = defineProps<{ rows: RankRow[]; meId?: number; loaded: boolean }>()
@@ -1,5 +1,5 @@
import { explainCCompileError } from "oj/problem/utils/cError" import { explainCCompileError } from "oj/problem/utils/cError"
import { explainPythonCompileError } from "oj/problem/utils/pythonError" import { explainPythonCompileError } from "utils/pythonError"
import { explainRuntimeError } from "oj/problem/utils/runtimeError" import { explainRuntimeError } from "oj/problem/utils/runtimeError"
import { SubmissionStatus } from "utils/constants" import { SubmissionStatus } from "utils/constants"
import type { Submission } from "utils/types" import type { Submission } from "utils/types"
@@ -2,7 +2,7 @@
import { Icon } from "@iconify/vue" import { Icon } from "@iconify/vue"
import { useThemeVars } from "naive-ui" import { useThemeVars } from "naive-ui"
import { resetAvatar, uploadAvatar } from "oj/api" import { resetAvatar, uploadAvatar } from "oj/api"
import RankAvatar from "oj/rank/components/RankAvatar.vue" import RankAvatar from "shared/components/RankAvatar.vue"
import { useUserStore } from "shared/store/user" import { useUserStore } from "shared/store/user"
/** /**
@@ -3,7 +3,7 @@ import { MOOD_MAX, STUDENT_ROLES, type ProfileOverview } from "@oj2/contract"
import { Icon } from "@iconify/vue" import { Icon } from "@iconify/vue"
import { useThemeVars } from "naive-ui" import { useThemeVars } from "naive-ui"
import { clearMood, setRankHidden, updateProfile } from "oj/api" import { clearMood, setRankHidden, updateProfile } from "oj/api"
import RankAvatar from "oj/rank/components/RankAvatar.vue" import RankAvatar from "shared/components/RankAvatar.vue"
import { classLabel } from "utils/classes" import { classLabel } from "utils/classes"
import UserName from "shared/components/UserName.vue" import UserName from "shared/components/UserName.vue"
import { useUserStore } from "shared/store/user" import { useUserStore } from "shared/store/user"
+42 -2
View File
@@ -1,8 +1,16 @@
import type { LoginRequest, RegisterRequest } from "@oj2/contract" import type { LoginRequest, RegisterRequest } from "@oj2/contract"
import type { Quote, WebsiteConfig } from "@oj2/contract" import type {
ClassActivity,
MarkAchievementsRead,
ProblemAuthor,
ProblemList,
Quote,
WebsiteConfig,
} from "@oj2/contract"
import api from "utils/api" import api from "utils/api"
import { contractDeferred } from "utils/contract" import { contractDeferred } from "utils/contract"
import type { Profile, Tag } from "utils/types" import { toProblemRow } from "utils/transforms"
import type { PendingAchievement, Profile, Tag } from "utils/types"
export function login(data: { username: string; password: string }) { export function login(data: { username: string; password: string }) {
return api.post("auth/login", data satisfies LoginRequest) return api.post("auth/login", data satisfies LoginRequest)
@@ -51,3 +59,35 @@ export function getClassUsernames(classroom: string) {
export function getWebsiteConfig() { export function getWebsiteConfig() {
return api.get<WebsiteConfig>("site") return api.get<WebsiteConfig>("site")
} }
export async function getProblemList(
offset = 0,
limit = 10,
searchParams: Record<string, unknown> = {},
) {
const res = await api.get<ProblemList>("problems", {
params: { paging: true, offset, limit, ...searchParams },
})
return {
results: res.results.map(toProblemRow),
total: res.total,
}
}
export function getAuthors(all = false) {
return api.get<ProblemAuthor[]>("problem-authors", {
params: { all: all ? "1" : "0" },
})
}
export function getClassActivity() {
return api.get<ClassActivity>("me/class-activity")
}
export function getPendingAchievements() {
return api.get<PendingAchievement[]>("achievements/pending")
}
export function markAchievementsRead(ids: number[]) {
return api.post("achievements/pending/read", { ids } satisfies MarkAchievementsRead)
}
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from "@iconify/vue" import { Icon } from "@iconify/vue"
import { errorKind } from "oj/classroom/errorKind" import { errorKind } from "utils/errorKind"
import { useBreakpoints } from "shared/composables/breakpoints" import { useBreakpoints } from "shared/composables/breakpoints"
import { useTeacherHelp } from "shared/composables/teacherHelp" import { useTeacherHelp } from "shared/composables/teacherHelp"
import { useCollabStore } from "shared/store/collab" import { useCollabStore } from "shared/store/collab"
+1 -1
View File
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from "@iconify/vue" import { Icon } from "@iconify/vue"
import { useThemeVars } from "naive-ui" import { useThemeVars } from "naive-ui"
import RankAvatar from "oj/rank/components/RankAvatar.vue" import RankAvatar from "shared/components/RankAvatar.vue"
import { classLabel, groupClassOptions } from "utils/classes" import { classLabel, groupClassOptions } from "utils/classes"
import { errorCode } from "utils/api" import { errorCode } from "utils/api"
import { STORAGE_KEY } from "utils/constants" import { STORAGE_KEY } from "utils/constants"
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from "@iconify/vue" import { Icon } from "@iconify/vue"
import { useThemeVars } from "naive-ui" import { useThemeVars } from "naive-ui"
import { useLessonStore } from "oj/store/lesson" import { useLessonStore } from "shared/store/lesson"
import { useProblemJump } from "shared/composables/problemJump" import { useProblemJump } from "shared/composables/problemJump"
import { useUserStore } from "shared/store/user" import { useUserStore } from "shared/store/user"
import type { ClassActivityProblem } from "utils/types" import type { ClassActivityProblem } from "utils/types"
@@ -1,4 +1,4 @@
import { getProblemList } from "oj/api" import { getProblemList } from "shared/api"
/** /**
* 按题号直达。课上老师报一个题号、全班去找,是非比赛提交最主要的来路, * 按题号直达。课上老师报一个题号、全班去找,是非比赛提交最主要的来路,
+1 -1
View File
@@ -1,4 +1,4 @@
import { getPendingAchievements, markAchievementsRead } from "oj/achievement/api" import { getPendingAchievements, markAchievementsRead } from "shared/api"
import type { QueuedAchievement } from "utils/types" import type { QueuedAchievement } from "utils/types"
/** /**
@@ -1,5 +1,5 @@
import { defineStore } from "pinia" import { defineStore } from "pinia"
import { getClassActivity } from "oj/api" import { getClassActivity } from "shared/api"
import { LANGUAGE_SHOW_VALUE } from "utils/constants" import { LANGUAGE_SHOW_VALUE } from "utils/constants"
import type { ClassActivity, ClassActivityProblem } from "utils/types" import type { ClassActivity, ClassActivityProblem } from "utils/types"
@@ -1,4 +1,4 @@
import { classifyDiff, type DiffKind } from "oj/problem/utils/wrongAnswer" import { classifyDiff, type DiffKind } from "utils/wrongAnswer"
import { JUDGE_STATUS, SubmissionStatus } from "utils/constants" import { JUDGE_STATUS, SubmissionStatus } from "utils/constants"
import type { ClassBoardCell } from "utils/types" import type { ClassBoardCell } from "utils/types"
File renamed without changes.
@@ -1,5 +1,5 @@
import type { StatisticInfo } from "@oj2/contract" import type { StatisticInfo } from "@oj2/contract"
import { PUNCTUATION_MAP, PUNCTUATION_NAMES } from "./pythonError" import { PUNCTUATION_MAP, PUNCTUATION_NAMES } from "utils/pythonError"
/** /**
* 答案错误时,比较学生在**公开样例**上的输出和正确输出,归一类、给一句中文提示。 * 答案错误时,比较学生在**公开样例**上的输出和正确输出,归一类、给一句中文提示。