培养方案“学期视图”。

This commit is contained in:
2026-08-09 16:47:19 +08:00 Unverified
parent f246378d5b
commit ae24371d7f
2 changed files with 92 additions and 3 deletions
+23
View File
@@ -431,6 +431,8 @@ button { cursor: pointer; }
.module-toolbar b, .module-toolbar span { display: block; }
.module-toolbar b { font-size: 15px; }
.module-toolbar span { margin-top: 4px; color: var(--muted); font-size: 10px; }
.curriculum-view-actions { display: flex; align-items: center; gap: 10px; }
.curriculum-view-actions .el-button + .el-button { margin-left: 0; }
.curriculum-module { margin-top: 12px; border: 1px solid var(--line); }
.curriculum-module > header { min-height: 72px; padding: 13px 16px; display: flex; align-items: center; gap: 18px; background: #f8fafb; border-bottom: 1px solid var(--line); }
.curriculum-module > header > div:first-child { min-width: 160px; }
@@ -439,6 +441,24 @@ button { cursor: pointer; }
.curriculum-module > header p { margin: 0 auto 0 0; color: var(--muted); font-size: 11px; }
.curriculum-module > header > div:last-child { display: flex; align-items: center; white-space: nowrap; }
.curriculum-course-table { min-height: 80px; }
.semester-view { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.semester-card { min-width: 0; border: 1px solid var(--line); background: #fff; }
.semester-card > header { min-height: 70px; padding: 14px 16px; display: flex; align-items: center; justify-content: space-between; background: linear-gradient(135deg, #f4faf9, #f8fafb); border-bottom: 1px solid var(--line); }
.semester-card > header span { color: var(--teal); font: 700 9px/1 Consolas, monospace; letter-spacing: .12em; }
.semester-card h4 { margin: 7px 0 0; font-size: 15px; }
.semester-card > header > b { color: #1d6b68; font: 700 18px/1 Consolas, monospace; }
.semester-summary { display: flex; gap: 14px; padding: 9px 16px; color: var(--muted); font-size: 11px; border-bottom: 1px solid var(--line); }
.semester-summary span + span { padding-left: 14px; border-left: 1px solid var(--line); }
.semester-courses { margin: 0; padding: 0; list-style: none; }
.semester-courses li { min-height: 68px; padding: 11px 16px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid #edf0f2; }
.semester-courses li:last-child { border-bottom: none; }
.semester-courses li > div { min-width: 0; display: grid; gap: 3px; }
.semester-courses li span { color: var(--teal); font-size: 10px; }
.semester-courses li b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
.semester-courses li small { color: var(--muted); font-size: 10px; }
.semester-courses li > strong { flex: none; font: 700 15px/1 Consolas, monospace; }
.semester-courses li > strong small { margin-left: 3px; font: 400 9px/1 inherit; }
.semester-empty { margin: 0; padding: 25px 16px; color: var(--muted); font-size: 11px; text-align: center; }
.task-summary { min-height: 82px; padding: 15px 22px; display: flex; align-items: center; gap: 28px; color: white; background: linear-gradient(108deg, #17295a, #263f80); }
.task-summary > div { min-width: 170px; display: flex; align-items: baseline; gap: 8px; }
.task-summary span, .task-summary small { color: #b8c1de; font-size: 10px; }
@@ -1280,6 +1300,9 @@ button { cursor: pointer; }
.plan-metrics > div:nth-child(-n+2) { border-bottom: 1px solid var(--line); }
.curriculum-module > header { align-items: flex-start; flex-wrap: wrap; }
.curriculum-module > header p { order: 3; width: 100%; }
.module-toolbar { align-items: flex-start; flex-direction: column; gap: 12px; }
.curriculum-view-actions { width: 100%; justify-content: space-between; }
.semester-view { grid-template-columns: 1fr; }
.task-summary { align-items: flex-start; flex-direction: column; gap: 12px; }
.task-summary > div { width: 100%; }
.task-summary p { padding: 12px 0 0; border-left: none; border-top: 1px solid rgba(255,255,255,.16); line-height: 1.6; }
+69 -3
View File
@@ -18,6 +18,7 @@ const planDialog = ref(false)
const moduleDialog = ref(false)
const courseDialog = ref(false)
const cloneDialog = ref(false)
const detailView = ref<'structure' | 'semester'>('structure')
const editingPlanId = ref('')
const editingPlanStatus = ref('')
const editingModuleId = ref('')
@@ -74,6 +75,34 @@ const configuredCredits = computed(() =>
0,
) ?? 0,
)
const semesterGroups = computed(() => {
if (!selected.value) return []
const courses = selected.value.modules.flatMap((module: any) =>
module.courses.map((course: any) => ({
...course,
moduleCode: module.code,
moduleName: module.name,
})),
)
const semesterCount = Number(selected.value.schoolingYears) * 2
return Array.from({ length: semesterCount }, (_, index) => {
const semester = index + 1
const items = courses
.filter((course: any) => Number(course.recommendedSemester) === semester)
.sort((first: any, second: any) =>
first.moduleCode.localeCompare(second.moduleCode) ||
first.courseCode.localeCompare(second.courseCode),
)
return {
semester,
courses: items,
credits: items.reduce((sum: number, course: any) => sum + Number(course.credits), 0),
totalHours: items.reduce((sum: number, course: any) => sum + Number(course.totalHours), 0),
}
})
})
async function loadPlans(keepSelection = true) {
loading.value = true
@@ -450,12 +479,21 @@ onMounted(async () => {
<div class="module-toolbar">
<div>
<b>课程结构</b>
<span>指定必修须逐门通过英语体育等多选课程用组内选修修满模块最低学分即可</span>
<b>{{ detailView === 'structure' ? '课程结构' : '学期视图' }}</b>
<span>{{ detailView === 'structure'
? '指定必修须逐门通过;英语、体育等多选课程用“组内选修”,修满模块最低学分即可'
: '按建议修读学期展示课程安排,包含每学期的课程数量、学分和学时。' }}</span>
</div>
<div class="curriculum-view-actions">
<el-radio-group v-model="detailView" size="small" aria-label="培养方案展示方式">
<el-radio-button value="structure">课程结构</el-radio-button>
<el-radio-button value="semester">学期视图</el-radio-button>
</el-radio-group>
<el-button v-if="canEdit && detailView === 'structure'" :icon="Plus" @click="openModule()">新增模块</el-button>
</div>
<el-button v-if="canEdit" :icon="Plus" @click="openModule()">新增模块</el-button>
</div>
<template v-if="detailView === 'structure'">
<section v-for="module in selected.modules" :key="module.id" class="curriculum-module">
<header>
<div>
@@ -500,6 +538,34 @@ onMounted(async () => {
</el-table>
</section>
<el-empty v-if="selected.modules.length === 0" description="先建立课程模块,再添加课程" />
</template>
<section v-else class="semester-view" aria-label="课程学期安排">
<article v-for="group in semesterGroups" :key="group.semester" class="semester-card">
<header>
<div>
<span>SEMESTER {{ String(group.semester).padStart(2, '0') }}</span>
<h4> {{ group.semester }} 学期</h4>
</div>
<b>{{ group.courses.length }} </b>
</header>
<div class="semester-summary">
<span>{{ group.credits }} 学分</span>
<span>{{ group.totalHours }} 学时</span>
</div>
<ul v-if="group.courses.length" class="semester-courses">
<li v-for="course in group.courses" :key="course.id">
<div>
<span>{{ course.moduleName }}</span>
<b>{{ course.courseName }}</b>
<small>{{ course.courseCode }} · {{ courseTypeLabels[course.type] }}</small>
</div>
<strong>{{ course.credits }}<small>学分</small></strong>
</li>
</ul>
<p v-else class="semester-empty">本学期暂未安排课程</p>
</article>
</section>
</template>
</main>
</div>