学业预警自动化

This commit is contained in:
2026-07-25 17:28:53 +08:00 Unverified
parent d23b5156b4
commit 3d4d9953e8
7 changed files with 120 additions and 146 deletions
+32 -41
View File
@@ -16,7 +16,6 @@ const myWarnings = ref<any[]>([])
const loading = ref(false)
const detecting = ref(false)
const filterType = ref('')
const schedule = reactive({ isEnabled: false, dayOfWeek: 1, hour: 8, minute: 0, lastRunAt: null as string | null })
const typeLabels: Record<number, string> = { 1: '不及格学分', 2: '低绩点', 3: '缺勤', 4: '延毕风险' }
const statusLabels: Record<number, string> = { 1: '生效中', 2: '已确认', 3: '已处理', 4: '已忽略' }
@@ -31,12 +30,13 @@ function thresholdUnit(type: number): string {
return '门 累计不及格'
}
// Reactive rule objects — one per type, bound directly to v-model
const weekOptions = [{ value: 0, label: '每天' }, { value: 1, label: '周一' }, { value: 2, label: '周二' }, { value: 3, label: '周三' }, { value: 4, label: '周四' }, { value: 5, label: '周五' }, { value: 6, label: '周六' }, { value: 7, label: '周日' }]
const ruleRows = reactive([
{ type: 1 as number, name: '不及格学分', threshold: 2, isEnabled: true, notifyStudent: true, notifyCounselor: true, description: '' },
{ type: 2, name: '低绩点', threshold: 2, isEnabled: true, notifyStudent: true, notifyCounselor: true, description: '' },
{ type: 3, name: '缺勤', threshold: 3, isEnabled: true, notifyStudent: true, notifyCounselor: true, description: '' },
{ type: 4, name: '延毕风险', threshold: 5, isEnabled: true, notifyStudent: true, notifyCounselor: true, description: '' },
{ type: 1 as number, name: '不及格学分', threshold: 2, isEnabled: true, notifyStudent: true, notifyCounselor: true, description: '', autoCheckEnabled: false, checkDayOfWeek: 0, checkHour: 8, checkMinute: 0, lastCheckAt: null as string | null },
{ type: 2, name: '低绩点', threshold: 2, isEnabled: true, notifyStudent: true, notifyCounselor: true, description: '', autoCheckEnabled: false, checkDayOfWeek: 0, checkHour: 8, checkMinute: 0, lastCheckAt: null as string | null },
{ type: 3, name: '缺勤', threshold: 3, isEnabled: true, notifyStudent: true, notifyCounselor: true, description: '', autoCheckEnabled: false, checkDayOfWeek: 0, checkHour: 8, checkMinute: 0, lastCheckAt: null as string | null },
{ type: 4, name: '延毕风险', threshold: 5, isEnabled: true, notifyStudent: true, notifyCounselor: true, description: '', autoCheckEnabled: false, checkDayOfWeek: 0, checkHour: 8, checkMinute: 0, lastCheckAt: null as string | null },
])
async function load() {
@@ -53,9 +53,13 @@ async function load() {
row.notifyStudent = sr.notifyStudent
row.notifyCounselor = sr.notifyCounselor
row.description = sr.description || ''
row.autoCheckEnabled = sr.autoCheckEnabled
row.checkDayOfWeek = sr.checkDayOfWeek
row.checkHour = sr.checkHour
row.checkMinute = sr.checkMinute
row.lastCheckAt = sr.lastCheckAt
}
}
await loadSchedule()
}
if (isSuperAdmin.value || isCounselor.value)
records.value = (await http.get('/warnings/records', { params: { academicTermId: termId.value || undefined, type: filterType.value || undefined } })).data
@@ -63,19 +67,9 @@ async function load() {
} catch (e) { ElMessage.error(apiErrorMessage(e)) } finally { loading.value = false }
}
async function loadSchedule() {
if (!termId.value) return
try { const { data } = await http.get('/warnings/schedule', { params: { academicTermId: termId.value } }); Object.assign(schedule, data) }
catch (_) {}
}
async function saveSchedule() {
try { await http.put('/warnings/schedule', { isEnabled: schedule.isEnabled, dayOfWeek: schedule.dayOfWeek, hour: schedule.hour, minute: schedule.minute }, { params: { academicTermId: termId.value } }); ElMessage.success('已保存') }
catch (e) { ElMessage.error(apiErrorMessage(e)) }
}
async function saveRules() {
try {
const payload = ruleRows.map(r => ({ type: r.type, name: r.name, threshold: r.threshold, isEnabled: r.isEnabled, notifyStudent: r.notifyStudent, notifyCounselor: r.notifyCounselor, description: r.description }))
const payload = ruleRows.map(r => ({ type: r.type, name: r.name, threshold: r.threshold, isEnabled: r.isEnabled, notifyStudent: r.notifyStudent, notifyCounselor: r.notifyCounselor, description: r.description, autoCheckEnabled: r.autoCheckEnabled, checkDayOfWeek: r.checkDayOfWeek, checkHour: r.checkHour, checkMinute: r.checkMinute }))
await http.put('/warnings/rules', payload, { params: { academicTermId: termId.value } })
ElMessage.success('预警规则已保存')
} catch (e) { ElMessage.error(apiErrorMessage(e)) }
@@ -114,26 +108,6 @@ onMounted(async () => {
</div>
</section>
<!-- Admin: Schedule config -->
<section v-if="isSuperAdmin && termId" class="warn-schedule">
<div style="display:flex;justify-content:space-between;align-items:center">
<h3 style="margin:0">定时检测</h3>
<el-button type="primary" size="small" @click="saveSchedule">保存定时</el-button>
</div>
<div style="display:flex;align-items:center;gap:12px;margin-top:10px;flex-wrap:wrap">
<el-switch v-model="schedule.isEnabled" active-text="启用自动检测" />
<span></span>
<el-select v-model="schedule.dayOfWeek" size="small" style="width:100px" :disabled="!schedule.isEnabled">
<el-option v-for="d in 7" :key="d" :label="['','周一','周二','周三','周四','周五','周六','周日'][d]" :value="d" />
</el-select>
<el-input-number v-model="schedule.hour" :min="0" :max="23" size="small" style="width:80px" :disabled="!schedule.isEnabled" />
<span></span>
<el-input-number v-model="schedule.minute" :min="0" :max="59" size="small" style="width:80px" :disabled="!schedule.isEnabled" />
<span></span>
<span v-if="schedule.lastRunAt" style="font-size:11px;color:var(--muted);margin-left:auto">上次执行{{ new Date(schedule.lastRunAt).toLocaleString('zh-CN') }}</span>
</div>
</section>
<!-- Admin: Rule config -->
<section v-if="isSuperAdmin && termId" class="warn-rules">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
@@ -156,10 +130,28 @@ onMounted(async () => {
<el-switch v-model="row.isEnabled" size="small" active-text="启用" style="margin-right:12px" />
</template>
</el-table-column>
<el-table-column label="通知" width="200">
<el-table-column label="通知" width="180">
<template #default="{row}">
<el-checkbox v-model="row.notifyStudent" :disabled="!row.isEnabled">学生</el-checkbox>
<el-checkbox v-model="row.notifyCounselor" :disabled="!row.isEnabled" style="margin-left:12px">辅导员</el-checkbox>
<el-checkbox v-model="row.notifyCounselor" :disabled="!row.isEnabled" style="margin-left:8px">辅导员</el-checkbox>
</template>
</el-table-column>
<el-table-column label="自动检测" min-width="290">
<template #default="{row}">
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">
<el-switch v-model="row.autoCheckEnabled" size="small" :disabled="!row.isEnabled" />
<template v-if="row.autoCheckEnabled">
<span style="font-size:11px"></span>
<el-select v-model="row.checkDayOfWeek" size="small" style="width:75px">
<el-option v-for="o in weekOptions" :key="String(o.value)" :label="o.label" :value="o.value" />
</el-select>
<el-input-number v-model="row.checkHour" :min="0" :max="23" size="small" :controls="false" style="width:56px" />
<span style="font-size:11px"></span>
<el-input-number v-model="row.checkMinute" :min="0" :max="59" size="small" :controls="false" style="width:56px" />
<span style="font-size:11px"></span>
<span v-if="row.lastCheckAt" style="font-size:10px;color:var(--muted)">上次: {{ new Date(row.lastCheckAt).toLocaleString('zh-CN') }}</span>
</template>
</div>
</template>
</el-table-column>
<el-table-column label="说明"><template #default="{row}"><el-input v-model="row.description" size="small" placeholder="可选" maxlength="300" /></template></el-table-column>
@@ -198,7 +190,6 @@ onMounted(async () => {
</template>
<style scoped>
.warn-schedule { background:#f0f9eb; border:1px solid #c6e2ff; border-radius:8px; padding:14px 20px; margin-bottom:16px; }
.warn-rules { background:#fff; border:1px solid #e4e7ed; border-radius:8px; padding:16px 20px; margin-bottom:20px; }
.warn-records { background:#fff; border:1px solid #e4e7ed; border-radius:8px; padding:16px 20px; }
.warn-mine { display:grid; gap:10px; }