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

学生首页:显示当前生效预警的自然语言摘要与最多 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
+72 -1
View File
@@ -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<DashboardData | null>(null)
const counselorWarnings = ref<WarningRecord[]>([])
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<DashboardData>('/dashboard')).data
if (isCounselorDashboard.value) {
counselorWarnings.value = (await http.get<WarningRecord[]>('/warnings/records', {
params: { academicTermId: data.value.currentTerm?.id },
})).data
}
} catch {
loadError.value = '教务总览暂时无法加载,请检查服务连接后重试。'
} finally {
@@ -483,6 +506,29 @@ onMounted(async () => {
</section>
<section class="dashboard-work-grid">
<article v-if="isCounselorDashboard" class="dashboard-panel counselor-radar">
<header class="panel-heading">
<div>
<span class="panel-index">RADAR / COUNSELOR</span>
<h2>需重点关注的学生</h2>
<p>{{ activeCounselorWarnings.length ? `当前有 ${activeCounselorWarnings.length} 条生效预警,按最新记录展示。` : '当前没有生效中的学业预警。' }}</p>
</div>
<button type="button" class="radar-link" @click="router.push('/warnings')">完整预警 </button>
</header>
<div v-if="activeCounselorWarnings.length" class="counselor-risk-list">
<button v-for="warning in activeCounselorWarnings.slice(0, 4)" :key="warning.id" type="button" @click="router.push('/warnings')">
<span>{{ warning.className }}</span>
<b>{{ warning.studentName }}</b>
<small>{{ warning.detail }}</small>
<i>查看 </i>
</button>
</div>
<div v-else class="todo-empty">
<el-icon><Checked /></el-icon>
<div><b>当前没有重点关注学生</b><span>新的学业预警会自动出现在这里</span></div>
</div>
</article>
<article class="dashboard-panel todo-panel">
<header class="panel-heading">
<div>
@@ -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 {