This commit is contained in:
2026-05-21 08:07:04 +08:00
parent 9903da092d
commit e52e5701f9
24 changed files with 695 additions and 0 deletions
+43
View File
@@ -13,6 +13,8 @@ ApplicationWindow {
title: "人脸识别考勤系统"
color: "#f6f7f9"
// 全局设计变量:颜色、字体和响应式断点集中定义,后续页面和组件只引用这些变量。
// 这样桌面端与 Android 端能保持统一风格,也便于毕业设计截图统一视觉效果。
readonly property color ink900: "#182230"
readonly property color ink700: "#475467"
readonly property color ink500: "#667085"
@@ -58,6 +60,7 @@ ApplicationWindow {
font.family: appFontFamily
// 窗口拖拽缩放时先标记 resizingWindow,部分复杂列表和动画可据此降低刷新频率。
onWidthChanged: {
resizingWindow = true;
resizeSettledTimer.restart();
@@ -68,6 +71,7 @@ ApplicationWindow {
resizeSettledTimer.restart();
}
// 函数作用:切换主页面,并把页面下标限制在合法范围内。
function switchPage(index) {
currentPageIndex = Math.max(0, Math.min(4, index));
}
@@ -79,6 +83,8 @@ ApplicationWindow {
onTriggered: resizingWindow = false
}
// 以下 component 是本文件内部的轻量设计系统:输入框、按钮、空状态、标题等
// 都在这里统一样式,主页面只负责组合业务内容。
component AppCard: Rectangle {
radius: 8
color: panelStrong
@@ -441,15 +447,18 @@ ApplicationWindow {
}
}
// 函数作用:把记录筛选栏的当前条件提交给 C++ 后端。
function applyRecordFilters() {
appController.applyFilters(classFilter.currentText, studentFilter.currentText, nameFilter.text, dateFilter.currentIndex, specificDateField.text);
}
// 函数作用:根据统计页预设选项计算开始日期和结束日期。
function applyStatisticsPreset() {
const today = new Date();
let start = new Date(today);
let end = new Date(today);
// 预设只负责改写日期输入框,真正的统计重算仍统一交给 C++ 控制器完成。
if (statisticsPreset === "今天") {
start = new Date(today);
} else if (statisticsPreset === "最近7天") {
@@ -465,9 +474,11 @@ ApplicationWindow {
appController.updateStatisticsRange(statsStartField.text, statsEndField.text);
}
// 函数作用:打开时间段编辑弹窗,index 为 -1 时表示新增。
function openTimeSlotDialog(index) {
editingTimeSlotIndex = index;
if (index >= 0) {
// 编辑模式从模型读取当前行,避免 QML delegate 中维护重复状态。
const item = appController.timeSlotModel.get(index);
timeSlotNameField.text = item.name;
timeSlotStartField.text = item.startTime;
@@ -475,6 +486,7 @@ ApplicationWindow {
timeSlotEnabledBox.checked = item.enabled;
timeSlotDialog.title = "编辑时间段";
} else {
// 新增模式使用一组可直接修改的默认值,用户确认后再写入模型。
timeSlotNameField.text = "";
timeSlotStartField.text = "08:00";
timeSlotEndField.text = "09:30";
@@ -484,6 +496,7 @@ ApplicationWindow {
timeSlotDialog.open();
}
// 函数作用:保存时间段弹窗中的内容,并同步到 C++ 后端。
function saveTimeSlotDialog() {
if (editingTimeSlotIndex >= 0) {
appController.timeSlotModel.updateTimeSlot(editingTimeSlotIndex, timeSlotNameField.text, timeSlotStartField.text, timeSlotEndField.text, timeSlotEnabledBox.checked);
@@ -493,6 +506,7 @@ ApplicationWindow {
appController.saveTimeSlots();
}
// 函数作用:根据班级编号查找班级下拉框中对应的选项下标。
function classComboIndex(classId) {
var display = appController.classDisplayForId(classId);
var options = appController.classSelectionOptions;
@@ -504,6 +518,7 @@ ApplicationWindow {
return options.length > 0 ? 0 : -1;
}
// 函数作用:在普通选项数组中查找指定值的下标,找不到时返回 0。
function optionIndex(options, value) {
for (var i = 0; i < options.length; ++i) {
if (options[i] === value) {
@@ -513,18 +528,22 @@ ApplicationWindow {
return 0;
}
// 函数作用:标记配置中心存在尚未保存的改动。
function markMappingsDirty() {
mappingsDirty = true;
}
// 函数作用:提交学生档案编辑结果,并把界面班级文本转换为真实班级编号。
function commitStudentProfile(index, studentId, studentName, classDisplay) {
appController.updateStudentProfile(index, studentId, studentName, appController.classIdFromDisplay(classDisplay));
markMappingsDirty();
}
// 函数作用:在提交学生档案前检查学号变化是否需要二次确认。
function requestStudentProfileCommit(index, studentId, studentName, classDisplay) {
var impactMessage = appController.studentIdChangeImpactMessage(index, studentId);
if (impactMessage.length > 0) {
// 学号变更会连带人脸绑定和历史记录显示,先暂存待提交值,等确认弹窗回调再真正保存。
pendingStudentIdChangeIndex = index;
pendingStudentIdChangeNewId = studentId;
pendingStudentIdChangeName = studentName;
@@ -536,6 +555,7 @@ ApplicationWindow {
commitStudentProfile(index, studentId, studentName, classDisplay);
}
// 函数作用:判断某条学生档案是否符合搜索、班级和绑定状态过滤条件。
function studentProfileMatches(searchText, classFilter, bindingFilter, studentId, studentName, classId, className, faceIds) {
var normalizedSearch = searchText ? searchText.trim().toLowerCase() : "";
var classFilterId = appController.classIdFromDisplay(classFilter);
@@ -551,10 +571,12 @@ ApplicationWindow {
if (normalizedSearch.length === 0) {
return true;
}
// 搜索同时覆盖学号、姓名、班级编号、班级名称和人脸编号,方便答辩演示时快速定位记录。
var haystack = (studentId + " " + studentName + " " + classId + " " + className + " " + faceIds).toLowerCase();
return haystack.indexOf(normalizedSearch) >= 0;
}
// 函数作用:打开人脸档案编辑弹窗,并填充当前绑定信息。
function openFaceEditor(faceId, studentId, studentName, classId) {
editingFaceId = faceId;
editingFaceOriginalStudentId = studentId;
@@ -564,6 +586,7 @@ ApplicationWindow {
faceEditDialog.open();
}
// 函数作用:直接保存人脸编辑弹窗中的绑定信息。
function saveFaceEditorNow() {
var savedLocally = appController.editFaceProfile(editingFaceId,
faceEditStudentIdField.text,
@@ -577,10 +600,12 @@ ApplicationWindow {
appController.saveFaceMappings();
}
// 函数作用:保存人脸编辑前检查绑定学号变化是否需要确认。
function requestSaveFaceEditor() {
var oldId = editingFaceOriginalStudentId ? editingFaceOriginalStudentId.trim() : "";
var newId = faceEditStudentIdField.text ? faceEditStudentIdField.text.trim() : "";
if (oldId.length > 0 && oldId !== newId) {
// 已绑定人脸改到另一个学号时影响范围较大,交给确认弹窗完成最后一次用户确认。
faceStudentIdChangeDialog.text = "修改该人脸绑定会影响识别后发送给 STM32 的学号、考勤记录显示、班级筛选和导出结果。确定继续吗?";
faceStudentIdChangeDialog.open();
return;
@@ -588,6 +613,7 @@ ApplicationWindow {
saveFaceEditorNow();
}
// 函数作用:页面加载完成后初始化默认记录筛选和统计日期区间。
Component.onCompleted: {
applyRecordFilters();
applyStatisticsPreset();
@@ -596,10 +622,12 @@ ApplicationWindow {
Connections {
target: appController
// 函数作用:监听 C++ 状态消息变化,并转换为右下角 toast 提示。
function onStatusMessageChanged() {
if (!appController.statusMessage || appController.statusMessage.length === 0) {
return;
}
// 后端保存成功后清除脏标记,界面不再提示用户有未保存配置。
if (appController.statusMessage.indexOf("映射配置已保存") >= 0
|| appController.statusMessage.indexOf("映射配置已同步") >= 0
|| appController.statusMessage.indexOf("人脸档案已保存") >= 0) {
@@ -640,6 +668,7 @@ ApplicationWindow {
Dialog {
id: clearDialog
// 清空全部考勤记录属于高风险操作,必须通过确认弹窗触发。
title: "确认清空"
modal: true
anchors.centerIn: Overlay.overlay
@@ -709,6 +738,7 @@ ApplicationWindow {
Dialog {
id: timeSlotDialog
// 时间段新增/编辑共用同一个弹窗,通过 editingTimeSlotIndex 判断保存方式。
modal: true
anchors.centerIn: Overlay.overlay
width: 420
@@ -787,6 +817,7 @@ ApplicationWindow {
Dialog {
id: deleteFaceDialog
// 删除人脸信息可能连带删除考勤记录,因此单独弹窗确认并提供复选项。
modal: true
anchors.centerIn: Overlay.overlay
width: 430
@@ -857,6 +888,7 @@ ApplicationWindow {
Dialog {
id: faceEditDialog
// 编辑人脸档案时只改绑定关系和学生资料,不修改 K210 生成的人脸编号。
modal: true
anchors.centerIn: Overlay.overlay
width: phoneLayout ? Math.min(window.width - 32, 420) : 460
@@ -977,6 +1009,7 @@ ApplicationWindow {
anchors.margins: slimLayout ? 10 : 16
spacing: slimLayout ? 0 : 16
// 桌面侧边栏导航;窄屏时隐藏,改用下方的移动端 TabBar。
Rectangle {
visible: !slimLayout
Layout.preferredWidth: visible ? sidebarWidth : 0
@@ -1289,6 +1322,7 @@ ApplicationWindow {
StackLayout {
id: pageLayout
// 五个主页面共用 StackLayout:考勤记录、统计分析、配置中心、人脸管理、关于。
Layout.fillWidth: true
Layout.fillHeight: true
currentIndex: window.currentPageIndex
@@ -2372,6 +2406,7 @@ ApplicationWindow {
spacing: 8
model: appController.timeSlotModel
// 桌面端和手机端共用同一份时间段模型,只在 delegate 内切换行布局。
delegate: Rectangle {
width: ListView.view.width
height: phoneLayout ? 96 : 52
@@ -2404,6 +2439,7 @@ ApplicationWindow {
visible: !phoneLayout
spacing: 12
// 桌面布局按表格列对齐,便于快速比较多个时间段的起止时间。
Label {
text: model.name
Layout.preferredWidth: 220
@@ -2444,6 +2480,7 @@ ApplicationWindow {
visible: phoneLayout
spacing: 8
// 手机布局拆成两行,避免名称、时间和按钮在窄屏上互相挤压。
RowLayout {
Layout.fillWidth: true
spacing: 8
@@ -2580,6 +2617,7 @@ ApplicationWindow {
spacing: 8
model: appController.classMappingModel
// 班级列表直接编辑模型,保存按钮再统一把完整配置同步到服务端。
delegate: Rectangle {
width: ListView.view.width
height: phoneLayout ? 112 : 56
@@ -2608,6 +2646,7 @@ ApplicationWindow {
visible: !phoneLayout
spacing: 12
// 桌面端让编号和名称保持两列,方便核对 class_id 与 class_name。
AppTextField {
id: classKeyField
Layout.preferredWidth: 220
@@ -2648,6 +2687,7 @@ ApplicationWindow {
visible: phoneLayout
spacing: 8
// 窄屏下将编号、名称、删除按钮分行显示,避免输入框宽度过小。
AppTextField {
id: classKeyFieldPhone
Layout.fillWidth: true
@@ -2777,6 +2817,7 @@ ApplicationWindow {
cellHeight: phoneLayout ? 444 : 404
model: appController.faceModel
// 人脸卡片展示的是控制器合并后的结果:照片缓存、绑定学号、学生档案和班级名称。
delegate: Rectangle {
width: Math.min(phoneLayout ? 300 : 244, GridView.view.cellWidth - 18)
height: phoneLayout ? 420 : 380
@@ -2815,6 +2856,7 @@ ApplicationWindow {
color: model.hasPhoto ? "#eef3f5" : goldSoft
border.color: model.hasPhoto ? "#dbe4ea" : "#eadbb0"
// 照片由 C++ 下载到本地缓存后以 file:// URL 暴露,QML 只负责显示。
Image {
anchors.fill: parent
anchors.margins: 8
@@ -2916,6 +2958,7 @@ ApplicationWindow {
Layout.fillWidth: true
text: "删除"
onClicked: {
// 删除操作先记录当前卡片的人脸编号,再交给确认弹窗决定是否连带删除考勤记录。
pendingDeleteFaceId = model.faceId;
deleteFaceRecordsCheck.checked = false;
deleteFaceDialog.open();