Files
bishe-STM32/QT/qml/Main.qml
T
2026-05-01 20:29:02 +08:00

2946 lines
151 KiB
QML

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 ? (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: 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 + (phoneLayout ? 132 : 64) + 12 + (phoneLayout ? 0 : 42) + (phoneLayout ? 0 : 12) + Math.max(phoneLayout ? 96 : 52, timeSlotListView.contentHeight) + 18
: (phoneLayout ? 124 : 100)
clip: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 12
ColumnLayout {
Layout.fillWidth: true
Layout.preferredHeight: phoneLayout ? 132 : 64
spacing: 12
SectionHeader {
Layout.fillWidth: true
title: "时间段配置"
description: "维护签到与签退时段,并同步到服务端。"
accent: accentColor
}
GridLayout {
Layout.fillWidth: true
columns: phoneLayout ? 3 : 5
columnSpacing: 8
rowSpacing: 8
SecondaryButton {
Layout.fillWidth: true
text: timeSlotsExpanded ? "收起" : "展开"
onClicked: timeSlotsExpanded = !timeSlotsExpanded
}
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: phoneLayout ? "同步" : "从服务器同步"
onClicked: appController.reloadTimeSlotsFromServer()
}
PrimaryButton {
Layout.fillWidth: true
visible: timeSlotsExpanded
text: phoneLayout ? "保存" : "保存时间段"
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 ? contentHeight : 0
clip: false
interactive: false
spacing: 8
model: appController.timeSlotModel
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: mappingsExpanded
? 18 + (phoneLayout ? 120 : 64) + 12 + mappingTabs.implicitHeight + 12 + Math.max(phoneLayout ? 112 : 64, mappingTabs.currentIndex === 0 ? studentMappingListView.contentHeight : classMappingListView.contentHeight) + 18
: (phoneLayout ? 124 : 100)
clip: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 12
ColumnLayout {
Layout.fillWidth: true
Layout.preferredHeight: phoneLayout ? 120 : 64
spacing: 12
SectionHeader {
Layout.fillWidth: true
title: "名称映射配置"
description: "让学号和班级编号显示为更清晰的真实名称。"
accent: tealColor
}
GridLayout {
Layout.fillWidth: true
columns: phoneLayout ? 3 : 3
columnSpacing: 8
rowSpacing: 8
SecondaryButton {
Layout.fillWidth: true
text: mappingsExpanded ? "收起" : "展开"
onClicked: mappingsExpanded = !mappingsExpanded
}
SecondaryButton {
Layout.fillWidth: true
visible: mappingsExpanded
text: phoneLayout ? "加载" : "从服务器加载"
onClicked: appController.loadMappingsFromServer()
}
PrimaryButton {
Layout.fillWidth: true
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: phoneLayout ? 112 : 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
visible: !phoneLayout
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)
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
visible: phoneLayout
spacing: 8
AppTextField {
id: studentKeyFieldPhone
Layout.fillWidth: true
text: model.entryKey
placeholderText: "学号"
onEditingFinished: appController.studentMappingModel.updateEntry(index, text, studentValueFieldPhone.text)
}
RowLayout {
Layout.fillWidth: true
spacing: 8
AppTextField {
id: studentValueFieldPhone
Layout.fillWidth: true
text: model.entryValue
placeholderText: "真实姓名"
onEditingFinished: appController.studentMappingModel.updateEntry(index, studentKeyFieldPhone.text, text)
}
SecondaryButton {
Layout.preferredWidth: 76
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: phoneLayout ? 112 : 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
visible: !phoneLayout
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)
}
}
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)
}
RowLayout {
Layout.fillWidth: true
spacing: 8
AppTextField {
id: classValueFieldPhone
Layout.fillWidth: true
text: model.entryValue
placeholderText: "班级名称"
onEditingFinished: appController.classMappingModel.updateEntry(index, classKeyFieldPhone.text, text)
}
SecondaryButton {
Layout.preferredWidth: 76
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
}
TextArea {
Layout.fillWidth: true
Layout.fillHeight: true
text: "本系统为纪田野的个人毕业设计作品,仅用于毕业设计展示、学习研究与系统功能验证。\nCopyright © 2026 纪田野。保留所有权利。\n仓库地址:https://git.yzfh.eu.cc/yzfh/qt.git\n查看账号:jtybishe 查看密码:jtybishe"
readOnly: true
selectByMouse: true
selectByKeyboard: true
wrapMode: TextEdit.WrapAnywhere
color: ink700
font.pixelSize: 12
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
background: Rectangle {
color: "transparent"
}
}
}
}
}
}
}
}
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
}
}
}
}