优化
This commit is contained in:
@@ -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, Close, Medal, Plus, Refresh, Stamp } from '@element-plus/icons-vue'
|
||||
import http, { apiErrorMessage } from '../api/http'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
@@ -17,6 +17,10 @@ const reviewDialog = ref(false)
|
||||
const reviewTarget = 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,
|
||||
@@ -29,12 +33,8 @@ 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)))
|
||||
})
|
||||
const resultItems = computed(() => selected.value?.results?.items ?? [])
|
||||
const resultTotal = computed(() => selected.value?.results?.total ?? 0)
|
||||
function dateText(value?: string) {
|
||||
if (!value) return '—'
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
@@ -60,7 +60,28 @@ async function load() {
|
||||
}
|
||||
}
|
||||
async function selectBatch(id: string) {
|
||||
selected.value = (await http.get(`/degree-awards/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(`/degree-awards/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() {
|
||||
try {
|
||||
@@ -99,7 +120,7 @@ async function saveReview() {
|
||||
await http.put(`/degree-awards/results/${reviewTarget.value.id}`, reviewForm)
|
||||
reviewDialog.value = false
|
||||
ElMessage.success('学位复核结论已保存。')
|
||||
await selectBatch(selected.value.id)
|
||||
await loadBatch()
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
@@ -117,6 +138,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>
|
||||
|
||||
@@ -160,25 +192,33 @@ onMounted(load)
|
||||
<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><span>参审人数</span><b>{{ selected.resultCount }}</b></div>
|
||||
<div><span>建议授予</span><b>{{ selected.grantedCount }}</b></div>
|
||||
<div><span>暂不授予</span><b>{{ selected.notGrantedCount }}</b></div>
|
||||
<div><span>人工调整</span><b>{{ selected.overrideCount }}</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>
|
||||
<span>共 {{ resultTotal }} 条</span>
|
||||
</div>
|
||||
<div class="degree-result-list">
|
||||
<article v-for="row in filteredResults" :key="row.id">
|
||||
<article v-for="row in resultItems" :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="没有符合筛选条件的授予记录" />
|
||||
<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="尚未建立学位授予批次" />
|
||||
|
||||
@@ -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="尚未建立毕业资格审核批次" />
|
||||
|
||||
@@ -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, CircleCheck, Plus, Refresh, Stamp } from '@element-plus/icons-vue'
|
||||
import http, { apiErrorMessage } from '../api/http'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
@@ -16,6 +16,12 @@ const createDialog = ref(false)
|
||||
const recordDialog = ref(false)
|
||||
const recordTarget = ref<any | null>(null)
|
||||
const recordForm = reactive({ status: 'Completed', notes: '' })
|
||||
const keyword = ref('')
|
||||
const progressFilter = 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,
|
||||
@@ -34,23 +40,8 @@ const statusLabels: Record<string, string> = {
|
||||
const roleLabels: Record<string, string> = {
|
||||
AcademicAdmin: '校级办理', CollegeAdmin: '学院办理', Counselor: '辅导员办理',
|
||||
}
|
||||
const studentLedgers = computed(() => {
|
||||
const map = new Map<string, any>()
|
||||
for (const record of selected.value?.records ?? []) {
|
||||
if (!map.has(record.studentId)) {
|
||||
map.set(record.studentId, {
|
||||
studentId: record.studentId,
|
||||
studentNumber: record.studentNumber,
|
||||
name: record.name,
|
||||
className: record.className,
|
||||
majorName: record.majorName,
|
||||
records: [],
|
||||
})
|
||||
}
|
||||
map.get(record.studentId).records.push(record)
|
||||
}
|
||||
return [...map.values()]
|
||||
})
|
||||
const studentItems = computed(() => selected.value?.students?.items ?? [])
|
||||
const studentTotal = computed(() => selected.value?.students?.total ?? 0)
|
||||
function dateText(value?: string) {
|
||||
if (!value) return '—'
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
@@ -88,7 +79,28 @@ async function load() {
|
||||
}
|
||||
}
|
||||
async function selectBatch(id: string) {
|
||||
selected.value = (await http.get(`/graduation-clearance/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-clearance/batches/${id}`, {
|
||||
params: {
|
||||
page: resultPage.value,
|
||||
pageSize: resultPageSize,
|
||||
keyword: keyword.value.trim() || undefined,
|
||||
progress: progressFilter.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
|
||||
}
|
||||
}
|
||||
function addItem() {
|
||||
batchForm.items.push({
|
||||
@@ -135,7 +147,7 @@ async function saveRecord() {
|
||||
await http.put(`/graduation-clearance/records/${recordTarget.value.id}`, recordForm)
|
||||
recordDialog.value = false
|
||||
ElMessage.success('离校事项办理状态已更新。')
|
||||
await selectBatch(selected.value.id)
|
||||
await loadBatch()
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
@@ -153,6 +165,17 @@ async function closeBatch() {
|
||||
}
|
||||
}
|
||||
|
||||
watch([keyword, progressFilter], () => {
|
||||
resultPage.value = 1
|
||||
if (filterTimer) clearTimeout(filterTimer)
|
||||
filterTimer = setTimeout(() => loadBatch(), 300)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (filterTimer) clearTimeout(filterTimer)
|
||||
resultRequestId.value++
|
||||
})
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
@@ -195,15 +218,23 @@ onMounted(load)
|
||||
|
||||
<section v-if="selected" class="clearance-workbench" v-loading="loading">
|
||||
<header>
|
||||
<div><span>CLEARANCE LEDGER</span><h3>{{ selected.name }}</h3><p>{{ selected.items.length }} 项离校手续 · {{ studentLedgers.length }} 名毕业生</p></div>
|
||||
<div><span>CLEARANCE LEDGER</span><h3>{{ selected.name }}</h3><p>{{ selected.items.length }} 项离校手续 · {{ selected.studentCount }} 名毕业生</p></div>
|
||||
<div v-if="selected.status === 'Open' && isManager"><el-button :icon="Refresh" @click="generate">补齐名单</el-button><el-button type="primary" :icon="Stamp" @click="closeBatch">关闭批次</el-button></div>
|
||||
<el-tag v-else type="success" effect="plain">批次已锁定</el-tag>
|
||||
</header>
|
||||
<div class="clearance-item-legend">
|
||||
<div v-for="item in selected.items" :key="item.id"><span>{{ item.code }}</span><b>{{ item.name }}</b><small>{{ item.responsibleUnit }} · {{ roleLabels[item.responsibleRole] }}</small></div>
|
||||
</div>
|
||||
<div class="graduation-filter">
|
||||
<el-input v-model="keyword" clearable placeholder="搜索学号、姓名、专业或班级" />
|
||||
<el-select v-model="progressFilter" clearable placeholder="全部进度">
|
||||
<el-option label="有必办待办" value="Pending" />
|
||||
<el-option label="必办已完成" value="Completed" />
|
||||
</el-select>
|
||||
<span>共 {{ studentTotal }} 名学生</span>
|
||||
</div>
|
||||
<div class="clearance-ledgers">
|
||||
<article v-for="student in studentLedgers" :key="student.studentId">
|
||||
<article v-for="student in studentItems" :key="student.studentId">
|
||||
<header><div><span>{{ student.studentNumber }} · {{ student.className }}</span><h4>{{ student.name }}</h4><p>{{ student.majorName }}</p></div><b>{{ completedCount(student.records) }}/{{ student.records.length }}</b></header>
|
||||
<div class="clearance-record-grid">
|
||||
<div v-for="record in student.records" :key="record.id" :class="{ done: record.status !== 'Pending' }">
|
||||
@@ -218,7 +249,15 @@ onMounted(load)
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<el-empty v-if="!studentLedgers.length" description="尚未生成毕业生离校办理记录" />
|
||||
<el-empty v-if="!studentItems.length" description="没有符合筛选条件的离校办理记录" />
|
||||
<el-pagination
|
||||
v-if="studentTotal > resultPageSize"
|
||||
v-model:current-page="resultPage"
|
||||
:page-size="resultPageSize"
|
||||
:total="studentTotal"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="loadBatch()"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<el-empty v-else v-loading="loading" description="尚未建立毕业离校批次" />
|
||||
|
||||
Reference in New Issue
Block a user