已完成“专业课教学任务下放学院管理”。

专业必修、专业选修、实践课程:由课程所属学院创建、修改、删除、发布和关闭。
公共必修、公共选修:仍由校级教务管理。
校级教务可以查看全校专业课任务,但不能代替学院维护。
超级管理员保留跨学院纠错权限。
教师分配统一要求:教师已申报该课程,并通过学院审核。
兼岗账号同时拥有校级公共课和本学院专业课职责。
This commit is contained in:
2026-07-24 19:43:24 +08:00 Unverified
parent e6d8eb0662
commit 5da1a8a403
5 changed files with 212 additions and 20 deletions
+47 -14
View File
@@ -5,6 +5,10 @@ import http, { apiErrorMessage } from '../api/http'
import { useAuthStore } from '../stores/auth'
const auth = useAuthStore()
const isSuperAdmin = computed(() => auth.user?.roles.includes('SuperAdmin') ?? false)
const isAcademicAdmin = computed(() => auth.user?.roles.includes('AcademicAdmin') ?? false)
const isCollegeAdmin = computed(() => auth.user?.roles.includes('CollegeAdmin') ?? false)
const canGeneratePublic = computed(() => isSuperAdmin.value || isAcademicAdmin.value)
const loading = ref(false)
const rows = ref<any[]>([])
const total = ref(0)
@@ -43,11 +47,8 @@ const availableClasses = computed(() => {
const selectedCourse = computed(() =>
courses.value.find((item) => item.id === form.courseId),
)
const manualCourseIsPublic = computed(() =>
['GeneralRequired', 'GeneralElective'].includes(selectedCourse.value?.nature),
)
const assignableTeachers = computed(() => {
if (!manualCourseIsPublic.value) return teachers.value
if (!selectedCourse.value) return teachers.value
const approvedIds = new Set(manualEligibleTeachers.value.map((item) => item.teacherId))
return teachers.value.filter((item) =>
approvedIds.has(item.id) || form.teacherIds?.includes(item.id),
@@ -61,10 +62,35 @@ const publicCourses = computed(() =>
['GeneralRequired', 'GeneralElective'].includes(item.nature),
),
)
const manageableCourses = computed(() => {
if (isSuperAdmin.value) return courses.value
return courses.value.filter((item) => {
const isPublic = ['GeneralRequired', 'GeneralElective'].includes(item.nature)
return isAcademicAdmin.value && isPublic ||
isCollegeAdmin.value &&
item.collegeId === auth.user?.collegeId &&
['MajorRequired', 'MajorElective', 'Practice'].includes(item.nature)
})
})
const responsibilityText = computed(() => {
if (isSuperAdmin.value) return '超级管理员可处理全部教学任务,并保留跨层级纠错权限。'
if (isAcademicAdmin.value && isCollegeAdmin.value) {
return '兼岗账号负责校级公共课,以及本学院开设的专业必修、专业选修和实践课程。'
}
if (isCollegeAdmin.value) return '学院负责本院专业必修、专业选修和实践课程的教学任务。'
return '校级教务负责公共必修和公共选修课程;专业课教学任务由课程所属学院维护。'
})
const generationGroupCount = computed(() =>
Math.ceil((generationForm.classIds?.length ?? 0) / (generationForm.classesPerTask || 1)),
)
function canManageRow(row: any) {
if (isSuperAdmin.value) return true
const isPublic = ['GeneralRequired', 'GeneralElective'].includes(row.courseNature)
return isAcademicAdmin.value && isPublic ||
isCollegeAdmin.value && !isPublic
}
async function load() {
loading.value = true
try {
@@ -132,7 +158,7 @@ async function openEdit(row: any) {
async function loadManualEligibleTeachers() {
manualEligibleTeachers.value = []
if (!form.academicTermId || !form.courseId || !manualCourseIsPublic.value) return
if (!form.academicTermId || !form.courseId) return
try {
manualEligibleTeachers.value = (await http.get('/teacher-course-applications/eligible', {
params: {
@@ -298,14 +324,14 @@ onMounted(async () => {
<p>将学期课程授课教师和行政班组合为可排课可选课的教学班</p>
</div>
<div class="page-actions">
<el-button @click="openGeneration">公共课合班生成</el-button>
<el-button v-if="canGeneratePublic" @click="openGeneration">公共课合班生成</el-button>
<el-button type="primary" :icon="Plus" @click="openCreate">新建教学任务</el-button>
</div>
</section>
<section class="task-summary">
<div><span>当前结果</span><b>{{ total }}</b><small>个教学班</small></div>
<p>发布校验主讲教师唯一 · 行政班已关联 · 容量覆盖在籍学生 · 周次有效</p>
<p>{{ responsibilityText }} 发布时还会校验教师授课资格行政班容量和周次</p>
</section>
<section class="data-card">
@@ -324,7 +350,13 @@ onMounted(async () => {
<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>
<template #default="{ row }">
<div class="course-name">
<b>{{ row.name }}</b>
<span>{{ row.courseCode }} · {{ row.courseName }}</span>
<small>{{ ['GeneralRequired', 'GeneralElective'].includes(row.courseNature) ? '校级教务负责' : '开课学院负责' }}</small>
</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>
@@ -334,10 +366,11 @@ onMounted(async () => {
<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>
<el-button v-if="canManageRow(row) && row.status === 'Draft'" link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button v-if="canManageRow(row) && row.status === 'Draft'" link type="success" @click="publish(row)">发布</el-button>
<el-button v-if="canManageRow(row) && row.status === 'Draft'" link type="danger" @click="remove(row)">删除</el-button>
<el-button v-if="canManageRow(row) && row.status === 'Published'" link type="warning" @click="closeTask(row)">结课</el-button>
<span v-if="!canManageRow(row) && row.status !== 'Closed'" class="muted-action">只读</span>
<span v-if="row.status === 'Closed'" class="muted-action">已归档</span>
</template>
</el-table-column>
@@ -357,7 +390,7 @@ onMounted(async () => {
</div>
<div class="form-grid">
<el-form-item label="开课学期" required><el-select v-model="form.academicTermId" @change="loadManualEligibleTeachers"><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 @change="form.teacherIds = []; form.primaryTeacherId = undefined; loadManualEligibleTeachers()"><el-option v-for="item in courses" :key="item.id" :label="`${item.code} · ${item.name}`" :value="item.id" /></el-select></el-form-item>
<el-form-item label="课程" required><el-select v-model="form.courseId" filterable @change="form.teacherIds = []; form.primaryTeacherId = undefined; loadManualEligibleTeachers()"><el-option v-for="item in manageableCourses" :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>
@@ -369,7 +402,7 @@ onMounted(async () => {
<el-select v-model="form.teacherIds" multiple filterable @change="onTeachersChanged">
<el-option v-for="item in assignableTeachers" :key="item.id" :label="`${item.teacherNumber} · ${item.name}`" :value="item.id" />
</el-select>
<small v-if="manualCourseIsPublic" class="field-hint">公共课仅显示本学期申报且学院审核通过的教师</small>
<small v-if="selectedCourse" class="field-hint">仅显示本学期申报该课程且学院审核通过的教师</small>
</el-form-item>
<el-form-item label="主讲教师">
<el-select v-model="form.primaryTeacherId" clearable :disabled="!form.teacherIds?.length">