- 新接口 /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
@@ -9,6 +9,7 @@ import {
|
||||
type Metrics,
|
||||
type TutorialSummary,
|
||||
type ClassActivity,
|
||||
type LastVisit,
|
||||
type ClassBoard,
|
||||
type ClassLesson,
|
||||
type KnowledgeMap,
|
||||
@@ -246,6 +247,11 @@ export function getClassActivity() {
|
||||
return api.get<ClassActivity>("me/class-activity")
|
||||
}
|
||||
|
||||
/** 首页「上次来」卡:上次登录到这次登录之间交过什么、哪道还没做对 */
|
||||
export function getLastVisit() {
|
||||
return api.get<LastVisit>("me/last-visit")
|
||||
}
|
||||
|
||||
export function getClassRank(grade?: number | null) {
|
||||
return api.get<ClassRankItem[]>("rankings/classes", { params: { grade } })
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
@@ -292,7 +292,7 @@ export type { WeeklyRank, WeeklyRankItem } from "@oj2/contract"
|
||||
export type { ClassComparison, ClassRankItem, ClassUserRank } from "@oj2/contract"
|
||||
|
||||
/** 班里最近一次一起做的题(课上老师点的那几道),见后端 `/me/class-activity` */
|
||||
export type { ClassActivity, ClassActivityProblem } from "@oj2/contract"
|
||||
export type { ClassActivity, ClassActivityProblem, LastVisit } from "@oj2/contract"
|
||||
|
||||
/** 课堂看板:一个班今天这节课的题 × 全班学生 */
|
||||
export type { ClassBoard, ClassBoardStudent, ClassBoardCell, ClassLesson } from "@oj2/contract"
|
||||
|
||||
Reference in new issue
Block a user