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>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-16 15:11:21 +08:00
1 parent d4aadc7722
commit e8a20d12f9
38 files changed
+78 -78

No files matched your search

@@ -3,7 +3,7 @@ import type { RankRow, WeeklyChampion } from "@oj2/contract"
import { useThemeVars } from "naive-ui"
import UserName from "shared/components/UserName.vue"
import { parseTime } from "utils/functions"
import RankAvatar from "./RankAvatar.vue"
import RankAvatar from "shared/components/RankAvatar.vue"
/** 本班最近几周的每周冠军,底下再挂这张榜上「进步最大」的那个人 */
defineProps<{
@@ -9,7 +9,7 @@ import { useBackClose } from "shared/composables/backClose"
import { useBreakpoints } from "shared/composables/breakpoints"
import { parseTime } from "utils/functions"
import { useRankPalette } from "../palette"
import RankAvatar from "./RankAvatar.vue"
import RankAvatar from "shared/components/RankAvatar.vue"
// Markdown 连着整套代码高亮(两百多 KB),这里要等点开 / AI 写完才用得上,别让它拖慢页面
const Markdown = defineAsyncComponent(() => import("shared/components/Markdown.vue"))
@@ -1,79 +0,0 @@
<script setup lang="ts">
import { useDark } from "@vueuse/core"
import { rgba } from "shared/composables/tone"
import { USERNAME_CLASS_RE } from "utils/constants"
/**
* 排名页的头像。九成以上的学生没换过头像(1797 个号是 default.png),一排一模一样的
* 默认图分不出人,所以默认头像画成「名字最后一个字 + 彩色圆」,颜色按用户名固定 ——
* 同一个人在领奖台、赛道、「你」那张卡里是同一个颜色。自己是实心绿。
*/
const props = defineProps<{
username: string
avatar: string | null
size: number
me?: boolean
}>()
const isDark = useDark()
/** 头像文件丢了(备份恢复后没拷上传目录、被删)就退回名字那个字,别露出破图 */
const broken = ref(false)
watch(
() => props.avatar,
() => (broken.value = false),
)
const PALETTE = ["#2f6fd0", "#c76a12", "#6a4bd8", "#0f7a6c", "#b4315f", "#4d5967", "#8a6a00"]
const letter = computed(() => {
const name = props.username.replace(USERNAME_CLASS_RE, "") || props.username
return Array.from(name).at(-1) ?? "?"
})
const colors = computed(() => {
if (props.me) return { background: "#18a058", color: "#ffffff" }
let hash = 0
for (const char of props.username) hash = (hash * 31 + char.codePointAt(0)!) >>> 0
const base = PALETTE[hash % PALETTE.length]!
return { background: rgba(base, isDark.value ? 0.28 : 0.13), color: base }
})
</script>
<template>
<img
v-if="avatar && !broken"
class="avatar"
:src="avatar"
alt=""
@error="broken = true"
:style="{ width: `${size}px`, height: `${size}px` }"
/>
<span
v-else
class="avatar letter"
aria-hidden="true"
:style="{
width: `${size}px`,
height: `${size}px`,
fontSize: `${Math.round(size * 0.5)}px`,
...colors,
}"
>{{ letter }}</span
>
</template>
<style scoped>
.avatar {
border-radius: 50%;
flex-shrink: 0;
object-fit: cover;
}
.letter {
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 700;
line-height: 1;
}
</style>
+1 -1
View File
@@ -4,7 +4,7 @@ import { useThemeVars } from "naive-ui"
import UserName from "shared/components/UserName.vue"
import { gapText, toPass } from "../utils"
import MoodPopover from "./MoodPopover.vue"
import RankAvatar from "./RankAvatar.vue"
import RankAvatar from "shared/components/RankAvatar.vue"
export interface MiniRank {
label: string
@@ -4,7 +4,7 @@ import { useThemeVars } from "naive-ui"
import { useBreakpoints } from "shared/composables/breakpoints"
import UserName from "shared/components/UserName.vue"
import { useRankPalette } from "../palette"
import RankAvatar from "./RankAvatar.vue"
import RankAvatar from "shared/components/RankAvatar.vue"
/**
* 领奖台:前三名站在台阶上,第一名戴皇冠。赛道从第 4 名接着排,前三不再出现第二遍。
@@ -5,7 +5,7 @@ import UserName from "shared/components/UserName.vue"
import { useBreakpoints } from "shared/composables/breakpoints"
import { useRankPalette } from "../palette"
import MoodPopover from "./MoodPopover.vue"
import RankAvatar from "./RankAvatar.vue"
import RankAvatar from "shared/components/RankAvatar.vue"
/**
* 赛道:每人一根条,条越长做对越多,一样长的先做到的排前面(设计稿 G1 / G2)。
+1 -1
View File
@@ -3,7 +3,7 @@ import type { RankRow } from "@oj2/contract"
import { useThemeVars } from "naive-ui"
import UserName from "shared/components/UserName.vue"
import { useRankPalette } from "../palette"
import RankAvatar from "./RankAvatar.vue"
import RankAvatar from "shared/components/RankAvatar.vue"
/** 这周本班前 5 名(做对过新题的),每周一清零 */
const props = defineProps<{ rows: RankRow[]; meId?: number; loaded: boolean }>()