chore(格式): Prettier 统一到全仓,后端和契约一次性格式化
Some checks failed
Deploy / deploy (push) Has been cancelled

原来只有 `apps/web` 在 Prettier 下(配置在 `apps/web/.prettierrc.toml`、脚本在
web 的 package.json),后端和契约从来没格式化过 —— 手写在 100 列上下,`db/schema.ts`
还是 drizzle-kit pull 留下的 tab 缩进。两套口径分叉久了,跨端改一处就得记着「这边
什么风格」。

- 配置搬到根目录 `.prettierrc.toml`,内容不变(`semi=false`,其余全默认,
  printWidth 80 —— 和前端已有的格式一致,不另立一套宽度);
- 脚本统一成根目录 `bun run fmt`,覆盖 `apps/*/src`、`apps/web/tests` 和两个构建
  配置;web 自己那份 `fmt` 和重复的 prettier 依赖删掉;
- `.prettierignore` 挡掉两类不该碰的:drizzle-kit 生成的 `src/db/meta/` 结构快照
  (它是 db:generate 的比对输入,只该由 drizzle-kit 写)、unplugin 每次 dev 都会
  重写的 `auto-imports.d.ts` / `components.d.ts`;
- 全量跑了一遍。纯格式,无行为改动:api typecheck / check:routes / check:ast、
  前端 type-check 全过,起 api 打了接口确认正常。前端这 39 个文件的小改动是
  prettier 版本漂移(类型断言的换行口径变了),不是新配置带来的。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 08:27:34 -06:00
parent e600fd24cf
commit ed56a209ea
122 changed files with 10557 additions and 4843 deletions

View File

