From f23d9d98d35dd93bc7b117fd79ad2c3df285c634 Mon Sep 17 00:00:00 2001 From: zhoulexin Date: Fri, 5 Jun 2026 17:35:43 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E8=B0=83=E6=95=B4=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E8=AE=B0=E5=BD=95=E6=9F=A5=E8=AF=A2=E3=80=81=E5=AD=A6?= =?UTF-8?q?=E7=94=9F=E4=BF=A1=E6=81=AF=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/info.js | 49 ++ src/components/SideMenu.vue | 12 +- src/router/index.js | 28 +- src/utils/request.js | 15 +- src/views/history/index.vue | 175 +++++-- .../info/components/ClassCourseDialog.vue | 467 +++++++++++++++--- src/views/info/course.vue | 286 +++++++++++ src/views/settings/personnel.vue | 2 +- 8 files changed, 875 insertions(+), 159 deletions(-) create mode 100644 src/views/info/course.vue 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 }} 人 +
+ {{ record.className }} + {{ record.classroomName }} +
@@ -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 @@ + + 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 @@ + + + + + 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 @@