各类申请

This commit is contained in:
2026-07-25 16:51:12 +08:00 Unverified
parent d02b0c56a1
commit 5e4c76a9e4
10 changed files with 901 additions and 4 deletions
+183
View File
@@ -0,0 +1,183 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { Check, Close, Plus, Refresh } from '@element-plus/icons-vue'
import http, { apiErrorMessage } from '../api/http'
import { useAuthStore } from '../stores/auth'
const auth = useAuthStore()
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)
const pending = ref<any[]>([])
const loading = ref(false)
const tab = ref(isManager.value ? 'pending' : 'mine')
const dialog = ref(false)
const dialogType = ref('')
const rejectDialog = ref(false)
const rejectItem = ref<any>(null)
const rejectComment = ref('')
const enrolledCourses = ref<any[]>([])
const gradeRecords = ref<any[]>([])
const myGrades = ref<any[]>([])
const myExemptions = ref<any[]>([])
const myDeferred = ref<any[]>([])
const form = reactive({ teachingTaskId: '', reason: '', originalCourseId: '', substituteCourseId: '', gradeRecordId: '', requestedScore: 0 })
async function load() {
loading.value = true
try {
if (isManager.value) pending.value = (await http.get('/approvals/pending')).data
if (isStudent.value) {
const [courses, grades, ex, df] = await Promise.all([
http.get('/approvals/my-courses'), http.get('/approvals/my-grades'),
http.get('/approvals/exemptions/mine'), http.get('/approvals/deferred/mine'),
])
enrolledCourses.value = courses.data; myGrades.value = grades.data
myExemptions.value = ex.data; myDeferred.value = df.data
}
if (isTeacher.value) gradeRecords.value = (await http.get('/approvals/grade-records')).data
} catch (e) { ElMessage.error(apiErrorMessage(e)) }
finally { loading.value = false }
}
function openDialog(type: string) {
dialogType.value = type
Object.assign(form, { teachingTaskId: '', reason: '', originalCourseId: '', substituteCourseId: '', gradeRecordId: '', requestedScore: 0 })
dialog.value = true
}
async function submitForm() {
if (!form.reason.trim()) { ElMessage.warning('请填写原因'); return }
try {
if (dialogType.value === 'exemption') {
if (!form.teachingTaskId) { ElMessage.warning('请选择课程'); return }
await http.post('/approvals/exemptions', { teachingTaskId: form.teachingTaskId, reason: form.reason })
} else if (dialogType.value === 'deferred') {
if (!form.teachingTaskId) { ElMessage.warning('请选择课程'); return }
await http.post('/approvals/deferred', { teachingTaskId: form.teachingTaskId, reason: form.reason })
} else if (dialogType.value === 'gradeMod') {
if (!form.gradeRecordId) { ElMessage.warning('请选择成绩记录'); return }
await http.post('/approvals/grade-modifications', { gradeRecordId: form.gradeRecordId, requestedScore: form.requestedScore, reason: form.reason })
} else if (dialogType.value === 'substitution') {
if (!form.originalCourseId || !form.substituteCourseId) { ElMessage.warning('请选择两门课程'); return }
await http.post('/approvals/substitutions', { originalCourseId: form.originalCourseId, substituteCourseId: form.substituteCourseId, reason: form.reason })
}
dialog.value = false; ElMessage.success('申请已提交'); await load()
} catch (e) { ElMessage.error(apiErrorMessage(e)) }
}
async function approveItem(item: any) {
try {
const ep = `/approvals/${item.type === 'CourseExemption' ? 'exemptions' : item.type === 'DeferredExam' ? 'deferred' : item.type === 'CourseSubstitution' ? 'substitutions' : ''}`;
if (ep) { await http.post(`${ep}/${item.id}/approve`); ElMessage.success('已通过'); await load() }
} catch (e) { ElMessage.error(apiErrorMessage(e)) }
}
function openReject(item: any) { rejectItem.value = item; rejectComment.value = ''; rejectDialog.value = true }
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' : ''}`;
if (ep) { await http.post(`${ep}/${rejectItem.value.id}/reject`, { comment: rejectComment.value }); rejectDialog.value = false; ElMessage.success('已驳回'); await load() }
} catch (e) { ElMessage.error(apiErrorMessage(e)) }
}
onMounted(load)
</script>
<template>
<div class="page-stack appr-page">
<section class="page-intro">
<div><span class="section-kicker">APPROVAL CENTER</span><h2>审批中心</h2><p>学籍异动调停课成绩审核考勤申诉免修缓考成绩修改课程替代</p></div>
<div style="display:flex;gap:8px">
<el-button v-if="isStudent" :icon="Plus" @click="openDialog('exemption')">免修</el-button>
<el-button v-if="isStudent" :icon="Plus" @click="openDialog('deferred')">缓考</el-button>
<el-button v-if="isStudent" :icon="Plus" @click="openDialog('substitution')">课程替代</el-button>
<el-button v-if="isTeacher" :icon="Plus" type="primary" @click="openDialog('gradeMod')">成绩修改</el-button>
<el-button :icon="Refresh" @click="load">刷新</el-button>
</div>
</section>
<el-segmented v-model="tab" :options="[
...(isManager ? [{ label: `待审批 (${pending.length})`, value: 'pending' }] : []),
{ label: '我的记录', value: 'mine' },
]" style="margin-bottom:16px" />
<!-- Pending -->
<section v-if="tab === 'pending'" v-loading="loading" class="appr-list">
<article v-for="item in pending" :key="`${item.type}-${item.id}`" class="appr-card">
<el-tag size="small" :type="item.label === '考勤申诉' ? 'warning' : 'primary'">{{ item.label }}</el-tag>
<div class="appr-body"><b>{{ item.title }}</b><p>{{ item.desc }}</p><small>{{ item.college }} · {{ new Date(item.time).toLocaleString('zh-CN') }}</small></div>
<div class="appr-actions" v-if="['CourseExemption','DeferredExam','CourseSubstitution'].includes(item.type)">
<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>
<div class="appr-actions" v-else><el-button 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')">查看 </el-button></div>
</article>
<el-empty v-if="!pending.length" description="暂无待审批" />
</section>
<!-- My records -->
<section v-if="tab === 'mine'" v-loading="loading">
<template v-if="isStudent">
<h4 style="margin-bottom:8px">免修申请</h4>
<div class="appr-list" style="margin-bottom:16px">
<article v-for="e in myExemptions" :key="e.id" class="appr-card"><el-tag size="small" :type="e.status === 'Approved' ? 'success' : e.status === 'Rejected' ? 'danger' : 'warning'">{{ e.status === 'Submitted' ? '待审核' : e.status === 'Approved' ? '已通过' : '已驳回' }}</el-tag><div class="appr-body"><b>{{ e.courseName }}</b><p>{{ e.reason }}</p><small v-if="e.reviewComment">{{ e.reviewComment }}</small></div></article>
<el-empty v-if="!myExemptions.length" description="无" />
</div>
<h4 style="margin-bottom:8px">缓考申请</h4>
<div class="appr-list" style="margin-bottom:16px">
<article v-for="d in myDeferred" :key="d.id" class="appr-card"><el-tag size="small" :type="d.status === 'Approved' ? 'success' : d.status === 'Rejected' ? 'danger' : 'warning'">{{ d.status === 'Submitted' ? '待审核' : d.status === 'Approved' ? '已通过' : '已驳回' }}</el-tag><div class="appr-body"><b>{{ d.courseName }}</b><p>{{ d.reason }}</p></div></article>
<el-empty v-if="!myDeferred.length" description="无" />
</div>
</template>
</section>
<!-- Dialog -->
<el-dialog v-model="dialog" :title="dialogType === 'exemption' ? '免修申请' : dialogType === 'deferred' ? '缓考申请' : dialogType === 'gradeMod' ? '成绩修改申请' : '课程替代申请'" width="560px">
<el-form label-position="top">
<template v-if="dialogType === 'exemption' || dialogType === 'deferred'">
<el-form-item label="选择课程" required>
<el-select v-model="form.teachingTaskId" filterable><el-option v-for="c in enrolledCourses" :key="c.teachingTaskId" :label="`${c.courseCode} ${c.courseName}`" :value="c.teachingTaskId" /></el-select>
</el-form-item>
</template>
<template v-if="dialogType === 'substitution'">
<el-form-item label="未通过课程(被替代)" required>
<el-select v-model="form.originalCourseId" filterable><el-option v-for="g in myGrades.filter((x:any) => Number(x.totalScore) < 60)" :key="g.courseId" :label="`${g.courseCode} ${g.courseName} (${g.totalScore}分)`" :value="g.courseId" /></el-select>
</el-form-item>
<el-form-item label="已通过课程(替代课程)" required>
<el-select v-model="form.substituteCourseId" filterable><el-option v-for="g in myGrades.filter((x:any) => Number(x.totalScore) >= 60)" :key="g.courseId" :label="`${g.courseCode} ${g.courseName} (${g.totalScore}分)`" :value="g.courseId" /></el-select>
</el-form-item>
</template>
<template v-if="dialogType === 'gradeMod'">
<el-form-item label="成绩记录" required>
<el-select v-model="form.gradeRecordId" filterable><el-option v-for="r in gradeRecords" :key="r.id" :label="`${r.studentName}(${r.studentNumber}) — ${r.courseName} — ${r.totalScore}分`" :value="r.id" /></el-select>
</el-form-item>
<el-form-item label="修改为" required><el-input-number v-model="form.requestedScore" :min="0" :max="100" /></el-form-item>
</template>
<el-form-item label="原因" required><el-input v-model="form.reason" type="textarea" :rows="3" maxlength="500" show-word-limit /></el-form-item>
</el-form>
<template #footer><el-button @click="dialog = false">取消</el-button><el-button type="primary" @click="submitForm">提交申请</el-button></template>
</el-dialog>
<!-- Reject -->
<el-dialog v-model="rejectDialog" title="驳回" width="460px">
<el-input v-model="rejectComment" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="驳回原因" />
<template #footer><el-button @click="rejectDialog = false">取消</el-button><el-button type="danger" @click="rejectSubmit">确认驳回</el-button></template>
</el-dialog>
</div>
</template>
<style scoped>
.appr-list { display: grid; gap: 8px; }
.appr-card { display: flex; align-items: center; gap: 14px; padding: 14px 18px; background: #fff; border: 1px solid #e4e7ed; border-radius: 8px; }
.appr-body { flex: 1; min-width: 0; }
.appr-body b { font-size: 14px; display: block; margin-bottom: 4px; }
.appr-body p { font-size: 13px; color: #606266; margin: 0 0 4px; }
.appr-body small { font-size: 11px; color: var(--muted); }
.appr-actions { display: flex; gap: 6px; flex-shrink: 0; }
</style>
+7 -1
View File
@@ -289,7 +289,13 @@ onMounted(async () => {
<el-table-column label="学号" width="120">
<template #default="{ row }"><span class="registry-number">{{ row.studentNumber }}</span></template>
</el-table-column>
<el-table-column prop="name" label="姓名" min-width="80" />
<el-table-column label="姓名" min-width="110">
<template #default="{ row }">
<span>{{ row.name }}</span>
<el-tag v-if="row.isExempt" size="small" type="success" style="margin-left:4px">免修</el-tag>
<el-tag v-if="row.isDeferred" size="small" type="warning" style="margin-left:4px">缓考</el-tag>
</template>
</el-table-column>
<el-table-column label="考勤" width="110">
<template #default="{ row }">
<el-select