Implement comprehensive academic workflow updates

This commit is contained in:
biss committed 2026-08-18 11:49:25 +08:00
1 parent 4f031d4b60
commit 868367855d
11 files changed
+8366 -8

No files matched your search

+2
View File
@@ -54,6 +54,8 @@ declare module 'vue' {
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTimeline: typeof import('element-plus/es')['ElTimeline']
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
ElTimeSelect: typeof import('element-plus/es')['ElTimeSelect']
ElUpload: typeof import('element-plus/es')['ElUpload']
NativePlaceCascader: typeof import('./components/NativePlaceCascader.vue')['default']
+63 -2
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { Check, Refresh, Search } from '@element-plus/icons-vue'
import { Check, EditPen, Refresh, Search } from '@element-plus/icons-vue'
import http, { apiErrorMessage } from '../api/http'
import { useAuthStore } from '../stores/auth'
import { academicTermLabel, academicTermOptionClass, defaultAcademicTermId } from '../utils/academicTerms'
@@ -14,12 +14,17 @@ const terms = ref<any[]>([])
const termId = ref('')
const records = ref<any[]>([])
const myWarnings = ref<any[]>([])
const myRiskCases = ref<any[]>([])
const loading = ref(false)
const detecting = ref(false)
const filterType = ref('')
const recordsPage = ref(1)
const recordsTotal = ref(0)
const recordsPageSize = 20
const riskCases = ref<any[]>([])
const riskDialogVisible = ref(false)
const selectedRiskCase = ref<any>(null)
const riskEntry = reactive({ type: 1, content: '', occurredAt: '' })
const typeLabels: Record<number, string> = { 1: '不及格学分', 2: '低绩点', 3: '缺勤', 4: '延毕风险' }
const statusLabels: Record<number, string> = { 1: '生效中', 2: '已确认', 3: '已处理', 4: '已忽略' }
@@ -82,8 +87,12 @@ async function load(resetRecordsPage = false) {
recordsPage.value--
await load()
}
riskCases.value = (await http.get('/warnings/risk-cases', { params: { mineOnly: isCounselor.value } })).data
}
if (isStudent.value) {
myWarnings.value = (await http.get('/warnings/my-warnings')).data
myRiskCases.value = (await http.get('/warnings/my-risk-cases')).data
}
if (isStudent.value) myWarnings.value = (await http.get('/warnings/my-warnings')).data
} catch (e) { ElMessage.error(apiErrorMessage(e)) } finally { loading.value = false }
}
@@ -113,6 +122,25 @@ async function acknowledge(w: any) {
} catch (e: any) { if (e !== 'cancel' && e !== 'close') ElMessage.error(apiErrorMessage(e)) }
}
async function openRiskCase(row: any) {
try { selectedRiskCase.value = (await http.get(`/warnings/risk-cases/${row.id}`)).data; riskEntry.type = 1; riskEntry.content = ''; riskEntry.occurredAt = ''; riskDialogVisible.value = true }
catch (e) { ElMessage.error(apiErrorMessage(e)) }
}
async function saveRiskCase() {
try {
const c = selectedRiskCase.value
await http.put(`/warnings/risk-cases/${c.id}`, { status: c.status, followUpDueDate: c.followUpDueDate, reviewDate: c.reviewDate || null })
ElMessage.success('跟进任务已更新'); await load()
} catch (e) { ElMessage.error(apiErrorMessage(e)) }
}
async function addRiskEntry() {
if (!riskEntry.content.trim()) { ElMessage.warning('请填写记录内容'); return }
try {
await http.post(`/warnings/risk-cases/${selectedRiskCase.value.id}/entries`, { type: riskEntry.type, content: riskEntry.content, occurredAt: riskEntry.occurredAt || null })
await openRiskCase(selectedRiskCase.value); await load(); ElMessage.success('处置记录已保存')
} catch (e) { ElMessage.error(apiErrorMessage(e)) }
}
onMounted(async () => {
try { terms.value = (await http.get('/base-data/terms')).data; termId.value = defaultAcademicTermId(terms.value) ?? ''; await load() }
catch (e) { ElMessage.error(apiErrorMessage(e)) }
@@ -204,6 +232,19 @@ onMounted(async () => {
<el-empty v-if="!records.length" description="暂无预警记录,请先执行检测。" />
</section>
<section v-if="isWarningManager || isCounselor" class="risk-tasks">
<div class="risk-head"><div><h3>风险处置跟进</h3><p>谈话和帮扶记录为敏感信息,仅被指派辅导员和学业管理人员可见。</p></div><el-tag type="danger">待办 {{ riskCases.filter(x => x.status !== 4).length }}</el-tag></div>
<el-table :data="riskCases" size="small">
<el-table-column label="学生" min-width="140"><template #default="{row}"><b>{{ row.studentName }}</b><small>{{ row.studentNumber }}</small></template></el-table-column>
<el-table-column label="预警类型" width="110"><template #default="{row}">{{ typeLabels[row.warningType] }}</template></el-table-column>
<el-table-column label="跟进截止" width="120"><template #default="{row}">{{ row.followUpDueDate }}</template></el-table-column>
<el-table-column label="复核日期" width="120"><template #default="{row}">{{ row.reviewDate || '待设置' }}</template></el-table-column>
<el-table-column label="状态" width="110"><template #default="{row}">{{ ['','待跟进','跟进中','待复核','已结案'][row.status] }}</template></el-table-column>
<el-table-column label="操作" width="110"><template #default="{row}"><el-button size="small" type="primary" :icon="EditPen" @click="openRiskCase(row)">处置</el-button></template></el-table-column>
</el-table>
<el-empty v-if="!riskCases.length" description="暂无需要跟进的风险任务" :image-size="70" />
</section>
<!-- Student -->
<section v-if="isStudent" v-loading="loading" class="warn-mine">
<article v-for="w in myWarnings" :key="w.id" class="warn-card" :class="{ active: w.status===1 }">
@@ -215,8 +256,25 @@ onMounted(async () => {
</div>
</article>
<el-empty v-if="!myWarnings.length" description="暂无学业预警" />
<section v-if="myRiskCases.length" class="my-risk-progress">
<div><h3>我的帮扶与复核进度</h3><p>这里展示辅导员针对本人的谈话、帮扶和复核记录。</p></div>
<article v-for="riskCase in myRiskCases" :key="riskCase.id" class="my-risk-case">
<div class="risk-case-dates"><span>跟进截止:{{ riskCase.followUpDueDate }}</span><span>复核日期:{{ riskCase.reviewDate || '待安排' }}</span></div>
<el-timeline><el-timeline-item v-for="entry in riskCase.entries" :key="entry.id" :timestamp="new Date(entry.occurredAt).toLocaleString('zh-CN')"><b>{{ ['','谈话记录','帮扶措施','复核记录'][entry.type] }}</b><p>{{ entry.content }}</p><small>记录人:{{ entry.recordedByName }}</small></el-timeline-item></el-timeline>
<el-empty v-if="!riskCase.entries.length" description="辅导员暂未填写处置记录" :image-size="55" />
</article>
</section>
</section>
</div>
<el-dialog v-model="riskDialogVisible" title="学业风险处置(敏感)" width="680px" destroy-on-close>
<template v-if="selectedRiskCase">
<p class="risk-warning"><b>{{ selectedRiskCase.studentName }}</b>({{ selectedRiskCase.studentNumber }}) · {{ selectedRiskCase.warningDetail }}</p>
<el-form label-width="90px" class="risk-form"><el-form-item label="跟进状态"><el-select v-model="selectedRiskCase.status"><el-option :value="1" label="待跟进"/><el-option :value="2" label="跟进中"/><el-option :value="3" label="待复核"/><el-option :value="4" label="已结案"/></el-select></el-form-item><el-form-item label="跟进截止"><el-date-picker v-model="selectedRiskCase.followUpDueDate" type="date" value-format="YYYY-MM-DD" /></el-form-item><el-form-item label="复核日期"><el-date-picker v-model="selectedRiskCase.reviewDate" type="date" clearable value-format="YYYY-MM-DD" /></el-form-item><el-form-item><el-button type="primary" @click="saveRiskCase">保存任务</el-button></el-form-item></el-form>
<el-divider>新增处置记录</el-divider>
<el-form label-width="90px"><el-form-item label="记录类别"><el-radio-group v-model="riskEntry.type"><el-radio :value="1">谈话记录</el-radio><el-radio :value="2">帮扶措施</el-radio><el-radio :value="3">复核记录</el-radio></el-radio-group></el-form-item><el-form-item label="发生时间"><el-date-picker v-model="riskEntry.occurredAt" type="datetime" clearable value-format="YYYY-MM-DDTHH:mm:ss" /></el-form-item><el-form-item label="内容"><el-input v-model="riskEntry.content" type="textarea" maxlength="2000" show-word-limit :rows="4" placeholder="记录谈话要点、帮扶措施或复核结论" /></el-form-item><el-form-item><el-button type="primary" @click="addRiskEntry">保存记录</el-button></el-form-item></el-form>
<el-divider>处置历史</el-divider><el-timeline><el-timeline-item v-for="entry in selectedRiskCase.entries" :key="entry.id" :timestamp="new Date(entry.occurredAt).toLocaleString('zh-CN')"><b>{{ ['','谈话记录','帮扶措施','复核记录'][entry.type] }}</b> · {{ entry.recordedByName }}<p>{{ entry.content }}</p></el-timeline-item></el-timeline>
</template>
</el-dialog>
</template>
<style scoped>
@@ -229,4 +287,7 @@ onMounted(async () => {
.warn-body b { font-size:14px; display:block; margin-bottom:4px; }
.warn-body small { font-size:11px; color:var(--muted); }
.warn-status { display:flex; align-items:center; gap:8px; }
.risk-tasks { background:#fff; border:1px solid #e4e7ed; border-radius:8px; padding:16px 20px; margin-top:20px; }
.risk-head { display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:12px; }.risk-head h3 { margin:0 0 4px; }.risk-head p { margin:0; font-size:12px; color:var(--muted); }.risk-tasks small { display:block; color:var(--muted); font-size:11px; }.risk-warning { background:#fff7e6; padding:10px 12px; border-radius:4px; }.risk-form { max-width:420px; }
.my-risk-progress { background:#fff; border:1px solid #e4e7ed; border-radius:8px; padding:16px 20px; }.my-risk-progress h3 { margin:0 0 4px; }.my-risk-progress > div > p { margin:0 0 14px; color:var(--muted); font-size:12px; }.my-risk-case { border-top:1px solid #f0f1f3; padding-top:14px; margin-top:14px; }.my-risk-case:first-of-type { border-top:0; padding-top:0; margin-top:0; }.risk-case-dates { display:flex; flex-wrap:wrap; gap:16px; margin-bottom:12px; font-size:12px; color:var(--muted); }.my-risk-case p { margin:4px 0; white-space:pre-wrap; }.my-risk-case small { color:var(--muted); }
</style>