diff --git a/src/api/attendance.js b/src/api/attendance.js index ca482bb..719ccb7 100644 --- a/src/api/attendance.js +++ b/src/api/attendance.js @@ -1,6 +1,6 @@ import request from '@/utils/request' -/** 分页查询考勤任务 */ -export const getTaskPage = (params) => { - return request.get('/attendance/task/page', { params }) +/** 分页查询考勤记录 */ +export const getRecordPage = (params) => { + return request.get('/attendance/record/page', { params }) } diff --git a/src/api/info.js b/src/api/info.js index 2855588..e5d583b 100644 --- a/src/api/info.js +++ b/src/api/info.js @@ -159,6 +159,10 @@ export function deleteTeacher(ids) { } // ==================== 课程信息 ==================== +// 获取课程列表(下拉用,返回全部) +export function getCourseList() { + return request({ url: '/course/list', method: 'get' }) +} // 获取课程列表(分页) export function getCourses(params) { diff --git a/src/components/TopNavbar.vue b/src/components/TopNavbar.vue index a0e269f..847fb83 100644 --- a/src/components/TopNavbar.vue +++ b/src/components/TopNavbar.vue @@ -14,29 +14,17 @@ - - - -
-
-
- -
-
-
{{ item.title }}
-
{{ item.time }}
-
-
- -
-
@@ -84,22 +56,12 @@ import { useRoute, useRouter } from 'vue-router' import { useUserStore } from '@/stores/user' import { useAppStore } from '@/stores/app' import { ElMessage } from 'element-plus' -import { Search, UserFilled } from '@element-plus/icons-vue' +import { UserFilled } from '@element-plus/icons-vue' const route = useRoute() const router = useRouter() const userStore = useUserStore() const appStore = useAppStore() - -const searchKeyword = ref('') -const showNotifications = ref(false) - -const notifications = ref([ - { title: '今日考勤报告已生成', time: '10分钟前', type: 'success', icon: 'CircleCheckFilled' }, - { title: '301教室摄像头离线', time: '30分钟前', type: 'danger', icon: 'WarningFilled' }, - { title: '系统更新通知:新增行为分析功能', time: '2小时前', type: 'info', icon: 'InfoFilled' } -]) - const toggleFullscreen = () => { if (document.fullscreenElement) { document.exitFullscreen() @@ -143,11 +105,6 @@ const handleCommand = async (cmd) => { gap: 8px; } -.header-search { - width: 200px; - margin-right: 8px; -} - .search-input { :deep(.el-input__wrapper) { background: #f5f7fa; @@ -197,61 +154,4 @@ const handleCommand = async (cmd) => { transition: transform 0.2s ease; } } - -.notification-list { - max-height: 320px; - overflow-y: auto; -} - -.notification-item { - display: flex; - align-items: flex-start; - gap: 12px; - padding: 12px; - border-radius: 8px; - transition: background 0.2s ease; - - &:hover { - background: #f5f7fa; - } - - .notif-icon { - width: 32px; - height: 32px; - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - - &.success { - background: #f0fdf0; - color: #52c41a; - } - &.danger { - background: #fff1f0; - color: #f5222d; - } - &.info { - background: #e8f4ff; - color: #1890ff; - } - } - - .notif-content { - flex: 1; - - .notif-title { - font-size: 14px; - color: #262626; - line-height: 1.5; - } - - .notif-time { - font-size: 12px; - color: #bfbfbf; - margin-top: 2px; - } - } -} diff --git a/src/composables/useNameMaps.js b/src/composables/useNameMaps.js new file mode 100644 index 0000000..e11e081 --- /dev/null +++ b/src/composables/useNameMaps.js @@ -0,0 +1,44 @@ +import { ref } from 'vue' +import { getCourseList, getTeacherList, getClassList, getRoomsList } from '@/api/info' + +const courseMap = ref({}) +const teacherMap = ref({}) +const classMap = ref({}) +const roomMap = ref({}) +let loadPromise = null + +const loadNameMaps = () => { + if (!loadPromise) { + loadPromise = Promise.all([ + getCourseList(), + getTeacherList(), + getClassList(), + getRoomsList() + ]).then(([courseRes, teacherRes, classRes, roomRes]) => { + const toMap = (list, nameKey = 'name') => { + const map = {} + ;(list || []).forEach((item) => { map[item.id] = item[nameKey] }) + return map + } + courseMap.value = toMap(courseRes.data, 'courseName') + teacherMap.value = toMap(teacherRes.data, 'name') + classMap.value = toMap(classRes.data, 'className') + roomMap.value = toMap(roomRes.data, 'roomName') + }).catch((e) => { + // 加载失败保留空映射,重置 promise 以便下次可重试 + console.error('[useNameMaps] 加载名称映射失败:', e) + loadPromise = null + }) + } + return loadPromise +} + +/** 根据 ID 获取名称,兜底返回 ID 本身 */ +const courseName = (id) => courseMap.value[id] ?? id ?? '' +const teacherName = (id) => teacherMap.value[id] ?? id ?? '' +const className = (id) => classMap.value[id] ?? id ?? '' +const roomName = (id) => roomMap.value[id] ?? id ?? '' + +export function useNameMaps() { + return { loadNameMaps, courseName, teacherName, className, roomName } +} diff --git a/src/stores/app.js b/src/stores/app.js index ed49d4c..b25d3af 100644 --- a/src/stores/app.js +++ b/src/stores/app.js @@ -4,7 +4,6 @@ import { ref } from 'vue' export const useAppStore = defineStore('app', () => { const sidebarCollapsed = ref(false) const loading = ref(false) - const messageCount = ref(3) const toggleSidebar = () => { sidebarCollapsed.value = !sidebarCollapsed.value @@ -14,16 +13,10 @@ export const useAppStore = defineStore('app', () => { loading.value = val } - const clearMessage = () => { - messageCount.value = 0 - } - return { sidebarCollapsed, loading, - messageCount, toggleSidebar, - setLoading, - clearMessage + setLoading } }) diff --git a/src/utils/request.js b/src/utils/request.js index 54cc33f..5300531 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -2,6 +2,8 @@ import axios from 'axios' import { ElMessage } from 'element-plus' import { useUserStore } from '@/stores/user' +let isTokenExpired = false + const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000, @@ -35,9 +37,13 @@ request.interceptors.response.use( const { status } = error.response switch (status) { case 401: - ElMessage.error('登录已过期,请重新登录') - useUserStore().$patch({ token: '' }) - window.location.hash = '#/login' + if (!isTokenExpired) { + isTokenExpired = true + ElMessage.error('登录已过期,请重新登录') + useUserStore().$patch({ token: '' }) + window.location.hash = '#/login' + setTimeout(() => { isTokenExpired = false }, 2000) + } break case 403: ElMessage.error('没有访问权限') diff --git a/src/views/dashboard/components/AttendanceManage.vue b/src/views/dashboard/components/AttendanceManage.vue index 5a0604e..f38fee7 100644 --- a/src/views/dashboard/components/AttendanceManage.vue +++ b/src/views/dashboard/components/AttendanceManage.vue @@ -8,14 +8,6 @@ placeholder="选择日期" size="default" /> - 搜索 重置 @@ -86,25 +78,29 @@ import { ref, reactive, computed, onMounted, watch } from 'vue' import { ElMessage } from 'element-plus' import { Search } from '@element-plus/icons-vue' -import { getTaskPage } from '@/api/attendance' +import { getRecordPage } from '@/api/attendance' +import { useNameMaps } from '@/composables/useNameMaps' defineEmits(['showDetail']) +const { loadNameMaps, courseName, teacherName, className, roomName } = useNameMaps() + const loading = ref(false) -const filters = reactive({ attDate: null, keyword: '' }) +const filters = reactive({ attDate: null }) const pagination = reactive({ current: 1, pageSize: 10 }) const total = ref(0) /** 将接口记录转为表格行数据 */ const mapRecord = (item) => ({ - courseName: item.courseName, - teacher: item.teacherName, - classroom: item.classroomName, + courseName: courseName(item.courseId), + teacher: teacherName(item.teacherId), + classroom: roomName(item.classroomId), + className: className(item.classId), time: `${item.startTime || ''} 至 ${item.endTime || ''}`, total: item.totalCount, actual: item.actualCount, absentCount: item.absentCount, - absentRate: item.totalCount ? Math.round((item.absentCount / item.totalCount) * 1000) / 10 : 0 + absentRate: item.absentRate ?? 0 }) const tableData = ref([]) @@ -114,14 +110,13 @@ const fetchData = async () => { try { const params = { current: pagination.current, - size: pagination.pageSize, - keyword: filters.keyword + size: pagination.pageSize } if (filters.attDate) { const d = new Date(filters.attDate) params.attDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` } - const res = await getTaskPage(params) + const res = await getRecordPage(params) const { records, total: totalCount } = res.data if (Array.isArray(records)) { tableData.value = records.map(mapRecord) @@ -141,7 +136,10 @@ watch(() => pagination.pageSize, () => { fetchData() }) -onMounted(() => fetchData()) +onMounted(async () => { + await loadNameMaps() + fetchData() +}) const totalShould = computed(() => tableData.value.reduce((s, r) => s + r.total, 0)) const totalActual = computed(() => tableData.value.reduce((s, r) => s + r.actual, 0)) @@ -158,7 +156,7 @@ const handleSearch = () => { fetchData() } const handleReset = () => { - Object.assign(filters, { attDate: null, keyword: '' }) + Object.assign(filters, { attDate: null }) pagination.current = 1 fetchData() } diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue index 0f2d385..90b63b2 100644 --- a/src/views/dashboard/index.vue +++ b/src/views/dashboard/index.vue @@ -36,7 +36,6 @@
- 新增考勤任务 导出考勤报表 查看实时监控
@@ -95,7 +94,8 @@ import { ref, onMounted } from 'vue' import { ElMessage } from 'element-plus' import { getStats } from '@/api/dashboard' -import { getTaskPage } from '@/api/attendance' +import { getRecordPage } from '@/api/attendance' +import { useNameMaps } from '@/composables/useNameMaps' import DataCard from '@/components/DataCard.vue' import AttendanceTrendChart from './components/AttendanceTrendChart.vue' import ClassRanking from './components/ClassRanking.vue' @@ -105,16 +105,19 @@ import { Plus, Download, VideoCamera } from '@element-plus/icons-vue' /** 将接口记录转为表格行数据 */ const mapRecord = (item) => ({ - courseName: item.courseName, - teacher: item.teacherName, - classroom: item.classroomName, + courseName: courseName(item.courseId), + teacher: teacherName(item.teacherId), + classroom: roomName(item.classroomId), + className: className(item.classId), time: `${item.startTime || ''} - ${item.endTime || ''}`, total: item.totalCount, actual: item.actualCount, absentCount: item.absentCount, - absentRate: item.totalCount ? Math.round((item.absentCount / item.totalCount) * 1000) / 10 : 0 + absentRate: item.absentRate ?? 0 }) +const { loadNameMaps, courseName, teacherName, className, roomName } = useNameMaps() + // ===== 数据卡片 ===== const stats = ref({ attendanceRate: 0, classroomUsage: 0, warningCount: 0 }) @@ -132,7 +135,8 @@ const fetchStats = async () => { } } -onMounted(() => { +onMounted(async () => { + await loadNameMaps() fetchStats() fetchRecentRecords() }) @@ -145,7 +149,7 @@ const recentRecords = ref([]) const fetchRecentRecords = async () => { try { - const res = await getTaskPage({ current: 1, size: 10 }) + const res = await getRecordPage({ current: 1, size: 10 }) const { records } = res.data if (Array.isArray(records)) { recentRecords.value = records.map(mapRecord)