56 lines
6.3 KiB
Vue
56 lines
6.3 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import { ArrowRight, Calendar, DocumentChecked, Reading } from '@element-plus/icons-vue'
|
|
import { useRouter } from 'vue-router'
|
|
import http from '../api/http'
|
|
|
|
interface Greeting { title: string; subtitle: string; narrative: string; label: string; insights: Array<{ label: string; value: string; hint: string; tone: string }> }
|
|
interface Sheet { id: string; courseName: string; taskName: string; classNames: string[]; sheet?: { status: string; studentCount: number; completedCount: number } }
|
|
const router = useRouter()
|
|
const greeting = ref<Greeting | null>(null)
|
|
const sheets = ref<Sheet[]>([])
|
|
const loading = ref(true)
|
|
const failed = ref(false)
|
|
const statusLabel: Record<string, string> = { Draft: '待登记', Returned: '已退回', Submitted: '审核中', Approved: '已审核', Published: '已发布' }
|
|
const progress = (sheet?: Sheet['sheet']) => sheet?.studentCount ? Math.round(sheet.completedCount / sheet.studentCount * 100) : 0
|
|
const pendingSheets = computed(() => sheets.value.filter(x => x.sheet?.status === 'Draft' || x.sheet?.status === 'Returned'))
|
|
async function load() {
|
|
loading.value = true; failed.value = false
|
|
try {
|
|
const dashboard = await http.get<{ currentTerm?: { id: string }; greeting: Greeting }>('/dashboard')
|
|
greeting.value = dashboard.data.greeting
|
|
const result = await http.get<{ items: Sheet[] }>('/grades/sheets', { params: { academicTermId: dashboard.data.currentTerm?.id, pageSize: 50 } })
|
|
sheets.value = result.data.items
|
|
} catch { failed.value = true } finally { loading.value = false }
|
|
}
|
|
onMounted(load)
|
|
</script>
|
|
|
|
<template>
|
|
<div v-loading="loading" class="teacher-cockpit">
|
|
<el-result v-if="failed" icon="warning" title="教学驾驶舱加载失败" sub-title="请检查服务连接后重新加载。"><template #extra><el-button type="primary" @click="load">重新加载</el-button></template></el-result>
|
|
<template v-else-if="greeting">
|
|
<section class="cockpit-hero">
|
|
<span>TEACHING COCKPIT</span><h2>{{ greeting.title }}</h2><p>{{ greeting.subtitle }}</p><small>{{ greeting.narrative }}</small>
|
|
</section>
|
|
<section class="cockpit-metrics"><article v-for="item in greeting.insights" :key="item.label" :class="item.tone"><span>{{ item.label }}</span><strong>{{ item.value }}</strong><small>{{ item.hint }}</small></article></section>
|
|
<section class="cockpit-grid">
|
|
<article class="cockpit-panel">
|
|
<header><div><span><el-icon><DocumentChecked /></el-icon> GRADE PROGRESS</span><h3>成绩登记进度</h3></div><button @click="router.push('/grades')">成绩管理 <el-icon><ArrowRight /></el-icon></button></header>
|
|
<div v-if="pendingSheets.length" class="sheet-list"><button v-for="item in pendingSheets.slice(0, 5)" :key="item.id" @click="router.push('/grades')"><div><b>{{ item.courseName }}</b><small>{{ item.classNames.join('、') || item.taskName }} · {{ statusLabel[item.sheet?.status ?? ''] ?? '待登记' }}</small></div><strong>{{ progress(item.sheet) }}%</strong></button></div>
|
|
<div v-else class="cockpit-empty"><el-icon><DocumentChecked /></el-icon><b>当前没有待提交成绩</b><span>成绩登记册会在这里按优先级显示。</span></div>
|
|
</article>
|
|
<article class="cockpit-panel">
|
|
<header><div><span><el-icon><Reading /></el-icon> TEACHING CLASSES</span><h3>本学期教学班</h3></div><button @click="router.push('/teaching-tasks')">教学任务 <el-icon><ArrowRight /></el-icon></button></header>
|
|
<div v-if="sheets.length" class="class-list"><button v-for="item in sheets.slice(0, 5)" :key="item.id" @click="router.push('/grades')"><b>{{ item.courseName }}</b><span>{{ item.classNames.join('、') || item.taskName }}</span><small>{{ item.sheet ? `${item.sheet.studentCount} 人 · 已完成 ${item.sheet.completedCount} 人` : '尚未建立成绩登记册' }}</small></button></div>
|
|
<div v-else class="cockpit-empty"><el-icon><Calendar /></el-icon><b>本学期暂未分配教学班</b><span>教学任务发布后会自动汇总到这里。</span></div>
|
|
</article>
|
|
</section>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.teacher-cockpit{display:grid;gap:18px}.cockpit-hero{padding:31px 35px;color:#fff;background:linear-gradient(120deg,#17284f,#244276 62%,#08726d);}.cockpit-hero>span,.cockpit-panel header>div>span{color:#71d9ca;font:700 10px/1 Consolas,monospace;letter-spacing:.12em}.cockpit-hero h2{margin:13px 0 8px;font:700 clamp(27px,3vw,38px)/1.2 "STZhongsong","Songti SC",serif}.cockpit-hero p{margin:0;color:#d1daf0}.cockpit-hero small{display:block;margin-top:15px;color:#aebee0;font-size:12px}.cockpit-metrics{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}.cockpit-metrics article{padding:17px;border-left:3px solid #a9bcc6;background:#f7faf9;display:grid;gap:4px}.cockpit-metrics .positive{border-color:#098174}.cockpit-metrics .attention{border-color:#ce8b2c}.cockpit-metrics span,.cockpit-metrics small{color:#657488;font-size:11px}.cockpit-metrics strong{color:#17284f;font-size:23px}.cockpit-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}.cockpit-panel{padding:23px 26px;border:1px solid #e2e7ec;background:#fff}.cockpit-panel header{display:flex;justify-content:space-between;gap:12px;border-bottom:1px solid #e9edf0}.cockpit-panel h3{margin:8px 0 16px;color:#263648;font-size:18px}.cockpit-panel header button{border:0;background:transparent;color:#526078;font-size:12px;white-space:nowrap}.cockpit-panel button{cursor:pointer}.sheet-list button,.class-list button{width:100%;padding:14px 0;display:flex;justify-content:space-between;gap:12px;text-align:left;border:0;border-bottom:1px solid #edf0f3;background:transparent}.sheet-list b,.class-list b{display:block;color:#2e3c4c;font-size:14px}.sheet-list small,.class-list span,.class-list small{display:block;margin-top:5px;color:#748093;font-size:11px}.sheet-list strong{align-self:center;color:#0b8175;font:700 18px Consolas,monospace}.cockpit-empty{min-height:170px;display:grid;place-content:center;justify-items:center;color:#8c96a5;gap:8px;text-align:center;font-size:12px}.cockpit-empty .el-icon{font-size:26px}.cockpit-empty b{color:#5e6c7e}@media(max-width:760px){.cockpit-hero{padding:25px 21px}.cockpit-metrics,.cockpit-grid{grid-template-columns:1fr}.cockpit-panel{padding:20px 18px}}
|
|
</style>
|