feat(首页): 学生上课时首页最上面就是这节课和「接着做」,课后先看上次没做完的;老师有了自己的首页,看今天哪些班在上课、点进课堂看板
Deploy / deploy (push) Canceled after 0s

去掉和顶栏重复的找题框、常年空着的比赛卡、底部快捷入口,公告只留置顶那条。
修了当天上午上课、下午再登录时写「上次(今天的日期)还有几道没过」。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-13 14:53:38 +08:00
1 parent c8c38553ec
commit 10d2638c23
4 files changed
+480 -191

No files matched your search

+6
View File
@@ -13,6 +13,7 @@ import {
type TutorialSummary,
type Tutorial,
type ClassActivity,
type ClassToday,
type LastVisit,
type ClassBoard,
type ClassLesson,
@@ -305,6 +306,11 @@ export function getClassActivity() {
return api.get<ClassActivity>("me/class-activity")
}
/** 老师首页「今天的课」:今天在上课的每个班 */
export function getClassToday() {
return api.get<ClassToday>("classroom/today")
}
/** 首页「上次来」卡:上次登录到这次登录之间交过什么、哪道还没做对 */
export function getLastVisit() {
return api.get<LastVisit>("me/last-visit")
+313
View File
@@ -0,0 +1,313 @@
<script setup lang="ts">
import type { ClassToday } from "@oj2/contract"
import type { AnnouncementListItem } from "utils/types"
import { useThemeVars } from "naive-ui"
import { getAnnouncement, getAnnouncementList, getClassToday } from "oj/api"
import { classLabel } from "oj/submission/utils"
import { useUserStore } from "shared/store/user"
import { parseTime, zonedParts } from "utils/functions"
/**
* 老师的首页(设计稿「首页重设计」HomeTeacher)。原来老师看到的和学生一样(继续学习、最近提交),
* 对老师没用。现在最上面是「今天的课」:今天在上课的每个班一行 —— 正在上课 / 几点下课了、
* 布置了几道、几人交过、全做对几人,每道题一根小条,点「课堂看板」进那个班。
* 下面是常用入口和置顶的公告
*/
const theme = useThemeVars()
const router = useRouter()
const userStore = useUserStore()
const today = ref<ClassToday | null>(null)
const pinned = ref<AnnouncementListItem[]>([])
/** 最后一次有人交题在这么久以内,算「正在上课」 */
const LIVE_MS = 15 * 60 * 1000
onMounted(async () => {
const [classes, announcements] = await Promise.allSettled([
getClassToday(),
getAnnouncementList(0, 10),
])
if (classes.status === "fulfilled") today.value = classes.value
if (announcements.status === "fulfilled") {
pinned.value = announcements.value.results.filter((item) => item.top)
}
})
const greeting = computed(() => {
const hour = zonedParts(new Date())!.hour
if (hour < 5) return "夜深了"
if (hour < 11) return "上午好"
if (hour < 13) return "中午好"
if (hour < 18) return "下午好"
return "晚上好"
})
type ClassRow = ClassToday["classes"][number]
function stateOf(row: ClassRow) {
if (!row.lastSubmitAt) return { live: false, text: "布置了,还没人交" }
if (Date.now() - Date.parse(row.lastSubmitAt) < LIVE_MS) return { live: true, text: "正在上课" }
return { live: false, text: `${parseTime(row.lastSubmitAt, "HH:mm")} 下课了` }
}
function metaOf(row: ClassRow) {
const parts = [
row.source === "teacher" && row.assignedAt
? `${parseTime(row.assignedAt, "HH:mm")} 布置了 ${row.problems.length} 道`
: `做了 ${row.problems.length} 道`,
]
if (row.activeCount) parts.push(`${row.activeCount}/${row.rosterCount} 人交过`)
if (row.problems.length) parts.push(`全做对 ${row.allDoneCount} 人`)
return parts.join(" · ")
}
function openBoard(row: ClassRow) {
router.push({ name: "classroom board", query: { className: row.className } })
}
const shortcuts = computed(() =>
[
{ label: "课堂看板", to: "/classroom", show: true },
{ label: "数据统计", to: "/statistics", show: true },
{ label: "新建题目", to: "/admin/problem/create", show: userStore.hasProblemPermission },
{ label: "后台", to: userStore.isSuperAdmin ? "/admin" : "/admin/problem/list", show: true },
].filter((it) => it.show),
)
const announcementShow = ref(false)
const announcementTitle = ref("")
const announcementContent = ref("")
async function openAnnouncement(item: AnnouncementListItem) {
const res = await getAnnouncement(item.id)
announcementTitle.value = item.title
announcementContent.value = res.content
announcementShow.value = true
}
</script>
<template>
<div class="home">
<section class="hero">
<h1 class="hello">{{ greeting }},{{ userStore.user?.username }}</h1>
<n-text v-if="today" depth="3" class="tagline">
{{
today.classes.length ? `今天 ${today.classes.length} 个班上过课` : "今天还没有班在上课"
}}
</n-text>
</section>
<section v-if="today?.classes.length" class="card">
<h3>今天的课</h3>
<div v-for="row in today.classes" :key="row.className" class="class-row">
<div class="class-head">
<b class="class-name">{{ classLabel(row.className) }}</b>
<span class="state" :class="{ live: stateOf(row).live }">{{ stateOf(row).text }}</span>
<span class="muted">{{ metaOf(row) }}</span>
<span class="spacer"></span>
<n-button :type="stateOf(row).live ? 'primary' : 'default'" @click="openBoard(row)">
课堂看板
</n-button>
</div>
<div v-if="row.problems.length" class="minis">
<router-link
v-for="problem in row.problems"
:key="problem.problemDisplayId"
:to="`/problem/${problem.problemDisplayId}`"
class="mini"
:title="`${problem.problemDisplayId} ${problem.title}:${problem.userCount} 人交过,${problem.acceptedCount} 人做对`"
>
<span>
<b class="num">{{ problem.problemDisplayId }}</b>
<span class="muted num mini-count">
{{ problem.acceptedCount }}/{{ row.rosterCount }}</span
>
</span>
<i>
<b
:style="{
width: `${row.rosterCount ? (problem.acceptedCount / row.rosterCount) * 100 : 0}%`,
}"
></b>
</i>
</router-link>
</div>
</div>
</section>
<div class="shortcuts">
<n-button v-for="it in shortcuts" :key="it.to" size="large" @click="router.push(it.to)">
{{ it.label }}
</n-button>
</div>
<div
v-for="item in pinned"
:key="item.id"
class="card pinned"
role="button"
tabindex="0"
@click="openAnnouncement(item)"
@keyup.enter="openAnnouncement(item)"
>
<n-tag type="warning" size="small" :bordered="false">置顶</n-tag>
<b>{{ item.title }}</b>
<span class="spacer"></span>
<router-link to="/announcement" class="muted" @click.stop>公告 →</router-link>
</div>
<n-modal
v-model:show="announcementShow"
preset="card"
:style="{ maxWidth: '70vw', maxHeight: '80vh' }"
:content-style="{ overflow: 'auto' }"
:title="announcementTitle"
>
<div v-html="announcementContent"></div>
</n-modal>
</div>
</template>
<style scoped>
.home {
max-width: var(--oj-page-width);
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 18px;
}
.hero {
margin: 16px 0 4px;
display: flex;
flex-direction: column;
gap: 6px;
}
.hello {
margin: 0;
font-size: 28px;
font-weight: 600;
}
.tagline {
font-size: 15px;
}
.card {
display: flex;
flex-direction: column;
gap: 4px;
padding: 18px 20px;
border-radius: var(--oj-radius);
border: 1px solid v-bind("theme.dividerColor");
}
.card h3 {
margin: 0 0 6px;
font-size: 17px;
}
.class-row {
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px 0;
border-top: 1px solid v-bind("theme.dividerColor");
}
.class-row:first-of-type {
border-top: 0;
}
.class-head {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px 12px;
}
.class-name {
font-size: 16px;
}
.state {
padding: 2px 8px;
border-radius: 4px;
font-size: 13px;
background: v-bind("theme.actionColor");
color: v-bind("theme.textColor3");
}
.state.live {
color: v-bind("theme.successColor");
font-weight: 600;
}
.muted {
font-size: 14px;
color: v-bind("theme.textColor3");
text-decoration: none;
}
.spacer {
flex: 1 1 auto;
}
.minis {
display: flex;
flex-wrap: wrap;
gap: 10px 16px;
}
.mini {
display: inline-flex;
flex-direction: column;
gap: 4px;
min-width: 90px;
font-size: 13px;
color: inherit;
text-decoration: none;
}
.mini i {
display: block;
height: 5px;
border-radius: 3px;
background: v-bind("theme.actionColor");
overflow: hidden;
}
.mini i b {
display: block;
height: 5px;
background: v-bind("theme.successColor");
}
.num {
font-variant-numeric: tabular-nums;
}
.mini-count {
margin-left: 4px;
font-size: 13px;
}
.shortcuts {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.pinned {
flex-direction: row;
align-items: center;
gap: 10px;
padding: 14px 18px;
cursor: pointer;
}
.pinned:hover {
border-color: v-bind("theme.primaryColor");
}
</style>
+154 -190
View File
@@ -15,7 +15,6 @@ import {
} from "oj/api"
import SubmissionResultTag from "shared/components/SubmissionResultTag.vue"
import { useBreakpoints } from "shared/composables/breakpoints"
import { useProblemJump } from "shared/composables/problemJump"
import { pickHeadline, type Headline } from "oj/user/knowledge"
import { useUserStore } from "shared/store/user"
import { ContestStatus, CONTEST_STATUS, JUDGE_STATUS, LANGUAGE_SHOW_VALUE } from "utils/constants"
@@ -56,7 +55,6 @@ const lastVisit = ref<LastVisit | null>(null)
/** 知识点地图挑出来的那一句(本周升级 / 再做几道升档 / 去点亮一个),见 oj/user/knowledge.ts */
const knowledgeHeadline = ref<Headline | null>(null)
const loaded = ref(false)
const keyword = ref("")
/** 头一回登录、还没交过:问候换成「欢迎」,告诉他题在哪找 */
const firstVisit = computed(
@@ -151,9 +149,10 @@ async function loadSubmissions() {
submissions.value = res.results
}
/** 首页只放置顶的公告(用户定的),其余从顶栏「公告」进 */
async function loadAnnouncements() {
const res = await getAnnouncementList(0, 4)
announcements.value = res.results
const res = await getAnnouncementList(0, 10)
announcements.value = res.results.filter((item) => item.top)
}
async function loadKnowledge() {
@@ -216,6 +215,24 @@ const classSummaryText = computed(() => {
: `那天班里一起做了 ${problems.length} 道,你做对了 ${done} 道`
})
/**
* 「上次还有 N 道没过」那一句:上次来就是今天(上午上课、下午又登录)时不说 ——
* 原来照样写「上次(10月13日)」,和今天是同一天
*/
const visitBeforeToday = computed(
() =>
!!lastVisit.value?.previousLogin &&
dayKey(lastVisit.value.previousLogin) !== dayKey(new Date()),
)
/** 这节课下一道没做对的:卡底「接着做」那个大按钮 */
const nextProblem = computed(
() => classActivity.value?.problems.find((item) => item.myStatus !== "accepted") ?? null,
)
const doneInClass = computed(
() => classActivity.value?.problems.filter((item) => item.myStatus === "accepted").length ?? 0,
)
const solvedText = computed(() =>
(visit.value?.solved ?? []).map((item) => `${item.problemDisplayId} ${item.title}`).join("、"),
)
@@ -227,9 +244,9 @@ const classActivityTitle = computed(() => {
// 老师在课堂看板布置的,说成「老师布置的」,比「班里在做」更让人知道该做这几道
if (classActivity.value?.source === "teacher") {
const language = classActivity.value.language
return `今天老师布置的题${language ? ` · 用${LANGUAGE_SHOW_VALUE[language]}做` : ""}`
return `这节课${language ? ` · 用${LANGUAGE_SHOW_VALUE[language]}做` : ""}`
}
if (classToday.value) return "今天班里在做"
if (classToday.value) return "这节课"
const [, month, date] = day.split("-").map(Number)
return `${month}月${date}日班里做了`
})
@@ -238,9 +255,9 @@ const MY_STATUS: Record<
ClassActivityProblem["myStatus"],
{ label: string; type: "success" | "warning" | "default" }
> = {
accepted: { label: "已通过", type: "success" },
tried: { label: "未通过", type: "warning" },
none: { label: "没做", type: "default" },
accepted: { label: "已做对", type: "success" },
tried: { label: "没做对", type: "warning" },
none: { label: "还没做", type: "default" },
}
async function loadWeekly() {
@@ -272,8 +289,6 @@ function learnLink(track: Track) {
return lessonPath(track.type, track.step)
}
const { jump, jumping } = useProblemJump()
function openSubmission(row: SubmissionListItem) {
if (row.showLink) router.push(`/submission/${row.id}`)
else router.push(`/problem/${row.problemDisplayId}`)
@@ -313,45 +328,87 @@ async function openAnnouncement(item: AnnouncementListItem) {
<template>
<div class="home">
<!-- 中间原来还有一个「题号或题目名 找题」框,和顶栏右上角那个重复,拿掉了 -->
<section class="hero">
<div>
<h1 v-if="firstVisit" class="hello">欢迎来到判题狗,{{ userStore.user?.username }}</h1>
<h1 v-else class="hello">{{ greeting }},{{ userStore.user?.username }}</h1>
<n-text v-if="firstVisit" depth="3">老师报了题号,在右边的框里输入就能找到题</n-text>
<n-text v-else depth="3">
<template v-if="longAway">{{ awayText }} · </template>
已解决 {{ userStore.profile?.acceptedNumber ?? 0 }} 题 · 共提交
{{ userStore.profile?.submissionNumber ?? 0 }} 次
<template v-if="weeklyText">
·
<router-link to="/rank" class="plain-link">{{ weeklyText }}</router-link>
</template>
</n-text>
<div v-if="knowledgeHeadline" class="knowledge-line">
<h1 v-if="firstVisit" class="hello">欢迎来到判题狗,{{ userStore.user?.username }}</h1>
<h1 v-else class="hello">{{ greeting }},{{ userStore.user?.username }}</h1>
<n-text v-if="firstVisit" depth="3">老师报了题号,在右上角的框里输入就能找到题</n-text>
<n-text v-else depth="3">
<template v-if="longAway">{{ awayText }} · </template>
已解决 {{ userStore.profile?.acceptedNumber ?? 0 }} 题
<template v-if="weeklyText">
·
<router-link to="/rank" class="plain-link">{{ weeklyText }}</router-link>
</template>
<template v-if="knowledgeHeadline">
·
<router-link v-if="knowledgeHeadline.to" :to="knowledgeHeadline.to" class="plain-link">
{{ knowledgeHeadline.text }}
</router-link>
<span v-else>{{ knowledgeHeadline.text }}</span>
<router-link to="/user" class="more">我的知识点</router-link>
</div>
</div>
<n-input-group class="search">
<n-input
v-model:value="keyword"
placeholder="题号或题目名"
clearable
@keyup.enter="jump(keyword)"
/>
<n-button type="primary" :loading="jumping" @click="jump(keyword)">找题</n-button>
</n-input-group>
</template>
</n-text>
</section>
<div class="grid">
<div class="column">
<!-- 「上次来」:上次登录到这次登录之间交过题才有。班里今天在做的时候让位给班里卡,
只在那张卡底下留一句没做对的 -->
<!-- 上课时最上面是这节课(设计稿 HomeStuClass):几道题做没做对,底下一个「接着做」 -->
<n-card
v-if="classToday && classActivity?.problems.length"
:bordered="false"
class="card lesson"
>
<template #header>{{ classActivityTitle }}</template>
<template #header-extra>
<n-text depth="3" class="row-meta">
{{ classActivity.problems.length }} 道做对了 {{ doneInClass }} 道
</n-text>
</template>
<router-link
v-for="item in classActivity.problems"
:key="item.problemDisplayId"
:to="`/problem/${item.problemDisplayId}`"
class="row"
>
<n-tag :type="MY_STATUS[item.myStatus].type" size="small" :bordered="false">
{{ MY_STATUS[item.myStatus].label }}
</n-tag>
<span class="row-title">{{ item.problemDisplayId }} {{ item.title }}</span>
<!-- 老师刚布置、还没人交的时候不显示「0/0」 -->
<n-text v-if="item.userCount" depth="3" class="row-meta">
{{ item.acceptedCount }}/{{ item.userCount }} 人做对
</n-text>
</router-link>
<div class="lesson-foot">
<n-button
v-if="nextProblem"
type="primary"
size="large"
@click="router.push(`/problem/${nextProblem.problemDisplayId}`)"
>
接着做 {{ nextProblem.problemDisplayId }} {{ nextProblem.title }} →
</n-button>
<n-text v-else type="success">这节课的题都做对了</n-text>
</div>
<div v-if="visitBeforeToday && visit?.unsolved.length" class="visit-foot warn">
<span class="dot"></span>
<span class="row-title">
上次({{ dateText(lastVisit!.previousLogin!) }})还有
{{ visit.unsolved.length }} 道没做对:{{ visit.unsolved[0]!.problemDisplayId }}
{{ visit.unsolved[0]!.title }}
</span>
<router-link :to="`/problem/${visit.unsolved[0]!.problemDisplayId}`" class="go">
接着做 ›
</router-link>
</div>
</n-card>
<!-- 不在上课:上次没做完的排最前(设计稿 HomeStuAfter) -->
<n-card v-if="visit && !classToday" :bordered="false" class="card">
<template #header>上次来 · {{ visitText(lastVisit!.previousLogin!) }}</template>
<template #header>
{{ visit.unsolved.length ? "上次没做完" : "上次来" }} ·
{{ visitText(lastVisit!.previousLogin!) }}
</template>
<template #header-extra>
<n-text depth="3" class="row-meta">
做对 {{ visit.solvedCount }} 道 · 交了 {{ visit.submissionCount }} 次
@@ -363,7 +420,7 @@ async function openAnnouncement(item: AnnouncementListItem) {
:to="`/problem/${item.problemDisplayId}`"
class="unsolved"
>
<n-tag type="warning" size="small" :bordered="false">未通过</n-tag>
<n-tag type="warning" size="small" :bordered="false">没做对</n-tag>
<div class="unsolved-main">
<span class="row-title">
<n-text depth="3">{{ item.problemDisplayId }}</n-text> {{ item.title }}
@@ -375,20 +432,17 @@ async function openAnnouncement(item: AnnouncementListItem) {
<n-button type="primary" tag="span">接着做</n-button>
</router-link>
<div v-if="visit.solvedCount" class="row">
<n-tag type="success" size="small" :bordered="false">已通过</n-tag>
<n-tag type="success" size="small" :bordered="false">已做对</n-tag>
<span class="row-title">{{ visit.solvedCount }} 道:{{ solvedText }}</span>
<router-link to="/submission?myself=1" class="more">
看这 {{ visit.solvedCount }} 道 ›
</router-link>
</div>
<div v-if="visitCoversClass" class="visit-foot">
<Icon icon="ph:check-bold" :width="14" />{{ classSummaryText }}
</div>
</n-card>
<!-- 只在有的时候出现:没班级、最近一周班里没一起做过题的,这块不占位置 -->
<!-- 前几天班里一起做的(不是今天、也不是上次来那天):只在有的时候出现 -->
<n-card
v-if="classActivity?.problems.length && !visitCoversClass"
v-if="!classToday && classActivity?.problems.length && !visitCoversClass"
:title="classActivityTitle"
:bordered="false"
class="card"
@@ -406,59 +460,28 @@ async function openAnnouncement(item: AnnouncementListItem) {
{{ MY_STATUS[item.myStatus].label }}
</n-tag>
<span class="row-title">{{ item.problemDisplayId }} {{ item.title }}</span>
<!-- 老师刚布置、还没人交的时候不显示「0/0 人通过」 -->
<n-text v-if="item.userCount" depth="3" class="row-meta">
{{ item.acceptedCount }}/{{ item.userCount }} 人通过
{{ item.acceptedCount }}/{{ item.userCount }} 人做对
</n-text>
</router-link>
<div v-if="classToday && visit?.unsolved.length" class="visit-foot warn">
<span class="dot"></span>
<span class="row-title">
上次({{ dateText(lastVisit!.previousLogin!) }})还有
{{ visit.unsolved.length }} 道没过: {{ visit.unsolved[0]!.problemDisplayId }}
{{ visit.unsolved[0]!.title }}
</span>
<router-link :to="`/problem/${visit.unsolved[0]!.problemDisplayId}`" class="go">
接着做 ›
</router-link>
</div>
</n-card>
<!-- 继续学习:一门一行 -->
<n-card title="继续学习" :bordered="false" class="card">
<n-flex vertical :size="12">
<router-link
v-for="track in tracks"
:key="track.type"
:to="learnLink(track)"
class="track"
>
<div class="track-main">
<n-text depth="3" class="track-label">
{{ track.label }} · 第 {{ track.step }} / {{ track.total }} 课
</n-text>
<div class="track-title">{{ track.title }}</div>
<n-progress
type="line"
:percentage="Math.round((track.done / track.total) * 100)"
status="success"
:show-indicator="false"
:height="6"
/>
<n-text depth="3" class="track-meta">
学完 {{ track.done }} 课
<template v-if="track.lastViewedAt">
· 上次 {{ relativeTime(track.lastViewedAt) }}
</template>
</n-text>
</div>
<n-button :type="track.lastViewedAt ? 'primary' : 'default'" tag="span">
{{ track.lastViewedAt ? "继续" : "开始" }}
</n-button>
</router-link>
<n-empty v-if="loaded && !tracks.length" description="还没有公开的教程" />
</n-flex>
<router-link v-for="track in tracks" :key="track.type" :to="learnLink(track)" class="row">
<n-text depth="3" class="track-label">{{ track.label }}</n-text>
<span class="row-title">
第 {{ track.step }} / {{ track.total }} 课 · {{ track.title }}
</span>
<n-button :type="track.lastViewedAt ? 'primary' : 'default'" tag="span" size="small">
{{ track.lastViewedAt ? "继续" : "开始" }}
</n-button>
</router-link>
<n-empty v-if="loaded && !tracks.length" description="还没有公开的教程" />
</n-card>
</div>
<div class="column">
<n-card title="最近提交" :bordered="false" class="card">
<template #header-extra>
<router-link to="/submission?myself=1" class="more">全部</router-link>
@@ -479,10 +502,9 @@ async function openAnnouncement(item: AnnouncementListItem) {
</template>
</n-empty>
</n-card>
</div>
<div class="column">
<n-card title="比赛" :bordered="false" class="card">
<!-- 比赛只在有进行中 / 快开始的时候出现(这学期一场都没办,原来常年一块空卡) -->
<n-card v-if="contests.length" title="比赛" :bordered="false" class="card">
<template #header-extra>
<router-link to="/contest" class="more">全部</router-link>
</template>
@@ -500,47 +522,22 @@ async function openAnnouncement(item: AnnouncementListItem) {
</n-flex>
<n-text depth="3" class="row-meta">{{ contestTime(contest) }}</n-text>
</router-link>
<n-text v-if="loaded && !contests.length" depth="3">
现在没有进行中或即将开始的比赛
</n-text>
</n-card>
<n-card title="公告" :bordered="false" class="card">
<template #header-extra>
<router-link to="/announcement" class="more">全部</router-link>
</template>
<div
v-for="item in announcements"
:key="item.id"
class="row clickable"
@click="openAnnouncement(item)"
>
<n-tag v-if="item.top" type="error" size="small" :bordered="false">置顶</n-tag>
<span class="row-title">{{ item.title }}</span>
<n-text depth="3" class="row-meta">{{ parseTime(item.createTime, "M月D日") }}</n-text>
</div>
</n-card>
<n-card :bordered="false" class="card">
<n-flex :size="8" class="shortcuts">
<n-button secondary @click="router.push('/problem')">
<template #icon><Icon icon="fluent-emoji:memo" /></template>
题库
</n-button>
<n-button secondary @click="router.push('/problemset')">
<template #icon><Icon icon="fluent-emoji:clipboard" /></template>
题单
</n-button>
<n-button secondary @click="router.push('/achievement')">
<template #icon><Icon icon="fluent-emoji:sports-medal" /></template>
成就
</n-button>
<n-button secondary @click="router.push('/ai-analysis')">
<template #icon><Icon icon="fluent-emoji:crystal-ball" /></template>
智能分析
</n-button>
</n-flex>
</n-card>
<!-- 公告只放置顶的;别的从顶栏「公告」进 -->
<div
v-for="item in announcements"
:key="item.id"
class="card pinned"
role="button"
tabindex="0"
@click="openAnnouncement(item)"
@keyup.enter="openAnnouncement(item)"
>
<n-tag type="warning" size="small" :bordered="false">置顶</n-tag>
<span class="row-title">{{ item.title }}</span>
<router-link to="/announcement" class="more" @click.stop>公告 →</router-link>
</div>
</div>
</div>
@@ -564,37 +561,32 @@ async function openAnnouncement(item: AnnouncementListItem) {
.hero {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
margin: 16px 0 28px;
flex-direction: column;
gap: 6px;
margin: 16px 0 24px;
}
.hello {
font-size: 28px;
font-weight: 600;
margin: 0 0 8px;
margin: 0;
}
.knowledge-line {
margin-top: 6px;
/* 这节课:卡片描一圈主色,底下一个大按钮 */
.lesson {
box-shadow: inset 0 0 0 1px rgba(24, 160, 88, 0.35);
}
.lesson-foot {
padding-top: 12px;
}
.pinned {
display: flex;
gap: 12px;
align-items: baseline;
color: #18a058;
}
/* 这一行本身就是链接(去按知识点筛的题目列表),下划线说明它能点 */
.knowledge-line .plain-link {
color: #18a058;
text-decoration: underline;
text-underline-offset: 3px;
}
.search {
width: 340px;
max-width: 100%;
align-items: center;
gap: 10px;
padding: 14px 18px;
cursor: pointer;
}
.grid {
@@ -629,42 +621,14 @@ async function openAnnouncement(item: AnnouncementListItem) {
}
}
.track {
display: flex;
align-items: center;
gap: 20px;
padding: 16px 18px;
border-radius: 8px;
color: inherit;
text-decoration: none;
background-color: rgba(128, 128, 128, 0.06);
transition: background-color 0.2s;
}
.track:hover {
background-color: rgba(24, 160, 88, 0.1);
}
.track-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.track-label,
.track-meta,
.row-meta {
.track-label {
flex: none;
width: 64px;
font-size: var(--oj-fs-meta);
}
.track-title {
font-size: 17px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.row-meta {
font-size: var(--oj-fs-meta);
}
.row {
+7 -1
View File
@@ -5,6 +5,8 @@ import { useUserStore } from "shared/store/user"
// 两边都按需加载:没登录的访客只拿题目列表那一份,登录了的学生只拿首页那一份
const Dashboard = defineAsyncComponent(() => import("./dashboard.vue"))
// 老师的首页单独一张(设计稿「首页重设计」HomeTeacher):今天哪些班在上课
const TeacherHome = defineAsyncComponent(() => import("./TeacherHome.vue"))
const ProblemList = defineAsyncComponent(() => import("oj/problem/list.vue"))
const userStore = useUserStore()
@@ -17,6 +19,10 @@ const showDashboard = computed(() =>
</script>
<template>
<Dashboard v-if="showDashboard" />
<template v-if="showDashboard">
<!-- 资料回来之前分不出是不是老师,先空着,免得老师先闪一下学生的首页 -->
<TeacherHome v-if="userStore.isFinished && userStore.isTeacherOrAbove" />
<Dashboard v-else-if="userStore.isFinished" />
</template>
<ProblemList v-else />
</template>