培养方案下放学院维护,学院账号被后端限制在本学院范围;新增按年级、学院、专业联动筛选及维护范围提示。

课程库实行分级维护:学院只能维护本学院的专业必修、专业选修和实践课程;通识课程可查看、引用但不可修改。校级账号保留全校统筹权限。
“排课与课表”已从“教学建设”迁移到“教学运行”,原路由保持不变。
权限规则同时落实在前端和 API,并新增专项测试。
This commit is contained in:
2026-07-24 18:31:33 +08:00 Unverified
parent 022e79c48d
commit 5a7a8d530b
8 changed files with 265 additions and 41 deletions
+70 -10
View File
@@ -10,7 +10,9 @@ const detailLoading = ref(false)
const plans = ref<any[]>([])
const total = ref(0)
const selected = ref<any | null>(null)
const colleges = ref<any[]>([])
const majors = ref<any[]>([])
const grades = ref<number[]>([])
const courses = ref<any[]>([])
const planDialog = ref(false)
const moduleDialog = ref(false)
@@ -24,6 +26,7 @@ const query = reactive({
page: 1,
pageSize: 20,
keyword: '',
collegeId: undefined as string | undefined,
majorId: undefined as string | undefined,
effectiveGrade: undefined as number | undefined,
status: undefined as string | undefined,
@@ -42,11 +45,24 @@ const courseTypeLabels: Record<string, string> = {
Required: '必修',
Elective: '选修',
}
const availableMajors = computed(() =>
auth.user?.roles.includes('CollegeAdmin') && auth.user.collegeId
? majors.value.filter((item) => item.collegeId === auth.user!.collegeId)
const isCollegeAdmin = computed(() => auth.user?.roles.includes('CollegeAdmin') ?? false)
const availableMajors = computed(() => majors.value)
const filteredMajors = computed(() =>
query.collegeId
? majors.value.filter((item) => item.collegeId === query.collegeId)
: majors.value,
)
const availableGrades = computed(() =>
[...new Set([
...grades.value,
...plans.value.map((item) => Number(item.effectiveGrade)),
])].sort((first, second) => second - first),
)
const maintenanceScope = computed(() =>
isCollegeAdmin.value
? colleges.value[0]?.name ?? '所属学院'
: '全校各学院',
)
const isDraft = computed(() => selected.value?.status === 'Draft')
const configuredCredits = computed(() =>
selected.value?.modules.reduce(
@@ -96,6 +112,7 @@ function resetFilters() {
Object.assign(query, {
page: 1,
keyword: '',
collegeId: isCollegeAdmin.value ? auth.user?.collegeId : undefined,
majorId: undefined,
effectiveGrade: undefined,
status: undefined,
@@ -103,6 +120,13 @@ function resetFilters() {
loadPlans(false)
}
function changeCollege() {
if (query.majorId &&
!filteredMajors.value.some((item) => item.id === query.majorId)) {
query.majorId = undefined
}
}
function openPlan(row?: any) {
editingPlanId.value = row?.id ?? ''
Object.keys(planForm).forEach((key) => delete planForm[key])
@@ -268,12 +292,17 @@ async function deleteCourse(moduleId: string, item: any) {
}
onMounted(async () => {
const [majorRes, courseRes] = await Promise.all([
http.get('/base-data/majors'),
const [optionRes, courseRes] = await Promise.all([
http.get('/curriculum-plans/filter-options'),
http.get('/courses', { params: { page: 1, pageSize: 100, isEnabled: true } }),
])
majors.value = majorRes.data
colleges.value = optionRes.data.colleges
majors.value = optionRes.data.majors
grades.value = optionRes.data.grades
courses.value = courseRes.data.items
if (isCollegeAdmin.value && auth.user?.collegeId) {
query.collegeId = auth.user.collegeId
}
await loadPlans(false)
})
</script>
@@ -284,17 +313,48 @@ onMounted(async () => {
<div>
<span class="section-kicker">PROGRAMME OF STUDY</span>
<h2>培养方案</h2>
<p>按专业和入学年级管理版本化课程结构发布后锁定避免影响历史年级</p>
<p>由学院按专业和入学年级维护课程结构发布后锁定避免影响历史年级</p>
</div>
<el-button type="primary" :icon="Plus" @click="openPlan()">新建方案</el-button>
</section>
<section class="maintenance-scope" aria-label="培养方案维护范围">
<div>
<span>维护主体</span>
<b>{{ isCollegeAdmin ? '学院教务' : '校级统筹' }}</b>
</div>
<div>
<span>当前范围</span>
<b>{{ maintenanceScope }}</b>
</div>
<p>
{{ isCollegeAdmin
? '可新建、编制并发布本学院各专业培养方案'
: '可跨学院查看、维护并统筹培养方案版本' }}
</p>
</section>
<section class="curriculum-filter">
<el-input v-model="query.keyword" :prefix-icon="Search" clearable placeholder="搜索方案、版本或专业" @keyup.enter="query.page = 1; loadPlans(false)" />
<el-select v-model="query.majorId" clearable filterable placeholder="全部专业">
<el-option v-for="item in availableMajors" :key="item.id" :label="item.name" :value="item.id" />
<el-select
v-model="query.effectiveGrade"
clearable
placeholder="全部年级"
>
<el-option v-for="grade in availableGrades" :key="grade" :label="`${grade} 级`" :value="grade" />
</el-select>
<el-select
v-model="query.collegeId"
clearable
:disabled="isCollegeAdmin"
placeholder="全部学院"
@change="changeCollege"
>
<el-option v-for="item in colleges" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-select v-model="query.majorId" clearable filterable placeholder="全部专业">
<el-option v-for="item in filteredMajors" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-input-number v-model="query.effectiveGrade" :min="2000" :max="2200" placeholder="适用年级" />
<el-select v-model="query.status" clearable placeholder="全部状态">
<el-option v-for="(label, value) in statusLabels" :key="value" :label="label" :value="value" />
</el-select>