184 lines
11 KiB
Vue
184 lines
11 KiB
Vue
<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>
|