Files
Academic-Affairs-System/web/src/views/TeacherRosterView.vue
T
biss c91d826169 学年学期页面新增“设为当前”“归档”“撤销归档”,当前学期不能归档、删除或直接取消;已归档学期需先撤销才能重新设为当前。[BaseDataController.cs (line 286)](/E:/jiaowu/src/Jiaowu.Api/Controllers/BaseDataController.cs:286)
切换后,其他学期自动变为历史态;已归档学期进一步弱化。所有主要学期选择器统一默认当前,并显示“当前 / 历史 / 已归档”。[academicTerms.ts (line 1)](/E:/jiaowu/web/src/utils/academicTerms.ts:1)
桌面使用分层表格,390px 手机端改为独立学期卡片,操作不会再挤压表格。[BaseDataView.vue (line 211)](/E:/jiaowu/web/src/views/BaseDataView.vue:211)
归档定义为“历史显示状态”,不是成绩冻结:补考成绩仍可录入并回写,已发布成绩仍可走成绩更正审批。
已加入 SQLite 开发迁移、MySQL 正式迁移和切换/归档/撤销测试。[20260726143000_AcademicTermArchiving.cs (line 1)](/E:/jiaowu/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260726143000_AcademicTermArchiving.cs:1)
2026-07-26 11:14:05 +08:00

164 lines
6.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { Download, Refresh } from '@element-plus/icons-vue'
import http, { apiErrorMessage } from '../api/http'
import { downloadApiFile } from '../api/excel'
import { academicTermLabel, academicTermOptionClass, defaultAcademicTermId } from '../utils/academicTerms'
const loading = ref(false)
const detailLoading = ref(false)
const terms = ref<any[]>([])
const offerings = ref<any[]>([])
const selectedOffering = ref<any>(null)
const roster = ref<any>(null)
const termId = ref<string>()
async function loadOfferings() {
loading.value = true
try {
offerings.value = (await http.get('/course-selections/my-offerings', {
params: { academicTermId: termId.value || undefined },
})).data
const preferred = offerings.value.find((item: any) => item.id === selectedOffering.value?.id)
?? offerings.value[0]
if (preferred) await selectOffering(preferred)
else {
selectedOffering.value = null
roster.value = null
}
} catch (error) {
ElMessage.error(apiErrorMessage(error))
} finally {
loading.value = false
}
}
async function selectOffering(offering: any) {
selectedOffering.value = offering
detailLoading.value = true
try {
roster.value = (await http.get(`/course-selections/offerings/${offering.id}/roster`)).data
} catch (error) {
roster.value = null
ElMessage.error(apiErrorMessage(error))
} finally {
detailLoading.value = false
}
}
async function exportRoster() {
if (!selectedOffering.value) return
try {
await downloadApiFile(
`/course-selections/offerings/${selectedOffering.value.id}/roster/export.xlsx`,
`选课名单-${selectedOffering.value.taskNumber}.xlsx`,
)
} catch (error) {
ElMessage.error(apiErrorMessage(error))
}
}
onMounted(async () => {
terms.value = (await http.get('/base-data/terms')).data
termId.value = defaultAcademicTermId(terms.value)
await loadOfferings()
})
</script>
<template>
<div class="page-stack roster-page">
<section class="page-intro">
<div>
<span class="section-kicker">ENROLLMENT ROSTER</span>
<h2>选课名单</h2>
<p>查看本人授课班级的选课学生名单支持 Excel 导出</p>
</div>
<el-button :icon="Refresh" @click="loadOfferings">刷新</el-button>
</section>
<section class="roster-toolbar">
<el-select v-model="termId" clearable placeholder="全部学期" @change="loadOfferings">
<el-option v-for="term in terms" :key="term.id" :label="academicTermLabel(term)" :value="term.id" :class="academicTermOptionClass(term)" />
</el-select>
<span> {{ offerings.length }} 个教学班</span>
</section>
<section class="roster-workspace" v-loading="loading">
<aside class="roster-task-list">
<button
v-for="offering in offerings"
:key="offering.id"
type="button"
:class="{ active: selectedOffering?.id === offering.id }"
@click="selectOffering(offering)"
>
<span>{{ offering.courseCode }} · {{ offering.taskNumber }}</span>
<b>{{ offering.courseName }}</b>
<small>{{ offering.termName }} · {{ offering.roundName }}</small>
<i>{{ offering.enrolledCount }} / {{ offering.capacity }} </i>
</button>
<el-empty v-if="!offerings.length" description="当前学期没有选课教学班" />
</aside>
<main class="roster-detail" v-loading="detailLoading">
<el-empty v-if="!selectedOffering" description="请选择一个教学班" />
<template v-else-if="roster">
<header class="roster-head">
<div>
<span>{{ roster.courseCode }} · {{ roster.taskNumber }}</span>
<h3>{{ roster.courseName }}选课名单</h3>
<p>已选 {{ roster.enrolledCount }} / 容量 {{ roster.capacity }} </p>
</div>
<el-button :icon="Download" @click="exportRoster">导出 Excel</el-button>
</header>
<el-table :data="roster.students" class="data-table">
<el-table-column label="学号" width="135">
<template #default="{ row }"><span class="registry-number">{{ row.studentNumber }}</span></template>
</el-table-column>
<el-table-column prop="name" label="姓名" min-width="100" />
<el-table-column prop="className" label="班级" min-width="130" />
<el-table-column prop="majorName" label="专业" min-width="150" />
<el-table-column label="选课时间" width="170">
<template #default="{ row }">{{ new Date(row.enrolledAt).toLocaleString('zh-CN') }}</template>
</el-table-column>
</el-table>
</template>
</main>
</section>
</div>
</template>
<style scoped>
.roster-toolbar {
min-height: 56px; padding: 10px 16px;
display: flex; align-items: center; gap: 12px;
border: 1px solid var(--line); background: #fbfcfd;
}
.roster-toolbar .el-select { width: 260px; }
.roster-toolbar > span { color: var(--muted); font-size: 12px; }
.roster-workspace { display: grid; grid-template-columns: 280px minmax(0, 1fr); min-height: 420px; border: 1px solid var(--line); background: white; }
.roster-task-list { border-right: 1px solid var(--line); overflow-y: auto; max-height: 600px; }
.roster-task-list button {
display: grid; gap: 3px; width: 100%; padding: 14px 16px; border: none; border-bottom: 1px solid #edf0f4;
background: none; cursor: pointer; text-align: left; transition: background .15s;
}
.roster-task-list button:hover { background: #f5f7fa; }
.roster-task-list button.active { background: #e9f3f5; border-left: 3px solid #176b87; }
.roster-task-list button > span { color: var(--teal); font: 700 10px/1.2 Consolas, monospace; }
.roster-task-list button > b { font-size: 14px; }
.roster-task-list button > small { color: var(--muted); font-size: 11px; }
.roster-task-list button > i { color: var(--indigo); font-size: 10px; font-weight: 700; }
.roster-detail { min-height: 420px; }
.roster-head {
padding: 18px 22px; display: flex; align-items: center; justify-content: space-between; gap: 16px;
border-bottom: 1px solid var(--line); background: #f8fafb;
}
.roster-head h3 { margin: 5px 0 4px; font-size: 18px; }
.roster-head p { color: var(--muted); font-size: 12px; margin: 0; }
.roster-head span { color: var(--teal); font-size: 10px; font-weight: 700; }
@media (max-width: 760px) {
.roster-workspace { grid-template-columns: 1fr; }
.roster-task-list { max-height: 220px; border-right: none; border-bottom: 1px solid var(--line); }
}
</style>