update
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
2026-08-30 22:11:31 -06:00
parent 38b81881d5
commit 0e083225f1
2 changed files with 65 additions and 25 deletions

View File

@@ -40,6 +40,18 @@ const router = useRouter()
const { isMobile, isDesktop } = useBreakpoints() const { isMobile, isDesktop } = useBreakpoints()
const { learnStep } = useLearnProgress() const { learnStep } = useLearnProgress()
/**
* 课堂求助的入口收进姓名下拉里了,顶栏只留姓名按钮上的角标 —— 老师不用展开
* 菜单也能看见有没有人举手。桌面端限定,教师端接单后要在弹框里替学生写代码。
*/
const showHelpRequests = ref(false)
const hasHelpEntry = computed(
() => isDesktop.value && userStore.isTeacherOrAbove,
)
const pendingHelpCount = computed(() =>
hasHelpEntry.value ? collabStore.pendingCount : 0,
)
const isDark = useDark() const isDark = useDark()
/** /**
@@ -241,6 +253,17 @@ const menus = computed<MenuOption[]>(() => [
]) ])
const options = computed<Array<DropdownOption | DropdownDividerOption>>(() => [ const options = computed<Array<DropdownOption | DropdownDividerOption>>(() => [
{
label: pendingHelpCount.value
? `课堂求助(${pendingHelpCount.value}`
: "课堂求助",
key: "help",
show: hasHelpEntry.value,
icon: renderIcon("streamline-emojis:raising-hands-2"),
props: {
onClick: () => (showHelpRequests.value = true),
},
},
{ {
label: "我的主页", label: "我的主页",
key: "home", key: "home",
@@ -348,15 +371,16 @@ function handleMenuSelect(key: string) {
> >
{{ screenMode }} {{ screenMode }}
</n-button> </n-button>
<HelpRequestList v-if="isDesktop && userStore.isTeacherOrAbove" />
<div v-if="userStore.isFinished"> <div v-if="userStore.isFinished">
<n-dropdown v-if="userStore.isAuthed" :options="options" size="large"> <n-dropdown v-if="userStore.isAuthed" :options="options" size="large">
<n-button> <n-badge :value="pendingHelpCount" :max="99">
<Icon :icon="avatar" height="20"></Icon> <n-button>
<span style="padding-left: 8px"> <Icon :icon="avatar" height="20"></Icon>
{{ userStore.user!.username }} <span style="padding-left: 8px">
</span> {{ userStore.user!.username }}
</n-button> </span>
</n-button>
</n-badge>
</n-dropdown> </n-dropdown>
<n-flex align="center" v-else> <n-flex align="center" v-else>
<n-button <n-button
@@ -389,6 +413,7 @@ function handleMenuSelect(key: string) {
整个丢弃header 行随之丢掉 `max-width: 2000px` 那条居中样式。 整个丢弃header 行随之丢掉 `max-width: 2000px` 那条居中样式。
n-modal 默认 teleport 到 body塞在这里不影响它的实际渲染位置。 n-modal 默认 teleport 到 body塞在这里不影响它的实际渲染位置。
--> -->
<HelpRequestList v-if="hasHelpEntry" v-model:show="showHelpRequests" />
<CollabModal v-if="userStore.isTeacherOrAbove" /> <CollabModal v-if="userStore.isTeacherOrAbove" />
</n-flex> </n-flex>
</template> </template>

View File

@@ -2,18 +2,35 @@
import { Icon } from "@iconify/vue" import { Icon } from "@iconify/vue"
import { useCollabStore } from "shared/store/collab" import { useCollabStore } from "shared/store/collab"
/** 由顶栏的姓名下拉菜单打开 */
const show = defineModel<boolean>("show", { default: false })
const collabStore = useCollabStore() const collabStore = useCollabStore()
// 等待时长要每秒走一格,所以自己转一个 now // 等待时长要每秒走一格,所以自己转一个 now
// 只在弹框开着时转 —— 这个组件跟着顶栏常驻,关着的时候没人看这个数。
const now = ref(Date.now()) const now = ref(Date.now())
let timer: number | null = null let timer: number | null = null
onMounted(() => {
timer = window.setInterval(() => (now.value = Date.now()), 1000) function stopTimer() {
}) if (timer === null) return
onUnmounted(() => { window.clearInterval(timer)
if (timer !== null) window.clearInterval(timer) timer = null
}
watch(show, (opened) => {
if (!opened) {
stopTimer()
return
}
now.value = Date.now()
if (timer === null) {
timer = window.setInterval(() => (now.value = Date.now()), 1000)
}
}) })
onUnmounted(stopTimer)
const waited = (createdAt: number) => { const waited = (createdAt: number) => {
const seconds = Math.max(0, Math.floor((now.value - createdAt) / 1000)) const seconds = Math.max(0, Math.floor((now.value - createdAt) / 1000))
const m = Math.floor(seconds / 60) const m = Math.floor(seconds / 60)
@@ -25,21 +42,19 @@ const handleAccept = (studentId: number, status: string) => {
// 已被别的老师接走的不能点 // 已被别的老师接走的不能点
if (status === "active") return if (status === "active") return
collabStore.accept(studentId) collabStore.accept(studentId)
// 接单后马上要弹 CollabModal这个列表得让位
show.value = false
} }
</script> </script>
<template> <template>
<n-popover trigger="click" placement="bottom-end" style="padding: 0"> <n-modal
<template #trigger> v-model:show="show"
<n-badge :value="collabStore.pendingCount" :max="99"> preset="card"
<n-button> title="课堂求助"
<Icon icon="streamline-emojis:raising-hands-2" height="20" /> :style="{ width: '420px' }"
<span style="padding-left: 8px">求助</span> >
</n-button> <div style="max-height: 60vh; overflow: auto">
</n-badge>
</template>
<div style="width: 320px; max-height: 60vh; overflow: auto; padding: 8px">
<n-empty v-if="collabStore.groupedRequests.length === 0" description="暂无求助" /> <n-empty v-if="collabStore.groupedRequests.length === 0" description="暂无求助" />
<div v-for="group in collabStore.groupedRequests" :key="group.problemId"> <div v-for="group in collabStore.groupedRequests" :key="group.problemId">
@@ -94,5 +109,5 @@ const handleAccept = (studentId: number, status: string) => {
<n-divider style="margin: 4px 0" /> <n-divider style="margin: 4px 0" />
</div> </div>
</div> </div>
</n-popover> </n-modal>
</template> </template>