培养方案下放学院维护,学院账号被后端限制在本学院范围;新增按年级、学院、专业联动筛选及维护范围提示。
课程库实行分级维护:学院只能维护本学院的专业必修、专业选修和实践课程;通识课程可查看、引用但不可修改。校级账号保留全校统筹权限。 “排课与课表”已从“教学建设”迁移到“教学运行”,原路由保持不变。 权限规则同时落实在前端和 API,并新增专项测试。
This commit is contained in:
1 parent
022e79c48d
commit
5a7a8d530b
8 files changed
+265
-41
No files matched your search
@@ -77,16 +77,16 @@ const navigationGroups = computed<NavigationGroup[]>(() => [
|
||||
{ path: '/courses', label: '课程库' },
|
||||
...whenVisible(isTeachingAdmin.value, { path: '/curriculum', label: '培养方案' }),
|
||||
...whenVisible(isTeachingAdmin.value, { path: '/teaching-tasks', label: '教学任务' }),
|
||||
...whenVisible(
|
||||
hasAnyRole(['SuperAdmin', 'AcademicAdmin']),
|
||||
{ path: '/schedules', label: '排课与课表' },
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'teaching-operation',
|
||||
label: '教学运行',
|
||||
items: [
|
||||
...whenVisible(
|
||||
hasAnyRole(['SuperAdmin', 'AcademicAdmin']),
|
||||
{ path: '/schedules', label: '排课与课表' },
|
||||
),
|
||||
...whenVisible(
|
||||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student']),
|
||||
{
|
||||
|
||||
+13
-4
@@ -235,9 +235,16 @@ button { cursor: pointer; }
|
||||
.ledger-label span { align-self: flex-start; color: #aeb8d6; font-size: 11px; }
|
||||
.ledger-label b { margin-left: auto; font: 700 31px/1 Consolas, monospace; color: #58d0bf; }
|
||||
.ledger-label small { color: #c4cce3; font-size: 10px; }
|
||||
.ledger-rule { display: flex; align-items: center; gap: 0; position: relative; z-index: 1; }
|
||||
.ledger-rule span { padding: 8px 28px; border-right: 1px solid rgba(255,255,255,.12); color: #d2d8e9; font-size: 11px; letter-spacing: .08em; }
|
||||
.ledger-rule { padding: 17px 28px; display: grid; align-content: center; gap: 4px; position: relative; z-index: 1; }
|
||||
.ledger-rule span { color: #8fa0cb; font-size: 9px; letter-spacing: .13em; }
|
||||
.ledger-rule b { color: #f5f7ff; font-size: 14px; font-weight: 650; }
|
||||
.ledger-rule small { color: #b9c3dd; font-size: 10px; }
|
||||
|
||||
.maintenance-scope { min-height: 76px; display: grid; grid-template-columns: 170px 220px 1fr; align-items: stretch; color: white; background: linear-gradient(105deg, #17295a, #263f80 72%, #19736f); overflow: hidden; }
|
||||
.maintenance-scope > div { padding: 15px 20px; display: grid; align-content: center; gap: 5px; border-right: 1px solid rgba(255,255,255,.13); }
|
||||
.maintenance-scope span { color: #91a2cc; font-size: 9px; letter-spacing: .12em; }
|
||||
.maintenance-scope b { color: #f7f9ff; font-size: 13px; font-weight: 650; }
|
||||
.maintenance-scope p { margin: 0; padding: 0 24px; align-self: center; color: #cbd4e8; font-size: 11px; }
|
||||
.curriculum-filter { padding: 14px 18px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; border: 1px solid var(--line); background: white; }
|
||||
.curriculum-filter > .el-input { width: 260px; }
|
||||
.curriculum-filter > .el-select { width: 170px; }
|
||||
@@ -905,9 +912,11 @@ button { cursor: pointer; }
|
||||
.filter-bar > .el-input { width: 100%; }
|
||||
.filter-bar > .el-select { flex: 1 1 140px; width: auto; }
|
||||
.course-ledger { min-height: auto; display: block; }
|
||||
.maintenance-scope { grid-template-columns: 1fr 1fr; }
|
||||
.maintenance-scope p { grid-column: 1 / -1; padding: 12px 18px; border-top: 1px solid rgba(255,255,255,.13); }
|
||||
.ledger-label { width: 100%; }
|
||||
.ledger-rule { overflow-x: auto; }
|
||||
.ledger-rule span { padding: 12px 20px; white-space: nowrap; }
|
||||
.ledger-rule { overflow: visible; }
|
||||
.ledger-rule span { padding: 0; white-space: nowrap; }
|
||||
.data-card { overflow: hidden; }
|
||||
.curriculum-filter > .el-input { width: 100%; }
|
||||
.curriculum-filter > .el-select { flex: 1 1 140px; width: auto; }
|
||||
|
||||
@@ -26,7 +26,8 @@ const canManage = computed(() =>
|
||||
['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(role),
|
||||
) ?? false,
|
||||
)
|
||||
const availableColleges = computed(() =>
|
||||
const isCollegeAdmin = computed(() => auth.user?.roles.includes('CollegeAdmin') ?? false)
|
||||
const formColleges = computed(() =>
|
||||
auth.user?.roles.includes('CollegeAdmin') && auth.user.collegeId
|
||||
? colleges.value.filter((item) => item.id === auth.user!.collegeId)
|
||||
: colleges.value,
|
||||
@@ -42,6 +43,23 @@ const assessmentLabels: Record<string, string> = {
|
||||
Examination: '考试',
|
||||
Assessment: '考查',
|
||||
}
|
||||
const collegeNatureLabels = computed(() => {
|
||||
if (!isCollegeAdmin.value) return natureLabels
|
||||
return Object.fromEntries(
|
||||
Object.entries(natureLabels).filter(([value]) =>
|
||||
['MajorRequired', 'MajorElective', 'Practice'].includes(value),
|
||||
),
|
||||
)
|
||||
})
|
||||
const maintenanceScope = computed(() =>
|
||||
isCollegeAdmin.value
|
||||
? `${formColleges.value[0]?.name ?? '所属学院'} · 专业课与实践课`
|
||||
: '全校课程 · 全性质',
|
||||
)
|
||||
|
||||
function canManageRow(row: any) {
|
||||
return row.canManage === true
|
||||
}
|
||||
|
||||
function resetForm(row?: any) {
|
||||
Object.keys(form).forEach((key) => delete form[key])
|
||||
@@ -54,7 +72,7 @@ function resetForm(row?: any) {
|
||||
totalHours: 32,
|
||||
lectureHours: 24,
|
||||
practiceHours: 8,
|
||||
nature: 'MajorRequired',
|
||||
nature: isCollegeAdmin.value ? 'MajorRequired' : 'GeneralRequired',
|
||||
assessmentMethod: 'Examination',
|
||||
description: '',
|
||||
isEnabled: true,
|
||||
@@ -88,7 +106,7 @@ async function resetFilters() {
|
||||
Object.assign(query, {
|
||||
page: 1,
|
||||
keyword: '',
|
||||
collegeId: auth.user?.roles.includes('CollegeAdmin') ? auth.user.collegeId : undefined,
|
||||
collegeId: undefined,
|
||||
nature: undefined,
|
||||
isEnabled: undefined,
|
||||
})
|
||||
@@ -144,9 +162,6 @@ async function remove(row: any) {
|
||||
|
||||
onMounted(async () => {
|
||||
colleges.value = (await http.get('/base-data/colleges')).data
|
||||
if (auth.user?.roles.includes('CollegeAdmin') && auth.user.collegeId) {
|
||||
query.collegeId = auth.user.collegeId
|
||||
}
|
||||
await load()
|
||||
})
|
||||
</script>
|
||||
@@ -157,21 +172,21 @@ onMounted(async () => {
|
||||
<div>
|
||||
<span class="section-kicker">COURSE CATALOGUE</span>
|
||||
<h2>课程库</h2>
|
||||
<p>维护全校统一课程编码、学分学时和考核方式,供培养方案与教学任务引用。</p>
|
||||
<p>通识课程由校级统筹,专业课与实践课由开课学院维护,统一供培养方案与教学任务引用。</p>
|
||||
</div>
|
||||
<el-button v-if="canManage" type="primary" :icon="Plus" @click="openCreate">新增课程</el-button>
|
||||
</section>
|
||||
|
||||
<section class="course-ledger">
|
||||
<div class="ledger-label">
|
||||
<span>课程目录</span>
|
||||
<span>当前可见</span>
|
||||
<b>{{ total }}</b>
|
||||
<small>门课程</small>
|
||||
</div>
|
||||
<div class="ledger-rule">
|
||||
<span>编码唯一</span>
|
||||
<span>学时守恒</span>
|
||||
<span>归属明确</span>
|
||||
<span>维护范围</span>
|
||||
<b>{{ maintenanceScope }}</b>
|
||||
<small>{{ isCollegeAdmin ? '通识课程可查看、可引用' : '负责跨学院标准统筹' }}</small>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -185,7 +200,7 @@ onMounted(async () => {
|
||||
@keyup.enter="query.page = 1; load()"
|
||||
/>
|
||||
<el-select v-model="query.collegeId" clearable placeholder="全部开课学院">
|
||||
<el-option v-for="item in availableColleges" :key="item.id" :label="item.name" :value="item.id" />
|
||||
<el-option v-for="item in colleges" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
<el-select v-model="query.nature" clearable placeholder="全部课程性质">
|
||||
<el-option v-for="(label, value) in natureLabels" :key="value" :label="label" :value="value" />
|
||||
@@ -230,8 +245,11 @@ onMounted(async () => {
|
||||
</el-table-column>
|
||||
<el-table-column v-if="canManage" label="操作" width="145" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="remove(row)">删除</el-button>
|
||||
<template v-if="canManageRow(row)">
|
||||
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="remove(row)">删除</el-button>
|
||||
</template>
|
||||
<span v-else class="muted-action">校级维护</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty description="没有符合条件的课程" /></template>
|
||||
@@ -258,12 +276,12 @@ onMounted(async () => {
|
||||
<div class="form-grid">
|
||||
<el-form-item label="开课学院" required>
|
||||
<el-select v-model="form.collegeId">
|
||||
<el-option v-for="item in availableColleges" :key="item.id" :label="item.name" :value="item.id" />
|
||||
<el-option v-for="item in formColleges" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="课程性质">
|
||||
<el-select v-model="form.nature">
|
||||
<el-option v-for="(label, value) in natureLabels" :key="value" :label="label" :value="value" />
|
||||
<el-option v-for="(label, value) in collegeNatureLabels" :key="value" :label="label" :value="value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
Loaded 3 of 8 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user