@@ -94,9 +94,7 @@ export function editProblem(problem: AdminProblem | BlankProblem) {
}
export function toggleProblemVisible(problemID: number) {
return api.put<{ visible: boolean }>(
`admin/problems/${problemID}/visibility`,
)
return api.put<{ visible: boolean }>(`admin/problems/${problemID}/visibility`)
}
export function generateFlowchartFromPythonCode(python: string) {
@@ -135,7 +133,11 @@ export function batchTagProblems(
}
// 用户排名(后台版,无 100 名上限;公开榜单是 oj/api.ts 的 getRank
export function getAdminUserRank(offset: number, limit: number, keyword: string) {
export function getAdminUserRank(
offset: number,
limit: number,
keyword: string,
) {
return api.get<AdminUserRank>("admin/rankings/users", {
params: { offset, limit, keyword },
})
@@ -236,9 +238,7 @@ export function previewSQLTestcase(data: {
// 回显已上传的 SQL 测试点脚本内容(按 1.sql, 2.sql... 排序)
export function getSQLTestcaseScripts(problemId: number) {
return api.get<SqlTestCaseScript[]>(
`admin/problems/${problemId}/sql-scripts`,
)
return api.get<SqlTestCaseScript[]>(`admin/problems/${problemId}/sql-scripts`)
}
// AI 根据标准答案生成一个 SQL 测试点初始化脚本
@@ -412,10 +412,7 @@ export function createTutorial(data: Partial<Tutorial>) {
}
export function updateTutorial(data: Partial<Tutorial>) {
return api.put<Tutorial>(
`admin/tutorials/${data.id}`,
toTutorialBody(data),
)
return api.put<Tutorial>(`admin/tutorials/${data.id}`, toTutorialBody(data))
}
export function deleteTutorial(id: number) {

View File

@@ -168,10 +168,14 @@ const tutorialColumns = computed<DataTableColumn<LearnTutorialProgress>[]>(
const exerciseColumns = computed<DataTableColumn<LearnExerciseProgress>[]>(
() => [
{ type: "expand", renderExpand: (row) => h(ExerciseAttempts, {
exerciseId: row.exerciseId,
className: className.value.trim(),
}) },
{
type: "expand",
renderExpand: (row) =>
h(ExerciseAttempts, {
exerciseId: row.exerciseId,
className: className.value.trim(),
}),
},
{
title: "课",
key: "tutorialOrder",
@@ -289,7 +293,8 @@ onMounted(load)
</n-text>
<!-- 口径写在表上方免得老师对着已读 0 / 累计 25 分钟猜是不是坏了 -->
<n-text depth="3" style="font-size: 12px">
已读按累计停留满 {{ TUTORIAL_READ_SECONDS / 60 }} 分钟算不足的只计时长
已读按累计停留满
{{ TUTORIAL_READ_SECONDS / 60 }} 分钟算不足的只计时长
</n-text>
</n-flex>

View File

@@ -78,7 +78,10 @@ function nodeTargetOptions(lang: string): SelectOption[] {
// 逻辑名 and/or/not 在 C 里显示成 && / || / !,存进去的还是逻辑名
function operatorTargetOptions(lang: string): SelectOption[] {
return Object.entries(AST_OPERATOR_TARGETS_BY_LANGUAGE[lang] ?? {}).map(
([value, label]) => ({ label: label === value ? value : `${label}${value}`, value }),
([value, label]) => ({
label: label === value ? value : `${label}${value}`,
value,
}),
)
}
@@ -173,7 +176,8 @@ function getTargetLabel(
engine: string,
target: string,
): string | undefined {
if (isNodeEngine(engine)) return AST_NODE_TARGETS_BY_LANGUAGE[lang]?.[target]?.label
if (isNodeEngine(engine))
return AST_NODE_TARGETS_BY_LANGUAGE[lang]?.[target]?.label
// 运算符不写 label判题结果的文案按语言翻译astOperatorLabel
// 存一个固定 label 反而会把 C 的 && 钉死成 and
return undefined
@@ -252,7 +256,8 @@ watch(supportedLanguages, (langs) => {
:bordered="false"
style="margin-bottom: 8px"
>
{{ unsupportedLanguages.join("、") }} 暂不支持代码规则检查判题机只能检查
{{ unsupportedLanguages.join("、") }}
暂不支持代码规则检查判题机只能检查
{{ AST_SUPPORTED_LANGUAGES.join(" / ") }}
</n-alert>
<n-tabs
@@ -393,9 +398,7 @@ watch(supportedLanguages, (langs) => {
<n-empty
v-else
:description="
languages.length
? '当前语言不支持代码规则检查'
: '请先选择编程语言'
languages.length ? '当前语言不支持代码规则检查' : '请先选择编程语言'
"
/>
</n-collapse-item>

View File

@@ -59,9 +59,7 @@ async function submit() {
props.action,
)
const verb = props.action === "add" ? "添加" : "移除"
message.success(
`已为 ${res.problemCount} 道题${verb} ${res.tagCount} 个标签`,
)
message.success(`已为 ${res.problemCount} 道题${verb} ${res.tagCount} 个标签`)
close()
emit("done")
}

View File

@@ -179,11 +179,10 @@ async function run() {
async function upload() {
isUploading.value = true
try {
const data = uploadable.value
.flatMap((f, i) => [
{ name: `${i + 1}.in`, content: f.in },
{ name: `${i + 1}.out`, content: f.out },
])
const data = uploadable.value.flatMap((f, i) => [
{ name: `${i + 1}.in`, content: f.in },
{ name: `${i + 1}.out`, content: f.out },
])
const blob = createZipBlob(data)
const file = new File([blob], "testcase.zip", { type: "application/zip" })

View File

@@ -896,7 +896,11 @@ watch(
v-model:value="problem.showFlowchart"
:disabled="problem.allowFlowchart"
/>
<n-text v-if="problem.allowFlowchart" depth="3" style="font-size: 12px">
<n-text
v-if="problem.allowFlowchart"
depth="3"
style="font-size: 12px"
>
让学生自己画图时标准流程图不会下发给学生这个开关没有意义
</n-text>
</n-flex>

View File

@@ -122,9 +122,7 @@ async function saveTag(tag: AdminTag) {
}
const res = await renameTag(tag.id, name)
if (res.merged) {
message.success(
`已合并到「${res.name}」,影响 ${res.affectedCount} 道题`,
)
message.success(`已合并到「${res.name}」,影响 ${res.affectedCount} 道题`)
} else {
message.success("已重命名")
}

View File

@@ -110,9 +110,7 @@ function startRolling(finalName: string) {
async function getRandom() {
const res = await randomUser10(query.classroom)
const names = (res as string[]).map(
(name) => name.split(query.classroom)[1],
)
const names = (res as string[]).map((name) => name.split(query.classroom)[1])
rollingNames.value = names
const finalName = names[names.length - 1]
startRolling(finalName)

View File

@@ -23,8 +23,7 @@ defineEmits<{
*/
const maskable = computed(
() =>
props.user.adminType !== USER_TYPE.REGULAR_USER &&
!!props.user.rawPassword,
props.user.adminType !== USER_TYPE.REGULAR_USER && !!props.user.rawPassword,
)
</script>
<template>

View File

@@ -57,7 +57,9 @@ async function uploadUsers() {
message.success("用户已上传成功")
// 只导出用户名和密码两列 —— 发给学生的就是这两样,邮箱是占位生成的、
// 真名本来就是老师粘进来的那一列,都不用回传
const csv = users.value.map(([username, password]) => `${username},${password}`).join("\n")
const csv = users.value
.map(([username, password]) => `${username},${password}`)
.join("\n")
const hiddenElement = document.createElement("a")
hiddenElement.href = "data:text/csv;charset=utf-8," + encodeURI(csv)
hiddenElement.target = "_blank"

View File

@@ -77,7 +77,8 @@ const columns: DataTableColumn<User>[] = [
user: row,
revealed: revealedPasswords.value.has(row.id),
onToggle: (id: number) => {
if (!revealedPasswords.value.delete(id)) revealedPasswords.value.add(id)
if (!revealedPasswords.value.delete(id))
revealedPasswords.value.add(id)
},
}),
},

View File

@@ -74,7 +74,9 @@ aiStore.targetUsername = urlUsername.value
aiStore.duration = urlDuration.value
const subOptions = computed<Duration>(
() => durationFromValue(aiStore.duration) ?? durationFromValue(DURATION_OPTIONS[0].value)!,
() =>
durationFromValue(aiStore.duration) ??
durationFromValue(DURATION_OPTIONS[0].value)!,
)
const start = computed(() => formatISO(sub(new Date(), subOptions.value)))

View File

@@ -31,7 +31,8 @@ const { chartKey } = useChartTheme()
// 第二个 tab 里列成表格。这里直接画出来,不用改后端和契约
const items = computed(() =>
[...aiStore.detailsData.flowcharts].sort(
(a, b) => b.bestScore - a.bestScore || a.problemId.localeCompare(b.problemId),
(a, b) =>
b.bestScore - a.bestScore || a.problemId.localeCompare(b.problemId),
),
)

View File

@@ -255,12 +255,9 @@ export function getContestAccess(id: string) {
// 注意和 GET /access 不一样:这个返回裸 true密码错是 403 走 catch
export function checkContestPassword(contestID: string, password: string) {
return api.post<boolean>(
`contests/${encodeURIComponent(contestID)}/access`,
{
password,
},
)
return api.post<boolean>(`contests/${encodeURIComponent(contestID)}/access`, {
password,
})
}
export async function getContestProblems(contestID: string) {
@@ -295,9 +292,12 @@ export function updateProfile(data: { realName: string; mood: string }) {
}
export function getAnnouncementList(offset = 0, limit = 10) {
return api.get<{ results: AnnouncementListItem[]; total: number }>("announcements", {
params: { limit, offset },
})
return api.get<{ results: AnnouncementListItem[]; total: number }>(
"announcements",
{
params: { limit, offset },
},
)
}
export function getAnnouncement(id: number) {
@@ -465,7 +465,6 @@ export function joinProblemSet(problemSetId: number) {
return api.post("problem-set-progress", { problemSetId })
}
export function getUserBadges(username?: string) {
return api.get<UserBadge[]>(
`users/${encodeURIComponent(username ?? "me")}/badges`,

View File

@@ -66,7 +66,9 @@ const timeRangeOptions: SelectOption[] = [
]
// 「全部时间」的 value 是空串,解不出来就是 null —— 正是不带时间条件的意思
const subOptions = computed<Duration | null>(() => durationFromValue(duration.value))
const subOptions = computed<Duration | null>(() =>
durationFromValue(duration.value),
)
// 根据时间段选项计算开始和结束时间
function getTimeRange(): {

View File

@@ -55,7 +55,10 @@ function submit() {
/** 给老师看的一句人话:选项按 A/B/C 报,报下标没人看得懂 */
function describe(sel: Set<number>) {
return `选了 ${[...sel].sort((a, b) => a - b).map((i) => String.fromCharCode(65 + i)).join("、")}`
return `选了 ${[...sel]
.sort((a, b) => a - b)
.map((i) => String.fromCharCode(65 + i))
.join("、")}`
}
function reset() {

View File

@@ -23,10 +23,7 @@ const IDLE_MS = 10 * 60 * 1000
* @param tutorialId 当前这一课0 表示还没加载好
* @param enabled 是否留痕。未登录时为 false教程本身保持免登录可读只是不记
*/
export function useLearnTrace(
tutorialId: Ref<number>,
enabled: Ref<boolean>,
) {
export function useLearnTrace(tutorialId: Ref<number>, enabled: Ref<boolean>) {
const visibility = useDocumentVisibility()
const { idle } = useIdle(IDLE_MS)

View File

@@ -126,7 +126,12 @@
<script setup lang="ts">
import { MdPreview } from "md-editor-v3"
import "md-editor-v3/lib/preview.css"
import type { Tutorial, Exercise, LANGUAGE, TutorialProgress } from "utils/types"
import type {
Tutorial,
Exercise,
LANGUAGE,
TutorialProgress,
} from "utils/types"
import {
getTutorial,
getTutorials,
@@ -220,7 +225,9 @@ async function loadProgress() {
}
try {
const rows = await getLearnProgress(type.value)
progress.value = Object.fromEntries(rows.map((row) => [row.tutorialId, row]))
progress.value = Object.fromEntries(
rows.map((row) => [row.tutorialId, row]),
)
} catch {
progress.value = {}
}

View File

@@ -311,7 +311,9 @@ function type(status: ProblemStatus) {
</p>
<n-list bordered style="margin-bottom: 8px">
<n-list-item v-for="(rule, i) in rules" :key="i">
<n-tag :type="KIND_TAG_TYPE[rule.kind]">{{ rule.description }}</n-tag>
<n-tag :type="KIND_TAG_TYPE[rule.kind]">{{
rule.description
}}</n-tag>
</n-list-item>
</n-list>
</div>

View File

@@ -111,8 +111,7 @@ const POLL_INTERVAL = 3000
const POLL_TIMEOUT = 3 * 60 * 1000
type Outcome =
| { ok: true; score: number; grade: string }
| { ok: false; error?: string }
{ ok: true; score: number; grade: string } | { ok: false; error?: string }
const { pause: pausePolling, resume: resumePolling } = useIntervalFn(
async () => {
@@ -509,11 +508,7 @@ onUnmounted(() => {
</n-card>
<!-- 详细评分 -->
<n-card
v-if="sortedCriteria.length"
size="small"
title="详细评分"
>
<n-card v-if="sortedCriteria.length" size="small" title="详细评分">
<div
v-for="[key, detail] in sortedCriteria"
:key="key"

View File

@@ -182,11 +182,7 @@ watch(
<n-tab-pane name="content" tab="题目描述">
<ProblemContent />
</n-tab-pane>
<n-tab-pane
v-if="canShowFlowchart"
name="flowchart"
tab="流程图表"
>
<n-tab-pane v-if="canShowFlowchart" name="flowchart" tab="流程图表">
<ProblemFlowchart />
</n-tab-pane>
<n-tab-pane name="info" tab="题目统计" :disabled="!!problemSetId">
@@ -234,11 +230,7 @@ watch(
<n-tab-pane name="content" tab="题目描述">
<ProblemContent />
</n-tab-pane>
<n-tab-pane
v-if="canShowFlowchart"
name="flowchart"
tab="流程图表"
>
<n-tab-pane v-if="canShowFlowchart" name="flowchart" tab="流程图表">
<ProblemFlowchart />
</n-tab-pane>
<n-tab-pane name="info" tab="题目统计" :disabled="!!problemSetId">

View File

@@ -42,11 +42,15 @@ function getProgressPercentage() {
// 有选做题时把「必做 N 题」标出来,否则「共 10 道题目」和「9 / 9」对不上
const optionalCount = computed(
() => props.problemSet.problemsCount - (props.problemSet.userProgress?.totalCount ?? 0),
() =>
props.problemSet.problemsCount -
(props.problemSet.userProgress?.totalCount ?? 0),
)
const endTimeText = computed(() =>
props.problemSet.endTime ? parseTime(props.problemSet.endTime, "YYYY-MM-DD HH:mm") : "",
props.problemSet.endTime
? parseTime(props.problemSet.endTime, "YYYY-MM-DD HH:mm")
: "",
)
function handleJoin() {

View File

@@ -286,7 +286,9 @@ const options: SelectOption[] = [...LONG_DURATION_OPTIONS]
// 认不出来退回 options[1](一个月内),和 duration 的初值一致
const subOptions = computed<Duration>(
() => durationFromValue(duration.value) ?? durationFromValue(LONG_DURATION_OPTIONS[1]!.value)!,
() =>
durationFromValue(duration.value) ??
durationFromValue(LONG_DURATION_OPTIONS[1]!.value)!,
)
onMounted(() => {

View File

@@ -69,11 +69,7 @@
</n-card>
<!-- 详细评分 -->
<n-card
v-if="sortedCriteria.length > 0"
size="small"
title="详细评分"
>
<n-card v-if="sortedCriteria.length > 0" size="small" title="详细评分">
<div
v-for="[key, detail] in sortedCriteria"
:key="key"
@@ -148,7 +144,9 @@ const criteriaDetails = computed<
)
})
// jsonb 不保留键序,直接遍历会把 40 分的「逻辑正确性」排到最后
const sortedCriteria = computed(() => sortFlowchartCriteria(criteriaDetails.value))
const sortedCriteria = computed(() =>
sortFlowchartCriteria(criteriaDetails.value),
)
const loading = ref(false)
const rendering = ref(false)

View File

@@ -43,7 +43,9 @@ const loading = ref(false)
* 测试点明细。`info` 在契约里是「完整形状或空对象」的联合(非管理员拿到的是空对象),
* `data` 本身也可能为 null —— 两种情况都由这个访问器归成空数组,模板里不再直接取。
*/
const caseResults = computed(() => submissionCaseResults(submission.value?.info))
const caseResults = computed(() =>
submissionCaseResults(submission.value?.info),
)
async function init() {
submission.value = props.submission

View File

@@ -80,14 +80,8 @@ async function init() {
const metricsRes = await getMetrics(res.user.id)
firstSubmissionAt.value = parseTime(metricsRes.first)
latestSubmissionAt.value = parseTime(metricsRes.latest)
toLatestAt.value = durationToDays(
metricsRes.latest,
metricsRes.now,
)
learnDuration.value = durationToDays(
metricsRes.first,
metricsRes.latest,
)
toLatestAt.value = durationToDays(metricsRes.latest, metricsRes.now)
learnDuration.value = durationToDays(metricsRes.first, metricsRes.latest)
}
} finally {
toggle(false)

View File

@@ -41,7 +41,5 @@ export function getHitokoto() {
}
export function getClassUsernames(classroom: string) {
return api.get<string[]>(
`classes/${encodeURIComponent(classroom)}/usernames`,
)
return api.get<string[]>(`classes/${encodeURIComponent(classroom)}/usernames`)
}

View File

@@ -158,7 +158,10 @@
import { formatISO, sub, type Duration } from "date-fns"
import type { FlowchartStatistics } from "@oj2/contract"
import { getFlowchartStatistics } from "oj/api"
import { PANEL_DURATION_OPTIONS, FLOWCHART_CRITERIA_ORDER } from "utils/constants"
import {
PANEL_DURATION_OPTIONS,
FLOWCHART_CRITERIA_ORDER,
} from "utils/constants"
import { durationFromValue } from "utils/functions"
import { useHiddenStudents } from "../composables/hiddenStudents"
import { Doughnut, Radar, Bar } from "vue-chartjs"
@@ -471,7 +474,8 @@ function renderWordCloud() {
const subOptions = computed<Duration>(
() =>
durationFromValue(query.duration) ?? durationFromValue(PANEL_DURATION_OPTIONS[0].value)!,
durationFromValue(query.duration) ??
durationFromValue(PANEL_DURATION_OPTIONS[0].value)!,
)
async function handleStatistics() {

View File

@@ -249,19 +249,23 @@ const ATTEMPT_COLORS: Record<string, string> = {
* 已通过的沉底,它们只是「做完了」,不需要再看。
*/
function groupByProblem(list: SubmissionStatisticsItems["items"]) {
const groups = new Map<string, {
problem: string
problemTitle: string
items: SubmissionStatisticsItems["items"]
}>()
const groups = new Map<
string,
{
problem: string
problemTitle: string
items: SubmissionStatisticsItems["items"]
}
>()
for (const item of list) {
const group = groups.get(item.problem)
if (group) group.items.push(item)
else groups.set(item.problem, {
problem: item.problem,
problemTitle: item.problemTitle,
items: [item],
})
else
groups.set(item.problem, {
problem: item.problem,
problemTitle: item.problemTitle,
items: [item],
})
}
return [...groups.values()]
.map((group) => ({
@@ -276,8 +280,9 @@ function groupByProblem(list: SubmissionStatisticsItems["items"]) {
item.result === SubmissionStatus.ast_check_failed,
),
}))
.sort((a, b) =>
Number(a.solved) - Number(b.solved) || b.items.length - a.items.length,
.sort(
(a, b) =>
Number(a.solved) - Number(b.solved) || b.items.length - a.items.length,
)
}
@@ -298,9 +303,18 @@ const columns: DataTableColumn<SubmissionStatisticsUser>[] = [
h(NFlex, { size: "small", align: "flex-start", wrap: false }, () => [
h(
NFlex,
{ size: 4, align: "center", wrap: false, style: "width: 200px; flex: none" },
{
size: 4,
align: "center",
wrap: false,
style: "width: 200px; flex: none",
},
() => [
h(NTag, { size: "small", bordered: false }, () => group.problem),
h(
NTag,
{ size: "small", bordered: false },
() => group.problem,
),
h(
NText,
{
@@ -318,39 +332,46 @@ const columns: DataTableColumn<SubmissionStatisticsUser>[] = [
depth: 3,
style: "width: 104px; flex: none",
},
() => `${group.items.length} 次 · ${group.solved ? "已通过" : "未通过"}`,
() =>
`${group.items.length} 次 · ${group.solved ? "已通过" : "未通过"}`,
),
h(NFlex, { size: 4, wrap: true, style: "flex: 1; min-width: 0" }, () =>
group.items.map((item) =>
h(
NTooltip,
{ delay: 200 },
{
trigger: () =>
h("button", {
// 内联样式而不是 class这些方块是 h() 出来、挂在 NDataTable 的
// 展开槽里渲染的,<style scoped> 能不能盖到它并不确定
style: {
width: "14px",
height: "14px",
padding: "0",
border: "none",
borderRadius: "3px",
cursor: "pointer",
background: ATTEMPT_COLORS[JUDGE_STATUS[item.result]?.type ?? "default"],
},
onClick: (event: MouseEvent) => {
event.stopPropagation()
openSubmission(item.id)
},
}),
default: () =>
`${JUDGE_STATUS[item.result]?.name ?? item.result} · ` +
`${parseTime(item.createTime, "MM-DD HH:mm:ss")} · ` +
`${item.id.toString().slice(0, 12)}`,
},
h(
NFlex,
{ size: 4, wrap: true, style: "flex: 1; min-width: 0" },
() =>
group.items.map((item) =>
h(
NTooltip,
{ delay: 200 },
{
trigger: () =>
h("button", {
// 内联样式而不是 class这些方块是 h() 出来、挂在 NDataTable 的
// 展开槽里渲染的,<style scoped> 能不能盖到它并不确定
style: {
width: "14px",
height: "14px",
padding: "0",
border: "none",
borderRadius: "3px",
cursor: "pointer",
background:
ATTEMPT_COLORS[
JUDGE_STATUS[item.result]?.type ?? "default"
],
},
onClick: (event: MouseEvent) => {
event.stopPropagation()
openSubmission(item.id)
},
}),
default: () =>
`${JUDGE_STATUS[item.result]?.name ?? item.result} · ` +
`${parseTime(item.createTime, "MM-DD HH:mm:ss")} · ` +
`${item.id.toString().slice(0, 12)}`,
},
),
),
),
),
]),
),
@@ -358,7 +379,8 @@ const columns: DataTableColumn<SubmissionStatisticsUser>[] = [
? h(
NText,
{ depth: 3 },
() => `只显示最近 ${loaded.items.length} 条,上面「提交数」才是总数`,
() =>
`只显示最近 ${loaded.items.length} 条,上面「提交数」才是总数`,
)
: null,
])
@@ -372,7 +394,11 @@ const columns: DataTableColumn<SubmissionStatisticsUser>[] = [
render: (row) =>
h(
NTag,
{ size: "small", type: row.done ? "success" : "default", bordered: false },
{
size: "small",
type: row.done ? "success" : "default",
bordered: false,
},
() => (row.done ? "已完成" : "未完成"),
),
},
@@ -693,7 +719,9 @@ const completionChartOptions = {
const subOptions = computed<Duration>(
// 认不出来(含 all就退回列表第一档和原来 `?? options[0]` 一致;
// all 实际不会走到这里handleStatistics 先分支掉了
() => durationFromValue(query.duration) ?? durationFromValue(PANEL_DURATION_OPTIONS[0].value)!,
() =>
durationFromValue(query.duration) ??
durationFromValue(PANEL_DURATION_OPTIONS[0].value)!,
)
function goSubmissions() {

View File

@@ -84,7 +84,8 @@ const handleEditorReady = (payload: EditorReadyPayload) => {
watch(
() => collabStore.room,
(room) => {
if (room && !collabStore.isTeacher && editorView.value) bind(editorView.value)
if (room && !collabStore.isTeacher && editorView.value)
bind(editorView.value)
else stop()
},
)

View File

@@ -47,15 +47,21 @@ export function useCollabDoc() {
if (doc) stop()
const myGeneration = ++generation
const [Y, awarenessProtocol, syncProtocol, encoding, decoding, { yCollab }] =
await Promise.all([
import("yjs"),
import("y-protocols/awareness"),
import("y-protocols/sync"),
import("lib0/encoding"),
import("lib0/decoding"),
import("y-codemirror.next"),
])
const [
Y,
awarenessProtocol,
syncProtocol,
encoding,
decoding,
{ yCollab },
] = await Promise.all([
import("yjs"),
import("y-protocols/awareness"),
import("y-protocols/sync"),
import("lib0/encoding"),
import("lib0/decoding"),
import("y-codemirror.next"),
])
// 等 chunk 的这段时间里房间关了(或者又开了新的一轮),整个放弃
if (myGeneration !== generation) return
@@ -106,7 +112,11 @@ export function useCollabDoc() {
detachDocUpdate = () => doc?.off("update", onDocUpdate)
const onAwarenessUpdate = (
{ added, updated, removed }: { added: number[]; updated: number[]; removed: number[] },
{
added,
updated,
removed,
}: { added: number[]; updated: number[]; removed: number[] },
origin: any,
) => {
if (origin === "remote") return

View File

@@ -64,7 +64,9 @@ export function useHiddenStudents(storageKey: string) {
// 把已经到期的清掉再落一次盘,否则这张表只增不减
const now = Date.now()
const cleaned = Object.fromEntries(
Object.entries(hiddenStudents.value).filter(([, expiresAt]) => expiresAt > now),
Object.entries(hiddenStudents.value).filter(
([, expiresAt]) => expiresAt > now,
),
)
hiddenStudents.value = cleaned
save(cleaned)

View File

@@ -211,7 +211,9 @@ export class BaseWebSocket<T extends WebSocketMessage = WebSocketMessage> {
RECONNECT_MAX_DELAY,
)
const delay = Math.round(base * (0.5 + Math.random() * 0.5))
console.log(`[WebSocket] 将在 ${delay}ms 后重连 (第 ${this.reconnectAttempts} 次)`)
console.log(
`[WebSocket] 将在 ${delay}ms 后重连 (第 ${this.reconnectAttempts} 次)`,
)
this.reconnectTimer = window.setTimeout(() => {
this.reconnectTimer = null
this.connect()
@@ -516,7 +518,9 @@ export interface SubmissionUpdate extends WebSocketMessage {
}
/** 判题进度。subscribe(submissionId) 认领,断线重连会自动补订阅 */
export function useSubmissionWebSocket(handler?: MessageHandler<SubmissionUpdate>) {
export function useSubmissionWebSocket(
handler?: MessageHandler<SubmissionUpdate>,
) {
return useChannel<SubmissionUpdate>("/ws/submissions", handler)
}

View File

@@ -11,6 +11,7 @@ import type { LANGUAGE } from "utils/types"
* Java / Golang / JavaScript 没有单独的包,落到 cpp(),是既有行为,不是遗漏。
*/
export function languageExtension(language: LANGUAGE): Extension {
if (language === "SQL") return sql({ dialect: SQLite, upperCaseKeywords: true })
if (language === "SQL")
return sql({ dialect: SQLite, upperCaseKeywords: true })
return ["Python2", "Python3"].includes(language) ? python() : cpp()
}

View File

@@ -60,11 +60,7 @@ const options = computed<MenuOption[]>(() => {
},
{
label: () =>
h(
RouterLink,
{ to: "/admin/ai/reports" },
{ default: () => "报告" },
),
h(RouterLink, { to: "/admin/ai/reports" }, { default: () => "报告" }),
key: "admin ai reports",
},
{
@@ -148,11 +144,7 @@ const options = computed<MenuOption[]>(() => {
},
{
label: () =>
h(
RouterLink,
{ to: "/admin/ai/reports" },
{ default: () => "报告" },
),
h(RouterLink, { to: "/admin/ai/reports" }, { default: () => "报告" }),
key: "admin ai reports",
},
{

View File

@@ -47,8 +47,9 @@ instance.interceptors.response.use(
// 这里**故意**不返回 AxiosResponse把 axios 的外层和后端的 { data } 信封一起
// 剥掉,让调用方直接拿到业务数据。类型上和 axios 的拦截器签名对不上(它期望原样
// 返回响应),文件末尾的 `as unknown as ApiClient` 就是为了把真实形状交出去。
((response: AxiosResponse) =>
response.data.data) as unknown as (response: AxiosResponse) => AxiosResponse,
((response: AxiosResponse) => response.data.data) as unknown as (
response: AxiosResponse,
) => AxiosResponse,
(error) => {
const payload = error.response?.data as ApiError | undefined
const code = payload?.error?.code ?? "network-error"

View File

@@ -98,9 +98,9 @@ export async function consumeJSONEventStream<T = any>(
* 「无法解析服务端事件数据: {...}」。后端 error.message 是英文的,按 code 换成中文。
*/
export async function aiStreamError(response: Response) {
const body = (await response.json().catch(() => null)) as
| { error?: { code?: string } }
| null
const body = (await response.json().catch(() => null)) as {
error?: { code?: string }
} | null
switch (body?.error?.code) {
case "too-many-requests":
return new Error("AI 请求太频繁了,歇一会儿再试")

View File

@@ -82,7 +82,6 @@ export type SUBMISSION_RESULT = JudgeStatus | 9
export type ProblemStatus = "passed" | "failed" | "not_test"
/**
* 题目标签。用契约的 —— 它比手抄那份多一个 `problemCount`
* shared/api.ts 原来还得用 `Tag & { problemCount: number }` 把它补回来。
@@ -403,11 +402,7 @@ export type Message = ContractMessage
*
* 注意 `ReactionCounts` 是 Partial 的:后端只下发有票的类型,没人投的键不出现。
*/
export type {
ReactionKey,
ReactionCounts,
ReactionState,
} from "@oj2/contract"
export type { ReactionKey, ReactionCounts, ReactionState } from "@oj2/contract"
import type { ReactionKey } from "@oj2/contract"
/**