培养方案:专业/年级版本、课程模块、学分校验、复制版本、发布锁定、旧版本归档。

教学任务:学期开课、授课教师、唯一主讲、合班、容量与周次校验、发布及结课。
SQLite 开发库已自动升级,无需删除原数据库。
新增对应 MySQL EF Core 迁移。
Vue 已编译进 wwwroot,仍可只运行 ASP.NET Core 单服务。
桌面端及 390px 窄屏页面验证通过,无前端控制台错误。
This commit is contained in:
2026-07-24 14:10:43 +08:00 Unverified
parent 30d793773c
commit 628944f64d
23 changed files with 5430 additions and 47 deletions
+284
View File
@@ -0,0 +1,284 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { Plus, Refresh, Search } from '@element-plus/icons-vue'
import http, { apiErrorMessage } from '../api/http'
import { useAuthStore } from '../stores/auth'
const auth = useAuthStore()
const loading = ref(false)
const rows = ref<any[]>([])
const total = ref(0)
const dialogVisible = ref(false)
const editingId = ref('')
const terms = ref<any[]>([])
const courses = ref<any[]>([])
const teachers = ref<any[]>([])
const classes = ref<any[]>([])
const majors = ref<any[]>([])
const query = reactive({
page: 1,
pageSize: 20,
keyword: '',
academicTermId: undefined as string | undefined,
status: undefined as string | undefined,
})
const form = reactive<Record<string, any>>({})
const statusLabels: Record<string, string> = {
Draft: '草稿',
Published: '已发布',
Closed: '已结课',
}
const availableClasses = computed(() => {
if (!auth.user?.roles.includes('CollegeAdmin') || !auth.user.collegeId) return classes.value
const ids = new Set(
majors.value.filter((item) => item.collegeId === auth.user!.collegeId).map((item) => item.id),
)
return classes.value.filter((item) => ids.has(item.majorId))
})
const selectedTeachers = computed(() =>
teachers.value.filter((item) => form.teacherIds?.includes(item.id)),
)
async function load() {
loading.value = true
try {
const { data } = await http.get('/teaching-tasks', {
params: {
...query,
keyword: query.keyword || undefined,
},
})
rows.value = data.items
total.value = data.total
} catch (error) {
ElMessage.error(apiErrorMessage(error))
} finally {
loading.value = false
}
}
function resetFilters() {
Object.assign(query, {
page: 1,
keyword: '',
academicTermId: undefined,
status: undefined,
})
load()
}
function resetForm(detail?: any) {
Object.keys(form).forEach((key) => delete form[key])
const currentTerm = terms.value.find((item) => item.isCurrent)
Object.assign(form, {
taskNumber: '',
name: '',
academicTermId: currentTerm?.id,
courseId: undefined,
capacity: 60,
startWeek: 1,
endWeek: 16,
weeklyHours: 2,
teacherIds: [],
primaryTeacherId: undefined,
classIds: [],
notes: '',
}, detail ?? {})
}
function openCreate() {
editingId.value = ''
resetForm()
dialogVisible.value = true
}
async function openEdit(row: any) {
try {
const detail = (await http.get(`/teaching-tasks/${row.id}`)).data
editingId.value = row.id
resetForm(detail)
dialogVisible.value = true
} catch (error) {
ElMessage.error(apiErrorMessage(error))
}
}
function onTeachersChanged(values: string[]) {
if (form.primaryTeacherId && !values.includes(form.primaryTeacherId)) {
form.primaryTeacherId = undefined
}
}
async function save() {
if (!form.taskNumber?.trim() || !form.name?.trim() || !form.academicTermId || !form.courseId) {
ElMessage.warning('请填写任务编号、名称、学期和课程。')
return
}
if (form.startWeek > form.endWeek) {
ElMessage.warning('开始周不能晚于结束周。')
return
}
try {
if (editingId.value) await http.put(`/teaching-tasks/${editingId.value}`, form)
else await http.post('/teaching-tasks', form)
ElMessage.success(editingId.value ? '教学任务已更新' : '教学任务草稿已创建')
dialogVisible.value = false
await load()
} catch (error) {
ElMessage.error(apiErrorMessage(error))
}
}
async function remove(row: any) {
try {
await ElMessageBox.confirm(`确定删除教学任务“${row.name}”吗?`, '删除草稿', {
type: 'warning',
})
await http.delete(`/teaching-tasks/${row.id}`)
await load()
} catch (error: any) {
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
}
}
async function publish(row: any) {
try {
await ElMessageBox.confirm(
'发布后任务将锁定,并可进入排课与选课流程。确定发布吗?',
'发布教学任务',
{ type: 'warning', confirmButtonText: '发布', cancelButtonText: '取消' },
)
await http.post(`/teaching-tasks/${row.id}/publish`)
ElMessage.success('教学任务已发布')
await load()
} catch (error: any) {
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
}
}
async function closeTask(row: any) {
try {
await ElMessageBox.confirm('结课后将停止后续选课和排课调整,确定继续吗?', '教学任务结课', {
type: 'warning',
confirmButtonText: '确认结课',
cancelButtonText: '取消',
})
await http.post(`/teaching-tasks/${row.id}/close`)
await load()
} catch (error: any) {
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
}
}
onMounted(async () => {
const [termRes, courseRes, teacherRes, classRes, majorRes] = await Promise.all([
http.get('/base-data/terms'),
http.get('/courses', { params: { page: 1, pageSize: 100, isEnabled: true } }),
http.get('/personnel/teachers', { params: { page: 1, pageSize: 100, teacherStatus: 'Active' } }),
http.get('/base-data/classes'),
http.get('/base-data/majors'),
])
terms.value = termRes.data
courses.value = courseRes.data.items
teachers.value = teacherRes.data.items
classes.value = classRes.data
majors.value = majorRes.data
query.academicTermId = terms.value.find((item) => item.isCurrent)?.id
await load()
})
</script>
<template>
<div class="page-stack">
<section class="page-intro">
<div>
<span class="section-kicker">TEACHING COMMISSION</span>
<h2>教学任务</h2>
<p>将学期课程授课教师和行政班组合为可排课可选课的教学班</p>
</div>
<el-button type="primary" :icon="Plus" @click="openCreate">新建教学任务</el-button>
</section>
<section class="task-summary">
<div><span>当前结果</span><b>{{ total }}</b><small>个教学班</small></div>
<p>发布校验主讲教师唯一 · 行政班已关联 · 容量覆盖在籍学生 · 周次有效</p>
</section>
<section class="data-card">
<div class="filter-bar">
<el-input v-model="query.keyword" :prefix-icon="Search" clearable placeholder="搜索任务编号、教学班或课程" @keyup.enter="query.page = 1; load()" />
<el-select v-model="query.academicTermId" clearable placeholder="全部学期">
<el-option v-for="item in terms" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-select v-model="query.status" clearable placeholder="全部状态">
<el-option v-for="(label, value) in statusLabels" :key="value" :label="label" :value="value" />
</el-select>
<el-button type="primary" @click="query.page = 1; load()">查询</el-button>
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
</div>
<el-table v-loading="loading" :data="rows" class="data-table task-table">
<el-table-column label="任务编号" width="155"><template #default="{ row }"><span class="registry-number">{{ row.taskNumber }}</span></template></el-table-column>
<el-table-column label="教学班 / 课程" min-width="230">
<template #default="{ row }"><div class="course-name"><b>{{ row.name }}</b><span>{{ row.courseCode }} · {{ row.courseName }}</span></div></template>
</el-table-column>
<el-table-column prop="termName" label="开课学期" min-width="180" />
<el-table-column label="授课教师" min-width="130"><template #default="{ row }">{{ row.teacherNames.join('、') || '未安排' }}</template></el-table-column>
<el-table-column label="行政班" min-width="150"><template #default="{ row }">{{ row.classNames.join('、') || '未关联' }}</template></el-table-column>
<el-table-column label="人数 / 容量" width="105"><template #default="{ row }">{{ row.studentCount }} / {{ row.capacity }}</template></el-table-column>
<el-table-column label="周次" width="105"><template #default="{ row }">{{ row.startWeek }}{{ row.endWeek }} </template></el-table-column>
<el-table-column label="状态" width="90"><template #default="{ row }"><span class="table-status" :class="{ off: row.status === 'Closed' }">{{ statusLabels[row.status] }}</span></template></el-table-column>
<el-table-column label="操作" width="185" fixed="right">
<template #default="{ row }">
<el-button v-if="row.status === 'Draft'" link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button v-if="row.status === 'Draft'" link type="success" @click="publish(row)">发布</el-button>
<el-button v-if="row.status === 'Draft'" link type="danger" @click="remove(row)">删除</el-button>
<el-button v-if="row.status === 'Published'" link type="warning" @click="closeTask(row)">结课</el-button>
<span v-if="row.status === 'Closed'" class="muted-action">已归档</span>
</template>
</el-table-column>
<template #empty><el-empty description="没有符合条件的教学任务" /></template>
</el-table>
<div class="pagination-bar">
<span> {{ total }} 个教学班</span>
<el-pagination v-model:current-page="query.page" :page-size="query.pageSize" :total="total" layout="prev, pager, next" @current-change="load" />
</div>
</section>
<el-dialog v-model="dialogVisible" :title="editingId ? '编辑教学任务' : '新建教学任务'" width="720px">
<el-form label-position="top">
<div class="form-grid">
<el-form-item label="任务编号" required><el-input v-model="form.taskNumber" placeholder="如 2026-1-CS101-01" /></el-form-item>
<el-form-item label="教学班名称" required><el-input v-model="form.name" /></el-form-item>
</div>
<div class="form-grid">
<el-form-item label="开课学期" required><el-select v-model="form.academicTermId"><el-option v-for="item in terms" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
<el-form-item label="课程" required><el-select v-model="form.courseId" filterable><el-option v-for="item in courses" :key="item.id" :label="`${item.code} · ${item.name}`" :value="item.id" /></el-select></el-form-item>
</div>
<div class="form-grid three">
<el-form-item label="容量"><el-input-number v-model="form.capacity" :min="1" /></el-form-item>
<el-form-item label="开始周"><el-input-number v-model="form.startWeek" :min="1" :max="30" /></el-form-item>
<el-form-item label="结束周"><el-input-number v-model="form.endWeek" :min="1" :max="30" /></el-form-item>
</div>
<el-form-item label="周学时"><el-input-number v-model="form.weeklyHours" :min="1" :max="40" /></el-form-item>
<el-form-item label="授课教师">
<el-select v-model="form.teacherIds" multiple filterable @change="onTeachersChanged">
<el-option v-for="item in teachers" :key="item.id" :label="`${item.teacherNumber} · ${item.name}`" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="主讲教师">
<el-select v-model="form.primaryTeacherId" clearable :disabled="!form.teacherIds?.length">
<el-option v-for="item in selectedTeachers" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="关联行政班">
<el-select v-model="form.classIds" multiple filterable>
<el-option v-for="item in availableClasses" :key="item.id" :label="`${item.code} · ${item.name}`" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="备注"><el-input v-model="form.notes" type="textarea" :rows="2" /></el-form-item>
</el-form>
<template #footer><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" @click="save">保存草稿</el-button></template>
</el-dialog>
</div>
</template>