Files
Academic-Affairs-System/web/src/views/EventAnalyticsView.vue
T
2026-08-10 17:30:52 +08:00

130 lines
5.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { DataAnalysis, Refresh } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import http, { apiErrorMessage } from '../api/http'
interface Status {
enabled: boolean
syncIntervalSeconds: number
sourceLookbackDays: number
batchSize: number
database: string
}
const status = ref<Status>()
const attendance = ref<any[]>([])
const grades = ref<any[]>([])
const audit = ref<any[]>([])
const loading = ref(false)
const range = ref<[Date, Date]>([
new Date(Date.now() - 29 * 24 * 60 * 60 * 1000),
new Date(),
])
const attendanceTotals = computed(() => attendance.value.reduce((total, row) => total + Number(row.total ?? 0), 0))
const absentTotals = computed(() => attendance.value.reduce((total, row) => total + Number(row.absent ?? 0), 0))
const auditTotals = computed(() => audit.value.reduce((total, row) => total + Number(row.total ?? 0), 0))
function dateOnly(value: Date) {
const offset = value.getTimezoneOffset() * 60_000
return new Date(value.getTime() - offset).toISOString().slice(0, 10)
}
async function load() {
loading.value = true
try {
const [statusResponse, overviewResponse] = await Promise.all([
http.get<Status>('/clickhouse-analytics/status'),
http.get('/clickhouse-analytics/overview', {
params: { from: dateOnly(range.value[0]), to: dateOnly(range.value[1]) },
}),
])
status.value = statusResponse.data
attendance.value = overviewResponse.data.attendance ?? []
grades.value = overviewResponse.data.grades ?? []
audit.value = overviewResponse.data.audit ?? []
} catch (error) {
ElMessage.error(apiErrorMessage(error) || '加载运行数据分析失败。')
} finally {
loading.value = false
}
}
onMounted(load)
</script>
<template>
<main v-loading="loading" class="event-analytics page-stack">
<section class="page-intro">
<div>
<span class="eyebrow">CLICKHOUSE READ MODEL</span>
<h2>运行数据分析</h2>
<p>考勤教学班成绩与访问审计的只读聚合不会影响教务业务写入</p>
</div>
<div class="actions">
<el-date-picker v-model="range" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" :clearable="false" />
<el-button type="primary" :icon="Refresh" @click="load">刷新</el-button>
</div>
</section>
<el-alert v-if="status && !status.enabled" type="warning" :closable="false" show-icon title="ClickHouse 分析未启用">
请在服务配置中启用 ClickHouseAnalytics 后再查看分析数据
</el-alert>
<template v-else>
<section class="metrics">
<article><el-icon><DataAnalysis /></el-icon><span>考勤记录</span><b>{{ attendanceTotals.toLocaleString() }}</b></article>
<article><el-icon><DataAnalysis /></el-icon><span>缺勤记录</span><b>{{ absentTotals.toLocaleString() }}</b></article>
<article v-if="audit.length"><el-icon><DataAnalysis /></el-icon><span>访问审计</span><b>{{ auditTotals.toLocaleString() }}</b></article>
</section>
<section class="analysis-grid">
<el-card shadow="never">
<template #header>每日考勤</template>
<el-table :data="attendance" size="small" empty-text="所选范围暂无考勤投影数据">
<el-table-column prop="attendanceDate" label="日期" min-width="110" />
<el-table-column prop="total" label="总人次" align="right" />
<el-table-column prop="present" label="到课" align="right" />
<el-table-column prop="absent" label="缺勤" align="right" />
<el-table-column prop="late" label="迟到" align="right" />
</el-table>
</el-card>
<el-card shadow="never">
<template #header>学期成绩趋势</template>
<el-table :data="grades" size="small" empty-text="暂无已计算的教学班成绩统计">
<el-table-column prop="academicTermName" label="学期" min-width="130" />
<el-table-column prop="studentCount" label="学生数" align="right" />
<el-table-column prop="averageScore" label="加权平均分" align="right" />
<el-table-column prop="passRate" label="通过率" align="right">
<template #default="{ row }">{{ (Number(row.passRate) * 100).toFixed(1) }}%</template>
</el-table-column>
</el-table>
</el-card>
</section>
<el-card v-if="audit.length" shadow="never">
<template #header>访问审计仅全校数据范围</template>
<el-table :data="audit" size="small">
<el-table-column prop="date" label="日期" min-width="110" />
<el-table-column prop="total" label="操作量" align="right" />
<el-table-column prop="failed" label="异常响应" align="right" />
</el-table>
</el-card>
</template>
</main>
</template>
<style scoped>
.event-analytics { --ink: #18324f; --line: #dce3ec; }
.page-intro { display: flex; justify-content: space-between; align-items: end; gap: 18px; }
.eyebrow { color: #3d75aa; font-size: 12px; letter-spacing: .12em; font-weight: 700; }
h2 { margin: 5px 0; color: var(--ink); } p { margin: 0; color: #6b7b8d; }
.actions { display: flex; gap: 10px; flex-wrap: wrap; }
.metrics, .analysis-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
.analysis-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.metrics article { padding: 20px; border: 1px solid var(--line); background: #fff; border-radius: 8px; display: grid; grid-template-columns: 26px 1fr; gap: 4px 9px; }
.metrics .el-icon { color: #3574a8; grid-row: span 2; font-size: 21px; } .metrics span { color: #657689; font-size: 13px; } .metrics b { color: var(--ink); font-size: 24px; }
@media (max-width: 760px) { .page-intro { align-items: start; flex-direction: column; } .metrics, .analysis-grid { grid-template-columns: 1fr; } }
</style>