diff --git a/apps/web/src/oj/api.ts b/apps/web/src/oj/api.ts index b01da55..bd6fda6 100644 --- a/apps/web/src/oj/api.ts +++ b/apps/web/src/oj/api.ts @@ -10,6 +10,7 @@ import { type TutorialSummary, type ClassActivity, type ClassBoard, + type ClassLesson, type KnowledgeMap, type ClassComparisonResponse, type ClassRankItem, @@ -223,6 +224,11 @@ export function getClassBoard(className?: string) { return api.get("classroom/board", { params: { className } }) } +/** 这个班「这节课」的题(提交列表那颗按钮用)。不传班级就猜正在上课的班,和看板一样 */ +export function getClassLesson(className?: string) { + return api.get("classroom/lesson", { params: { className } }) +} + /** 给这个班布置今天的题,空数组 = 清掉 */ export function setClassLesson(className: string, problemDisplayIds: string[]) { return api.put("classroom/lesson", { className, problemDisplayIds }) @@ -412,6 +418,7 @@ export function getFlowchartSubmission(id: string) { export function getFlowchartSubmissions(params: { username?: string + className?: string problemDisplayId?: string myself?: string offset?: number diff --git a/apps/web/src/oj/classroom/board.vue b/apps/web/src/oj/classroom/board.vue index f3c6242..919e7bc 100644 --- a/apps/web/src/oj/classroom/board.vue +++ b/apps/web/src/oj/classroom/board.vue @@ -198,6 +198,24 @@ const GROUP_LABEL: Record< working: { text: "在做", type: "info" }, done: { text: "做完了", type: "success" }, } + +const router = useRouter() + +/** + * 点格子 / 名字到提交列表看他今天交的代码。新标签打开:看板多半正投在屏幕上,别把它顶掉。 + * 用户名按整名匹配(学号互相包含是常态,ks24a1 会混进 ks24a10) + */ +function submissionsHref(student: ClassBoardStudent, problemDisplayId?: string) { + return router.resolve({ + name: "submissions", + query: { + username: student.username, + exactUsername: "1", + today: "1", + ...(problemDisplayId ? { problem: problemDisplayId } : {}), + }, + }).href +} diff --git a/apps/web/src/oj/submission/components/FlowchartPane.vue b/apps/web/src/oj/submission/components/FlowchartPane.vue new file mode 100644 index 0000000..1fc9980 --- /dev/null +++ b/apps/web/src/oj/submission/components/FlowchartPane.vue @@ -0,0 +1,470 @@ + + + + + diff --git a/apps/web/src/oj/submission/components/FlowchartState.vue b/apps/web/src/oj/submission/components/FlowchartState.vue new file mode 100644 index 0000000..d116e32 --- /dev/null +++ b/apps/web/src/oj/submission/components/FlowchartState.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/apps/web/src/oj/submission/components/StatusPill.vue b/apps/web/src/oj/submission/components/StatusPill.vue new file mode 100644 index 0000000..a977799 --- /dev/null +++ b/apps/web/src/oj/submission/components/StatusPill.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/apps/web/src/oj/submission/components/SubmissionPane.vue b/apps/web/src/oj/submission/components/SubmissionPane.vue new file mode 100644 index 0000000..b4edc23 --- /dev/null +++ b/apps/web/src/oj/submission/components/SubmissionPane.vue @@ -0,0 +1,629 @@ + + + + + diff --git a/apps/web/src/oj/submission/components/UserName.vue b/apps/web/src/oj/submission/components/UserName.vue new file mode 100644 index 0000000..35bcb0e --- /dev/null +++ b/apps/web/src/oj/submission/components/UserName.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/apps/web/src/oj/submission/composables/copySubmission.ts b/apps/web/src/oj/submission/composables/copySubmission.ts new file mode 100644 index 0000000..f513505 --- /dev/null +++ b/apps/web/src/oj/submission/composables/copySubmission.ts @@ -0,0 +1,60 @@ +import { LANGUAGE_FORMAT_VALUE } from "utils/constants" +import { compressToBase64 } from "utils/functions" +import type { Submission } from "utils/types" +import { useCodeStore } from "oj/store/code" +import storage from "utils/storage" + +/** + * 「复制到自测猫」「复制回到题目」。提交列表的右栏和独立的 /submission/:id 页面共用。 + */ +export function useCopySubmission() { + const route = useRoute() + const router = useRouter() + const codeStore = useCodeStore() + + function copyToCat(submission: Submission) { + const data = { + lang: LANGUAGE_FORMAT_VALUE[submission.language], + code: submission.code, + input: "", + } + const base64 = compressToBase64(JSON.stringify(data)) + const url = `${import.meta.env.PUBLIC_CODE_URL}?share=${encodeURIComponent(base64)}` + window.open(url, "_blank") + } + + /** + * 编辑器的 storageKey 用 display id(problem._id),不是 submission.problemId + * (内部数字 id)。**不能只靠调用方传的题号** —— 独立的 /submission/:id 路由 + * 只有提交 id,原来会一路带进 router.push 抛 `Missing required param "problemID"`。 + * 响应里的 problemDisplayId 就是干这个的。 + * + * 比赛提交的 contestId 只下发给管理员;学生在比赛里复制,靠的是当前路由上的比赛 id。 + */ + function copyToProblem(submission: Submission) { + const { code, language } = submission + const problemID = submission.problemDisplayId + const contestId = + submission.contestId ?? (route.params.contestID ? Number(route.params.contestID) : null) + const storageKey = `problem_${problemID}_contest_${contestId || null}_lang_${language}` + storage.set(storageKey, code) + // 设置语言 + 代码:localStorage 覆盖全新挂载的编辑器, + // setCode 覆盖已挂载(同页 modal)的编辑器 + codeStore.setLanguage(language) + codeStore.setCode(code) + + const problemSetId = (route.params.problemSetId as string) ?? "" + if (contestId) { + router.push({ + name: "contest problem", + params: { contestID: String(contestId), problemID }, + }) + } else if (problemSetId) { + router.push({ name: "problemset problem", params: { problemSetId, problemID } }) + } else { + router.push({ name: "problem", params: { problemID } }) + } + } + + return { copyToCat, copyToProblem } +} diff --git a/apps/web/src/oj/submission/composables/tone.ts b/apps/web/src/oj/submission/composables/tone.ts new file mode 100644 index 0000000..f24b37a --- /dev/null +++ b/apps/web/src/oj/submission/composables/tone.ts @@ -0,0 +1,50 @@ +import { useThemeVars } from "naive-ui" +import { useDark } from "@vueuse/core" + +export type Tone = "success" | "error" | "warning" | "info" | "default" + +function rgba(hex: string, alpha: number) { + const value = hex.replace("#", "") + const full = + value.length === 3 + ? value + .split("") + .map((c) => c + c) + .join("") + : value.slice(0, 6) + const n = parseInt(full, 16) + return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})` +} + +/** + * 提交列表里各种小胶囊的配色:浅底 + 深一档的字(设计稿「提交列表重设计 · 定稿」)。 + * 从主题色里取,暗色下字换成主题色本身(深色字放在暗底上看不清)。 + * 不用 color-mix:Chrome 105 没有(111 才有),机房有一部分还是 105。 + */ +export function useTone() { + const theme = useThemeVars() + const isDark = useDark() + + return function tone(kind: Tone) { + const t = theme.value + const base = { + success: t.successColor, + error: t.errorColor, + warning: t.warningColor, + info: t.infoColor, + default: t.textColor3, + }[kind] + const pressed = { + success: t.successColorPressed, + error: t.errorColorPressed, + warning: t.warningColorPressed, + info: t.infoColorPressed, + default: t.textColor2, + }[kind] + return { + color: isDark.value ? base : pressed, + background: kind === "default" ? t.actionColor : rgba(base, isDark.value ? 0.16 : 0.12), + solid: base, + } + } +} diff --git a/apps/web/src/oj/submission/detail.vue b/apps/web/src/oj/submission/detail.vue index 5cba86c..b9664bf 100644 --- a/apps/web/src/oj/submission/detail.vue +++ b/apps/web/src/oj/submission/detail.vue @@ -8,13 +8,11 @@ import { submissionMemoryFormat, submissionResultTitle, submissionTimeFormat, - compressToBase64, } from "utils/functions" import type { Submission } from "utils/types" import SubmissionResultTag from "shared/components/SubmissionResultTag.vue" import { useBreakpoints } from "shared/composables/breakpoints" -import { useCodeStore } from "oj/store/code" -import storage from "utils/storage" +import { useCopySubmission } from "./composables/copySubmission" const props = defineProps<{ submissionID: string @@ -26,10 +24,6 @@ const props = defineProps<{ // 在弹框中使用时,父组件监听此事件关闭弹框,否则弹框会挡住已更新的编辑器 const emit = defineEmits<{ copied: [] }>() -const route = useRoute() -const router = useRouter() -const codeStore = useCodeStore() - const { isMobile, isDesktop } = useBreakpoints() const submission = ref() @@ -69,51 +63,14 @@ const columns: DataTableColumn[] = [ }, ] +const { copyToCat: catCopy, copyToProblem: problemCopy } = useCopySubmission() + function copyToCat() { - const lang = LANGUAGE_FORMAT_VALUE[submission.value!.language] - const data = { - lang, - code: submission.value!.code, - input: "", - } - const base64 = compressToBase64(JSON.stringify(data)) - const url = `${import.meta.env.PUBLIC_CODE_URL}?share=${encodeURIComponent(base64)}` - window.open(url, "_blank") + catCopy(submission.value!) } function copyToProblem() { - const { code, language, contestId } = submission.value! - // 编辑器的 storageKey 用 display id(problem._id),不是 submission.problemId - // (内部数字 id)。**不能只靠 props.problemID** —— 独立的 /submission/:id 路由 - // 只喂 submissionID,那个 prop 是 undefined,原来会一路带进 router.push 抛 - // `Missing required param "problemID"`。响应里的 problemDisplayId 就是干这个的。 - const problemID = props.problemID ?? submission.value!.problemDisplayId - const contestIDForKey = contestId || null - const storageKey = `problem_${problemID}_contest_${contestIDForKey}_lang_${language}` - storage.set(storageKey, code) - // 设置语言 + 代码:localStorage 覆盖全新挂载的编辑器, - // setCode 覆盖已挂载(同页 modal)的编辑器 - codeStore.setLanguage(language) - codeStore.setCode(code) - - const problemSetId = (route.params.problemSetId as string) ?? "" - if (contestId) { - router.push({ - name: "contest problem", - params: { contestID: String(contestId), problemID }, - }) - } else if (problemSetId) { - router.push({ - name: "problemset problem", - params: { problemSetId, problemID }, - }) - } else { - router.push({ - name: "problem", - params: { problemID }, - }) - } - + problemCopy(submission.value!) emit("copied") } diff --git a/apps/web/src/oj/submission/list.vue b/apps/web/src/oj/submission/list.vue index 83ffbf7..5bd768e 100644 --- a/apps/web/src/oj/submission/list.vue +++ b/apps/web/src/oj/submission/list.vue @@ -1,90 +1,110 @@ + + diff --git a/apps/web/src/oj/submission/utils.ts b/apps/web/src/oj/submission/utils.ts new file mode 100644 index 0000000..60edc43 --- /dev/null +++ b/apps/web/src/oj/submission/utils.ts @@ -0,0 +1,36 @@ +import { parseTime } from "utils/functions" + +/** + * 提交时间的显示:今天的只写钟点,别的日子带上月日,跨年再带年份。列表里一眼看的是 + * 「刚才」还是「以前」,完整的年月日时分秒只会把这一列撑宽。 + * 日历按东八区算(parseTime),不跟浏览器时区走。 + */ +export function submissionTimeText(time: string, withSeconds = false) { + const clock = parseTime(time, withSeconds ? "HH:mm:ss" : "HH:mm") + const day = parseTime(time, "YYYY-MM-DD") + const now = parseTime(new Date(), "YYYY-MM-DD") + if (day === now) return clock + if (day.slice(0, 4) === now.slice(0, 4)) return `${parseTime(time, "M月D日")} ${clock}` + return `${parseTime(time, "YYYY年M月D日")} ${clock}` +} + +/** 列表里每行只写钟点,日期在分隔行上(submissionDayText) */ +export function submissionClockText(time: string) { + return parseTime(time, "HH:mm") +} + +/** 分隔行上的日子:今天 / 昨天 / 9月28日(跨年带年份) */ +export function submissionDayText(time: string) { + const day = parseTime(time, "YYYY-MM-DD") + const now = parseTime(new Date(), "YYYY-MM-DD") + const yesterday = parseTime(new Date(Date.now() - 86_400_000), "YYYY-MM-DD") + if (day === now) return "今天" + if (day === yesterday) return `昨天 · ${parseTime(time, "M月D日")}` + if (day.slice(0, 4) === now.slice(0, 4)) return parseTime(time, "M月D日") + return parseTime(time, "YYYY年M月D日") +} + +/** 班级下拉的标签:`253` → `25计算机3班`,和统计面板、班级 PK 同一个写法 */ +export function classLabel(className: string) { + return `${className.slice(0, 2)}计算机${className.slice(2)}班` +} diff --git a/apps/web/src/utils/types.ts b/apps/web/src/utils/types.ts index bb74a2a..ea4c531 100644 --- a/apps/web/src/utils/types.ts +++ b/apps/web/src/utils/types.ts @@ -268,6 +268,8 @@ export interface SubmissionListPayload { username?: string /** "1" = username 精确匹配(默认是「包含」)。协作中老师看某个学生的提交用 */ exactUsername?: "1" + /** 班级(user.class_name,如 `253`),精确匹配;和 username 可以同时用 */ + className?: string contestId?: string problemDisplayId?: string language: LANGUAGE | "" @@ -293,7 +295,7 @@ export type { ClassComparison, ClassRankItem, ClassUserRank } from "@oj2/contrac export type { ClassActivity, ClassActivityProblem } from "@oj2/contract" /** 课堂看板:一个班今天这节课的题 × 全班学生 */ -export type { ClassBoard, ClassBoardStudent, ClassBoardCell } from "@oj2/contract" +export type { ClassBoard, ClassBoardStudent, ClassBoardCell, ClassLesson } from "@oj2/contract" /** 知识点地图:每个知识点做对几道、在第几档,只给自己看 */ export type { KnowledgeLevel, KnowledgeMap } from "@oj2/contract"