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 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
@@ -59,22 +47,6 @@
-
-
-
-
-
-
-
-
-
-
{{ 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)