diff --git a/src/api/info.js b/src/api/info.js
index de7c3cc..301996c 100644
--- a/src/api/info.js
+++ b/src/api/info.js
@@ -2,6 +2,11 @@ import request from '@/utils/request'
// ==================== 教学楼信息 ====================
+// 获取教学楼列表(下拉用,返回全部)
+export function getBuildingList() {
+ return request({ url: '/building/list', method: 'get' })
+}
+
// 获取教学楼列表(分页)
export function getBuildings(params) {
return request({ url: '/building/page', method: 'get', params })
@@ -115,3 +120,47 @@ export function getTeacherDetail(id) {
export function deleteTeacher(ids) {
return request({ url: '/teacher', method: 'delete', data: ids })
}
+
+// ==================== 课程信息 ====================
+
+// 获取课程列表(分页)
+export function getCourses(params) {
+ return request({ url: '/course/page', method: 'get', params })
+}
+
+// 新增课程
+export function addCourse(data) {
+ return request({ url: '/course', method: 'post', data })
+}
+
+// 编辑课程
+export function updateCourse(data) {
+ return request({ url: `/course/${data.id}`, method: 'put', data })
+}
+
+// 删除课程(支持批量,传入 id 数组)
+export function deleteCourse(ids) {
+ return request({ url: '/course', method: 'delete', data: ids })
+}
+
+// ==================== 课程安排 ====================
+
+// 获取课程安排列表(分页)
+export function getSchedulePage(params) {
+ return request({ url: '/schedule/page', method: 'get', params })
+}
+
+// 新增课程安排
+export function addSchedule(data) {
+ return request({ url: '/schedule', method: 'post', data })
+}
+
+// 编辑课程安排
+export function updateSchedule(data) {
+ return request({ url: `/schedule/${data.id}`, method: 'put', data })
+}
+
+// 删除课程安排(支持批量,传入 id 数组)
+export function deleteSchedule(ids) {
+ return request({ url: '/schedule', method: 'delete', data: ids })
+}
diff --git a/src/components/SideMenu.vue b/src/components/SideMenu.vue
index 0936a5f..e6b42ac 100644
--- a/src/components/SideMenu.vue
+++ b/src/components/SideMenu.vue
@@ -46,10 +46,6 @@
系统设置
-
-
- 人员管理
-
设备管理
@@ -81,6 +77,14 @@
教师信息
+
+
+ 学生信息
+
+
+
+ 课程信息
+
diff --git a/src/router/index.js b/src/router/index.js
index 9b4e99f..6c02f21 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -1,4 +1,5 @@
import { createRouter, createWebHashHistory } from 'vue-router'
+import { useUserStore } from '@/stores/user'
const routes = [
{
@@ -39,12 +40,6 @@ const routes = [
component: () => import('@/views/bigscreen/index.vue'),
meta: { title: '数据展示大屏', icon: 'DataAnalysis' }
},
- {
- path: 'settings/personnel',
- name: 'Personnel',
- component: () => import('@/views/settings/personnel.vue'),
- meta: { title: '人员管理', icon: 'User' }
- },
{
path: 'settings/device',
name: 'Device',
@@ -63,6 +58,12 @@ const routes = [
component: () => import('@/views/settings/permissions.vue'),
meta: { title: '权限管理', icon: 'Lock' }
},
+ {
+ path: 'info/student',
+ name: 'InfoStudent',
+ component: () => import('@/views/settings/personnel.vue'),
+ meta: { title: '学生信息', icon: 'User' }
+ },
{
path: 'info/building',
name: 'InfoBuilding',
@@ -80,6 +81,12 @@ const routes = [
name: 'InfoTeacher',
component: () => import('@/views/info/teacher.vue'),
meta: { title: '教师信息', icon: 'UserFilled' }
+ },
+ {
+ path: 'info/course',
+ name: 'InfoCourse',
+ component: () => import('@/views/info/course.vue'),
+ meta: { title: '课程信息', icon: 'Reading' }
}
]
}
@@ -93,13 +100,8 @@ const router = createRouter({
router.beforeEach((to, from, next) => {
document.title = to.meta.title ? `${to.meta.title} - 教室智能人脸考勤系统` : '教室智能人脸考勤系统'
- // 登录鉴权:从持久化的 user store 中读取 token
- let token = ''
- try {
- const store = localStorage.getItem('user')
- if (store) token = JSON.parse(store).token || ''
- } catch { /* ignore */ }
- if (!token && to.path !== '/login') {
+ const store = useUserStore()
+ if (!store.token && to.path !== '/login') {
next('/login')
} else {
next()
diff --git a/src/utils/request.js b/src/utils/request.js
index 39a36e8..54cc33f 100644
--- a/src/utils/request.js
+++ b/src/utils/request.js
@@ -1,5 +1,6 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
+import { useUserStore } from '@/stores/user'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
@@ -10,15 +11,9 @@ const request = axios.create({
// 请求拦截器
request.interceptors.request.use(
(config) => {
- // 从 pinia-plugin-persistedstate 持久化的 user store 中读取 token
- const store = localStorage.getItem('user')
- if (store) {
- try {
- const { token } = JSON.parse(store)
- if (token) {
- config.headers.Authorization = `Bearer ${token}`
- }
- } catch { /* ignore */ }
+ const store = useUserStore()
+ if (store.token) {
+ config.headers.Authorization = `Bearer ${store.token}`
}
return config
},
@@ -41,6 +36,8 @@ request.interceptors.response.use(
switch (status) {
case 401:
ElMessage.error('登录已过期,请重新登录')
+ useUserStore().$patch({ token: '' })
+ window.location.hash = '#/login'
break
case 403:
ElMessage.error('没有访问权限')
diff --git a/src/views/history/index.vue b/src/views/history/index.vue
index b94a1cf..949ed75 100644
--- a/src/views/history/index.vue
+++ b/src/views/history/index.vue
@@ -16,15 +16,6 @@
style="width: 200px"
/>
-
-
-
-
查询
重置
@@ -44,14 +35,27 @@
@click="viewDetail(record)"
>
-
-
-
{{ record.attendanceRate }}%
-
出勤率
+
+
+
{{ record.attendanceRate }}%
+
出勤率
+ {{ record.courseName }}
{{ record.attDate }}
@@ -61,9 +65,13 @@
实到 {{ record.actualCount }}
-
+
缺勤 {{ record.absentCount }} 人
+
@@ -73,7 +81,7 @@
v-model:current-page="pagination.current"
:total="pagination.total"
:page-size="pagination.size"
- small
+ size="small"
background
layout="prev, pager, next"
@current-change="doSearch"
@@ -111,19 +119,16 @@
@@ -251,33 +250,74 @@ onMounted(() => {
}
.masonry-thumb {
- position: relative;
-}
-
-.thumb-placeholder {
- height: 140px;
- background: #f5f7fa;
+ background: #f9fafb;
display: flex;
+ flex-direction: column;
align-items: center;
justify-content: center;
- border-bottom: 3px solid #d9d9d9;
+ padding: 18px 0 12px;
}
-.attendance-stat {
- text-align: center;
+/* ========== 水球玻璃 ========== */
+.water-glass {
+ width: 110px;
+ height: 110px;
+ border-radius: 50%;
+ position: relative;
+ overflow: hidden;
+ background: rgba(255, 255, 255, 0.25);
+ border: 3px solid rgba(255, 255, 255, 0.5);
+ box-shadow:
+ 0 0 15px rgba(0, 0, 0, 0.08),
+ inset 0 2px 8px rgba(255, 255, 255, 0.4),
+ inset 0 -2px 8px rgba(0, 0, 0, 0.05);
}
-.attendance-rate {
- display: block;
- font-size: 36px;
- font-weight: 700;
- color: #262626;
- line-height: 1.2;
+.water-fill {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 0%;
+ transition: height 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+ overflow: visible;
+}
+
+.water-wave {
+ position: absolute;
+ top: -16px;
+ left: 0;
+ width: 200%;
+ height: 20px;
+ animation: wave 3s linear infinite;
+}
+
+.water-wave-back {
+ animation: wave 4s linear infinite reverse;
}
-.attendance-label {
+@keyframes wave {
+ 0% { transform: translateX(0); }
+ 100% { transform: translateX(-50%); }
+}
+
+.water-label {
font-size: 12px;
- color: #bfbfbf;
+ color: #8c8c8c;
+ margin-top: 4px;
+}
+
+.water-value {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 24px;
+ font-weight: 700;
+ color: #262626;
+ z-index: 2;
+ pointer-events: none;
+ text-shadow: 0 1px 2px rgba(255, 255, 255, 0.7);
}
.masonry-info {
@@ -287,8 +327,17 @@ onMounted(() => {
gap: 4px;
}
+.masonry-course {
+ font-size: 14px;
+ font-weight: 600;
+ color: #262626;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
.masonry-time {
- font-size: 12px;
+ font-size: 13px;
color: #525252;
display: flex;
align-items: center;
@@ -297,7 +346,7 @@ onMounted(() => {
}
.masonry-stats {
- font-size: 12px;
+ font-size: 13px;
color: #bfbfbf;
display: flex;
align-items: center;
@@ -305,10 +354,28 @@ onMounted(() => {
}
.masonry-absent {
- font-size: 12px;
+ font-size: 13px;
color: #ff4d4f;
}
+.masonry-extra {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ margin-top: 2px;
+}
+
+.masonry-tag {
+ font-size: 12px;
+ color: #8c8c8c;
+ background: #f5f5f5;
+ border-radius: 3px;
+ padding: 1px 6px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
.pagination-wrap {
display: flex;
justify-content: center;
diff --git a/src/views/info/components/ClassCourseDialog.vue b/src/views/info/components/ClassCourseDialog.vue
index 34cb09d..f643294 100644
--- a/src/views/info/components/ClassCourseDialog.vue
+++ b/src/views/info/components/ClassCourseDialog.vue
@@ -1,56 +1,132 @@
-
-
-
添加课程
+
+
+
+
+
+
+
+ {{ getCourseName(row.courseId) }}
+
+
+ {{ getTeacherName(row.teacherId) }}
+
+
+ {{ weekLabel[row.weekDay] || row.weekDay }}
+
+
+ {{ row.semester }}
+
+
+
+ {{ row.status === 1 ? '进行中' : '已结束' }}
+
+
+
+
+ 详情
+ 修改
+ 删除
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
关闭
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 启用
+ 结束
+
+
+
+
+ 取消
+ 保存
+
+
+
+
+
+
+ {{ getCourseName(detailData.courseId) }}
+ {{ getTeacherName(detailData.teacherId) }}
+ {{ weekLabel[detailData.weekDay] || detailData.weekDay }}
+ {{ detailData.semester }}
+ {{ detailData.startSection }}
+ {{ detailData.endSection }}
+ {{ detailData.startWeek }}
+ {{ detailData.endWeek }}
+ {{ getBuildingName(detailData.classroomId) }}
+ {{ getRoomName(detailData.classroomId) }}
+
+ {{ detailData.status === 1 ? '进行中' : '已结束' }}
+
+
+
+ 关闭
+
+
+
+
diff --git a/src/views/info/course.vue b/src/views/info/course.vue
new file mode 100644
index 0000000..63819b2
--- /dev/null
+++ b/src/views/info/course.vue
@@ -0,0 +1,286 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加课程
+ 批量删除
+
+
+
+
+
+
+
+
+
+
+ {{ getTeacherName(row.teacherId) }}
+
+
+
+
+ {{ row.status === 1 ? '开放中' : '已结束' }}
+
+
+
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 开放中
+ 已结束
+
+
+
+
+ 取消
+ 保存
+
+
+
+
+
+
+
+
diff --git a/src/views/settings/personnel.vue b/src/views/settings/personnel.vue
index 1ce8731..58f165c 100644
--- a/src/views/settings/personnel.vue
+++ b/src/views/settings/personnel.vue
@@ -1,7 +1,7 @@