优化毕业审核、学位审核

This commit is contained in:
biss committed 2026-10-05 11:07:23 +08:00
1 parent 71ee21b063
commit 5dc1ae4e3a
19 files changed
+9645 -57

No files matched your search

+23 -8
View File
@@ -1,7 +1,8 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { Check, Close, Medal, Plus, Refresh, Stamp } from '@element-plus/icons-vue'
import { Check, Close, Download, Medal, Plus, Refresh, Stamp } from '@element-plus/icons-vue'
import http, { apiErrorMessage } from '../api/http'
import { downloadApiFile } from '../api/excel'
import { useAuthStore } from '../stores/auth'
const auth = useAuthStore()
@@ -17,6 +18,10 @@ const reviewDialog = ref(false)
const reviewTarget = ref<any | null>(null)
const keyword = ref('')
const conclusionFilter = ref('')
const majorId = ref('')
const administrativeClassId = ref('')
const majors = ref<any[]>([])
const classes = ref<any[]>([])
const resultPage = ref(1)
const resultCursors = ref<string[]>([])
const nextResultCursor = ref<string>()
@@ -27,6 +32,7 @@ let filterTimer: ReturnType<typeof setTimeout> | undefined
const batchForm = reactive({
name: '2030届学士学位授予审核',
graduationYear: 2030,
enrollmentYear: 2026,
degreeName: '工学学士',
minimumGradePoint: 2,
notes: '',
@@ -52,7 +58,10 @@ async function load() {
myResult.value = (await http.get('/degree-awards/my-result')).data
return
}
batches.value = (await http.get('/degree-awards/batches')).data
const [batchResponse, majorResponse, classResponse] = await Promise.all([
http.get('/degree-awards/batches'), http.get('/base-data/majors'), http.get('/base-data/classes'),
])
batches.value = batchResponse.data; majors.value = majorResponse.data; classes.value = classResponse.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
@@ -79,6 +88,8 @@ async function loadBatch(id = selected.value?.id) {
pageSize: resultPageSize,
keyword: keyword.value.trim() || undefined,
conclusion: conclusionFilter.value || undefined,
majorId: majorId.value || undefined,
administrativeClassId: administrativeClassId.value || undefined,
},
})
if (requestId === resultRequestId.value) selected.value = response.data
@@ -92,6 +103,8 @@ async function loadBatch(id = selected.value?.id) {
if (requestId === resultRequestId.value) loading.value = false
}
}
async function exportRows() { try { await downloadApiFile(`/degree-awards/batches/${selected.value.id}/export.xlsx`, '学位授予审核.xlsx', { params: { keyword: keyword.value.trim() || undefined, conclusion: conclusionFilter.value || undefined, majorId: majorId.value || undefined, administrativeClassId: administrativeClassId.value || undefined } }); ElMessage.success('已导出当前筛选范围的全部记录。') } catch (error) { ElMessage.error(apiErrorMessage(error)) } }
async function removeBatch() { try { await ElMessageBox.confirm(`删除草稿批次“${selected.value.name}”及其审核结果?此操作不可恢复。`, '删除授予批次', { type: 'warning', confirmButtonText: '删除' }); await http.delete(`/degree-awards/batches/${selected.value.id}`); ElMessage.success('草稿批次已删除。'); await load() } catch (error: any) { if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error)) } }
async function nextResultPage() { if (nextResultCursor.value && !loading.value) { resultCursors.value.push(nextResultCursor.value); resultPage.value++; await loadBatch() } }
async function previousResultPage() { if (resultCursors.value.length && !loading.value) { resultCursors.value.pop(); resultPage.value--; await loadBatch() } }
@@ -100,7 +113,8 @@ async function createBatch() {
const response = await http.post('/degree-awards/batches', batchForm)
createDialog.value = false
await http.post(`/degree-awards/batches/${response.data.id}/calculate`)
ElMessage.success('授予批次已建立,并完成首次规则计算。')
ElMessage.success('授予批次已建立,规则计算已进入后台队列。')
ElMessage.success('已提交后台重新计算,请刷新查看结果。')
await load()
} catch (error) {
ElMessage.error(apiErrorMessage(error))
@@ -150,7 +164,7 @@ async function publish() {
}
}
watch([keyword, conclusionFilter], () => {
watch([keyword, conclusionFilter, majorId, administrativeClassId], () => {
resultPage.value = 1
resultCursors.value = []
nextResultCursor.value = undefined
@@ -201,9 +215,8 @@ onMounted(load)
</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>
<div><span>CONFERRAL REGISTER</span><h3>{{ selected.name }}</h3><p>{{ selected.degreeName }} · 最低平均绩点 {{ selected.minimumGradePoint }} · {{ selected.calculationStatus === 'Queued' ? '后台计算排队中' : selected.calculationStatus === 'Running' ? '后台计算进行中' : `计算于 ${dateText(selected.calculatedAt)}` }}</p></div>
<div><el-button :icon="Download" @click="exportRows">导出</el-button><template v-if="selected.status === 'Draft' && isPublisher"><el-button :icon="Refresh" @click="calculate">重新计算</el-button><el-button type="danger" plain @click="removeBatch">删除批次</el-button><el-button type="primary" :icon="Stamp" @click="publish">发布结果</el-button></template><el-tag v-else type="success" effect="plain">授予结果已锁定</el-tag></div>
</header>
<div class="degree-summary">
<div><span>参审人数</span><b>{{ selected.resultCount }}</b></div>
@@ -214,6 +227,8 @@ onMounted(load)
<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>
<el-select v-model="majorId" clearable filterable placeholder="全部专业"><el-option v-for="item in majors" :key="item.id" :label="item.name" :value="item.id" /></el-select>
<el-select v-model="administrativeClassId" clearable filterable placeholder="全部班级"><el-option v-for="item in classes.filter((x: any) => !majorId || x.majorId === majorId)" :key="item.id" :label="`${item.majorName} · ${item.name}`" :value="item.id" /></el-select>
<span>共 {{ resultTotal }} 条</span>
</div>
<div class="degree-result-list">
@@ -234,7 +249,7 @@ onMounted(load)
<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>
<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-number v-model="batchForm.enrollmentYear" :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>
+26 -8
View File
@@ -1,7 +1,8 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { Check, DocumentChecked, Plus, Refresh, Stamp } from '@element-plus/icons-vue'
import { Check, DocumentChecked, Download, Plus, Refresh, Stamp } from '@element-plus/icons-vue'
import http, { apiErrorMessage } from '../api/http'
import { downloadApiFile } from '../api/excel'
import { useAuthStore } from '../stores/auth'
const auth = useAuthStore()
@@ -17,6 +18,10 @@ const decisionDialog = ref(false)
const decisionTarget = ref<any | null>(null)
const keyword = ref('')
const conclusionFilter = ref('')
const majorId = ref('')
const administrativeClassId = ref('')
const majors = ref<any[]>([])
const classes = ref<any[]>([])
const resultPage = ref(1)
const resultPageSize = 20
const resultRequestId = ref(0)
@@ -51,7 +56,10 @@ async function load() {
myResult.value = (await http.get('/graduation-audits/my-result')).data
return
}
batches.value = (await http.get('/graduation-audits/batches')).data
const [batchResponse, majorResponse, classResponse] = await Promise.all([
http.get('/graduation-audits/batches'), http.get('/base-data/majors'), http.get('/base-data/classes'),
])
batches.value = batchResponse.data; majors.value = majorResponse.data; classes.value = classResponse.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
@@ -76,6 +84,8 @@ async function loadBatch(id = selected.value?.id) {
pageSize: resultPageSize,
keyword: keyword.value.trim() || undefined,
conclusion: conclusionFilter.value || undefined,
majorId: majorId.value || undefined,
administrativeClassId: administrativeClassId.value || undefined,
},
})
if (requestId === resultRequestId.value) selected.value = response.data
@@ -85,13 +95,15 @@ async function loadBatch(id = selected.value?.id) {
if (requestId === resultRequestId.value) loading.value = false
}
}
async function exportRows() { try { await downloadApiFile(`/graduation-audits/batches/${selected.value.id}/export.xlsx`, '毕业资格审核.xlsx', { params: { keyword: keyword.value.trim() || undefined, conclusion: conclusionFilter.value || undefined, majorId: majorId.value || undefined, administrativeClassId: administrativeClassId.value || undefined } }); ElMessage.success('已导出当前筛选范围的全部记录。') } catch (error) { ElMessage.error(apiErrorMessage(error)) } }
async function removeBatch() { try { await ElMessageBox.confirm(`删除草稿批次“${selected.value.name}”及其审核结果?此操作不可恢复。`, '删除审核批次', { type: 'warning', confirmButtonText: '删除' }); await http.delete(`/graduation-audits/batches/${selected.value.id}`); ElMessage.success('草稿批次已删除。'); await load() } catch (error: any) { if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error)) } }
async function createBatch() {
if (!batchForm.name.trim()) return
try {
const response = await http.post('/graduation-audits/batches', batchForm)
createDialog.value = false
await http.post(`/graduation-audits/batches/${response.data.id}/calculate`)
ElMessage.success('审核批次已建立,并完成首次资格计算。')
ElMessage.success('审核批次已建立,资格计算已进入后台队列。')
await load()
} catch (error) {
ElMessage.error(apiErrorMessage(error))
@@ -103,7 +115,7 @@ async function calculate() {
type: 'warning', confirmButtonText: '重新计算',
})
await http.post(`/graduation-audits/batches/${selected.value.id}/calculate`)
ElMessage.success('已按最新培养方案与发布成绩重新计算。')
ElMessage.success('已提交后台重新计算,请刷新查看结果。')
await load()
} catch (error: any) {
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
@@ -144,7 +156,7 @@ async function publish() {
}
}
watch([keyword, conclusionFilter], () => {
watch([keyword, conclusionFilter, majorId, administrativeClassId], () => {
resultPage.value = 1
if (filterTimer) clearTimeout(filterTimer)
filterTimer = setTimeout(() => loadBatch(), 300)
@@ -210,12 +222,16 @@ onMounted(load)
<section v-if="selected" class="graduation-workbench" v-loading="loading">
<header>
<div><span>AUDIT REGISTER</span><h3>{{ selected.name }}</h3><p>计算时间 {{ dateText(selected.calculatedAt) }} · 规则基于已发布培养方案与成绩</p></div>
<div v-if="selected.status === 'Draft' && isPublisher">
<div><span>AUDIT REGISTER</span><h3>{{ selected.name }}</h3><p>{{ selected.calculationStatus === 'Queued' ? '后台计算排队中' : selected.calculationStatus === 'Running' ? '后台计算进行中' : `计算时间 ${dateText(selected.calculatedAt)}` }} · 规则基于已发布培养方案与成绩</p></div>
<div>
<el-button :icon="Download" @click="exportRows">导出</el-button>
<template v-if="selected.status === 'Draft' && isPublisher">
<el-button :icon="Refresh" @click="calculate">重新计算</el-button>
<el-button type="danger" plain @click="removeBatch">删除批次</el-button>
<el-button type="primary" :icon="Stamp" @click="publish">发布结果</el-button>
</template>
<el-tag v-else type="success" effect="plain">结果已锁定</el-tag>
</div>
<el-tag v-else type="success" effect="plain">结果已锁定</el-tag>
</header>
<div class="graduation-summary">
@@ -231,6 +247,8 @@ onMounted(load)
<el-option label="符合毕业条件" value="Eligible" />
<el-option label="暂不符合" value="Ineligible" />
</el-select>
<el-select v-model="majorId" clearable filterable placeholder="全部专业"><el-option v-for="item in majors" :key="item.id" :label="item.name" :value="item.id" /></el-select>
<el-select v-model="administrativeClassId" clearable filterable placeholder="全部班级"><el-option v-for="item in classes.filter((x: any) => !majorId || x.majorId === majorId)" :key="item.id" :label="`${item.majorName} · ${item.name}`" :value="item.id" /></el-select>
<span>共 {{ resultTotal }} 条</span>
</div>