Преглед на файлове

1、虚拟键盘字号调大
2、右上角按钮显示不全
3、取出钥匙后的步骤优化
4、其他bug修复

lsb преди 3 месеца
родител
ревизия
9921fd0318

+ 135 - 137
src/interactive/InteractiveFace.cpp

@@ -1,41 +1,54 @@
 #include "InteractiveFace.h"
 
-//#include <QMediaDevices>
+#include <QCameraInfo>
 #include <QRandomGenerator>
-#include <QTimerEvent>
 #include <QPainter>
 #include <QFont>
 #include <QPen>
+#include <QDir>
+#include <QDateTime>
+#include <QCoreApplication>
+#include <QAbstractVideoBuffer>
 
 InteractiveFace* InteractiveFace::pInstance = nullptr;
 
 InteractiveFace::InteractiveFace(QObject *parent)
-    : QThread(parent)
+    : QObject(parent)
     , QQuickImageProvider(QQuickImageProvider::Image)
-{
-    if (hasCamera())
-    {
-        // initCamera();
+    , m_FrameId(0)
+    , m_camera(nullptr)
+    , m_probe(nullptr)
+    , m_saveFrameTimer(nullptr)
+{
+    m_probe = new QVideoProbe(this);
+    m_saveFrameTimer = new QTimer(this);
+    m_saveFrameTimer->setInterval(1000); // 每秒一帧
+    connect(m_saveFrameTimer, &QTimer::timeout, this, &InteractiveFace::onSaveFrameTimer);
+    connect(m_probe, &QVideoProbe::videoFrameProbed, this, &InteractiveFace::onVideoFrameProbed);
+
+    if (hasCamera()) {
+        initCamera();
     }
-    // 图像取1s 10帧
-//    m_timerId = startTimer(100);
 }
 
 InteractiveFace::~InteractiveFace()
 {
-
+    destroyCamera();
 }
 
 QString InteractiveFace::getImageUrl()
 {
-    // image://InteractiveFaceImage/(0-9999)
     return QString("image://") +
            QString(INTERACTIVE_FACE_IMAGE_URL) +
            QString("/") +
-           // 取随机数,确保图片刷新(Qt机制,两次url地址相同,图片不会显示)
            QString::number(QRandomGenerator::global()->bounded(10000));
 }
 
+QString InteractiveFace::imageUrl()
+{
+    return getImageUrl();
+}
+
 InteractiveFace *InteractiveFace::instance()
 {
     if (!pInstance) {
@@ -78,30 +91,21 @@ void InteractiveFace::setCallBackFaceStatus(QJSValue isAppearCallback)
 
 void InteractiveFace::cameraImagePlay()
 {
-    if (!hasCamera())
-    {
+    if (!hasCamera()) {
+        qWarning() << "[InteractiveFace] 无可用摄像头";
         return;
     }
-
-
-//    if (!m_camera.isActive())
-//    {
-//        cameraPlay();
-//        m_laseEpoch = 0;
-//        m_laseCount = 0;
-
-//        m_FrameId = 0;
-//    }
+    cameraPlay();
+    m_FrameId = 0;
+    m_saveFrameTimer->start();
 }
 
 void InteractiveFace::cameraImageStop()
 {
-    if (!hasCamera())
-    {
-        return;
+    m_saveFrameTimer->stop();
+    if (hasCamera()) {
+        cameraStop();
     }
-
-    cameraStop();
 }
 
 QImage InteractiveFace::getImage()
@@ -112,138 +116,132 @@ QImage InteractiveFace::getImage()
 
 QImage InteractiveFace::requestImage(const QString &, QSize *, const QSize &)
 {
-    return m_image;
+    QMutexLocker locker(&m_mutex);
+    if (m_image.isNull() || m_image.width() == 0 || m_image.height() == 0) {
+        // 返回一个小的占位图像,避免返回空图像导致 QML 报错
+        QImage placeholder(1, 1, QImage::Format_RGB32);
+        placeholder.fill(Qt::black);
+        return placeholder;
+    }
+    return m_image.copy();
 }
 
-void InteractiveFace::timerEvent(QTimerEvent *event)
+void InteractiveFace::onVideoFrameProbed(const QVideoFrame &frame)
 {
-    if (event->timerId() == m_timerId)
-    {
-        if (!hasCamera())
-        {
-            return;
-        }
+    if (!frame.isValid()) return;
 
-//        if (m_camera.isActive())
-//        {
-//            QMutexLocker locker(&m_mutex);
-//            QVideoFrame currentFormat = m_videoSink.videoFrame();
-
-//            QImage image;       // 图像
-//            QPainter painter;   // 画笔
-
-//            // 过滤无效帧, 前10帧不做处理
-//            if(++m_FrameId <= 30)
-//            {
-//                m_image = image.copy();
-//                if (m_imageGatherCallback.isCallable())
-//                {
-//                    QJSValueList args;
-//                    args << getImageUrl();
-//                    m_imageGatherCallback.call(args);
-//                }
-//                return;
-//            }
-
-//            if (currentFormat.isValid())
-//            {
-//                if (currentFormat.map(QVideoFrame::ReadOnly))
-//                {
-//                    image = currentFormat.toImage();
-//                    // image = image.scaled(QSize(800, 600), Qt::KeepAspectRatio, Qt::SmoothTransformation);
-//                }
-//                currentFormat.unmap();
-//            }
-
-//            if (!image.isNull())
-//            {
-//                // image底部框
-//                painter.begin(&image);
-//                painter.setRenderHint(QPainter::Antialiasing);
-//                painter.setFont(QFont("Arial", 24));
-//                painter.setBrush(QColor(255, 255, 255, 127));
-//                painter.setPen(Qt::NoPen);
-//                painter.drawRect(QRect(0, image.height() - 50, image.width(), 50));
-
-//                // TODO: 绘制人脸区域
-//                // QImage convertedImage = image.convertToFormat(QImage::Format_RGB888);
-
-//                painter.end();
-
-//                m_image = image.copy();
-
-//                if (m_imageGatherCallback.isCallable())
-//                {
-//                    QJSValueList args;
-//                    args << getImageUrl();
-//                    m_imageGatherCallback.call(args);
-//                }
-//            }
-//        }
+    QImage image;
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
+    image = frame.image();
+#else
+    if (frame.map(QAbstractVideoBuffer::ReadOnly)) {
+        QImage::Format format = QVideoFrame::imageFormatFromPixelFormat(frame.pixelFormat());
+        if (format != QImage::Format_Invalid) {
+            image = QImage(frame.bits(), frame.width(), frame.height(), frame.bytesPerLine(), format).copy();
+        }
+        frame.unmap();
+    }
+#endif
+    if (!image.isNull()) {
+        if (++m_FrameId <= 5) return; // 跳过前几帧
+        QMutexLocker locker(&m_mutex);
+        m_image = image;
+        if (m_imageGatherCallback.isCallable()) {
+            QJSValueList args;
+            args << getImageUrl();
+            m_imageGatherCallback.call(args);
+        }
     }
 }
 
-void InteractiveFace::initCamera()
+void InteractiveFace::onSaveFrameTimer()
 {
-//    m_camera.setCameraDevice(QMediaDevices::defaultVideoInput());
+    saveCurrentFrameToImg();
+}
 
-//    QCameraDevice device = QMediaDevices::defaultVideoInput();
-//    QList<QCameraFormat> formats = device.videoFormats();
-//    // 选择最适合的格式
-//    for (auto &format : formats)
-//    {
-//        // qDebug() << "fps---" << format.maxFrameRate() << format.resolution().width() << format.resolution().height();
-//        if (format.maxFrameRate() >= 10.0 && format.resolution().width() % 4 == 0)
-//        {
-//            m_camera.setCameraFormat(format);
-//            break;
-//        }
-//    }
-//    m_session.setCamera(&m_camera);
-//    m_session.setVideoSink(&m_videoSink);
+void InteractiveFace::initCamera()
+{
+    if (m_camera) return;
+    QList<QCameraInfo> cameras = QCameraInfo::availableCameras();
+    if (cameras.isEmpty()) {
+        qWarning() << "[InteractiveFace] 未检测到摄像头";
+        return;
+    }
+    QCameraInfo info = QCameraInfo::defaultCamera();
+    if (info.isNull()) info = cameras.first();
+    m_camera = new QCamera(info, this);
+    if (!m_probe->setSource(m_camera)) {
+        qWarning() << "[InteractiveFace] QVideoProbe 绑定摄像头失败";
+    }
 }
 
 void InteractiveFace::destroyCamera()
 {
-
+    cameraStop();
+    if (m_camera) {
+        m_probe->setSource(static_cast<QMediaObject*>(nullptr));
+        delete m_camera;
+        m_camera = nullptr;
+    }
 }
 
 bool InteractiveFace::hasCamera()
 {
-//    return !QMediaDevices::videoInputs().isEmpty();
-    return false;
+    return !QCameraInfo::availableCameras().isEmpty();
 }
 
-void InteractiveFace::cameraPlay()
+bool InteractiveFace::hasValidImage()
 {
-    // 如果相机不是播放状态
-//    if (!m_camera.isActive())
-//    {
-//        QCameraDevice cameraDevice = QMediaDevices::defaultVideoInput();
-//        if (cameraDevice.isNull())
-//        {
-//            qWarning() << "系统中没有检测到任何摄像机设备";
-//            return;
-//        }
-
-//        m_camera.setCameraDevice(cameraDevice);
-
-//        m_session.setCamera(&m_camera);
-//        m_session.setVideoSink(&m_videoSink);
+    QMutexLocker locker(&m_mutex);
+    return !m_image.isNull() && m_image.width() > 0 && m_image.height() > 0;
+}
 
-//        m_camera.start();
-//    }
+void InteractiveFace::cameraPlay()
+{
+    if (!m_camera) return;
+    if (m_camera->state() != QCamera::ActiveState) {
+        m_camera->start();
+    }
 }
 
 void InteractiveFace::cameraStop()
 {
-//    if (m_camera.isActive())
-//    {
-//        m_camera.stop();
-//        m_camera.setActive(false);
-//        m_camera.setCameraDevice(QCameraDevice());
+    if (m_camera && m_camera->state() == QCamera::ActiveState) {
+        m_camera->stop();
+    }
+}
 
-//        m_session.setCamera(nullptr);
-//        m_session.setVideoSink(nullptr);
-//    }
+void InteractiveFace::saveCurrentFrameToImg()
+{
+    QImage img = getImage();
+    if (img.isNull() || img.width() == 0 || img.height() == 0) {
+        qDebug() << "[InteractiveFace] 当前没有有效图像,跳过保存";
+        return;
+    }
+
+    // 使用当前工作目录下的 img 目录
+    QString baseDir = QDir::currentPath() + "/img";
+    QDir dir;
+    if (!dir.exists(baseDir)) {
+        if (!dir.mkpath(baseDir)) {
+            qWarning() << "[InteractiveFace] 创建 img 目录失败:" << baseDir;
+            // 尝试使用应用程序目录
+            baseDir = QCoreApplication::applicationDirPath() + "/img";
+            if (!dir.exists(baseDir)) {
+                if (!dir.mkpath(baseDir)) {
+                    qWarning() << "[InteractiveFace] 创建 img 目录失败(应用程序目录):" << baseDir;
+                    return;
+                }
+            }
+        }
+    }
+    
+    QString timestamp = QString::number(QDateTime::currentMSecsSinceEpoch());
+    QString fileName = baseDir + "/" + timestamp + ".jpg";
+    
+    if (img.save(fileName, "JPEG", 85)) {
+        qDebug() << "[InteractiveFace] 成功保存帧到:" << fileName;
+    } else {
+        qWarning() << "[InteractiveFace] 保存帧失败:" << fileName;
+    }
 }

+ 21 - 16
src/interactive/InteractiveFace.h

@@ -6,12 +6,13 @@
 #include <QMutex>
 #include <QImage>
 #include <QCamera>
-//#include <QVideoSink>
-//#include <QMediaCaptureSession>
+#include <QVideoProbe>
+#include <QVideoFrame>
+#include <QTimer>
 
 #define INTERACTIVE_FACE_IMAGE_URL "InteractiveFaceImage"
 
-class InteractiveFace : public QThread, public QQuickImageProvider
+class InteractiveFace : public QObject, public QQuickImageProvider
 {
     Q_OBJECT
     QML_SINGLETON
@@ -45,39 +46,45 @@ public:
     // 相机图像停止 (采集)
     Q_INVOKABLE void cameraImageStop();
 
+    // 获取图像 URL,供 QML Image 刷新显示
+    Q_INVOKABLE QString imageUrl();
+    // 是否有可用摄像头
+    Q_INVOKABLE bool hasCamera();
+    // 是否有有效的摄像头画面
+    Q_INVOKABLE bool hasValidImage();
     // 获取图像
     QImage getImage();
 protected:
     QImage requestImage(const QString &, QSize *, const QSize &);
 
-    void timerEvent(QTimerEvent *event);
+private slots:
+    void onVideoFrameProbed(const QVideoFrame &frame);
+    void onSaveFrameTimer();
+
 private:
     // 初始化相机
     void initCamera();
     // 销毁相机
     void destroyCamera();
 
-    // 判断电脑上是否存在摄像头
-    bool hasCamera();
-
+    // 判断电脑上是否存在摄像头(内部用,QML 用 Q_INVOKABLE hasCamera())
 
     void cameraPlay();
     void cameraStop();
 
+    // 将当前帧保存到 img 目录,文件名用时间戳
+    void saveCurrentFrameToImg();
+
 public:
     QMutex m_mutex;
 
 private:
-    int m_timerId;      // 当前定时器ID
     int m_FrameId;      // 帧过滤
-
-
     QImage m_image;     // 图像
 
-    QCamera m_camera;   // 照相机
-
-//    QVideoSink m_videoSink;         // 视频接收器
-//    QMediaCaptureSession m_session; // 媒体连接器
+    QCamera *m_camera;           // 照相机
+    QVideoProbe *m_probe;        // 视频帧探测
+    QTimer *m_saveFrameTimer;    // 每秒存一帧定时器
 
     // 记录信息
     qint32 m_laseCount = 0;
@@ -86,8 +93,6 @@ private:
     QJSValue m_imageGatherCallback; // 图像采集回调
     QJSValue m_imageAppearCallback; // 发现人脸回调
     QJSValue m_imageRemainCallback; // 人脸停留回调
-
-
     QJSValue m_isAppearCallback;
 };
 

+ 4 - 0
src/main.cpp

@@ -4,6 +4,7 @@
 #include <QDateTime>
 
 #include "./interactive/InteractiveCAN.h"
+#include "./interactive/InteractiveFace.h"
 #include "./usr/LotoQmlPlugin.h"
 #include "./usr/config.h"
 
@@ -27,6 +28,9 @@ int main(int argc, char **argv)
     // 注册C++类/对象到QML
     LotoQmlTypes::registerTypes();
 
+    // 人脸采集 ImageProvider,供 QML 中 Image source="image://InteractiveFaceImage/xxx" 使用
+    engine.addImageProvider(QStringLiteral("InteractiveFaceImage"), GetInteractiveFace());
+
     const QUrl url = QUrl(QStringLiteral("qrc:/qml/main.qml"));
     QObject::connect(
         &engine,

+ 195 - 35
src/qml/Login.qml

@@ -2,6 +2,7 @@ import QtQuick 2.12
 import QtQuick.Templates 2.12
 import QtQuick.Layouts 1.12
 import QtQuick.Shapes 1.12
+import QtGraphicalEffects 1.12
 import "./components"
 import Loto 1.0
 
@@ -338,53 +339,212 @@ Rectangle {
         height: control.height
         color: "transparent"
 
+        property int faceDiameter: Math.min(faceAreaSize, Math.min(width, height) * 0.6)
+        property int faceRadius: faceDiameter / 2
+        property real faceCx: width / 2
+        property real faceCy: height / 2
+
+        // 关闭人脸登录弹窗
+        function closeFaceLoginDialog() {
+            InteractiveFace.cameraImageStop();
+            cameraTimeoutTimer.stop();
+            loginInput.visible = false;
+            loginInput.sourceComponent = null;
+            control.forceActiveFocus();
+        }
+
+        Component.onCompleted: {
+            if (InteractiveFace.hasCamera()) {
+                InteractiveFace.cameraImagePlay();
+                // 启动60秒超时定时器
+                cameraTimeoutTimer.start();
+            }
+        }
+        Component.onDestruction: {
+            InteractiveFace.cameraImageStop();
+            cameraTimeoutTimer.stop();
+        }
+
+        // 60秒超时定时器
+        Timer {
+            id: cameraTimeoutTimer
+            interval: 60000  // 60秒
+            repeat: false
+            running: false
+            onTriggered: {
+                console.log("[Login.qml] 摄像头打开60秒超时,自动关闭");
+                closeFaceLoginDialog();
+            }
+        }
+
+        // 圆心摄像头影像(圆形裁剪)
+        Item {
+            id: cameraCircleArea
+            width: faceDiameter
+            height: faceDiameter
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.verticalCenter: parent.verticalCenter
+            anchors.verticalCenterOffset: -30  // 向上偏移,为下方提示文字留出空间
+
+            // 圆形背景(当没有视频流时显示)
+            Rectangle {
+                id: cameraBackground
+                anchors.fill: parent
+                radius: faceRadius
+                color: "#1A1A1A"  // 深色背景
+                visible: !hasValidCameraImage || faceCameraUrl === ""
+            }
+
+            // 圆形遮罩(用于裁剪)
+            Rectangle {
+                id: circleMask
+                anchors.fill: parent
+                radius: faceRadius
+                color: "white"
+                visible: false
+            }
+
+            // 摄像头画面(圆形裁剪)
+            OpacityMask {
+                anchors.fill: parent
+                source: cameraImage
+                maskSource: circleMask
+                visible: hasValidCameraImage && faceCameraUrl !== ""
+            }
+
+            // 摄像头画面源(隐藏,只用于 OpacityMask)
+            Image {
+                id: cameraImage
+                anchors.fill: parent
+                fillMode: Image.PreserveAspectCrop
+                source: hasValidCameraImage ? faceCameraUrl : ""
+                cache: false
+                visible: false  // 隐藏,只通过 OpacityMask 显示
+                asynchronous: false
+            }
+
+            // 无图像提示
+            Text {
+                id: noImageText
+                anchors.centerIn: parent
+                text: "无图像"
+                color: "#888888"
+                font.pixelSize: 28
+                font.bold: true
+                visible: !hasValidCameraImage || faceCameraUrl === ""
+                z: 2
+            }
+
+            // 圆形识别框边框(仅保留圆形边框,无任何其他装饰或图案)
+            Rectangle {
+                id: faceRecognitionFrame
+                anchors.fill: parent
+                radius: faceRadius
+                color: "transparent"
+                border.color: "#40C7FF"  // 使用主题色
+                border.width: 3
+                z: 3
+                antialiasing: true
+            }
+        }
+
+
+        // 提示文字
+        Text {
+            id: faceTipText
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: cameraCircleArea.bottom
+            anchors.topMargin: 20
+            text: "请将脸部放入框内"
+            color: "#FFFFFF"
+            font.pixelSize: 24
+            font.bold: true
+            horizontalAlignment: Text.AlignHCenter
+            z: 10  // 确保显示在遮罩上方
+        }
+
+        property string faceCameraUrl: ""
+        property bool hasValidCameraImage: false
+        
+        Timer {
+            id: faceCameraRefreshTimer
+            interval: 100
+            repeat: true
+            running: faceInputRect.visible && InteractiveFace.hasCamera()
+            onTriggered: {
+                // 先检查是否有有效图像
+                var hasImage = InteractiveFace.hasValidImage();
+                if (hasImage) {
+                    // 只有在有有效图像时才更新 URL
+                    faceInputRect.faceCameraUrl = InteractiveFace.imageUrl();
+                    faceInputRect.hasValidCameraImage = true;
+                } else {
+                    // 没有图像时清空 URL,避免请求空图像
+                    if (faceInputRect.hasValidCameraImage) {
+                        faceInputRect.faceCameraUrl = "";
+                    }
+                    faceInputRect.hasValidCameraImage = false;
+                }
+            }
+        }
+
+        // 半透明遮罩 + 圆形镂空(圆心区域透明,显示摄像头)
         Shape {
-            id: shape
+            anchors.fill: parent
             ShapePath {
-                id: path
+                fillRule: ShapePath.OddEvenFill
                 strokeWidth: 0
                 fillColor: "#99000000"
                 startX: 0
                 startY: 0
 
-                PathLine {
-                    x: faceInputRect.width
-                    y: 0
-                }
-                PathLine {
-                    x: faceInputRect.width
-                    y: faceInputRect.height
-                }
-                PathLine {
-                    x: 0
-                    y: faceInputRect.height
-                }
-                PathLine {
-                    x: 0
-                    y: 0
+                PathLine { x: faceInputRect.width; y: 0 }
+                PathLine { x: faceInputRect.width; y: faceInputRect.height }
+                PathLine { x: 0; y: faceInputRect.height }
+                PathLine { x: 0; y: 0 }
+                // 内圈:圆心圆形镂空
+                PathMove { x: faceInputRect.faceCx - faceInputRect.faceRadius; y: faceInputRect.faceCy }
+                PathArc {
+                    x: faceInputRect.faceCx; y: faceInputRect.faceCy + faceInputRect.faceRadius
+                    radiusX: faceInputRect.faceRadius; radiusY: faceInputRect.faceRadius
+                    direction: PathArc.Clockwise
                 }
-                PathMove {
-                    x: (faceInputRect.width - faceAreaSize) / 2
-                    y: (faceInputRect.height - faceAreaSize) / 2
+                PathArc {
+                    x: faceInputRect.faceCx + faceInputRect.faceRadius; y: faceInputRect.faceCy
+                    radiusX: faceInputRect.faceRadius; radiusY: faceInputRect.faceRadius
+                    direction: PathArc.Clockwise
                 }
-                PathLine {
-                    x: (faceInputRect.width - faceAreaSize) / 2 + faceAreaSize
-                    y: (faceInputRect.height - faceAreaSize) / 2
+                PathArc {
+                    x: faceInputRect.faceCx; y: faceInputRect.faceCy - faceInputRect.faceRadius
+                    radiusX: faceInputRect.faceRadius; radiusY: faceInputRect.faceRadius
+                    direction: PathArc.Clockwise
                 }
-                PathLine {
-                    x: (faceInputRect.width - faceAreaSize) / 2 + faceAreaSize
-                    y: (faceInputRect.height - faceAreaSize) / 2 + faceAreaSize
-                }
-                PathLine {
-                    x: (faceInputRect.width - faceAreaSize) / 2
-                    y: (faceInputRect.height - faceAreaSize) / 2 + faceAreaSize
-                }
-                PathLine {
-                    x: (faceInputRect.width - faceAreaSize) / 2
-                    y: (faceInputRect.height - faceAreaSize) / 2
+                PathArc {
+                    x: faceInputRect.faceCx - faceInputRect.faceRadius; y: faceInputRect.faceCy
+                    radiusX: faceInputRect.faceRadius; radiusY: faceInputRect.faceRadius
+                    direction: PathArc.Clockwise
                 }
             }
         }
+
+        // 关闭按钮
+        MButton {
+            id: faceCloseBtn
+            anchors.right: parent.right
+            anchors.rightMargin: 80
+            anchors.top: parent.top
+            anchors.topMargin: 60
+            width: 120
+            height: 52
+            text: "关闭"
+            buttonColor: "#CC0F1929"
+            textColor: "white"
+            iconCharacter: "\uf00d"
+            iconColor: "#40a9ff"
+            onClicked: {
+                closeFaceLoginDialog();
+            }
+        }
     }
     property Component passwordInputDelegate: Rectangle {
         id: passwordInput
@@ -407,7 +567,7 @@ Rectangle {
             Rectangle {
                 id: passwordInputArea
                 anchors.horizontalCenter: parent.horizontalCenter
-                y: virtualKeyboard.keyboardVisible ? 30 : (parent.height - height) / 2
+                y: virtualKeyboard.keyboardVisible ? 150 : (parent.height - height) / 2
                 width: 497
                 height: 438
 

+ 2 - 2
src/qml/components/CustomVirtualKeyboard.qml

@@ -24,7 +24,7 @@ Rectangle {
     property int keySpacing: 6
     property int keyRadius: 8
     property int keyHeight: 52
-    property int fontSize: 20
+    property int fontSize: 28  // 增大字号,从20改为28
     
     width: parent ? parent.width : 800
     height: 300
@@ -145,7 +145,7 @@ Rectangle {
                 anchors.centerIn: parent
                 text: display
                 color: keyboard.keyTextColor
-                font.pixelSize: isSpecial ? keyboard.fontSize - 2 : keyboard.fontSize
+                font.pixelSize: isSpecial ? keyboard.fontSize - 3 : keyboard.fontSize  // 特殊键字号也相应增大
                 font.bold: isConfirm
             }
             

+ 238 - 125
src/qml/components/JobTicketProcess.qml

@@ -140,144 +140,157 @@ Rectangle {
             width: stackLayout.width
             height: stackLayout.height
             color: "transparent"
-            MButton {
-                id: __startJobProcessBtn
-                x: 422
-                y: 30
+
+            // 按钮和提示区域容器(固定在顶部)
+            Item {
+                id: buttonAndTipContainer
+                anchors.horizontalCenter: parent.horizontalCenter
+                anchors.top: parent.top
+                anchors.topMargin: 30
+                width: __startJobProcessBtn.width + tipRow.width + 15
                 height: 72
-                width: 377
 
-                buttonColor: "#40A9FF"
+                // 立即获取锁具按钮
+                MButton {
+                    id: __startJobProcessBtn
+                    anchors.left: parent.left
+                    anchors.verticalCenter: parent.verticalCenter
+                    height: 72
+                    width: 377
 
-                text: qsTr("现在取出设备")
-                textColor: "white"
-                iconCharacter: "\uf084"
-                iconColor: "white"
-
-                onClicked: {
-                    __sleepTimer.start();
-                    InteractiveCAN.cardNo = control.jobTicketNo;
-                    InteractiveCAN.taskCode = WorkNodeFormModel.workId
-                    InteractiveCAN.openEKey();
-                }
-            }
+                    buttonColor: "#40A9FF"
+
+                    text: qsTr("立即获取锁具")
+                    textColor: "white"
+                    iconCharacter: "\uf084"
+                    iconColor: "white"
 
-            Text {
-                id: __tipIconLabel
-                x: tipLabelX
-                y: 55
-
-                text: "\uf0a5"
-                color: "orange"
-                font.pixelSize: 20
-                font.family: iconFont.name
-                verticalAlignment: Text.AlignVCenter
-                horizontalAlignment: Text.AlignHCenter
-
-                SequentialAnimation on x {
-                    loops: Animation.Infinite // 无限循环动画
-                    NumberAnimation { from: tipLabelX; to: tipLabelX + 10; duration: 400 } // 向右移动
-                    PauseAnimation { duration: 100 } // 暂停一段时间(可选)
-                    NumberAnimation { from: tipLabelX+10; to: tipLabelX; duration: 400 } // 向左移动返回原点
-                    PauseAnimation { duration: 100 } // 再次暂停(可选)
+                    onClicked: {
+                        // 点击按钮:开始流程
+                        // 设置所有步骤为处理中状态(蓝色边框)
+                        subProcess1.processingColor = true;
+                        subProcess2.processingColor = true;
+                        subProcess3.processingColor = true;
+                        subProcess4.processingColor = true;
+                        __sleepTimer.start();
+                        InteractiveCAN.cardNo = control.jobTicketNo;
+                        InteractiveCAN.taskCode = WorkNodeFormModel.workId
+                        InteractiveCAN.openEKey();
+                    }
                 }
-            }
 
-            Text {
-                id: __tipLabel
-                x: tipLabelX + 30
-                y: 58
-                text: qsTr("请点击按钮进行操作")
-                color: "white"
-                font.pixelSize: 16
-                // 普通文字不使用图标字体
-                verticalAlignment: Text.AlignVCenter
-                horizontalAlignment: Text.AlignHCenter
+                // 提示区域(手指图标和文字水平排列)
+                Row {
+                    id: tipRow
+                    anchors.left: __startJobProcessBtn.right
+                    anchors.leftMargin: 15
+                    anchors.verticalCenter: __startJobProcessBtn.verticalCenter
+                    spacing: 8
+
+                    Text {
+                        id: __tipIconLabel
+                        anchors.verticalCenter: parent.verticalCenter
+                        text: "\uf0a5"
+                        color: "orange"
+                        font.pixelSize: 20
+                        font.family: iconFont.name
+                        verticalAlignment: Text.AlignVCenter
+                        horizontalAlignment: Text.AlignHCenter
+
+                        property real animationOffset: 0
+
+                        SequentialAnimation on animationOffset {
+                            running: true
+                            loops: Animation.Infinite // 无限循环动画
+                            NumberAnimation { from: 0; to: 5; duration: 400 } // 向右移动
+                            PauseAnimation { duration: 100 } // 暂停一段时间(可选)
+                            NumberAnimation { from: 5; to: 0; duration: 400 } // 向左移动返回原点
+                            PauseAnimation { duration: 100 } // 再次暂停(可选)
+                        }
+
+                        x: animationOffset
+                    }
+
+                    Text {
+                        id: __tipLabel
+                        anchors.verticalCenter: parent.verticalCenter
+                        text: qsTr("请点击按钮进行操作")
+                        color: "white"
+                        font.pixelSize: 16
+                        verticalAlignment: Text.AlignVCenter
+                        horizontalAlignment: Text.AlignLeft
+                    }
+                }
             }
 
-            Flickable {
-                id: __subProcesses
-                anchors.top: __startJobProcessBtn.bottom
+            // 四个状态步骤容器(默认显示)
+            Item {
+                id: __subProcessesContainer
+                anchors.top: buttonAndTipContainer.bottom
                 anchors.topMargin: 35
                 anchors.horizontalCenter: parent.horizontalCenter
+                width: parent.width
+                height: parent.height - buttonAndTipContainer.height - 65
+                clip: true
 
-                Column {
+                Flickable {
+                    id: __subProcesses
                     anchors.fill: parent
+                    contentWidth: parent.width
+                    contentHeight: stepsColumn.height
 
-                    spacing: 20
-
-                    JobTicketSubProcess {
-                        id: subProcess1
-                        anchors.horizontalCenter: parent.horizontalCenter
-                        width: 801
-                        height: 99
-
-                        jobTicketStatusTypeName: "分配钥匙"
-                        jobTicketStatusValue: "正在分配钥匙>>"
-                    }
-
-                    JobTicketSubProcess {
-                        id: subProcess2
-                        anchors.horizontalCenter: parent.horizontalCenter
-                        width: 801
-                        height: 99
-
-                        jobTicketStatusTypeName: "下发作业票"
-                        jobTicketStatusValue: "正在下发作业票>>"
-                    }
-
-                    JobTicketSubProcess {
-                        id: subProcess3
+                    Column {
+                        id: stepsColumn
                         anchors.horizontalCenter: parent.horizontalCenter
-                        width: 801
-                        height: 99
+                        spacing: 20
 
-                        jobTicketStatusTypeName: "取出锁"
-                        jobTicketStatusValue: "打开锁扣>>"
-                    }
+                        JobTicketSubProcess {
+                            id: subProcess1
+                            anchors.horizontalCenter: parent.horizontalCenter
+                            width: 801
+                            height: 99
 
-                    JobTicketSubProcess {
-                        id: subProcess4
-                        anchors.horizontalCenter: parent.horizontalCenter
-                        width: 801
-                        height: 99
+                            jobTicketStatusTypeName: "分配钥匙"
+                            jobTicketStatusValue: "正在分配钥匙>>"
+                        }
 
-                        jobTicketStatusTypeName: "取出钥匙"
-                        jobTicketStatusValue: "打开钥匙扣>>"
-                    }
+                        JobTicketSubProcess {
+                            id: subProcess2
+                            anchors.horizontalCenter: parent.horizontalCenter
+                            width: 801
+                            height: 99
 
-                    Rectangle {
-                        id: __successFirstStep
-                        width: 800
-                        height: 49
-                        anchors.horizontalCenter: parent.horizontalCenter
-                        visible: false
+                            jobTicketStatusTypeName: "下发作业票"
+                            jobTicketStatusValue: "正在下发作业票>>"
+                        }
 
-                        color: "#1890FF"
-                        radius: 12
+                        JobTicketSubProcess {
+                            id: subProcess3
+                            anchors.horizontalCenter: parent.horizontalCenter
+                            width: 801
+                            height: 99
 
-                        IconText {
-                            anchors.centerIn: parent
-                            iconCharacter: "\uf058"
-                            iconSize: 18
-                            iconColor: "green"
-                            text: qsTr("锁具和钥匙已经弹出,请及时取走并前往其对应点位上锁。上锁完成请返回锁柜还回钥匙")
-                            textColor: "white"
+                            jobTicketStatusTypeName: "取出锁"
+                            jobTicketStatusValue: "打开锁扣>>"
                         }
 
-                        MouseArea {
-                            anchors.fill: parent
+                        JobTicketSubProcess {
+                            id: subProcess4
+                            anchors.horizontalCenter: parent.horizontalCenter
+                            width: 801
+                            height: 99
 
-                            onClicked: {
-                                tabBar.currentIndex += 1;
-                            }
+                            jobTicketStatusTypeName: "取出钥匙"
+                            jobTicketStatusValue: "打开钥匙扣>>"
                         }
                     }
                 }
             }
 
             onVisibleChanged: {
-                InteractiveCAN.updateKeyAndLockStatus();
+                if (visible) {
+                    InteractiveCAN.updateKeyAndLockStatus();
+                }
             }
         }
         Rectangle {
@@ -285,29 +298,103 @@ Rectangle {
             height: stackLayout.height
             color: "transparent"
 
-            MButton {
-                id: __lockTips
+            // 第二步提示文字(居中显示,优化样式和排版)
+            Rectangle {
+                id: __lockTipsContainer
                 anchors.horizontalCenter: parent.horizontalCenter
-                y: 30
-                height: 48
-                width: 800
+                anchors.top: parent.top
+                anchors.topMargin: 120  // 向下调整,从40改为120
+                width: 1200
+                height: 280
+                color: "#1A1A2E"
+                radius: 16
+                border.color: "#40C7FF"
+                border.width: 2
 
-                btnRadius: 8
-                buttonColor: "#1A40A9FF"
-                pressedScale: 1.0
-                cancelMouseArea: true
+                Column {
+                    anchors.centerIn: parent
+                    spacing: 24
+                    width: parent.width - 80
 
-                text: qsTr("请将钥匙插入锁柜,插入后,系统会自动检查上锁状态")
-                textColor: "white"
+                    // 标题行
+                    Row {
+                        anchors.horizontalCenter: parent.horizontalCenter
+                        spacing: 12
+
+                        Text {
+                            text: "\uf05a"
+                            color: "#40C7FF"
+                            font.pixelSize: 28
+                            font.family: iconFont.name
+                            anchors.verticalCenter: parent.verticalCenter
+                        }
+
+                        Text {
+                            text: "您的作业任务相关钥匙和锁已经取出"
+                            color: "#40C7FF"
+                            font.pixelSize: 24
+                            font.bold: true
+                            anchors.verticalCenter: parent.verticalCenter
+                        }
+                    }
+
+                    // 主要内容
+                    Text {
+                        width: parent.width
+                        anchors.horizontalCenter: parent.horizontalCenter
+                        text: "请在作业任务对应的点位进行上锁作业,作业完成后,请将钥匙归还到锁柜。"
+                        color: "#FFFFFF"
+                        font.pixelSize: 20
+                        horizontalAlignment: Text.AlignHCenter
+                        wrapMode: Text.WordWrap
+                        lineHeight: 1.6
+                    }
+
+                    // 注意事项
+                    Rectangle {
+                        anchors.horizontalCenter: parent.horizontalCenter
+                        width: parent.width
+                        height: noticeText.height + 20
+                        color: "#2A1A0A"
+                        radius: 8
+                        border.color: "#FFA500"
+                        border.width: 1
+
+                        Row {
+                            anchors.left: parent.left
+                            anchors.leftMargin: 16
+                            anchors.verticalCenter: parent.verticalCenter
+                            spacing: 10
+
+                            Text {
+                                text: "\uf071"
+                                color: "#FFA500"
+                                font.pixelSize: 20
+                                font.family: iconFont.name
+                                anchors.verticalCenter: parent.verticalCenter
+                            }
+
+                            Text {
+                                id: noticeText
+                                width: parent.parent.width - 50
+                                text: "注意,您可以随时,在任何锁柜插入钥匙即可,系统会自动检测和识别作业状态。"
+                                color: "#FFA500"
+                                font.pixelSize: 18
+                                wrapMode: Text.WordWrap
+                                lineHeight: 1.5
+                            }
+                        }
+                    }
+                }
             }
 
             Flickable {
                 id: __lockView
                 anchors.horizontalCenter: parent.horizontalCenter
-                anchors.top: __lockTips.bottom
-                anchors.topMargin: 135
+                anchors.top: __lockTipsContainer.bottom
+                anchors.topMargin: 50
                 width: Math.min(__lockRow.width, control.lockStatusCardWidth*4+45)
-                height: parent.height - __lockTips.height - 75
+                height: parent.height - __lockTipsContainer.height - 150
                 clip: true
                 flickableDirection: Flickable.HorizontalFlick
 
@@ -751,6 +838,28 @@ Rectangle {
         }
     }
 
+    // 显示完成对话框
+    function showCompletionDialog() {
+        var component = Qt.createComponent("components/AlertDialog.qml");
+        if (component.status === Component.Ready) {
+            var alertDialogComp = component.createObject(appAlertDialog, {
+                messageTypeName: "提示",
+                messageValue: "所有钥匙和锁已经弹出,请即刻取走",
+                messageIconCharacter: "\uf058"
+            });
+            alertDialogComp.parent = appAlertDialog;
+            alertDialogComp.confirm.connect(function () {
+                alertDialogComp.visible = false;
+                appAlertDialog.sourceComponent = null;
+                alertDialogComp.destroy();
+                // 点击确认后自动跳转到第二步
+                tabBar.currentIndex = 1;
+            });
+        } else {
+            console.log("[JobTicketProcess] 加载AlertDialog组件失败:", component.errorString());
+        }
+    }
+
     Timer {
         id: __sleepTimer
         interval: 200
@@ -760,18 +869,22 @@ Rectangle {
         onTriggered: {
             if (InteractiveCAN.okOpenKey) {
                 subProcess1.showSuccessfulColor = true;
+                subProcess1.processingColor = true;
             }
             if (InteractiveCAN.okSendJobTicket) {
                 subProcess2.showSuccessfulColor = true;
+                subProcess2.processingColor = true;
             }
             if (InteractiveCAN.okUnLockLocks) {
                 subProcess3.showSuccessfulColor = true;
+                subProcess3.processingColor = true;
             }
             if (InteractiveCAN.okUnlockKey) {
                 subProcess4.showSuccessfulColor = true;
+                subProcess4.processingColor = true;
                 __sleepTimer.stop();
-                __successFirstStep.visible = true;
-                tabBar.currentIndex = 1;
+                // 弹出完成对话框
+                showCompletionDialog();
             }
         }
     }

+ 1 - 0
src/qml/components/JobTicketSubProcess.qml

@@ -18,6 +18,7 @@ Rectangle {
     property int iconSize: 24
 
     border.color: processingColor ? showSuccessfulColor ? "green" : "#40a9ff" : "#4d40A9FF"
+    border.width: processingColor && showSuccessfulColor ? 3 : 1
 
     Rectangle {
         id: __iconArea

+ 5 - 2
src/qml/main.qml

@@ -13,7 +13,9 @@ ApplicationWindow {
     width: 1920
     height: 1080
 
-    visibility: Window.FullScreen       // 设置窗口为全屏
+    //888888888888888888 默认全屏设置(已注释,如需还原请取消注释)
+    //visibility: Window.FullScreen       // 设置窗口为全屏
+    visibility: Window.Windowed          // 窗口模式,非全屏
     flags: Qt.FramelessWindowHint       // 设置窗口无边框
 
     property int defaultLogoutSeconds: 120 + 1
@@ -424,9 +426,10 @@ ApplicationWindow {
             id: logout_settings
             visible: appShowLogout
             x: 1449
-            y: 58
+            y: 80
             width: 418
             height: 52
+            z: 1000
 
             spacing: 14
 

+ 3 - 2
src/usr/LotoQmlPlugin.cpp

@@ -16,7 +16,7 @@
 #include "../httpclient/WorkNodeFormModel.h"
 
 #include "../interactive/InteractiveData.h"
-//#include "../interactive/InteractiveFace.h"
+#include "../interactive/InteractiveFace.h"
 #include "../interactive/InteractiveHttp.h"
 #include "../interactive/InteractiveTask.h"
 #include "../interactive/InteractiveCAN.h"
@@ -92,7 +92,8 @@ void LotoQmlTypes::registerTypes()
     qmlRegisterSingletonType<WorkNodeFormModel>(uri, majorVersion, minorVersion, "WorkNodeFormModel", &WorkNodeFormModel::create);
 
     qmlRegisterSingletonType<InteractiveCAN>(uri, majorVersion, minorVersion, "InteractiveCAN", &InteractiveCAN::create);
-    
+    qmlRegisterSingletonType<InteractiveFace>(uri, majorVersion, minorVersion, "InteractiveFace", &InteractiveFace::create);
+
     // 注册归还钥匙和锁管理器
     qmlRegisterSingletonType<ReturnKeyLockManager>(uri, majorVersion, minorVersion, "ReturnKeyLockManager", &ReturnKeyLockManager::create);
 }

+ 279 - 279
src/usr/config.cpp

@@ -1,279 +1,279 @@
-#include "config.h"
-
-#include <QXmlStreamReader>
-#include <QXmlStreamWriter>
-#include <QDebug>
-#include <QFile>
-#include <QDir>
-#include <QSettings>
-#include <QNetworkInterface>
-
-config* config::pInstance = nullptr;
-
-QList<QString> getSystemMacAddresses() {
-    QList<QString> macList;
-
-    QList<QNetworkInterface> interfaces = QNetworkInterface::allInterfaces();
-
-    for (const QNetworkInterface &interface : interfaces) {
-        if (!interface.isValid()) {
-            continue;
-        }
-
-        if (interface.flags().testFlag(QNetworkInterface::IsLoopBack)) {
-            continue;
-        }
-
-        QString mac = interface.hardwareAddress();
-        if (mac.isEmpty() || mac == "00:00:00:00:00:00") {
-            continue;
-        }
-
-        bool hasIPv4 = false;
-        QList<QNetworkAddressEntry> entries = interface.addressEntries();
-        for (const QNetworkAddressEntry &entry : entries) {
-            if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol) {
-                hasIPv4 = true;
-                break;
-            }
-        }
-        if (!hasIPv4) {
-            continue;
-        }
-
-        macList.append(mac);
-    }
-
-    return macList;
-}
-
-config::config() {
-    // 测试时无法调用product_uuid文件,测试用例
-    //devUuid = "CABINET_017";
-    devUuid = getDeviceUUID();
-
-    QList<QString> macList = getSystemMacAddresses();
-
-    m_systemMACAddr = macList.isEmpty() ? "" : macList[0];
-}
-
-config *config::instance()
-{
-    if (!pInstance) {
-        pInstance = new config;
-    }
-    return pInstance;
-}
-
-config *config::create(QQmlEngine *, QJSEngine *)
-{
-    return instance();
-}
-
-QString config::getDeviceUUID(void)
-{
-    QFile file("/sys/class/dmi/id/product_uuid");
-    if (!file.open(QIODevice::ReadOnly)) {
-        return QString(); // 无法打开文件
-    }
-
-    QString uuid = file.readAll();
-    uuid = uuid.trimmed(); // 去除可能的空格
-    uuid = uuid.replace("-", "");
-    return uuid;
-}
-
-void config::configWrite(void)
-{
-    QFile file(configfile);
-#ifndef Q_OS_WIN
-    QDir mdir;
-    if(!mdir.exists(configpath))
-    {
-        mdir.mkpath(configpath);
-    }
-#endif
-    if (!file.open(QIODevice::WriteOnly))
-    {
-        qDebug() << "open config.xml with WriteOnly failure!";
-        return;
-    }
-    QXmlStreamWriter xmlWriter(&file);
-    xmlWriter.setAutoFormatting(true); // 使输出格式化
-    xmlWriter.writeStartDocument();
-    xmlWriter.writeStartElement("config");
-    //device
-    xmlWriter.writeStartElement("device");
-    xmlWriter.writeTextElement("devInit", QString::number(devInit));
-    xmlWriter.writeTextElement("devUuid", devUuid);
-    xmlWriter.writeTextElement("logfileStat", QString::number(logfileStat));
-    xmlWriter.writeTextElement("heartTime", QString::number(heartTime));
-    xmlWriter.writeTextElement("loginTimeout", QString::number(loginTimeout));
-    xmlWriter.writeTextElement("lockCloseTimeout", QString::number(lockCloseTimeout));
-    xmlWriter.writeTextElement("readBee", QString::number(readBee));
-    xmlWriter.writeEndElement(); // device
-    //server
-    xmlWriter.writeStartElement("server");
-    xmlWriter.writeTextElement("httpHost", httpHost);
-    xmlWriter.writeTextElement("tenant_id", tenant_id);
-    xmlWriter.writeStartElement("httpPort");
-    xmlWriter.writeEndElement(); // httpPort
-    xmlWriter.writeEndElement(); // server
-    //param
-    xmlWriter.writeStartElement("param");
-    xmlWriter.writeTextElement("usernameLogin_url", usernameLogin_url);
-    xmlWriter.writeTextElement("cardLogin_url", cardLogin_url);
-    xmlWriter.writeTextElement("logout_url", logout_url);
-
-    xmlWriter.writeTextElement("getSysUserCharacteristicPage_url", getSysUserCharacteristicPage_url);
-    xmlWriter.writeTextElement("getInfo_url", getInfo_url);
-    xmlWriter.writeTextElement("insertUserFace_url", insertUserFace_url);
-    xmlWriter.writeTextElement("loginByFace_url", loginByFace_url);
-
-    xmlWriter.writeEndElement(); // param
-
-    xmlWriter.writeEndElement(); // config
-    xmlWriter.writeEndDocument();
-
-    // 关闭文件
-    file.close();
-}
-
-bool config::configRead(void)
-{
-    xmlType type = xmlType::XML_NULL;
-    QString elemName = "";
-    QFile file(configfile);
-    if (!file.open(QIODevice::ReadOnly)) {
-        qDebug() << "open config.xml with ReadOnly failure!";
-        return false;
-    }
-    QXmlStreamReader xml(&file);
-    while (!xml.atEnd() && !xml.hasError()) {
-        QXmlStreamReader::TokenType token = xml.readNext();
-        switch (token) {
-        case QXmlStreamReader::StartElement:
-            // qDebug() << "StartElement:" << xml.name();
-            if(xml.name() == QString("device")){
-                type = xmlType::XML_DEVICE;
-            }
-            else if(xml.name() == QString("server")){
-                type = xmlType::XML_SERVER;
-            }
-            else if(xml.name() == QString("param")){
-                type = xmlType::XML_PARAM;
-            }
-            else{
-                elemName = xml.name().toString();
-            }
-            break;
-        case QXmlStreamReader::EndElement:
-            // qDebug() << "EndElement:" << xml.name();
-            break;
-        case QXmlStreamReader::Characters:
-            if (!xml.isWhitespace())
-            {
-                // qDebug() << "Characters:" << xml.text();
-                if(type == xmlType::XML_DEVICE)
-                {
-                    getDeviceValue(elemName, xml.text().toString());
-                }
-                else if(type == xmlType::XML_SERVER)
-                {
-                    getServerValue(elemName, xml.text().toString());
-                }
-                else if(type == xmlType::XML_PARAM)
-                {
-                    getParamValue(elemName, xml.text().toString());
-                }
-            }
-
-            break;
-        default:
-            break;
-        }
-    }
-
-    if (xml.hasError()) {
-        qDebug() << "Error:" << xml.errorString();
-        return false;
-    }
-
-    return true;
-}
-
-void config::getDeviceValue(QString key, QString value)
-{
-    if(key == "devInit")
-    {
-        devInit = (value == "0")?false:true;
-    }
-    else if(key == "devUuid")
-    {
-        devUuid = value;
-    }
-    else if(key == "logfileStat")
-    {
-        logfileStat = (value == "0")?false:true;
-    }
-    else if(key == "heartTime")
-    {
-        heartTime = value.toInt();
-    }
-    else if(key == "loginTimeout")
-    {
-        loginTimeout = value.toInt();
-    }
-    else if(key == "lockCloseTimeout")
-    {
-        lockCloseTimeout = value.toInt();
-    }
-    else if(key == "readBee")
-    {
-        readBee = (value == "0")?false:true;
-    }
-}
-
-void config::getServerValue(QString key, QString value)
-{
-    if(key == "httpHost")
-    {
-        httpHost = value;
-    }
-    else if(key == "tenant_id")
-    {
-        tenant_id = value;
-    }
-}
-
-void config::getParamValue(QString key, QString value)
-{
-    if(key == "usernameLogin_url")
-    {
-        usernameLogin_url = value;
-    }
-    else if(key == "cardLogin_url")
-    {
-        cardLogin_url = value;
-    }
-    else if(key == "logout_url")
-    {
-        logout_url = value;
-    }
-    else if (key == "getSysUserCharacteristicPage_url")
-    {
-        getSysUserCharacteristicPage_url = value;
-    }
-    else if (key == "getInfo_url")
-    {
-        getInfo_url = value;
-    }
-    else if (key == "insertUserFace_url")
-    {
-        insertUserFace_url = value;
-    }
-    else if (key == "loginByFace_url")
-    {
-        loginByFace_url = value;
-    }
-}
+#include "config.h"
+
+#include <QXmlStreamReader>
+#include <QXmlStreamWriter>
+#include <QDebug>
+#include <QFile>
+#include <QDir>
+#include <QSettings>
+#include <QNetworkInterface>
+
+config* config::pInstance = nullptr;
+
+QList<QString> getSystemMacAddresses() {
+    QList<QString> macList;
+
+    QList<QNetworkInterface> interfaces = QNetworkInterface::allInterfaces();
+
+    for (const QNetworkInterface &interface : interfaces) {
+        if (!interface.isValid()) {
+            continue;
+        }
+
+        if (interface.flags().testFlag(QNetworkInterface::IsLoopBack)) {
+            continue;
+        }
+
+        QString mac = interface.hardwareAddress();
+        if (mac.isEmpty() || mac == "00:00:00:00:00:00") {
+            continue;
+        }
+
+        bool hasIPv4 = false;
+        QList<QNetworkAddressEntry> entries = interface.addressEntries();
+        for (const QNetworkAddressEntry &entry : entries) {
+            if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol) {
+                hasIPv4 = true;
+                break;
+            }
+        }
+        if (!hasIPv4) {
+            continue;
+        }
+
+        macList.append(mac);
+    }
+
+    return macList;
+}
+
+config::config() {
+    // 测试时无法调用product_uuid文件,测试用例
+    //devUuid = "CABINET_017";
+    devUuid = getDeviceUUID();
+
+    QList<QString> macList = getSystemMacAddresses();
+
+    m_systemMACAddr = macList.isEmpty() ? "" : macList[0];
+}
+
+config *config::instance()
+{
+    if (!pInstance) {
+        pInstance = new config;
+    }
+    return pInstance;
+}
+
+config *config::create(QQmlEngine *, QJSEngine *)
+{
+    return instance();
+}
+
+QString config::getDeviceUUID(void)
+{
+    QFile file("/sys/class/dmi/id/product_uuid");
+    if (!file.open(QIODevice::ReadOnly)) {
+        return QString(); // 无法打开文件
+    }
+
+    QString uuid = file.readAll();
+    uuid = uuid.trimmed(); // 去除可能的空格
+    uuid = uuid.replace("-", "");
+    return uuid;
+}
+
+void config::configWrite(void)
+{
+    QFile file(configfile);
+#ifndef Q_OS_WIN
+    QDir mdir;
+    if(!mdir.exists(configpath))
+    {
+        mdir.mkpath(configpath);
+    }
+#endif
+    if (!file.open(QIODevice::WriteOnly))
+    {
+        qDebug() << "open config.xml with WriteOnly failure!";
+        return;
+    }
+    QXmlStreamWriter xmlWriter(&file);
+    xmlWriter.setAutoFormatting(true); // 使输出格式化
+    xmlWriter.writeStartDocument();
+    xmlWriter.writeStartElement("config");
+    //device
+    xmlWriter.writeStartElement("device");
+    xmlWriter.writeTextElement("devInit", QString::number(devInit));
+    xmlWriter.writeTextElement("devUuid", devUuid);
+    xmlWriter.writeTextElement("logfileStat", QString::number(logfileStat));
+    xmlWriter.writeTextElement("heartTime", QString::number(heartTime));
+    xmlWriter.writeTextElement("loginTimeout", QString::number(loginTimeout));
+    xmlWriter.writeTextElement("lockCloseTimeout", QString::number(lockCloseTimeout));
+    xmlWriter.writeTextElement("readBee", QString::number(readBee));
+    xmlWriter.writeEndElement(); // device
+    //server
+    xmlWriter.writeStartElement("server");
+    xmlWriter.writeTextElement("httpHost", httpHost);
+    xmlWriter.writeTextElement("tenant_id", tenant_id);
+    xmlWriter.writeStartElement("httpPort");
+    xmlWriter.writeEndElement(); // httpPort
+    xmlWriter.writeEndElement(); // server
+    //param
+    xmlWriter.writeStartElement("param");
+    xmlWriter.writeTextElement("usernameLogin_url", usernameLogin_url);
+    xmlWriter.writeTextElement("cardLogin_url", cardLogin_url);
+    xmlWriter.writeTextElement("logout_url", logout_url);
+
+    xmlWriter.writeTextElement("getSysUserCharacteristicPage_url", getSysUserCharacteristicPage_url);
+    xmlWriter.writeTextElement("getInfo_url", getInfo_url);
+    xmlWriter.writeTextElement("insertUserFace_url", insertUserFace_url);
+    xmlWriter.writeTextElement("loginByFace_url", loginByFace_url);
+
+    xmlWriter.writeEndElement(); // param
+
+    xmlWriter.writeEndElement(); // config
+    xmlWriter.writeEndDocument();
+
+    // 关闭文件
+    file.close();
+}
+
+bool config::configRead(void)
+{
+    xmlType type = xmlType::XML_NULL;
+    QString elemName = "";
+    QFile file(configfile);
+    if (!file.open(QIODevice::ReadOnly)) {
+        qDebug() << "open config.xml with ReadOnly failure!";
+        return false;
+    }
+    QXmlStreamReader xml(&file);
+    while (!xml.atEnd() && !xml.hasError()) {
+        QXmlStreamReader::TokenType token = xml.readNext();
+        switch (token) {
+        case QXmlStreamReader::StartElement:
+            // qDebug() << "StartElement:" << xml.name();
+            if(xml.name() == QString("device")){
+                type = xmlType::XML_DEVICE;
+            }
+            else if(xml.name() == QString("server")){
+                type = xmlType::XML_SERVER;
+            }
+            else if(xml.name() == QString("param")){
+                type = xmlType::XML_PARAM;
+            }
+            else{
+                elemName = xml.name().toString();
+            }
+            break;
+        case QXmlStreamReader::EndElement:
+            // qDebug() << "EndElement:" << xml.name();
+            break;
+        case QXmlStreamReader::Characters:
+            if (!xml.isWhitespace())
+            {
+                // qDebug() << "Characters:" << xml.text();
+                if(type == xmlType::XML_DEVICE)
+                {
+                    getDeviceValue(elemName, xml.text().toString());
+                }
+                else if(type == xmlType::XML_SERVER)
+                {
+                    getServerValue(elemName, xml.text().toString());
+                }
+                else if(type == xmlType::XML_PARAM)
+                {
+                    getParamValue(elemName, xml.text().toString());
+                }
+            }
+
+            break;
+        default:
+            break;
+        }
+    }
+
+    if (xml.hasError()) {
+        qDebug() << "Error:" << xml.errorString();
+        return false;
+    }
+
+    return true;
+}
+
+void config::getDeviceValue(QString key, QString value)
+{
+    if(key == "devInit")
+    {
+        devInit = (value == "0")?false:true;
+    }
+    else if(key == "devUuid")
+    {
+        devUuid = value;
+    }
+    else if(key == "logfileStat")
+    {
+        logfileStat = (value == "0")?false:true;
+    }
+    else if(key == "heartTime")
+    {
+        heartTime = value.toInt();
+    }
+    else if(key == "loginTimeout")
+    {
+        loginTimeout = value.toInt();
+    }
+    else if(key == "lockCloseTimeout")
+    {
+        lockCloseTimeout = value.toInt();
+    }
+    else if(key == "readBee")
+    {
+        readBee = (value == "0")?false:true;
+    }
+}
+
+void config::getServerValue(QString key, QString value)
+{
+    if(key == "httpHost")
+    {
+        httpHost = value;
+    }
+    else if(key == "tenant_id")
+    {
+        tenant_id = value;
+    }
+}
+
+void config::getParamValue(QString key, QString value)
+{
+    if(key == "usernameLogin_url")
+    {
+        usernameLogin_url = value;
+    }
+    else if(key == "cardLogin_url")
+    {
+        cardLogin_url = value;
+    }
+    else if(key == "logout_url")
+    {
+        logout_url = value;
+    }
+    else if (key == "getSysUserCharacteristicPage_url")
+    {
+        getSysUserCharacteristicPage_url = value;
+    }
+    else if (key == "getInfo_url")
+    {
+        getInfo_url = value;
+    }
+    else if (key == "insertUserFace_url")
+    {
+        insertUserFace_url = value;
+    }
+    else if (key == "loginByFace_url")
+    {
+        loginByFace_url = value;
+    }
+}