选课名单/课程修读情况

This commit is contained in:
biss committed 2026-08-14 21:50:14 +08:00
1 parent 4575758812
commit ef3fbf92f0
7 files changed
+241 -139

No files matched your search

+1 -8
View File
@@ -171,13 +171,6 @@ const navigationGroups = computed<NavigationGroup[]>(() => [
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin']),
{ path: '/course-enrollments', label: '修读课程管理' },
),
...whenVisible(
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher']),
{
path: '/course-adjustments',
label: isTeacher.value ? '我的调停课' : '调停课审核',
},
),
],
},
{
@@ -247,7 +240,7 @@ const navigationGroups = computed<NavigationGroup[]>(() => [
),
...whenVisible(
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor', 'Teacher', 'Student']),
{ path: '/approvals', label: '审批中心' },
{ path: '/approvals', label: '审批工作台' },
),
...whenVisible(
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'Counselor', 'Student']),
+46 -8
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { useRoute } from 'vue-router'
import {
Check,
Clock,
@@ -49,6 +50,7 @@ interface SubstitutionTarget {
}
const auth = useAuthStore()
const route = useRoute()
const isManager = computed(() => auth.user?.roles.some(r => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(r)) ?? false)
const isTeacher = computed(() => auth.user?.roles.includes('Teacher') && !isManager.value)
const isStudent = computed(() => auth.user?.roles.includes('Student') && !isManager.value)
@@ -92,6 +94,8 @@ const dialogTitle = computed(() => ({
substitution: '发起课程替代申请',
}[dialogType.value] ?? '发起申请'))
const actionableApprovalTypes = ['CourseExemption', 'DeferredExam', 'CourseSubstitution', 'CourseAdjustment']
const selectedCourse = computed(() =>
enrolledCourses.value.find(course => course.teachingTaskId === form.teachingTaskId))
@@ -278,7 +282,7 @@ async function submitForm() {
async function approveItem(item: any) {
try {
const ep = `/approvals/${item.type === 'CourseExemption' ? 'exemptions' : item.type === 'DeferredExam' ? 'deferred' : item.type === 'CourseSubstitution' ? 'substitutions' : ''}`
const ep = approvalEndpoint(item.type)
if (ep) {
await http.post(`${ep}/${item.id}/approve`)
ElMessage.success('已通过')
@@ -298,7 +302,7 @@ function openReject(item: any) {
async function rejectSubmit() {
if (!rejectItem.value) return
try {
const ep = `/approvals/${rejectItem.value.type === 'CourseExemption' ? 'exemptions' : rejectItem.value.type === 'DeferredExam' ? 'deferred' : rejectItem.value.type === 'CourseSubstitution' ? 'substitutions' : ''}`
const ep = approvalEndpoint(rejectItem.value.type)
if (ep) {
await http.post(`${ep}/${rejectItem.value.id}/reject`, { comment: rejectComment.value })
rejectDialog.value = false
@@ -310,6 +314,14 @@ async function rejectSubmit() {
}
}
function approvalEndpoint(type: string) {
if (type === 'CourseAdjustment') return '/course-adjustments'
const segment = type === 'CourseExemption' ? 'exemptions'
: type === 'DeferredExam' ? 'deferred'
: type === 'CourseSubstitution' ? 'substitutions' : ''
return segment ? `/approvals/${segment}` : ''
}
function statusText(status: string) {
return status === 'Submitted' ? '待审核' : status === 'Approved' ? '已通过' : '已驳回'
}
@@ -323,7 +335,10 @@ function courseScheduleText(course: CourseOption) {
return course.hasPublishedSchedule ? '课表已发布' : '等待课表发布'
}
onMounted(load)
onMounted(() => {
if (isManager.value && route.query.tab === 'pending') tab.value = 'pending'
return load()
})
</script>
<template>
@@ -331,8 +346,8 @@ onMounted(load)
<section class="page-intro">
<div>
<span class="section-kicker">APPROVAL CENTER</span>
<h2>审批中心</h2>
<p>{{ isStudent ? '从实际修读课程发起申请,查看审核进度与处理结果。' : '集中处理教务申请与审核任务。' }}</p>
<h2>审批工作台</h2>
<p>{{ isStudent ? '从实际修读课程发起申请,查看审核进度与处理结果。' : isTeacher ? '从一个入口发起教学申请,集中追踪处理进度。' : '集中处理全部教务申请与审核任务。' }}</p>
</div>
<el-button :icon="Refresh" @click="load">刷新</el-button>
</section>
@@ -367,6 +382,27 @@ onMounted(load)
</button>
</section>
<section v-if="isTeacher" class="application-launcher teacher-launcher">
<button class="launch-card adjustment" type="button" @click="$router.push({ path: '/course-adjustments', query: { create: '1', from: 'approvals' } })">
<span class="launch-icon"><Switch /></span>
<span class="launch-copy">
<small>COURSE ADJUSTMENT</small>
<b>发起调停课</b>
<em>调课、停课、补课、代课统一从审批工作台发起</em>
</span>
<span class="launch-meta">填写申请 <Plus /></span>
</button>
<button class="launch-card grade-modification" type="button" @click="openDialog('gradeMod')">
<span class="launch-icon"><DocumentChecked /></span>
<span class="launch-copy">
<small>GRADE MODIFICATION</small>
<b>发起成绩修改</b>
<em>提交成绩修正依据,进入同一审批队列</em>
</span>
<span class="launch-meta">填写申请 <Plus /></span>
</button>
</section>
<div class="approval-tabs">
<el-segmented
v-model="tab"
@@ -375,7 +411,6 @@ onMounted(load)
{ label: `我的记录${isStudent ? ` (${recordCount})` : ''}`, value: 'mine' },
]"
/>
<el-button v-if="isTeacher" :icon="Plus" type="primary" @click="openDialog('gradeMod')">成绩修改</el-button>
</div>
<section v-if="tab === 'pending'" v-loading="loading" class="appr-list">
@@ -386,7 +421,7 @@ onMounted(load)
<p>{{ item.desc }}</p>
<small>{{ item.college }} · {{ new Date(item.time).toLocaleString('zh-CN') }}</small>
</div>
<div v-if="['CourseExemption','DeferredExam','CourseSubstitution'].includes(item.type)" class="appr-actions">
<div v-if="actionableApprovalTypes.includes(item.type)" class="appr-actions">
<el-button size="small" type="danger" plain :icon="Close" @click="openReject(item)">驳回</el-button>
<el-button size="small" type="success" :icon="Check" @click="approveItem(item)">通过</el-button>
</div>
@@ -395,7 +430,7 @@ onMounted(load)
size="small"
text
type="primary"
@click="$router.push(item.type==='StudentStatusChange'?'/student-status-changes':item.type==='CourseAdjustment'?'/course-adjustments':item.type==='GradeSheet'?'/grades':'/teacher-attendance')"
@click="$router.push(item.type==='StudentStatusChange'?'/student-status-changes':item.type==='GradeSheet'?'/grades':'/teacher-attendance')"
>
查看 →
</el-button>
@@ -668,6 +703,9 @@ onMounted(load)
.launch-icon svg, .dialog-lead-icon svg { width: 21px; }
.deferred .launch-icon, .dialog-lead-icon.deferred { color: #a85d00; background: #fff3dc; }
.substitution .launch-icon { color: #087f5b; background: #e8f8f1; }
.adjustment .launch-icon { color: #7c3aed; background: #f1eafe; }
.grade-modification .launch-icon { color: #0d7490; background: #e1f5f8; }
.teacher-launcher { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.launch-copy { display: grid; gap: 3px; min-width: 0; }
.launch-copy small { color: #8791a3; font: 600 9px/1.2 ui-monospace, monospace; letter-spacing: .1em; }
.launch-copy b { margin-top: 3px; font-size: 17px; }
+7
View File
@@ -1,11 +1,14 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Check, Plus, Refresh } from '@element-plus/icons-vue'
import http, { apiErrorMessage } from '../api/http'
import { useAuthStore } from '../stores/auth'
import { academicTermLabel, academicTermOptionClass, defaultAcademicTermId } from '../utils/academicTerms'
const auth = useAuthStore()
const route = useRoute()
const router = useRouter()
const isManager = computed(() =>
auth.user?.roles.some(r => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(r)) ?? false)
const isTeacher = computed(() => auth.user?.roles.includes('Teacher') && !isManager.value)
@@ -246,6 +249,10 @@ onMounted(async () => {
ElMessage.error(apiErrorMessage(error))
}
await load()
if (isTeacher.value && route.query.create === '1') {
openCreate()
await router.replace({ path: '/course-adjustments' })
}
})
function changeType() {
Loaded 3 of 7 files, more files were not shown because too many files have changed in this diff. Show more