学年学期页面新增“设为当前”“归档”“撤销归档”,当前学期不能归档、删除或直接取消;已归档学期需先撤销才能重新设为当前。[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)
This commit is contained in:
biss committed 2026-07-26 11:14:05 +08:00
1 parent 3b73292d93
commit c91d826169
32 files changed
+752 -73

No files matched your search

+149 -7
View File
@@ -5,9 +5,19 @@ import { useRoute } from 'vue-router'
import http, { apiErrorMessage } from '../api/http'
import { downloadApiFile, importExcel } from '../api/excel'
import { useAuthStore } from '../stores/auth'
import { academicTermRowClass } from '../utils/academicTerms'
type Kind = 'campuses' | 'colleges' | 'majors' | 'classes' | 'terms' | 'buildings' | 'classrooms' | 'course-categories'
interface Row { id: string; code: string; name: string; isEnabled: boolean; [key: string]: unknown }
interface Row {
id: string
code: string
name: string
isEnabled: boolean
academicYear?: string
isCurrent?: boolean
isArchived?: boolean
[key: string]: unknown
}
const allTabs: { key: Kind; label: string; hint: string }[] = [
{ key: 'campuses', label: '校区', hint: '学校的物理校区' },
@@ -210,6 +220,47 @@ async function remove(row: any) {
}
}
function termTableRowClass({ row }: { row: any }) {
return active.value === 'terms' ? academicTermRowClass(row) : ''
}
async function setCurrentTerm(row: any) {
try {
await ElMessageBox.confirm(
`将“${row.name}”切换为当前学期?其他学期会自动转为历史显示,但不会自动归档,也不会冻结成绩和补考。`,
'切换当前学期',
{ type: 'warning', confirmButtonText: '切换为当前', cancelButtonText: '取消' },
)
await http.post(`/base-data/terms/${row.id}/set-current`)
ElMessage.success(`当前学期已切换为“${row.name}”`)
await load()
} catch (error: any) {
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
}
}
async function toggleTermArchive(row: any) {
const restoring = row.isArchived
try {
await ElMessageBox.confirm(
restoring
? `撤销“${row.name}”的归档状态?撤销后仍作为历史学期显示,可再次设为当前。`
: `归档“${row.name}”?归档只降低历史数据的显示优先级,不会冻结成绩更正或补考成绩录入。`,
restoring ? '撤销学期归档' : '归档学期',
{
type: restoring ? 'info' : 'warning',
confirmButtonText: restoring ? '撤销归档' : '确认归档',
cancelButtonText: '取消',
},
)
await http.post(`/base-data/terms/${row.id}/${restoring ? 'unarchive' : 'archive'}`)
ElMessage.success(restoring ? '已撤销归档' : '学期已归档')
await load()
} catch (error: any) {
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
}
}
onMounted(async () => {
await Promise.all([load(), loadReferences()])
})
@@ -276,7 +327,65 @@ watch(
<span>共 {{ filteredRows.length }} 条</span>
</div>
<el-table v-loading="loading" :data="filteredRows" class="data-table">
<el-alert
v-if="active === 'terms'"
class="term-policy-note"
type="info"
:closable="false"
show-icon
title="切换当前学期只改变默认工作学期;归档需手动执行,归档后仍可处理补考与成绩更正。"
/>
<div v-if="active === 'terms'" class="term-mobile-list">
<article
v-for="row in filteredRows"
:key="row.id"
:class="academicTermRowClass(row)"
>
<header>
<div>
<span>{{ row.code }}</span>
<h3>{{ row.name }}</h3>
<p>{{ row.academicYear }}</p>
</div>
<el-tag v-if="row.isCurrent" type="success">当前</el-tag>
<el-tag v-else-if="row.isArchived" type="info">已归档</el-tag>
<span v-else class="term-history-label">历史学期</span>
</header>
<footer v-if="canManage">
<el-button
v-if="!row.isCurrent && !row.isArchived"
size="small"
type="success"
plain
@click="setCurrentTerm(row)"
>设为当前</el-button>
<el-button
v-if="!row.isCurrent"
size="small"
:type="row.isArchived ? 'primary' : 'warning'"
plain
@click="toggleTermArchive(row)"
>{{ row.isArchived ? '撤销归档' : '归档' }}</el-button>
<el-button size="small" @click="openEdit(row)">编辑</el-button>
<el-button
v-if="!row.isCurrent"
size="small"
type="danger"
plain
@click="remove(row)"
>删除</el-button>
</footer>
</article>
</div>
<el-table
v-loading="loading"
:data="filteredRows"
:row-class-name="termTableRowClass"
class="data-table"
:class="{ 'term-desktop-table': active === 'terms' }"
>
<el-table-column prop="code" label="编码" min-width="130" />
<el-table-column prop="name" label="名称" min-width="180" />
<el-table-column v-if="active === 'colleges'" prop="campusName" label="所属校区" min-width="140" />
@@ -288,8 +397,12 @@ watch(
<template #default="{ row }">{{ row.counselorName || '未分配' }}</template>
</el-table-column>
<el-table-column v-if="active === 'terms'" prop="academicYear" label="学年" width="120" />
<el-table-column v-if="active === 'terms'" label="当前学期" width="100">
<template #default="{ row }"><el-tag v-if="row.isCurrent" type="success">当前</el-tag><span v-else>—</span></template>
<el-table-column v-if="active === 'terms'" label="运行状态" width="120">
<template #default="{ row }">
<el-tag v-if="row.isCurrent" type="success">当前</el-tag>
<el-tag v-else-if="row.isArchived" type="info">已归档</el-tag>
<span v-else>历史学期</span>
</template>
</el-table-column>
<el-table-column v-if="active === 'buildings'" prop="campusName" label="所属校区" min-width="140" />
<el-table-column v-if="active === 'classrooms'" prop="buildingName" label="教学楼" min-width="130" />
@@ -299,10 +412,34 @@ watch(
<span class="table-status" :class="{ off: !row.isEnabled }">{{ row.isEnabled ? '启用' : '停用' }}</span>
</template>
</el-table-column>
<el-table-column v-if="canManage" label="操作" width="150" fixed="right">
<el-table-column
v-if="canManage"
label="操作"
:width="active === 'terms' ? 280 : 150"
fixed="right"
>
<template #default="{ row }">
<template v-if="active === 'terms'">
<el-button
v-if="!row.isCurrent && !row.isArchived"
link
type="success"
@click="setCurrentTerm(row)"
>设为当前</el-button>
<el-button
v-if="!row.isCurrent"
link
:type="row.isArchived ? 'primary' : 'warning'"
@click="toggleTermArchive(row)"
>{{ row.isArchived ? '撤销归档' : '归档' }}</el-button>
</template>
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="remove(row)">删除</el-button>
<el-button
v-if="active !== 'terms' || !row.isCurrent"
link
type="danger"
@click="remove(row)"
>删除</el-button>
</template>
</el-table-column>
<template #empty><el-empty description="暂无数据,点击右上角开始新增" /></template>
@@ -366,7 +503,12 @@ watch(
<el-form-item label="开始日期"><el-date-picker v-model="form.startDate" value-format="YYYY-MM-DD" /></el-form-item>
<el-form-item label="结束日期"><el-date-picker v-model="form.endDate" value-format="YYYY-MM-DD" /></el-form-item>
</div>
<el-form-item><el-checkbox v-model="form.isCurrent">设为当前学期</el-checkbox></el-form-item>
<el-form-item>
<el-checkbox v-model="form.isCurrent" :disabled="form.isArchived">
设为当前学期
</el-checkbox>
<span v-if="form.isArchived" class="field-help">已归档学期需先撤销归档。</span>
</el-form-item>
</template>
<el-form-item v-if="active === 'classrooms'" label="所属教学楼" required>
<el-select v-model="form.buildingId">
+3 -2
View File
@@ -3,6 +3,7 @@ import { computed, onMounted, reactive, ref } from 'vue'
import { Bell, Check, Plus, Refresh } from '@element-plus/icons-vue'
import http, { apiErrorMessage } from '../api/http'
import { useAuthStore } from '../stores/auth'
import { academicTermLabel, academicTermOptionClass, defaultAcademicTermId } from '../utils/academicTerms'
const auth = useAuthStore()
const isManager = computed(() =>
@@ -172,7 +173,7 @@ onMounted(async () => {
])
terms.value = termRes.data
tasks.value = taskRes.data.items
termId.value = terms.value.find(t => t.isCurrent)?.id
termId.value = defaultAcademicTermId(terms.value)
} catch (_) {}
await load()
})
@@ -204,7 +205,7 @@ function showCancel(type: string) { return type === 'Cancel' }
<section class="adj-toolbar">
<el-select v-model="termId" clearable placeholder="全部学期" @change="load">
<el-option v-for="t in terms" :key="t.id" :label="t.name" :value="t.id" />
<el-option v-for="t in terms" :key="t.id" :label="academicTermLabel(t)" :value="t.id" :class="academicTermOptionClass(t)" />
</el-select>
<el-segmented v-model="tab" :options="[
...(isTeacher ? [{ label: '我的申请', value: 'mine' }] : []),
+10 -4
View File
@@ -12,6 +12,7 @@ import {
} from '@element-plus/icons-vue'
import http, { apiErrorMessage } from '../api/http'
import { useAuthStore } from '../stores/auth'
import { academicTermLabel, academicTermOptionClass, defaultAcademicTermId } from '../utils/academicTerms'
const auth = useAuthStore()
const managerRoles = ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin']
@@ -309,6 +310,8 @@ async function loadRounds(keepSelection = true) {
rounds.value = (await http.get('/course-selections/rounds')).data
const previousId = keepSelection ? selectedRound.value?.id : undefined
const preferred = rounds.value.find((item) => item.id === previousId)
?? rounds.value.find((item) => item.isAvailableNow && item.termIsCurrent)
?? rounds.value.find((item) => item.termIsCurrent)
?? rounds.value.find((item) => item.isAvailableNow)
?? rounds.value[0]
if (preferred) await selectRound(preferred)
@@ -362,12 +365,11 @@ async function loadTasks(academicTermId: string) {
function openRound(round?: any) {
editingRoundId.value = round?.id ?? ''
const currentTerm = terms.value.find((item) => item.isCurrent)
const now = new Date()
const ends = new Date(now.getTime() + 7 * 86400000)
const withdrawal = new Date(now.getTime() + 14 * 86400000)
Object.assign(roundForm, {
academicTermId: round?.academicTermId ?? currentTerm?.id,
academicTermId: round?.academicTermId ?? defaultAcademicTermId(terms.value),
name: round?.name ?? '',
startsAt: round ? toPickerValue(round.startsAt) : toPickerValue(now.toISOString()),
endsAt: round ? toPickerValue(round.endsAt) : toPickerValue(ends.toISOString()),
@@ -719,7 +721,11 @@ onMounted(async () => {
v-for="round in rounds"
:key="round.id"
type="button"
:class="{ active: selectedRound?.id === round.id }"
:class="{
active: selectedRound?.id === round.id,
'historical-record': !round.termIsCurrent && !round.termIsArchived,
'archived-record': round.termIsArchived,
}"
@click="selectRound(round)"
>
<span>{{ round.termName }}</span>
@@ -1056,7 +1062,7 @@ onMounted(async () => {
<div class="form-grid">
<el-form-item label="开课学期" required>
<el-select v-model="roundForm.academicTermId">
<el-option v-for="term in terms" :key="term.id" :label="term.name" :value="term.id" />
<el-option v-for="term in terms" :key="term.id" :label="academicTermLabel(term)" :value="term.id" :class="academicTermOptionClass(term)" />
</el-select>
</el-form-item>
<el-form-item label="批次名称" required>
Loaded 3 of 32 files, more files were not shown because too many files have changed in this diff. Show more