feat(首页): 登录后不再弹「登录速报」,上次来做了什么、哪道还没做对放进首页,带「接着做」
Deploy / deploy (push) Canceled after 0s

- 新接口 /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:
xuyueandClaude Opus 5.5 committed 2026-09-30 00:28:14 +08:00
1 parent 2b4676ae34
commit 91d9c9c6d7
15 files changed
+389 -289

No files matched your search

+2 -6
View File
@@ -1,4 +1,4 @@
import type { LoginSummary, Quote, WebsiteConfig } from "@oj2/contract"
import type { Quote, WebsiteConfig } from "@oj2/contract"
import api from "utils/api"
import { contractDeferred } from "utils/contract"
import type { Profile, Tag } from "utils/types"
@@ -43,14 +43,10 @@ export function getClassUsernames(classroom: string) {
return api.get<string[]>(`classes/${encodeURIComponent(classroom)}/usernames`)
}
// 下面两个是启动就要调的(config / loginSummary 两个 store),所以放在 shared 而不是
// 下面这个是启动就要调的(config store),所以放在 shared 而不是
// oj/api.ts:oj/api.ts 同步 import 了题目详情、提交详情两个 schema(它们的契约闸门必须
// 同步,submissionDetailSchema 里有 `.catch()`,放行原文和解析结果不等价),入口一旦
// 引到它,zod 整个就进了首屏包。
export function getWebsiteConfig() {
return api.get<WebsiteConfig>("site")
}
export function getAILoginSummary() {
return api.get<LoginSummary>("ai/login-summary")
}
+1 -5
View File
@@ -7,10 +7,8 @@ import { storeToRefs } from "pinia"
import { useAuthModalStore } from "../store/authModal"
import { useConfigStore } from "../store/config"
import { useUserStore } from "../store/user"
import { useLoginSummaryStore } from "../store/loginSummary"
const userStore = useUserStore()
const loginSummaryStore = useLoginSummaryStore()
const configStore = useConfigStore()
const authStore = useAuthModalStore()
@@ -142,10 +140,8 @@ function submit() {
storage.set(STORAGE_KEY.LOGIN_CLASS, form.value.class)
}
authStore.closeLoginModal()
// 「上次登录以来」不再弹框,拆进了首页的「上次来」卡(/me/last-visit)
await userStore.getMyProfile()
// 登录后弹「上次登录以来」的学情小结。移植时漏掉了这一行,
// LoginSummaryModal 一直挂在 layout 里但没人触发,整条链路等于死的
loginSummaryStore.open()
}
})
}
@@ -1,70 +0,0 @@
<script setup lang="ts">
import { useBreakpoints } from "shared/composables/breakpoints"
import { useLoginSummaryStore } from "shared/store/loginSummary"
import { parseTime } from "utils/functions"
// md-editor-v3 按需加载:这个弹框挂在默认布局上,静态 import 的话首页每次都要多拉
// ~160KB JS + ~76KB CSS,而 AI 分析只在登录后、且期间提交够 3 次才有
const MdPreview = defineAsyncComponent(() => import("./LoginSummaryPreview.vue"))
const loginSummaryStore = useLoginSummaryStore()
const { isDesktop } = useBreakpoints()
const lastLoginTime = computed(() => {
const summary = loginSummaryStore.summary
if (!summary?.start) {
return ""
}
return parseTime(summary.start, "YYYY-MM-DD HH:mm")
})
const hasAnalysis = computed(() => !!loginSummaryStore.analysis)
</script>
<template>
<n-modal
v-model:show="loginSummaryStore.show"
preset="card"
title="登录速报"
style="width: min(760px, 92vw)"
>
<n-spin :show="loginSummaryStore.loading" size="small">
<n-flex vertical size="large">
<n-text v-if="lastLoginTime">上次登录时间:{{ lastLoginTime }}</n-text>
<n-grid :cols="isDesktop ? 3 : 1" :x-gap="16" :y-gap="16">
<n-gi>
<n-statistic
label="新增题目"
:value="loginSummaryStore.summary?.newProblemCount ?? 0"
/>
</n-gi>
<n-gi>
<n-statistic
label="提交次数"
:value="loginSummaryStore.summary?.submissionCount ?? 0"
/>
</n-gi>
<n-gi>
<n-statistic label="AC 次数" :value="loginSummaryStore.summary?.acceptedCount ?? 0" />
</n-gi>
<n-gi>
<n-statistic label="AC 题目数" :value="loginSummaryStore.summary?.solvedCount ?? 0" />
</n-gi>
<n-gi>
<n-statistic
label="流程图提交"
:value="loginSummaryStore.summary?.flowchartSubmissionCount ?? 0"
/>
</n-gi>
</n-grid>
<n-divider>AI 分析</n-divider>
<n-alert v-if="loginSummaryStore.analysisError" type="warning" :show-icon="false">
{{ loginSummaryStore.analysisError }}
</n-alert>
<MdPreview v-if="hasAnalysis" :model-value="loginSummaryStore.analysis" />
<n-empty v-else description="期间提交数少于 3 次,暂不生成 AI 分析" />
</n-flex>
</n-spin>
</n-modal>
</template>
@@ -1,10 +0,0 @@
<script setup lang="ts">
import { MdPreview } from "md-editor-v3"
import "md-editor-v3/lib/preview.css"
defineProps<{ modelValue: string }>()
</script>
<template>
<MdPreview :model-value="modelValue" />
</template>
-2
View File
@@ -3,7 +3,6 @@ import Beian from "../components/Beian.vue"
import Header from "../components/Header.vue"
import Login from "../components/Login.vue"
import Signup from "../components/Signup.vue"
import LoginSummaryModal from "../components/LoginSummaryModal.vue"
import AchievementToast from "../components/AchievementToast.vue"
import { useAchievementStore } from "shared/store/achievement"
import { useUserStore } from "shared/store/user"
@@ -46,7 +45,6 @@ watch(
</n-layout-content>
<Login />
<Signup />
<LoginSummaryModal />
<AchievementToast />
<Beian />
</n-layout>
-67
View File
@@ -1,67 +0,0 @@
import type { LoginSummary as ContractLoginSummary } from "@oj2/contract"
import { getAILoginSummary } from "shared/api"
type LoginSummary = ContractLoginSummary["summary"]
export const useLoginSummaryStore = defineStore("loginSummary", () => {
const show = ref(false)
const loading = ref(false)
const summary = ref<LoginSummary | null>(null)
const analysis = ref("")
const analysisError = ref("")
function shouldShowSummary(nextSummary: LoginSummary | null) {
if (!nextSummary) {
return false
}
const values = [
nextSummary.newProblemCount,
nextSummary.submissionCount,
nextSummary.acceptedCount,
nextSummary.solvedCount,
nextSummary.flowchartSubmissionCount,
]
const zeroCount = values.filter((value) => value === 0).length
return zeroCount < Math.floor(values.length / 2) + 1
}
async function fetchSummary() {
loading.value = true
analysis.value = ""
analysisError.value = ""
try {
const res = await getAILoginSummary()
summary.value = res.summary
analysis.value = res.analysis || ""
analysisError.value = res.analysisError || ""
} catch {
analysisError.value = "获取登录统计失败,请稍后再试"
} finally {
loading.value = false
}
}
async function open() {
await fetchSummary()
if (!summary.value && analysisError.value) {
show.value = true
return
}
show.value = shouldShowSummary(summary.value)
}
function close() {
show.value = false
}
return {
show,
loading,
summary,
analysis,
analysisError,
fetchSummary,
open,
close,
}
})