- 新接口 /me/last-visit:上次登录到这次登录之间的题库提交,不调 AI(原来要同步等模型最长 60 秒弹框才出来) - 班里今天在做时让位给班里卡,只在卡底留一句;班里那天就是上次来那天时两张卡合成一张 - 第一次登录说「欢迎来到判题狗」,隔了 30 天以上说「好久不见」 - 删掉弹框、store 和 /ai/login-summary Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2b4676ae34
commit
91d9c9c6d7
15 files changed
+389
-289
No files matched your search
@@ -6,6 +6,7 @@ import {
|
||||
getClassActivity,
|
||||
getKnowledgeMap,
|
||||
getContestList,
|
||||
getLastVisit,
|
||||
getLearnProgress,
|
||||
getSubmissions,
|
||||
getTutorials,
|
||||
@@ -16,13 +17,14 @@ 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 } from "utils/constants"
|
||||
import { duration, parseTime, zonedParts } from "utils/functions"
|
||||
import { ContestStatus, CONTEST_STATUS, JUDGE_STATUS } from "utils/constants"
|
||||
import { duration, parseTime, zonedParts, zonedYear } from "utils/functions"
|
||||
import type {
|
||||
AnnouncementListItem,
|
||||
ClassActivity,
|
||||
ClassActivityProblem,
|
||||
Contest,
|
||||
LastVisit,
|
||||
SubmissionListItem,
|
||||
WeeklyRank,
|
||||
} from "utils/types"
|
||||
@@ -51,12 +53,43 @@ const submissions = ref<SubmissionListItem[]>([])
|
||||
const announcements = ref<AnnouncementListItem[]>([])
|
||||
const weekly = ref<WeeklyRank | null>(null)
|
||||
const classActivity = ref<ClassActivity | null>(null)
|
||||
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(
|
||||
() =>
|
||||
!!lastVisit.value &&
|
||||
!lastVisit.value.previousLogin &&
|
||||
(userStore.profile?.submissionNumber ?? 0) === 0,
|
||||
)
|
||||
|
||||
/** 离上次来多少天。超过 30 天后端不给「上次来」卡,这里只在问候里说一句 */
|
||||
const awayDays = computed(() => {
|
||||
const previous = lastVisit.value?.previousLogin
|
||||
if (!previous) return 0
|
||||
return Math.floor((Date.now() - new Date(previous).getTime()) / 864e5)
|
||||
})
|
||||
const longAway = computed(() => awayDays.value > 30)
|
||||
|
||||
const awayText = computed(() => {
|
||||
const previous = lastVisit.value?.previousLogin
|
||||
if (!previous) return ""
|
||||
const days = awayDays.value
|
||||
const gap =
|
||||
days >= 365
|
||||
? `隔了 ${Math.floor(days / 365)} 年多`
|
||||
: days >= 60
|
||||
? `隔了 ${Math.floor(days / 30)} 个月`
|
||||
: `隔了 ${days} 天`
|
||||
return `上次来是 ${dateText(previous)},${gap}`
|
||||
})
|
||||
|
||||
const greeting = computed(() => {
|
||||
if (longAway.value) return "好久不见"
|
||||
const hour = zonedParts(new Date())!.hour
|
||||
if (hour < 5) return "夜深了"
|
||||
if (hour < 11) return "早上好"
|
||||
@@ -133,15 +166,69 @@ async function loadClassActivity() {
|
||||
classActivity.value = await getClassActivity()
|
||||
}
|
||||
|
||||
async function loadLastVisit() {
|
||||
lastVisit.value = await getLastVisit()
|
||||
}
|
||||
|
||||
/** 东八区的 YYYY-MM-DD,和后端 class-activity 的 day 同一个写法 */
|
||||
function dayKey(value: Date | string) {
|
||||
const p = zonedParts(value)!
|
||||
return `${p.year}-${String(p.month).padStart(2, "0")}-${String(p.day).padStart(2, "0")}`
|
||||
}
|
||||
|
||||
function dateText(value: string) {
|
||||
return parseTime(value, zonedParts(value)!.year === zonedYear() ? "M月D日" : "YYYY年M月D日")
|
||||
}
|
||||
|
||||
const WEEKDAYS = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"]
|
||||
|
||||
/** 「9月27日 周日上午」:机房一个班一周来一两次,说星期几和上下午比说几点几分好认 */
|
||||
function visitText(value: string) {
|
||||
const p = zonedParts(value)!
|
||||
const weekday = WEEKDAYS[new Date(Date.UTC(p.year, p.month - 1, p.day)).getUTCDay()]
|
||||
const part = p.hour < 12 ? "上午" : p.hour < 18 ? "下午" : "晚上"
|
||||
return `${dateText(value)} ${weekday}${part}`
|
||||
}
|
||||
|
||||
/** 班里的题是今天的(老师布置的就是今天):今天的课排最前,「上次来」只剩卡底一句 */
|
||||
const classToday = computed(
|
||||
() =>
|
||||
classActivity.value?.source === "teacher" || classActivity.value?.day === dayKey(new Date()),
|
||||
)
|
||||
|
||||
const visit = computed(() => lastVisit.value?.summary ?? null)
|
||||
|
||||
/**
|
||||
* 班里那天就是上次来的那天:机房一个班一周来一次,上次来做的往往就是班里那节课的题,
|
||||
* 「上次来」卡已经讲了,班里卡不再把同一批题列一遍,只在卡底说一句
|
||||
*/
|
||||
const visitCoversClass = computed(
|
||||
() =>
|
||||
!!visit.value &&
|
||||
!!classActivity.value?.problems.length &&
|
||||
!classToday.value &&
|
||||
classActivity.value.day === dayKey(visit.value.lastSubmitTime),
|
||||
)
|
||||
|
||||
const classSummaryText = computed(() => {
|
||||
const problems = classActivity.value?.problems ?? []
|
||||
const done = problems.filter((item) => item.myStatus === "accepted").length
|
||||
return done === problems.length
|
||||
? `那天班里一起做的 ${problems.length} 道,你都做对了`
|
||||
: `那天班里一起做了 ${problems.length} 道,你做对了 ${done} 道`
|
||||
})
|
||||
|
||||
const solvedText = computed(() =>
|
||||
(visit.value?.solved ?? []).map((item) => `${item.problemDisplayId} ${item.title}`).join("、"),
|
||||
)
|
||||
|
||||
/** 今天的就叫「今天」,往前找到的那天标出日期 —— 周一早上看到的是上周五的课 */
|
||||
const classActivityTitle = computed(() => {
|
||||
const day = classActivity.value?.day
|
||||
if (!day) return ""
|
||||
const now = zonedParts(new Date())!
|
||||
const today = `${now.year}-${String(now.month).padStart(2, "0")}-${String(now.day).padStart(2, "0")}`
|
||||
// 老师在课堂看板布置的,说成「老师布置的」,比「班里在做」更让人知道该做这几道
|
||||
if (classActivity.value?.source === "teacher") return "今天老师布置的题"
|
||||
if (day === today) return "今天班里在做"
|
||||
if (classToday.value) return "今天班里在做"
|
||||
const [, month, date] = day.split("-").map(Number)
|
||||
return `${month}月${date}日班里做了`
|
||||
})
|
||||
@@ -168,6 +255,7 @@ async function load() {
|
||||
loadAnnouncements(),
|
||||
loadWeekly(),
|
||||
loadClassActivity(),
|
||||
loadLastVisit(),
|
||||
loadKnowledge(),
|
||||
])
|
||||
loaded.value = true
|
||||
@@ -226,8 +314,11 @@ async function openAnnouncement(item: AnnouncementListItem) {
|
||||
<div class="home">
|
||||
<section class="hero">
|
||||
<div>
|
||||
<h1 class="hello">{{ greeting }},{{ userStore.user?.username }}</h1>
|
||||
<n-text depth="3">
|
||||
<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">
|
||||
@@ -256,9 +347,47 @@ async function openAnnouncement(item: AnnouncementListItem) {
|
||||
|
||||
<div class="grid">
|
||||
<div class="column">
|
||||
<!-- 「上次来」:上次登录到这次登录之间交过题才有。班里今天在做的时候让位给班里卡,
|
||||
只在那张卡底下留一句没做对的 -->
|
||||
<n-card v-if="visit && !classToday" size="small" :bordered="false" class="card">
|
||||
<template #header>上次来 · {{ visitText(lastVisit!.previousLogin!) }}</template>
|
||||
<template #header-extra>
|
||||
<n-text depth="3" class="row-meta">
|
||||
做对 {{ visit.solvedCount }} 道 · 交了 {{ visit.submissionCount }} 次
|
||||
</n-text>
|
||||
</template>
|
||||
<router-link
|
||||
v-for="item in visit.unsolved"
|
||||
:key="item.problemDisplayId"
|
||||
:to="`/problem/${item.problemDisplayId}`"
|
||||
class="unsolved"
|
||||
>
|
||||
<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 }}
|
||||
</span>
|
||||
<n-text depth="3" class="row-meta">
|
||||
交了 {{ item.attempts }} 次,最后一次是{{ JUDGE_STATUS[item.lastResult].name }}
|
||||
</n-text>
|
||||
</div>
|
||||
<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>
|
||||
<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"
|
||||
v-if="classActivity?.problems.length && !visitCoversClass"
|
||||
:title="classActivityTitle"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
@@ -282,6 +411,17 @@ async function openAnnouncement(item: AnnouncementListItem) {
|
||||
{{ 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="继续学习" size="small" :bordered="false" class="card">
|
||||
@@ -561,6 +701,62 @@ a.row:hover {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 「上次来」没做对的那几道:一道一个白底块,「接着做」是这张卡唯一的主按钮 */
|
||||
.unsolved {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(128, 128, 128, 0.18);
|
||||
background-color: var(--n-color, #fff);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.unsolved:hover {
|
||||
border-color: rgba(24, 160, 88, 0.5);
|
||||
}
|
||||
|
||||
.unsolved-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.visit-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
padding: 10px 4px 2px;
|
||||
border-top: 1px solid rgba(128, 128, 128, 0.12);
|
||||
font-size: 13px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.visit-foot.warn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.visit-foot .dot {
|
||||
flex: none;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: #f0a020;
|
||||
}
|
||||
|
||||
.visit-foot .go {
|
||||
flex: none;
|
||||
color: #18a058;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.more,
|
||||
.plain-link {
|
||||
color: inherit;
|
||||
|
||||
Reference in new issue
Block a user