diff --git a/package-lock.json b/package-lock.json index 1839c25..36edace 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ "pinia": "^2.1.7", "pinia-plugin-persistedstate": "^3.2.3", "vue": "^3.4.21", - "vue-router": "^4.3.0" + "vue-router": "^4.3.0", + "xlsx": "^0.18.5" }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.4", @@ -1367,6 +1368,15 @@ "vue": "^3.5.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", @@ -1416,6 +1426,19 @@ "node": ">= 0.4" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", @@ -1432,6 +1455,15 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1444,6 +1476,18 @@ "node": ">= 0.8" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", @@ -1681,6 +1725,15 @@ "node": ">= 6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", @@ -2125,6 +2178,18 @@ "node": ">=0.10.0" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tslib": { "version": "2.3.0", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", @@ -2261,6 +2326,45 @@ "vue": "^3.5.0" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/zrender": { "version": "5.6.1", "resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.6.1.tgz", diff --git a/package.json b/package.json index 2d8cd1f..7a69564 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "pinia": "^2.1.7", "pinia-plugin-persistedstate": "^3.2.3", "vue": "^3.4.21", - "vue-router": "^4.3.0" + "vue-router": "^4.3.0", + "xlsx": "^0.18.5" }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.4", diff --git a/src/api/attendance.js b/src/api/attendance.js index 719ccb7..56bbdfb 100644 --- a/src/api/attendance.js +++ b/src/api/attendance.js @@ -2,5 +2,10 @@ import request from '@/utils/request' /** 分页查询考勤记录 */ export const getRecordPage = (params) => { - return request.get('/attendance/record/page', { params }) + return request.get('/attendance/task/page', { params }) +} + +/** 分页查询考勤详情 */ +export const getDetailPage = (params) => { + return request.get('/attendance/detail/page', { params }) } diff --git a/src/api/dashboard.js b/src/api/dashboard.js index 908e50a..6ecb8e4 100644 --- a/src/api/dashboard.js +++ b/src/api/dashboard.js @@ -11,6 +11,6 @@ export const getTrend = () => { } /** 获取各班级出勤率排名 */ -export const getRanking = () => { - return request.get('/dashboard/ranking') +export const getRanking = (params) => { + return request.get('/dashboard/ranking', { params }) } diff --git a/src/assets/avatar.png b/src/assets/avatar.png new file mode 100644 index 0000000..0c3ff57 Binary files /dev/null and b/src/assets/avatar.png differ diff --git a/src/styles/variables.scss b/src/styles/variables.scss index 8a29544..32fcac1 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -58,7 +58,7 @@ $font-family-mono: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace; // 布局 $sidebar-width: 240px; $sidebar-collapsed-width: 64px; -$navbar-height: 56px; +$navbar-height: 90px; // 过渡 $transition-fast: 0.15s ease; diff --git a/src/utils/fileHttp.js b/src/utils/fileHttp.js new file mode 100644 index 0000000..4c302a4 --- /dev/null +++ b/src/utils/fileHttp.js @@ -0,0 +1,2 @@ +const fileHttp = 'https://shipllm.ngsk.tech:7001/storage' +export default fileHttp \ No newline at end of file diff --git a/src/utils/request.js b/src/utils/request.js index 5300531..86a4082 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -17,6 +17,10 @@ request.interceptors.request.use( if (store.token) { config.headers.Authorization = `Bearer ${store.token}` } + // FormData:让浏览器自动设置带 boundary 的 Content-Type + if (config.data instanceof FormData) { + delete config.headers['Content-Type'] + } return config }, (error) => Promise.reject(error) diff --git a/src/views/behavior/index.vue b/src/views/behavior/index.vue index c65831d..29dd77a 100644 --- a/src/views/behavior/index.vue +++ b/src/views/behavior/index.vue @@ -20,7 +20,7 @@

行为分布占比

- 今日 +
diff --git a/src/views/dashboard/components/AttendanceDetail.vue b/src/views/dashboard/components/AttendanceDetail.vue index 5994fc5..02e4bcc 100644 --- a/src/views/dashboard/components/AttendanceDetail.vue +++ b/src/views/dashboard/components/AttendanceDetail.vue @@ -3,7 +3,7 @@ :model-value="props.modelValue" @update:model-value="emit('update:modelValue', $event)" :title="`${props.detailData.courseName} - 考勤详情`" - width="720px" + width="1010px" destroy-on-close >
@@ -13,24 +13,52 @@
- - + + + + + - - - - + + + + + + + + + - - -
+
@@ -42,9 +70,9 @@ @@ -106,4 +211,11 @@ watch(detailTab, (val) => { font-size: 13px; color: #525252; } + +.search-bar { + display: flex; + gap: 12px; + align-items: center; + margin-bottom: 16px; +} diff --git a/src/views/dashboard/components/AttendanceManage.vue b/src/views/dashboard/components/AttendanceManage.vue index f38fee7..d6b5921 100644 --- a/src/views/dashboard/components/AttendanceManage.vue +++ b/src/views/dashboard/components/AttendanceManage.vue @@ -78,7 +78,7 @@ import { ref, reactive, computed, onMounted, watch } from 'vue' import { ElMessage } from 'element-plus' import { Search } from '@element-plus/icons-vue' -import { getRecordPage } from '@/api/attendance' +import { getRecordPage, getDetailPage } from '@/api/attendance' import { useNameMaps } from '@/composables/useNameMaps' defineEmits(['showDetail']) @@ -100,7 +100,8 @@ const mapRecord = (item) => ({ total: item.totalCount, actual: item.actualCount, absentCount: item.absentCount, - absentRate: item.absentRate ?? 0 + absentRate: item.absentRate ?? 0, + id: item.id }) const tableData = ref([]) @@ -160,7 +161,36 @@ const handleReset = () => { pagination.current = 1 fetchData() } -const handleExport = (row) => ElMessage.success(`正在导出 ${row.courseName} 考勤明细...`) +const handleExport = async () => { + try { + const res = await getDetailPage({ current: 1, size: 9999 }) + const list = res?.data?.records || [] + if (list.length === 0) { + ElMessage.warning('暂无考勤详情数据') + return + } + + const statusLabels = { 0: '未签到', 1: '正常', 2: '迟到', 3: '缺勤', 4: '早退', 5: '请假' } + const headers = ['学号', '姓名', '考勤日期', '签到时间', '签退时间', '人脸相似度', '考勤状态'] + const rows = list.map(r => [ + r.studentNo, r.studentName, r.attDate, r.checkInTime, r.checkOutTime, + r.faceSimilarity ? (r.faceSimilarity * 100).toFixed(1) + '%' : '', + statusLabels[r.attStatus] || '未知' + ]) + const csv = [headers, ...rows] + .map(r => r.map(c => `"${String(c ?? '').replace(/"/g, '""')}"`).join(',')) + .join('\n') + const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }) + const a = document.createElement('a') + a.href = URL.createObjectURL(blob) + a.download = `考勤明细_${new Date().toLocaleDateString()}.csv` + a.click() + URL.revokeObjectURL(a.href) + ElMessage.success(`已导出 ${list.length} 条记录`) + } catch { + ElMessage.error('导出失败') + } +} diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue index 90b63b2..0215baa 100644 --- a/src/views/dashboard/index.vue +++ b/src/views/dashboard/index.vue @@ -55,6 +55,7 @@
+ - - - + @@ -149,7 +145,7 @@ const recentRecords = ref([]) const fetchRecentRecords = async () => { try { - const res = await getRecordPage({ current: 1, size: 10 }) + const res = await getRecordPage({ current: 1, size: 10, taskStatus: 2 }) const { records } = res.data if (Array.isArray(records)) { recentRecords.value = records.map(mapRecord) diff --git a/src/views/info/components/ClassCourseDialog.vue b/src/views/info/components/ClassCourseDialog.vue index f643294..83f1121 100644 --- a/src/views/info/components/ClassCourseDialog.vue +++ b/src/views/info/components/ClassCourseDialog.vue @@ -82,6 +82,9 @@ + + + @@ -109,6 +112,7 @@ {{ detailData.endSection }} {{ detailData.startWeek }} {{ detailData.endWeek }} + {{ detailData.firstClassTime }} {{ getBuildingName(detailData.classroomId) }} {{ getRoomName(detailData.classroomId) }} @@ -271,7 +275,8 @@ const addRules = { startSection: [{ required: true, message: '请输入开始节次', trigger: 'blur' }], endSection: [{ required: true, message: '请输入结束节次', trigger: 'blur' }], startWeek: [{ required: true, message: '请输入开始周', trigger: 'blur' }], - endWeek: [{ required: true, message: '请输入结束周', trigger: 'blur' }] + endWeek: [{ required: true, message: '请输入结束周', trigger: 'blur' }], + firstClassTime: [{ required: true, message: '请选择上课时间', trigger: 'change' }] } const defaultForm = () => ({ @@ -284,6 +289,7 @@ const defaultForm = () => ({ endSection: 2, startWeek: 1, endWeek: 16, + firstClassTime: '', semester: '', status: 1 }) @@ -315,6 +321,7 @@ const showEditDialog = (row) => { endSection: row.endSection, startWeek: row.startWeek, endWeek: row.endWeek, + firstClassTime: row.firstClassTime || '', semester: row.semester || '', status: row.status } @@ -339,6 +346,7 @@ const saveSchedule = async () => { endSection: addForm.value.endSection, startWeek: addForm.value.startWeek, endWeek: addForm.value.endWeek, + firstClassTime: addForm.value.firstClassTime, semester: addForm.value.semester, status: addForm.value.status } diff --git a/src/views/info/student.vue b/src/views/info/student.vue index 3b711e6..d18b017 100644 --- a/src/views/info/student.vue +++ b/src/views/info/student.vue @@ -2,7 +2,7 @@
@@ -20,6 +20,13 @@
+ + + @@ -56,12 +63,18 @@
- - - + +
+
+ + {{ form.name?.charAt(0) }} + +
+ + - + @@ -70,7 +83,7 @@ - + @@ -84,18 +97,72 @@ 离校 - + + + + + + + + + + + + + + + +
+ +
+ +
+ + + +
+ +
+