终版
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user