577 lines
23 KiB
Vue
577 lines
23 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, reactive, ref } from 'vue'
|
||
import { CopyDocument, Plus, Promotion, 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 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)
|
||
const courseDialog = ref(false)
|
||
const cloneDialog = ref(false)
|
||
const editingPlanId = ref('')
|
||
const editingPlanStatus = ref('')
|
||
const editingModuleId = ref('')
|
||
const editingCourseId = ref('')
|
||
const activeModuleId = ref('')
|
||
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,
|
||
})
|
||
const planForm = reactive<Record<string, any>>({})
|
||
const moduleForm = reactive<Record<string, any>>({})
|
||
const courseForm = reactive<Record<string, any>>({})
|
||
const cloneForm = reactive<Record<string, any>>({})
|
||
|
||
const statusLabels: Record<string, string> = {
|
||
Draft: '草稿',
|
||
Published: '已发布',
|
||
Archived: '已归档',
|
||
}
|
||
const courseTypeLabels: Record<string, string> = {
|
||
Required: '指定必修',
|
||
Elective: '组内选修',
|
||
}
|
||
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 isPublished = computed(() => selected.value?.status === 'Published')
|
||
const canEdit = computed(() => isDraft.value || isPublished.value)
|
||
const isEditingPublished = computed(() => editingPlanStatus.value === 'Published')
|
||
const configuredCredits = computed(() =>
|
||
selected.value?.modules.reduce(
|
||
(sum: number, module: any) => sum + Number(module.requiredCredits),
|
||
0,
|
||
) ?? 0,
|
||
)
|
||
|
||
async function loadPlans(keepSelection = true) {
|
||
loading.value = true
|
||
try {
|
||
const { data } = await http.get('/curriculum-plans', {
|
||
params: {
|
||
...query,
|
||
keyword: query.keyword || undefined,
|
||
},
|
||
})
|
||
plans.value = data.items
|
||
total.value = data.total
|
||
const targetId = keepSelection && selected.value
|
||
? selected.value.id
|
||
: plans.value[0]?.id
|
||
if (targetId && plans.value.some((item) => item.id === targetId)) {
|
||
await loadDetail(targetId)
|
||
} else {
|
||
selected.value = null
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error(apiErrorMessage(error))
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadDetail(id: string) {
|
||
detailLoading.value = true
|
||
try {
|
||
selected.value = (await http.get(`/curriculum-plans/${id}`)).data
|
||
} catch (error) {
|
||
ElMessage.error(apiErrorMessage(error))
|
||
} finally {
|
||
detailLoading.value = false
|
||
}
|
||
}
|
||
|
||
function resetFilters() {
|
||
Object.assign(query, {
|
||
page: 1,
|
||
keyword: '',
|
||
collegeId: isCollegeAdmin.value ? auth.user?.collegeId : undefined,
|
||
majorId: undefined,
|
||
effectiveGrade: undefined,
|
||
status: undefined,
|
||
})
|
||
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 ?? ''
|
||
editingPlanStatus.value = row?.status ?? ''
|
||
Object.keys(planForm).forEach((key) => delete planForm[key])
|
||
Object.assign(planForm, {
|
||
majorId: row?.majorId,
|
||
name: row?.name ?? '',
|
||
version: row?.version ?? `${new Date().getFullYear()}版`,
|
||
effectiveGrade: row?.effectiveGrade ?? new Date().getFullYear(),
|
||
totalCredits: row?.totalCredits ?? 160,
|
||
description: row?.description ?? '',
|
||
})
|
||
planDialog.value = true
|
||
}
|
||
|
||
async function confirmPublishedChange(action: string) {
|
||
if (!isPublished.value) return true
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`${action}保存后会立即影响学生端培养方案展示及毕业审核依据。确定继续吗?`,
|
||
'修改已发布方案',
|
||
{
|
||
type: 'warning',
|
||
confirmButtonText: '确认修改',
|
||
cancelButtonText: '取消',
|
||
},
|
||
)
|
||
return true
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
|
||
async function savePlan() {
|
||
if (!planForm.majorId || !planForm.name?.trim() || !planForm.version?.trim()) {
|
||
ElMessage.warning('请填写专业、方案名称和版本号。')
|
||
return
|
||
}
|
||
if (editingPlanId.value && !await confirmPublishedChange('方案信息')) return
|
||
try {
|
||
if (editingPlanId.value) {
|
||
await http.put(`/curriculum-plans/${editingPlanId.value}`, planForm)
|
||
} else {
|
||
await http.post('/curriculum-plans', planForm)
|
||
}
|
||
ElMessage.success(editingPlanId.value ? '方案信息已更新' : '培养方案草稿已创建')
|
||
planDialog.value = false
|
||
await loadPlans(false)
|
||
} catch (error) {
|
||
ElMessage.error(apiErrorMessage(error))
|
||
}
|
||
}
|
||
|
||
function openClone() {
|
||
Object.assign(cloneForm, {
|
||
name: selected.value.name,
|
||
version: `${new Date().getFullYear()}版`,
|
||
effectiveGrade: Math.max(new Date().getFullYear(), selected.value.effectiveGrade + 1),
|
||
})
|
||
cloneDialog.value = true
|
||
}
|
||
|
||
async function clonePlan() {
|
||
try {
|
||
await http.post(`/curriculum-plans/${selected.value.id}/clone`, cloneForm)
|
||
ElMessage.success('已复制为新的草稿版本')
|
||
cloneDialog.value = false
|
||
await loadPlans(false)
|
||
} catch (error) {
|
||
ElMessage.error(apiErrorMessage(error))
|
||
}
|
||
}
|
||
|
||
async function publishPlan() {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
'发布后该版本将成为学生端展示和毕业审核依据,并自动归档同专业、同年级的旧发布版本。已发布方案仍可调整课程内容,但适用专业、年级和版本号不可更改。确定发布吗?',
|
||
'发布培养方案',
|
||
{ type: 'warning', confirmButtonText: '确认发布', cancelButtonText: '取消' },
|
||
)
|
||
await http.post(`/curriculum-plans/${selected.value.id}/publish`)
|
||
ElMessage.success('培养方案已发布')
|
||
await loadPlans()
|
||
} catch (error: any) {
|
||
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||
}
|
||
}
|
||
|
||
async function deletePlan() {
|
||
try {
|
||
await ElMessageBox.confirm('确定删除这份草稿及其全部课程配置吗?', '删除草稿', {
|
||
type: 'warning',
|
||
confirmButtonText: '删除',
|
||
cancelButtonText: '取消',
|
||
})
|
||
await http.delete(`/curriculum-plans/${selected.value.id}`)
|
||
ElMessage.success('草稿已删除')
|
||
await loadPlans(false)
|
||
} catch (error: any) {
|
||
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||
}
|
||
}
|
||
|
||
function openModule(module?: any) {
|
||
editingModuleId.value = module?.id ?? ''
|
||
Object.assign(moduleForm, {
|
||
code: module?.code ?? '',
|
||
name: module?.name ?? '',
|
||
requiredCredits: module?.requiredCredits ?? 0,
|
||
sortOrder: module?.sortOrder ?? (selected.value.modules.length + 1) * 10,
|
||
})
|
||
moduleDialog.value = true
|
||
}
|
||
|
||
async function saveModule() {
|
||
if (!moduleForm.code?.trim() || !moduleForm.name?.trim()) {
|
||
ElMessage.warning('请填写模块编码和名称。')
|
||
return
|
||
}
|
||
if (!await confirmPublishedChange('课程模块')) return
|
||
try {
|
||
const base = `/curriculum-plans/${selected.value.id}/modules`
|
||
if (editingModuleId.value) await http.put(`${base}/${editingModuleId.value}`, moduleForm)
|
||
else await http.post(base, moduleForm)
|
||
moduleDialog.value = false
|
||
await loadDetail(selected.value.id)
|
||
} catch (error) {
|
||
ElMessage.error(apiErrorMessage(error))
|
||
}
|
||
}
|
||
|
||
async function deleteModule(module: any) {
|
||
try {
|
||
const impact = isPublished.value
|
||
? '删除后会立即影响学生端展示及毕业审核。'
|
||
: ''
|
||
await ElMessageBox.confirm(`删除“${module.name}”及其中课程?${impact}`, '删除课程模块', {
|
||
type: 'warning',
|
||
})
|
||
await http.delete(`/curriculum-plans/${selected.value.id}/modules/${module.id}`)
|
||
await loadDetail(selected.value.id)
|
||
} catch (error: any) {
|
||
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||
}
|
||
}
|
||
|
||
function openCourse(moduleId: string, item?: any) {
|
||
activeModuleId.value = moduleId
|
||
editingCourseId.value = item?.id ?? ''
|
||
Object.assign(courseForm, {
|
||
courseId: item?.courseId,
|
||
recommendedSemester: item?.recommendedSemester ?? 1,
|
||
type: item?.type ?? 'Required',
|
||
notes: item?.notes ?? '',
|
||
})
|
||
courseDialog.value = true
|
||
}
|
||
|
||
async function saveCourse() {
|
||
if (!courseForm.courseId) {
|
||
ElMessage.warning('请选择课程。')
|
||
return
|
||
}
|
||
if (!await confirmPublishedChange('方案课程')) return
|
||
try {
|
||
const base = `/curriculum-plans/${selected.value.id}/modules/${activeModuleId.value}/courses`
|
||
if (editingCourseId.value) await http.put(`${base}/${editingCourseId.value}`, courseForm)
|
||
else await http.post(base, courseForm)
|
||
courseDialog.value = false
|
||
await loadDetail(selected.value.id)
|
||
} catch (error) {
|
||
ElMessage.error(apiErrorMessage(error))
|
||
}
|
||
}
|
||
|
||
async function deleteCourse(moduleId: string, item: any) {
|
||
try {
|
||
const impact = isPublished.value
|
||
? '移除后会立即影响学生端展示及毕业审核。'
|
||
: ''
|
||
await ElMessageBox.confirm(`从方案中移除“${item.courseName}”?${impact}`, '移除课程', {
|
||
type: 'warning',
|
||
})
|
||
await http.delete(
|
||
`/curriculum-plans/${selected.value.id}/modules/${moduleId}/courses/${item.id}`,
|
||
)
|
||
await loadDetail(selected.value.id)
|
||
} catch (error: any) {
|
||
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||
}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
const [optionRes, courseRes] = await Promise.all([
|
||
http.get('/curriculum-plans/filter-options'),
|
||
http.get('/courses/options'),
|
||
])
|
||
colleges.value = optionRes.data.colleges
|
||
majors.value = optionRes.data.majors
|
||
grades.value = optionRes.data.grades
|
||
courses.value = courseRes.data
|
||
if (isCollegeAdmin.value && auth.user?.collegeId) {
|
||
query.collegeId = auth.user.collegeId
|
||
}
|
||
await loadPlans(false)
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="page-stack">
|
||
<section class="page-intro">
|
||
<div>
|
||
<span class="section-kicker">PROGRAMME OF STUDY</span>
|
||
<h2>培养方案</h2>
|
||
<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.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-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; loadPlans(false)">查询</el-button>
|
||
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
||
</section>
|
||
|
||
<div class="curriculum-workspace">
|
||
<aside v-loading="loading" class="plan-list">
|
||
<button
|
||
v-for="plan in plans"
|
||
:key="plan.id"
|
||
type="button"
|
||
:class="{ active: selected?.id === plan.id }"
|
||
@click="loadDetail(plan.id)"
|
||
>
|
||
<span>{{ plan.majorName }} · {{ plan.effectiveGrade }}级</span>
|
||
<b>{{ plan.name }}</b>
|
||
<small>{{ plan.version }} · {{ plan.courseCount }} 门课程</small>
|
||
<i :class="plan.status.toLowerCase()">{{ statusLabels[plan.status] }}</i>
|
||
</button>
|
||
<el-empty v-if="!loading && plans.length === 0" description="暂无培养方案" :image-size="70" />
|
||
<div class="plan-pagination">
|
||
<span>共 {{ total }} 版</span>
|
||
<el-pagination v-model:current-page="query.page" :page-size="query.pageSize" :total="total" layout="prev, next" @current-change="loadPlans(false)" />
|
||
</div>
|
||
</aside>
|
||
|
||
<main v-loading="detailLoading" class="plan-detail">
|
||
<el-empty v-if="!selected" description="请选择或新建一份培养方案" />
|
||
<template v-else>
|
||
<header class="plan-detail-head">
|
||
<div>
|
||
<span>{{ selected.collegeName }} / {{ selected.majorName }}</span>
|
||
<h3>{{ selected.name }}</h3>
|
||
<p>{{ selected.version }} · 适用 {{ selected.effectiveGrade }} 级 · {{ selected.schoolingYears }} 年制</p>
|
||
</div>
|
||
<div class="plan-actions">
|
||
<el-button v-if="canEdit" @click="openPlan(selected)">编辑</el-button>
|
||
<el-button :icon="CopyDocument" @click="openClone">复制版本</el-button>
|
||
<el-button v-if="isDraft" type="success" :icon="Promotion" @click="publishPlan">发布</el-button>
|
||
<el-button v-if="isDraft" type="danger" plain @click="deletePlan">删除</el-button>
|
||
</div>
|
||
</header>
|
||
|
||
<el-alert
|
||
v-if="isPublished"
|
||
class="published-edit-notice"
|
||
type="warning"
|
||
show-icon
|
||
:closable="false"
|
||
title="当前方案已发布,修改保存后会立即影响学生端展示及毕业审核依据;适用专业、入学年级和版本号不可更改。"
|
||
/>
|
||
|
||
<div class="plan-metrics">
|
||
<div><span>方案状态</span><b>{{ statusLabels[selected.status] }}</b></div>
|
||
<div><span>总学分要求</span><b>{{ selected.totalCredits }}</b></div>
|
||
<div><span>模块学分合计</span><b :class="{ warning: configuredCredits !== Number(selected.totalCredits) }">{{ configuredCredits }}</b></div>
|
||
<div><span>课程数量</span><b>{{ selected.modules.reduce((sum: number, item: any) => sum + item.courses.length, 0) }}</b></div>
|
||
</div>
|
||
|
||
<div class="module-toolbar">
|
||
<div>
|
||
<b>课程结构</b>
|
||
<span>指定必修须逐门通过;英语、体育等多选课程用“组内选修”,修满模块最低学分即可</span>
|
||
</div>
|
||
<el-button v-if="canEdit" :icon="Plus" @click="openModule()">新增模块</el-button>
|
||
</div>
|
||
|
||
<section v-for="module in selected.modules" :key="module.id" class="curriculum-module">
|
||
<header>
|
||
<div>
|
||
<span>{{ module.code }}</span>
|
||
<h4>{{ module.name }}</h4>
|
||
</div>
|
||
<p>
|
||
最低 {{ module.requiredCredits }} 学分 · 已配置 {{ module.assignedCredits }} 学分
|
||
<template v-if="module.courses.some((course: any) => course.type === 'Elective')">
|
||
· 课程组选项
|
||
</template>
|
||
</p>
|
||
<div v-if="canEdit">
|
||
<el-button link type="primary" @click="openModule(module)">编辑</el-button>
|
||
<el-button link type="danger" @click="deleteModule(module)">删除</el-button>
|
||
<el-button size="small" :icon="Plus" @click="openCourse(module.id)">添加课程</el-button>
|
||
</div>
|
||
</header>
|
||
<el-table :data="module.courses" class="curriculum-course-table">
|
||
<el-table-column prop="courseCode" label="课程编码" width="115" />
|
||
<el-table-column label="课程名称" min-width="180">
|
||
<template #default="{ row }"><b>{{ row.courseName }}</b></template>
|
||
</el-table-column>
|
||
<el-table-column label="类别" width="80">
|
||
<template #default="{ row }">{{ courseTypeLabels[row.type] }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="课程分类" width="100">
|
||
<template #default="{ row }">{{ row.categoryName || '未分类' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="credits" label="学分" width="70" />
|
||
<el-table-column prop="totalHours" label="学时" width="70" />
|
||
<el-table-column label="建议学期" width="90">
|
||
<template #default="{ row }">第 {{ row.recommendedSemester }} 学期</template>
|
||
</el-table-column>
|
||
<el-table-column v-if="canEdit" label="操作" width="120">
|
||
<template #default="{ row }">
|
||
<el-button link type="primary" @click="openCourse(module.id, row)">编辑</el-button>
|
||
<el-button link type="danger" @click="deleteCourse(module.id, row)">移除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
<template #empty><el-empty description="该模块尚未配置课程" :image-size="54" /></template>
|
||
</el-table>
|
||
</section>
|
||
<el-empty v-if="selected.modules.length === 0" description="先建立课程模块,再添加课程" />
|
||
</template>
|
||
</main>
|
||
</div>
|
||
|
||
<el-dialog v-model="planDialog" :title="editingPlanId ? '编辑培养方案' : '新建培养方案'" width="620px">
|
||
<el-alert
|
||
v-if="isEditingPublished"
|
||
type="warning"
|
||
show-icon
|
||
:closable="false"
|
||
title="已发布方案的修改会立即生效。适用专业、入学年级和版本号已锁定,如需调整请复制为新版本。"
|
||
/>
|
||
<el-form label-position="top">
|
||
<el-form-item label="适用专业" required>
|
||
<el-select v-model="planForm.majorId" filterable :disabled="isEditingPublished"><el-option v-for="item in availableMajors" :key="item.id" :label="item.name" :value="item.id" /></el-select>
|
||
</el-form-item>
|
||
<el-form-item label="方案名称" required><el-input v-model="planForm.name" /></el-form-item>
|
||
<div class="form-grid">
|
||
<el-form-item label="版本号" required><el-input v-model="planForm.version" :disabled="isEditingPublished" /></el-form-item>
|
||
<el-form-item label="适用入学年级" required><el-input-number v-model="planForm.effectiveGrade" :min="2000" :max="2200" :disabled="isEditingPublished" /></el-form-item>
|
||
</div>
|
||
<el-form-item label="毕业总学分要求" required><el-input-number v-model="planForm.totalCredits" :min="0.1" :precision="1" :step="0.5" /></el-form-item>
|
||
<el-form-item label="方案说明"><el-input v-model="planForm.description" type="textarea" :rows="3" /></el-form-item>
|
||
</el-form>
|
||
<template #footer><el-button @click="planDialog = false">取消</el-button><el-button type="primary" @click="savePlan">保存</el-button></template>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="cloneDialog" title="复制为新版本" width="520px">
|
||
<el-form label-position="top">
|
||
<el-form-item label="方案名称" required><el-input v-model="cloneForm.name" /></el-form-item>
|
||
<div class="form-grid">
|
||
<el-form-item label="新版本号" required><el-input v-model="cloneForm.version" /></el-form-item>
|
||
<el-form-item label="适用入学年级" required><el-input-number v-model="cloneForm.effectiveGrade" :min="2000" :max="2200" /></el-form-item>
|
||
</div>
|
||
</el-form>
|
||
<template #footer><el-button @click="cloneDialog = false">取消</el-button><el-button type="primary" @click="clonePlan">复制</el-button></template>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="moduleDialog" :title="editingModuleId ? '编辑课程模块' : '新增课程模块'" width="520px">
|
||
<el-form label-position="top">
|
||
<div class="form-grid">
|
||
<el-form-item label="模块编码" required><el-input v-model="moduleForm.code" /></el-form-item>
|
||
<el-form-item label="排序"><el-input-number v-model="moduleForm.sortOrder" :min="0" /></el-form-item>
|
||
</div>
|
||
<el-form-item label="模块名称" required><el-input v-model="moduleForm.name" /></el-form-item>
|
||
<el-form-item label="最低学分要求">
|
||
<el-input-number v-model="moduleForm.requiredCredits" :min="0" :precision="1" :step="0.5" />
|
||
<span class="form-help">体育、英语等课程组填写学生必须从组内修满的学分。</span>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer><el-button @click="moduleDialog = false">取消</el-button><el-button type="primary" @click="saveModule">保存</el-button></template>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="courseDialog" :title="editingCourseId ? '编辑方案课程' : '添加方案课程'" width="560px">
|
||
<el-form label-position="top">
|
||
<el-form-item label="课程" required>
|
||
<el-select v-model="courseForm.courseId" filterable>
|
||
<el-option v-for="item in courses" :key="item.id" :label="`${item.code} · ${item.name}${item.categoryName ? ` · ${item.categoryName}` : ''}`" :value="item.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<div class="form-grid">
|
||
<el-form-item label="修读规则"><el-select v-model="courseForm.type"><el-option v-for="(label, value) in courseTypeLabels" :key="value" :label="label" :value="value" /></el-select></el-form-item>
|
||
<el-form-item label="建议学期"><el-input-number v-model="courseForm.recommendedSemester" :min="1" :max="selected?.schoolingYears * 2" /></el-form-item>
|
||
</div>
|
||
<el-form-item label="备注"><el-input v-model="courseForm.notes" type="textarea" :rows="2" /></el-form-item>
|
||
<div class="course-rule-help">
|
||
<b>指定必修</b><span>该门课程必须通过</span>
|
||
<b>组内选修</b><span>足球、篮球、英语分层等作为选项,按模块最低学分验收</span>
|
||
</div>
|
||
</el-form>
|
||
<template #footer><el-button @click="courseDialog = false">取消</el-button><el-button type="primary" @click="saveCourse">保存</el-button></template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|