From 545071a7dd691390e58fad1986604bcb7b8b5c63 Mon Sep 17 00:00:00 2001 From: biss Date: Mon, 10 Aug 2026 10:24:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AD=A6=E4=B8=9A=E9=A3=8E=E9=99=A9=E9=9B=B7?= =?UTF-8?q?=E8=BE=BE=E5=B7=B2=E6=8E=A5=E5=85=A5=E9=A6=96=E9=A1=B5=EF=BC=8C?= =?UTF-8?q?=E5=A4=8D=E7=94=A8=E7=8E=B0=E6=9C=89=E6=AD=A3=E5=BC=8F=E9=A2=84?= =?UTF-8?q?=E8=AD=A6=E8=A7=84=E5=88=99=E3=80=81=E6=A3=80=E6=B5=8B=E3=80=81?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=E5=92=8C=E7=A1=AE=E8=AE=A4=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=EF=BC=9A=20=E5=AD=A6=E7=94=9F=E9=A6=96=E9=A1=B5=EF=BC=9A?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E5=BD=93=E5=89=8D=E7=94=9F=E6=95=88=E9=A2=84?= =?UTF-8?q?=E8=AD=A6=E7=9A=84=E8=87=AA=E7=84=B6=E8=AF=AD=E8=A8=80=E6=91=98?= =?UTF-8?q?=E8=A6=81=E4=B8=8E=E6=9C=80=E5=A4=9A=203=20=E6=9D=A1=E9=87=8D?= =?UTF-8?q?=E7=82=B9=E4=BA=8B=E9=A1=B9=EF=BC=8C=E5=8F=AF=E7=9B=B4=E8=BE=BE?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=A1=B5=E3=80=82=20=E8=BE=85=E5=AF=BC?= =?UTF-8?q?=E5=91=98=E9=A6=96=E9=A1=B5=EF=BC=9A=E6=98=BE=E7=A4=BA=E6=89=80?= =?UTF-8?q?=E7=AE=A1=E7=8F=AD=E7=BA=A7=E7=9A=84=E7=94=9F=E6=95=88=E9=A2=84?= =?UTF-8?q?=E8=AD=A6=E5=AD=A6=E7=94=9F=E4=B8=8E=E8=AF=A6=E6=83=85=EF=BC=8C?= =?UTF-8?q?=E5=8F=AF=E7=9B=B4=E8=BE=BE=E5=AE=8C=E6=95=B4=E9=A2=84=E8=AD=A6?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E3=80=82=20=E9=A3=8E=E9=99=A9=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=8A=A0=E8=BD=BD=E5=A4=B1=E8=B4=A5=E4=BC=9A=E6=98=8E?= =?UTF-8?q?=E7=A1=AE=E6=8F=90=E7=A4=BA=EF=BC=8C=E4=B8=8D=E4=BC=9A=E8=AF=AF?= =?UTF-8?q?=E6=8A=A5=E2=80=9C=E6=9A=82=E6=97=A0=E9=A3=8E=E9=99=A9=E2=80=9D?= =?UTF-8?q?=E3=80=82=20=E6=B7=B7=E5=90=88=E2=80=9C=E5=AD=A6=E7=94=9F=20+?= =?UTF-8?q?=20=E8=BE=85=E5=AF=BC=E5=91=98=E2=80=9D=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E4=BC=9A=E4=BC=98=E5=85=88=E8=BF=9B=E5=85=A5=E8=BE=85=E5=AF=BC?= =?UTF-8?q?=E5=91=98=E5=B7=A5=E4=BD=9C=E5=8F=B0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/views/DashboardView.vue | 73 +++++++++++++++++++++++++- web/src/views/StudentDashboardView.vue | 59 +++++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) diff --git a/web/src/views/DashboardView.vue b/web/src/views/DashboardView.vue index aa61024..f2477bd 100644 --- a/web/src/views/DashboardView.vue +++ b/web/src/views/DashboardView.vue @@ -83,24 +83,42 @@ interface DashboardLink { icon: Component } +interface WarningRecord { + id: string + studentName: string + studentNumber: string + className: string + status: number + detail: string +} + const router = useRouter() const auth = useAuthStore() const roles = computed(() => auth.user?.roles ?? []) const isStudentOverview = computed(() => roles.value.includes('Student') && !roles.value.some((role) => - ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(role), + ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor'].includes(role), ), ) const loading = ref(true) const loadError = ref('') const data = ref(null) +const counselorWarnings = ref([]) const now = ref(new Date()) function hasRole(...allowedRoles: string[]) { return roles.value.some((role) => allowedRoles.includes(role)) } +const isCounselorDashboard = computed(() => + hasRole('Counselor') && !hasRole('SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'), +) + +const activeCounselorWarnings = computed(() => + counselorWarnings.value.filter((warning) => warning.status === 1), +) + function parseDateOnly(value?: string) { if (!value) return null const [year, month, day] = value.slice(0, 10).split('-').map(Number) @@ -366,6 +384,11 @@ async function loadDashboard() { loadError.value = '' try { data.value = (await http.get('/dashboard')).data + if (isCounselorDashboard.value) { + counselorWarnings.value = (await http.get('/warnings/records', { + params: { academicTermId: data.value.currentTerm?.id }, + })).data + } } catch { loadError.value = '教务总览暂时无法加载,请检查服务连接后重试。' } finally { @@ -483,6 +506,29 @@ onMounted(async () => {
+
+
+
+ RADAR / COUNSELOR +

需重点关注的学生

+

{{ activeCounselorWarnings.length ? `当前有 ${activeCounselorWarnings.length} 条生效预警,按最新记录展示。` : '当前没有生效中的学业预警。' }}

+
+ +
+
+ +
+
+ +
当前没有重点关注学生新的学业预警会自动出现在这里。
+
+
+
@@ -943,6 +989,28 @@ onMounted(async () => { background: var(--dashboard-paper); } +.counselor-radar { border-top: 3px solid var(--dashboard-amber); } +.radar-link { padding: 5px 0; border: 0; color: #806136; background: transparent; font-size: 12px; white-space: nowrap; } +.radar-link:hover { color: var(--dashboard-blue); } +.counselor-risk-list { display: grid; } +.counselor-risk-list button { + padding: 13px 0; + display: grid; + grid-template-columns: 76px 68px minmax(0, 1fr) auto; + gap: 10px; + align-items: center; + text-align: left; + border: 0; + border-bottom: 1px solid #eee8dc; + background: transparent; +} +.counselor-risk-list button:last-child { border-bottom: 0; } +.counselor-risk-list button:hover b { color: var(--dashboard-blue); } +.counselor-risk-list span { color: #987337; font: 700 9px/1.3 Consolas, monospace; letter-spacing: .04em; } +.counselor-risk-list b { color: #3a4758; font-size: 13px; } +.counselor-risk-list small { overflow: hidden; color: #6c7788; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } +.counselor-risk-list i { color: #8d7042; font-size: 11px; font-style: normal; white-space: nowrap; } + .todo-panel, .quick-panel, .readiness-panel { @@ -1250,6 +1318,9 @@ button:focus-visible { @media (max-width: 760px) { .greeting-insights { padding: 15px; } .greeting-insights > div { grid-template-columns: 1fr; } + .counselor-risk-list button { grid-template-columns: 1fr auto; } + .counselor-risk-list span { grid-column: 1 / -1; } + .counselor-risk-list small { white-space: normal; } .admin-dashboard { gap: 10px; } .overview-hero { diff --git a/web/src/views/StudentDashboardView.vue b/web/src/views/StudentDashboardView.vue index 3e3e209..96a3ac0 100644 --- a/web/src/views/StudentDashboardView.vue +++ b/web/src/views/StudentDashboardView.vue @@ -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([]) const grades = ref([]) const dashboardGreeting = ref(null) +const warnings = ref([]) const now = ref(new Date()) const categoryLabels: Record = { @@ -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('/dashboard/greeting'), + http.get('/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)

{{ dashboardGreeting.narrative }}

+
+
+
+ ACADEMIC RADAR +

学业风险雷达

+
+ +
+

{{ radarSummary }}

+
+ +
+
+ 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; }