Files
2026-05-21 08:07:04 +08:00

3160 lines
159 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
ApplicationWindow {
id: window
width: 1560
height: 920
minimumWidth: Qt.platform.os === "android" ? 360 : 900
minimumHeight: Qt.platform.os === "android" ? 560 : 620
visible: true
title: "人脸识别考勤系统"
color: "#f6f7f9"
// 全局设计变量:颜色、字体和响应式断点集中定义,后续页面和组件只引用这些变量。
// 这样桌面端与 Android 端能保持统一风格,也便于毕业设计截图统一视觉效果。
readonly property color ink900: "#182230"
readonly property color ink700: "#475467"
readonly property color ink500: "#667085"
readonly property color lineColor: "#d8dde5"
readonly property color shellColor: "#f6f7f9"
readonly property color panelColor: "#f9fafb"
readonly property color panelStrong: "#ffffff"
readonly property color accentColor: "#b6543a"
readonly property color accentDeep: "#8e3f29"
readonly property color accentSoft: "#f4d6c8"
readonly property color tealColor: "#1f6f78"
readonly property color tealSoft: "#d7ecec"
readonly property color goldColor: "#aa7a1f"
readonly property color goldSoft: "#f5e8c6"
readonly property color successColor: "#1f7a4d"
readonly property color dangerColor: "#b9382f"
readonly property string appFontFamily: Qt.platform.os === "android" ? "sans-serif" : "Microsoft YaHei UI"
readonly property int appBoldWeight: Qt.platform.os === "android" ? Font.Medium : Font.Bold
property string statisticsPreset: "最近30天"
property int editingTimeSlotIndex: -1
property string pendingDeleteFaceId: ""
property int pendingStudentIdChangeIndex: -1
property string pendingStudentIdChangeNewId: ""
property string pendingStudentIdChangeName: ""
property string pendingStudentIdChangeClassDisplay: ""
property string editingFaceId: ""
property string editingFaceOriginalStudentId: ""
property string toastMessage: ""
property bool toastVisible: false
property bool resizingWindow: false
property int currentPageIndex: 0
property bool timeSlotsExpanded: false
property bool mappingsExpanded: false
property bool mappingsDirty: false
readonly property bool androidLayout: Qt.platform.os === "android"
readonly property bool compactLayout: width < 1460
readonly property bool denseLayout: width < 1380
readonly property bool narrowLayout: width < 1280
readonly property bool slimLayout: width < 1040
readonly property bool phoneLayout: width < (androidLayout ? 640 : 560)
readonly property int sidebarWidth: slimLayout ? 0 : (compactLayout ? 212 : 240)
font.family: appFontFamily
// 窗口拖拽缩放时先标记 resizingWindow,部分复杂列表和动画可据此降低刷新频率。
onWidthChanged: {
resizingWindow = true;
resizeSettledTimer.restart();
}
onHeightChanged: {
resizingWindow = true;
resizeSettledTimer.restart();
}
// 函数作用:切换主页面,并把页面下标限制在合法范围内。
function switchPage(index) {
currentPageIndex = Math.max(0, Math.min(4, index));
}
Timer {
id: resizeSettledTimer
interval: 160
repeat: false
onTriggered: resizingWindow = false
}
// 以下 component 是本文件内部的轻量设计系统:输入框、按钮、空状态、标题等
// 都在这里统一样式,主页面只负责组合业务内容。
component AppCard: Rectangle {
radius: 8
color: panelStrong
border.color: lineColor
}
component AppTextField: TextField {
id: control
implicitHeight: 42
leftPadding: 14
rightPadding: 14
topPadding: 10
bottomPadding: 10
color: ink900
selectedTextColor: "#ffffff"
selectionColor: accentColor
placeholderTextColor: "#98a2b3"
background: Rectangle {
radius: 8
color: "#ffffff"
border.width: control.activeFocus ? 2 : 1
border.color: control.activeFocus ? accentColor : "#d6cec4"
}
}
component AppComboBox: ComboBox {
id: control
implicitHeight: 42
leftPadding: 14
rightPadding: 32
font.pixelSize: 13
delegate: ItemDelegate {
width: ListView.view.width
text: modelData
highlighted: control.highlightedIndex === index
}
contentItem: Text {
leftPadding: 0
rightPadding: 0
text: control.displayText
font: control.font
color: ink900
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
indicator: Label {
x: control.width - width - 12
y: (control.height - height) / 2
text: "⌄"
color: ink500
font.pixelSize: 16
}
background: Rectangle {
radius: 8
color: "#ffffff"
border.width: control.activeFocus ? 2 : 1
border.color: control.activeFocus ? accentColor : "#d6cec4"
}
popup: Popup {
y: control.height + 6
width: control.width
padding: 6
background: Rectangle {
radius: 8
color: "#ffffff"
border.color: lineColor
}
contentItem: ListView {
implicitHeight: contentHeight
model: control.popup.visible ? control.delegateModel : null
currentIndex: control.highlightedIndex
clip: true
}
}
}
component PrimaryButton: Button {
id: control
property color fillColor: accentColor
property color labelColor: "#fff7f2"
hoverEnabled: true
implicitHeight: 42
implicitWidth: 112
background: Rectangle {
radius: 8
color: control.down ? Qt.darker(control.fillColor, 1.12) : control.hovered ? Qt.lighter(control.fillColor, 1.06) : control.fillColor
}
contentItem: Label {
width: control.availableWidth
text: control.text
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: control.labelColor
font.pixelSize: 13
font.weight: appBoldWeight
elide: Text.ElideRight
}
}
component SecondaryButton: Button {
id: control
property color fillColor: "#ffffff"
property color strokeColor: "#d6cec4"
property color labelColor: ink900
hoverEnabled: true
implicitHeight: 42
implicitWidth: 96
background: Rectangle {
radius: 8
color: control.down ? "#efe6db" : control.hovered ? "#f7f1ea" : control.fillColor
border.color: control.hovered ? accentSoft : control.strokeColor
}
contentItem: Label {
width: control.availableWidth
text: control.text
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: control.labelColor
font.pixelSize: 13
font.weight: appBoldWeight
elide: Text.ElideRight
}
}
component DangerButton: PrimaryButton {
fillColor: dangerColor
labelColor: "#fff5f4"
}
component AppCheckBox: CheckBox {
id: control
spacing: 10
indicator: Rectangle {
implicitWidth: 20
implicitHeight: 20
radius: 6
color: control.checked ? accentColor : "#ffffff"
border.width: 1
border.color: control.checked ? accentColor : "#cfc6bc"
Rectangle {
anchors.centerIn: parent
width: 8
height: 8
radius: 4
color: "#fff7f2"
visible: control.checked
}
}
contentItem: Text {
text: control.text
color: ink700
font: control.font
verticalAlignment: Text.AlignVCenter
leftPadding: control.indicator.width + control.spacing
}
}
component AppScrollBar: ScrollBar {
id: control
implicitWidth: 12
policy: ScrollBar.AsNeeded
contentItem: Rectangle {
implicitWidth: 8
radius: 4
color: control.pressed ? accentColor : "#cfbeb1"
opacity: control.active ? 0.9 : 0.55
}
background: Item {}
}
component EmptyState: Item {
id: emptyState
property string title: ""
property string description: ""
property string badgeText: ""
property color accent: accentColor
readonly property bool tightState: phoneLayout || height < 190
implicitWidth: phoneLayout ? 280 : 320
implicitHeight: phoneLayout ? 154 : 220
width: Math.min(implicitWidth, parent ? Math.max(180, parent.width - 28) : implicitWidth)
height: Math.min(implicitHeight, parent ? Math.max(126, parent.height - 20) : implicitHeight)
z: 20
Rectangle {
anchors.fill: parent
radius: 8
color: "#fffaf4"
border.color: "#e7ddd2"
Column {
anchors.centerIn: parent
spacing: emptyState.tightState ? 8 : 12
width: parent.width - 32
Rectangle {
width: emptyState.tightState ? 52 : 64
height: width
radius: 8
color: Qt.tint("#ffffff", Qt.rgba(accent.r, accent.g, accent.b, 0.16))
anchors.horizontalCenter: parent.horizontalCenter
Label {
anchors.centerIn: parent
text: badgeText
color: accent
font.pixelSize: emptyState.tightState ? 18 : 22
font.weight: appBoldWeight
}
}
Label {
width: parent.width
text: title
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
color: ink900
font.pixelSize: emptyState.tightState ? 18 : 20
font.weight: appBoldWeight
}
Label {
width: parent.width
visible: !emptyState.tightState || parent.width > 260
text: description
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
color: ink500
font.pixelSize: emptyState.tightState ? 12 : 13
}
}
}
}
component InfoChip: Rectangle {
property string textValue: ""
property color tintColor: accentColor
implicitHeight: 30
implicitWidth: chipLabel.implicitWidth + 18
radius: 15
color: Qt.tint("#ffffff", Qt.rgba(tintColor.r, tintColor.g, tintColor.b, 0.16))
border.color: Qt.tint("#ffffff", Qt.rgba(tintColor.r, tintColor.g, tintColor.b, 0.34))
Label {
id: chipLabel
anchors.centerIn: parent
text: parent.textValue
color: parent.tintColor
font.pixelSize: 11
font.weight: appBoldWeight
}
}
component SectionHeader: Item {
id: sectionHeader
property string title: ""
property string description: ""
property color accent: accentColor
implicitHeight: descriptionLabel.visible ? (phoneLayout ? 64 : 52) : 32
implicitWidth: 300
RowLayout {
anchors.fill: parent
spacing: 12
Rectangle {
Layout.preferredWidth: 6
Layout.fillHeight: true
radius: 3
color: sectionHeader.accent
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
Label {
Layout.fillWidth: true
text: sectionHeader.title
font.pixelSize: phoneLayout ? 20 : 22
font.weight: appBoldWeight
color: ink900
wrapMode: Text.WordWrap
}
Label {
id: descriptionLabel
Layout.fillWidth: true
visible: text.length > 0
text: sectionHeader.description
color: ink500
font.pixelSize: 12
wrapMode: Text.WordWrap
}
}
}
}
component SegmentTabButton: TabButton {
id: control
implicitHeight: 40
padding: 0
background: Rectangle {
radius: 8
color: control.checked ? tealColor : "#ffffff"
border.width: 1
border.color: control.checked ? tealColor : "#d6cec4"
}
contentItem: Label {
text: control.text
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: control.checked ? "#f6fbfb" : ink700
font.pixelSize: 13
font.weight: appBoldWeight
elide: Text.ElideRight
}
}
component MobileNavTabButton: TabButton {
id: control
implicitHeight: 42
padding: 0
background: Rectangle {
radius: 8
color: control.checked ? tealColor : panelStrong
border.width: 1
border.color: control.checked ? tealColor : lineColor
}
contentItem: Label {
text: control.text
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: control.checked ? "#f6fbfb" : ink700
font.pixelSize: phoneLayout ? 12 : 13
font.weight: appBoldWeight
elide: Text.ElideRight
}
}
// 函数作用:把记录筛选栏的当前条件提交给 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天") {
start.setDate(start.getDate() - 7);
} else if (statisticsPreset === "最近30天") {
start.setDate(start.getDate() - 30);
} else if (statisticsPreset === "本月") {
start = new Date(today.getFullYear(), today.getMonth(), 1);
}
statsStartField.text = Qt.formatDate(start, "yyyy-MM-dd");
statsEndField.text = Qt.formatDate(end, "yyyy-MM-dd");
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;
timeSlotEndField.text = item.endTime;
timeSlotEnabledBox.checked = item.enabled;
timeSlotDialog.title = "编辑时间段";
} else {
// 新增模式使用一组可直接修改的默认值,用户确认后再写入模型。
timeSlotNameField.text = "";
timeSlotStartField.text = "08:00";
timeSlotEndField.text = "09:30";
timeSlotEnabledBox.checked = true;
timeSlotDialog.title = "新增时间段";
}
timeSlotDialog.open();
}
// 函数作用:保存时间段弹窗中的内容,并同步到 C++ 后端。
function saveTimeSlotDialog() {
if (editingTimeSlotIndex >= 0) {
appController.timeSlotModel.updateTimeSlot(editingTimeSlotIndex, timeSlotNameField.text, timeSlotStartField.text, timeSlotEndField.text, timeSlotEnabledBox.checked);
} else {
appController.timeSlotModel.addTimeSlot(timeSlotNameField.text, timeSlotStartField.text, timeSlotEndField.text, timeSlotEnabledBox.checked);
}
appController.saveTimeSlots();
}
// 函数作用:根据班级编号查找班级下拉框中对应的选项下标。
function classComboIndex(classId) {
var display = appController.classDisplayForId(classId);
var options = appController.classSelectionOptions;
for (var i = 0; i < options.length; ++i) {
if (options[i] === display) {
return i;
}
}
return options.length > 0 ? 0 : -1;
}
// 函数作用:在普通选项数组中查找指定值的下标,找不到时返回 0。
function optionIndex(options, value) {
for (var i = 0; i < options.length; ++i) {
if (options[i] === value) {
return i;
}
}
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;
pendingStudentIdChangeClassDisplay = classDisplay;
studentIdChangeDialog.text = impactMessage;
studentIdChangeDialog.open();
return;
}
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);
if (classFilter && classFilter !== "全部" && classId !== classFilterId) {
return false;
}
if (bindingFilter === "已绑定" && faceIds.length === 0) {
return false;
}
if (bindingFilter === "未绑定" && faceIds.length > 0) {
return false;
}
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;
faceEditStudentIdField.text = studentId;
faceEditStudentNameField.text = studentName;
faceEditClassCombo.currentIndex = classComboIndex(classId);
faceEditDialog.open();
}
// 函数作用:直接保存人脸编辑弹窗中的绑定信息。
function saveFaceEditorNow() {
var savedLocally = appController.editFaceProfile(editingFaceId,
faceEditStudentIdField.text,
faceEditStudentNameField.text,
appController.classIdFromDisplay(faceEditClassCombo.currentText));
if (!savedLocally) {
faceEditDialog.open();
return;
}
markMappingsDirty();
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;
}
saveFaceEditorNow();
}
// 函数作用:页面加载完成后初始化默认记录筛选和统计日期区间。
Component.onCompleted: {
applyRecordFilters();
applyStatisticsPreset();
}
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) {
mappingsDirty = false;
}
toastMessage = appController.statusMessage;
toastVisible = true;
toastTimer.restart();
}
}
Timer {
id: toastTimer
interval: 2600
repeat: false
onTriggered: toastVisible = false
}
background: Rectangle {
color: shellColor
}
FileDialog {
id: recordExportDialog
fileMode: FileDialog.SaveFile
nameFilters: ["CSV 文件 (*.csv)"]
defaultSuffix: "csv"
onAccepted: appController.exportFilteredRecords(selectedFile)
}
FileDialog {
id: statisticsExportDialog
fileMode: FileDialog.SaveFile
nameFilters: ["CSV 文件 (*.csv)"]
defaultSuffix: "csv"
onAccepted: appController.exportStatistics(selectedFile)
}
Dialog {
id: clearDialog
// 清空全部考勤记录属于高风险操作,必须通过确认弹窗触发。
title: "确认清空"
modal: true
anchors.centerIn: Overlay.overlay
width: 440
standardButtons: Dialog.Yes | Dialog.No
background: Rectangle {
radius: 20
color: panelStrong
border.color: lineColor
}
enter: Transition {
ParallelAnimation {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: 150
}
NumberAnimation {
property: "scale"
from: 0.96
to: 1.0
duration: 180
easing.type: Easing.OutCubic
}
}
}
exit: Transition {
ParallelAnimation {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: 120
}
NumberAnimation {
property: "scale"
from: 1.0
to: 0.98
duration: 120
}
}
}
contentItem: ColumnLayout {
spacing: 14
Label {
text: "确定要清空服务器上的所有签到记录吗?"
font.pixelSize: 20
font.weight: appBoldWeight
color: ink900
}
Label {
text: "该操作不可恢复,建议先导出数据备份。"
wrapMode: Text.WordWrap
color: ink700
}
}
onAccepted: appController.clearAllRecords()
}
Dialog {
id: timeSlotDialog
// 时间段新增/编辑共用同一个弹窗,通过 editingTimeSlotIndex 判断保存方式。
modal: true
anchors.centerIn: Overlay.overlay
width: 420
standardButtons: Dialog.Save | Dialog.Cancel
background: Rectangle {
radius: 20
color: panelStrong
border.color: lineColor
}
enter: Transition {
ParallelAnimation {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: 150
}
NumberAnimation {
property: "scale"
from: 0.96
to: 1.0
duration: 180
easing.type: Easing.OutCubic
}
}
}
exit: Transition {
ParallelAnimation {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: 120
}
NumberAnimation {
property: "scale"
from: 1.0
to: 0.98
duration: 120
}
}
}
contentItem: ColumnLayout {
spacing: 12
AppTextField {
id: timeSlotNameField
Layout.fillWidth: true
placeholderText: "时段名称"
}
AppTextField {
id: timeSlotStartField
Layout.fillWidth: true
placeholderText: "开始时间,例如 08:00"
}
AppTextField {
id: timeSlotEndField
Layout.fillWidth: true
placeholderText: "结束时间,例如 09:30"
}
AppCheckBox {
id: timeSlotEnabledBox
text: "启用此时段"
}
}
onAccepted: saveTimeSlotDialog()
}
Dialog {
id: deleteFaceDialog
// 删除人脸信息可能连带删除考勤记录,因此单独弹窗确认并提供复选项。
modal: true
anchors.centerIn: Overlay.overlay
width: 430
title: "删除人脸信息"
standardButtons: Dialog.Yes | Dialog.No
background: Rectangle {
radius: 20
color: panelStrong
border.color: lineColor
}
enter: Transition {
ParallelAnimation {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: 150
}
NumberAnimation {
property: "scale"
from: 0.96
to: 1.0
duration: 180
easing.type: Easing.OutCubic
}
}
}
exit: Transition {
ParallelAnimation {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: 120
}
NumberAnimation {
property: "scale"
from: 1.0
to: 0.98
duration: 120
}
}
}
contentItem: ColumnLayout {
spacing: 12
Label {
text: "确定删除人脸编号 " + pendingDeleteFaceId + " 吗?"
wrapMode: Text.WordWrap
font.pixelSize: 18
font.weight: appBoldWeight
color: ink900
}
AppCheckBox {
id: deleteFaceRecordsCheck
text: "同时删除该学生的所有考勤记录"
checked: false
}
}
onAccepted: appController.deleteFace(pendingDeleteFaceId, deleteFaceRecordsCheck.checked)
}
Dialog {
id: faceEditDialog
// 编辑人脸档案时只改绑定关系和学生资料,不修改 K210 生成的人脸编号。
modal: true
anchors.centerIn: Overlay.overlay
width: phoneLayout ? Math.min(window.width - 32, 420) : 460
title: "编辑人脸档案"
standardButtons: Dialog.Save | Dialog.Cancel
background: Rectangle {
radius: 20
color: panelStrong
border.color: lineColor
}
enter: Transition {
ParallelAnimation {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: 150
}
NumberAnimation {
property: "scale"
from: 0.96
to: 1.0
duration: 180
easing.type: Easing.OutCubic
}
}
}
exit: Transition {
ParallelAnimation {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: 120
}
NumberAnimation {
property: "scale"
from: 1.0
to: 0.98
duration: 120
}
}
}
contentItem: ColumnLayout {
spacing: 12
Label {
Layout.fillWidth: true
text: "人脸编号: " + editingFaceId
color: ink900
font.pixelSize: 18
font.weight: appBoldWeight
elide: Text.ElideRight
}
AppTextField {
id: faceEditStudentIdField
Layout.fillWidth: true
placeholderText: "真实学号"
}
AppTextField {
id: faceEditStudentNameField
Layout.fillWidth: true
placeholderText: "姓名"
}
AppComboBox {
id: faceEditClassCombo
Layout.fillWidth: true
model: appController.classSelectionOptions
}
Label {
Layout.fillWidth: true
text: "人脸编号由 K210 生成,不要改成学号;这里只编辑学号、姓名和班级。留空学号可解除绑定。"
wrapMode: Text.WordWrap
color: ink500
font.pixelSize: 12
}
}
onAccepted: requestSaveFaceEditor()
}
MessageDialog {
id: faceStudentIdChangeDialog
title: "确认修改绑定学号"
buttons: MessageDialog.Yes | MessageDialog.No
onAccepted: saveFaceEditorNow()
}
MessageDialog {
id: studentIdChangeDialog
title: "确认修改学号"
buttons: MessageDialog.Yes | MessageDialog.No
onAccepted: {
commitStudentProfile(pendingStudentIdChangeIndex,
pendingStudentIdChangeNewId,
pendingStudentIdChangeName,
pendingStudentIdChangeClassDisplay);
}
}
Rectangle {
anchors.fill: parent
anchors.margins: slimLayout ? 8 : 16
radius: 12
color: "#ffffff"
border.color: "#dbd6ce"
RowLayout {
anchors.fill: parent
anchors.margins: slimLayout ? 10 : 16
spacing: slimLayout ? 0 : 16
// 桌面侧边栏导航;窄屏时隐藏,改用下方的移动端 TabBar。
Rectangle {
visible: !slimLayout
Layout.preferredWidth: visible ? sidebarWidth : 0
Layout.maximumWidth: visible ? sidebarWidth : 0
Layout.fillHeight: true
radius: 12
color: "#15323d"
ColumnLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 16
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: compactLayout ? 74 : 86
radius: 8
color: "#214654"
Label {
anchors.centerIn: parent
width: parent.width - 18
text: "人脸识别考勤系统"
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
font.pixelSize: compactLayout ? 17 : 19
font.weight: appBoldWeight
color: "#ffffff"
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 8
Repeater {
model: [
{
title: "考勤记录",
hint: "查看筛选与导出",
icon: "01"
},
{
title: "统计分析",
hint: "按时间统计趋势",
icon: "02"
},
{
title: "配置中心",
hint: "时间段与映射设置",
icon: "03"
},
{
title: "人脸管理",
hint: "照片与学号绑定",
icon: "04"
},
{
title: "关于",
hint: "系统说明",
icon: "05"
}
]
delegate: Rectangle {
id: navButton
required property var modelData
required property int index
readonly property bool selected: window.currentPageIndex === navButton.index
Layout.fillWidth: true
Layout.preferredHeight: 74
radius: 8
color: selected ? "#eef7f7" : navMouse.containsMouse ? "#214957" : "#173743"
border.width: 1
border.color: selected ? "#9fd4d7" : navMouse.containsMouse ? "#3b6470" : "#254a55"
Behavior on color {
enabled: !resizingWindow
ColorAnimation {
duration: 120
}
}
Behavior on border.color {
enabled: !resizingWindow
ColorAnimation {
duration: 120
}
}
Rectangle {
anchors.right: parent.right
anchors.rightMargin: 6
anchors.verticalCenter: parent.verticalCenter
width: 4
height: navButton.selected ? 42 : 0
radius: 2
color: tealColor
opacity: navButton.selected ? 1 : 0
Behavior on height {
enabled: !resizingWindow
NumberAnimation {
duration: 150
}
}
Behavior on opacity {
enabled: !resizingWindow
NumberAnimation {
duration: 120
}
}
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 24
spacing: compactLayout ? 10 : 12
Rectangle {
Layout.preferredWidth: 38
Layout.preferredHeight: 38
radius: 8
color: navButton.selected ? tealSoft : "#2a515d"
Label {
anchors.centerIn: parent
text: modelData.icon
color: navButton.selected ? tealColor : "#d6e4e9"
font.weight: appBoldWeight
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
Label {
Layout.fillWidth: true
text: modelData.title
color: navButton.selected ? ink900 : "#f4fbff"
font.pixelSize: compactLayout ? 14 : 15
font.weight: appBoldWeight
horizontalAlignment: Text.AlignLeft
}
Label {
Layout.fillWidth: true
text: modelData.hint
visible: !denseLayout
color: navButton.selected ? ink500 : "#adc4cc"
font.pixelSize: compactLayout ? 10 : 11
horizontalAlignment: Text.AlignLeft
}
}
}
MouseArea {
id: navMouse
anchors.fill: parent
z: 20
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
preventStealing: true
onPressed: window.switchPage(navButton.index)
onClicked: window.switchPage(navButton.index)
}
}
}
}
Item {
Layout.fillHeight: true
}
PrimaryButton {
Layout.fillWidth: true
text: "刷新全部数据"
onClicked: appController.refreshAll()
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: 26
color: "transparent"
ColumnLayout {
anchors.fill: parent
spacing: 16
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: phoneLayout ? 108 : (slimLayout ? 96 : 142)
radius: 8
color: panelStrong
border.color: lineColor
clip: true
RowLayout {
anchors.fill: parent
anchors.margins: slimLayout ? 14 : 20
spacing: 18
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: 6
Label {
Layout.fillWidth: true
text: ["考勤记录总览", "统计分析面板", "配置与同步中心", "人脸资料管理", "关于"][window.currentPageIndex]
font.pixelSize: slimLayout ? 20 : (compactLayout ? 24 : 28)
font.weight: appBoldWeight
color: ink900
elide: Text.ElideRight
}
Label {
visible: !slimLayout
text: ["筛选记录、导出数据并快速查看考勤状态。", "按时间区间查看签到情况,导出统计报表。", "集中管理时间段、映射配置和连接信息。", "同步照片、调整学号映射并执行删除操作。", "了解系统用途、功能范围和运行信息。"][window.currentPageIndex]
color: ink500
font.pixelSize: compactLayout ? 12 : 13
}
Flow {
visible: !slimLayout
Layout.fillWidth: true
spacing: 8
InfoChip {
textValue: ["实时筛选", "区间统计", "配置同步", "人脸映射", "系统说明"][window.currentPageIndex]
tintColor: [accentColor, goldColor, tealColor, accentColor, tealColor][window.currentPageIndex]
}
InfoChip {
textValue: compactLayout ? "紧凑布局" : "桌面布局"
tintColor: tealColor
}
}
}
Flow {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
Layout.preferredWidth: implicitWidth
spacing: compactLayout ? 8 : 10
PrimaryButton {
visible: window.currentPageIndex === 0
text: "导出当前结果"
onClicked: recordExportDialog.open()
}
SecondaryButton {
visible: window.currentPageIndex === 1
text: "导出统计报表"
onClicked: statisticsExportDialog.open()
}
SecondaryButton {
visible: window.currentPageIndex === 2
text: "测试连接"
onClicked: appController.testConnection()
}
SecondaryButton {
visible: window.currentPageIndex === 3
text: "刷新人脸库"
onClicked: appController.refreshFaces()
}
}
}
}
TabBar {
id: navigationBar
visible: slimLayout
Layout.fillWidth: true
Layout.preferredHeight: visible ? 42 : 0
spacing: 6
padding: 0
property real tabWidth: (width - spacing * 4) / 5
currentIndex: window.currentPageIndex
onCurrentIndexChanged: window.switchPage(currentIndex)
background: Item {}
MobileNavTabButton {
width: navigationBar.tabWidth
text: "考勤记录"
}
MobileNavTabButton {
width: navigationBar.tabWidth
text: "统计分析"
}
MobileNavTabButton {
width: navigationBar.tabWidth
text: "配置中心"
}
MobileNavTabButton {
width: navigationBar.tabWidth
text: "人脸管理"
}
MobileNavTabButton {
width: navigationBar.tabWidth
text: "关于"
}
}
StackLayout {
id: pageLayout
// 五个主页面共用 StackLayout:考勤记录、统计分析、配置中心、人脸管理、关于。
Layout.fillWidth: true
Layout.fillHeight: true
currentIndex: window.currentPageIndex
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: StackLayout.isCurrentItem
opacity: StackLayout.isCurrentItem ? 1 : 0
scale: resizingWindow || StackLayout.isCurrentItem ? 1 : 0.985
Behavior on opacity {
enabled: !resizingWindow
NumberAnimation {
duration: 140
}
}
Behavior on scale {
enabled: !resizingWindow
NumberAnimation {
duration: 180
easing.type: Easing.OutCubic
}
}
ColumnLayout {
anchors.fill: parent
spacing: 14
GridLayout {
Layout.fillWidth: true
columns: slimLayout ? 3 : (compactLayout ? 1 : 3)
rowSpacing: 10
columnSpacing: compactLayout ? 10 : 14
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 82
radius: 8
color: panelStrong
border.color: lineColor
clip: true
Column {
anchors.fill: parent
anchors.topMargin: slimLayout ? 12 : 18
anchors.bottomMargin: slimLayout ? 12 : 18
anchors.leftMargin: slimLayout ? 34 : 44
anchors.rightMargin: slimLayout ? 12 : 18
spacing: 6
Label {
text: "总记录数"
color: ink500
}
Label {
text: appController.totalRecords
font.pixelSize: slimLayout ? 22 : (compactLayout ? 30 : 34)
font.weight: appBoldWeight
color: ink900
}
}
Rectangle {
anchors.left: parent.left
anchors.leftMargin: slimLayout ? 12 : 18
anchors.verticalCenter: parent.verticalCenter
width: 6
height: Math.min(parent.height - 18, 64)
radius: 3
color: accentColor
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 82
radius: 8
color: panelStrong
border.color: lineColor
clip: true
Column {
anchors.fill: parent
anchors.topMargin: slimLayout ? 12 : 18
anchors.bottomMargin: slimLayout ? 12 : 18
anchors.leftMargin: slimLayout ? 34 : 44
anchors.rightMargin: slimLayout ? 12 : 18
spacing: 6
Label {
text: "今日签到"
color: ink500
}
Label {
text: appController.todayRecords
font.pixelSize: slimLayout ? 22 : (compactLayout ? 30 : 34)
font.weight: appBoldWeight
color: tealColor
}
}
Rectangle {
anchors.left: parent.left
anchors.leftMargin: slimLayout ? 12 : 18
anchors.verticalCenter: parent.verticalCenter
width: 6
height: Math.min(parent.height - 18, 64)
radius: 3
color: tealColor
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 82
radius: 8
color: panelStrong
border.color: lineColor
clip: true
Column {
anchors.fill: parent
anchors.topMargin: slimLayout ? 12 : 18
anchors.bottomMargin: slimLayout ? 12 : 18
anchors.leftMargin: slimLayout ? 34 : 44
anchors.rightMargin: slimLayout ? 12 : 18
spacing: 6
Label {
text: "筛选结果"
color: ink500
}
Label {
text: appController.filteredCount + " / " + appController.loadedRecords
font.pixelSize: slimLayout ? 20 : (compactLayout ? 26 : 30)
font.weight: appBoldWeight
color: goldColor
}
}
Rectangle {
anchors.left: parent.left
anchors.leftMargin: slimLayout ? 12 : 18
anchors.verticalCenter: parent.verticalCenter
width: 6
height: Math.min(parent.height - 18, 64)
radius: 3
color: goldColor
}
}
}
Rectangle {
Layout.fillWidth: true
radius: 8
color: panelStrong
border.color: lineColor
Layout.preferredHeight: phoneLayout ? (dateFilter.currentIndex === 4 ? 300 : 236) : (slimLayout ? (dateFilter.currentIndex === 4 ? 300 : 220) : 98)
clip: true
GridLayout {
anchors.fill: parent
anchors.margins: slimLayout ? 12 : 14
columns: slimLayout ? 2 : (dateFilter.currentIndex === 4 ? 6 : 5)
columnSpacing: 12
rowSpacing: 12
Column {
Layout.fillWidth: true
Layout.columnSpan: 1
Layout.preferredWidth: slimLayout ? 1 : 170
Layout.minimumWidth: slimLayout ? 0 : 140
spacing: 6
Label {
text: "班级"
color: ink700
font.weight: appBoldWeight
font.pixelSize: 12
}
AppComboBox {
id: classFilter
width: parent.width
model: appController.classOptions
onCurrentTextChanged: applyRecordFilters()
}
}
Column {
Layout.fillWidth: true
Layout.columnSpan: 1
Layout.preferredWidth: slimLayout ? 1 : 170
Layout.minimumWidth: slimLayout ? 0 : 140
spacing: 6
Label {
text: "学号"
color: ink700
font.weight: appBoldWeight
font.pixelSize: 12
}
AppComboBox {
id: studentFilter
width: parent.width
model: appController.studentOptions
onCurrentTextChanged: applyRecordFilters()
}
}
Column {
Layout.fillWidth: true
Layout.columnSpan: 1
Layout.preferredWidth: slimLayout ? 1 : 230
Layout.minimumWidth: slimLayout ? 0 : 180
spacing: 6
Label {
text: "姓名"
color: ink700
font.weight: appBoldWeight
font.pixelSize: 12
}
AppTextField {
id: nameFilter
width: parent.width
placeholderText: "输入姓名关键字"
onTextChanged: applyRecordFilters()
}
}
Column {
Layout.fillWidth: true
Layout.columnSpan: 1
Layout.preferredWidth: slimLayout ? 1 : 170
Layout.minimumWidth: slimLayout ? 0 : 140
spacing: 6
Label {
text: "日期"
color: ink700
font.weight: appBoldWeight
font.pixelSize: 12
}
AppComboBox {
id: dateFilter
width: parent.width
model: ["全部时间", "今天", "最近7天", "最近30天", "指定日期"]
currentIndex: 0
onCurrentIndexChanged: applyRecordFilters()
}
}
Column {
Layout.fillWidth: true
Layout.columnSpan: 1
Layout.preferredWidth: slimLayout ? 1 : 170
Layout.minimumWidth: slimLayout ? 0 : 140
width: dateFilter.currentIndex === 4 ? implicitWidth : 0
height: dateFilter.currentIndex === 4 ? implicitHeight : 0
visible: dateFilter.currentIndex === 4
spacing: 6
Label {
text: "指定日期"
color: ink700
font.weight: appBoldWeight
font.pixelSize: 12
}
AppTextField {
id: specificDateField
width: parent.width
text: Qt.formatDate(new Date(), "yyyy-MM-dd")
placeholderText: "yyyy-MM-dd"
onTextChanged: if (visible)
applyRecordFilters()
}
}
Item {
Layout.columnSpan: slimLayout ? 2 : 1
Layout.fillWidth: slimLayout
Layout.preferredWidth: slimLayout ? 1 : (dateFilter.currentIndex === 4 ? 300 : 330)
Layout.minimumWidth: slimLayout ? 0 : 280
Layout.preferredHeight: slimLayout ? 42 : 64
Layout.alignment: Qt.AlignBottom
GridLayout {
anchors.bottom: parent.bottom
anchors.right: parent.right
width: parent.width
columns: 2
columnSpacing: 8
rowSpacing: 10
SecondaryButton {
Layout.fillWidth: true
Layout.preferredWidth: 1
text: "刷新"
onClicked: appController.refreshRecords()
}
DangerButton {
Layout.fillWidth: true
Layout.preferredWidth: 1
text: "清空记录"
onClicked: clearDialog.open()
}
}
}
}
}
Rectangle {
id: recordTableCard
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: slimLayout ? 220 : 0
radius: 8
color: panelStrong
border.color: lineColor
property real timeColWidth: phoneLayout ? 112 : Math.max(168, Math.min(220, width * 0.21))
property real classColWidth: phoneLayout ? 0 : Math.max(88, Math.min(124, width * 0.11))
property real studentColWidth: phoneLayout ? 64 : Math.max(86, Math.min(116, width * 0.10))
property real nameColWidth: phoneLayout ? 88 : Math.max(118, Math.min(158, width * 0.14))
property real confidenceColWidth: phoneLayout ? 0 : Math.max(96, Math.min(126, width * 0.11))
property real statusColWidth: phoneLayout ? 64 : Math.max(84, Math.min(102, width * 0.09))
property bool showClassColumn: !denseLayout
property bool showConfidenceColumn: !narrowLayout
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 8
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 40
radius: 8
color: "#eef2f6"
RowLayout {
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
spacing: 12
Label {
text: "签到时间"
Layout.preferredWidth: recordTableCard.timeColWidth
font.weight: appBoldWeight
color: ink900
}
Label {
text: "班级"
visible: recordTableCard.showClassColumn
Layout.preferredWidth: recordTableCard.classColWidth
font.weight: appBoldWeight
color: ink900
}
Label {
text: "学号"
Layout.preferredWidth: recordTableCard.studentColWidth
font.weight: appBoldWeight
color: ink900
}
Label {
text: "姓名"
Layout.preferredWidth: recordTableCard.nameColWidth
font.weight: appBoldWeight
color: ink900
}
Label {
text: "置信度"
visible: recordTableCard.showConfidenceColumn
Layout.preferredWidth: recordTableCard.confidenceColWidth
font.weight: appBoldWeight
color: ink900
}
Label {
text: "状态"
Layout.preferredWidth: recordTableCard.statusColWidth
font.weight: appBoldWeight
color: ink900
}
Item {
Layout.fillWidth: true
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
ListView {
id: recordListView
anchors.fill: parent
clip: true
spacing: 4
model: appController.recordModel
delegate: Rectangle {
width: ListView.view.width
height: 44
property bool hovered: recordHoverHandler.hovered
radius: 8
color: hovered ? "#f6f9fc" : (index % 2 === 0 ? "#ffffff" : "#fafbfc")
border.color: hovered ? "#cbd5e1" : "#edf0f3"
Behavior on color {
enabled: !resizingWindow
ColorAnimation {
duration: 120
}
}
Behavior on border.color {
enabled: !resizingWindow
ColorAnimation {
duration: 120
}
}
HoverHandler {
id: recordHoverHandler
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
spacing: 12
Label {
text: model.timestamp
Layout.preferredWidth: recordTableCard.timeColWidth
color: ink900
elide: Text.ElideRight
}
Label {
text: model.className
visible: recordTableCard.showClassColumn
Layout.preferredWidth: recordTableCard.classColWidth
color: ink700
}
Label {
text: model.studentId
Layout.preferredWidth: recordTableCard.studentColWidth
color: ink700
elide: Text.ElideRight
}
Label {
text: model.name
Layout.preferredWidth: recordTableCard.nameColWidth
color: ink900
font.weight: appBoldWeight
elide: Text.ElideRight
}
Label {
text: model.confidenceText
visible: recordTableCard.showConfidenceColumn
Layout.preferredWidth: recordTableCard.confidenceColWidth
color: ink700
}
Rectangle {
Layout.preferredWidth: recordTableCard.statusColWidth
Layout.preferredHeight: 26
radius: 8
color: model.success ? "#dbeedb" : "#f6d9d4"
Label {
anchors.centerIn: parent
text: model.statusText
color: model.success ? successColor : dangerColor
font.weight: appBoldWeight
}
}
Item {
Layout.fillWidth: true
}
}
}
ScrollBar.vertical: AppScrollBar {}
}
EmptyState {
anchors.centerIn: parent
visible: recordListView.count === 0
title: "没有匹配记录"
description: "试着放宽筛选条件,或者先点击顶部按钮刷新最新数据。"
badgeText: "00"
accent: accentColor
}
}
}
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: StackLayout.isCurrentItem
opacity: StackLayout.isCurrentItem ? 1 : 0
scale: resizingWindow || StackLayout.isCurrentItem ? 1 : 0.985
Behavior on opacity {
enabled: !resizingWindow
NumberAnimation {
duration: 140
}
}
Behavior on scale {
enabled: !resizingWindow
NumberAnimation {
duration: 180
easing.type: Easing.OutCubic
}
}
ColumnLayout {
anchors.fill: parent
spacing: 14
Rectangle {
Layout.fillWidth: true
radius: 8
color: panelStrong
border.color: lineColor
Layout.preferredHeight: phoneLayout ? 150 : (slimLayout ? 150 : 78)
clip: true
GridLayout {
id: statisticsFilterGrid
anchors.fill: parent
anchors.margins: slimLayout ? 12 : 18
columns: slimLayout ? 2 : 4
rowSpacing: 10
columnSpacing: 12
AppComboBox {
id: quickStatisticsFilter
Layout.fillWidth: true
Layout.preferredWidth: 1
model: ["今天", "最近7天", "最近30天", "本月"]
currentIndex: 2
onCurrentTextChanged: {
statisticsPreset = currentText;
applyStatisticsPreset();
}
}
AppTextField {
id: statsStartField
Layout.fillWidth: true
Layout.preferredWidth: 1
placeholderText: "开始日期"
onEditingFinished: appController.updateStatisticsRange(text, statsEndField.text)
}
AppTextField {
id: statsEndField
Layout.fillWidth: true
Layout.preferredWidth: 1
placeholderText: "结束日期"
onEditingFinished: appController.updateStatisticsRange(statsStartField.text, text)
}
SecondaryButton {
Layout.fillWidth: true
Layout.preferredWidth: 1
text: "刷新统计"
onClicked: appController.updateStatisticsRange(statsStartField.text, statsEndField.text)
}
}
}
GridLayout {
Layout.fillWidth: true
columns: phoneLayout ? 2 : (slimLayout ? 3 : (compactLayout ? 1 : 3))
rowSpacing: 10
columnSpacing: 14
Rectangle {
Layout.fillWidth: true
Layout.preferredWidth: 1
Layout.preferredHeight: phoneLayout ? 68 : (slimLayout ? 74 : 100)
radius: 8
color: panelStrong
border.color: lineColor
clip: true
Column {
anchors.fill: parent
anchors.topMargin: slimLayout ? 12 : 18
anchors.bottomMargin: slimLayout ? 12 : 18
anchors.leftMargin: slimLayout ? 34 : 44
anchors.rightMargin: slimLayout ? 12 : 18
spacing: 6
Label {
text: "统计学生数"
color: ink500
}
Label {
text: appController.statisticsStudentCount
font.pixelSize: slimLayout ? 22 : (compactLayout ? 28 : 32)
font.weight: appBoldWeight
color: ink900
}
}
Rectangle {
anchors.left: parent.left
anchors.leftMargin: slimLayout ? 12 : 18
anchors.verticalCenter: parent.verticalCenter
width: 6
height: Math.min(parent.height - 28, 64)
radius: 3
color: accentColor
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredWidth: 1
Layout.preferredHeight: phoneLayout ? 68 : (slimLayout ? 74 : 100)
radius: 8
color: panelStrong
border.color: lineColor
clip: true
Column {
anchors.fill: parent
anchors.topMargin: slimLayout ? 12 : 18
anchors.bottomMargin: slimLayout ? 12 : 18
anchors.leftMargin: slimLayout ? 34 : 44
anchors.rightMargin: slimLayout ? 12 : 18
spacing: 6
Label {
text: "统计记录数"
color: ink500
}
Label {
text: appController.statisticsRecordCount
font.pixelSize: slimLayout ? 22 : (compactLayout ? 28 : 32)
font.weight: appBoldWeight
color: tealColor
}
}
Rectangle {
anchors.left: parent.left
anchors.leftMargin: slimLayout ? 12 : 18
anchors.verticalCenter: parent.verticalCenter
width: 6
height: Math.min(parent.height - 28, 64)
radius: 3
color: tealColor
}
}
Rectangle {
Layout.columnSpan: phoneLayout ? 2 : 1
Layout.fillWidth: true
Layout.preferredWidth: 1
Layout.preferredHeight: phoneLayout ? 68 : (slimLayout ? 74 : 100)
radius: 8
color: panelStrong
border.color: lineColor
clip: true
Column {
anchors.fill: parent
anchors.topMargin: slimLayout ? 12 : 18
anchors.bottomMargin: slimLayout ? 12 : 18
anchors.leftMargin: slimLayout ? 34 : 44
anchors.rightMargin: slimLayout ? 12 : 18
spacing: 6
Label {
text: "统计范围"
color: ink500
}
Label {
text: appController.statisticsRangeText
font.pixelSize: slimLayout ? 14 : (compactLayout ? 18 : 20)
font.weight: appBoldWeight
color: goldColor
elide: Text.ElideRight
}
}
Rectangle {
anchors.left: parent.left
anchors.leftMargin: slimLayout ? 12 : 18
anchors.verticalCenter: parent.verticalCenter
width: 6
height: Math.min(parent.height - 28, 64)
radius: 3
color: goldColor
}
}
}
Rectangle {
id: statisticsTableCard
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: slimLayout ? 220 : 0
radius: 8
color: panelStrong
border.color: lineColor
property real studentColWidth: phoneLayout ? 68 : Math.max(90, Math.min(128, width * 0.12))
property real nameColWidth: phoneLayout ? 96 : Math.max(128, Math.min(188, width * 0.17))
property real totalColWidth: phoneLayout ? 92 : Math.max(96, Math.min(124, width * 0.12))
property real todayColWidth: Math.max(92, Math.min(110, width * 0.10))
property real lastColWidth: Math.max(180, Math.min(240, width * 0.20))
property real avgColWidth: Math.max(98, Math.min(126, width * 0.11))
property bool showTodayColumn: !denseLayout
property bool showLastColumn: !narrowLayout
property bool showAverageColumn: !phoneLayout
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 8
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 40
radius: 8
color: "#eef2f6"
RowLayout {
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
spacing: 12
Label {
text: "学号"
Layout.preferredWidth: statisticsTableCard.studentColWidth
font.weight: appBoldWeight
}
Label {
text: "姓名"
Layout.preferredWidth: statisticsTableCard.nameColWidth
font.weight: appBoldWeight
}
Label {
text: "总签到次数"
Layout.preferredWidth: statisticsTableCard.totalColWidth
font.weight: appBoldWeight
}
Label {
text: "今日签到"
visible: statisticsTableCard.showTodayColumn
Layout.preferredWidth: statisticsTableCard.todayColWidth
font.weight: appBoldWeight
}
Label {
text: "最后签到时间"
visible: statisticsTableCard.showLastColumn
Layout.preferredWidth: statisticsTableCard.lastColWidth
font.weight: appBoldWeight
}
Label {
text: "平均置信度"
visible: statisticsTableCard.showAverageColumn
Layout.preferredWidth: statisticsTableCard.avgColWidth
font.weight: appBoldWeight
}
Item {
Layout.fillWidth: true
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
ListView {
id: statisticsListView
anchors.fill: parent
clip: true
spacing: 8
model: appController.statisticsModel
delegate: Rectangle {
width: ListView.view.width
height: 52
property bool hovered: statisticsHoverHandler.hovered
radius: 16
color: hovered ? "#f7f5e8" : (index % 2 === 0 ? "#ffffff" : "#f8f3ec")
border.color: hovered ? "#e4d6aa" : (index % 2 === 0 ? "#ece4da" : "#e7ded3")
Behavior on color {
enabled: !resizingWindow
ColorAnimation {
duration: 120
}
}
Behavior on border.color {
enabled: !resizingWindow
ColorAnimation {
duration: 120
}
}
HoverHandler {
id: statisticsHoverHandler
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
spacing: 12
Label {
text: model.studentId
Layout.preferredWidth: statisticsTableCard.studentColWidth
color: ink700
}
Label {
text: model.name
Layout.preferredWidth: statisticsTableCard.nameColWidth
color: ink900
font.weight: appBoldWeight
}
Label {
text: model.totalCount
Layout.preferredWidth: statisticsTableCard.totalColWidth
color: ink700
}
Label {
text: model.todayCount
visible: statisticsTableCard.showTodayColumn
Layout.preferredWidth: statisticsTableCard.todayColWidth
color: ink700
}
Label {
text: model.lastCheckIn
visible: statisticsTableCard.showLastColumn
Layout.preferredWidth: statisticsTableCard.lastColWidth
color: ink700
}
Label {
text: model.averageConfidenceText
visible: statisticsTableCard.showAverageColumn
Layout.preferredWidth: statisticsTableCard.avgColWidth
color: tealColor
font.weight: appBoldWeight
}
Item {
Layout.fillWidth: true
}
}
}
ScrollBar.vertical: AppScrollBar {}
}
EmptyState {
anchors.centerIn: parent
visible: statisticsListView.count === 0
title: "统计范围内没有数据"
description: "可以切换时间范围,或者先返回记录页检查同步是否正常。"
badgeText: "Σ"
accent: goldColor
}
}
}
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: StackLayout.isCurrentItem
opacity: StackLayout.isCurrentItem ? 1 : 0
scale: resizingWindow || StackLayout.isCurrentItem ? 1 : 0.985
Behavior on opacity {
enabled: !resizingWindow
NumberAnimation {
duration: 140
}
}
Behavior on scale {
enabled: !resizingWindow
NumberAnimation {
duration: 180
easing.type: Easing.OutCubic
}
}
ScrollView {
id: configScrollView
anchors.fill: parent
clip: true
contentWidth: availableWidth
contentHeight: configContentLayout.implicitHeight
ColumnLayout {
id: configContentLayout
width: configScrollView.availableWidth
spacing: 14
Rectangle {
Layout.fillWidth: true
radius: 8
color: panelStrong
border.color: lineColor
Layout.preferredHeight: serverConfigGrid.implicitHeight + (slimLayout ? 24 : 36)
clip: true
GridLayout {
id: serverConfigGrid
anchors.fill: parent
anchors.margins: slimLayout ? 12 : 18
columns: slimLayout ? 1 : 8
rowSpacing: 12
columnSpacing: 12
Label {
Layout.alignment: Qt.AlignVCenter
text: "服务器地址"
font.weight: appBoldWeight
color: ink700
}
AppTextField {
id: serverUrlField
Layout.fillWidth: true
Layout.preferredWidth: slimLayout ? 0 : 360
Layout.columnSpan: slimLayout ? 1 : 2
text: appController.serverUrl
placeholderText: "http://IP:端口"
}
Label {
Layout.alignment: Qt.AlignVCenter
text: "API Token"
font.weight: appBoldWeight
color: ink700
}
AppTextField {
id: apiTokenField
Layout.fillWidth: true
Layout.preferredWidth: slimLayout ? 0 : 300
Layout.columnSpan: slimLayout ? 1 : 2
text: appController.apiToken
placeholderText: "服务端访问令牌"
echoMode: TextInput.Password
passwordCharacter: "*"
}
SecondaryButton {
Layout.fillWidth: slimLayout
text: "保存"
onClicked: appController.saveServerConfig(serverUrlField.text, apiTokenField.text)
}
PrimaryButton {
Layout.fillWidth: slimLayout
text: "测试连接"
fillColor: tealColor
onClicked: appController.testConnection()
}
}
}
Rectangle {
Layout.fillWidth: true
radius: 8
color: panelStrong
border.color: lineColor
Layout.preferredHeight: timeSlotCardLayout.implicitHeight + 36
clip: true
ColumnLayout {
id: timeSlotCardLayout
anchors.fill: parent
anchors.margins: 18
spacing: 12
ColumnLayout {
Layout.fillWidth: true
spacing: 12
RowLayout {
Layout.fillWidth: true
spacing: 12
SectionHeader {
Layout.fillWidth: true
title: "时间段配置"
description: "维护签到与签退时段,并同步到服务端。"
accent: accentColor
}
SecondaryButton {
Layout.preferredWidth: phoneLayout ? 76 : 88
Layout.alignment: Qt.AlignTop
text: timeSlotsExpanded ? "收起" : "展开"
onClicked: timeSlotsExpanded = !timeSlotsExpanded
}
}
GridLayout {
Layout.fillWidth: true
columns: slimLayout ? 2 : 4
columnSpacing: 8
rowSpacing: 8
SecondaryButton {
Layout.fillWidth: true
visible: timeSlotsExpanded
text: "新增时段"
onClicked: openTimeSlotDialog(-1)
}
SecondaryButton {
Layout.fillWidth: true
visible: timeSlotsExpanded
text: "清空时段"
onClicked: {
appController.timeSlotModel.resetToDefault();
appController.saveTimeSlots();
}
}
SecondaryButton {
Layout.fillWidth: true
visible: timeSlotsExpanded
text: slimLayout ? "同步" : "从服务器同步"
onClicked: appController.reloadTimeSlotsFromServer()
}
PrimaryButton {
Layout.fillWidth: true
visible: timeSlotsExpanded
text: slimLayout ? "保存" : "保存时间段"
onClicked: appController.saveTimeSlots()
}
}
}
Rectangle {
visible: timeSlotsExpanded && !phoneLayout
Layout.fillWidth: true
Layout.preferredHeight: visible ? 42 : 0
radius: 8
color: "#efe7dd"
RowLayout {
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
Label {
text: "名称"
Layout.preferredWidth: 220
font.weight: appBoldWeight
}
Label {
text: "开始"
Layout.preferredWidth: 120
font.weight: appBoldWeight
}
Label {
text: "结束"
Layout.preferredWidth: 120
font.weight: appBoldWeight
}
Label {
text: "状态"
Layout.preferredWidth: 100
font.weight: appBoldWeight
}
Item {
Layout.fillWidth: true
}
}
}
ListView {
id: timeSlotListView
visible: timeSlotsExpanded
Layout.fillWidth: true
Layout.preferredHeight: visible ? Math.max(phoneLayout ? 96 : 52, contentHeight) : 0
clip: false
interactive: false
spacing: 8
model: appController.timeSlotModel
// 桌面端和手机端共用同一份时间段模型,只在 delegate 内切换行布局。
delegate: Rectangle {
width: ListView.view.width
height: phoneLayout ? 96 : 52
property bool hovered: timeSlotHoverHandler.hovered
radius: 16
color: hovered ? "#f9f1ea" : (index % 2 === 0 ? "#ffffff" : "#f8f3ec")
border.color: hovered ? "#e5cfc1" : (index % 2 === 0 ? "#ece4da" : "#e7ded3")
Behavior on color {
enabled: !resizingWindow
ColorAnimation {
duration: 120
}
}
Behavior on border.color {
enabled: !resizingWindow
ColorAnimation {
duration: 120
}
}
HoverHandler {
id: timeSlotHoverHandler
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
visible: !phoneLayout
spacing: 12
// 桌面布局按表格列对齐,便于快速比较多个时间段的起止时间。
Label {
text: model.name
Layout.preferredWidth: 220
}
Label {
text: model.startTime
Layout.preferredWidth: 120
}
Label {
text: model.endTime
Layout.preferredWidth: 120
}
Label {
text: model.enabled ? "启用" : "禁用"
Layout.preferredWidth: 100
color: model.enabled ? successColor : goldColor
font.weight: appBoldWeight
}
Item {
Layout.fillWidth: true
}
SecondaryButton {
text: "编辑"
onClicked: openTimeSlotDialog(index)
}
SecondaryButton {
text: "删除"
onClicked: {
appController.timeSlotModel.removeTimeSlot(index);
appController.saveTimeSlots();
}
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
visible: phoneLayout
spacing: 8
// 手机布局拆成两行,避免名称、时间和按钮在窄屏上互相挤压。
RowLayout {
Layout.fillWidth: true
spacing: 8
Label {
text: model.name
Layout.fillWidth: true
color: ink900
font.pixelSize: 15
font.weight: appBoldWeight
elide: Text.ElideRight
}
Label {
text: model.enabled ? "启用" : "禁用"
color: model.enabled ? successColor : goldColor
font.pixelSize: 12
font.weight: appBoldWeight
}
}
RowLayout {
Layout.fillWidth: true
spacing: 8
Label {
text: model.startTime + " - " + model.endTime
Layout.fillWidth: true
color: ink700
font.pixelSize: 13
}
SecondaryButton {
Layout.preferredWidth: 76
text: "编辑"
onClicked: openTimeSlotDialog(index)
}
SecondaryButton {
Layout.preferredWidth: 76
text: "删除"
onClicked: {
appController.timeSlotModel.removeTimeSlot(index);
appController.saveTimeSlots();
}
}
}
}
}
}
}
}
Rectangle {
Layout.fillWidth: true
radius: 8
color: panelStrong
border.color: lineColor
Layout.preferredHeight: mappingCardLayout.implicitHeight + 36
clip: true
ColumnLayout {
id: mappingCardLayout
anchors.fill: parent
anchors.margins: 18
spacing: 12
ColumnLayout {
Layout.fillWidth: true
spacing: 12
RowLayout {
Layout.fillWidth: true
spacing: 12
SectionHeader {
Layout.fillWidth: true
title: "班级管理"
description: "新建、命名和删除班级;学生归属在人脸管理的编辑窗口中维护。"
accent: tealColor
}
SecondaryButton {
Layout.preferredWidth: phoneLayout ? 76 : 88
Layout.alignment: Qt.AlignTop
text: mappingsExpanded ? "收起" : "展开"
onClicked: mappingsExpanded = !mappingsExpanded
}
}
GridLayout {
Layout.fillWidth: true
columns: phoneLayout ? 1 : 4
columnSpacing: 8
rowSpacing: 8
SecondaryButton {
Layout.fillWidth: true
visible: mappingsExpanded
text: phoneLayout ? "加载" : "从服务器加载班级"
onClicked: appController.loadMappingsFromServer()
}
PrimaryButton {
Layout.fillWidth: true
visible: mappingsExpanded
text: "保存班级"
fillColor: tealColor
onClicked: appController.saveMappings()
}
SecondaryButton {
Layout.fillWidth: phoneLayout
Layout.preferredWidth: phoneLayout ? -1 : 120
visible: mappingsExpanded
text: "新增班级"
onClicked: {
appController.classMappingModel.addEntry("", "");
markMappingsDirty();
}
}
Label {
Layout.fillWidth: phoneLayout
Layout.alignment: Qt.AlignVCenter
visible: mappingsExpanded
text: mappingsDirty ? "有未保存改动" : "配置已保存"
color: mappingsDirty ? goldColor : successColor
font.pixelSize: 12
font.weight: appBoldWeight
}
}
}
ListView {
id: classMappingListView
visible: mappingsExpanded
Layout.fillWidth: true
Layout.preferredHeight: visible ? Math.max(phoneLayout ? 112 : 64, contentHeight) : 0
clip: false
interactive: false
spacing: 8
model: appController.classMappingModel
// 班级列表直接编辑模型,保存按钮再统一把完整配置同步到服务端。
delegate: Rectangle {
width: ListView.view.width
height: phoneLayout ? 112 : 56
property bool hovered: classMappingHoverHandler.hovered
radius: 8
color: hovered ? "#eef5f4" : (index % 2 === 0 ? "#ffffff" : "#f8f3ec")
border.color: hovered ? "#c7dddd" : (index % 2 === 0 ? "#ece4da" : "#e7ded3")
Behavior on color {
enabled: !resizingWindow
ColorAnimation { duration: 120 }
}
Behavior on border.color {
enabled: !resizingWindow
ColorAnimation { duration: 120 }
}
HoverHandler {
id: classMappingHoverHandler
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
visible: !phoneLayout
spacing: 12
// 桌面端让编号和名称保持两列,方便核对 class_id 与 class_name。
AppTextField {
id: classKeyField
Layout.preferredWidth: 220
text: model.entryKey
placeholderText: "班级编号"
onEditingFinished: {
appController.classMappingModel.updateEntry(index, text, classValueField.text);
markMappingsDirty();
}
}
AppTextField {
id: classValueField
Layout.preferredWidth: 260
text: model.entryValue
placeholderText: "班级名称"
onEditingFinished: {
appController.classMappingModel.updateEntry(index, classKeyField.text, text);
markMappingsDirty();
}
}
Item {
Layout.fillWidth: true
}
SecondaryButton {
text: "删除"
onClicked: {
if (appController.removeClassMapping(index))
markMappingsDirty();
}
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
visible: phoneLayout
spacing: 8
// 窄屏下将编号、名称、删除按钮分行显示,避免输入框宽度过小。
AppTextField {
id: classKeyFieldPhone
Layout.fillWidth: true
text: model.entryKey
placeholderText: "班级编号"
onEditingFinished: {
appController.classMappingModel.updateEntry(index, text, classValueFieldPhone.text);
markMappingsDirty();
}
}
RowLayout {
Layout.fillWidth: true
spacing: 8
AppTextField {
id: classValueFieldPhone
Layout.fillWidth: true
text: model.entryValue
placeholderText: "班级名称"
onEditingFinished: {
appController.classMappingModel.updateEntry(index, classKeyFieldPhone.text, text);
markMappingsDirty();
}
}
SecondaryButton {
Layout.preferredWidth: 76
text: "删除"
onClicked: {
if (appController.removeClassMapping(index))
markMappingsDirty();
}
}
}
}
}
}
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 14
}
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: StackLayout.isCurrentItem
opacity: StackLayout.isCurrentItem ? 1 : 0
scale: resizingWindow || StackLayout.isCurrentItem ? 1 : 0.985
Behavior on opacity {
enabled: !resizingWindow
NumberAnimation {
duration: 140
}
}
Behavior on scale {
enabled: !resizingWindow
NumberAnimation {
duration: 180
easing.type: Easing.OutCubic
}
}
ColumnLayout {
anchors.fill: parent
spacing: 14
Rectangle {
Layout.fillWidth: true
radius: 8
color: panelStrong
border.color: lineColor
Layout.preferredHeight: phoneLayout ? 170 : (slimLayout ? 132 : 88)
clip: true
GridLayout {
anchors.fill: parent
anchors.margins: slimLayout ? 12 : 18
columns: phoneLayout ? 1 : 2
rowSpacing: 12
columnSpacing: 12
SectionHeader {
Layout.fillWidth: true
title: "K210 人脸照片与档案绑定"
description: "同步照片状态,维护人脸、学号、姓名和班级信息。"
accent: goldColor
}
Flow {
Layout.fillWidth: phoneLayout
Layout.alignment: phoneLayout ? Qt.AlignLeft : (Qt.AlignRight | Qt.AlignVCenter)
spacing: 8
SecondaryButton {
text: "刷新"
onClicked: appController.refreshFaces()
}
PrimaryButton {
text: "保存档案"
onClicked: appController.saveFaceMappings()
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: 8
color: panelStrong
border.color: lineColor
GridView {
id: faceGridView
anchors.fill: parent
anchors.margins: 16
clip: true
property int cardColumns: phoneLayout ? 1 : Math.max(1, Math.floor(width / (compactLayout ? 236 : 260)))
cellWidth: Math.max(phoneLayout ? 280 : 240, Math.floor(width / cardColumns))
cellHeight: phoneLayout ? 444 : 404
model: appController.faceModel
// 人脸卡片展示的是控制器合并后的结果:照片缓存、绑定学号、学生档案和班级名称。
delegate: Rectangle {
width: Math.min(phoneLayout ? 300 : 244, GridView.view.cellWidth - 18)
height: phoneLayout ? 420 : 380
property bool hovered: faceHoverHandler.hovered
radius: 8
color: hovered ? "#fff6ef" : "#ffffff"
border.color: hovered ? "#e6c8b8" : "#e8ddd0"
Behavior on color {
enabled: !resizingWindow
ColorAnimation {
duration: 140
}
}
Behavior on border.color {
enabled: !resizingWindow
ColorAnimation {
duration: 140
}
}
HoverHandler {
id: faceHoverHandler
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 14
spacing: 10
Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.preferredHeight: phoneLayout ? 128 : (compactLayout ? 132 : 148)
radius: 8
color: model.hasPhoto ? "#eef3f5" : goldSoft
border.color: model.hasPhoto ? "#dbe4ea" : "#eadbb0"
// 照片由 C++ 下载到本地缓存后以 file:// URL 暴露,QML 只负责显示。
Image {
anchors.fill: parent
anchors.margins: 8
fillMode: Image.PreserveAspectFit
source: model.photoUrl
visible: model.hasPhoto
}
Label {
anchors.centerIn: parent
visible: !model.hasPhoto
text: "无照片"
color: goldColor
font.weight: appBoldWeight
}
}
Label {
text: "人脸编号: " + model.faceId
color: ink900
font.pixelSize: compactLayout ? 15 : 16
font.weight: appBoldWeight
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Label {
Layout.fillWidth: true
text: model.studentId.length > 0 ? "学号: " + model.studentId : "学号: 未绑定"
color: model.studentId.length > 0 ? ink700 : goldColor
font.pixelSize: 12
font.weight: model.studentId.length > 0 ? appBoldWeight : Font.Normal
elide: Text.ElideRight
}
Label {
Layout.fillWidth: true
text: model.studentName.length > 0 ? "姓名: " + model.studentName : "姓名: 未建档"
color: model.studentName.length > 0 ? ink700 : dangerColor
font.pixelSize: 12
elide: Text.ElideRight
}
Label {
Layout.fillWidth: true
text: model.className.length > 0 ? "班级: " + model.className : "班级: 未设置"
color: model.className.length > 0 ? ink700 : ink500
font.pixelSize: 12
elide: Text.ElideRight
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 34
radius: 8
color: model.studentId.length === 0 ? "#f9ebcf" : (model.studentName.length > 0 ? "#e6f4ec" : "#f9ebcf")
Label {
anchors.centerIn: parent
text: model.studentId.length === 0 ? "未绑定学生" : (model.studentName.length > 0 ? "已绑定学生档案" : "待补全学生信息")
color: model.studentId.length === 0 ? goldColor : (model.studentName.length > 0 ? successColor : goldColor)
font.pixelSize: narrowLayout ? 11 : 12
font.weight: appBoldWeight
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 30
radius: 8
color: model.hasPhoto ? "#eef5f4" : "#f9ebcf"
Label {
anchors.centerIn: parent
text: model.hasPhoto ? "照片已同步" : "无照片"
color: model.hasPhoto ? tealColor : goldColor
font.pixelSize: narrowLayout ? 11 : 12
font.weight: appBoldWeight
}
}
Item {
Layout.fillHeight: true
}
RowLayout {
Layout.fillWidth: true
SecondaryButton {
Layout.fillWidth: true
text: "编辑"
onClicked: openFaceEditor(model.faceId, model.studentId, model.studentName, model.classId)
}
DangerButton {
Layout.fillWidth: true
text: "删除"
onClicked: {
// 删除操作先记录当前卡片的人脸编号,再交给确认弹窗决定是否连带删除考勤记录。
pendingDeleteFaceId = model.faceId;
deleteFaceRecordsCheck.checked = false;
deleteFaceDialog.open();
}
}
}
}
}
ScrollBar.vertical: AppScrollBar {}
}
EmptyState {
anchors.centerIn: parent
visible: faceGridView.count === 0
title: "还没有可管理的人脸数据"
description: "等 K210 上传照片或服务端同步学号后,这里会自动显示对应卡片。"
badgeText: "人"
accent: tealColor
}
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: StackLayout.isCurrentItem
opacity: StackLayout.isCurrentItem ? 1 : 0
scale: resizingWindow || StackLayout.isCurrentItem ? 1 : 0.985
Behavior on opacity {
enabled: !resizingWindow
NumberAnimation {
duration: 140
}
}
Behavior on scale {
enabled: !resizingWindow
NumberAnimation {
duration: 180
easing.type: Easing.OutCubic
}
}
Rectangle {
anchors.fill: parent
radius: 8
color: panelStrong
border.color: lineColor
clip: true
ScrollView {
id: aboutScrollView
anchors.fill: parent
anchors.margins: phoneLayout ? 16 : 24
clip: true
contentWidth: availableWidth
ColumnLayout {
id: aboutContentLayout
width: aboutScrollView.availableWidth
spacing: phoneLayout ? 12 : 18
SectionHeader {
Layout.fillWidth: true
title: "人脸识别考勤系统"
description: "用于连接考勤服务端,集中查看签到记录、统计考勤情况、维护签到时段、管理名称映射与人脸照片绑定。"
accent: tealColor
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: lineColor
}
Label {
Layout.fillWidth: true
text: "本客户端基于 Qt Quick/QML 构建,面向人脸识别考勤场景。它把记录查询、统计导出、服务连接配置、时间段配置、学生与班级名称映射、人脸资料管理整合在一个窗口中,减少多窗口切换。"
wrapMode: Text.WordWrap
color: ink700
font.pixelSize: 14
lineHeight: 1.25
}
Label {
Layout.fillWidth: true
text: "数据通过服务端 API 同步;记录和统计结果可导出为 CSV。"
wrapMode: Text.WordWrap
color: ink700
font.pixelSize: 14
lineHeight: 1.25
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: thesisInfoTitle.implicitHeight + thesisInfoBody.paintedHeight + thesisInfoLayout.spacing + 32
Layout.minimumHeight: phoneLayout ? 204 : 184
radius: 8
color: "#f8fafb"
border.color: lineColor
clip: false
ColumnLayout {
id: thesisInfoLayout
anchors.fill: parent
anchors.margins: 16
spacing: 8
Label {
id: thesisInfoTitle
Layout.fillWidth: true
text: "毕业设计说明"
color: ink900
font.pixelSize: 16
font.weight: appBoldWeight
}
Label {
id: thesisInfoBody
Layout.fillWidth: true
Layout.preferredHeight: paintedHeight
text: "本系统为纪田野的个人毕业设计作品,仅用于毕业设计展示、学习研究与系统功能验证。\n\nCopyright © 2026 纪田野。保留所有权利。\n\n仓库地址:https://git.yzfh.eu.cc/yzfh/qt.git\n\n演示账号:jtybishe 密码:jtybishe"
wrapMode: Text.WrapAnywhere
color: ink700
font.pixelSize: 12
lineHeight: 1.25
}
}
}
}
ScrollBar.vertical: AppScrollBar {}
}
}
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
}
}
}
}
}
Rectangle {
width: Math.min(420, Math.max(220, toastLabel.implicitWidth + 44))
height: 52
radius: 16
color: "#16323d"
border.color: "#264754"
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: 30
anchors.bottomMargin: 28
opacity: toastVisible ? 0.98 : 0
visible: opacity > 0
y: toastVisible ? parent.height - height - 28 : parent.height - height - 10
Behavior on opacity {
enabled: !resizingWindow
NumberAnimation {
duration: 180
}
}
Behavior on y {
enabled: !resizingWindow
NumberAnimation {
duration: 180
easing.type: Easing.OutCubic
}
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
spacing: 10
Rectangle {
Layout.preferredWidth: 10
Layout.preferredHeight: 10
radius: 5
color: accentColor
}
Label {
id: toastLabel
text: toastMessage
color: "#f8fafc"
elide: Label.ElideRight
Layout.fillWidth: true
}
}
}
}