初始化
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
||||
build/
|
||||
bin/
|
||||
Makefile
|
||||
.qmake.stash
|
||||
|
||||
# Qt generated files
|
||||
moc_*.cpp
|
||||
ui_*.h
|
||||
qrc_*.cpp
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
# CMake generated files
|
||||
CMakeFiles/
|
||||
CMakeCache.txt
|
||||
cmake_install.cmake
|
||||
|
||||
# Local editor and OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.user
|
||||
*.autosave
|
||||
@@ -0,0 +1,82 @@
|
||||
#-------------------------------------------------
|
||||
# 人脸识别考勤系统 - Qt查看器
|
||||
# 项目配置文件 (qmake)
|
||||
#-------------------------------------------------
|
||||
|
||||
QT += core gui widgets network
|
||||
|
||||
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
|
||||
|
||||
TARGET = AttendanceViewer
|
||||
TEMPLATE = app
|
||||
|
||||
# C++17标准
|
||||
CONFIG += c++17
|
||||
|
||||
# 输出目录
|
||||
DESTDIR = $$PWD/bin
|
||||
OBJECTS_DIR = $$PWD/build/obj
|
||||
MOC_DIR = $$PWD/build/moc
|
||||
RCC_DIR = $$PWD/build/rcc
|
||||
UI_DIR = $$PWD/build/ui
|
||||
|
||||
# 包含路径
|
||||
INCLUDEPATH += $$PWD/include
|
||||
|
||||
# 源文件
|
||||
SOURCES += \
|
||||
src/main.cpp \
|
||||
src/MainWindow.cpp \
|
||||
src/AttendanceRecord.cpp \
|
||||
src/TimeSlotConfig.cpp \
|
||||
src/SettingsDialog.cpp \
|
||||
src/StatisticsDialog.cpp \
|
||||
src/NetworkApiManager.cpp \
|
||||
src/ServerConfigDialog.cpp \
|
||||
src/MappingConfigDialog.cpp \
|
||||
src/FaceManageDialog.cpp
|
||||
|
||||
# 头文件
|
||||
HEADERS += \
|
||||
include/MainWindow.h \
|
||||
include/AttendanceRecord.h \
|
||||
include/TimeSlotConfig.h \
|
||||
include/SettingsDialog.h \
|
||||
include/StatisticsDialog.h \
|
||||
include/NetworkApiManager.h \
|
||||
include/ServerConfigDialog.h \
|
||||
include/MappingConfigDialog.h \
|
||||
include/FaceManageDialog.h
|
||||
|
||||
# 默认部署规则
|
||||
qnx: target.path = /tmp/$${TARGET}/bin
|
||||
else: unix:!android: target.path = /opt/$${TARGET}/bin
|
||||
!isEmpty(target.path): INSTALLS += target
|
||||
|
||||
# 编译器警告
|
||||
QMAKE_CXXFLAGS += -Wall
|
||||
|
||||
# Windows特定配置
|
||||
win32 {
|
||||
RC_ICONS = icon.ico
|
||||
CONFIG += windows
|
||||
|
||||
# 链接OpenSSL库(用于AES加密/解密)
|
||||
LIBS += -lssl -lcrypto
|
||||
|
||||
# 如果使用预编译的OpenSSL,取消下面的注释并设置路径
|
||||
# INCLUDEPATH += "C:/OpenSSL/include"
|
||||
# LIBS += -L"C:/OpenSSL/lib"
|
||||
}
|
||||
|
||||
# Linux特定配置
|
||||
unix:!macx {
|
||||
CONFIG += link_pkgconfig
|
||||
|
||||
# 链接OpenSSL库(用于AES加密/解密)
|
||||
LIBS += -lssl -lcrypto
|
||||
}
|
||||
|
||||
# 版本信息
|
||||
VERSION = 1.0.0
|
||||
DEFINES += APP_VERSION=\\\"$$VERSION\\\"
|
||||
@@ -0,0 +1,65 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(AttendanceViewer VERSION 1.0.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# 自动处理Qt的MOC、UIC和RCC
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
# 查找Qt5组件
|
||||
find_package(Qt5 REQUIRED COMPONENTS
|
||||
Core
|
||||
Widgets
|
||||
Network
|
||||
)
|
||||
|
||||
# 源文件
|
||||
set(SOURCES
|
||||
src/main.cpp
|
||||
src/MainWindow.cpp
|
||||
src/AttendanceRecord.cpp
|
||||
src/TimeSlotConfig.cpp
|
||||
src/SettingsDialog.cpp
|
||||
src/StatisticsDialog.cpp
|
||||
src/NetworkApiManager.cpp
|
||||
src/ServerConfigDialog.cpp
|
||||
)
|
||||
|
||||
# 头文件
|
||||
set(HEADERS
|
||||
include/MainWindow.h
|
||||
include/AttendanceRecord.h
|
||||
include/TimeSlotConfig.h
|
||||
include/SettingsDialog.h
|
||||
include/StatisticsDialog.h
|
||||
include/NetworkApiManager.h
|
||||
include/ServerConfigDialog.h
|
||||
)
|
||||
|
||||
# 包含目录
|
||||
include_directories(${CMAKE_SOURCE_DIR}/include)
|
||||
|
||||
# 创建可执行文件
|
||||
add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS})
|
||||
|
||||
# 链接Qt库
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
Qt5::Core
|
||||
Qt5::Widgets
|
||||
Qt5::Network
|
||||
)
|
||||
|
||||
# Windows平台特定设置
|
||||
if(WIN32)
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES
|
||||
WIN32_EXECUTABLE ON
|
||||
)
|
||||
endif()
|
||||
|
||||
# 安装目标
|
||||
install(TARGETS ${PROJECT_NAME}
|
||||
RUNTIME DESTINATION bin
|
||||
)
|
||||
@@ -0,0 +1,330 @@
|
||||
# 人脸识别考勤系统 - Qt管理端
|
||||
|
||||
基于 Qt 5.15 的全功能考勤管理系统,用于管理STM32人脸识别考勤系统的签到数据。
|
||||
|
||||
## 📋 功能特性
|
||||
|
||||
### 📊 数据查看
|
||||
✅ **实时数据显示** - 自动读取服务器端CSV数据库
|
||||
✅ **自动刷新** - 每5秒自动刷新,支持文件监视
|
||||
✅ **多条件筛选** - 按学号、姓名、日期筛选记录
|
||||
✅ **统计信息** - 实时显示总记录数和今日签到数
|
||||
|
||||
### ⚙️ 系统管理
|
||||
✅ **时间段设置** - 自定义签到时间段(上午/下午/晚上等)
|
||||
✅ **记录管理** - 清空签到记录、导出备份
|
||||
✅ **文件选择** - 支持切换不同的CSV数据文件
|
||||
|
||||
### 📈 统计分析
|
||||
✅ **统计报表** - 按日期范围生成学生签到统计
|
||||
✅ **多维度分析** - 总签到次数、今日签到、平均置信度
|
||||
✅ **报表导出** - 支持导出统计报表为CSV格式
|
||||
|
||||
### 🎨 界面优化
|
||||
✅ **菜单栏** - 文件、管理、帮助菜单
|
||||
✅ **友好界面** - 清晰的表格展示,交替行颜色
|
||||
✅ **颜色标识** - 成功/失败状态颜色区分
|
||||
|
||||
## 🛠️ 环境要求
|
||||
|
||||
- **Qt 5.15** 或更高版本
|
||||
- **CMake 3.16** 或更高版本
|
||||
- **C++17** 编译器
|
||||
- Windows/Linux/macOS
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
QT/
|
||||
├── CMakeLists.txt # CMake配置文件
|
||||
├── AttendanceViewer.pro # qmake项目文件
|
||||
├── build.sh / build.bat # 构建脚本
|
||||
├── include/ # 头文件目录
|
||||
│ ├── MainWindow.h # 主窗口类
|
||||
│ ├── AttendanceRecord.h # 考勤记录数据模型
|
||||
│ ├── TimeSlotConfig.h # 时间段配置管理
|
||||
│ ├── SettingsDialog.h # 设置对话框
|
||||
│ └── StatisticsDialog.h # 统计报表对话框
|
||||
├── src/ # 源文件目录
|
||||
│ ├── main.cpp # 程序入口
|
||||
│ ├── MainWindow.cpp # 主窗口实现
|
||||
│ ├── AttendanceRecord.cpp # 数据模型实现
|
||||
│ ├── TimeSlotConfig.cpp # 时间段配置实现
|
||||
│ ├── SettingsDialog.cpp # 设置对话框实现
|
||||
│ └── StatisticsDialog.cpp # 统计报表实现
|
||||
└── README.md # 本文档
|
||||
```
|
||||
|
||||
## 🚀 编译和运行
|
||||
|
||||
### 方法1: 使用 Qt Creator(推荐)
|
||||
|
||||
1. **安装Qt 5.15**
|
||||
- 从 [Qt官网](https://www.qt.io/download) 下载Qt 5.15
|
||||
- 安装时确保选择 MinGW 或 MSVC 编译器
|
||||
|
||||
2. **打开项目**
|
||||
```
|
||||
- 启动 Qt Creator
|
||||
- 打开 QT/AttendanceViewer.pro
|
||||
- 选择合适的Kit (Qt 5.15 + MinGW/MSVC)
|
||||
```
|
||||
|
||||
3. **编译运行**
|
||||
- 点击左下角的 "运行" 按钮(绿色三角形)
|
||||
- 或按快捷键 `Ctrl+R`
|
||||
|
||||
### 方法2: 使用 qmake (命令行)
|
||||
|
||||
#### Windows
|
||||
|
||||
```bash
|
||||
cd QT
|
||||
|
||||
# 方法A: 使用构建脚本
|
||||
build.bat
|
||||
|
||||
# 方法B: 手动编译
|
||||
qmake AttendanceViewer.pro
|
||||
mingw32-make
|
||||
cd bin
|
||||
AttendanceViewer.exe
|
||||
```
|
||||
|
||||
#### Linux
|
||||
|
||||
```bash
|
||||
cd QT
|
||||
|
||||
# 安装Qt5 (如果还没安装)
|
||||
sudo apt-get install qt5-default
|
||||
|
||||
# 方法A: 使用构建脚本
|
||||
chmod +x build.sh
|
||||
./build.sh
|
||||
|
||||
# 方法B: 手动编译
|
||||
qmake AttendanceViewer.pro
|
||||
make
|
||||
cd bin
|
||||
./AttendanceViewer
|
||||
```
|
||||
|
||||
### 方法3: 使用 CMake
|
||||
|
||||
#### Linux
|
||||
|
||||
```bash
|
||||
cd QT
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make
|
||||
./AttendanceViewer
|
||||
```
|
||||
|
||||
#### Windows
|
||||
|
||||
```bash
|
||||
cd QT
|
||||
mkdir build
|
||||
cd build
|
||||
cmake -G "MinGW Makefiles" ..
|
||||
mingw32-make
|
||||
cd Release
|
||||
AttendanceViewer.exe
|
||||
```
|
||||
|
||||
## 📊 使用说明
|
||||
|
||||
### 主界面功能
|
||||
|
||||
#### 1. 菜单栏功能
|
||||
|
||||
**文件菜单**
|
||||
- **选择CSV文件**: 切换到不同的考勤数据文件
|
||||
- **导出数据**: 导出当前筛选的记录为CSV
|
||||
- **退出**: 关闭程序
|
||||
|
||||
**管理菜单**
|
||||
- **时间段设置**: 自定义签到时间段
|
||||
- **统计报表**: 生成考勤统计报表
|
||||
- **清空签到记录**: 清空所有签到数据(危险操作,需二次确认)
|
||||
|
||||
**帮助菜单**
|
||||
- **关于**: 显示软件版本和功能信息
|
||||
|
||||
#### 2. 统计信息栏
|
||||
- **总记录数**: 显示数据库中的总签到记录数
|
||||
- **今日签到**: 显示今天的签到人次
|
||||
- **状态**: 显示当前程序状态(就绪/加载中/显示记录数)
|
||||
|
||||
#### 3. 筛选条件
|
||||
- **学号**: 下拉选择特定学号,显示该学生的所有签到记录
|
||||
- **姓名**: 输入姓名关键字进行模糊搜索
|
||||
- **日期**:
|
||||
- 全部:显示所有记录
|
||||
- 今天:只显示今天的记录
|
||||
- 最近7天:显示过去一周的记录
|
||||
- 最近30天:显示过去一个月的记录
|
||||
- **清除筛选**: 一键清除所有筛选条件
|
||||
|
||||
#### 4. 数据表格
|
||||
显示筛选后的考勤记录,包含以下列:
|
||||
- **签到时间**: 格式 `2026-02-01 11:06:41`
|
||||
- **学号**: 学生学号
|
||||
- **姓名**: 学生姓名
|
||||
- **置信度**: 人脸识别置信度(百分比)
|
||||
- **状态**: 签到状态(成功/失败),成功显示为绿色
|
||||
|
||||
#### 5. 底部操作按钮
|
||||
- **刷新数据**: 手动刷新数据
|
||||
- **导出数据**: 导出当前筛选的数据为CSV
|
||||
- **时间段设置**: 快速打开时间段设置对话框
|
||||
- **统计报表**: 快速打开统计报表对话框
|
||||
- **清空记录**: 清空所有签到记录(危险操作)
|
||||
|
||||
### 自动刷新机制
|
||||
|
||||
程序采用双重刷新机制确保数据实时性:
|
||||
|
||||
1. **定时刷新**: 每5秒自动检查并更新数据
|
||||
2. **文件监视**: 当CSV文件被修改时立即触发刷新
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### CSV文件路径
|
||||
|
||||
程序默认读取路径:`../server/attendance_database.csv`
|
||||
|
||||
如需修改路径,请编辑 `src/MainWindow.cpp` 中的 `getDefaultCsvPath()` 函数:
|
||||
|
||||
```cpp
|
||||
QString MainWindow::getDefaultCsvPath()
|
||||
{
|
||||
// 修改为你的CSV文件路径
|
||||
return "C:/path/to/your/attendance_database.csv";
|
||||
}
|
||||
```
|
||||
|
||||
### CSV文件格式
|
||||
|
||||
程序读取的CSV格式如下:
|
||||
|
||||
```csv
|
||||
Timestamp,Student_ID,Name,Confidence,Status
|
||||
2026-02-01 11:06:41,2,ID2,0.90,success
|
||||
2026-02-01 11:06:58,1,ID1,0.87,success
|
||||
```
|
||||
|
||||
字段说明:
|
||||
- `Timestamp`: 签到时间(YYYY-MM-DD HH:MM:SS)
|
||||
- `Student_ID`: 学号
|
||||
- `Name`: 姓名
|
||||
- `Confidence`: 置信度(0.0-1.0)
|
||||
- `Status`: 状态(success/fail)
|
||||
|
||||
## 🎯 核心功能实现
|
||||
|
||||
### 数据模型 (AttendanceRecord)
|
||||
|
||||
负责解析和存储单条考勤记录:
|
||||
- 从CSV行解析数据
|
||||
- 时间戳格式转换
|
||||
- 数据验证
|
||||
|
||||
### 主窗口 (MainWindow)
|
||||
|
||||
核心功能实现:
|
||||
- UI界面构建(统计栏、筛选栏、表格、按钮)
|
||||
- CSV文件读取和解析
|
||||
- 数据筛选逻辑(学号、姓名、日期)
|
||||
- 表格动态更新
|
||||
- 统计信息计算
|
||||
- 文件监视和自动刷新
|
||||
- 数据导出
|
||||
|
||||
## 📝 开发说明
|
||||
|
||||
### 添加新功能
|
||||
|
||||
**示例:添加成绩统计图表**
|
||||
|
||||
1. 在 `CMakeLists.txt` 中添加 Qt Charts 模块(已包含)
|
||||
2. 在 `MainWindow.h` 中添加图表成员变量
|
||||
3. 在 `MainWindow.cpp` 中实现图表绘制逻辑
|
||||
|
||||
### 自定义样式
|
||||
|
||||
可以在 `MainWindow::setupUI()` 中修改界面样式:
|
||||
|
||||
```cpp
|
||||
// 修改表格样式
|
||||
m_tableWidget->setStyleSheet("QTableWidget { background-color: #f0f0f0; }");
|
||||
|
||||
// 修改按钮样式
|
||||
m_refreshButton->setStyleSheet("QPushButton { background-color: #4CAF50; color: white; }");
|
||||
```
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 问题1: 找不到CSV文件
|
||||
|
||||
**现象**: 提示"找不到考勤数据文件"
|
||||
|
||||
**解决方案**:
|
||||
1. 确认CSV文件路径是否正确
|
||||
2. 检查 `../server/attendance_database.csv` 是否存在
|
||||
3. 手动修改 `getDefaultCsvPath()` 函数中的路径
|
||||
|
||||
### 问题2: Qt Charts模块找不到
|
||||
|
||||
**现象**: 编译错误 `Could not find a package configuration file provided by "Qt5Charts"`
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install libqt5charts5-dev
|
||||
|
||||
# 或从CMakeLists.txt中移除Charts(如果不需要图表功能)
|
||||
find_package(Qt5 REQUIRED COMPONENTS Core Widgets) # 删除Charts
|
||||
```
|
||||
|
||||
### 问题3: 自动刷新不工作
|
||||
|
||||
**现象**: 数据不会自动更新
|
||||
|
||||
**解决方案**:
|
||||
1. 检查文件权限,确保程序有读取权限
|
||||
2. 某些网络驱动器可能不支持文件监视
|
||||
3. 手动点击"刷新数据"按钮
|
||||
|
||||
## 📈 性能优化
|
||||
|
||||
- 使用 `QVector` 存储数据,高效访问
|
||||
- 表格按需更新,避免全量刷新
|
||||
- 文件监视避免频繁读取磁盘
|
||||
- 筛选操作在内存中进行,速度快
|
||||
|
||||
## 🎓 扩展功能建议
|
||||
|
||||
- [ ] **图表统计** - 添加签到趋势图、人员统计图
|
||||
- [ ] **数据库支持** - 支持SQLite/MySQL数据库
|
||||
- [ ] **Excel导出** - 使用QXlsx库导出真正的Excel格式
|
||||
- [ ] **打印功能** - 支持打印考勤报表
|
||||
- [ ] **多语言支持** - 中英文切换
|
||||
- [ ] **主题切换** - 明暗主题切换
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目与主项目使用相同的许可证,仅供学习和研究使用。
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
感谢使用本考勤系统Qt查看器!
|
||||
|
||||
---
|
||||
|
||||
**开始使用**:
|
||||
1. 确保服务器端已生成 `attendance_database.csv` 文件
|
||||
2. 编译并运行Qt程序
|
||||
3. 享受实时查看考勤数据的便利!🚀
|
||||
@@ -0,0 +1,49 @@
|
||||
@echo off
|
||||
REM 人脸识别考勤系统 - Qt查看器编译脚本 (Windows)
|
||||
|
||||
echo =========================================
|
||||
echo 编译 AttendanceViewer
|
||||
echo =========================================
|
||||
|
||||
REM 检查是否设置了Qt环境变量
|
||||
where qmake >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo 错误: 未找到 qmake,请先设置 Qt 环境变量
|
||||
echo 或使用 Qt Creator 打开项目
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM 清理旧的编译文件
|
||||
echo [1/4] 清理旧的编译文件...
|
||||
if exist build rmdir /s /q build
|
||||
if exist bin rmdir /s /q bin
|
||||
mkdir build
|
||||
|
||||
REM 进入构建目录
|
||||
cd build
|
||||
|
||||
REM 生成 Makefile
|
||||
echo [2/4] 生成 Makefile...
|
||||
qmake ..\AttendanceViewer.pro
|
||||
|
||||
REM 编译
|
||||
echo [3/4] 编译项目...
|
||||
mingw32-make
|
||||
|
||||
REM 检查编译结果
|
||||
if %errorlevel% equ 0 (
|
||||
echo [4/4] 编译成功!
|
||||
echo.
|
||||
echo =========================================
|
||||
echo 运行程序:
|
||||
echo cd ..\bin
|
||||
echo AttendanceViewer.exe
|
||||
echo =========================================
|
||||
) else (
|
||||
echo [4/4] 编译失败!
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
pause
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 人脸识别考勤系统 - Qt查看器编译脚本 (Linux)
|
||||
|
||||
echo "========================================="
|
||||
echo " 编译 AttendanceViewer"
|
||||
echo "========================================="
|
||||
|
||||
# 检查是否安装了qmake
|
||||
if ! command -v qmake &> /dev/null; then
|
||||
echo "错误: 未找到 qmake,请先安装 Qt5"
|
||||
echo "运行: sudo apt-get install qt5-default"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 清理旧的编译文件
|
||||
echo "[1/4] 清理旧的编译文件..."
|
||||
rm -rf build bin
|
||||
mkdir -p build
|
||||
|
||||
# 进入构建目录
|
||||
cd build
|
||||
|
||||
# 生成 Makefile
|
||||
echo "[2/4] 生成 Makefile..."
|
||||
qmake ../AttendanceViewer.pro
|
||||
|
||||
# 编译
|
||||
echo "[3/4] 编译项目..."
|
||||
make -j$(nproc)
|
||||
|
||||
# 检查编译结果
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "[4/4] 编译成功!"
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo " 运行程序:"
|
||||
echo " cd ../bin && ./AttendanceViewer"
|
||||
echo "========================================="
|
||||
else
|
||||
echo "[4/4] 编译失败!"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,53 @@
|
||||
#ifndef ATTENDANCERECORD_H
|
||||
#define ATTENDANCERECORD_H
|
||||
|
||||
#include <QString>
|
||||
#include <QDateTime>
|
||||
|
||||
/**
|
||||
* @brief 考勤记录数据模型
|
||||
*
|
||||
* 对应CSV文件格式:Timestamp,Student_ID,Name,Confidence,Status,Class_ID
|
||||
*/
|
||||
class AttendanceRecord
|
||||
{
|
||||
public:
|
||||
AttendanceRecord();
|
||||
AttendanceRecord(const QString ×tamp, const QString &studentId,
|
||||
const QString &name, float confidence, const QString &status,
|
||||
const QString &classId = "0");
|
||||
|
||||
// Getters
|
||||
QString getTimestamp() const { return m_timestamp; }
|
||||
QString getStudentId() const { return m_studentId; }
|
||||
QString getName() const { return m_name; }
|
||||
float getConfidence() const { return m_confidence; }
|
||||
QString getStatus() const { return m_status; }
|
||||
QString getClassId() const { return m_classId; }
|
||||
QDateTime getDateTime() const { return m_dateTime; }
|
||||
|
||||
// Setters
|
||||
void setTimestamp(const QString ×tamp);
|
||||
void setStudentId(const QString &studentId) { m_studentId = studentId; }
|
||||
void setName(const QString &name) { m_name = name; }
|
||||
void setConfidence(float confidence) { m_confidence = confidence; }
|
||||
void setStatus(const QString &status) { m_status = status; }
|
||||
void setClassId(const QString &classId) { m_classId = classId; }
|
||||
|
||||
// 从CSV行解析
|
||||
static AttendanceRecord fromCsvLine(const QString &line);
|
||||
|
||||
// 转换为显示字符串
|
||||
QString toString() const;
|
||||
|
||||
private:
|
||||
QString m_timestamp; // 时间戳字符串
|
||||
QString m_studentId; // 学号
|
||||
QString m_name; // 姓名
|
||||
float m_confidence; // 置信度 (0.0-1.0)
|
||||
QString m_status; // 状态 (success/fail)
|
||||
QString m_classId; // 班级编号
|
||||
QDateTime m_dateTime; // 解析后的日期时间对象
|
||||
};
|
||||
|
||||
#endif // ATTENDANCERECORD_H
|
||||
@@ -0,0 +1,98 @@
|
||||
#ifndef FACEMANAGEDIALOG_H
|
||||
#define FACEMANAGEDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QGridLayout>
|
||||
#include <QScrollArea>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QMap>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
|
||||
/**
|
||||
* @brief 人脸照片项组件
|
||||
* 显示单个人脸照片及其学号映射
|
||||
*/
|
||||
class FacePhotoItem : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FacePhotoItem(const QString &faceId, bool hasPhoto = true, QWidget *parent = nullptr);
|
||||
|
||||
QString getFaceId() const { return m_faceId; }
|
||||
QString getStudentId() const { return m_studentIdEdit->text().trimmed(); }
|
||||
void setStudentId(const QString &studentId) { m_studentIdEdit->setText(studentId); }
|
||||
void setPhoto(const QPixmap &pixmap);
|
||||
bool hasPhoto() const { return m_hasPhoto; }
|
||||
|
||||
signals:
|
||||
void studentIdChanged(const QString &faceId, const QString &studentId);
|
||||
void deleteRequested(const QString &faceId);
|
||||
|
||||
private:
|
||||
QString m_faceId;
|
||||
bool m_hasPhoto;
|
||||
QLabel *m_photoLabel;
|
||||
QLabel *m_idLabel;
|
||||
QLineEdit *m_studentIdEdit;
|
||||
QPushButton *m_deleteButton;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 人脸管理对话框
|
||||
* 显示K210上传的人脸照片,允许配置学号映射
|
||||
*/
|
||||
class FaceManageDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FaceManageDialog(QWidget *parent = nullptr);
|
||||
~FaceManageDialog();
|
||||
|
||||
void setServerUrl(const QString &url) { m_serverUrl = url; }
|
||||
|
||||
signals:
|
||||
void mappingSaved();
|
||||
|
||||
public slots:
|
||||
void refreshPhotos();
|
||||
|
||||
private slots:
|
||||
void onPhotoListReceived(QNetworkReply *reply);
|
||||
void onPhotoReceived(QNetworkReply *reply);
|
||||
void onMappingReceived(QNetworkReply *reply);
|
||||
void onMappingSaved(QNetworkReply *reply);
|
||||
void onAllStudentIdsReceived(QNetworkReply *reply);
|
||||
void onFaceDeleted(QNetworkReply *reply);
|
||||
void saveMapping();
|
||||
void onDeleteRequested(const QString &studentId);
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void fetchPhotoList();
|
||||
void fetchPhoto(const QString &faceId);
|
||||
void fetchMapping();
|
||||
void fetchAllStudentIds();
|
||||
void uploadMapping();
|
||||
void deleteFace(const QString &studentId, bool deleteRecords);
|
||||
void clearPhotoItems();
|
||||
|
||||
QString m_serverUrl;
|
||||
QNetworkAccessManager *m_networkManager;
|
||||
QScrollArea *m_scrollArea;
|
||||
QWidget *m_contentWidget;
|
||||
QGridLayout *m_gridLayout;
|
||||
QPushButton *m_refreshButton;
|
||||
QPushButton *m_saveButton;
|
||||
QLabel *m_statusLabel;
|
||||
|
||||
QMap<QString, FacePhotoItem*> m_photoItems; // faceId -> item
|
||||
QMap<QString, QString> m_idMapping; // faceId -> studentId
|
||||
int m_pendingPhotoCount;
|
||||
};
|
||||
|
||||
#endif // FACEMANAGEDIALOG_H
|
||||
@@ -0,0 +1,135 @@
|
||||
#ifndef MAINWINDOW_H
|
||||
#define MAINWINDOW_H
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QTableWidget>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QTimer>
|
||||
#include <QComboBox>
|
||||
#include <QLineEdit>
|
||||
#include <QCheckBox>
|
||||
#include <QDateEdit>
|
||||
#include <QVector>
|
||||
#include <QEvent>
|
||||
#include <QDialog>
|
||||
#include "AttendanceRecord.h"
|
||||
#include "NetworkApiManager.h"
|
||||
|
||||
class MappingConfigDialog;
|
||||
|
||||
/**
|
||||
* @brief 对话框位置限制过滤器
|
||||
* 限制对话框只能在父窗口范围内移动
|
||||
*/
|
||||
class DialogPositionFilter : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DialogPositionFilter(QWidget *parent = nullptr) : QObject(parent) {}
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 考勤记录管理端主窗口(网络版)
|
||||
*
|
||||
* 功能:
|
||||
* - 通过HTTP API从服务器获取数据
|
||||
* - 显示考勤记录表格
|
||||
* - 自动刷新数据(30秒)
|
||||
* - 按学号、姓名、日期筛选
|
||||
* - 统计信息显示
|
||||
* - 服务器配置管理
|
||||
*/
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
~MainWindow();
|
||||
|
||||
private slots:
|
||||
// 数据操作
|
||||
void loadAttendanceData(); // 从服务器加载考勤数据
|
||||
void refreshData(); // 刷新数据
|
||||
void filterData(); // 筛选数据
|
||||
void exportToExcel(); // 导出为CSV
|
||||
void exportSelectedRecords(); // 导出选中的记录
|
||||
void clearFilter(); // 清除筛选条件
|
||||
void deleteSelectedRecord(); // 删除选中的记录
|
||||
void deleteSelectedRecords(); // 批量删除选中的记录
|
||||
void toggleSelectAll(); // 全选/取消全选
|
||||
void unselectAll(); // 取消选中
|
||||
|
||||
// 管理功能
|
||||
void onClearAllRecords(); // 清空所有签到记录(调用API)
|
||||
void onOpenSettings(); // 打开时间段设置
|
||||
void onOpenMappingConfig(); // 打开映射配置
|
||||
void onOpenFaceManage(); // 打开人脸管理
|
||||
void onShowStatistics(); // 显示统计报表
|
||||
void onAbout(); // 关于对话框
|
||||
|
||||
// 网络响应处理
|
||||
void onRecordsFetched(const QVector<AttendanceRecord> &records);
|
||||
void onStatisticsFetched(int total, int today, const QString &serverTime);
|
||||
void onRecordsCleared(bool success);
|
||||
void onRecordDeleted(bool success); // 新增:删除记录响应
|
||||
void onNetworkError(const QString &error);
|
||||
void onMappingConfigFetched(const QMap<QString, QString> &studentNameMap,
|
||||
const QMap<QString, QString> &classNameMap);
|
||||
void onMappingConfigUploaded(bool success);
|
||||
|
||||
private:
|
||||
void setupUI(); // 设置UI界面
|
||||
void setupMenuBar(); // 设置菜单栏
|
||||
void setupConnections(); // 设置信号槽连接
|
||||
void updateTable(const QVector<AttendanceRecord> &records); // 更新表格
|
||||
void updateStatistics(int total, int today); // 更新统计信息
|
||||
void showConnectionStatus(bool connected); // 显示连接状态
|
||||
|
||||
// UI组件
|
||||
QTableWidget *m_tableWidget;
|
||||
QPushButton *m_refreshButton;
|
||||
QPushButton *m_exportButton;
|
||||
QPushButton *m_exportSelectedButton; // 导出选中
|
||||
QPushButton *m_deleteSelectedButton; // 删除选中
|
||||
QPushButton *m_clearFilterButton;
|
||||
QPushButton *m_settingsButton;
|
||||
QPushButton *m_statsButton;
|
||||
QPushButton *m_clearRecordsButton;
|
||||
QCheckBox *m_selectAllCheckBox; // 全选复选框
|
||||
QPushButton *m_unselectAllButton; // 取消选中按钮
|
||||
QLabel *m_totalLabel;
|
||||
QLabel *m_todayLabel;
|
||||
QLabel *m_statusLabel;
|
||||
QLabel *m_connectionLabel;
|
||||
QLabel *m_selectedCountLabel; // 显示选中数量
|
||||
QComboBox *m_classFilterCombo; // 班级筛选下拉框
|
||||
QComboBox *m_studentFilterCombo;
|
||||
QLineEdit *m_nameFilterEdit;
|
||||
QComboBox *m_dateFilterCombo;
|
||||
QDateEdit *m_specificDateEdit; // 特定日期选择器
|
||||
QCheckBox *m_enableSpecificDateCheckBox; // 启用特定日期筛选
|
||||
|
||||
// 数据
|
||||
QVector<AttendanceRecord> m_allRecords; // 所有记录
|
||||
QVector<AttendanceRecord> m_filteredRecords; // 筛选后的记录
|
||||
QSet<QString> m_selectedRecordKeys; // 保存选中记录的唯一键(timestamp+studentId)
|
||||
QMap<QString, QString> m_studentNameMap; // 学号 -> 真实姓名 映射
|
||||
QMap<QString, QString> m_classNameMap; // 班级编号 -> 班级名称 映射
|
||||
|
||||
// 辅助函数
|
||||
QString getMappedStudentName(const QString &studentId, const QString &originalName) const;
|
||||
QString getMappedClassName(const QString &classId) const;
|
||||
void loadMappingConfigFromSettings();
|
||||
void saveMappingConfigToSettings();
|
||||
|
||||
// 网络
|
||||
NetworkApiManager *m_apiManager; // 网络API管理器
|
||||
QTimer *m_autoRefreshTimer; // 自动刷新定时器
|
||||
};
|
||||
|
||||
#endif // MAINWINDOW_H
|
||||
@@ -0,0 +1,89 @@
|
||||
#ifndef MAPPINGCONFIGDIALOG_H
|
||||
#define MAPPINGCONFIGDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QTableWidget>
|
||||
#include <QPushButton>
|
||||
#include <QLineEdit>
|
||||
#include <QMap>
|
||||
#include <QTabWidget>
|
||||
#include <QLabel>
|
||||
|
||||
/**
|
||||
* @brief 映射配置对话框
|
||||
*
|
||||
* 用于配置:
|
||||
* 1. 学号 -> 真实姓名 的映射
|
||||
* 2. 班级编号 -> 真实班级名称 的映射
|
||||
*
|
||||
* 特性:
|
||||
* - 双击单元格可直接编辑(类似Excel)
|
||||
* - 自动从服务器加载已有映射
|
||||
* - 一键保存到服务器
|
||||
*/
|
||||
class MappingConfigDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MappingConfigDialog(QWidget *parent = nullptr);
|
||||
~MappingConfigDialog();
|
||||
|
||||
// 获取映射数据
|
||||
QMap<QString, QString> getStudentNameMap() const { return m_studentNameMap; }
|
||||
QMap<QString, QString> getClassNameMap() const { return m_classNameMap; }
|
||||
|
||||
// 设置映射数据(用于从服务器加载后更新)
|
||||
void setStudentNameMap(const QMap<QString, QString> &map);
|
||||
void setClassNameMap(const QMap<QString, QString> &map);
|
||||
|
||||
// 从表格收集数据到映射
|
||||
void collectDataFromTables();
|
||||
|
||||
// 设置状态文本
|
||||
void setStatus(const QString &status);
|
||||
|
||||
signals:
|
||||
void mappingChanged(); // 映射数据已修改,需要保存
|
||||
void requestSyncFromServer(); // 请求从服务器加载数据
|
||||
|
||||
private slots:
|
||||
void onAddStudent();
|
||||
void onDeleteStudent();
|
||||
void onAddClass();
|
||||
void onDeleteClass();
|
||||
void onSave();
|
||||
void onSyncToServer();
|
||||
void onSyncFromServer();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void updateStudentTable();
|
||||
void updateClassTable();
|
||||
|
||||
// UI组件
|
||||
QTabWidget *m_tabWidget;
|
||||
QLabel *m_statusLabel;
|
||||
|
||||
// 学号-姓名映射表
|
||||
QTableWidget *m_studentTable;
|
||||
QPushButton *m_addStudentBtn;
|
||||
QPushButton *m_deleteStudentBtn;
|
||||
|
||||
// 班级编号-名称映射表
|
||||
QTableWidget *m_classTable;
|
||||
QPushButton *m_addClassBtn;
|
||||
QPushButton *m_deleteClassBtn;
|
||||
|
||||
// 底部按钮
|
||||
QPushButton *m_syncFromServerBtn;
|
||||
QPushButton *m_syncToServerBtn;
|
||||
QPushButton *m_saveBtn;
|
||||
QPushButton *m_cancelBtn;
|
||||
|
||||
// 数据
|
||||
QMap<QString, QString> m_studentNameMap; // 学号 -> 姓名
|
||||
QMap<QString, QString> m_classNameMap; // 班级编号 -> 班级名称
|
||||
};
|
||||
|
||||
#endif // MAPPINGCONFIGDIALOG_H
|
||||
@@ -0,0 +1,89 @@
|
||||
#ifndef NETWORKAPIMANAGER_H
|
||||
#define NETWORKAPIMANAGER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QVector>
|
||||
#include "AttendanceRecord.h"
|
||||
#include "TimeSlotConfig.h"
|
||||
|
||||
/**
|
||||
* @brief 网络API管理类
|
||||
*
|
||||
* 负责与服务器端HTTP API通信(支持AES-128-CBC加密)
|
||||
* API端点:http://server_ip:8082/api/
|
||||
*/
|
||||
class NetworkApiManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit NetworkApiManager(QObject *parent = nullptr);
|
||||
~NetworkApiManager();
|
||||
|
||||
// 设置服务器地址
|
||||
void setServerUrl(const QString &url);
|
||||
QString getServerUrl() const { return m_serverUrl; }
|
||||
|
||||
// 设置是否启用加密通信
|
||||
void setEncryptionEnabled(bool enabled) { m_encryptionEnabled = enabled; }
|
||||
bool isEncryptionEnabled() const { return m_encryptionEnabled; }
|
||||
|
||||
// API调用
|
||||
void fetchRecords(int limit = 100, int offset = 0);
|
||||
void fetchStatistics();
|
||||
void clearAllRecords();
|
||||
void deleteRecord(const QString ×tamp, const QString &studentId);
|
||||
void uploadTimeSlots(const QVector<TimeSlot> &timeSlots);
|
||||
void checkHealth();
|
||||
|
||||
// 映射配置API
|
||||
void uploadMappingConfig(const QMap<QString, QString> &studentNameMap,
|
||||
const QMap<QString, QString> &classNameMap);
|
||||
void fetchMappingConfig();
|
||||
|
||||
// 人脸照片管理API
|
||||
void fetchFacePhotos();
|
||||
void fetchFacePhoto(const QString &faceId);
|
||||
void uploadFaceIdMapping(const QMap<QString, QString> &mapping);
|
||||
void fetchFaceIdMapping();
|
||||
|
||||
signals:
|
||||
// 响应信号
|
||||
void recordsFetched(const QVector<AttendanceRecord> &records);
|
||||
void statisticsFetched(int total, int today, const QString &serverTime);
|
||||
void recordsCleared(bool success);
|
||||
void recordDeleted(bool success);
|
||||
void timeSlotsUploaded(bool success);
|
||||
void healthChecked(bool healthy);
|
||||
void errorOccurred(const QString &error);
|
||||
|
||||
// 映射配置信号
|
||||
void mappingConfigUploaded(bool success);
|
||||
void mappingConfigFetched(const QMap<QString, QString> &studentNameMap,
|
||||
const QMap<QString, QString> &classNameMap);
|
||||
|
||||
// 人脸照片管理信号
|
||||
void facePhotosFetched(const QJsonArray &photos);
|
||||
void facePhotoFetched(const QString &faceId, const QByteArray &photoData);
|
||||
void faceIdMappingUploaded(bool success);
|
||||
void faceIdMappingFetched(const QMap<QString, QString> &mapping);
|
||||
|
||||
private slots:
|
||||
void onReplyFinished(QNetworkReply *reply);
|
||||
|
||||
private:
|
||||
QNetworkAccessManager *m_networkManager;
|
||||
QString m_serverUrl;
|
||||
bool m_encryptionEnabled; // 是否启用加密通信
|
||||
|
||||
QString buildApiUrl(const QString &endpoint, const QMap<QString, QString> ¶ms = QMap<QString, QString>());
|
||||
QVector<AttendanceRecord> parseRecordsJson(const QByteArray &json);
|
||||
|
||||
// AES加密/解密功能
|
||||
QByteArray aesDecrypt(const QByteArray &encryptedData);
|
||||
QByteArray removePKCS7Padding(const QByteArray &data);
|
||||
};
|
||||
|
||||
#endif // NETWORKAPIMANAGER_H
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef SERVERCONFIGDIALOG_H
|
||||
#define SERVERCONFIGDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
|
||||
/**
|
||||
* @brief 服务器配置对话框
|
||||
*/
|
||||
class ServerConfigDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ServerConfigDialog(const QString ¤tUrl, QWidget *parent = nullptr);
|
||||
QString getServerUrl() const;
|
||||
|
||||
private slots:
|
||||
void onTestConnection();
|
||||
void onSave();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
|
||||
QLineEdit *m_urlEdit;
|
||||
QPushButton *m_testButton;
|
||||
QLabel *m_statusLabel;
|
||||
QString m_serverUrl;
|
||||
};
|
||||
|
||||
#endif // SERVERCONFIGDIALOG_H
|
||||
@@ -0,0 +1,75 @@
|
||||
#ifndef SETTINGSDIALOG_H
|
||||
#define SETTINGSDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QTableWidget>
|
||||
#include <QPushButton>
|
||||
#include <QTimeEdit>
|
||||
#include <QLineEdit>
|
||||
#include <QCheckBox>
|
||||
#include "TimeSlotConfig.h"
|
||||
#include "NetworkApiManager.h"
|
||||
|
||||
/**
|
||||
* @brief 设置对话框 - 管理签到时间段
|
||||
*/
|
||||
class SettingsDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SettingsDialog(QWidget *parent = nullptr);
|
||||
~SettingsDialog();
|
||||
|
||||
private slots:
|
||||
void onAddTimeSlot();
|
||||
void onEditTimeSlot();
|
||||
void onDeleteTimeSlot();
|
||||
void onResetToDefault();
|
||||
void onSave();
|
||||
void onCancel();
|
||||
void onTimeSlotsUploaded(bool success); // 新增:处理上传结果
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void loadTimeSlots();
|
||||
void saveTimeSlots();
|
||||
void refreshTimeSlots(); // 新增:只刷新表格显示,不重新加载数据
|
||||
|
||||
// UI组件
|
||||
QTableWidget *m_tableWidget;
|
||||
QPushButton *m_addButton;
|
||||
QPushButton *m_editButton;
|
||||
QPushButton *m_deleteButton;
|
||||
QPushButton *m_resetButton;
|
||||
QPushButton *m_saveButton;
|
||||
QPushButton *m_cancelButton;
|
||||
|
||||
// 临时存储
|
||||
QVector<TimeSlot> m_tempTimeSlots;
|
||||
|
||||
// 网络API管理器
|
||||
NetworkApiManager *m_apiManager;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 时间段编辑对话框
|
||||
*/
|
||||
class TimeSlotEditDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TimeSlotEditDialog(QWidget *parent = nullptr, const TimeSlot *slot = nullptr);
|
||||
TimeSlot getTimeSlot() const;
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
|
||||
QLineEdit *m_nameEdit;
|
||||
QTimeEdit *m_startTimeEdit;
|
||||
QTimeEdit *m_endTimeEdit;
|
||||
QCheckBox *m_enabledCheckBox;
|
||||
};
|
||||
|
||||
#endif // SETTINGSDIALOG_H
|
||||
@@ -0,0 +1,58 @@
|
||||
#ifndef STATISTICSDIALOG_H
|
||||
#define STATISTICSDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QTableWidget>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QDateEdit>
|
||||
#include <QComboBox>
|
||||
#include <QMap>
|
||||
#include "AttendanceRecord.h"
|
||||
|
||||
/**
|
||||
* @brief 统计报表对话框
|
||||
*/
|
||||
class StatisticsDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit StatisticsDialog(const QVector<AttendanceRecord> &records, QWidget *parent = nullptr);
|
||||
~StatisticsDialog();
|
||||
|
||||
private slots:
|
||||
void onDateRangeChanged();
|
||||
void onExportReport();
|
||||
void onRefresh();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void calculateStatistics();
|
||||
void updateTable();
|
||||
|
||||
// 统计数据结构
|
||||
struct StudentStats {
|
||||
QString studentId;
|
||||
QString name;
|
||||
int totalCount; // 总签到次数
|
||||
int todayCount; // 今日签到次数
|
||||
QDateTime lastCheckIn; // 最后签到时间
|
||||
double avgConfidence; // 平均置信度
|
||||
};
|
||||
|
||||
// UI组件
|
||||
QDateEdit *m_startDateEdit;
|
||||
QDateEdit *m_endDateEdit;
|
||||
QComboBox *m_quickSelectCombo;
|
||||
QTableWidget *m_tableWidget;
|
||||
QPushButton *m_exportButton;
|
||||
QPushButton *m_refreshButton;
|
||||
QLabel *m_summaryLabel;
|
||||
|
||||
// 数据
|
||||
QVector<AttendanceRecord> m_records;
|
||||
QMap<QString, StudentStats> m_statsMap;
|
||||
};
|
||||
|
||||
#endif // STATISTICSDIALOG_H
|
||||
@@ -0,0 +1,92 @@
|
||||
#ifndef TIMESLOTCONFIG_H
|
||||
#define TIMESLOTCONFIG_H
|
||||
|
||||
#include <QString>
|
||||
#include <QTime>
|
||||
#include <QVector>
|
||||
#include <QSettings>
|
||||
|
||||
/**
|
||||
* @brief 签到时间段结构
|
||||
*/
|
||||
struct TimeSlot
|
||||
{
|
||||
QString name; // 时段名称(如:上午签到)
|
||||
QTime startTime; // 开始时间
|
||||
QTime endTime; // 结束时间
|
||||
bool enabled; // 是否启用
|
||||
|
||||
TimeSlot()
|
||||
: enabled(true)
|
||||
{}
|
||||
|
||||
TimeSlot(const QString &n, const QTime &start, const QTime &end, bool en = true)
|
||||
: name(n), startTime(start), endTime(end), enabled(en)
|
||||
{}
|
||||
|
||||
// 检查给定时间是否在此时段内
|
||||
bool isInRange(const QTime &time) const {
|
||||
if (!enabled) return false;
|
||||
return time >= startTime && time <= endTime;
|
||||
}
|
||||
|
||||
QString toString() const {
|
||||
return QString("%1: %2 - %3 %4")
|
||||
.arg(name)
|
||||
.arg(startTime.toString("HH:mm"))
|
||||
.arg(endTime.toString("HH:mm"))
|
||||
.arg(enabled ? "(启用)" : "(禁用)");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 时间段配置管理类
|
||||
*/
|
||||
class TimeSlotConfig
|
||||
{
|
||||
public:
|
||||
static TimeSlotConfig& instance();
|
||||
|
||||
// 获取所有时间段
|
||||
QVector<TimeSlot> getTimeSlots() const { return m_timeSlots; }
|
||||
|
||||
// 设置时间段
|
||||
void setTimeSlots(const QVector<TimeSlot> &timeSlots);
|
||||
|
||||
// 添加时间段
|
||||
void addTimeSlot(const TimeSlot &timeSlot);
|
||||
|
||||
// 删除时间段
|
||||
void removeTimeSlot(int index);
|
||||
|
||||
// 更新时间段
|
||||
void updateTimeSlot(int index, const TimeSlot &timeSlot);
|
||||
|
||||
// 检查时间是否在任何启用的时段内
|
||||
bool isValidTime(const QTime &time) const;
|
||||
|
||||
// 获取当前时段名称
|
||||
QString getCurrentTimeSlotName(const QTime &time) const;
|
||||
|
||||
// 保存配置到文件
|
||||
void saveToFile();
|
||||
|
||||
// 从文件加载配置
|
||||
void loadFromFile();
|
||||
|
||||
// 重置为默认配置
|
||||
void resetToDefault();
|
||||
|
||||
private:
|
||||
TimeSlotConfig();
|
||||
~TimeSlotConfig();
|
||||
TimeSlotConfig(const TimeSlotConfig&) = delete;
|
||||
TimeSlotConfig& operator=(const TimeSlotConfig&) = delete;
|
||||
|
||||
QVector<TimeSlot> m_timeSlots;
|
||||
QString m_configFilePath;
|
||||
|
||||
void initDefaultTimeSlots();
|
||||
};
|
||||
|
||||
#endif // TIMESLOTCONFIG_H
|
||||
@@ -0,0 +1,59 @@
|
||||
#include "AttendanceRecord.h"
|
||||
#include <QStringList>
|
||||
|
||||
AttendanceRecord::AttendanceRecord()
|
||||
: m_confidence(0.0f)
|
||||
, m_classId("0")
|
||||
{
|
||||
}
|
||||
|
||||
AttendanceRecord::AttendanceRecord(const QString ×tamp, const QString &studentId,
|
||||
const QString &name, float confidence, const QString &status,
|
||||
const QString &classId)
|
||||
: m_timestamp(timestamp)
|
||||
, m_studentId(studentId)
|
||||
, m_name(name)
|
||||
, m_confidence(confidence)
|
||||
, m_status(status)
|
||||
, m_classId(classId)
|
||||
{
|
||||
// 解析时间戳 - 格式: "2026-02-01 11:06:41"
|
||||
m_dateTime = QDateTime::fromString(timestamp, "yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
void AttendanceRecord::setTimestamp(const QString ×tamp)
|
||||
{
|
||||
m_timestamp = timestamp;
|
||||
m_dateTime = QDateTime::fromString(timestamp, "yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
AttendanceRecord AttendanceRecord::fromCsvLine(const QString &line)
|
||||
{
|
||||
// CSV格式: Timestamp,Student_ID,Name,Confidence,Status,Class_ID
|
||||
// 示例: 2026-02-01 11:06:41,2,ID2,0.90,success,1
|
||||
QStringList fields = line.split(',');
|
||||
|
||||
if (fields.size() < 5) {
|
||||
return AttendanceRecord(); // 返回空记录
|
||||
}
|
||||
|
||||
QString timestamp = fields[0].trimmed();
|
||||
QString studentId = fields[1].trimmed();
|
||||
QString name = fields[2].trimmed();
|
||||
float confidence = fields[3].trimmed().toFloat();
|
||||
QString status = fields[4].trimmed();
|
||||
QString classId = (fields.size() >= 6) ? fields[5].trimmed() : "0";
|
||||
|
||||
return AttendanceRecord(timestamp, studentId, name, confidence, status, classId);
|
||||
}
|
||||
|
||||
QString AttendanceRecord::toString() const
|
||||
{
|
||||
return QString("[%1] 班级:%2 学号:%3 姓名:%4 置信度:%.2f%% 状态:%5")
|
||||
.arg(m_timestamp)
|
||||
.arg(m_classId)
|
||||
.arg(m_studentId)
|
||||
.arg(m_name)
|
||||
.arg(m_confidence * 100)
|
||||
.arg(m_status);
|
||||
}
|
||||
@@ -0,0 +1,657 @@
|
||||
#include "FaceManageDialog.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QMessageBox>
|
||||
#include <QDebug>
|
||||
#include <QPixmap>
|
||||
#include <QBuffer>
|
||||
|
||||
// ============= FacePhotoItem 实现 =============
|
||||
|
||||
FacePhotoItem::FacePhotoItem(const QString &faceId, bool hasPhoto, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_faceId(faceId)
|
||||
, m_hasPhoto(hasPhoto)
|
||||
{
|
||||
setFixedSize(180, 280);
|
||||
setStyleSheet(R"(
|
||||
QWidget {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #EBEEF5;
|
||||
border-radius: 8px;
|
||||
}
|
||||
)");
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
layout->setSpacing(8);
|
||||
layout->setContentsMargins(10, 10, 10, 10);
|
||||
|
||||
// 照片显示
|
||||
m_photoLabel = new QLabel();
|
||||
m_photoLabel->setFixedSize(160, 160);
|
||||
m_photoLabel->setAlignment(Qt::AlignCenter);
|
||||
m_photoLabel->setStyleSheet(R"(
|
||||
QLabel {
|
||||
background-color: #F5F7FA;
|
||||
border: 1px solid #DCDFE6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
)");
|
||||
if (hasPhoto) {
|
||||
m_photoLabel->setText("加载中...");
|
||||
} else {
|
||||
m_photoLabel->setText("无照片");
|
||||
m_photoLabel->setStyleSheet(R"(
|
||||
QLabel {
|
||||
background-color: #FEF0F0;
|
||||
border: 1px solid #FBC4C4;
|
||||
border-radius: 4px;
|
||||
color: #F56C6C;
|
||||
}
|
||||
)");
|
||||
}
|
||||
layout->addWidget(m_photoLabel);
|
||||
|
||||
// 学号/ID显示
|
||||
m_idLabel = new QLabel(QString("ID: %1").arg(faceId));
|
||||
m_idLabel->setStyleSheet("color: #909399; font-size: 12px; border: none;");
|
||||
m_idLabel->setAlignment(Qt::AlignCenter);
|
||||
layout->addWidget(m_idLabel);
|
||||
|
||||
// 学号输入框
|
||||
m_studentIdEdit = new QLineEdit();
|
||||
m_studentIdEdit->setPlaceholderText("输入真实学号...");
|
||||
m_studentIdEdit->setStyleSheet(R"(
|
||||
QLineEdit {
|
||||
padding: 6px;
|
||||
border: 1px solid #DCDFE6;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
}
|
||||
QLineEdit:focus {
|
||||
border: 1px solid #409EFF;
|
||||
}
|
||||
)");
|
||||
layout->addWidget(m_studentIdEdit);
|
||||
|
||||
// 删除按钮
|
||||
m_deleteButton = new QPushButton("删除");
|
||||
m_deleteButton->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #F56C6C;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #F78989;
|
||||
}
|
||||
)");
|
||||
connect(m_deleteButton, &QPushButton::clicked, this, [this]() {
|
||||
emit deleteRequested(m_faceId);
|
||||
});
|
||||
layout->addWidget(m_deleteButton);
|
||||
|
||||
connect(m_studentIdEdit, &QLineEdit::textChanged, this, [this](const QString &text) {
|
||||
emit studentIdChanged(m_faceId, text);
|
||||
});
|
||||
}
|
||||
|
||||
void FacePhotoItem::setPhoto(const QPixmap &pixmap)
|
||||
{
|
||||
if (pixmap.isNull()) {
|
||||
m_photoLabel->setText("无照片");
|
||||
} else {
|
||||
m_photoLabel->setPixmap(pixmap.scaled(156, 156, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
}
|
||||
}
|
||||
|
||||
// ============= FaceManageDialog 实现 =============
|
||||
|
||||
FaceManageDialog::FaceManageDialog(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, m_networkManager(new QNetworkAccessManager(this))
|
||||
, m_pendingPhotoCount(0)
|
||||
{
|
||||
m_serverUrl = "http://27.106.113.205:8082"; // 默认服务器地址
|
||||
setupUI();
|
||||
|
||||
// 加载数据
|
||||
refreshPhotos();
|
||||
}
|
||||
|
||||
FaceManageDialog::~FaceManageDialog()
|
||||
{
|
||||
clearPhotoItems();
|
||||
}
|
||||
|
||||
void FaceManageDialog::setupUI()
|
||||
{
|
||||
setWindowTitle("人脸管理 - K210人脸照片与学号映射");
|
||||
resize(800, 600);
|
||||
setMinimumSize(600, 400);
|
||||
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setSpacing(16);
|
||||
mainLayout->setContentsMargins(20, 20, 20, 20);
|
||||
|
||||
// 标题和说明
|
||||
QLabel *titleLabel = new QLabel("K210 人脸照片管理");
|
||||
titleLabel->setStyleSheet("font-size: 18px; font-weight: bold; color: #303133;");
|
||||
mainLayout->addWidget(titleLabel);
|
||||
|
||||
QLabel *descLabel = new QLabel("以下显示所有人脸信息(包括有照片和无照片的),可以删除人脸及其考勤记录:");
|
||||
descLabel->setStyleSheet("color: #606266; font-size: 13px;");
|
||||
descLabel->setWordWrap(true);
|
||||
mainLayout->addWidget(descLabel);
|
||||
|
||||
// 工具栏
|
||||
QHBoxLayout *toolbarLayout = new QHBoxLayout();
|
||||
|
||||
m_refreshButton = new QPushButton("刷新照片");
|
||||
m_refreshButton->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #409EFF;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #66B1FF;
|
||||
}
|
||||
)");
|
||||
connect(m_refreshButton, &QPushButton::clicked, this, &FaceManageDialog::refreshPhotos);
|
||||
toolbarLayout->addWidget(m_refreshButton);
|
||||
|
||||
m_statusLabel = new QLabel("就绪");
|
||||
m_statusLabel->setStyleSheet("color: #909399;");
|
||||
toolbarLayout->addWidget(m_statusLabel);
|
||||
|
||||
toolbarLayout->addStretch();
|
||||
|
||||
m_saveButton = new QPushButton("保存映射配置");
|
||||
m_saveButton->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #67C23A;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #85CE61;
|
||||
}
|
||||
)");
|
||||
connect(m_saveButton, &QPushButton::clicked, this, &FaceManageDialog::saveMapping);
|
||||
toolbarLayout->addWidget(m_saveButton);
|
||||
|
||||
mainLayout->addLayout(toolbarLayout);
|
||||
|
||||
// 滚动区域
|
||||
m_scrollArea = new QScrollArea();
|
||||
m_scrollArea->setWidgetResizable(true);
|
||||
m_scrollArea->setStyleSheet(R"(
|
||||
QScrollArea {
|
||||
border: 1px solid #EBEEF5;
|
||||
border-radius: 8px;
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
)");
|
||||
|
||||
m_contentWidget = new QWidget();
|
||||
m_contentWidget->setStyleSheet("background-color: transparent;");
|
||||
m_gridLayout = new QGridLayout(m_contentWidget);
|
||||
m_gridLayout->setSpacing(16);
|
||||
m_gridLayout->setContentsMargins(16, 16, 16, 16);
|
||||
|
||||
m_scrollArea->setWidget(m_contentWidget);
|
||||
mainLayout->addWidget(m_scrollArea, 1);
|
||||
|
||||
// 底部按钮
|
||||
QHBoxLayout *bottomLayout = new QHBoxLayout();
|
||||
bottomLayout->addStretch();
|
||||
|
||||
QPushButton *closeButton = new QPushButton("关闭");
|
||||
closeButton->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: white;
|
||||
color: #606266;
|
||||
border: 1px solid #DCDFE6;
|
||||
border-radius: 4px;
|
||||
padding: 8px 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QPushButton:hover {
|
||||
color: #409EFF;
|
||||
border-color: #409EFF;
|
||||
}
|
||||
)");
|
||||
connect(closeButton, &QPushButton::clicked, this, &QDialog::accept);
|
||||
bottomLayout->addWidget(closeButton);
|
||||
|
||||
mainLayout->addLayout(bottomLayout);
|
||||
}
|
||||
|
||||
void FaceManageDialog::clearPhotoItems()
|
||||
{
|
||||
for (auto item : m_photoItems) {
|
||||
delete item;
|
||||
}
|
||||
m_photoItems.clear();
|
||||
}
|
||||
|
||||
void FaceManageDialog::refreshPhotos()
|
||||
{
|
||||
m_statusLabel->setText("正在加载...");
|
||||
m_refreshButton->setEnabled(false);
|
||||
|
||||
fetchMapping(); // 先获取映射配置
|
||||
fetchPhotoList(); // 获取有照片的列表
|
||||
fetchAllStudentIds(); // 获取所有学生ID(包括无照片的)
|
||||
}
|
||||
|
||||
void FaceManageDialog::fetchPhotoList()
|
||||
{
|
||||
QString url = m_serverUrl + "/api/face_photos";
|
||||
qDebug() << "[FaceManage] Fetching photo list from:" << url;
|
||||
|
||||
QNetworkRequest request(url);
|
||||
QNetworkReply *reply = m_networkManager->get(request);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
onPhotoListReceived(reply);
|
||||
});
|
||||
}
|
||||
|
||||
void FaceManageDialog::fetchPhoto(const QString &faceId)
|
||||
{
|
||||
QString url = m_serverUrl + "/api/face_photo/" + faceId;
|
||||
qDebug() << "[FaceManage] Fetching photo:" << url;
|
||||
|
||||
QNetworkRequest request(url);
|
||||
QNetworkReply *reply = m_networkManager->get(request);
|
||||
reply->setProperty("faceId", faceId);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
onPhotoReceived(reply);
|
||||
});
|
||||
}
|
||||
|
||||
void FaceManageDialog::fetchMapping()
|
||||
{
|
||||
QString url = m_serverUrl + "/api/face_id_mapping";
|
||||
qDebug() << "[FaceManage] Fetching mapping from:" << url;
|
||||
|
||||
QNetworkRequest request(url);
|
||||
QNetworkReply *reply = m_networkManager->get(request);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
onMappingReceived(reply);
|
||||
});
|
||||
}
|
||||
|
||||
void FaceManageDialog::uploadMapping()
|
||||
{
|
||||
QString url = m_serverUrl + "/api/face_id_mapping";
|
||||
qDebug() << "[FaceManage] Uploading mapping to:" << url;
|
||||
|
||||
// 构建JSON
|
||||
QJsonObject mappingsObj;
|
||||
for (auto it = m_idMapping.begin(); it != m_idMapping.end(); ++it) {
|
||||
if (!it.value().isEmpty()) {
|
||||
mappingsObj[it.key()] = it.value();
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject rootObj;
|
||||
rootObj["mappings"] = mappingsObj;
|
||||
|
||||
QJsonDocument doc(rootObj);
|
||||
QByteArray jsonData = doc.toJson(QJsonDocument::Compact);
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QNetworkReply *reply = m_networkManager->post(request, jsonData);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
onMappingSaved(reply);
|
||||
});
|
||||
}
|
||||
|
||||
void FaceManageDialog::onPhotoListReceived(QNetworkReply *reply)
|
||||
{
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
m_statusLabel->setText("加载失败: " + reply->errorString());
|
||||
m_refreshButton->setEnabled(true);
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
if (!doc.isObject()) {
|
||||
m_statusLabel->setText("数据格式错误");
|
||||
m_refreshButton->setEnabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray photosArray = doc.object()["photos"].toArray();
|
||||
qDebug() << "[FaceManage] Received" << photosArray.size() << "photos";
|
||||
|
||||
// 清空现有项
|
||||
clearPhotoItems();
|
||||
|
||||
// 清空网格布局
|
||||
while (m_gridLayout->count() > 0) {
|
||||
QLayoutItem *item = m_gridLayout->takeAt(0);
|
||||
delete item;
|
||||
}
|
||||
|
||||
if (photosArray.isEmpty()) {
|
||||
QLabel *emptyLabel = new QLabel("暂无人脸照片\n\n请在K210设备上添加人脸后,照片将自动上传到服务器");
|
||||
emptyLabel->setAlignment(Qt::AlignCenter);
|
||||
emptyLabel->setStyleSheet("color: #909399; font-size: 14px;");
|
||||
m_gridLayout->addWidget(emptyLabel, 0, 0);
|
||||
m_statusLabel->setText("暂无照片");
|
||||
m_refreshButton->setEnabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
m_pendingPhotoCount = photosArray.size();
|
||||
int col = 0, row = 0;
|
||||
int maxCols = 4;
|
||||
|
||||
for (const QJsonValue &value : photosArray) {
|
||||
QJsonObject photoObj = value.toObject();
|
||||
QString faceId = photoObj["id"].toString();
|
||||
|
||||
FacePhotoItem *item = new FacePhotoItem(faceId, true, m_contentWidget);
|
||||
|
||||
// 设置已有的学号映射
|
||||
if (m_idMapping.contains(faceId)) {
|
||||
item->setStudentId(m_idMapping[faceId]);
|
||||
}
|
||||
|
||||
// 连接信号
|
||||
connect(item, &FacePhotoItem::studentIdChanged, this, [this](const QString &faceId, const QString &studentId) {
|
||||
m_idMapping[faceId] = studentId;
|
||||
});
|
||||
|
||||
connect(item, &FacePhotoItem::deleteRequested, this, &FaceManageDialog::onDeleteRequested);
|
||||
|
||||
m_photoItems[faceId] = item;
|
||||
m_gridLayout->addWidget(item, row, col);
|
||||
|
||||
col++;
|
||||
if (col >= maxCols) {
|
||||
col = 0;
|
||||
row++;
|
||||
}
|
||||
|
||||
// 获取照片
|
||||
fetchPhoto(faceId);
|
||||
}
|
||||
|
||||
m_statusLabel->setText(QString("正在加载 %1 张照片...").arg(photosArray.size()));
|
||||
}
|
||||
|
||||
void FaceManageDialog::onPhotoReceived(QNetworkReply *reply)
|
||||
{
|
||||
QString faceId = reply->property("faceId").toString();
|
||||
m_pendingPhotoCount--;
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray data = reply->readAll();
|
||||
QPixmap pixmap;
|
||||
if (pixmap.loadFromData(data)) {
|
||||
if (m_photoItems.contains(faceId)) {
|
||||
m_photoItems[faceId]->setPhoto(pixmap);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
qWarning() << "[FaceManage] Failed to load photo for" << faceId << ":" << reply->errorString();
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if (m_pendingPhotoCount <= 0) {
|
||||
m_statusLabel->setText(QString("已加载 %1 张照片").arg(m_photoItems.size()));
|
||||
m_refreshButton->setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
void FaceManageDialog::onMappingReceived(QNetworkReply *reply)
|
||||
{
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "[FaceManage] Failed to fetch mapping:" << reply->errorString();
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
if (!doc.isObject()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject mappingsObj = doc.object()["mappings"].toObject();
|
||||
m_idMapping.clear();
|
||||
|
||||
for (auto it = mappingsObj.begin(); it != mappingsObj.end(); ++it) {
|
||||
m_idMapping[it.key()] = it.value().toString();
|
||||
}
|
||||
|
||||
qDebug() << "[FaceManage] Loaded" << m_idMapping.size() << "mappings";
|
||||
|
||||
// 更新已显示的项
|
||||
for (auto it = m_photoItems.begin(); it != m_photoItems.end(); ++it) {
|
||||
if (m_idMapping.contains(it.key())) {
|
||||
it.value()->setStudentId(m_idMapping[it.key()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FaceManageDialog::saveMapping()
|
||||
{
|
||||
// 从所有项收集映射
|
||||
for (auto it = m_photoItems.begin(); it != m_photoItems.end(); ++it) {
|
||||
QString studentId = it.value()->getStudentId();
|
||||
if (!studentId.isEmpty()) {
|
||||
m_idMapping[it.key()] = studentId;
|
||||
}
|
||||
}
|
||||
|
||||
m_statusLabel->setText("正在保存...");
|
||||
m_saveButton->setEnabled(false);
|
||||
|
||||
uploadMapping();
|
||||
}
|
||||
|
||||
void FaceManageDialog::onMappingSaved(QNetworkReply *reply)
|
||||
{
|
||||
m_saveButton->setEnabled(true);
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
m_statusLabel->setText("保存失败: " + reply->errorString());
|
||||
QMessageBox::warning(this, "保存失败", "映射配置保存失败:" + reply->errorString());
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
if (doc.isObject() && doc.object()["success"].toBool()) {
|
||||
m_statusLabel->setText("映射配置已保存");
|
||||
QMessageBox::information(this, "保存成功", "人脸ID映射配置已保存到服务器!\n\n签到时将自动使用真实学号。");
|
||||
emit mappingSaved();
|
||||
} else {
|
||||
m_statusLabel->setText("保存失败");
|
||||
QMessageBox::warning(this, "保存失败", "服务器返回错误");
|
||||
}
|
||||
}
|
||||
|
||||
void FaceManageDialog::fetchAllStudentIds()
|
||||
{
|
||||
QString url = m_serverUrl + "/api/all_student_ids";
|
||||
qDebug() << "[FaceManage] Fetching all student IDs from:" << url;
|
||||
|
||||
QNetworkRequest request(url);
|
||||
QNetworkReply *reply = m_networkManager->get(request);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
onAllStudentIdsReceived(reply);
|
||||
});
|
||||
}
|
||||
|
||||
void FaceManageDialog::onAllStudentIdsReceived(QNetworkReply *reply)
|
||||
{
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "[FaceManage] Failed to fetch student IDs:" << reply->errorString();
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
if (!doc.isObject()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray idsArray = doc.object()["student_ids"].toArray();
|
||||
qDebug() << "[FaceManage] Received" << idsArray.size() << "student IDs from records";
|
||||
|
||||
// 找出没有照片的学生ID
|
||||
int col = m_photoItems.size() % 4;
|
||||
int row = m_photoItems.size() / 4;
|
||||
int maxCols = 4;
|
||||
int addedCount = 0;
|
||||
|
||||
for (const QJsonValue &value : idsArray) {
|
||||
QString studentId = value.toString();
|
||||
|
||||
// 跳过已经有照片项的
|
||||
if (m_photoItems.contains(studentId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 创建无照片的项
|
||||
FacePhotoItem *item = new FacePhotoItem(studentId, false, m_contentWidget);
|
||||
item->setStudentId(studentId);
|
||||
|
||||
// 连接信号
|
||||
connect(item, &FacePhotoItem::studentIdChanged, this, [this](const QString &faceId, const QString &newStudentId) {
|
||||
m_idMapping[faceId] = newStudentId;
|
||||
});
|
||||
|
||||
connect(item, &FacePhotoItem::deleteRequested, this, &FaceManageDialog::onDeleteRequested);
|
||||
|
||||
m_photoItems[studentId] = item;
|
||||
m_gridLayout->addWidget(item, row, col);
|
||||
|
||||
col++;
|
||||
if (col >= maxCols) {
|
||||
col = 0;
|
||||
row++;
|
||||
}
|
||||
addedCount++;
|
||||
}
|
||||
|
||||
if (addedCount > 0) {
|
||||
qDebug() << "[FaceManage] Added" << addedCount << "items without photos";
|
||||
}
|
||||
|
||||
m_statusLabel->setText(QString("共 %1 条人脸信息").arg(m_photoItems.size()));
|
||||
m_refreshButton->setEnabled(true);
|
||||
}
|
||||
|
||||
void FaceManageDialog::onDeleteRequested(const QString &studentId)
|
||||
{
|
||||
QMessageBox msgBox(this);
|
||||
msgBox.setWindowTitle("确认删除");
|
||||
msgBox.setText(QString("确定要删除 ID: %1 的人脸信息吗?").arg(studentId));
|
||||
msgBox.setInformativeText("请选择是否同时删除该学生的所有考勤记录:");
|
||||
|
||||
QPushButton *deleteAllBtn = msgBox.addButton("删除人脸和考勤记录", QMessageBox::DestructiveRole);
|
||||
QPushButton *deleteFaceOnlyBtn = msgBox.addButton("仅删除人脸信息", QMessageBox::AcceptRole);
|
||||
QPushButton *cancelBtn = msgBox.addButton("取消", QMessageBox::RejectRole);
|
||||
|
||||
msgBox.setDefaultButton(cancelBtn);
|
||||
msgBox.exec();
|
||||
|
||||
if (msgBox.clickedButton() == deleteAllBtn) {
|
||||
deleteFace(studentId, true);
|
||||
} else if (msgBox.clickedButton() == deleteFaceOnlyBtn) {
|
||||
deleteFace(studentId, false);
|
||||
}
|
||||
}
|
||||
|
||||
void FaceManageDialog::deleteFace(const QString &studentId, bool deleteRecords)
|
||||
{
|
||||
QString url = m_serverUrl + "/api/face/" + studentId;
|
||||
if (deleteRecords) {
|
||||
url += "?delete_records=true";
|
||||
}
|
||||
|
||||
qDebug() << "[FaceManage] Deleting face:" << url;
|
||||
m_statusLabel->setText("正在删除...");
|
||||
|
||||
QNetworkRequest request(url);
|
||||
QNetworkReply *reply = m_networkManager->deleteResource(request);
|
||||
reply->setProperty("studentId", studentId);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
onFaceDeleted(reply);
|
||||
});
|
||||
}
|
||||
|
||||
void FaceManageDialog::onFaceDeleted(QNetworkReply *reply)
|
||||
{
|
||||
QString studentId = reply->property("studentId").toString();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
m_statusLabel->setText("删除失败: " + reply->errorString());
|
||||
QMessageBox::warning(this, "删除失败", "删除人脸信息失败:" + reply->errorString());
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
if (doc.isObject() && doc.object()["success"].toBool()) {
|
||||
int recordsDeleted = doc.object()["records_deleted"].toInt();
|
||||
|
||||
// 从界面中移除该项
|
||||
if (m_photoItems.contains(studentId)) {
|
||||
FacePhotoItem *item = m_photoItems.take(studentId);
|
||||
m_gridLayout->removeWidget(item);
|
||||
delete item;
|
||||
}
|
||||
|
||||
// 从映射中移除
|
||||
m_idMapping.remove(studentId);
|
||||
|
||||
QString message = QString("已删除 ID: %1 的人脸信息").arg(studentId);
|
||||
if (recordsDeleted > 0) {
|
||||
message += QString("\n同时删除了 %1 条考勤记录").arg(recordsDeleted);
|
||||
}
|
||||
|
||||
m_statusLabel->setText(QString("共 %1 条人脸信息").arg(m_photoItems.size()));
|
||||
QMessageBox::information(this, "删除成功", message);
|
||||
} else {
|
||||
m_statusLabel->setText("删除失败");
|
||||
QMessageBox::warning(this, "删除失败", "服务器返回错误");
|
||||
}
|
||||
}
|
||||
+1589
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,472 @@
|
||||
#include "MappingConfigDialog.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QMessageBox>
|
||||
#include <QLabel>
|
||||
#include <QGroupBox>
|
||||
|
||||
MappingConfigDialog::MappingConfigDialog(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setupUI();
|
||||
setWindowTitle("名称映射配置");
|
||||
resize(700, 550);
|
||||
}
|
||||
|
||||
MappingConfigDialog::~MappingConfigDialog()
|
||||
{
|
||||
}
|
||||
|
||||
void MappingConfigDialog::setupUI()
|
||||
{
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setSpacing(16);
|
||||
mainLayout->setContentsMargins(20, 20, 20, 20);
|
||||
|
||||
// 创建选项卡
|
||||
m_tabWidget = new QTabWidget();
|
||||
m_tabWidget->setStyleSheet(R"(
|
||||
QTabWidget::pane {
|
||||
border: 1px solid #DCDFE6;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
}
|
||||
QTabBar::tab {
|
||||
padding: 8px 20px;
|
||||
margin-right: 4px;
|
||||
border: 1px solid #DCDFE6;
|
||||
border-bottom: none;
|
||||
border-radius: 4px 4px 0 0;
|
||||
background: #F5F7FA;
|
||||
}
|
||||
QTabBar::tab:selected {
|
||||
background: white;
|
||||
border-bottom: 1px solid white;
|
||||
}
|
||||
)");
|
||||
|
||||
// ============= 学号-姓名映射选项卡 =============
|
||||
QWidget *studentTab = new QWidget();
|
||||
QVBoxLayout *studentLayout = new QVBoxLayout(studentTab);
|
||||
studentLayout->setSpacing(12);
|
||||
|
||||
// 说明标签
|
||||
QLabel *studentDesc = new QLabel(
|
||||
"💡 提示:双击单元格可直接编辑内容。修改完成后点击「保存到服务器」同步数据。");
|
||||
studentDesc->setWordWrap(true);
|
||||
studentDesc->setStyleSheet("color: #909399; padding: 8px; background: #F5F7FA; border-radius: 4px;");
|
||||
studentLayout->addWidget(studentDesc);
|
||||
|
||||
// 学号表格 - 启用编辑
|
||||
m_studentTable = new QTableWidget(0, 2);
|
||||
m_studentTable->setHorizontalHeaderLabels(QStringList() << "学号" << "真实姓名");
|
||||
m_studentTable->horizontalHeader()->setStretchLastSection(true);
|
||||
m_studentTable->setColumnWidth(0, 150);
|
||||
m_studentTable->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_studentTable->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_studentTable->setAlternatingRowColors(true);
|
||||
// 允许双击编辑
|
||||
m_studentTable->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed);
|
||||
m_studentTable->setStyleSheet(R"(
|
||||
QTableWidget {
|
||||
border: 1px solid #EBEEF5;
|
||||
border-radius: 4px;
|
||||
gridline-color: #EBEEF5;
|
||||
}
|
||||
QTableWidget::item {
|
||||
padding: 8px;
|
||||
}
|
||||
QTableWidget::item:selected {
|
||||
background-color: #ECF5FF;
|
||||
color: #409EFF;
|
||||
}
|
||||
QHeaderView::section {
|
||||
background-color: #F5F7FA;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-bottom: 1px solid #EBEEF5;
|
||||
font-weight: bold;
|
||||
}
|
||||
)");
|
||||
studentLayout->addWidget(m_studentTable);
|
||||
|
||||
// 按钮区
|
||||
QHBoxLayout *studentBtnLayout = new QHBoxLayout();
|
||||
|
||||
m_addStudentBtn = new QPushButton("➕ 添加新行");
|
||||
m_addStudentBtn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #67C23A;
|
||||
color: white;
|
||||
padding: 8px 20px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover { background-color: #85CE61; }
|
||||
)");
|
||||
connect(m_addStudentBtn, &QPushButton::clicked, this, &MappingConfigDialog::onAddStudent);
|
||||
studentBtnLayout->addWidget(m_addStudentBtn);
|
||||
|
||||
m_deleteStudentBtn = new QPushButton("🗑️ 删除选中");
|
||||
m_deleteStudentBtn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #F56C6C;
|
||||
color: white;
|
||||
padding: 8px 20px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover { background-color: #F78989; }
|
||||
)");
|
||||
connect(m_deleteStudentBtn, &QPushButton::clicked, this, &MappingConfigDialog::onDeleteStudent);
|
||||
studentBtnLayout->addWidget(m_deleteStudentBtn);
|
||||
|
||||
studentBtnLayout->addStretch();
|
||||
studentLayout->addLayout(studentBtnLayout);
|
||||
|
||||
m_tabWidget->addTab(studentTab, "📋 学号-姓名映射");
|
||||
|
||||
// ============= 班级编号-名称映射选项卡 =============
|
||||
QWidget *classTab = new QWidget();
|
||||
QVBoxLayout *classLayout = new QVBoxLayout(classTab);
|
||||
classLayout->setSpacing(12);
|
||||
|
||||
// 说明标签
|
||||
QLabel *classDesc = new QLabel(
|
||||
"💡 提示:双击单元格可直接编辑内容。班级编号对应STM32设备上定义的CLASS_ID。");
|
||||
classDesc->setWordWrap(true);
|
||||
classDesc->setStyleSheet("color: #909399; padding: 8px; background: #F5F7FA; border-radius: 4px;");
|
||||
classLayout->addWidget(classDesc);
|
||||
|
||||
// 班级表格 - 启用编辑
|
||||
m_classTable = new QTableWidget(0, 2);
|
||||
m_classTable->setHorizontalHeaderLabels(QStringList() << "班级编号" << "班级名称");
|
||||
m_classTable->horizontalHeader()->setStretchLastSection(true);
|
||||
m_classTable->setColumnWidth(0, 150);
|
||||
m_classTable->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_classTable->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_classTable->setAlternatingRowColors(true);
|
||||
// 允许双击编辑
|
||||
m_classTable->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed);
|
||||
m_classTable->setStyleSheet(R"(
|
||||
QTableWidget {
|
||||
border: 1px solid #EBEEF5;
|
||||
border-radius: 4px;
|
||||
gridline-color: #EBEEF5;
|
||||
}
|
||||
QTableWidget::item {
|
||||
padding: 8px;
|
||||
}
|
||||
QTableWidget::item:selected {
|
||||
background-color: #ECF5FF;
|
||||
color: #409EFF;
|
||||
}
|
||||
QHeaderView::section {
|
||||
background-color: #F5F7FA;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-bottom: 1px solid #EBEEF5;
|
||||
font-weight: bold;
|
||||
}
|
||||
)");
|
||||
classLayout->addWidget(m_classTable);
|
||||
|
||||
// 按钮区
|
||||
QHBoxLayout *classBtnLayout = new QHBoxLayout();
|
||||
|
||||
m_addClassBtn = new QPushButton("➕ 添加新行");
|
||||
m_addClassBtn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #67C23A;
|
||||
color: white;
|
||||
padding: 8px 20px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover { background-color: #85CE61; }
|
||||
)");
|
||||
connect(m_addClassBtn, &QPushButton::clicked, this, &MappingConfigDialog::onAddClass);
|
||||
classBtnLayout->addWidget(m_addClassBtn);
|
||||
|
||||
m_deleteClassBtn = new QPushButton("🗑️ 删除选中");
|
||||
m_deleteClassBtn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #F56C6C;
|
||||
color: white;
|
||||
padding: 8px 20px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover { background-color: #F78989; }
|
||||
)");
|
||||
connect(m_deleteClassBtn, &QPushButton::clicked, this, &MappingConfigDialog::onDeleteClass);
|
||||
classBtnLayout->addWidget(m_deleteClassBtn);
|
||||
|
||||
classBtnLayout->addStretch();
|
||||
classLayout->addLayout(classBtnLayout);
|
||||
|
||||
m_tabWidget->addTab(classTab, "🏫 班级编号-名称映射");
|
||||
|
||||
mainLayout->addWidget(m_tabWidget);
|
||||
|
||||
// ============= 状态标签 =============
|
||||
m_statusLabel = new QLabel("准备就绪");
|
||||
m_statusLabel->setStyleSheet("color: #909399; padding: 4px;");
|
||||
mainLayout->addWidget(m_statusLabel);
|
||||
|
||||
// ============= 底部按钮区 =============
|
||||
QHBoxLayout *buttonLayout = new QHBoxLayout();
|
||||
|
||||
m_syncFromServerBtn = new QPushButton("🔄 从服务器加载");
|
||||
m_syncFromServerBtn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #409EFF;
|
||||
color: white;
|
||||
padding: 10px 24px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover { background-color: #66B1FF; }
|
||||
)");
|
||||
connect(m_syncFromServerBtn, &QPushButton::clicked, this, &MappingConfigDialog::onSyncFromServer);
|
||||
buttonLayout->addWidget(m_syncFromServerBtn);
|
||||
|
||||
buttonLayout->addStretch();
|
||||
|
||||
m_saveBtn = new QPushButton("💾 保存到服务器");
|
||||
m_saveBtn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #67C23A;
|
||||
color: white;
|
||||
padding: 10px 24px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover { background-color: #85CE61; }
|
||||
)");
|
||||
connect(m_saveBtn, &QPushButton::clicked, this, &MappingConfigDialog::onSave);
|
||||
buttonLayout->addWidget(m_saveBtn);
|
||||
|
||||
m_cancelBtn = new QPushButton("取消");
|
||||
m_cancelBtn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #909399;
|
||||
color: white;
|
||||
padding: 10px 24px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
}
|
||||
QPushButton:hover { background-color: #A6A9AD; }
|
||||
)");
|
||||
connect(m_cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
|
||||
buttonLayout->addWidget(m_cancelBtn);
|
||||
|
||||
mainLayout->addLayout(buttonLayout);
|
||||
}
|
||||
|
||||
void MappingConfigDialog::setStudentNameMap(const QMap<QString, QString> &map)
|
||||
{
|
||||
m_studentNameMap = map;
|
||||
updateStudentTable();
|
||||
}
|
||||
|
||||
void MappingConfigDialog::setClassNameMap(const QMap<QString, QString> &map)
|
||||
{
|
||||
m_classNameMap = map;
|
||||
updateClassTable();
|
||||
}
|
||||
|
||||
void MappingConfigDialog::updateStudentTable()
|
||||
{
|
||||
m_studentTable->setRowCount(0);
|
||||
|
||||
QList<QString> keys = m_studentNameMap.keys();
|
||||
// 按学号排序
|
||||
std::sort(keys.begin(), keys.end(), [](const QString &a, const QString &b) {
|
||||
return a.toInt() < b.toInt();
|
||||
});
|
||||
|
||||
for (const QString &key : keys) {
|
||||
int row = m_studentTable->rowCount();
|
||||
m_studentTable->insertRow(row);
|
||||
|
||||
QTableWidgetItem *idItem = new QTableWidgetItem(key);
|
||||
QTableWidgetItem *nameItem = new QTableWidgetItem(m_studentNameMap[key]);
|
||||
|
||||
m_studentTable->setItem(row, 0, idItem);
|
||||
m_studentTable->setItem(row, 1, nameItem);
|
||||
}
|
||||
|
||||
m_statusLabel->setText(QString("已加载 %1 条学号映射").arg(m_studentNameMap.size()));
|
||||
}
|
||||
|
||||
void MappingConfigDialog::updateClassTable()
|
||||
{
|
||||
m_classTable->setRowCount(0);
|
||||
|
||||
QList<QString> keys = m_classNameMap.keys();
|
||||
// 按编号排序
|
||||
std::sort(keys.begin(), keys.end(), [](const QString &a, const QString &b) {
|
||||
return a.toInt() < b.toInt();
|
||||
});
|
||||
|
||||
for (const QString &key : keys) {
|
||||
int row = m_classTable->rowCount();
|
||||
m_classTable->insertRow(row);
|
||||
|
||||
QTableWidgetItem *idItem = new QTableWidgetItem(key);
|
||||
QTableWidgetItem *nameItem = new QTableWidgetItem(m_classNameMap[key]);
|
||||
|
||||
m_classTable->setItem(row, 0, idItem);
|
||||
m_classTable->setItem(row, 1, nameItem);
|
||||
}
|
||||
}
|
||||
|
||||
void MappingConfigDialog::collectDataFromTables()
|
||||
{
|
||||
// 从学号表格收集数据
|
||||
m_studentNameMap.clear();
|
||||
for (int row = 0; row < m_studentTable->rowCount(); ++row) {
|
||||
QTableWidgetItem *idItem = m_studentTable->item(row, 0);
|
||||
QTableWidgetItem *nameItem = m_studentTable->item(row, 1);
|
||||
|
||||
if (idItem && nameItem) {
|
||||
QString id = idItem->text().trimmed();
|
||||
QString name = nameItem->text().trimmed();
|
||||
|
||||
if (!id.isEmpty() && !name.isEmpty()) {
|
||||
m_studentNameMap[id] = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从班级表格收集数据
|
||||
m_classNameMap.clear();
|
||||
for (int row = 0; row < m_classTable->rowCount(); ++row) {
|
||||
QTableWidgetItem *idItem = m_classTable->item(row, 0);
|
||||
QTableWidgetItem *nameItem = m_classTable->item(row, 1);
|
||||
|
||||
if (idItem && nameItem) {
|
||||
QString id = idItem->text().trimmed();
|
||||
QString name = nameItem->text().trimmed();
|
||||
|
||||
if (!id.isEmpty() && !name.isEmpty()) {
|
||||
m_classNameMap[id] = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MappingConfigDialog::onAddStudent()
|
||||
{
|
||||
int row = m_studentTable->rowCount();
|
||||
m_studentTable->insertRow(row);
|
||||
|
||||
// 创建可编辑的空项
|
||||
QTableWidgetItem *idItem = new QTableWidgetItem("");
|
||||
QTableWidgetItem *nameItem = new QTableWidgetItem("");
|
||||
|
||||
m_studentTable->setItem(row, 0, idItem);
|
||||
m_studentTable->setItem(row, 1, nameItem);
|
||||
|
||||
// 自动选中新行并开始编辑
|
||||
m_studentTable->selectRow(row);
|
||||
m_studentTable->editItem(idItem);
|
||||
|
||||
m_statusLabel->setText("已添加新行,请输入学号和姓名");
|
||||
}
|
||||
|
||||
void MappingConfigDialog::onDeleteStudent()
|
||||
{
|
||||
int row = m_studentTable->currentRow();
|
||||
if (row < 0) {
|
||||
QMessageBox::warning(this, "提示", "请先选择要删除的行!");
|
||||
return;
|
||||
}
|
||||
|
||||
QTableWidgetItem *idItem = m_studentTable->item(row, 0);
|
||||
QString id = idItem ? idItem->text() : "";
|
||||
|
||||
m_studentTable->removeRow(row);
|
||||
m_statusLabel->setText(QString("已删除学号 %1 的映射").arg(id));
|
||||
}
|
||||
|
||||
void MappingConfigDialog::onAddClass()
|
||||
{
|
||||
int row = m_classTable->rowCount();
|
||||
m_classTable->insertRow(row);
|
||||
|
||||
// 创建可编辑的空项
|
||||
QTableWidgetItem *idItem = new QTableWidgetItem("");
|
||||
QTableWidgetItem *nameItem = new QTableWidgetItem("");
|
||||
|
||||
m_classTable->setItem(row, 0, idItem);
|
||||
m_classTable->setItem(row, 1, nameItem);
|
||||
|
||||
// 自动选中新行并开始编辑
|
||||
m_classTable->selectRow(row);
|
||||
m_classTable->editItem(idItem);
|
||||
|
||||
m_statusLabel->setText("已添加新行,请输入班级编号和名称");
|
||||
}
|
||||
|
||||
void MappingConfigDialog::onDeleteClass()
|
||||
{
|
||||
int row = m_classTable->currentRow();
|
||||
if (row < 0) {
|
||||
QMessageBox::warning(this, "提示", "请先选择要删除的行!");
|
||||
return;
|
||||
}
|
||||
|
||||
QTableWidgetItem *idItem = m_classTable->item(row, 0);
|
||||
QString id = idItem ? idItem->text() : "";
|
||||
|
||||
m_classTable->removeRow(row);
|
||||
m_statusLabel->setText(QString("已删除班级编号 %1 的映射").arg(id));
|
||||
}
|
||||
|
||||
void MappingConfigDialog::onSave()
|
||||
{
|
||||
// 先从表格收集数据
|
||||
collectDataFromTables();
|
||||
|
||||
if (m_studentNameMap.isEmpty() && m_classNameMap.isEmpty()) {
|
||||
QMessageBox::StandardButton reply = QMessageBox::question(
|
||||
this, "确认",
|
||||
"映射表为空,确定要保存吗?",
|
||||
QMessageBox::Yes | QMessageBox::No
|
||||
);
|
||||
if (reply != QMessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_statusLabel->setText("正在保存到服务器...");
|
||||
emit mappingChanged();
|
||||
accept();
|
||||
}
|
||||
|
||||
void MappingConfigDialog::onSyncToServer()
|
||||
{
|
||||
collectDataFromTables();
|
||||
m_statusLabel->setText("正在同步到服务器...");
|
||||
emit mappingChanged();
|
||||
}
|
||||
|
||||
void MappingConfigDialog::onSyncFromServer()
|
||||
{
|
||||
m_statusLabel->setText("正在从服务器加载...");
|
||||
emit requestSyncFromServer();
|
||||
}
|
||||
|
||||
void MappingConfigDialog::setStatus(const QString &status)
|
||||
{
|
||||
m_statusLabel->setText(status);
|
||||
}
|
||||
@@ -0,0 +1,674 @@
|
||||
#include "NetworkApiManager.h"
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QUrlQuery>
|
||||
#include <QSettings>
|
||||
#include <QDebug>
|
||||
#include <QCryptographicHash>
|
||||
#include <QMessageAuthenticationCode>
|
||||
|
||||
// AES加密所需(使用QCA或OpenSSL)
|
||||
// 注:Qt没有内置AES,需要使用QtCrypto(QCA)或直接调用OpenSSL
|
||||
// 这里提供基于Qt内置功能的简化实现
|
||||
|
||||
NetworkApiManager::NetworkApiManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_networkManager(new QNetworkAccessManager(this))
|
||||
, m_encryptionEnabled(true) // ✅ 启用加密通信
|
||||
{
|
||||
connect(m_networkManager, &QNetworkAccessManager::finished,
|
||||
this, &NetworkApiManager::onReplyFinished);
|
||||
|
||||
// 统一服务器地址
|
||||
m_serverUrl = "http://27.106.113.205:8082";
|
||||
|
||||
qDebug() << "[CRYPTO] 加密通信已启用 (AES-128-CBC)";
|
||||
}
|
||||
|
||||
NetworkApiManager::~NetworkApiManager()
|
||||
{
|
||||
}
|
||||
|
||||
void NetworkApiManager::setServerUrl(const QString &url)
|
||||
{
|
||||
m_serverUrl = url;
|
||||
|
||||
// 保存到配置
|
||||
QSettings settings;
|
||||
settings.setValue("server/url", url);
|
||||
settings.sync();
|
||||
}
|
||||
|
||||
QString NetworkApiManager::buildApiUrl(const QString &endpoint, const QMap<QString, QString> ¶ms)
|
||||
{
|
||||
QString url = m_serverUrl + "/api" + endpoint;
|
||||
|
||||
if (!params.isEmpty()) {
|
||||
QUrlQuery query;
|
||||
for (auto it = params.begin(); it != params.end(); ++it) {
|
||||
query.addQueryItem(it.key(), it.value());
|
||||
}
|
||||
url += "?" + query.toString();
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
void NetworkApiManager::fetchRecords(int limit, int offset)
|
||||
{
|
||||
QMap<QString, QString> params;
|
||||
params["limit"] = QString::number(limit);
|
||||
params["offset"] = QString::number(offset);
|
||||
|
||||
QString url = buildApiUrl("/records", params);
|
||||
qDebug() << "Fetching records from:" << url;
|
||||
|
||||
QNetworkRequest request(url);
|
||||
|
||||
// 如果启用加密,添加请求头
|
||||
if (m_encryptionEnabled) {
|
||||
request.setRawHeader("X-Encrypted-Response", "true");
|
||||
qDebug() << "[CRYPTO] 请求加密响应 (X-Encrypted-Response: true)";
|
||||
}
|
||||
|
||||
QNetworkReply *reply = m_networkManager->get(request);
|
||||
reply->setProperty("requestType", "fetchRecords");
|
||||
}
|
||||
|
||||
void NetworkApiManager::fetchStatistics()
|
||||
{
|
||||
QString url = buildApiUrl("/statistics");
|
||||
qDebug() << "Fetching statistics from:" << url;
|
||||
|
||||
QNetworkRequest request(url);
|
||||
|
||||
// 如果启用加密,添加请求头
|
||||
if (m_encryptionEnabled) {
|
||||
request.setRawHeader("X-Encrypted-Response", "true");
|
||||
qDebug() << "[CRYPTO] 请求加密响应 (X-Encrypted-Response: true)";
|
||||
}
|
||||
|
||||
QNetworkReply *reply = m_networkManager->get(request);
|
||||
reply->setProperty("requestType", "fetchStatistics");
|
||||
}
|
||||
|
||||
void NetworkApiManager::clearAllRecords()
|
||||
{
|
||||
QString url = buildApiUrl("/clear");
|
||||
qDebug() << "Clearing records at:" << url;
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QNetworkReply *reply = m_networkManager->post(request, QByteArray());
|
||||
reply->setProperty("requestType", "clearRecords");
|
||||
}
|
||||
|
||||
void NetworkApiManager::deleteRecord(const QString ×tamp, const QString &studentId)
|
||||
{
|
||||
QMap<QString, QString> params;
|
||||
params["timestamp"] = timestamp;
|
||||
params["student_id"] = studentId;
|
||||
|
||||
QString url = buildApiUrl("/record", params);
|
||||
qDebug() << "Deleting record:" << url;
|
||||
|
||||
QNetworkRequest request(url);
|
||||
|
||||
// 使用自定义DELETE方法
|
||||
QNetworkReply *reply = m_networkManager->deleteResource(request);
|
||||
reply->setProperty("requestType", "deleteRecord");
|
||||
}
|
||||
|
||||
void NetworkApiManager::uploadTimeSlots(const QVector<TimeSlot> &timeSlots)
|
||||
{
|
||||
QString url = buildApiUrl("/timeslots");
|
||||
qDebug() << "Uploading time slots to:" << url;
|
||||
|
||||
// 构建JSON数据
|
||||
QJsonArray timeSlotsArray;
|
||||
for (const TimeSlot &slot : timeSlots) {
|
||||
QJsonObject slotObj;
|
||||
slotObj["name"] = slot.name;
|
||||
slotObj["start_time"] = slot.startTime.toString("HH:mm");
|
||||
slotObj["end_time"] = slot.endTime.toString("HH:mm");
|
||||
slotObj["enabled"] = slot.enabled;
|
||||
timeSlotsArray.append(slotObj);
|
||||
}
|
||||
|
||||
QJsonObject jsonObj;
|
||||
jsonObj["time_slots"] = timeSlotsArray;
|
||||
|
||||
QJsonDocument doc(jsonObj);
|
||||
QByteArray jsonData = doc.toJson();
|
||||
|
||||
qDebug() << "Time slots JSON:" << QString::fromUtf8(jsonData);
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QNetworkReply *reply = m_networkManager->post(request, jsonData);
|
||||
reply->setProperty("requestType", "uploadTimeSlots");
|
||||
}
|
||||
|
||||
void NetworkApiManager::checkHealth()
|
||||
{
|
||||
QString url = buildApiUrl("/health");
|
||||
|
||||
QNetworkRequest request(url);
|
||||
QNetworkReply *reply = m_networkManager->get(request);
|
||||
reply->setProperty("requestType", "checkHealth");
|
||||
}
|
||||
|
||||
void NetworkApiManager::onReplyFinished(QNetworkReply *reply)
|
||||
{
|
||||
QString requestType = reply->property("requestType").toString();
|
||||
|
||||
// 检查网络错误
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
QString errorMsg = QString("网络错误: %1").arg(reply->errorString());
|
||||
qWarning() << errorMsg;
|
||||
emit errorOccurred(errorMsg);
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取响应数据
|
||||
QByteArray responseData = reply->readAll();
|
||||
|
||||
// 如果启用了加密,先解密响应数据
|
||||
if (m_encryptionEnabled && (requestType == "fetchRecords" || requestType == "fetchStatistics")) {
|
||||
qDebug() << "========== 加密响应,开始解密 ==========";
|
||||
qDebug() << "加密数据长度:" << responseData.size();
|
||||
|
||||
QByteArray decryptedData = aesDecrypt(responseData);
|
||||
|
||||
if (decryptedData.isEmpty()) {
|
||||
qWarning() << "解密失败!";
|
||||
emit errorOccurred("服务器响应解密失败");
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "✓ 解密成功,数据长度:" << decryptedData.size();
|
||||
responseData = decryptedData;
|
||||
}
|
||||
|
||||
// 调试:打印原始响应数据
|
||||
qDebug() << "========== 服务器响应 ==========";
|
||||
qDebug() << "请求类型:" << requestType;
|
||||
qDebug() << "响应数据:" << QString::fromUtf8(responseData);
|
||||
qDebug() << "数据长度:" << responseData.size();
|
||||
qDebug() << "================================";
|
||||
|
||||
// 根据请求类型处理响应(其余代码保持不变)
|
||||
if (requestType == "fetchRecords") {
|
||||
QVector<AttendanceRecord> records = parseRecordsJson(responseData);
|
||||
qDebug() << "解析到" << records.size() << "条记录";
|
||||
emit recordsFetched(records);
|
||||
|
||||
} else if (requestType == "fetchStatistics") {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(responseData);
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
qWarning() << "统计数据JSON解析失败!";
|
||||
emit errorOccurred("统计数据格式错误");
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject obj = doc.object();
|
||||
int total = obj["total"].toInt();
|
||||
int today = obj["today"].toInt();
|
||||
QString serverTime = obj["server_time"].toString();
|
||||
|
||||
qDebug() << "统计信息: total=" << total << "today=" << today;
|
||||
emit statisticsFetched(total, today, serverTime);
|
||||
|
||||
} else if (requestType == "clearRecords") {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(responseData);
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
qWarning() << "清空记录响应JSON解析失败!";
|
||||
emit errorOccurred("服务器响应格式错误");
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject obj = doc.object();
|
||||
bool success = obj["success"].toBool();
|
||||
QString message = obj["message"].toString();
|
||||
|
||||
qDebug() << "清空记录结果: success=" << success << "message=" << message;
|
||||
emit recordsCleared(success);
|
||||
|
||||
} else if (requestType == "deleteRecord") {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(responseData);
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
qWarning() << "删除记录响应JSON解析失败!";
|
||||
emit errorOccurred("服务器响应格式错误");
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject obj = doc.object();
|
||||
bool success = obj["success"].toBool();
|
||||
QString message = obj["message"].toString();
|
||||
|
||||
qDebug() << "删除记录结果: success=" << success << "message=" << message;
|
||||
emit recordDeleted(success);
|
||||
|
||||
} else if (requestType == "uploadTimeSlots") {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(responseData);
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
qWarning() << "上传时间段响应JSON解析失败!";
|
||||
emit errorOccurred("服务器响应格式错误");
|
||||
emit timeSlotsUploaded(false);
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject obj = doc.object();
|
||||
bool success = obj["success"].toBool();
|
||||
QString message = obj["message"].toString();
|
||||
|
||||
qDebug() << "上传时间段结果: success=" << success << "message=" << message;
|
||||
emit timeSlotsUploaded(success);
|
||||
|
||||
} else if (requestType == "checkHealth") {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(responseData);
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
qWarning() << "健康检查响应JSON解析失败!";
|
||||
emit healthChecked(false);
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject obj = doc.object();
|
||||
bool healthy = (obj["status"].toString() == "healthy");
|
||||
|
||||
qDebug() << "健康检查: healthy=" << healthy;
|
||||
emit healthChecked(healthy);
|
||||
|
||||
} else if (requestType == "uploadMappingConfig") {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(responseData);
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
qWarning() << "上传映射配置响应JSON解析失败!";
|
||||
emit mappingConfigUploaded(false);
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject obj = doc.object();
|
||||
bool success = obj["success"].toBool();
|
||||
QString message = obj["message"].toString();
|
||||
|
||||
qDebug() << "上传映射配置结果: success=" << success << "message=" << message;
|
||||
emit mappingConfigUploaded(success);
|
||||
|
||||
} else if (requestType == "fetchMappingConfig") {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(responseData);
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
qWarning() << "获取映射配置响应JSON解析失败!";
|
||||
emit mappingConfigFetched(QMap<QString, QString>(), QMap<QString, QString>());
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject obj = doc.object();
|
||||
QMap<QString, QString> studentNameMap;
|
||||
QMap<QString, QString> classNameMap;
|
||||
|
||||
// 解析学号-姓名映射
|
||||
QJsonObject studentMapObj = obj["student_name_map"].toObject();
|
||||
for (auto it = studentMapObj.begin(); it != studentMapObj.end(); ++it) {
|
||||
studentNameMap[it.key()] = it.value().toString();
|
||||
}
|
||||
|
||||
// 解析班级编号-名称映射
|
||||
QJsonObject classMapObj = obj["class_name_map"].toObject();
|
||||
for (auto it = classMapObj.begin(); it != classMapObj.end(); ++it) {
|
||||
classNameMap[it.key()] = it.value().toString();
|
||||
}
|
||||
|
||||
qDebug() << "获取映射配置: 学生映射=" << studentNameMap.size() << "条, 班级映射=" << classNameMap.size() << "条";
|
||||
emit mappingConfigFetched(studentNameMap, classNameMap);
|
||||
|
||||
} else if (requestType == "fetchFacePhotos") {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(responseData);
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
qWarning() << "获取人脸照片列表响应JSON解析失败!";
|
||||
emit facePhotosFetched(QJsonArray());
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray photos = doc.object()["photos"].toArray();
|
||||
qDebug() << "获取人脸照片列表:" << photos.size() << "张";
|
||||
emit facePhotosFetched(photos);
|
||||
|
||||
} else if (requestType == "fetchFacePhoto") {
|
||||
QString faceId = reply->property("faceId").toString();
|
||||
qDebug() << "获取人脸照片:" << faceId << ", 大小:" << responseData.size() << "bytes";
|
||||
emit facePhotoFetched(faceId, responseData);
|
||||
|
||||
} else if (requestType == "uploadFaceIdMapping") {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(responseData);
|
||||
bool success = false;
|
||||
if (doc.isObject()) {
|
||||
success = doc.object()["success"].toBool();
|
||||
}
|
||||
qDebug() << "上传人脸ID映射结果:" << success;
|
||||
emit faceIdMappingUploaded(success);
|
||||
|
||||
} else if (requestType == "fetchFaceIdMapping") {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(responseData);
|
||||
QMap<QString, QString> mapping;
|
||||
|
||||
if (doc.isObject()) {
|
||||
QJsonObject mappingsObj = doc.object()["mappings"].toObject();
|
||||
for (auto it = mappingsObj.begin(); it != mappingsObj.end(); ++it) {
|
||||
mapping[it.key()] = it.value().toString();
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "获取人脸ID映射:" << mapping.size() << "条";
|
||||
emit faceIdMappingFetched(mapping);
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
QVector<AttendanceRecord> NetworkApiManager::parseRecordsJson(const QByteArray &json)
|
||||
{
|
||||
QVector<AttendanceRecord> records;
|
||||
|
||||
qDebug() << "开始解析JSON,数据长度:" << json.size();
|
||||
|
||||
// 检查JSON是否以[开头
|
||||
if (json.isEmpty() || json.at(0) != '[') {
|
||||
qWarning() << "JSON不是以[开头!";
|
||||
qWarning() << "前100字符:" << QString::fromUtf8(json).left(100);
|
||||
return records;
|
||||
}
|
||||
|
||||
// 检查JSON是否有非法字符
|
||||
for (int i = 0; i < qMin(json.size(), 1000); i++) {
|
||||
unsigned char c = json.at(i);
|
||||
if (c < 32 && c != '\n' && c != '\t' && c != '\r') {
|
||||
qWarning() << "发现非法控制字符在位置" << i << ":" << (int)c;
|
||||
}
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(json, &parseError);
|
||||
|
||||
if (parseError.error != QJsonParseError::NoError) {
|
||||
qWarning() << "JSON解析错误:" << parseError.errorString();
|
||||
qWarning() << "错误位置:" << parseError.offset;
|
||||
qWarning() << "错误附近内容:" << QString::fromUtf8(json).mid(parseError.offset - 50, 100);
|
||||
return records;
|
||||
}
|
||||
|
||||
if (!doc.isArray()) {
|
||||
qWarning() << "无效的JSON格式 - 期望数组";
|
||||
qWarning() << "实际收到:" << QString::fromUtf8(json).left(200);
|
||||
return records;
|
||||
}
|
||||
|
||||
QJsonArray array = doc.array();
|
||||
qDebug() << "JSON数组包含" << array.size() << "个元素";
|
||||
|
||||
for (const QJsonValue &value : array) {
|
||||
if (!value.isObject()) {
|
||||
qWarning() << "跳过非对象元素";
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject obj = value.toObject();
|
||||
|
||||
// 读取字段并清理空白字符(包括\r\n)
|
||||
QString timestamp = obj["timestamp"].toString().trimmed();
|
||||
QString studentId = obj["student_id"].toString().trimmed();
|
||||
QString name = obj["name"].toString().trimmed();
|
||||
float confidence = obj["confidence"].toDouble();
|
||||
QString status = obj["status"].toString().trimmed(); // 清理\r\n等空白字符
|
||||
QString classId = obj["class_id"].toString().trimmed(); // 班级编号
|
||||
if (classId.isEmpty()) classId = "0"; // 兼容旧数据
|
||||
|
||||
// 验证数据完整性
|
||||
if (timestamp.isEmpty() || studentId.isEmpty()) {
|
||||
qWarning() << "跳过不完整的记录: timestamp=" << timestamp << "studentId=" << studentId;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 跳过错误的时间戳(以"0000-"开头的)
|
||||
if (timestamp.startsWith("0000-")) {
|
||||
qDebug() << "跳过错误的时间戳:" << timestamp << "学号:" << studentId;
|
||||
continue;
|
||||
}
|
||||
|
||||
AttendanceRecord record(timestamp, studentId, name, confidence, status, classId);
|
||||
records.append(record);
|
||||
}
|
||||
|
||||
qDebug() << "成功解析" << records.size() << "条有效记录";
|
||||
return records;
|
||||
}
|
||||
|
||||
// ============= AES-128-CBC解密实现 =============
|
||||
// 注:需要链接OpenSSL库
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/aes.h>
|
||||
#else
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/aes.h>
|
||||
#endif
|
||||
|
||||
QByteArray NetworkApiManager::removePKCS7Padding(const QByteArray &data)
|
||||
{
|
||||
if (data.isEmpty()) {
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
// 获取最后一个字节作为填充长度
|
||||
unsigned char paddingLen = (unsigned char)data.at(data.size() - 1);
|
||||
|
||||
// 验证填充长度
|
||||
if (paddingLen < 1 || paddingLen > 16 || paddingLen > data.size()) {
|
||||
qWarning() << "[CRYPTO] Invalid padding length:" << paddingLen;
|
||||
return data;
|
||||
}
|
||||
|
||||
// 验证所有填充字节都相同
|
||||
for (int i = data.size() - paddingLen; i < data.size(); i++) {
|
||||
if ((unsigned char)data.at(i) != paddingLen) {
|
||||
qWarning() << "[CRYPTO] Invalid padding at position" << i;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
// 移除填充
|
||||
return data.left(data.size() - paddingLen);
|
||||
}
|
||||
|
||||
QByteArray NetworkApiManager::aesDecrypt(const QByteArray &encryptedBase64)
|
||||
{
|
||||
// 与STM32和服务器相同的密钥和IV
|
||||
static const unsigned char AES_KEY[16] = {
|
||||
0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
|
||||
0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c
|
||||
};
|
||||
|
||||
static const unsigned char AES_IV[16] = {
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
|
||||
};
|
||||
|
||||
// 1. Base64解码
|
||||
QByteArray encryptedData = QByteArray::fromBase64(encryptedBase64);
|
||||
if (encryptedData.isEmpty() || encryptedData.size() % 16 != 0) {
|
||||
qWarning() << "[CRYPTO] Invalid encrypted data length:" << encryptedData.size();
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
qDebug() << "[CRYPTO] Encrypted data length:" << encryptedData.size();
|
||||
|
||||
// 2. 使用OpenSSL进行AES-128-CBC解密
|
||||
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
|
||||
if (!ctx) {
|
||||
qWarning() << "[CRYPTO] Failed to create context";
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
// 初始化解密
|
||||
if (EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, AES_KEY, AES_IV) != 1) {
|
||||
qWarning() << "[CRYPTO] DecryptInit failed";
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
// 禁用自动填充验证(手动处理PKCS#7)
|
||||
EVP_CIPHER_CTX_set_padding(ctx, 0);
|
||||
|
||||
// 准备输出缓冲区
|
||||
QByteArray plaintext;
|
||||
plaintext.resize(encryptedData.size() + 16);
|
||||
|
||||
int len = 0;
|
||||
int plaintextLen = 0;
|
||||
|
||||
// 解密
|
||||
if (EVP_DecryptUpdate(ctx, (unsigned char*)plaintext.data(), &len,
|
||||
(unsigned char*)encryptedData.constData(),
|
||||
encryptedData.size()) != 1) {
|
||||
qWarning() << "[CRYPTO] DecryptUpdate failed";
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
return QByteArray();
|
||||
}
|
||||
plaintextLen = len;
|
||||
|
||||
// 完成解密
|
||||
if (EVP_DecryptFinal_ex(ctx, (unsigned char*)plaintext.data() + len, &len) != 1) {
|
||||
qWarning() << "[CRYPTO] DecryptFinal failed";
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
return QByteArray();
|
||||
}
|
||||
plaintextLen += len;
|
||||
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
|
||||
plaintext.resize(plaintextLen);
|
||||
|
||||
// 3. 移除PKCS#7填充
|
||||
QByteArray result = removePKCS7Padding(plaintext);
|
||||
|
||||
qDebug() << "[CRYPTO] ✓ Decryption successful, length:" << result.size();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============= 映射配置上传(加密) =============
|
||||
void NetworkApiManager::uploadMappingConfig(const QMap<QString, QString> &studentNameMap,
|
||||
const QMap<QString, QString> &classNameMap)
|
||||
{
|
||||
QString url = buildApiUrl("/mapping_config");
|
||||
qDebug() << "Uploading mapping config to:" << url;
|
||||
|
||||
// 构建JSON数据
|
||||
QJsonObject studentMapObj;
|
||||
for (auto it = studentNameMap.begin(); it != studentNameMap.end(); ++it) {
|
||||
studentMapObj[it.key()] = it.value();
|
||||
}
|
||||
|
||||
QJsonObject classMapObj;
|
||||
for (auto it = classNameMap.begin(); it != classNameMap.end(); ++it) {
|
||||
classMapObj[it.key()] = it.value();
|
||||
}
|
||||
|
||||
QJsonObject jsonObj;
|
||||
jsonObj["student_name_map"] = studentMapObj;
|
||||
jsonObj["class_name_map"] = classMapObj;
|
||||
|
||||
QJsonDocument doc(jsonObj);
|
||||
QByteArray jsonData = doc.toJson(QJsonDocument::Compact);
|
||||
|
||||
qDebug() << "Mapping config JSON:" << QString::fromUtf8(jsonData);
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QNetworkReply *reply = m_networkManager->post(request, jsonData);
|
||||
reply->setProperty("requestType", "uploadMappingConfig");
|
||||
}
|
||||
|
||||
// ============= 映射配置下载 =============
|
||||
void NetworkApiManager::fetchMappingConfig()
|
||||
{
|
||||
QString url = buildApiUrl("/mapping_config");
|
||||
qDebug() << "Fetching mapping config from:" << url;
|
||||
|
||||
QNetworkRequest request(url);
|
||||
QNetworkReply *reply = m_networkManager->get(request);
|
||||
reply->setProperty("requestType", "fetchMappingConfig");
|
||||
}
|
||||
|
||||
// ============= 人脸照片管理API =============
|
||||
|
||||
void NetworkApiManager::fetchFacePhotos()
|
||||
{
|
||||
QString url = buildApiUrl("/face_photos");
|
||||
qDebug() << "Fetching face photos list from:" << url;
|
||||
|
||||
QNetworkRequest request(url);
|
||||
QNetworkReply *reply = m_networkManager->get(request);
|
||||
reply->setProperty("requestType", "fetchFacePhotos");
|
||||
}
|
||||
|
||||
void NetworkApiManager::fetchFacePhoto(const QString &faceId)
|
||||
{
|
||||
QString url = m_serverUrl + "/api/face_photo/" + faceId;
|
||||
qDebug() << "Fetching face photo:" << url;
|
||||
|
||||
QNetworkRequest request(url);
|
||||
QNetworkReply *reply = m_networkManager->get(request);
|
||||
reply->setProperty("requestType", "fetchFacePhoto");
|
||||
reply->setProperty("faceId", faceId);
|
||||
}
|
||||
|
||||
void NetworkApiManager::uploadFaceIdMapping(const QMap<QString, QString> &mapping)
|
||||
{
|
||||
QString url = buildApiUrl("/face_id_mapping");
|
||||
qDebug() << "Uploading face ID mapping to:" << url;
|
||||
|
||||
// 构建JSON数据
|
||||
QJsonObject mappingsObj;
|
||||
for (auto it = mapping.begin(); it != mapping.end(); ++it) {
|
||||
mappingsObj[it.key()] = it.value();
|
||||
}
|
||||
|
||||
QJsonObject jsonObj;
|
||||
jsonObj["mappings"] = mappingsObj;
|
||||
|
||||
QJsonDocument doc(jsonObj);
|
||||
QByteArray jsonData = doc.toJson(QJsonDocument::Compact);
|
||||
|
||||
qDebug() << "Face ID mapping JSON:" << QString::fromUtf8(jsonData);
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QNetworkReply *reply = m_networkManager->post(request, jsonData);
|
||||
reply->setProperty("requestType", "uploadFaceIdMapping");
|
||||
}
|
||||
|
||||
void NetworkApiManager::fetchFaceIdMapping()
|
||||
{
|
||||
QString url = buildApiUrl("/face_id_mapping");
|
||||
qDebug() << "Fetching face ID mapping from:" << url;
|
||||
|
||||
QNetworkRequest request(url);
|
||||
QNetworkReply *reply = m_networkManager->get(request);
|
||||
reply->setProperty("requestType", "fetchFaceIdMapping");
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
#include "ServerConfigDialog.h"
|
||||
#include "NetworkApiManager.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QFormLayout>
|
||||
#include <QMessageBox>
|
||||
#include <QLabel>
|
||||
|
||||
ServerConfigDialog::ServerConfigDialog(const QString ¤tUrl, QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, m_serverUrl(currentUrl)
|
||||
{
|
||||
// 设置窗口标志,使对话框相对于父窗口居中
|
||||
setWindowModality(Qt::WindowModal);
|
||||
|
||||
setupUI();
|
||||
m_urlEdit->setText(currentUrl);
|
||||
}
|
||||
|
||||
void ServerConfigDialog::setupUI()
|
||||
{
|
||||
setWindowTitle("服务器配置");
|
||||
resize(500, 200);
|
||||
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
|
||||
// 说明文字
|
||||
QLabel *infoLabel = new QLabel(
|
||||
"配置考勤服务器地址,确保服务器已启动并可访问。\n"
|
||||
"格式: http://IP地址:端口 (例如: http://192.168.1.100:8082)");
|
||||
infoLabel->setStyleSheet("color: #666; padding: 10px; background-color: #f0f0f0; border-radius: 5px;");
|
||||
mainLayout->addWidget(infoLabel);
|
||||
|
||||
// 服务器地址输入
|
||||
QFormLayout *formLayout = new QFormLayout();
|
||||
|
||||
m_urlEdit = new QLineEdit();
|
||||
m_urlEdit->setPlaceholderText("http://192.168.1.100:8082");
|
||||
formLayout->addRow("服务器地址:", m_urlEdit);
|
||||
|
||||
mainLayout->addLayout(formLayout);
|
||||
|
||||
// 测试连接按钮和状态
|
||||
QHBoxLayout *testLayout = new QHBoxLayout();
|
||||
m_testButton = new QPushButton("测试连接");
|
||||
m_statusLabel = new QLabel("");
|
||||
|
||||
testLayout->addWidget(m_testButton);
|
||||
testLayout->addWidget(m_statusLabel);
|
||||
testLayout->addStretch();
|
||||
|
||||
mainLayout->addLayout(testLayout);
|
||||
|
||||
// 底部按钮
|
||||
QHBoxLayout *buttonLayout = new QHBoxLayout();
|
||||
QPushButton *saveButton = new QPushButton("保存");
|
||||
QPushButton *cancelButton = new QPushButton("取消");
|
||||
|
||||
saveButton->setStyleSheet("QPushButton { background-color: #4CAF50; color: white; padding: 8px 20px; }");
|
||||
cancelButton->setStyleSheet("QPushButton { padding: 8px 20px; }");
|
||||
|
||||
buttonLayout->addStretch();
|
||||
buttonLayout->addWidget(saveButton);
|
||||
buttonLayout->addWidget(cancelButton);
|
||||
|
||||
mainLayout->addLayout(buttonLayout);
|
||||
|
||||
// 连接信号
|
||||
connect(m_testButton, &QPushButton::clicked, this, &ServerConfigDialog::onTestConnection);
|
||||
connect(saveButton, &QPushButton::clicked, this, &ServerConfigDialog::onSave);
|
||||
connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject);
|
||||
}
|
||||
|
||||
void ServerConfigDialog::onTestConnection()
|
||||
{
|
||||
QString url = m_urlEdit->text().trimmed();
|
||||
|
||||
if (url.isEmpty()) {
|
||||
m_statusLabel->setText("❌ 请输入服务器地址");
|
||||
m_statusLabel->setStyleSheet("color: red;");
|
||||
return;
|
||||
}
|
||||
|
||||
m_statusLabel->setText("⏳ 测试中...");
|
||||
m_statusLabel->setStyleSheet("color: orange;");
|
||||
m_testButton->setEnabled(false);
|
||||
|
||||
// 创建临时网络管理器测试连接
|
||||
NetworkApiManager *tempManager = new NetworkApiManager(this);
|
||||
tempManager->setServerUrl(url);
|
||||
|
||||
connect(tempManager, &NetworkApiManager::healthChecked, this, [this](bool healthy) {
|
||||
if (healthy) {
|
||||
m_statusLabel->setText("✓ 连接成功");
|
||||
m_statusLabel->setStyleSheet("color: green;");
|
||||
} else {
|
||||
m_statusLabel->setText("❌ 服务器无响应");
|
||||
m_statusLabel->setStyleSheet("color: red;");
|
||||
}
|
||||
m_testButton->setEnabled(true);
|
||||
});
|
||||
|
||||
connect(tempManager, &NetworkApiManager::errorOccurred, this, [this](const QString &error) {
|
||||
m_statusLabel->setText("❌ " + error);
|
||||
m_statusLabel->setStyleSheet("color: red;");
|
||||
m_testButton->setEnabled(true);
|
||||
});
|
||||
|
||||
tempManager->checkHealth();
|
||||
}
|
||||
|
||||
void ServerConfigDialog::onSave()
|
||||
{
|
||||
QString url = m_urlEdit->text().trimmed();
|
||||
|
||||
if (url.isEmpty()) {
|
||||
QMessageBox::warning(this, "错误", "请输入服务器地址!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证URL格式
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
QMessageBox::warning(this, "错误", "服务器地址必须以 http:// 或 https:// 开头!");
|
||||
return;
|
||||
}
|
||||
|
||||
m_serverUrl = url;
|
||||
accept();
|
||||
}
|
||||
|
||||
QString ServerConfigDialog::getServerUrl() const
|
||||
{
|
||||
return m_serverUrl;
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
#include "SettingsDialog.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QMessageBox>
|
||||
#include <QLabel>
|
||||
#include <QFormLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QMoveEvent>
|
||||
|
||||
// ============= 对话框位置限制过滤器(本地实现) =============
|
||||
class LocalDialogPositionFilter : public QObject
|
||||
{
|
||||
public:
|
||||
explicit LocalDialogPositionFilter(QObject *parent = nullptr) : QObject(parent) {}
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *obj, QEvent *event) override
|
||||
{
|
||||
if (event->type() == QEvent::Move) {
|
||||
QDialog *dialog = qobject_cast<QDialog*>(obj);
|
||||
if (dialog && dialog->parentWidget()) {
|
||||
QWidget *parent = dialog->parentWidget();
|
||||
QRect parentRect = parent->geometry();
|
||||
QRect dialogRect = dialog->frameGeometry();
|
||||
|
||||
int x = dialog->x();
|
||||
int y = dialog->y();
|
||||
|
||||
if (x < parentRect.x()) x = parentRect.x();
|
||||
if (x + dialogRect.width() > parentRect.x() + parentRect.width()) {
|
||||
x = parentRect.x() + parentRect.width() - dialogRect.width();
|
||||
}
|
||||
if (y < parentRect.y()) y = parentRect.y();
|
||||
if (y + dialogRect.height() > parentRect.y() + parentRect.height()) {
|
||||
y = parentRect.y() + parentRect.height() - dialogRect.height();
|
||||
}
|
||||
|
||||
if (x != dialog->x() || y != dialog->y()) {
|
||||
dialog->move(x, y);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return QObject::eventFilter(obj, event);
|
||||
}
|
||||
};
|
||||
|
||||
SettingsDialog::SettingsDialog(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, m_apiManager(nullptr)
|
||||
{
|
||||
// 设置窗口标志,使对话框相对于父窗口居中
|
||||
setWindowModality(Qt::WindowModal);
|
||||
|
||||
// 创建网络API管理器
|
||||
m_apiManager = new NetworkApiManager(this);
|
||||
|
||||
// 连接上传时间段的响应信号
|
||||
connect(m_apiManager, &NetworkApiManager::timeSlotsUploaded,
|
||||
this, &SettingsDialog::onTimeSlotsUploaded);
|
||||
connect(m_apiManager, &NetworkApiManager::errorOccurred,
|
||||
this, [this](const QString &error) {
|
||||
QMessageBox::warning(this, "网络错误",
|
||||
QString("同步时间段设置到服务器失败:\n%1\n\n设置已保存到本地,但STM32设备可能未同步。").arg(error));
|
||||
});
|
||||
|
||||
setupUI();
|
||||
|
||||
// 初始化时从配置加载数据
|
||||
m_tempTimeSlots = TimeSlotConfig::instance().getTimeSlots();
|
||||
refreshTimeSlots(); // 显示加载的数据
|
||||
}
|
||||
|
||||
SettingsDialog::~SettingsDialog()
|
||||
{
|
||||
}
|
||||
|
||||
void SettingsDialog::setupUI()
|
||||
{
|
||||
setWindowTitle("签到时间段设置");
|
||||
resize(700, 500);
|
||||
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
|
||||
// 说明文字
|
||||
QLabel *infoLabel = new QLabel(
|
||||
"配置签到时间段,只有在设定的时间段内才能进行签到。\n"
|
||||
"✓ 设置保存后会自动同步到服务器,STM32设备会自动获取最新配置。");
|
||||
infoLabel->setStyleSheet("color: #666; padding: 10px; background-color: #f0f0f0; border-radius: 5px;");
|
||||
mainLayout->addWidget(infoLabel);
|
||||
|
||||
// 时间段表格
|
||||
m_tableWidget = new QTableWidget(0, 4);
|
||||
QStringList headers;
|
||||
headers << "时段名称" << "开始时间" << "结束时间" << "状态";
|
||||
m_tableWidget->setHorizontalHeaderLabels(headers);
|
||||
m_tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_tableWidget->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_tableWidget->horizontalHeader()->setStretchLastSection(true);
|
||||
m_tableWidget->setColumnWidth(0, 200);
|
||||
m_tableWidget->setColumnWidth(1, 150);
|
||||
m_tableWidget->setColumnWidth(2, 150);
|
||||
mainLayout->addWidget(m_tableWidget);
|
||||
|
||||
// 按钮区域
|
||||
QHBoxLayout *buttonLayout = new QHBoxLayout();
|
||||
|
||||
m_addButton = new QPushButton("添加时段");
|
||||
m_editButton = new QPushButton("编辑时段");
|
||||
m_deleteButton = new QPushButton("删除时段");
|
||||
m_resetButton = new QPushButton("恢复默认");
|
||||
|
||||
buttonLayout->addWidget(m_addButton);
|
||||
buttonLayout->addWidget(m_editButton);
|
||||
buttonLayout->addWidget(m_deleteButton);
|
||||
buttonLayout->addStretch();
|
||||
buttonLayout->addWidget(m_resetButton);
|
||||
|
||||
mainLayout->addLayout(buttonLayout);
|
||||
|
||||
// 底部按钮
|
||||
QHBoxLayout *bottomLayout = new QHBoxLayout();
|
||||
m_saveButton = new QPushButton("保存");
|
||||
m_cancelButton = new QPushButton("取消");
|
||||
|
||||
m_saveButton->setStyleSheet("QPushButton { background-color: #4CAF50; color: white; padding: 8px 20px; }");
|
||||
m_cancelButton->setStyleSheet("QPushButton { padding: 8px 20px; }");
|
||||
|
||||
bottomLayout->addStretch();
|
||||
bottomLayout->addWidget(m_saveButton);
|
||||
bottomLayout->addWidget(m_cancelButton);
|
||||
|
||||
mainLayout->addLayout(bottomLayout);
|
||||
|
||||
// 连接信号槽
|
||||
connect(m_addButton, &QPushButton::clicked, this, &SettingsDialog::onAddTimeSlot);
|
||||
connect(m_editButton, &QPushButton::clicked, this, &SettingsDialog::onEditTimeSlot);
|
||||
connect(m_deleteButton, &QPushButton::clicked, this, &SettingsDialog::onDeleteTimeSlot);
|
||||
connect(m_resetButton, &QPushButton::clicked, this, &SettingsDialog::onResetToDefault);
|
||||
connect(m_saveButton, &QPushButton::clicked, this, &SettingsDialog::onSave);
|
||||
connect(m_cancelButton, &QPushButton::clicked, this, &SettingsDialog::onCancel);
|
||||
connect(m_tableWidget, &QTableWidget::doubleClicked, this, &SettingsDialog::onEditTimeSlot);
|
||||
}
|
||||
|
||||
void SettingsDialog::loadTimeSlots()
|
||||
{
|
||||
// 只在初始化时从配置加载,之后只刷新表格显示
|
||||
// 不要重新加载,否则会覆盖临时修改
|
||||
// m_tempTimeSlots = TimeSlotConfig::instance().getTimeSlots();
|
||||
|
||||
m_tableWidget->setRowCount(0);
|
||||
|
||||
for (int i = 0; i < m_tempTimeSlots.size(); ++i) {
|
||||
const TimeSlot &slot = m_tempTimeSlots[i];
|
||||
|
||||
int row = m_tableWidget->rowCount();
|
||||
m_tableWidget->insertRow(row);
|
||||
|
||||
m_tableWidget->setItem(row, 0, new QTableWidgetItem(slot.name));
|
||||
m_tableWidget->setItem(row, 1, new QTableWidgetItem(slot.startTime.toString("HH:mm")));
|
||||
m_tableWidget->setItem(row, 2, new QTableWidgetItem(slot.endTime.toString("HH:mm")));
|
||||
|
||||
QTableWidgetItem *statusItem = new QTableWidgetItem(slot.enabled ? "启用" : "禁用");
|
||||
statusItem->setForeground(slot.enabled ? QBrush(Qt::darkGreen) : QBrush(Qt::red));
|
||||
m_tableWidget->setItem(row, 3, statusItem);
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsDialog::refreshTimeSlots()
|
||||
{
|
||||
// 新增:专门用于刷新表格显示,不改变m_tempTimeSlots
|
||||
m_tableWidget->setRowCount(0);
|
||||
|
||||
for (int i = 0; i < m_tempTimeSlots.size(); ++i) {
|
||||
const TimeSlot &slot = m_tempTimeSlots[i];
|
||||
|
||||
int row = m_tableWidget->rowCount();
|
||||
m_tableWidget->insertRow(row);
|
||||
|
||||
m_tableWidget->setItem(row, 0, new QTableWidgetItem(slot.name));
|
||||
m_tableWidget->setItem(row, 1, new QTableWidgetItem(slot.startTime.toString("HH:mm")));
|
||||
m_tableWidget->setItem(row, 2, new QTableWidgetItem(slot.endTime.toString("HH:mm")));
|
||||
|
||||
QTableWidgetItem *statusItem = new QTableWidgetItem(slot.enabled ? "启用" : "禁用");
|
||||
statusItem->setForeground(slot.enabled ? QBrush(Qt::darkGreen) : QBrush(Qt::red));
|
||||
m_tableWidget->setItem(row, 3, statusItem);
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsDialog::saveTimeSlots()
|
||||
{
|
||||
TimeSlotConfig::instance().setTimeSlots(m_tempTimeSlots);
|
||||
}
|
||||
|
||||
void SettingsDialog::onAddTimeSlot()
|
||||
{
|
||||
TimeSlotEditDialog dialog(this);
|
||||
|
||||
// 安装事件过滤器,限制对话框在父窗口内移动
|
||||
LocalDialogPositionFilter *filter = new LocalDialogPositionFilter(&dialog);
|
||||
dialog.installEventFilter(filter);
|
||||
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
TimeSlot newSlot = dialog.getTimeSlot();
|
||||
m_tempTimeSlots.append(newSlot);
|
||||
refreshTimeSlots(); // 使用 refreshTimeSlots 而不是 loadTimeSlots
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsDialog::onEditTimeSlot()
|
||||
{
|
||||
int row = m_tableWidget->currentRow();
|
||||
if (row < 0) {
|
||||
QMessageBox::warning(this, "提示", "请先选择要编辑的时段!");
|
||||
return;
|
||||
}
|
||||
|
||||
TimeSlotEditDialog dialog(this, &m_tempTimeSlots[row]);
|
||||
|
||||
// 安装事件过滤器,限制对话框在父窗口内移动
|
||||
LocalDialogPositionFilter *filter = new LocalDialogPositionFilter(&dialog);
|
||||
dialog.installEventFilter(filter);
|
||||
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
m_tempTimeSlots[row] = dialog.getTimeSlot();
|
||||
refreshTimeSlots(); // 使用 refreshTimeSlots 而不是 loadTimeSlots
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsDialog::onDeleteTimeSlot()
|
||||
{
|
||||
int row = m_tableWidget->currentRow();
|
||||
if (row < 0) {
|
||||
QMessageBox::warning(this, "提示", "请先选择要删除的时段!");
|
||||
return;
|
||||
}
|
||||
|
||||
QMessageBox::StandardButton reply = QMessageBox::question(
|
||||
this,
|
||||
"确认删除",
|
||||
QString("确定要删除时段 \"%1\" 吗?").arg(m_tempTimeSlots[row].name),
|
||||
QMessageBox::Yes | QMessageBox::No
|
||||
);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
m_tempTimeSlots.removeAt(row);
|
||||
refreshTimeSlots(); // 使用 refreshTimeSlots 而不是 loadTimeSlots
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsDialog::onResetToDefault()
|
||||
{
|
||||
QMessageBox::StandardButton reply = QMessageBox::question(
|
||||
this,
|
||||
"确认重置",
|
||||
"确定要恢复默认时间段设置吗?这将清除所有自定义设置。",
|
||||
QMessageBox::Yes | QMessageBox::No
|
||||
);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
TimeSlotConfig::instance().resetToDefault();
|
||||
m_tempTimeSlots = TimeSlotConfig::instance().getTimeSlots();
|
||||
refreshTimeSlots(); // 使用 refreshTimeSlots 而不是 loadTimeSlots
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsDialog::onSave()
|
||||
{
|
||||
if (m_tempTimeSlots.isEmpty()) {
|
||||
QMessageBox::warning(this, "警告", "至少需要设置一个签到时段!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存到本地配置文件
|
||||
saveTimeSlots();
|
||||
|
||||
// 上传到服务器,由服务器同步到STM32设备
|
||||
m_saveButton->setEnabled(false);
|
||||
m_saveButton->setText("正在同步...");
|
||||
m_apiManager->uploadTimeSlots(m_tempTimeSlots);
|
||||
}
|
||||
|
||||
void SettingsDialog::onTimeSlotsUploaded(bool success)
|
||||
{
|
||||
m_saveButton->setEnabled(true);
|
||||
m_saveButton->setText("保存");
|
||||
|
||||
if (success) {
|
||||
QMessageBox::information(this, "成功",
|
||||
"时间段设置已保存并成功同步到服务器!\n\n"
|
||||
"STM32设备将在下次连接时自动获取最新配置。");
|
||||
accept();
|
||||
} else {
|
||||
QMessageBox::warning(this, "部分成功",
|
||||
"时间段设置已保存到本地,但同步到服务器失败。\n\n"
|
||||
"请检查网络连接或稍后重试。\n"
|
||||
"STM32设备可能需要手动配置。");
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsDialog::onCancel()
|
||||
{
|
||||
reject();
|
||||
}
|
||||
|
||||
// ============= TimeSlotEditDialog =============
|
||||
|
||||
TimeSlotEditDialog::TimeSlotEditDialog(QWidget *parent, const TimeSlot *slot)
|
||||
: QDialog(parent)
|
||||
{
|
||||
// 设置窗口标志,使对话框相对于父窗口居中
|
||||
setWindowModality(Qt::WindowModal);
|
||||
|
||||
setupUI();
|
||||
|
||||
if (slot) {
|
||||
setWindowTitle("编辑时间段");
|
||||
m_nameEdit->setText(slot->name);
|
||||
m_startTimeEdit->setTime(slot->startTime);
|
||||
m_endTimeEdit->setTime(slot->endTime);
|
||||
m_enabledCheckBox->setChecked(slot->enabled);
|
||||
} else {
|
||||
setWindowTitle("添加时间段");
|
||||
m_startTimeEdit->setTime(QTime(8, 0));
|
||||
m_endTimeEdit->setTime(QTime(9, 0));
|
||||
m_enabledCheckBox->setChecked(true);
|
||||
}
|
||||
}
|
||||
|
||||
void TimeSlotEditDialog::setupUI()
|
||||
{
|
||||
resize(400, 250);
|
||||
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
|
||||
QFormLayout *formLayout = new QFormLayout();
|
||||
|
||||
m_nameEdit = new QLineEdit();
|
||||
m_nameEdit->setPlaceholderText("例如:上午签到");
|
||||
|
||||
m_startTimeEdit = new QTimeEdit();
|
||||
m_startTimeEdit->setDisplayFormat("HH:mm");
|
||||
|
||||
m_endTimeEdit = new QTimeEdit();
|
||||
m_endTimeEdit->setDisplayFormat("HH:mm");
|
||||
|
||||
m_enabledCheckBox = new QCheckBox("启用此时段");
|
||||
|
||||
formLayout->addRow("时段名称:", m_nameEdit);
|
||||
formLayout->addRow("开始时间:", m_startTimeEdit);
|
||||
formLayout->addRow("结束时间:", m_endTimeEdit);
|
||||
formLayout->addRow("", m_enabledCheckBox);
|
||||
|
||||
mainLayout->addLayout(formLayout);
|
||||
|
||||
// 按钮
|
||||
QHBoxLayout *buttonLayout = new QHBoxLayout();
|
||||
QPushButton *okButton = new QPushButton("确定");
|
||||
QPushButton *cancelButton = new QPushButton("取消");
|
||||
|
||||
okButton->setStyleSheet("QPushButton { background-color: #4CAF50; color: white; padding: 8px 20px; }");
|
||||
cancelButton->setStyleSheet("QPushButton { padding: 8px 20px; }");
|
||||
|
||||
buttonLayout->addStretch();
|
||||
buttonLayout->addWidget(okButton);
|
||||
buttonLayout->addWidget(cancelButton);
|
||||
|
||||
mainLayout->addLayout(buttonLayout);
|
||||
|
||||
connect(okButton, &QPushButton::clicked, this, &QDialog::accept);
|
||||
connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject);
|
||||
}
|
||||
|
||||
TimeSlot TimeSlotEditDialog::getTimeSlot() const
|
||||
{
|
||||
TimeSlot slot;
|
||||
slot.name = m_nameEdit->text().trimmed();
|
||||
slot.startTime = m_startTimeEdit->time();
|
||||
slot.endTime = m_endTimeEdit->time();
|
||||
slot.enabled = m_enabledCheckBox->isChecked();
|
||||
return slot;
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
#include "StatisticsDialog.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QGroupBox>
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QDateTime>
|
||||
|
||||
StatisticsDialog::StatisticsDialog(const QVector<AttendanceRecord> &records, QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, m_records(records)
|
||||
{
|
||||
// 设置窗口标志,使对话框相对于父窗口居中
|
||||
setWindowModality(Qt::WindowModal);
|
||||
|
||||
setupUI();
|
||||
calculateStatistics();
|
||||
updateTable();
|
||||
}
|
||||
|
||||
StatisticsDialog::~StatisticsDialog()
|
||||
{
|
||||
}
|
||||
|
||||
void StatisticsDialog::setupUI()
|
||||
{
|
||||
setWindowTitle("考勤统计报表");
|
||||
resize(900, 600);
|
||||
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
|
||||
// === 日期范围选择区域 ===
|
||||
QGroupBox *dateGroup = new QGroupBox("统计范围");
|
||||
QHBoxLayout *dateLayout = new QHBoxLayout(dateGroup);
|
||||
|
||||
dateLayout->addWidget(new QLabel("开始日期:"));
|
||||
m_startDateEdit = new QDateEdit();
|
||||
m_startDateEdit->setCalendarPopup(true);
|
||||
m_startDateEdit->setDate(QDate::currentDate().addDays(-30));
|
||||
dateLayout->addWidget(m_startDateEdit);
|
||||
|
||||
dateLayout->addWidget(new QLabel("结束日期:"));
|
||||
m_endDateEdit = new QDateEdit();
|
||||
m_endDateEdit->setCalendarPopup(true);
|
||||
m_endDateEdit->setDate(QDate::currentDate());
|
||||
dateLayout->addWidget(m_endDateEdit);
|
||||
|
||||
dateLayout->addWidget(new QLabel("快速选择:"));
|
||||
m_quickSelectCombo = new QComboBox();
|
||||
m_quickSelectCombo->addItems({"今天", "最近7天", "最近30天", "本月", "全部"});
|
||||
m_quickSelectCombo->setCurrentIndex(2);
|
||||
dateLayout->addWidget(m_quickSelectCombo);
|
||||
|
||||
dateLayout->addStretch();
|
||||
|
||||
mainLayout->addWidget(dateGroup);
|
||||
|
||||
// === 汇总信息 ===
|
||||
m_summaryLabel = new QLabel();
|
||||
m_summaryLabel->setStyleSheet("padding: 10px; background-color: #e3f2fd; border-radius: 5px;");
|
||||
QFont summaryFont;
|
||||
summaryFont.setPointSize(10);
|
||||
m_summaryLabel->setFont(summaryFont);
|
||||
mainLayout->addWidget(m_summaryLabel);
|
||||
|
||||
// === 统计表格 ===
|
||||
m_tableWidget = new QTableWidget(0, 6);
|
||||
QStringList headers;
|
||||
headers << "学号" << "姓名" << "总签到次数" << "今日签到" << "最后签到时间" << "平均置信度";
|
||||
m_tableWidget->setHorizontalHeaderLabels(headers);
|
||||
m_tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_tableWidget->setAlternatingRowColors(true);
|
||||
m_tableWidget->horizontalHeader()->setStretchLastSection(true);
|
||||
m_tableWidget->setColumnWidth(0, 100);
|
||||
m_tableWidget->setColumnWidth(1, 120);
|
||||
m_tableWidget->setColumnWidth(2, 120);
|
||||
m_tableWidget->setColumnWidth(3, 100);
|
||||
m_tableWidget->setColumnWidth(4, 180);
|
||||
mainLayout->addWidget(m_tableWidget);
|
||||
|
||||
// === 底部按钮 ===
|
||||
QHBoxLayout *buttonLayout = new QHBoxLayout();
|
||||
m_refreshButton = new QPushButton("刷新统计");
|
||||
m_exportButton = new QPushButton("导出报表");
|
||||
QPushButton *closeButton = new QPushButton("关闭");
|
||||
|
||||
m_exportButton->setStyleSheet("QPushButton { background-color: #2196F3; color: white; padding: 8px 20px; }");
|
||||
closeButton->setStyleSheet("QPushButton { padding: 8px 20px; }");
|
||||
|
||||
buttonLayout->addWidget(m_refreshButton);
|
||||
buttonLayout->addWidget(m_exportButton);
|
||||
buttonLayout->addStretch();
|
||||
buttonLayout->addWidget(closeButton);
|
||||
|
||||
mainLayout->addLayout(buttonLayout);
|
||||
|
||||
// 连接信号槽
|
||||
connect(m_startDateEdit, &QDateEdit::dateChanged, this, &StatisticsDialog::onDateRangeChanged);
|
||||
connect(m_endDateEdit, &QDateEdit::dateChanged, this, &StatisticsDialog::onDateRangeChanged);
|
||||
connect(m_quickSelectCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &StatisticsDialog::onDateRangeChanged);
|
||||
connect(m_exportButton, &QPushButton::clicked, this, &StatisticsDialog::onExportReport);
|
||||
connect(m_refreshButton, &QPushButton::clicked, this, &StatisticsDialog::onRefresh);
|
||||
connect(closeButton, &QPushButton::clicked, this, &QDialog::accept);
|
||||
}
|
||||
|
||||
void StatisticsDialog::calculateStatistics()
|
||||
{
|
||||
m_statsMap.clear();
|
||||
|
||||
QDate startDate = m_startDateEdit->date();
|
||||
QDate endDate = m_endDateEdit->date();
|
||||
QDate today = QDate::currentDate();
|
||||
|
||||
for (const AttendanceRecord &record : m_records) {
|
||||
QDate recordDate = record.getDateTime().date();
|
||||
|
||||
// 日期过滤
|
||||
if (recordDate < startDate || recordDate > endDate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString studentId = record.getStudentId();
|
||||
|
||||
if (!m_statsMap.contains(studentId)) {
|
||||
StudentStats stats;
|
||||
stats.studentId = studentId;
|
||||
stats.name = record.getName();
|
||||
stats.totalCount = 0;
|
||||
stats.todayCount = 0;
|
||||
stats.avgConfidence = 0.0;
|
||||
m_statsMap[studentId] = stats;
|
||||
}
|
||||
|
||||
StudentStats &stats = m_statsMap[studentId];
|
||||
stats.totalCount++;
|
||||
|
||||
if (recordDate == today) {
|
||||
stats.todayCount++;
|
||||
}
|
||||
|
||||
// 更新最后签到时间
|
||||
if (!stats.lastCheckIn.isValid() || record.getDateTime() > stats.lastCheckIn) {
|
||||
stats.lastCheckIn = record.getDateTime();
|
||||
}
|
||||
|
||||
// 累加置信度(稍后计算平均值)
|
||||
stats.avgConfidence += record.getConfidence();
|
||||
}
|
||||
|
||||
// 计算平均置信度
|
||||
for (auto it = m_statsMap.begin(); it != m_statsMap.end(); ++it) {
|
||||
if (it->totalCount > 0) {
|
||||
it->avgConfidence /= it->totalCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StatisticsDialog::updateTable()
|
||||
{
|
||||
m_tableWidget->setRowCount(0);
|
||||
|
||||
// 更新汇总信息
|
||||
int totalStudents = m_statsMap.size();
|
||||
int totalRecords = 0;
|
||||
for (const auto &stats : m_statsMap) {
|
||||
totalRecords += stats.totalCount;
|
||||
}
|
||||
|
||||
m_summaryLabel->setText(QString(
|
||||
"📊 统计汇总:共 %1 名学生,总签到记录 %2 次 | "
|
||||
"统计时间:%3 至 %4")
|
||||
.arg(totalStudents)
|
||||
.arg(totalRecords)
|
||||
.arg(m_startDateEdit->date().toString("yyyy-MM-dd"))
|
||||
.arg(m_endDateEdit->date().toString("yyyy-MM-dd")));
|
||||
|
||||
// 填充表格
|
||||
for (auto it = m_statsMap.begin(); it != m_statsMap.end(); ++it) {
|
||||
const StudentStats &stats = it.value();
|
||||
|
||||
int row = m_tableWidget->rowCount();
|
||||
m_tableWidget->insertRow(row);
|
||||
|
||||
m_tableWidget->setItem(row, 0, new QTableWidgetItem(stats.studentId));
|
||||
m_tableWidget->setItem(row, 1, new QTableWidgetItem(stats.name));
|
||||
|
||||
QTableWidgetItem *totalItem = new QTableWidgetItem(QString::number(stats.totalCount));
|
||||
totalItem->setTextAlignment(Qt::AlignCenter);
|
||||
m_tableWidget->setItem(row, 2, totalItem);
|
||||
|
||||
QTableWidgetItem *todayItem = new QTableWidgetItem(QString::number(stats.todayCount));
|
||||
todayItem->setTextAlignment(Qt::AlignCenter);
|
||||
m_tableWidget->setItem(row, 3, todayItem);
|
||||
|
||||
QString lastCheckInStr = stats.lastCheckIn.isValid()
|
||||
? stats.lastCheckIn.toString("yyyy-MM-dd HH:mm:ss")
|
||||
: "无";
|
||||
m_tableWidget->setItem(row, 4, new QTableWidgetItem(lastCheckInStr));
|
||||
|
||||
QTableWidgetItem *confItem = new QTableWidgetItem(
|
||||
QString::number(stats.avgConfidence * 100, 'f', 2) + "%");
|
||||
confItem->setTextAlignment(Qt::AlignCenter);
|
||||
m_tableWidget->setItem(row, 5, confItem);
|
||||
}
|
||||
}
|
||||
|
||||
void StatisticsDialog::onDateRangeChanged()
|
||||
{
|
||||
// 处理快速选择
|
||||
QString quickSelect = m_quickSelectCombo->currentText();
|
||||
QDate today = QDate::currentDate();
|
||||
|
||||
if (quickSelect == "今天") {
|
||||
m_startDateEdit->setDate(today);
|
||||
m_endDateEdit->setDate(today);
|
||||
} else if (quickSelect == "最近7天") {
|
||||
m_startDateEdit->setDate(today.addDays(-7));
|
||||
m_endDateEdit->setDate(today);
|
||||
} else if (quickSelect == "最近30天") {
|
||||
m_startDateEdit->setDate(today.addDays(-30));
|
||||
m_endDateEdit->setDate(today);
|
||||
} else if (quickSelect == "本月") {
|
||||
m_startDateEdit->setDate(QDate(today.year(), today.month(), 1));
|
||||
m_endDateEdit->setDate(today);
|
||||
} else if (quickSelect == "全部") {
|
||||
// 找到最早的记录日期
|
||||
QDate earliest = today;
|
||||
for (const AttendanceRecord &record : m_records) {
|
||||
QDate recordDate = record.getDateTime().date();
|
||||
if (recordDate < earliest) {
|
||||
earliest = recordDate;
|
||||
}
|
||||
}
|
||||
m_startDateEdit->setDate(earliest);
|
||||
m_endDateEdit->setDate(today);
|
||||
}
|
||||
|
||||
calculateStatistics();
|
||||
updateTable();
|
||||
}
|
||||
|
||||
void StatisticsDialog::onExportReport()
|
||||
{
|
||||
QString fileName = QFileDialog::getSaveFileName(
|
||||
this,
|
||||
"导出统计报表",
|
||||
QString("attendance_statistics_%1.csv")
|
||||
.arg(QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss")),
|
||||
"CSV文件 (*.csv)");
|
||||
|
||||
if (fileName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
QMessageBox::critical(this, "错误", "无法创建导出文件!");
|
||||
return;
|
||||
}
|
||||
|
||||
QTextStream out(&file);
|
||||
out.setCodec("UTF-8");
|
||||
|
||||
// 写入标题
|
||||
out << QString("考勤统计报表 - 统计时间:%1 至 %2\n")
|
||||
.arg(m_startDateEdit->date().toString("yyyy-MM-dd"))
|
||||
.arg(m_endDateEdit->date().toString("yyyy-MM-dd"));
|
||||
out << "\n";
|
||||
|
||||
// 写入表头
|
||||
out << "学号,姓名,总签到次数,今日签到,最后签到时间,平均置信度\n";
|
||||
|
||||
// 写入数据
|
||||
for (auto it = m_statsMap.begin(); it != m_statsMap.end(); ++it) {
|
||||
const StudentStats &stats = it.value();
|
||||
out << stats.studentId << ","
|
||||
<< stats.name << ","
|
||||
<< stats.totalCount << ","
|
||||
<< stats.todayCount << ","
|
||||
<< (stats.lastCheckIn.isValid() ? stats.lastCheckIn.toString("yyyy-MM-dd HH:mm:ss") : "无") << ","
|
||||
<< QString::number(stats.avgConfidence, 'f', 4) << "\n";
|
||||
}
|
||||
|
||||
file.close();
|
||||
|
||||
QMessageBox::information(this, "导出成功",
|
||||
QString("统计报表已导出到:\n%1").arg(fileName));
|
||||
}
|
||||
|
||||
void StatisticsDialog::onRefresh()
|
||||
{
|
||||
calculateStatistics();
|
||||
updateTable();
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
#include "TimeSlotConfig.h"
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QStandardPaths>
|
||||
#include <QDir>
|
||||
#include <QDebug>
|
||||
|
||||
TimeSlotConfig& TimeSlotConfig::instance()
|
||||
{
|
||||
static TimeSlotConfig instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
TimeSlotConfig::TimeSlotConfig()
|
||||
{
|
||||
// 配置文件路径
|
||||
QString configPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
||||
QDir().mkpath(configPath);
|
||||
m_configFilePath = configPath + "/timeslot_config.json";
|
||||
|
||||
// 加载配置或使用默认值
|
||||
if (QFile::exists(m_configFilePath)) {
|
||||
loadFromFile();
|
||||
} else {
|
||||
initDefaultTimeSlots();
|
||||
saveToFile();
|
||||
}
|
||||
}
|
||||
|
||||
TimeSlotConfig::~TimeSlotConfig()
|
||||
{
|
||||
saveToFile();
|
||||
}
|
||||
|
||||
void TimeSlotConfig::initDefaultTimeSlots()
|
||||
{
|
||||
m_timeSlots.clear();
|
||||
|
||||
// 默认时间段(对应STM32端的配置)
|
||||
m_timeSlots.append(TimeSlot("上午签到", QTime(8, 0), QTime(9, 30), true));
|
||||
m_timeSlots.append(TimeSlot("中午签退", QTime(12, 0), QTime(13, 0), true));
|
||||
m_timeSlots.append(TimeSlot("下午签到", QTime(14, 0), QTime(15, 0), true));
|
||||
m_timeSlots.append(TimeSlot("晚上签退", QTime(17, 0), QTime(18, 30), true));
|
||||
}
|
||||
|
||||
void TimeSlotConfig::setTimeSlots(const QVector<TimeSlot> &timeSlots)
|
||||
{
|
||||
m_timeSlots = timeSlots;
|
||||
saveToFile();
|
||||
}
|
||||
|
||||
void TimeSlotConfig::addTimeSlot(const TimeSlot &timeSlot)
|
||||
{
|
||||
m_timeSlots.append(timeSlot);
|
||||
saveToFile();
|
||||
}
|
||||
|
||||
void TimeSlotConfig::removeTimeSlot(int index)
|
||||
{
|
||||
if (index >= 0 && index < m_timeSlots.size()) {
|
||||
m_timeSlots.removeAt(index);
|
||||
saveToFile();
|
||||
}
|
||||
}
|
||||
|
||||
void TimeSlotConfig::updateTimeSlot(int index, const TimeSlot &timeSlot)
|
||||
{
|
||||
if (index >= 0 && index < m_timeSlots.size()) {
|
||||
m_timeSlots[index] = timeSlot;
|
||||
saveToFile();
|
||||
}
|
||||
}
|
||||
|
||||
bool TimeSlotConfig::isValidTime(const QTime &time) const
|
||||
{
|
||||
for (const TimeSlot &slot : m_timeSlots) {
|
||||
if (slot.isInRange(time)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QString TimeSlotConfig::getCurrentTimeSlotName(const QTime &time) const
|
||||
{
|
||||
for (const TimeSlot &slot : m_timeSlots) {
|
||||
if (slot.isInRange(time)) {
|
||||
return slot.name;
|
||||
}
|
||||
}
|
||||
return "非签到时段";
|
||||
}
|
||||
|
||||
void TimeSlotConfig::saveToFile()
|
||||
{
|
||||
QJsonArray slotsArray;
|
||||
|
||||
for (const TimeSlot &slot : m_timeSlots) {
|
||||
QJsonObject obj;
|
||||
obj["name"] = slot.name;
|
||||
obj["startTime"] = slot.startTime.toString("HH:mm");
|
||||
obj["endTime"] = slot.endTime.toString("HH:mm");
|
||||
obj["enabled"] = slot.enabled;
|
||||
slotsArray.append(obj);
|
||||
}
|
||||
|
||||
QJsonObject root;
|
||||
root["timeSlots"] = slotsArray;
|
||||
|
||||
QJsonDocument doc(root);
|
||||
QFile file(m_configFilePath);
|
||||
if (file.open(QIODevice::WriteOnly)) {
|
||||
file.write(doc.toJson());
|
||||
file.close();
|
||||
qDebug() << "时间段配置已保存:" << m_configFilePath;
|
||||
} else {
|
||||
qWarning() << "无法保存时间段配置:" << m_configFilePath;
|
||||
}
|
||||
}
|
||||
|
||||
void TimeSlotConfig::loadFromFile()
|
||||
{
|
||||
QFile file(m_configFilePath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qWarning() << "无法加载时间段配置:" << m_configFilePath;
|
||||
initDefaultTimeSlots();
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = file.readAll();
|
||||
file.close();
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
if (!doc.isObject()) {
|
||||
qWarning() << "时间段配置格式错误";
|
||||
initDefaultTimeSlots();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject root = doc.object();
|
||||
QJsonArray slotsArray = root["timeSlots"].toArray();
|
||||
|
||||
m_timeSlots.clear();
|
||||
for (const QJsonValue &value : slotsArray) {
|
||||
QJsonObject obj = value.toObject();
|
||||
TimeSlot slot;
|
||||
slot.name = obj["name"].toString();
|
||||
slot.startTime = QTime::fromString(obj["startTime"].toString(), "HH:mm");
|
||||
slot.endTime = QTime::fromString(obj["endTime"].toString(), "HH:mm");
|
||||
slot.enabled = obj["enabled"].toBool();
|
||||
m_timeSlots.append(slot);
|
||||
}
|
||||
|
||||
qDebug() << "已加载" << m_timeSlots.size() << "个时间段配置";
|
||||
}
|
||||
|
||||
void TimeSlotConfig::resetToDefault()
|
||||
{
|
||||
initDefaultTimeSlots();
|
||||
saveToFile();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#include "MainWindow.h"
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
|
||||
// 设置应用程序信息
|
||||
QApplication::setApplicationName("人脸识别考勤系统");
|
||||
QApplication::setApplicationVersion("1.0.0");
|
||||
QApplication::setOrganizationName("STM32 Face Recognition");
|
||||
|
||||
// 创建并显示主窗口
|
||||
MainWindow window;
|
||||
window.show();
|
||||
|
||||
qDebug() << "考勤查看器启动成功!";
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
Reference in New Issue
Block a user