import QtQuick import QtQuick.Controls import QtQuick.Layouts import QtQuick.Dialogs ApplicationWindow { id: window width: 1560 height: 920 minimumWidth: 900 minimumHeight: 620 visible: true title: "人脸识别考勤系统" color: "#f6f7f9" 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 string toastMessage: "" property bool toastVisible: false property bool resizingWindow: false property int currentPageIndex: 0 property bool timeSlotsExpanded: false property bool mappingsExpanded: false 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 < 560 readonly property int sidebarWidth: slimLayout ? 0 : (compactLayout ? 212 : 240) font.family: appFontFamily 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 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 { text: control.text horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter color: control.labelColor font.pixelSize: 13 font.weight: appBoldWeight } } 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 { text: control.text horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter color: control.labelColor font.pixelSize: 13 font.weight: appBoldWeight } } 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 } } 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) 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) } function openTimeSlotDialog(index) { editingTimeSlotIndex = index if (index >= 0) { 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() } 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() } Component.onCompleted: { applyRecordFilters() applyStatisticsPreset() } Connections { target: appController function onStatusMessageChanged() { if (!appController.statusMessage || appController.statusMessage.length === 0) { return } 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)"] currentFile: "attendance_export.csv" onAccepted: appController.exportFilteredRecords(selectedFile) } FileDialog { id: statisticsExportDialog fileMode: FileDialog.SaveFile nameFilters: ["CSV 文件 (*.csv)"] currentFile: "attendance_statistics.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 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: "确定删除 ID 为 " + pendingDeleteFaceId + " 的人脸信息吗?" wrapMode: Text.WordWrap font.pixelSize: 18 font.weight: appBoldWeight color: ink900 } AppCheckBox { id: deleteFaceRecordsCheck text: "同时删除该学生的所有考勤记录" checked: false } } onAccepted: appController.deleteFace(pendingDeleteFaceId, deleteFaceRecordsCheck.checked) } 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 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 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 ? 236 : (slimLayout ? 174 : 92) 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.preferredWidth: phoneLayout ? 1 : 150 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.preferredWidth: phoneLayout ? 1 : 160 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.preferredWidth: phoneLayout ? 1 : 190 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.preferredWidth: phoneLayout ? 1 : 160 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.preferredWidth: phoneLayout ? 1 : 150 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() } } GridLayout { Layout.columnSpan: slimLayout ? 2 : 1 Layout.fillWidth: true Layout.alignment: Qt.AlignBottom columns: phoneLayout ? 2 : 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 : 5 rowSpacing: 10 columnSpacing: 12 AppComboBox { id: quickStatisticsFilter Layout.fillWidth: true Layout.preferredWidth: phoneLayout ? 1 : 150 model: ["今天", "最近7天", "最近30天", "本月"] currentIndex: 2 onCurrentTextChanged: { statisticsPreset = currentText applyStatisticsPreset() } } AppTextField { id: statsStartField Layout.fillWidth: true Layout.preferredWidth: phoneLayout ? 1 : 150 placeholderText: "开始日期" onEditingFinished: appController.updateStatisticsRange(text, statsEndField.text) } AppTextField { id: statsEndField Layout.fillWidth: true Layout.preferredWidth: phoneLayout ? 1 : 150 placeholderText: "结束日期" onEditingFinished: appController.updateStatisticsRange(statsStartField.text, text) } SecondaryButton { Layout.fillWidth: true Layout.preferredWidth: phoneLayout ? 1 : implicitWidth 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: phoneLayout ? 1 : 100 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: phoneLayout ? 1 : 100 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.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: phoneLayout ? 218 : (slimLayout ? 174 : 88) clip: true GridLayout { id: serverConfigGrid anchors.fill: parent anchors.margins: slimLayout ? 12 : 18 columns: slimLayout ? 1 : 4 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 : 520 text: appController.serverUrl placeholderText: "http://IP:端口" } SecondaryButton { Layout.fillWidth: phoneLayout text: "保存地址" onClicked: appController.serverUrl = serverUrlField.text } PrimaryButton { Layout.fillWidth: phoneLayout text: "测试连接" fillColor: tealColor onClicked: appController.testConnection() } } } Rectangle { Layout.fillWidth: true radius: 8 color: panelStrong border.color: lineColor Layout.preferredHeight: timeSlotsExpanded ? 18 + 64 + 12 + 42 + 12 + Math.max(52, timeSlotListView.contentHeight) + 18 : 100 clip: true ColumnLayout { anchors.fill: parent anchors.margins: 18 spacing: 12 RowLayout { Layout.fillWidth: true Layout.preferredHeight: 64 spacing: 12 SectionHeader { Layout.fillWidth: true title: "时间段配置" description: "维护签到与签退时段,并同步到服务端。" accent: accentColor } Row { Layout.alignment: Qt.AlignRight | Qt.AlignVCenter spacing: 8 SecondaryButton { text: timeSlotsExpanded ? "收起" : "展开" onClicked: timeSlotsExpanded = !timeSlotsExpanded } SecondaryButton { visible: timeSlotsExpanded text: "新增时段" onClicked: openTimeSlotDialog(-1) } SecondaryButton { visible: timeSlotsExpanded text: "恢复默认" onClicked: { appController.timeSlotModel.resetToDefault(); appController.saveTimeSlots() } } SecondaryButton { visible: timeSlotsExpanded text: "从服务器同步" onClicked: appController.reloadTimeSlotsFromServer() } PrimaryButton { visible: timeSlotsExpanded text: "保存时间段" onClicked: appController.saveTimeSlots() } } } Rectangle { visible: timeSlotsExpanded 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 ? contentHeight : 0 clip: false interactive: false spacing: 8 model: appController.timeSlotModel delegate: Rectangle { width: ListView.view.width height: 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 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() } } } } } } } Rectangle { Layout.fillWidth: true radius: 8 color: panelStrong border.color: lineColor Layout.preferredHeight: mappingsExpanded ? 18 + 64 + 12 + mappingTabs.implicitHeight + 12 + Math.max(64, mappingTabs.currentIndex === 0 ? studentMappingListView.contentHeight : classMappingListView.contentHeight) + 18 : 100 clip: true ColumnLayout { anchors.fill: parent anchors.margins: 18 spacing: 12 RowLayout { Layout.fillWidth: true Layout.preferredHeight: 64 spacing: 12 SectionHeader { Layout.fillWidth: true title: "名称映射配置" description: "让学号和班级编号显示为更清晰的真实名称。" accent: tealColor } Row { Layout.alignment: Qt.AlignRight | Qt.AlignVCenter spacing: 8 SecondaryButton { text: mappingsExpanded ? "收起" : "展开" onClicked: mappingsExpanded = !mappingsExpanded } SecondaryButton { visible: mappingsExpanded text: "从服务器加载" onClicked: appController.loadMappingsFromServer() } PrimaryButton { visible: mappingsExpanded text: "保存映射" fillColor: tealColor onClicked: appController.saveMappings() } } } TabBar { id: mappingTabs visible: mappingsExpanded Layout.fillWidth: true Layout.preferredHeight: visible ? 42 : 0 spacing: 8 padding: 0 background: Rectangle { color: "transparent" } SegmentTabButton { width: (mappingTabs.width - mappingTabs.spacing) / 2 text: "学号 → 姓名" } SegmentTabButton { width: (mappingTabs.width - mappingTabs.spacing) / 2 text: "班级编号 → 名称" } } StackLayout { visible: mappingsExpanded Layout.fillWidth: true Layout.preferredHeight: visible ? (mappingTabs.currentIndex === 0 ? studentMappingListView.contentHeight : classMappingListView.contentHeight) : 0 currentIndex: mappingTabs.currentIndex Rectangle { color: "transparent" ListView { id: studentMappingListView anchors.fill: parent clip: false interactive: false spacing: 8 model: appController.studentMappingModel header: RowLayout { width: ListView.view.width spacing: 12 SecondaryButton { text: "新增映射" onClicked: appController.studentMappingModel.addEntry("", "") } Item { Layout.fillWidth: true } } delegate: Rectangle { width: ListView.view.width height: 56 property bool hovered: studentMappingHoverHandler.hovered radius: 16 color: hovered ? "#f9f0ea" : (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: studentMappingHoverHandler } RowLayout { anchors.fill: parent anchors.leftMargin: 12 anchors.rightMargin: 12 spacing: 12 AppTextField { id: studentKeyField Layout.preferredWidth: 220 text: model.entryKey placeholderText: "学号" onEditingFinished: appController.studentMappingModel.updateEntry(index, text, studentValueField.text) } AppTextField { id: studentValueField Layout.preferredWidth: 240 text: model.entryValue placeholderText: "真实姓名" onEditingFinished: appController.studentMappingModel.updateEntry(index, studentKeyField.text, text) } Item { Layout.fillWidth: true } SecondaryButton { text: "删除"; onClicked: appController.studentMappingModel.removeEntry(index) } } } } } Rectangle { color: "transparent" ListView { id: classMappingListView anchors.fill: parent clip: false interactive: false spacing: 8 model: appController.classMappingModel header: RowLayout { width: ListView.view.width spacing: 12 SecondaryButton { text: "新增映射" onClicked: appController.classMappingModel.addEntry("", "") } Item { Layout.fillWidth: true } } delegate: Rectangle { width: ListView.view.width height: 56 property bool hovered: classMappingHoverHandler.hovered radius: 16 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 spacing: 12 AppTextField { id: classKeyField Layout.preferredWidth: 220 text: model.entryKey placeholderText: "班级编号" onEditingFinished: appController.classMappingModel.updateEntry(index, text, classValueField.text) } AppTextField { id: classValueField Layout.preferredWidth: 260 text: model.entryValue placeholderText: "班级名称" onEditingFinished: appController.classMappingModel.updateEntry(index, classKeyField.text, text) } Item { Layout.fillWidth: true } SecondaryButton { text: "删除"; onClicked: appController.classMappingModel.removeEntry(index) } } } } } } } } 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 ? 392 : 352 model: appController.faceModel delegate: Rectangle { width: Math.min(phoneLayout ? 300 : 244, GridView.view.cellWidth - 18) height: phoneLayout ? 368 : 328 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 ? 140 : (compactLayout ? 152 : 168) radius: 16 color: model.hasPhoto ? "#eef3f5" : goldSoft border.color: model.hasPhoto ? "#dbe4ea" : "#eadbb0" 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: "ID: " + model.faceId color: ink900 font.pixelSize: compactLayout ? 15 : 16 font.weight: appBoldWeight } AppTextField { Layout.fillWidth: true text: model.studentId placeholderText: narrowLayout ? "学号" : "输入真实学号" onEditingFinished: appController.faceModel.updateStudentId(index, text) } Rectangle { Layout.fillWidth: true Layout.preferredHeight: 34 radius: 12 color: model.hasPhoto ? "#e6f4ec" : "#f9ebcf" Label { anchors.centerIn: parent text: model.hasPhoto ? "照片已同步" : "仅检测到记录,无照片" color: model.hasPhoto ? successColor : goldColor font.pixelSize: narrowLayout ? 11 : 12 font.weight: appBoldWeight } } Item { Layout.fillHeight: true } 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 ColumnLayout { anchors.fill: parent anchors.margins: phoneLayout ? 16 : 24 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 } Item { Layout.fillHeight: true } Rectangle { Layout.fillWidth: true Layout.preferredHeight: phoneLayout ? 188 : 164 radius: 8 color: "#f8fafb" border.color: lineColor ColumnLayout { anchors.fill: parent anchors.margins: 16 spacing: 8 Label { Layout.fillWidth: true text: "毕业设计说明" color: ink900 font.pixelSize: 16 font.weight: appBoldWeight } Label { Layout.fillWidth: true text: "本系统为纪田野的个人毕业设计作品,仅用于毕业设计展示、学习研究与系统功能验证。" wrapMode: Text.WordWrap color: ink700 font.pixelSize: 13 } Label { Layout.fillWidth: true text: "Copyright © 2026 纪田野。保留所有权利。" wrapMode: Text.WordWrap color: ink500 font.pixelSize: 12 } Label { Layout.fillWidth: true text: "仓库地址:https://git.yzfh.eu.cc/yzfh/qt.git" wrapMode: Text.WrapAnywhere color: ink500 font.pixelSize: 12 } Label { Layout.fillWidth: true text: "查看账号:jtybishe 查看密码:jtybishe" wrapMode: Text.WordWrap color: ink500 font.pixelSize: 12 } } } } } } } 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 } } } }