This commit is contained in:
2026-07-26 22:16:58 +08:00 Unverified
parent dd1b752fa3
commit bc6ffa2ada
7 changed files with 582 additions and 146 deletions
+56 -16
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { Check, DocumentChecked, Plus, Refresh, Stamp } from '@element-plus/icons-vue'
import http, { apiErrorMessage } from '../api/http'
import { useAuthStore } from '../stores/auth'
@@ -17,6 +17,10 @@ const decisionDialog = ref(false)
const decisionTarget = ref<any | null>(null)
const keyword = ref('')
const conclusionFilter = ref('')
const resultPage = ref(1)
const resultPageSize = 20
const resultRequestId = ref(0)
let filterTimer: ReturnType<typeof setTimeout> | undefined
const batchForm = reactive({
name: '2030届本科生毕业资格审核',
graduationYear: 2030,
@@ -26,12 +30,8 @@ const batchForm = reactive({
const decisionForm = reactive({ conclusion: 'Ineligible', comment: '' })
const statusLabels: Record<string, string> = { Draft: '审核中', Published: '已发布' }
const conclusionLabels: Record<string, string> = { Eligible: '符合毕业条件', Ineligible: '暂不符合' }
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)))
})
const resultItems = computed(() => selected.value?.results?.items ?? [])
const resultTotal = computed(() => selected.value?.results?.total ?? 0)
function percent(earned: number, required: number) {
if (!required) return 0
@@ -62,7 +62,28 @@ async function load() {
}
}
async function selectBatch(id: string) {
selected.value = (await http.get(`/graduation-audits/batches/${id}`)).data
resultPage.value = 1
await loadBatch(id)
}
async function loadBatch(id = selected.value?.id) {
if (!id) return
const requestId = ++resultRequestId.value
loading.value = true
try {
const response = await http.get(`/graduation-audits/batches/${id}`, {
params: {
page: resultPage.value,
pageSize: resultPageSize,
keyword: keyword.value.trim() || undefined,
conclusion: conclusionFilter.value || undefined,
},
})
if (requestId === resultRequestId.value) selected.value = response.data
} catch (error) {
if (requestId === resultRequestId.value) ElMessage.error(apiErrorMessage(error))
} finally {
if (requestId === resultRequestId.value) loading.value = false
}
}
async function createBatch() {
if (!batchForm.name.trim()) return
@@ -103,7 +124,7 @@ async function saveDecision() {
await http.put(`/graduation-audits/results/${decisionTarget.value.id}`, decisionForm)
decisionDialog.value = false
ElMessage.success('人工复核结论已保存。')
await selectBatch(selected.value.id)
await loadBatch()
} catch (error) {
ElMessage.error(apiErrorMessage(error))
}
@@ -123,6 +144,17 @@ async function publish() {
}
}
watch([keyword, conclusionFilter], () => {
resultPage.value = 1
if (filterTimer) clearTimeout(filterTimer)
filterTimer = setTimeout(() => loadBatch(), 300)
})
onUnmounted(() => {
if (filterTimer) clearTimeout(filterTimer)
resultRequestId.value++
})
onMounted(load)
</script>
@@ -187,10 +219,10 @@ onMounted(load)
</header>
<div class="graduation-summary">
<div><span>参审学生</span><b>{{ selected.results.length }}</b><small></small></div>
<div><span>符合条件</span><b>{{ selected.results.filter((x: any) => x.conclusion === 'Eligible').length }}</b><small></small></div>
<div><span>暂不符合</span><b>{{ selected.results.filter((x: any) => x.conclusion === 'Ineligible').length }}</b><small></small></div>
<div><span>人工调整</span><b>{{ selected.results.filter((x: any) => x.isOverridden).length }}</b><small></small></div>
<div><span>参审学生</span><b>{{ selected.resultCount }}</b><small></small></div>
<div><span>符合条件</span><b>{{ selected.eligibleCount }}</b><small></small></div>
<div><span>暂不符合</span><b>{{ selected.ineligibleCount }}</b><small></small></div>
<div><span>人工调整</span><b>{{ selected.overrideCount }}</b><small></small></div>
</div>
<div class="graduation-filter">
@@ -199,18 +231,26 @@ onMounted(load)
<el-option label="符合毕业条件" value="Eligible" />
<el-option label="暂不符合" value="Ineligible" />
</el-select>
<span>显示 {{ filteredResults.length }} </span>
<span> {{ resultTotal }} </span>
</div>
<div class="graduation-result-list">
<article v-for="row in filteredResults" :key="row.id">
<article v-for="row in resultItems" :key="row.id">
<div class="graduation-student"><span>{{ row.studentNumber }} · {{ row.className }}</span><h4>{{ row.name }}</h4><p>{{ row.majorName }} · {{ row.planName || '未匹配培养方案' }}</p></div>
<div class="credit-progress"><span>学分完成度</span><b>{{ row.earnedCredits }} / {{ row.requiredCredits }}</b><el-progress :percentage="percent(row.earnedCredits, row.requiredCredits)" :show-text="false" /></div>
<div class="course-clearance"><span>必修要求</span><b>{{ row.passedRequiredCourseCount }} / {{ row.requiredCourseCount }}</b><small v-if="row.missingCourseNames">{{ row.missingCourseNames }}</small><small v-else>指定必修与课程组均已完成</small></div>
<div class="graduation-conclusion" :class="{ eligible: row.conclusion === 'Eligible' }"><el-icon><Check /></el-icon><span>{{ conclusionLabels[row.conclusion] }}</span><small v-if="row.isOverridden">人工复核</small><small v-else>规则计算</small></div>
<el-button v-if="selected.status === 'Draft'" link type="primary" @click="openDecision(row)">人工复核</el-button>
</article>
<el-empty v-if="!filteredResults.length" description="没有符合筛选条件的审核结果" />
<el-empty v-if="!resultItems.length" description="没有符合筛选条件的审核结果" />
<el-pagination
v-if="resultTotal > resultPageSize"
v-model:current-page="resultPage"
:page-size="resultPageSize"
:total="resultTotal"
layout="total, prev, pager, next"
@current-change="loadBatch()"
/>
</div>
</section>
<el-empty v-else v-loading="loading" description="尚未建立毕业资格审核批次" />