学业风险雷达已接入首页,复用现有正式预警规则、检测、通知和确认流程:

学生首页:显示当前生效预警的自然语言摘要与最多 3 条重点事项,可直达处理页。
辅导员首页:显示所管班级的生效预警学生与详情,可直达完整预警列表。
风险接口加载失败会明确提示,不会误报“暂无风险”。
混合“学生 + 辅导员”角色会优先进入辅导员工作台。
This commit is contained in:
2026-08-10 10:24:54 +08:00 Unverified
parent 360af12d02
commit 545071a7dd
2 changed files with 131 additions and 1 deletions
+59
View File
@@ -83,6 +83,14 @@ interface DashboardGreeting {
}>
}
interface WarningItem {
id: string
type: number
status: number
detail: string
createdAt: string
}
const router = useRouter()
const auth = useAuthStore()
const loading = ref(true)
@@ -93,6 +101,7 @@ const unreadCount = ref(0)
const exams = ref<ExamItem[]>([])
const grades = ref<GradeItem[]>([])
const dashboardGreeting = ref<DashboardGreeting | null>(null)
const warnings = ref<WarningItem[]>([])
const now = ref(new Date())
const categoryLabels: Record<string, string> = {
@@ -173,6 +182,11 @@ const recentGrades = computed(() =>
.slice(0, 4),
)
const activeWarnings = computed(() => warnings.value.filter((warning) => warning.status === 1))
const radarSummary = computed(() => activeWarnings.value.length
? `发现 ${activeWarnings.value.length} 项需要你关注的学习风险,建议优先处理下方提示。`
: '系统暂未发现需要你处理的学业风险,继续保持当前学习节奏。')
function parseDateOnly(value?: string) {
if (!value) return null
const [year, month, day] = value.slice(0, 10).split('-').map(Number)
@@ -296,6 +310,7 @@ async function loadOverview() {
http.get('/exams/my-schedule'),
http.get('/grades/student/transcript'),
http.get<DashboardGreeting>('/dashboard/greeting'),
http.get<WarningItem[]>('/warnings/my-warnings'),
])
if (results[0].status === 'fulfilled') {
@@ -322,6 +337,11 @@ async function loadOverview() {
if (results[4].status === 'fulfilled') {
dashboardGreeting.value = results[4].value.data
}
if (results[5].status === 'fulfilled') {
warnings.value = results[5].value.data
} else {
failedSections.value.push('学业风险雷达')
}
loading.value = false
}
@@ -353,6 +373,24 @@ onMounted(loadOverview)
</section>
<p v-if="dashboardGreeting?.narrative" class="student-greeting-narrative">{{ dashboardGreeting.narrative }}</p>
<section class="student-risk-radar" :class="{ attention: activeWarnings.length }">
<header>
<div>
<span class="panel-kicker">ACADEMIC RADAR</span>
<h3>学业风险雷达</h3>
</div>
<button type="button" @click="router.push('/warnings')">查看全部 <el-icon><ArrowRight /></el-icon></button>
</header>
<p>{{ radarSummary }}</p>
<div v-if="activeWarnings.length" class="risk-list">
<button v-for="warning in activeWarnings.slice(0, 3)" :key="warning.id" type="button" @click="router.push('/warnings')">
<span>{{ categoryLabels.Warning }}</span>
<strong>{{ warning.detail }}</strong>
<i>去处理 </i>
</button>
</div>
</section>
<el-alert
v-if="failedSections.length"
type="warning"
@@ -605,6 +643,24 @@ onMounted(loadOverview)
line-height: 1.7;
}
.student-risk-radar {
padding: 20px 22px;
border: 1px solid #dce6eb;
background: #fbfdfd;
}
.student-risk-radar.attention { border-left: 3px solid #c4812a; background: #fffcf6; }
.student-risk-radar header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.student-risk-radar h3 { margin: 6px 0 0; color: var(--ink); font-size: 17px; }
.student-risk-radar header button { padding: 4px 0; display: inline-flex; align-items: center; gap: 4px; border: 0; color: #526078; background: transparent; font-size: 12px; white-space: nowrap; }
.student-risk-radar header button:hover { color: var(--indigo); }
.student-risk-radar > p { margin: 13px 0 0; color: #59677a; font-size: 12px; line-height: 1.7; }
.risk-list { margin-top: 13px; display: grid; }
.risk-list button { padding: 12px 0; display: grid; grid-template-columns: 76px minmax(0, 1fr) auto; gap: 10px; text-align: left; border: 0; border-top: 1px solid #eee6d8; background: transparent; }
.risk-list span { align-self: center; color: #a16b1c; font: 700 9px/1.4 Consolas, monospace; letter-spacing: .06em; }
.risk-list strong { color: #38475a; font-size: 12px; font-weight: 600; line-height: 1.55; }
.risk-list i { align-self: center; color: #9a743d; font-size: 11px; font-style: normal; white-space: nowrap; }
.today-status {
min-width: 180px;
margin-left: auto;
@@ -945,6 +1001,9 @@ onMounted(loadOverview)
@media (max-width: 600px) {
.student-greeting-insights { padding: 15px 18px; grid-template-columns: 1fr; }
.student-risk-radar { padding: 18px; }
.risk-list button { grid-template-columns: 1fr auto; }
.risk-list span { grid-column: 1 / -1; }
.student-overview {
gap: 12px;
}