学年学期页面新增“设为当前”“归档”“撤销归档”,当前学期不能归档、删除或直接取消;已归档学期需先撤销才能重新设为当前。[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:
1 parent
3b73292d93
commit
c91d826169
32 files changed
+752
-73
No files matched your search
@@ -182,6 +182,160 @@ button { cursor: pointer; }
|
||||
.table-toolbar .el-input { width: min(340px, 60vw); }
|
||||
.table-toolbar > span { margin-left: auto; color: var(--muted); font-size: 11px; }
|
||||
.data-table { min-height: 360px; }
|
||||
|
||||
.el-select-dropdown__item.academic-term-option {
|
||||
transition: color 160ms ease, opacity 160ms ease, background-color 160ms ease;
|
||||
}
|
||||
|
||||
.el-select-dropdown__item.academic-term-option--current {
|
||||
color: #17694b;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.el-select-dropdown__item.academic-term-option--historical {
|
||||
color: #6f7d8c;
|
||||
opacity: .76;
|
||||
}
|
||||
|
||||
.el-select-dropdown__item.academic-term-option--archived {
|
||||
color: #98a2ad;
|
||||
opacity: .62;
|
||||
}
|
||||
|
||||
.el-select-dropdown__item.academic-term-option--historical.is-hovering,
|
||||
.el-select-dropdown__item.academic-term-option--archived.is-hovering {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.academic-term-row--historical > .el-table__cell {
|
||||
color: #73808e;
|
||||
opacity: .78;
|
||||
}
|
||||
|
||||
.academic-term-row--archived > .el-table__cell {
|
||||
color: #8e98a3;
|
||||
background: #f8fafc;
|
||||
opacity: .66;
|
||||
}
|
||||
|
||||
.academic-term-row--historical:hover > .el-table__cell,
|
||||
.academic-term-row--archived:hover > .el-table__cell {
|
||||
opacity: .94;
|
||||
}
|
||||
|
||||
.historical-record {
|
||||
opacity: .68;
|
||||
filter: saturate(.72);
|
||||
}
|
||||
|
||||
.historical-record:hover,
|
||||
.historical-record.active {
|
||||
opacity: 1;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.archived-record {
|
||||
opacity: .54;
|
||||
filter: grayscale(.18) saturate(.58);
|
||||
}
|
||||
|
||||
.archived-record:hover,
|
||||
.archived-record.active {
|
||||
opacity: .92;
|
||||
}
|
||||
|
||||
.term-policy-note {
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
|
||||
.field-help {
|
||||
margin-left: 10px;
|
||||
color: #8994a1;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.term-mobile-list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.term-desktop-table {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.term-mobile-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.term-mobile-list article {
|
||||
border: 1px solid #e5e9ef;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.term-mobile-list article.academic-term-row--historical {
|
||||
color: #73808e;
|
||||
opacity: .78;
|
||||
}
|
||||
|
||||
.term-mobile-list article.academic-term-row--archived {
|
||||
color: #8e98a3;
|
||||
background: #f8fafc;
|
||||
opacity: .66;
|
||||
}
|
||||
|
||||
.term-mobile-list article:focus-within,
|
||||
.term-mobile-list article:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.term-mobile-list header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.term-mobile-list header span:first-child {
|
||||
color: #258779;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .06em;
|
||||
}
|
||||
|
||||
.term-mobile-list h3 {
|
||||
margin: 5px 0 3px;
|
||||
color: inherit;
|
||||
font-size: 15px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.term-mobile-list p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.term-history-label {
|
||||
white-space: nowrap;
|
||||
color: #7c8895;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.term-mobile-list footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #edf0f4;
|
||||
}
|
||||
|
||||
.term-mobile-list footer .el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
.el-table th.el-table__cell { color: #596274; background: #fafbfc; font-size: 12px; font-weight: 650; }
|
||||
.el-table .cell { font-size: 12px; }
|
||||
.table-status { display: inline-flex; align-items: center; gap: 6px; font-size: 11px; color: var(--teal); }
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
export interface AcademicTermLike {
|
||||
id: string
|
||||
name: string
|
||||
isCurrent?: boolean
|
||||
isArchived?: boolean
|
||||
isEnabled?: boolean
|
||||
hasPublishedTimetable?: boolean
|
||||
}
|
||||
|
||||
export function academicTermLabel(term: AcademicTermLike): string {
|
||||
if (term.isCurrent) return `${term.name} · 当前`
|
||||
if (term.isArchived) return `${term.name} · 已归档`
|
||||
return `${term.name} · 历史`
|
||||
}
|
||||
|
||||
export function academicTermOptionClass(term: AcademicTermLike): string {
|
||||
if (term.isCurrent) return 'academic-term-option academic-term-option--current'
|
||||
if (term.isArchived) return 'academic-term-option academic-term-option--archived'
|
||||
return 'academic-term-option academic-term-option--historical'
|
||||
}
|
||||
|
||||
export function defaultAcademicTermId(
|
||||
terms: AcademicTermLike[],
|
||||
requirePublishedTimetable = false,
|
||||
): string | undefined {
|
||||
const usable = requirePublishedTimetable
|
||||
? terms.filter((term) => term.hasPublishedTimetable)
|
||||
: terms
|
||||
return usable.find((term) => term.isCurrent)?.id
|
||||
?? usable.find((term) => !term.isArchived)?.id
|
||||
?? usable[0]?.id
|
||||
}
|
||||
|
||||
export function academicTermRowClass(term: {
|
||||
isCurrent?: boolean
|
||||
isArchived?: boolean
|
||||
}): string {
|
||||
if (term.isCurrent) return 'academic-term-row academic-term-row--current'
|
||||
if (term.isArchived) return 'academic-term-row academic-term-row--archived'
|
||||
return 'academic-term-row academic-term-row--historical'
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
Loaded 3 of 32 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user