毕业审核:批次计算、缺失课程检查、人工复核、结果发布。 学位授予:毕业资格与 GPA 计算、人工调整、发布授予结果。 毕业离校:离校事项配置、责任角色分工、逐项办理及批次关闭。
202 lines
11 KiB
Vue
202 lines
11 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, reactive, ref } from 'vue'
|
||
import { Check, Close, Medal, Plus, Refresh, Stamp } from '@element-plus/icons-vue'
|
||
import http, { apiErrorMessage } from '../api/http'
|
||
import { useAuthStore } from '../stores/auth'
|
||
|
||
const auth = useAuthStore()
|
||
const isStudent = computed(() => auth.user?.roles.includes('Student') ?? false)
|
||
const isPublisher = computed(() =>
|
||
auth.user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin'].includes(role)) ?? false)
|
||
const loading = ref(false)
|
||
const batches = ref<any[]>([])
|
||
const selected = ref<any | null>(null)
|
||
const myResult = ref<any | null>(null)
|
||
const createDialog = ref(false)
|
||
const reviewDialog = ref(false)
|
||
const reviewTarget = ref<any | null>(null)
|
||
const keyword = ref('')
|
||
const conclusionFilter = ref('')
|
||
const batchForm = reactive({
|
||
name: '2030届学士学位授予审核',
|
||
graduationYear: 2030,
|
||
degreeName: '工学学士',
|
||
minimumGradePoint: 2,
|
||
notes: '',
|
||
})
|
||
const reviewForm = reactive({ conclusion: 'NotGranted', comment: '' })
|
||
const conclusionLabels: Record<string, string> = {
|
||
Granted: '建议授予',
|
||
NotGranted: '暂不授予',
|
||
}
|
||
const filteredResults = computed(() => {
|
||
const q = keyword.value.trim().toLowerCase()
|
||
return (selected.value?.results ?? []).filter((x: any) =>
|
||
(!conclusionFilter.value || x.conclusion === conclusionFilter.value) &&
|
||
(!q || `${x.studentNumber}${x.name}${x.className}${x.majorName}`.toLowerCase().includes(q)))
|
||
})
|
||
function dateText(value?: string) {
|
||
if (!value) return '—'
|
||
return new Intl.DateTimeFormat('zh-CN', {
|
||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||
hour: '2-digit', minute: '2-digit', hour12: false,
|
||
}).format(new Date(value))
|
||
}
|
||
async function load() {
|
||
loading.value = true
|
||
try {
|
||
if (isStudent.value) {
|
||
myResult.value = (await http.get('/degree-awards/my-result')).data
|
||
return
|
||
}
|
||
batches.value = (await http.get('/degree-awards/batches')).data
|
||
const batch = batches.value.find((x) => x.id === selected.value?.id) ?? batches.value[0]
|
||
if (batch) await selectBatch(batch.id)
|
||
else selected.value = null
|
||
} catch (error) {
|
||
ElMessage.error(apiErrorMessage(error))
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
async function selectBatch(id: string) {
|
||
selected.value = (await http.get(`/degree-awards/batches/${id}`)).data
|
||
}
|
||
async function createBatch() {
|
||
try {
|
||
const response = await http.post('/degree-awards/batches', batchForm)
|
||
createDialog.value = false
|
||
await http.post(`/degree-awards/batches/${response.data.id}/calculate`)
|
||
ElMessage.success('授予批次已建立,并完成首次规则计算。')
|
||
await load()
|
||
} catch (error) {
|
||
ElMessage.error(apiErrorMessage(error))
|
||
}
|
||
}
|
||
async function calculate() {
|
||
try {
|
||
await ElMessageBox.confirm('重新计算会覆盖当前人工复核结论。', '重新计算学位资格', {
|
||
type: 'warning', confirmButtonText: '重新计算',
|
||
})
|
||
await http.post(`/degree-awards/batches/${selected.value.id}/calculate`)
|
||
await load()
|
||
} catch (error: any) {
|
||
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||
}
|
||
}
|
||
function openReview(row: any) {
|
||
reviewTarget.value = row
|
||
reviewForm.conclusion = row.conclusion
|
||
reviewForm.comment = row.reviewComment ?? ''
|
||
reviewDialog.value = true
|
||
}
|
||
async function saveReview() {
|
||
if (reviewForm.comment.trim().length < 5) {
|
||
ElMessage.warning('复核意见至少填写 5 个字。')
|
||
return
|
||
}
|
||
try {
|
||
await http.put(`/degree-awards/results/${reviewTarget.value.id}`, reviewForm)
|
||
reviewDialog.value = false
|
||
ElMessage.success('学位复核结论已保存。')
|
||
await selectBatch(selected.value.id)
|
||
} catch (error) {
|
||
ElMessage.error(apiErrorMessage(error))
|
||
}
|
||
}
|
||
async function publish() {
|
||
try {
|
||
await ElMessageBox.confirm('发布后授予结论将锁定并开放学生查询。', '发布学位授予结果', {
|
||
type: 'warning', confirmButtonText: '确认发布',
|
||
})
|
||
await http.post(`/degree-awards/batches/${selected.value.id}/publish`)
|
||
await load()
|
||
ElMessage.success('学位授予结果已发布。')
|
||
} catch (error: any) {
|
||
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||
}
|
||
}
|
||
|
||
onMounted(load)
|
||
</script>
|
||
|
||
<template>
|
||
<div class="page-stack degree-page">
|
||
<section class="page-intro">
|
||
<div>
|
||
<span class="section-kicker">DEGREE CONFERRAL</span>
|
||
<h2>{{ isStudent ? '我的学位授予结果' : '学位授予审核' }}</h2>
|
||
<p>{{ isStudent ? '查看学校正式发布的学位授予结论。' : '以已发布毕业资格为前置条件,结合正式成绩加权平均绩点生成授予建议。' }}</p>
|
||
</div>
|
||
<el-button v-if="isPublisher && !isStudent" type="primary" :icon="Plus" @click="createDialog = true">新建授予批次</el-button>
|
||
<el-button v-else :icon="Refresh" @click="load">刷新</el-button>
|
||
</section>
|
||
|
||
<template v-if="isStudent">
|
||
<section v-if="myResult" class="degree-certificate" :class="{ granted: myResult.conclusion === 'Granted' }" v-loading="loading">
|
||
<div class="degree-seal"><el-icon><Medal /></el-icon><span>DEGREE CONFERRAL</span></div>
|
||
<div class="degree-copy">
|
||
<span>{{ myResult.graduationYear }}届 · {{ myResult.batchName }}</span>
|
||
<h3>{{ conclusionLabels[myResult.conclusion] }}</h3>
|
||
<p>{{ myResult.name }}({{ myResult.studentNumber }}),{{ myResult.majorName }}</p>
|
||
<dl><div><dt>学位名称</dt><dd>{{ myResult.degreeName }}</dd></div><div><dt>平均绩点</dt><dd>{{ myResult.averageGradePoint }}</dd></div><div><dt>最低要求</dt><dd>{{ myResult.minimumGradePoint }}</dd></div></dl>
|
||
<footer><span>发布于 {{ dateText(myResult.publishedAt) }}</span><b>{{ myResult.reviewComment || myResult.exceptionReason || '符合批次授予规则' }}</b></footer>
|
||
</div>
|
||
</section>
|
||
<el-empty v-else v-loading="loading" description="学校尚未发布你的学位授予结果" />
|
||
</template>
|
||
|
||
<template v-else>
|
||
<section class="degree-batch-strip">
|
||
<button v-for="batch in batches" :key="batch.id" :class="{ active: selected?.id === batch.id }" @click="selectBatch(batch.id)">
|
||
<span>{{ batch.graduationYear }}届 · {{ batch.degreeName }}</span><b>{{ batch.name }}</b>
|
||
<small>{{ batch.resultCount }} 人 · {{ batch.grantedCount }} 人建议授予</small><i>{{ batch.status === 'Published' ? '已发布' : '审核中' }}</i>
|
||
</button>
|
||
</section>
|
||
<section v-if="selected" class="degree-workbench" v-loading="loading">
|
||
<header>
|
||
<div><span>CONFERRAL REGISTER</span><h3>{{ selected.name }}</h3><p>{{ selected.degreeName }} · 最低平均绩点 {{ selected.minimumGradePoint }} · 计算于 {{ dateText(selected.calculatedAt) }}</p></div>
|
||
<div v-if="selected.status === 'Draft' && isPublisher"><el-button :icon="Refresh" @click="calculate">重新计算</el-button><el-button type="primary" :icon="Stamp" @click="publish">发布结果</el-button></div>
|
||
<el-tag v-else type="success" effect="plain">授予结果已锁定</el-tag>
|
||
</header>
|
||
<div class="degree-summary">
|
||
<div><span>参审人数</span><b>{{ selected.results.length }}</b></div>
|
||
<div><span>建议授予</span><b>{{ selected.results.filter((x: any) => x.conclusion === 'Granted').length }}</b></div>
|
||
<div><span>暂不授予</span><b>{{ selected.results.filter((x: any) => x.conclusion === 'NotGranted').length }}</b></div>
|
||
<div><span>人工调整</span><b>{{ selected.results.filter((x: any) => x.isOverridden).length }}</b></div>
|
||
</div>
|
||
<div class="graduation-filter">
|
||
<el-input v-model="keyword" clearable placeholder="搜索学号、姓名、专业或班级" />
|
||
<el-select v-model="conclusionFilter" clearable placeholder="全部结论"><el-option label="建议授予" value="Granted" /><el-option label="暂不授予" value="NotGranted" /></el-select>
|
||
<span>显示 {{ filteredResults.length }} 条</span>
|
||
</div>
|
||
<div class="degree-result-list">
|
||
<article v-for="row in filteredResults" :key="row.id">
|
||
<div><span>{{ row.studentNumber }} · {{ row.className }}</span><h4>{{ row.name }}</h4><p>{{ row.majorName }}</p></div>
|
||
<div><span>加权平均绩点</span><b>{{ row.averageGradePoint }}</b><small>批次要求 ≥ {{ selected.minimumGradePoint }}</small></div>
|
||
<div class="degree-rule-note"><span>规则说明</span><b>{{ row.exceptionReason || '毕业资格与绩点均符合' }}</b></div>
|
||
<div class="degree-result-chip" :class="{ granted: row.conclusion === 'Granted' }"><el-icon><Check v-if="row.conclusion === 'Granted'" /><Close v-else /></el-icon><b>{{ conclusionLabels[row.conclusion] }}</b><small>{{ row.isOverridden ? '人工复核' : '规则计算' }}</small></div>
|
||
<el-button v-if="selected.status === 'Draft'" link type="primary" @click="openReview(row)">人工复核</el-button>
|
||
</article>
|
||
<el-empty v-if="!filteredResults.length" description="没有符合筛选条件的授予记录" />
|
||
</div>
|
||
</section>
|
||
<el-empty v-else v-loading="loading" description="尚未建立学位授予批次" />
|
||
</template>
|
||
|
||
<el-dialog v-model="createDialog" title="新建学位授予批次" width="640px">
|
||
<el-form label-position="top">
|
||
<el-form-item label="批次名称"><el-input v-model="batchForm.name" /></el-form-item>
|
||
<div class="form-grid three"><el-form-item label="毕业年份"><el-input-number v-model="batchForm.graduationYear" :min="2000" :max="2200" /></el-form-item><el-form-item label="学位名称"><el-input v-model="batchForm.degreeName" /></el-form-item><el-form-item label="最低平均绩点"><el-input-number v-model="batchForm.minimumGradePoint" :min="0" :max="5" :step="0.1" /></el-form-item></div>
|
||
<el-form-item label="说明"><el-input v-model="batchForm.notes" type="textarea" :rows="3" /></el-form-item>
|
||
</el-form>
|
||
<template #footer><el-button @click="createDialog=false">取消</el-button><el-button type="primary" @click="createBatch">建立并计算</el-button></template>
|
||
</el-dialog>
|
||
<el-dialog v-model="reviewDialog" title="人工复核学位授予结论" width="620px">
|
||
<div v-if="reviewTarget" class="review-target"><span>{{ reviewTarget.studentNumber }}</span><b>{{ reviewTarget.name }} · 平均绩点 {{ reviewTarget.averageGradePoint }}</b><p>规则结论:{{ conclusionLabels[reviewTarget.calculatedConclusion] }}</p></div>
|
||
<el-form label-position="top"><el-form-item label="复核结论"><el-radio-group v-model="reviewForm.conclusion"><el-radio-button value="Granted">建议授予</el-radio-button><el-radio-button value="NotGranted">暂不授予</el-radio-button></el-radio-group></el-form-item><el-form-item label="复核意见"><el-input v-model="reviewForm.comment" type="textarea" :rows="4" maxlength="500" show-word-limit /></el-form-item></el-form>
|
||
<template #footer><el-button @click="reviewDialog=false">取消</el-button><el-button type="primary" @click="saveReview">保存复核结论</el-button></template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|