2312 lines
116 KiB
QML
2312 lines
116 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: "人脸识别考勤系统 - QML 管理端"
|
|
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"
|
|
|
|
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 int sidebarWidth: slimLayout ? 0 : (compactLayout ? 212 : 240)
|
|
|
|
font.family: "Microsoft YaHei UI"
|
|
|
|
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.bold: true
|
|
}
|
|
}
|
|
|
|
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.bold: true
|
|
}
|
|
}
|
|
|
|
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 {
|
|
property string title: ""
|
|
property string description: ""
|
|
property string badgeText: ""
|
|
property color accent: accentColor
|
|
|
|
implicitWidth: 320
|
|
implicitHeight: 220
|
|
z: 20
|
|
|
|
Rectangle {
|
|
anchors.centerIn: parent
|
|
width: Math.min(parent.width, 320)
|
|
height: 200
|
|
radius: 8
|
|
color: "#fffaf4"
|
|
border.color: "#e7ddd2"
|
|
|
|
Column {
|
|
anchors.centerIn: parent
|
|
spacing: 12
|
|
width: parent.width - 40
|
|
|
|
Rectangle {
|
|
width: 64
|
|
height: 64
|
|
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: 22
|
|
font.bold: true
|
|
}
|
|
}
|
|
|
|
Label {
|
|
width: parent.width
|
|
text: title
|
|
horizontalAlignment: Text.AlignHCenter
|
|
wrapMode: Text.WordWrap
|
|
color: ink900
|
|
font.pixelSize: 20
|
|
font.bold: true
|
|
}
|
|
|
|
Label {
|
|
width: parent.width
|
|
text: description
|
|
horizontalAlignment: Text.AlignHCenter
|
|
wrapMode: Text.WordWrap
|
|
color: ink500
|
|
font.pixelSize: 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.bold: true
|
|
}
|
|
}
|
|
|
|
component SectionHeader: Item {
|
|
id: sectionHeader
|
|
property string title: ""
|
|
property string description: ""
|
|
property color accent: accentColor
|
|
|
|
implicitHeight: descriptionLabel.visible ? 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: 22
|
|
font.bold: true
|
|
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.bold: true
|
|
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.bold: true
|
|
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.bold: true
|
|
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
|
|
text: "管理端"
|
|
font.pixelSize: compactLayout ? 24 : 28
|
|
font.bold: true
|
|
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.bold: true
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 2
|
|
|
|
Label {
|
|
Layout.fillWidth: true
|
|
text: modelData.title
|
|
color: navButton.selected ? ink900 : "#f4fbff"
|
|
font.pixelSize: compactLayout ? 14 : 15
|
|
font.bold: true
|
|
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: 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 {
|
|
text: ["考勤记录总览", "统计分析面板", "配置与同步中心", "人脸资料管理", "关于"][window.currentPageIndex]
|
|
font.pixelSize: slimLayout ? 20 : (compactLayout ? 24 : 28)
|
|
font.bold: true
|
|
color: ink900
|
|
}
|
|
|
|
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 ? implicitHeight : 0
|
|
currentIndex: window.currentPageIndex
|
|
onCurrentIndexChanged: window.switchPage(currentIndex)
|
|
|
|
TabButton { text: "考勤记录" }
|
|
TabButton { text: "统计分析" }
|
|
TabButton { text: "配置中心" }
|
|
TabButton { text: "人脸管理" }
|
|
TabButton { 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.bold: true
|
|
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.bold: true
|
|
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.bold: true
|
|
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: 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.preferredWidth: 150
|
|
width: 150
|
|
spacing: 6
|
|
Label { text: "班级"; color: ink700; font.bold: true; font.pixelSize: 12 }
|
|
AppComboBox {
|
|
id: classFilter
|
|
width: parent.width
|
|
model: appController.classOptions
|
|
onCurrentTextChanged: applyRecordFilters()
|
|
}
|
|
}
|
|
|
|
Column {
|
|
Layout.preferredWidth: 160
|
|
width: 160
|
|
spacing: 6
|
|
Label { text: "学号"; color: ink700; font.bold: true; font.pixelSize: 12 }
|
|
AppComboBox {
|
|
id: studentFilter
|
|
width: parent.width
|
|
model: appController.studentOptions
|
|
onCurrentTextChanged: applyRecordFilters()
|
|
}
|
|
}
|
|
|
|
Column {
|
|
Layout.preferredWidth: 190
|
|
width: 190
|
|
spacing: 6
|
|
Label { text: "姓名"; color: ink700; font.bold: true; font.pixelSize: 12 }
|
|
AppTextField {
|
|
id: nameFilter
|
|
width: parent.width
|
|
placeholderText: "输入姓名关键字"
|
|
onTextChanged: applyRecordFilters()
|
|
}
|
|
}
|
|
|
|
Column {
|
|
Layout.preferredWidth: 160
|
|
width: 160
|
|
spacing: 6
|
|
Label { text: "日期"; color: ink700; font.bold: true; font.pixelSize: 12 }
|
|
AppComboBox {
|
|
id: dateFilter
|
|
width: parent.width
|
|
model: ["全部时间", "今天", "最近7天", "最近30天", "指定日期"]
|
|
currentIndex: 0
|
|
onCurrentIndexChanged: applyRecordFilters()
|
|
}
|
|
}
|
|
|
|
Column {
|
|
Layout.preferredWidth: 150
|
|
width: dateFilter.currentIndex === 4 ? 150 : 0
|
|
height: dateFilter.currentIndex === 4 ? implicitHeight : 0
|
|
visible: dateFilter.currentIndex === 4
|
|
spacing: 6
|
|
Label { text: "指定日期"; color: ink700; font.bold: true; 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()
|
|
}
|
|
}
|
|
|
|
Row {
|
|
Layout.alignment: Qt.AlignBottom
|
|
height: 42
|
|
spacing: 8
|
|
|
|
SecondaryButton {
|
|
text: "刷新"
|
|
onClicked: appController.refreshRecords()
|
|
}
|
|
|
|
PrimaryButton {
|
|
text: "导出"
|
|
fillColor: tealColor
|
|
onClicked: recordExportDialog.open()
|
|
}
|
|
|
|
DangerButton {
|
|
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: Math.max(168, Math.min(220, width * 0.21))
|
|
property real classColWidth: Math.max(88, Math.min(124, width * 0.11))
|
|
property real studentColWidth: Math.max(86, Math.min(116, width * 0.10))
|
|
property real nameColWidth: Math.max(118, Math.min(158, width * 0.14))
|
|
property real confidenceColWidth: Math.max(96, Math.min(126, width * 0.11))
|
|
property real statusColWidth: 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.bold: true; color: ink900 }
|
|
Label {
|
|
text: "班级"
|
|
visible: recordTableCard.showClassColumn
|
|
Layout.preferredWidth: recordTableCard.classColWidth
|
|
font.bold: true
|
|
color: ink900
|
|
}
|
|
Label { text: "学号"; Layout.preferredWidth: recordTableCard.studentColWidth; font.bold: true; color: ink900 }
|
|
Label { text: "姓名"; Layout.preferredWidth: recordTableCard.nameColWidth; font.bold: true; color: ink900 }
|
|
Label {
|
|
text: "置信度"
|
|
visible: recordTableCard.showConfidenceColumn
|
|
Layout.preferredWidth: recordTableCard.confidenceColWidth
|
|
font.bold: true
|
|
color: ink900
|
|
}
|
|
Label { text: "状态"; Layout.preferredWidth: recordTableCard.statusColWidth; font.bold: true; 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.bold: true; 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.bold: true
|
|
}
|
|
}
|
|
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: 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.preferredWidth: 150
|
|
model: ["今天", "最近7天", "最近30天", "本月"]
|
|
currentIndex: 2
|
|
onCurrentTextChanged: {
|
|
statisticsPreset = currentText
|
|
applyStatisticsPreset()
|
|
}
|
|
}
|
|
|
|
AppTextField {
|
|
id: statsStartField
|
|
Layout.preferredWidth: 150
|
|
placeholderText: "开始日期"
|
|
onEditingFinished: appController.updateStatisticsRange(text, statsEndField.text)
|
|
}
|
|
|
|
AppTextField {
|
|
id: statsEndField
|
|
Layout.preferredWidth: 150
|
|
placeholderText: "结束日期"
|
|
onEditingFinished: appController.updateStatisticsRange(statsStartField.text, text)
|
|
}
|
|
|
|
SecondaryButton {
|
|
text: "刷新统计"
|
|
onClicked: appController.updateStatisticsRange(statsStartField.text, statsEndField.text)
|
|
}
|
|
|
|
PrimaryButton {
|
|
text: "导出统计报表"
|
|
onClicked: statisticsExportDialog.open()
|
|
}
|
|
}
|
|
}
|
|
|
|
GridLayout {
|
|
Layout.fillWidth: true
|
|
columns: slimLayout ? 3 : (compactLayout ? 1 : 3)
|
|
rowSpacing: 10
|
|
columnSpacing: 14
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 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.bold: true; 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.preferredHeight: 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.bold: true; 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.fillWidth: true
|
|
Layout.preferredHeight: 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.bold: true
|
|
color: goldColor
|
|
}
|
|
}
|
|
|
|
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: Math.max(90, Math.min(128, width * 0.12))
|
|
property real nameColWidth: Math.max(128, Math.min(188, width * 0.17))
|
|
property real totalColWidth: 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
|
|
|
|
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.bold: true }
|
|
Label { text: "姓名"; Layout.preferredWidth: statisticsTableCard.nameColWidth; font.bold: true }
|
|
Label { text: "总签到次数"; Layout.preferredWidth: statisticsTableCard.totalColWidth; font.bold: true }
|
|
Label {
|
|
text: "今日签到"
|
|
visible: statisticsTableCard.showTodayColumn
|
|
Layout.preferredWidth: statisticsTableCard.todayColWidth
|
|
font.bold: true
|
|
}
|
|
Label {
|
|
text: "最后签到时间"
|
|
visible: statisticsTableCard.showLastColumn
|
|
Layout.preferredWidth: statisticsTableCard.lastColWidth
|
|
font.bold: true
|
|
}
|
|
Label { text: "平均置信度"; Layout.preferredWidth: statisticsTableCard.avgColWidth; font.bold: true }
|
|
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.bold: true }
|
|
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; Layout.preferredWidth: statisticsTableCard.avgColWidth; color: tealColor; font.bold: true }
|
|
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: 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.bold: true
|
|
color: ink700
|
|
}
|
|
AppTextField {
|
|
id: serverUrlField
|
|
Layout.fillWidth: true
|
|
Layout.preferredWidth: slimLayout ? 0 : 520
|
|
text: appController.serverUrl
|
|
placeholderText: "http://IP:端口"
|
|
}
|
|
|
|
SecondaryButton {
|
|
text: "保存地址"
|
|
onClicked: appController.serverUrl = serverUrlField.text
|
|
}
|
|
|
|
PrimaryButton {
|
|
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.bold: true }
|
|
Label { text: "开始"; Layout.preferredWidth: 120; font.bold: true }
|
|
Label { text: "结束"; Layout.preferredWidth: 120; font.bold: true }
|
|
Label { text: "状态"; Layout.preferredWidth: 100; font.bold: true }
|
|
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.bold: true
|
|
}
|
|
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: slimLayout ? 132 : 88
|
|
clip: true
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: slimLayout ? 12 : 18
|
|
spacing: 12
|
|
|
|
SectionHeader {
|
|
Layout.fillWidth: true
|
|
title: "K210 人脸照片与学号映射"
|
|
description: "同步照片状态,维护人脸与真实学号的对应关系。"
|
|
accent: goldColor
|
|
}
|
|
|
|
Row {
|
|
Layout.alignment: 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: Math.max(1, Math.floor(width / (compactLayout ? 236 : 260)))
|
|
cellWidth: Math.max(240, Math.floor(width / cardColumns))
|
|
cellHeight: 332
|
|
model: appController.faceModel
|
|
|
|
delegate: Rectangle {
|
|
width: Math.min(244, GridView.view.cellWidth - 18)
|
|
height: 308
|
|
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: 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.bold: true
|
|
}
|
|
}
|
|
|
|
Label {
|
|
text: "ID: " + model.faceId
|
|
color: ink900
|
|
font.pixelSize: compactLayout ? 15 : 16
|
|
font.bold: true
|
|
}
|
|
|
|
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.bold: true
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 24
|
|
spacing: 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: 92
|
|
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.bold: true
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|