Переглянути джерело

Merge branch 'dev_qt5' of http://192.168.0.253:3000/xj/ISCS_LOTO_Linux into dev_qt5

xj 3 місяців тому
батько
коміт
b49ede82a5

+ 15 - 1
src/httpclient/HttpCardLogin.cpp

@@ -73,13 +73,15 @@ void HttpCardLogin::slotHttpResponseCardLogin(QByteArray data)
     // 修复:原逻辑判断条件错误,应该是解析失败时返回错误
     if (error.error != QJsonParseError::NoError) {
         qDebug() << "[HttpCardLogin] JSON解析失败:" << error.errorString();
+        GetInteractiveData()->clearToken();
         emit signalLoginReturnStat(-3, "JSON解析失败: " + error.errorString());
         return;
     }
-    
+
     // 修复:原逻辑判断条件错误,应该是文档为空时返回错误
     if (jsonDoc.isNull() || jsonDoc.isEmpty()) {
         qDebug() << "[HttpCardLogin] JSON文档为空";
+        GetInteractiveData()->clearToken();
         emit signalLoginReturnStat(-3, "服务器返回数据为空");
         return;
     }
@@ -92,6 +94,12 @@ void HttpCardLogin::slotHttpResponseCardLogin(QByteArray data)
         int codeValue = rootObj.value("code").toInt();
         qDebug() << "[HttpCardLogin] 响应code:" << codeValue;
         
+        if (codeValue == 408) {
+            qDebug() << "[HttpCardLogin] 请求超时(408)";
+            GetInteractiveData()->clearToken();
+            emit signalLoginReturnStat(408, "处理超时,请稍后重试。");
+            return;
+        }
         if(codeValue == 200 || codeValue == 0)
         {
             if(rootObj.contains("data"))
@@ -99,11 +107,13 @@ void HttpCardLogin::slotHttpResponseCardLogin(QByteArray data)
                 QJsonObject vDataObj = rootObj.value("data").toObject();
                 if (vDataObj.empty()) {
                     qDebug() << "[HttpCardLogin] data对象为空";
+                    GetInteractiveData()->clearToken();
                     emit signalLoginReturnStat(-3, "登录失败:返回数据为空");
                     return;
                 }
                 if (!vDataObj.contains("accessToken")) {
                     qDebug() << "[HttpCardLogin] 缺少accessToken字段";
+                    GetInteractiveData()->clearToken();
                     emit signalLoginReturnStat(-3, "登录失败:缺少accessToken");
                     return;
                 }
@@ -149,11 +159,13 @@ void HttpCardLogin::slotHttpResponseCardLogin(QByteArray data)
                 }
                 else{
                     qDebug() << "[HttpCardLogin] accessToken类型不是字符串";
+                    GetInteractiveData()->clearToken();
                     emit signalLoginReturnStat(-3, "登录失败:accessToken格式错误");
                 }
             }
             else{
                 qDebug() << "[HttpCardLogin] 响应中缺少data字段";
+                GetInteractiveData()->clearToken();
                 emit signalLoginReturnStat(-3, "登录失败:响应缺少data字段");
             }
         }
@@ -164,11 +176,13 @@ void HttpCardLogin::slotHttpResponseCardLogin(QByteArray data)
                 errorMsg = rootObj.value("msg").toString();
             }
             qDebug() << "[HttpCardLogin] 登录失败,code:" << codeValue << ",msg:" << errorMsg;
+            GetInteractiveData()->clearToken();
             emit signalLoginReturnStat(-2, errorMsg);
         }
     }
     else{
         qDebug() << "[HttpCardLogin] 响应中缺少code字段";
+        GetInteractiveData()->clearToken();
         emit signalLoginReturnStat(-3, "登录失败:响应格式错误");
     }
 }

+ 6 - 1
src/httpclient/HttpClient.cpp

@@ -16,7 +16,12 @@
 #include <QDateTime>
 
 QString HttpClient::sToken = QString();
-const int HttpClient::HTTP_REQUEST_TIMEOUT = 5000;
+const int HttpClient::HTTP_REQUEST_TIMEOUT = 15000;
+
+void HttpClient::clearToken()
+{
+    sToken.clear();
+}
 
 HttpClient::HttpClient(QObject *parent)
     : QThread(parent)

+ 2 - 0
src/httpclient/HttpClient.h

@@ -28,6 +28,8 @@ public:
     ~HttpClient() override; // Qt5.15 override关键字需显式写
 
     static QString sToken; // 全局静态Token
+    /** 清空静态 sToken(与 InteractiveData::clearToken 配合使用) */
+    static void clearToken();
 
     void slotSetThreadStop();
 

+ 20 - 10
src/httpclient/HttpFaceLogin.cpp

@@ -72,7 +72,7 @@ void HttpFaceLogin::run()
     qDebug() << "[HttpFaceLogin] run() 结束";
 }
 
-static const int FACE_LOGIN_TIMEOUT_MS = 3000;
+static const int FACE_LOGIN_TIMEOUT_MS = 15000;
 
 void HttpFaceLogin::httpRequestFaceLogin()
 {
@@ -105,7 +105,9 @@ void HttpFaceLogin::httpRequestFaceLogin()
 
     // 人脸登录:本模块自建 QNAM 发 multipart POST,不占用 HttpClient
     QString fullUrl = QString("http://%1%2").arg(host).arg(path);
-    qDebug().noquote() << "[HttpFaceLogin] 请求人脸登录 API:" << fullUrl;
+    qDebug().noquote() << "[HttpFaceLogin] 请求人脸登录 API:" << fullUrl
+                       << " tenant-id=" << cfg->tenant_id
+                       << " Authorization=" << (token.isEmpty() ? "(空)" : "(已设置)");
     QUrl url(fullUrl);
     bool isPng = m_faceImageData.size() >= 8 && m_faceImageData.startsWith("\x89PNG\r\n\x1a\n");
     QString partContentType = isPng ? "image/png" : "image/jpeg";
@@ -115,18 +117,22 @@ void HttpFaceLogin::httpRequestFaceLogin()
     QHttpPart filePart;
     filePart.setHeader(QNetworkRequest::ContentTypeHeader, partContentType.toUtf8());
     filePart.setHeader(QNetworkRequest::ContentDispositionHeader,
-                      QString("form-data; name=\"file\"; filename=\"%1\"").arg(partFileName).toUtf8());
+                       QString("form-data; name=\"file\"; filename=\"%1\"").arg(partFileName).toUtf8());
     filePart.setBody(m_faceImageData);
     multiPart->append(filePart);
 
     QNetworkRequest request(url);
     request.setHeader(QNetworkRequest::ContentTypeHeader,
-                     "multipart/form-data; boundary=" + multiPart->boundary());
+                      "multipart/form-data; boundary=" + multiPart->boundary());
     QString authValue = token.trimmed();
     if (!authValue.isEmpty() && !authValue.startsWith("Bearer ", Qt::CaseInsensitive))
         authValue = "Bearer " + authValue;
     request.setRawHeader("Authorization", authValue.toUtf8());
-    request.setRawHeader("tenant-id", cfg->tenant_id.toUtf8());
+    QString tenantId = cfg->tenant_id.trimmed();
+    if (!tenantId.isEmpty())
+        request.setRawHeader("tenant-id", tenantId.toUtf8());
+    else
+        qWarning() << "[HttpFaceLogin] tenant_id 为空,未设置 tenant-id 请求头,可能导致 403 您无权访问该租户的数据";
 
     QNetworkAccessManager nam;
     QNetworkReply *reply = nam.post(request, multiPart);
@@ -171,11 +177,15 @@ void HttpFaceLogin::slotHttpResponseFaceLogin(QByteArray data)
     int code = obj.value("code").toInt(-1);
     // 兼容 code 0 或 200 表示成功
     if (code != 0 && code != 200) {
-        QString msg = obj.value("msg").toString();
-        if (msg.isEmpty()) msg = obj.value("result").toString();
-        if (msg.isEmpty()) msg = QString::fromUtf8(data).left(200);
-        qDebug().noquote() << "[HttpFaceLogin] API 返回 code:" << code << "msg:" << msg
-                           << (code == 902 || msg.contains("无法根据人脸确定您的身份") ? " [902-人脸识别失败,QML 会累计计数]" : "");
+        QString msg;
+        if (code == 408)
+            msg = QStringLiteral("处理超时,请稍后重试。");
+        else {
+            msg = obj.value("msg").toString();
+            if (msg.isEmpty()) msg = obj.value("result").toString();
+            if (msg.isEmpty()) msg = QString::fromUtf8(data).left(200);
+        }
+        qDebug().noquote() << "[HttpFaceLogin] API 返回非200 code:" << code << "msg:" << msg;
         emit signalFaceLoginError(msg);
         return;
     }

+ 4 - 0
src/httpclient/HttpGetJobTickets.cpp

@@ -60,6 +60,10 @@ void HttpGetJobTickets::slotHttpResponseGetJobTickets(QByteArray data)
     if(rootObj.contains("code"))
     {
         int codeValue = rootObj.value("code").toInt();
+        if (codeValue == 408) {
+            emit signalJobTicketsReturnStat(408, "处理超时,请稍后重试。");
+            return;
+        }
         if(codeValue == 200 || codeValue == 0) {
             if(rootObj.contains("data")) {
                 QJsonObject vDataObj = rootObj.value("data").toObject();

+ 4 - 0
src/httpclient/HttpGetUserInfo.cpp

@@ -58,6 +58,10 @@ void HttpGetUserInfo::slotHttpResponseGetUserInfo(QByteArray data)
     if(rootObj.contains("code"))
     {
         int codeValue = rootObj.value("code").toInt();
+        if (codeValue == 408) {
+            emit signalUserInfoReturnStat(408, "处理超时,请稍后重试。");
+            return;
+        }
         if(codeValue == 200 || codeValue == 0) {
             if(rootObj.contains("data")) {
                 QJsonObject vDataObj = rootObj.value("data").toObject();

+ 8 - 0
src/httpclient/HttpGetWorkNodeDetail.cpp

@@ -152,6 +152,10 @@ void HttpGetWorkNodeDetail::slotHttpResponseGetWorkNodeDetail(QByteArray data)
     QJsonObject rootObj = jsonDoc.object();
     if(rootObj.contains("code")) {
         int codeValue = rootObj.value("code").toInt();
+        if (codeValue == 408) {
+            emit signalWorkNodeDetailReturnStat(408, "处理超时,请稍后重试。");
+            return;
+        }
         if(codeValue == 200 || codeValue == 0) {
             if(rootObj.contains("data")) {
                 QJsonObject vDataObj = rootObj.value("data").toObject();
@@ -318,6 +322,10 @@ void HttpGetWorkNodeDetail::slotHttpResponseGetFormById(QByteArray data)
     if(rootObj.contains("code"))
     {
         int codeValue = rootObj.value("code").toInt();
+        if (codeValue == 408) {
+            emit signalWorkNodeDetailReturnStat(408, "处理超时,请稍后重试。");
+            return;
+        }
         if(codeValue == 200 || codeValue == 0) {
             if(rootObj.contains("data")) {
                 QJsonObject vDataObj = rootObj.value("data").toObject();

+ 110 - 100
src/httpclient/HttpPasswordLogin.cpp

@@ -1,100 +1,110 @@
-#include "HttpPasswordLogin.h"
-
-#include "../usr/config.h"
-#include "../interactive/InteractiveData.h"
-#include "../interactive/InteractiveHttp.h"
-
-#include <QJsonObject>
-
-HttpPasswordLogin::HttpPasswordLogin(QObject *parent)
-    : QThread(parent)
-{
-}
-
-void HttpPasswordLogin::run()
-{
-    httpRequestUsernameLogin();
-
-    //qDebug() << "HttpPasswordLogin thread exit!";
-}
-
-void HttpPasswordLogin::httpRequestUsernameLogin()
-{
-    QJsonObject jsonRoot;
-    QDateTime currentDateTime = QDateTime::currentDateTime();
-    qint64 timestampSeconds = currentDateTime.toMSecsSinceEpoch();
-
-    QString url = Config()->usernameLogin_url;
-    jsonRoot.insert("username", this->m_username);
-    jsonRoot.insert("password", this->m_password);
-
-    QJsonDocument jsonDoc(jsonRoot);
-    QByteArray jsonData = jsonDoc.toJson(QJsonDocument::Compact);
-    qDebug() << "json=" << QString::fromUtf8(jsonData);
-
-    emit signalPostRequestData(timestampSeconds, url, jsonData, NULL, m_accessToken);
-}
-
-void HttpPasswordLogin::slotHttpResponseUsernameLogin(QByteArray data)
-{
-    QJsonParseError error;
-    QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
-    if (error.error != QJsonParseError::NoError) {
-        emit signalLoginReturnStat(-3, "登录失败");
-        return;
-    }
-    if(jsonDoc.isNull() || jsonDoc.isEmpty()) {
-        emit signalLoginReturnStat(-3, "登录失败");
-        return;
-    }
-
-    QJsonObject rootObj = jsonDoc.object();
-    if(rootObj.contains("code"))
-    {
-        int codeValue = rootObj.value("code").toInt();
-        if(codeValue == 200 || codeValue == 0)
-        {
-            if(rootObj.contains("data"))
-            {
-                QJsonObject vDataObj = rootObj.value("data").toObject();
-                if (vDataObj.empty()) {
-                    emit signalLoginReturnStat(-3, "登录失败");
-                    return;
-                }
-                if (!vDataObj.contains("accessToken")) {
-                    emit signalLoginReturnStat(-3, "登录失败");
-                    return;
-                }
-                QJsonValue value = vDataObj.value("accessToken");
-                if(value.type() == QJsonValue::String)
-                {
-                    GetInteractiveData()->m_token = value.toString();
-                    InteractiveHttp::strToken = value.toString();
-                    m_accessToken = value.toString();
-                    if (vDataObj.contains("refreshToken") && vDataObj.value("refreshToken").type() == QJsonValue::String) {
-                        m_refreshToken = vDataObj.value("refreshToken").toString();
-                    }
-                    QString username = "未知用户";
-                    if(vDataObj.contains("nickname"))
-                    {
-                        QJsonValue name = vDataObj.value("nickname");
-                        if(name.type() == QJsonValue::String)
-                        {
-                            username = name.toString();
-                        }
-                    }
-                    Config()->username = username;
-                    GetInteractiveData()->m_userName = username;
-                    emit signalLoginReturnStat(0, "登录成功");
-                    emit signalLoginReturnParam(username, 0);
-                }
-                else {
-                    emit signalLoginReturnStat(-3, "登录失败");
-                }
-            }
-        }
-        else{
-            emit signalLoginReturnStat(-1, "用户名或者密码错误");
-        }
-    }
-}
+#include "HttpPasswordLogin.h"
+
+#include "../usr/config.h"
+#include "../interactive/InteractiveData.h"
+#include "../interactive/InteractiveHttp.h"
+
+#include <QJsonObject>
+
+HttpPasswordLogin::HttpPasswordLogin(QObject *parent)
+    : QThread(parent)
+{
+}
+
+void HttpPasswordLogin::run()
+{
+    httpRequestUsernameLogin();
+
+    //qDebug() << "HttpPasswordLogin thread exit!";
+}
+
+void HttpPasswordLogin::httpRequestUsernameLogin()
+{
+    QJsonObject jsonRoot;
+    QDateTime currentDateTime = QDateTime::currentDateTime();
+    qint64 timestampSeconds = currentDateTime.toMSecsSinceEpoch();
+
+    QString url = Config()->usernameLogin_url;
+    jsonRoot.insert("username", this->m_username);
+    jsonRoot.insert("password", this->m_password);
+
+    QJsonDocument jsonDoc(jsonRoot);
+    QByteArray jsonData = jsonDoc.toJson(QJsonDocument::Compact);
+    qDebug() << "json=" << QString::fromUtf8(jsonData);
+
+    emit signalPostRequestData(timestampSeconds, url, jsonData, NULL, m_accessToken);
+}
+
+void HttpPasswordLogin::slotHttpResponseUsernameLogin(QByteArray data)
+{
+    QJsonParseError error;
+    QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
+    if (error.error != QJsonParseError::NoError) {
+        GetInteractiveData()->clearToken();
+        emit signalLoginReturnStat(-3, "登录失败");
+        return;
+    }
+    if(jsonDoc.isNull() || jsonDoc.isEmpty()) {
+        GetInteractiveData()->clearToken();
+        emit signalLoginReturnStat(-3, "登录失败");
+        return;
+    }
+
+    QJsonObject rootObj = jsonDoc.object();
+    if(rootObj.contains("code"))
+    {
+        int codeValue = rootObj.value("code").toInt();
+        if (codeValue == 408) {
+            emit signalLoginReturnStat(408, "处理超时,请稍后重试。");
+            return;
+        }
+        if(codeValue == 200 || codeValue == 0)
+        {
+            if(rootObj.contains("data"))
+            {
+                QJsonObject vDataObj = rootObj.value("data").toObject();
+                if (vDataObj.empty()) {
+                    GetInteractiveData()->clearToken();
+                    emit signalLoginReturnStat(-3, "登录失败");
+                    return;
+                }
+                if (!vDataObj.contains("accessToken")) {
+                    GetInteractiveData()->clearToken();
+                    emit signalLoginReturnStat(-3, "登录失败");
+                    return;
+                }
+                QJsonValue value = vDataObj.value("accessToken");
+                if(value.type() == QJsonValue::String)
+                {
+                    GetInteractiveData()->m_token = value.toString();
+                    InteractiveHttp::strToken = value.toString();
+                    m_accessToken = value.toString();
+                    if (vDataObj.contains("refreshToken") && vDataObj.value("refreshToken").type() == QJsonValue::String) {
+                        m_refreshToken = vDataObj.value("refreshToken").toString();
+                    }
+                    QString username = "未知用户";
+                    if(vDataObj.contains("nickname"))
+                    {
+                        QJsonValue name = vDataObj.value("nickname");
+                        if(name.type() == QJsonValue::String)
+                        {
+                            username = name.toString();
+                        }
+                    }
+                    Config()->username = username;
+                    GetInteractiveData()->m_userName = username;
+                    emit signalLoginReturnStat(0, "登录成功");
+                    emit signalLoginReturnParam(username, 0);
+                }
+                else {
+                    GetInteractiveData()->clearToken();
+                    emit signalLoginReturnStat(-3, "登录失败");
+                }
+            }
+        }
+        else{
+            GetInteractiveData()->clearToken();
+            emit signalLoginReturnStat(-1, "用户名或者密码错误");
+        }
+    }
+}

+ 4 - 0
src/httpclient/HttpUpdateNodeApproval.cpp

@@ -52,6 +52,10 @@ void HttpUpdateNodeApproval::slotHttpResponseUpdateNodeApproval(QByteArray data)
     if(rootObj.contains("code"))
     {
         int codeValue = rootObj.value("code").toInt();
+        if (codeValue == 408) {
+            emit signalUpdateNodeApprovalReturnStat(408, "处理超时,请稍后重试。");
+            return;
+        }
         if(codeValue == 200 || codeValue == 0) {
             QString msg = "提交成功";
             if (rootObj.contains("msg")) {

+ 4 - 0
src/httpclient/HttpUpdateUserInfo.cpp

@@ -52,6 +52,10 @@ void HttpUpdateUserInfo::slotHttpResponseUpdateUserInfo(QByteArray data)
     if(rootObj.contains("code"))
     {
         int codeValue = rootObj.value("code").toInt();
+        if (codeValue == 408) {
+            emit signalUpdateUserInfoReturnStat(408, "处理超时,请稍后重试。");
+            return;
+        }
         if(codeValue == 200 || codeValue == 0) {
             QString msg = "更新密码成功";
             if (rootObj.contains("msg")) {

+ 4 - 0
src/httpclient/HttpUpdateUserPassword.cpp

@@ -49,6 +49,10 @@ void HttpUpdateUserPassword::slotHttpResponseUpdatePassword(QByteArray data)
     if(rootObj.contains("code"))
     {
         int codeValue = rootObj.value("code").toInt();
+        if (codeValue == 408) {
+            emit signalUpdatePasswordReturnStat(408, "处理超时,请稍后重试。");
+            return;
+        }
         if(codeValue == 200 || codeValue == 0) {
             QString msg = "更新密码成功";
             if (rootObj.contains("msg")) {

+ 28 - 0
src/interactive/InteractiveCAN.cpp

@@ -501,6 +501,10 @@ void InteractiveCAN::slotHttpResponseGetKeyMAC(QByteArray data)
     QJsonObject rootObj = jsonDoc.object();
     if(rootObj.contains("code")) {
         int codeValue = rootObj.value("code").toInt();
+        if (codeValue == 408) {
+            GetInteractiveData()->notifyRequestTimedOut();
+            return;
+        }
         if(codeValue == 200 || codeValue == 0) {
             if(rootObj.contains("data")) {
                 QJsonObject vDataObj = rootObj.value("data").toObject();
@@ -533,6 +537,10 @@ void InteractiveCAN::slotHttpResponseGetIsolationPointInfo(QByteArray data)
     QJsonObject rootObj = jsonDoc.object();
     if(rootObj.contains("code")) {
         int codeValue = rootObj.value("code").toInt();
+        if (codeValue == 408) {
+            GetInteractiveData()->notifyRequestTimedOut();
+            return;
+        }
         if(codeValue == 200 || codeValue == 0) {
             if(rootObj.contains("data")) {
                 IsolationPointInfo info;
@@ -587,6 +595,10 @@ void InteractiveCAN::slotHttpResponseUploadJobTicket(QByteArray data)
     QJsonObject rootObj = jsonDoc.object();
     if(rootObj.contains("code")) {
         int codeValue = rootObj.value("code").toInt();
+        if (codeValue == 408) {
+            GetInteractiveData()->notifyRequestTimedOut();
+            return;
+        }
         if(codeValue == 200 || codeValue == 0) {
 //            qDebug() << "[slotHttpResponseUploadJobTicket]" << jsonDoc;
             if (m_bleClient) {
@@ -633,6 +645,10 @@ void InteractiveCAN::slotHttpResponseUploadPositionInfo(QByteArray data)
     QJsonObject rootObj = jsonDoc.object();
     if(rootObj.contains("code")) {
         int codeValue = rootObj.value("code").toInt();
+        if (codeValue == 408) {
+            GetInteractiveData()->notifyRequestTimedOut();
+            return;
+        }
         if(codeValue == 200 || codeValue == 0) {
 //            qDebug() << "[slotHttpResponseUploadPositionInfo]" << jsonDoc;
         }
@@ -654,6 +670,10 @@ void InteractiveCAN::slotHttpResponseUpdatePointUnlock(QByteArray data)
     QJsonObject rootObj = jsonDoc.object();
     if(rootObj.contains("code")) {
         int codeValue = rootObj.value("code").toInt();
+        if (codeValue == 408) {
+            GetInteractiveData()->notifyRequestTimedOut();
+            return;
+        }
         if(codeValue == 200 || codeValue == 0) {
 //            qDebug() << "[slotHttpResponseUpdatePointUnlock]" << jsonDoc;
         }
@@ -675,6 +695,10 @@ void InteractiveCAN::slotHttpResponseUpdateBackLock(QByteArray data)
     QJsonObject rootObj = jsonDoc.object();
     if(rootObj.contains("code")) {
         int codeValue = rootObj.value("code").toInt();
+        if (codeValue == 408) {
+            GetInteractiveData()->notifyRequestTimedOut();
+            return;
+        }
         if(codeValue == 200 || codeValue == 0) {
             m_getLockedRfidFlag = false;
         }
@@ -698,6 +722,10 @@ void InteractiveCAN::slotHttpResponseGetWorkTicketByNodeId(QByteArray data)
     QJsonObject rootObj = jsonDoc.object();
     if(rootObj.contains("code")) {
         int codeValue = rootObj.value("code").toInt();
+        if (codeValue == 408) {
+            GetInteractiveData()->notifyRequestTimedOut();
+            return;
+        }
         if(codeValue == 200 || codeValue == 0) {
             if(rootObj.contains("data")) {
                 QJsonObject vDataObj = rootObj.value("data").toObject();

+ 81 - 66
src/interactive/InteractiveData.cpp

@@ -1,66 +1,81 @@
-#include "InteractiveData.h"
-
-InteractiveData* InteractiveData::pInstance = nullptr;
-
-InteractiveData::InteractiveData()
-{
-}
-
-InteractiveData *InteractiveData::instance()
-{
-    if (!pInstance) {
-        pInstance = new InteractiveData;
-    }
-    return pInstance;
-}
-
-InteractiveData *InteractiveData::create(QQmlEngine *, QJSEngine *)
-{
-    return instance();
-}
-
-bool InteractiveData::isHavePower(const QString &operation)
-{
-    // 如果角色包含超级管理员
-    if (m_roles.contains(QString("admin")))
-    {
-        return true;
-    }
-    // 如果是其他用户,判断是否有执行该操作的权利
-    else if (m_permissions.contains(operation))
-    {
-        return true;
-    }
-    // 如果没有权利执行该操作
-    else
-    {
-        return false;
-    }
-}
-
-QSet<QString> InteractiveData::roles()
-{
-    return m_roles;
-}
-
-void InteractiveData::setRoles(const QSet<QString> &roles)
-{
-    m_roles = roles;
-    emit rolesChanged();
-}
-
-QSet<QString> InteractiveData::permissions()
-{
-    return m_permissions;
-}
-
-void InteractiveData::setPermissions(const QSet<QString> &permissions)
-{
-    m_permissions = permissions;
-    emit permissionsChanged();
-}
-
-InteractiveData::~InteractiveData()
-{
-}
-
+#include "InteractiveData.h"
+#include "InteractiveHttp.h"
+#include "../httpclient/HttpClient.h"
+
+InteractiveData* InteractiveData::pInstance = nullptr;
+
+InteractiveData::InteractiveData()
+{
+    clearToken();
+}
+
+void InteractiveData::clearToken()
+{
+    m_token.clear();
+    InteractiveHttp::strToken.clear();
+    HttpClient::clearToken();
+}
+
+InteractiveData *InteractiveData::instance()
+{
+    if (!pInstance) {
+        pInstance = new InteractiveData;
+    }
+    return pInstance;
+}
+
+InteractiveData *InteractiveData::create(QQmlEngine *, QJSEngine *)
+{
+    return instance();
+}
+
+bool InteractiveData::isHavePower(const QString &operation)
+{
+    // 如果角色包含超级管理员
+    if (m_roles.contains(QString("admin")))
+    {
+        return true;
+    }
+    // 如果是其他用户,判断是否有执行该操作的权利
+    else if (m_permissions.contains(operation))
+    {
+        return true;
+    }
+    // 如果没有权利执行该操作
+    else
+    {
+        return false;
+    }
+}
+
+QSet<QString> InteractiveData::roles()
+{
+    return m_roles;
+}
+
+void InteractiveData::setRoles(const QSet<QString> &roles)
+{
+    m_roles = roles;
+    emit rolesChanged();
+}
+
+QSet<QString> InteractiveData::permissions()
+{
+    return m_permissions;
+}
+
+void InteractiveData::setPermissions(const QSet<QString> &permissions)
+{
+    m_permissions = permissions;
+    emit permissionsChanged();
+}
+
+void InteractiveData::notifyRequestTimedOut()
+{
+    emit requestTimedOut();
+}
+
+InteractiveData::~InteractiveData()
+{
+}
+

+ 62 - 56
src/interactive/InteractiveData.h

@@ -1,56 +1,62 @@
-#ifndef INTERACTIVEDATA_H
-#define INTERACTIVEDATA_H
-#include <QMap>
-#include <QList>
-#include <QVector>
-#include <QString>
-
-#include <QReadWriteLock>
-#include <QReadLocker>
-#include <QWriteLocker>
-#include <QtQml>
-
-#define MATERIALS_TYPE_ALL "0"
-
-class InteractiveData : public QObject
-{
-    Q_OBJECT
-    QML_SINGLETON
-    QML_NAMED_ELEMENT(InteractiveData)
-
-    Q_PROPERTY(QSet<QString> roles READ roles WRITE setRoles NOTIFY rolesChanged)
-    Q_PROPERTY(QSet<QString> permissions READ permissions WRITE setPermissions NOTIFY permissionsChanged)
-signals:
-    void rolesChanged();
-    void permissionsChanged();
-
-public:
-    static InteractiveData* pInstance;
-    static InteractiveData* instance();
-    static InteractiveData* create(QQmlEngine*, QJSEngine*);
-
-    Q_INVOKABLE bool isHavePower(const QString &operation);
-
-    QSet<QString> roles();
-    void setRoles(const QSet<QString> &roles);
-    QSet<QString> permissions();
-    void setPermissions(const QSet<QString> &permissions);
-
-    void cleanJobTicketsModel();
-
-    ~InteractiveData();
-private:
-    explicit InteractiveData();
-public:
-    QString m_userName;                     // 用户名
-    QSet<QString> m_roles;                  // 用户角色
-    QSet<QString> m_permissions;            // 用户许可
-
-    QString m_token = "ab3938d75cf84f02a2fb7fa62f9e4397";
-};
-
-inline InteractiveData* GetInteractiveData()
-{
-    return InteractiveData::instance();
-}
-#endif // INTERACTIVEDATA_H
+#ifndef INTERACTIVEDATA_H
+#define INTERACTIVEDATA_H
+#include <QMap>
+#include <QList>
+#include <QVector>
+#include <QString>
+
+#include <QReadWriteLock>
+#include <QReadLocker>
+#include <QWriteLocker>
+#include <QtQml>
+
+#define MATERIALS_TYPE_ALL "0"
+
+class InteractiveData : public QObject
+{
+    Q_OBJECT
+    QML_SINGLETON
+    QML_NAMED_ELEMENT(InteractiveData)
+
+    Q_PROPERTY(QSet<QString> roles READ roles WRITE setRoles NOTIFY rolesChanged)
+    Q_PROPERTY(QSet<QString> permissions READ permissions WRITE setPermissions NOTIFY permissionsChanged)
+signals:
+    void rolesChanged();
+    void permissionsChanged();
+    /** API 请求超时(408)时发出,QML 可统一显示超时弹窗 */
+    void requestTimedOut();
+
+public:
+    static InteractiveData* pInstance;
+    static InteractiveData* instance();
+    static InteractiveData* create(QQmlEngine*, QJSEngine*);
+
+    Q_INVOKABLE bool isHavePower(const QString &operation);
+    /** 清空 m_token(系统启动、登录失败、退出登录时调用),同时清空 InteractiveHttp::strToken 与 HttpClient::sToken */
+    Q_INVOKABLE void clearToken();
+    /** C++ 侧检测到 API 408 时调用,发出 requestTimedOut 供 QML 显示超时弹窗 */
+    void notifyRequestTimedOut();
+
+    QSet<QString> roles();
+    void setRoles(const QSet<QString> &roles);
+    QSet<QString> permissions();
+    void setPermissions(const QSet<QString> &permissions);
+
+    void cleanJobTicketsModel();
+
+    ~InteractiveData();
+private:
+    explicit InteractiveData();
+public:
+    QString m_userName;                     // 用户名
+    QSet<QString> m_roles;                  // 用户角色
+    QSet<QString> m_permissions;            // 用户许可
+
+    QString m_token;
+};
+
+inline InteractiveData* GetInteractiveData()
+{
+    return InteractiveData::instance();
+}
+#endif // INTERACTIVEDATA_H

+ 195 - 36
src/interactive/InteractiveFace.cpp

@@ -5,10 +5,17 @@
 #include <QPainter>
 #include <QFont>
 #include <QPen>
-#include <QDir>
-#include <QDateTime>
 #include <QCoreApplication>
+#include <QDir>
+#include <QFileInfo>
 #include <QAbstractVideoBuffer>
+#include <QBuffer>
+
+#ifdef INTERACTIVEFACE_OPENCV
+#include <opencv2/objdetect.hpp>
+#include <opencv2/imgproc.hpp>
+#include <opencv2/core.hpp>
+#endif
 
 InteractiveFace* InteractiveFace::pInstance = nullptr;
 
@@ -18,12 +25,8 @@ InteractiveFace::InteractiveFace(QObject *parent)
     , 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()) {
@@ -97,15 +100,26 @@ void InteractiveFace::cameraImagePlay()
     }
     cameraPlay();
     m_FrameId = 0;
-    m_saveFrameTimer->start();
 }
 
 void InteractiveFace::cameraImageStop()
 {
-    m_saveFrameTimer->stop();
     if (hasCamera()) {
         cameraStop();
     }
+    // 停止采集后立即释放当前帧与检测结果,避免占用内存
+    {
+        QMutexLocker locker(&m_mutex);
+        m_image = QImage();
+    }
+    {
+        QMutexLocker locker(&m_faceMutex);
+        m_faceCount = 0;
+        m_faceRects.clear();
+        m_imageWidth = 0;
+        m_imageHeight = 0;
+    }
+    emit faceDetectionUpdated();
 }
 
 QImage InteractiveFace::getImage()
@@ -146,6 +160,7 @@ void InteractiveFace::onVideoFrameProbed(const QVideoFrame &frame)
         if (++m_FrameId <= 5) return; // 跳过前几帧
         QMutexLocker locker(&m_mutex);
         m_image = image;
+        runFaceDetection(m_image);
         if (m_imageGatherCallback.isCallable()) {
             QJSValueList args;
             args << getImageUrl();
@@ -154,11 +169,6 @@ void InteractiveFace::onVideoFrameProbed(const QVideoFrame &frame)
     }
 }
 
-void InteractiveFace::onSaveFrameTimer()
-{
-    saveCurrentFrameToImg();
-}
-
 void InteractiveFace::initCamera()
 {
     if (m_camera) return;
@@ -211,37 +221,186 @@ void InteractiveFace::cameraStop()
     }
 }
 
-void InteractiveFace::saveCurrentFrameToImg()
+int InteractiveFace::faceCount() const
+{
+    QMutexLocker locker(&m_faceMutex);
+    return m_faceCount;
+}
+
+QVariantList InteractiveFace::faceRects() const
+{
+    QMutexLocker locker(&m_faceMutex);
+    QVariantList list;
+    for (const QRect &r : m_faceRects) {
+        QVariantMap m;
+        m["x"] = r.x();
+        m["y"] = r.y();
+        m["width"] = r.width();
+        m["height"] = r.height();
+        list.append(m);
+    }
+    return list;
+}
+
+int InteractiveFace::imageWidth() const
+{
+    QMutexLocker locker(&m_faceMutex);
+    return m_imageWidth;
+}
+
+int InteractiveFace::imageHeight() const
+{
+    QMutexLocker locker(&m_faceMutex);
+    return m_imageHeight;
+}
+
+QByteArray InteractiveFace::captureFaceImageForLogin()
 {
     QImage img = getImage();
-    if (img.isNull() || img.width() == 0 || img.height() == 0) {
-        qDebug() << "[InteractiveFace] 当前没有有效图像,跳过保存";
+    if (img.isNull() || img.width() < 10 || img.height() < 10)
+        return QByteArray();
+
+    int count = faceCount();
+    QVariantList rects = faceRects();
+    if (count != 1 || rects.isEmpty())
+        return QByteArray();
+
+    QVariantMap first = rects.first().toMap();
+    int x = first["x"].toInt();
+    int y = first["y"].toInt();
+    int w = first["width"].toInt();
+    int h = first["height"].toInt();
+    if (w < 20 || h < 20)
+        return QByteArray();
+
+    // 人脸需在画面内且相对完整(不贴边)
+    int margin = 15;
+    if (x < margin || y < margin || x + w > img.width() - margin || y + h > img.height() - margin)
+        return QByteArray();
+
+    // 取正方形区域(以人脸为中心,边长为 face 的 max(w,h) 加适量边距)
+    int side = qMax(w, h) + 80;
+    int cx = x + w / 2;
+    int cy = y + h / 2;
+    int x1 = qBound(0, cx - side / 2, qMax(0, img.width() - side));
+    int y1 = qBound(0, cy - side / 2, qMax(0, img.height() - side));
+    x1 = qMax(0, qMin(x1, img.width() - 1));
+    y1 = qMax(0, qMin(y1, img.height() - 1));
+    if (x1 + side > img.width()) side = img.width() - x1;
+    if (y1 + side > img.height()) side = img.height() - y1;
+    if (side < 30) return QByteArray();
+
+    QImage crop = img.copy(x1, y1, side, side);
+    if (crop.isNull()) return QByteArray();
+
+    QByteArray ba;
+    QBuffer buf(&ba);
+    buf.open(QIODevice::WriteOnly);
+    if (!crop.save(&buf, "JPEG", 85))
+        return QByteArray();
+    return ba;
+}
+
+bool InteractiveFace::canCaptureFaceForLogin() const
+{
+    int count = faceCount();
+    QVariantList rects = faceRects();
+    if (count != 1 || rects.isEmpty())
+        return false;
+
+    QVariantMap first = rects.first().toMap();
+    int x = first["x"].toInt();
+    int y = first["y"].toInt();
+    int w = first["width"].toInt();
+    int h = first["height"].toInt();
+    if (w < 20 || h < 20)
+        return false;
+
+    int imgW = imageWidth();
+    int imgH = imageHeight();
+    if (imgW < 10 || imgH < 10)
+        return false;
+
+    int margin = 15;
+    if (x < margin || y < margin || x + w > imgW - margin || y + h > imgH - margin)
+        return false;
+
+    return true;
+}
+
+void InteractiveFace::runFaceDetection(const QImage &image)
+{
+    if (image.isNull() || image.width() < 20 || image.height() < 20) {
+        QMutexLocker locker(&m_faceMutex);
+        m_imageWidth = image.width();
+        m_imageHeight = image.height();
+        m_faceCount = 0;
+        m_faceRects.clear();
         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;
-                }
+#ifdef INTERACTIVEFACE_OPENCV
+    cv::Mat gray;
+    QImage rgb = image.format() == QImage::Format_RGB32 || image.format() == QImage::Format_ARGB32
+                     ? image.copy()
+                     : image.convertToFormat(QImage::Format_RGB32);
+    if (rgb.bytesPerLine() <= 0) return;
+    cv::Mat mat(rgb.height(), rgb.width(), CV_8UC4, rgb.bits(), static_cast<size_t>(rgb.bytesPerLine()));
+    // Qt Format_RGB32/ARGB32 在内存中多为 BGRA 顺序,用 BGRA2GRAY
+    cv::cvtColor(mat, gray, cv::COLOR_BGRA2GRAY);
+    cv::equalizeHist(gray, gray);
+
+    static cv::CascadeClassifier cascade;
+    static bool cascadeLoaded = false;
+    static bool cascadeTried = false;
+    if (!cascadeLoaded && !cascadeTried) {
+        cascadeTried = true;
+        QStringList paths;
+        paths << QCoreApplication::applicationDirPath() + "/haarcascade_frontalface_default.xml"
+              << QCoreApplication::applicationDirPath() + "/../haarcascade_frontalface_default.xml"
+              << QDir::currentPath() + "/haarcascade_frontalface_default.xml"
+              << "/usr/share/opencv4/haarcascades/haarcascade_frontalface_default.xml"
+              << "/usr/share/opencv/haarcascades/haarcascade_frontalface_default.xml";
+        for (const QString &path : paths) {
+            if (QFileInfo::exists(path) && cascade.load(path.toStdString())) {
+                cascadeLoaded = true;
+                qDebug() << "[InteractiveFace] 加载人脸检测模型:" << path;
+                break;
             }
         }
+        if (!cascadeLoaded)
+            qWarning() << "[InteractiveFace] 未找到 haarcascade_frontalface_default.xml,请将文件放到可执行文件同目录或当前目录";
     }
-    
-    QString timestamp = QString::number(QDateTime::currentMSecsSinceEpoch());
-    QString fileName = baseDir + "/" + timestamp + ".jpg";
-    
-    if (img.save(fileName, "JPEG", 85)) {
-        qDebug() << "[InteractiveFace] 成功保存帧到:" << fileName;
+
+    if (cascadeLoaded && !cascade.empty()) {
+        std::vector<cv::Rect> faces;
+        cascade.detectMultiScale(gray, faces, 1.1, 4, 0, cv::Size(30, 30));
+        QMutexLocker locker(&m_faceMutex);
+        m_imageWidth = image.width();
+        m_imageHeight = image.height();
+        m_faceCount = static_cast<int>(faces.size());
+        m_faceRects.clear();
+        for (const cv::Rect &r : faces) {
+            m_faceRects.append(QRect(r.x, r.y, r.width, r.height));
+        }
+        locker.unlock();
+        emit faceDetectionUpdated();
     } else {
-        qWarning() << "[InteractiveFace] 保存帧失败:" << fileName;
+        QMutexLocker locker(&m_faceMutex);
+        m_imageWidth = image.width();
+        m_imageHeight = image.height();
+        m_faceCount = 0;
+        m_faceRects.clear();
+        locker.unlock();
+        emit faceDetectionUpdated();
     }
+#else
+    QMutexLocker locker(&m_faceMutex);
+    m_imageWidth = image.width();
+    m_imageHeight = image.height();
+    m_faceCount = 0;
+    m_faceRects.clear();
+    locker.unlock();
+    emit faceDetectionUpdated();
+#endif
 }

+ 24 - 4
src/interactive/InteractiveFace.h

@@ -9,6 +9,8 @@
 #include <QVideoProbe>
 #include <QVideoFrame>
 #include <QTimer>
+#include <QVariantList>
+#include <QRect>
 
 #define INTERACTIVE_FACE_IMAGE_URL "InteractiveFaceImage"
 
@@ -54,12 +56,24 @@ public:
     Q_INVOKABLE bool hasValidImage();
     // 获取图像
     QImage getImage();
+
+    // 人脸检测(需 OpenCV):检测框数量、框列表(图像像素坐标)、图像尺寸、截取单人脸图用于登录
+    Q_INVOKABLE int faceCount() const;
+    Q_INVOKABLE QVariantList faceRects() const;
+    Q_INVOKABLE int imageWidth() const;
+    Q_INVOKABLE int imageHeight() const;
+    Q_INVOKABLE QByteArray captureFaceImageForLogin();
+    /** 当前是否可截取单人脸用于登录(单人脸、完整在框内、不贴边) */
+    Q_INVOKABLE bool canCaptureFaceForLogin() const;
+
+signals:
+    void faceDetectionUpdated();
+
 protected:
     QImage requestImage(const QString &, QSize *, const QSize &);
 
 private slots:
     void onVideoFrameProbed(const QVideoFrame &frame);
-    void onSaveFrameTimer();
 
 private:
     // 初始化相机
@@ -72,8 +86,8 @@ private:
     void cameraPlay();
     void cameraStop();
 
-    // 将当前帧保存到 img 目录,文件名用时间戳
-    void saveCurrentFrameToImg();
+    // 人脸检测(内部,每帧或定时调用)
+    void runFaceDetection(const QImage &image);
 
 public:
     QMutex m_mutex;
@@ -84,7 +98,6 @@ private:
 
     QCamera *m_camera;           // 照相机
     QVideoProbe *m_probe;        // 视频帧探测
-    QTimer *m_saveFrameTimer;    // 每秒存一帧定时器
 
     // 记录信息
     qint32 m_laseCount = 0;
@@ -94,6 +107,13 @@ private:
     QJSValue m_imageAppearCallback; // 发现人脸回调
     QJSValue m_imageRemainCallback; // 人脸停留回调
     QJSValue m_isAppearCallback;
+
+    // 人脸检测结果(图像像素坐标)
+    mutable QMutex m_faceMutex;
+    int m_faceCount = 0;
+    QList<QRect> m_faceRects;
+    int m_imageWidth = 0;
+    int m_imageHeight = 0;
 };
 
 inline InteractiveFace* GetInteractiveFace() {

+ 4 - 0
src/main.cpp

@@ -4,6 +4,7 @@
 #include <QDateTime>
 
 #include "./interactive/InteractiveCAN.h"
+#include "./interactive/InteractiveData.h"
 #include "./interactive/InteractiveFace.h"
 #include "./usr/LotoQmlPlugin.h"
 #include "./usr/config.h"
@@ -28,6 +29,9 @@ int main(int argc, char **argv)
     // 注册C++类/对象到QML
     LotoQmlTypes::registerTypes();
 
+    // 系统启动时清空 token(未登录状态)
+    GetInteractiveData()->clearToken();
+
     // 人脸采集 ImageProvider,供 QML 中 Image source="image://InteractiveFaceImage/xxx" 使用
     engine.addImageProvider(QStringLiteral("InteractiveFaceImage"), GetInteractiveFace());
 

+ 528 - 113
src/qml/Login.qml

@@ -1,7 +1,6 @@
 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
@@ -31,12 +30,38 @@ Rectangle {
 
     property int faceAreaSize: 500
 
-    // 刷卡登录状态
-    property bool cardLoginLoading: false           // 刷卡登录loading状态
+    // 刷卡/作业票请求 loading
+    property bool cardLoginLoading: false
+    property bool jobTicketsLoading: false           // 登录成功后请求作业票列表时的 loading
     property bool showCardLoginError: false         // 是否显示刷卡登录错误提示
     property string cardLoginErrorText: ""          // 刷卡登录错误提示内容
     property var cardLoginErrorDialog: null         // 错误提示对话框对象
 
+    // 账号密码登录:loading 与错误弹窗
+    property bool passwordLoginLoading: false
+    property var passwordLoginErrorDialog: null
+
+    // API 超时弹窗(408):处理超时,请稍后重试。确认按钮 10 秒倒计时,关闭后刷新
+    property var timeoutDialog: null
+    property int timeoutCountdown: 10
+    property var timeoutRefreshCallback: null
+
+    // 人脸登录成功:显示「nickName、登录成功」提示,300ms 后关闭并执行刷卡成功逻辑
+    property bool faceLoginSuccessPending: false   // 为 true 时停止采集、API、队列
+    property bool showFaceLoginSuccessPrompt: false
+    property string faceLoginSuccessNickName: ""
+
+    // 人脸弹窗刚打开时忽略旧请求的 success,避免二次打开被误判为成功
+    property bool faceDialogJustOpened: false
+
+    // 人脸登录错误弹窗:API 返回错误时弹窗,确认按钮 10 秒倒计时自动关闭
+    property var faceLoginErrorDialog: null
+    property int faceLoginErrorCountdown: 10
+    // 累计人脸 API 非 200 次数(任意非 200 都统计),满 5 次则停止视频、停止识别并弹窗
+    property int faceLoginError902Count: 0
+    // 已达 5 次非 200,弹窗已显示,停止 API/采集定时器
+    property bool faceLoginError902Reached: false
+
     Component.onCompleted: {
         isCardInput = true;
         // 确保页面加载时获取焦点,以便接收键盘事件
@@ -44,6 +69,62 @@ Rectangle {
         console.log("[Login.qml] 页面加载完成,已获取焦点");
     }
 
+    // 释放人脸识别资源:清空队列与缓存,停止请求(关闭人脸弹窗或登录成功时调用)
+    function clearFaceLoginResources() {
+        if (typeof httpFaceLogin !== "undefined" && httpFaceLogin.clearFaceQueue)
+            httpFaceLogin.clearFaceQueue();
+    }
+
+    // 关闭人脸登录错误弹窗(不关视频框,仅关弹窗)
+    function closeFaceLoginErrorDialog() {
+        if (faceLoginErrorDialog !== null) {
+            faceLoginErrorAutoCloseTimer.stop();
+            faceLoginErrorDialog.visible = false;
+            faceLoginErrorDialog.destroy();
+            faceLoginErrorDialog = null;
+        }
+    }
+
+    // 人脸登录错误:关闭弹窗 + 关闭视频框 + 清理资源
+    function closeFaceLoginErrorAndDialog() {
+        faceLoginErrorAutoCloseTimer.stop();
+        if (faceLoginErrorDialog !== null) {
+            faceLoginErrorDialog.visible = false;
+            faceLoginErrorDialog.destroy();
+            faceLoginErrorDialog = null;
+        }
+        faceLoginError902Count = 0;
+        faceLoginError902Reached = false;
+        InteractiveFace.cameraImageStop();
+        if (typeof httpFaceLogin !== "undefined" && httpFaceLogin.clearFaceQueue)
+            httpFaceLogin.clearFaceQueue();
+        loginInput.visible = false;
+        loginInput.sourceComponent = null;
+        clearFaceLoginResources();
+        faceLoginSuccessPending = false;
+        control.forceActiveFocus();
+    }
+
+    // 显示人脸登录错误弹窗:错误信息 + 确认按钮 10 秒倒计时
+    function showFaceLoginErrorDialog(msg) {
+        closeFaceLoginErrorDialog();
+        faceLoginErrorCountdown = 10;
+        var component = Qt.createComponent("components/AlertDialog.qml");
+        if (component.status === Component.Ready) {
+            faceLoginErrorDialog = component.createObject(appAlertDialog, {
+                messageTypeName: "",  // 仅保留下方一条提示
+                messageValue: msg || "人脸识别登录失败,请重试",
+                messageIconCharacter: "\uf071",
+                confirmBtnText: "确认(10)"
+            });
+            faceLoginErrorDialog.parent = appAlertDialog;
+            faceLoginErrorDialog.confirm.connect(function () {
+                closeFaceLoginErrorAndDialog();
+            });
+            faceLoginErrorAutoCloseTimer.restart();
+        }
+    }
+
     // 关闭刷卡登录错误提示对话框
     function closeCardLoginErrorDialog() {
         console.log("[Login.qml] 关闭错误提示对话框");
@@ -56,6 +137,16 @@ Rectangle {
         cardLoginErrorAutoCloseTimer.stop();
     }
 
+    // 关闭账号密码登录错误提示对话框
+    function closePasswordLoginErrorDialog() {
+        if (passwordLoginErrorDialog !== null) {
+            passwordLoginErrorDialog.visible = false;
+            passwordLoginErrorDialog.destroy();
+            passwordLoginErrorDialog = null;
+        }
+        passwordLoginErrorAutoCloseTimer.stop();
+    }
+
     // 显示刷卡登录错误提示对话框
     function showCardLoginErrorDialog(errorMsg) {
         console.log("[Login.qml] 显示错误提示对话框,内容:", errorMsg);
@@ -86,6 +177,60 @@ Rectangle {
         }
     }
 
+    // 关闭 API 超时弹窗并执行刷新回调
+    function closeTimeoutDialog() {
+        timeoutAutoCloseTimer.stop();
+        if (timeoutDialog !== null) {
+            timeoutDialog.visible = false;
+            timeoutDialog.destroy();
+            timeoutDialog = null;
+        }
+        timeoutRefreshCallback = null;
+    }
+
+    // 显示 API 超时弹窗:处理超时,请稍后重试。确认(10) 倒计时,关闭后执行 refreshCallback(如刷新页面)
+    function showTimeoutDialog(refreshCallback) {
+        closeTimeoutDialog();
+        timeoutCountdown = 10;
+        timeoutRefreshCallback = refreshCallback || null;
+        var component = Qt.createComponent("components/AlertDialog.qml");
+        if (component.status === Component.Ready) {
+            timeoutDialog = component.createObject(appAlertDialog, {
+                messageTypeName: "",
+                messageValue: "处理超时,请稍后重试。",
+                messageIconCharacter: "\uf071",
+                confirmBtnText: "确认(10)"
+            });
+            timeoutDialog.parent = appAlertDialog;
+            timeoutDialog.confirm.connect(function () {
+                if (typeof timeoutRefreshCallback === "function")
+                    timeoutRefreshCallback();
+                closeTimeoutDialog();
+            });
+            timeoutAutoCloseTimer.restart();
+        }
+    }
+
+    // 显示账号密码登录错误提示对话框(与人脸/刷卡一致,使用 AlertDialog)
+    function showPasswordLoginErrorDialog(msg) {
+        closePasswordLoginErrorDialog();
+        var component = Qt.createComponent("components/AlertDialog.qml");
+        if (component.status === Component.Ready) {
+            passwordLoginErrorDialog = component.createObject(appAlertDialog, {
+                messageTypeName: "登录失败",
+                messageValue: msg || "用户名或者密码错误",
+                messageIconCharacter: "\uf071"
+            });
+            passwordLoginErrorDialog.parent = appAlertDialog;
+            passwordLoginErrorDialog.confirm.connect(function () {
+                closePasswordLoginErrorDialog();
+            });
+            passwordLoginErrorAutoCloseTimer.restart();
+        } else {
+            console.log("[Login.qml] 加载AlertDialog组件失败:", component.errorString());
+        }
+    }
+
     HttpPasswordLogin {
         id: httpPasswordLogin
 
@@ -103,12 +248,17 @@ Rectangle {
     Connections {
         target: httpPasswordLogin
         function onSignalLoginReturnStat(stat, data) {
-            if(stat === 0) {
-                loginSuccess = true
+            passwordLoginLoading = false;
+            if (stat === 408) {
+                showTimeoutDialog(function () {
+                    // 刷新:登录页保持,仅关闭 loading 与弹窗
+                });
+                return;
+            }
+            if (stat === 0) {
+                loginSuccess = true;
             } else {
-                showErrorLogin = true;
-                errorNoticeTimeout = 3;
-                errorNoticText = data;
+                showPasswordLoginErrorDialog(data || "用户名或者密码错误");
             }
         }
     }
@@ -127,20 +277,117 @@ Rectangle {
         }
     }
 
+    HttpFaceLogin {
+        id: httpFaceLogin
+
+        // 人脸登录 HTTP 在 HttpFaceLogin 内自建 QNAM 完成,不占用 HttpClient
+    }
+
+    Connections {
+        target: httpFaceLogin
+        function onSignalFaceLoginSuccess(nickName) {
+            // 弹窗未显示或不是人脸弹窗:忽略(可能是上次会话的延迟回调)
+            if (!loginInput.visible || loginInput.sourceComponent !== faceInputDelegate) return
+            // 刚打开弹窗的短时间内忽略,避免上次会话的 QueuedConnection 回调误触发
+            if (control.faceDialogJustOpened) return
+            // 只处理第一次成功,避免多请求同时返回时重复关界面、重复逻辑
+            if (control.faceLoginSuccessPending) return
+            control.faceLoginSuccessPending = true
+            // API 返回成功即释放资源:停摄像头并释放帧缓冲、清空人脸队列与缓存、释放相关引用
+            InteractiveFace.cameraImageStop()
+            if (typeof httpFaceLogin !== "undefined" && httpFaceLogin.clearFaceQueue)
+                httpFaceLogin.clearFaceQueue()
+            if (typeof control.clearFaceLoginResources === "function")
+                control.clearFaceLoginResources()
+            control.faceLoginSuccessNickName = nickName || "用户"
+            control.showFaceLoginSuccessPrompt = true
+            faceLoginSuccessCloseTimer.start()
+        }
+        function onSignalFaceLoginError(msg) {
+            if (msg === "处理超时,请稍后重试。") {
+                control.showTimeoutDialog(control.closeFaceLoginErrorAndDialog);
+                return;
+            }
+            var prevCount = control.faceLoginError902Count || 0;
+            control.faceLoginError902Count = prevCount + 1;
+            var curCount = control.faceLoginError902Count;
+            console.log("[Login.qml] 人脸 API 非200,累计:", curCount, "/5, msg:", msg);
+            if (curCount >= 5) {
+                console.log("[Login.qml] 非200 已达 5 次,停止视频与识别并弹窗");
+                control.faceLoginError902Count = 0;
+                control.faceLoginError902Reached = true;
+                InteractiveFace.cameraImageStop();
+                if (typeof httpFaceLogin !== "undefined" && httpFaceLogin.clearFaceQueue)
+                    httpFaceLogin.clearFaceQueue();
+                if (typeof control.clearFaceLoginResources === "function")
+                    control.clearFaceLoginResources();
+                control.showFaceLoginErrorDialog("人脸识别登录失败,请重试");
+                return;
+            }
+            console.log("[Login.qml] 人脸登录 API 错误,继续识别:", msg);
+        }
+    }
+
+    Timer {
+        id: faceLoginSuccessCloseTimer
+        interval: 300
+        repeat: false
+        running: false
+        onTriggered: {
+            // 按刷卡成功逻辑:关人脸弹窗、清理、跳转作业票
+            control.faceDialogJustOpened = false
+            control.faceLoginError902Count = 0
+            control.faceLoginError902Reached = false
+            if (typeof httpFaceLogin !== "undefined" && httpFaceLogin.clearFaceQueue)
+                httpFaceLogin.clearFaceQueue()
+            loginInput.visible = false
+            loginInput.sourceComponent = null
+            control.showFaceLoginSuccessPrompt = false
+            control.faceLoginSuccessPending = false
+            control.loginSuccess = true
+        }
+    }
+
+    // 人脸弹窗打开后一段时间内忽略旧会话的 success 回调
+    Timer {
+        id: faceDialogJustOpenedTimer
+        interval: 600
+        repeat: false
+        running: false
+        onTriggered: { control.faceDialogJustOpened = false }
+    }
+
+    // 人脸登录错误弹窗:确认按钮 10 秒倒计时,每秒更新按钮文字,到 0 自动关闭
+    Timer {
+        id: faceLoginErrorAutoCloseTimer
+        interval: 1000
+        repeat: true
+        running: false
+        onTriggered: {
+            faceLoginErrorCountdown--
+            if (control.faceLoginErrorDialog)
+                control.faceLoginErrorDialog.confirmBtnText = "确认(" + Math.max(0, faceLoginErrorCountdown) + ")"
+            if (faceLoginErrorCountdown <= 0) {
+                faceLoginErrorAutoCloseTimer.stop()
+                closeFaceLoginErrorAndDialog()
+            }
+        }
+    }
+
     Connections {
         target: httpCardLogin
         function onSignalLoginReturnStat(stat, data) {
             console.log("[Login.qml] 收到登录响应信号,stat:", stat, ",data:", data);
-            // 隐藏loading
             cardLoginLoading = false;
-            console.log("[Login.qml] 隐藏loading");
-            
-            if(stat === 0) {
+
+            if (stat === 0) {
                 console.log("[Login.qml] 登录成功!");
-                loginSuccess = true
+                loginSuccess = true;
+            } else if (stat === 408) {
+                showTimeoutDialog(function () {
+                    // 刷新:登录页保持
+                });
             } else {
-                console.log("[Login.qml] 登录失败,显示错误提示");
-                // 使用AlertDialog显示错误提示
                 showCardLoginErrorDialog(data);
             }
         }
@@ -148,6 +395,7 @@ Rectangle {
 
     // 作业票请求状态标志
     property bool isRequestingJobTickets: false
+    property string lastRequestedWorkNodeId: ""      // 最近一次请求的 nodeId,408 刷新时重试用
     HttpGetWorkNodeDetail {
         id: httpGetWorkNodeDetail
     }
@@ -159,6 +407,18 @@ Rectangle {
             httpClientThread.signalResponseGetWorkNodeDetail.disconnect(httpGetWorkNodeDetail.slotHttpResponseGetWorkNodeDetail);
             httpClientThread.signalResponseGetFormById.disconnect(httpGetWorkNodeDetail.slotHttpResponseGetFormById);
 
+            if (state === 408) {
+                control.cardLoginLoading = false;
+                control.showTimeoutDialog(function () {
+                    control.cardLoginLoading = true;
+                    httpGetWorkNodeDetail.nodeId = control.lastRequestedWorkNodeId;
+                    httpGetWorkNodeDetail.signalGetRequestData.connect(httpClientThread.slotGetRequestData);
+                    httpClientThread.signalResponseGetWorkNodeDetail.connect(httpGetWorkNodeDetail.slotHttpResponseGetWorkNodeDetail);
+                    httpClientThread.signalResponseGetFormById.connect(httpGetWorkNodeDetail.slotHttpResponseGetFormById);
+                    httpGetWorkNodeDetail.start();
+                });
+                return;
+            }
             var isShowNegativeBtn = false;
             var isShowOpsitiveBtn = false;
             var textNegativeBtnStr = "";
@@ -254,14 +514,21 @@ Rectangle {
     Connections {
         target: appHttpGetJobTickets
         function onSignalJobTicketsReturnStat(stat, msg) {
-            // 只有在本页面发起请求时才处理
-            if (!isRequestingJobTickets) {
-                console.log("[Login.qml] 忽略非本页面发起的作业票响应");
+            if (!isRequestingJobTickets)
                 return;
-            }
             isRequestingJobTickets = false;
-            console.log("[Login.qml] 作业票返回: stat=" + stat + ", msg=" + msg);
-            
+            jobTicketsLoading = false;
+            cardLoginLoading = false;
+            passwordLoginLoading = false;
+
+            if (stat === 408) {
+                showTimeoutDialog(function () {
+                    isRequestingJobTickets = true;
+                    jobTicketsLoading = true;
+                    appHttpGetJobTickets.start();
+                });
+                return;
+            }
             if (stat !== 0 || JobTicketModel.rowCount() === 0) {
                 appStackView.push("components/NoJobTicketDialog.qml");
             } else {
@@ -274,7 +541,7 @@ Rectangle {
                         appStackView.push("JobTicketPage.qml");
                     } else {
                         control.cardLoginLoading = true;
-                        cardLoginLoadingDialog.loadingText = "正在查询作业任务..."
+                        control.lastRequestedWorkNodeId = jobTicketinfo.nodeId;
                         httpGetWorkNodeDetail.nodeId = jobTicketinfo.nodeId;
                         // 防止Repeater复制槽函数
                         httpGetWorkNodeDetail.signalGetRequestData.connect(httpClientThread.slotGetRequestData);
@@ -333,21 +600,28 @@ Rectangle {
         }
     }
 
-    property Component faceInputDelegate: Rectangle {
+    property Component faceInputDelegate: Item {
         id: faceInputRect
         width: control.width
         height: control.height
-        color: "transparent"
+        // 使用 Item 而非 Rectangle,避免透明在某些环境下被渲染成黑色方框,只保留圆形区域与控件
 
         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() {
+            control.faceDialogJustOpened = false;
+            control.faceLoginError902Count = 0;
+            control.faceLoginError902Reached = false;
             InteractiveFace.cameraImageStop();
             cameraTimeoutTimer.stop();
+            if (typeof httpFaceLogin !== "undefined" && httpFaceLogin.clearFaceQueue)
+                httpFaceLogin.clearFaceQueue();
+            if (parent.parent && parent.parent.clearFaceLoginResources)
+                parent.parent.clearFaceLoginResources();
             loginInput.visible = false;
             loginInput.sourceComponent = null;
             control.forceActiveFocus();
@@ -363,21 +637,22 @@ Rectangle {
         Component.onDestruction: {
             InteractiveFace.cameraImageStop();
             cameraTimeoutTimer.stop();
+            if (parent.parent && parent.parent.clearFaceLoginResources)
+                parent.parent.clearFaceLoginResources();
         }
 
-        // 60秒超时定时器
+        // 超时与释放:摄像头 60s 无操作自动关;人脸 API 单次 3s;成功提示 300ms 后关
         Timer {
             id: cameraTimeoutTimer
-            interval: 60000  // 60秒
+            interval: 60000
             repeat: false
             running: false
             onTriggered: {
-                console.log("[Login.qml] 摄像头打开60秒超时,自动关闭");
                 closeFaceLoginDialog();
             }
         }
 
-        // 圆心摄像头影像(圆形裁剪)
+        // 摄像头影像(圆形):参考之前可用的写法,OpacityMask 只裁 Image,Image 隐藏仅通过 OpacityMask 显示
         Item {
             id: cameraCircleArea
             width: faceDiameter
@@ -386,40 +661,42 @@ Rectangle {
             anchors.verticalCenter: parent.verticalCenter
             anchors.verticalCenterOffset: -30  // 向上偏移,为下方提示文字留出空间
 
-            // 圆形背景(当没有视频流时显示)
+            // 圆形背景(视频流时显示)
             Rectangle {
                 id: cameraBackground
                 anchors.fill: parent
                 radius: faceRadius
-                color: "#1A1A1A"  // 深色背景
+                color: "#1A1A1A"
                 visible: !hasValidCameraImage || faceCameraUrl === ""
             }
 
-            // 圆形遮罩(用于裁剪)
+            // 圆形遮罩(白=显示,透明=裁剪)
             Rectangle {
                 id: circleMask
-                anchors.fill: parent
+                width: faceDiameter
+                height: faceDiameter
                 radius: faceRadius
                 color: "white"
                 visible: false
             }
 
-            // 摄像头画面(圆形裁剪)
+            // 摄像头画面(圆形裁剪):只对 Image 做 OpacityMask,有图时才显示
             OpacityMask {
                 anchors.fill: parent
                 source: cameraImage
                 maskSource: circleMask
                 visible: hasValidCameraImage && faceCameraUrl !== ""
+                cached: true
             }
 
-            // 摄像头画面源(隐藏,只用于 OpacityMask
+            // 摄像头画面源(隐藏,只通过 OpacityMask 显示为圆形
             Image {
                 id: cameraImage
                 anchors.fill: parent
                 fillMode: Image.PreserveAspectCrop
                 source: hasValidCameraImage ? faceCameraUrl : ""
                 cache: false
-                visible: false  // 隐藏,只通过 OpacityMask 显示
+                visible: false
                 asynchronous: false
             }
 
@@ -427,7 +704,7 @@ Rectangle {
             Text {
                 id: noImageText
                 anchors.centerIn: parent
-                text: "无图像"
+                text: "视频加载中..."
                 color: "#888888"
                 font.pixelSize: 28
                 font.bold: true
@@ -435,114 +712,190 @@ Rectangle {
                 z: 2
             }
 
-            // 圆形识别框边框(仅保留圆形边框,无任何其他装饰或图案)
+            // 人脸检测框(绿色矩形,画在圆形视频上方)
+            Repeater {
+                model: faceInputRect.faceRectsList
+                delegate: Rectangle {
+                    readonly property real imgW: faceInputRect.faceImageWidth > 0 ? faceInputRect.faceImageWidth : 1
+                    readonly property real imgH: faceInputRect.faceImageHeight > 0 ? faceInputRect.faceImageHeight : 1
+                    readonly property real scale: Math.max(cameraCircleArea.width / imgW, cameraCircleArea.height / imgH)
+                    readonly property real visSize: cameraCircleArea.width / scale
+                    readonly property real cropX: (imgW - visSize) / 2
+                    readonly property real cropY: (imgH - visSize) / 2
+                    x: (modelData && modelData.x !== undefined ? (modelData.x - cropX) * scale : 0)
+                    y: (modelData && modelData.y !== undefined ? (modelData.y - cropY) * scale : 0)
+                    width: (modelData && modelData.width !== undefined ? modelData.width * scale : 0)
+                    height: (modelData && modelData.height !== undefined ? modelData.height * scale : 0)
+                    color: "transparent"
+                    border.color: "#00FF00"
+                    border.width: 3
+                    visible: faceInputRect.hasValidCameraImage && faceInputRect.faceImageWidth > 0 && width > 0 && height > 0
+                    antialiasing: true
+                    z: 3
+                }
+            }
+
+            // 红色圆框
             Rectangle {
-                id: faceRecognitionFrame
                 anchors.fill: parent
                 radius: faceRadius
                 color: "transparent"
-                border.color: "#40C7FF"  // 使用主题色
+                border.color: "red"
                 border.width: 3
-                z: 3
                 antialiasing: true
+                z: 4
             }
         }
 
-
-        // 提示文字
+        // 提示文字(无人脸/不完整 / 多张人脸 / 单人脸)
         Text {
             id: faceTipText
             anchors.horizontalCenter: parent.horizontalCenter
             anchors.top: cameraCircleArea.bottom
             anchors.topMargin: 20
-            text: "请将脸部放入框内"
+            text: faceTipMessage
             color: "#FFFFFF"
             font.pixelSize: 24
             font.bold: true
             horizontalAlignment: Text.AlignHCenter
-            z: 10  // 确保显示在遮罩上方
+            z: 10
         }
 
         property string faceCameraUrl: ""
         property bool hasValidCameraImage: false
-        
+        property int faceCount: 0
+        property var faceRectsList: []
+        property int faceImageWidth: 0
+        property int faceImageHeight: 0
+        property string faceTipMessage: "请看向摄像头,确保人脸在圆框内"
+        property bool faceCanCapture: false
+
         Timer {
             id: faceCameraRefreshTimer
             interval: 100
             repeat: true
-            running: faceInputRect.visible && InteractiveFace.hasCamera()
+            running: (faceInputRect.visible || faceInputRect.width > 0) && InteractiveFace.hasCamera()
             onTriggered: {
-                // 先检查是否有有效图像
                 var hasImage = InteractiveFace.hasValidImage();
                 if (hasImage) {
-                    // 只有在有有效图像时才更新 URL
                     faceInputRect.faceCameraUrl = InteractiveFace.imageUrl();
                     faceInputRect.hasValidCameraImage = true;
+                    faceInputRect.faceCount = InteractiveFace.faceCount();
+                    faceInputRect.faceRectsList = InteractiveFace.faceRects();
+                    faceInputRect.faceImageWidth = InteractiveFace.imageWidth();
+                    faceInputRect.faceImageHeight = InteractiveFace.imageHeight();
+                    faceInputRect.faceCanCapture = InteractiveFace.canCaptureFaceForLogin();
+                    // 动态提示:无人脸 / 人脸不正或不完整 / 人脸完整
+                    if (faceInputRect.faceCount === 0) {
+                        faceInputRect.faceTipMessage = "请看向摄像头,确保人脸在圆框内";
+                    } else if (faceInputRect.faceCount > 1) {
+                        faceInputRect.faceTipMessage = "检测到多张人脸,请保持框内仅有一个人脸";
+                    } else if (!faceInputRect.faceCanCapture) {
+                        faceInputRect.faceTipMessage = "请正对着摄像头,确保人脸完全显示";
+                    } else {
+                        faceInputRect.faceTipMessage = "正在识别...";
+                    }
                 } else {
-                    // 没有图像时清空 URL,避免请求空图像
                     if (faceInputRect.hasValidCameraImage) {
                         faceInputRect.faceCameraUrl = "";
                     }
                     faceInputRect.hasValidCameraImage = false;
+                    faceInputRect.faceCount = 0;
+                    faceInputRect.faceRectsList = [];
+                    faceInputRect.faceImageWidth = 0;
+                    faceInputRect.faceImageHeight = 0;
+                    faceInputRect.faceCanCapture = false;
+                    faceInputRect.faceTipMessage = "请看向摄像头,确保人脸在圆框内";
                 }
             }
         }
 
-        // 半透明遮罩 + 圆形镂空(圆心区域透明,显示摄像头)
-        Shape {
-            anchors.fill: parent
-            ShapePath {
-                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 }
-                // 内圈:圆心圆形镂空
-                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
-                }
-                PathArc {
-                    x: faceInputRect.faceCx + faceInputRect.faceRadius; y: faceInputRect.faceCy
-                    radiusX: faceInputRect.faceRadius; radiusY: faceInputRect.faceRadius
-                    direction: PathArc.Clockwise
-                }
-                PathArc {
-                    x: faceInputRect.faceCx; y: faceInputRect.faceCy - faceInputRect.faceRadius
-                    radiusX: faceInputRect.faceRadius; radiusY: faceInputRect.faceRadius
-                    direction: PathArc.Clockwise
-                }
-                PathArc {
-                    x: faceInputRect.faceCx - faceInputRect.faceRadius; y: faceInputRect.faceCy
-                    radiusX: faceInputRect.faceRadius; radiusY: faceInputRect.faceRadius
-                    direction: PathArc.Clockwise
+        // 流程:检测到一张人脸(排除多张) → 截取合格则入队(最多10张FIFO) → 每300ms从队列随机取一张发API(超时3s);取比推略快故队列常为0~2
+        // 入队:仅当检测到恰好一张人脸且可截取时,每 150ms 截一帧入队(满足「一张人脸即入队」)
+        Timer {
+            id: faceLoginTriggerTimer
+            interval: 150
+            repeat: false
+            running: false
+            onTriggered: {
+                if (!faceInputRect.faceCanCapture) return;
+                var data = InteractiveFace.captureFaceImageForLogin();
+                var dataLen = (data && data.byteLength !== undefined) ? data.byteLength : (data && typeof data.size === "function" ? data.size() : (data ? (data.length || 0) : 0));
+                if (data && dataLen > 0) {
+                    httpFaceLogin.pushFaceImage(data);
+                    if (faceInputRect.faceCanCapture && !faceLoginTriggerTimer.running)
+                        faceLoginTriggerTimer.start();
                 }
             }
         }
+        // 定时请求 API:每 300ms 启动一次请求;登录成功后或 902 已达 5 次时停止
+        Timer {
+            id: faceLoginApiTimer
+            interval: 300
+            repeat: true
+            running: faceInputRect.visible && InteractiveFace.hasCamera()
+                      && !control.faceLoginSuccessPending && !control.faceLoginError902Reached
+            onTriggered: {
+                httpFaceLogin.start();
+            }
+        }
+        function checkFaceLoginTrigger() {
+            var pending = control.faceLoginSuccessPending || control.faceLoginError902Reached;
+            if (faceCanCapture && !faceLoginTriggerTimer.running && !pending)
+                faceLoginTriggerTimer.start();
+            else
+                faceLoginTriggerTimer.stop();
+        }
+        onFaceCanCaptureChanged: checkFaceLoginTrigger()
+        // 902 达 5 次时立即停止采集定时器
+        Connections {
+            target: control
+            function onFaceLoginError902ReachedChanged() {
+                if (control.faceLoginError902Reached)
+                    faceLoginTriggerTimer.stop();
+            }
+        }
 
-        // 关闭按钮
-        MButton {
+        // 关闭按钮:圆形框右上角,圆形图标按钮
+        Rectangle {
             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();
+            anchors.right: cameraCircleArea.right
+            anchors.top: cameraCircleArea.top
+            anchors.rightMargin: -4
+            anchors.topMargin: -4
+            width: 44
+            height: 44
+            radius: width / 2
+            color: faceCloseBtnMa.containsMouse ? "#80000000" : "#40000000"
+            border.width: 1
+            border.color: faceCloseBtnMa.containsMouse ? "#60ffffff" : "#30ffffff"
+            opacity: faceCloseBtnMa.containsMouse ? 1 : 0.9
+            z: 20
+
+            Behavior on color { ColorAnimation { duration: 120 } }
+            Behavior on border.color { ColorAnimation { duration: 120 } }
+            Behavior on opacity { NumberAnimation { duration: 120 } }
+
+            Text {
+                width: parent.width
+                height: parent.height
+                anchors.horizontalCenter: parent.horizontalCenter
+                anchors.verticalCenter: parent.verticalCenter
+                anchors.verticalCenterOffset: -3
+                text: "×"
+                font.pixelSize: 36
+                font.bold: true
+                color: "#ffffff"
+                verticalAlignment: Text.AlignVCenter
+                horizontalAlignment: Text.AlignHCenter
+            }
+
+            MouseArea {
+                id: faceCloseBtnMa
+                anchors.fill: parent
+                hoverEnabled: true
+                cursorShape: Qt.PointingHandCursor
+                onClicked: closeFaceLoginDialog()
             }
         }
     }
@@ -703,6 +1056,7 @@ Rectangle {
 
                         onClicked:  {
                             virtualKeyboard.hide();
+                            passwordLoginLoading = true;
                             httpPasswordLogin.username = usernameText.text;
                             httpPasswordLogin.password = passwordText.text;
                             httpPasswordLogin.start();
@@ -848,7 +1202,14 @@ Rectangle {
             textColor: "white"
 
             onClicked: {
-                // 弹出人脸遮罩
+                faceLoginSuccessPending = false;
+                showFaceLoginSuccessPrompt = false;
+                faceLoginError902Count = 0;
+                faceLoginError902Reached = false;
+                if (typeof httpFaceLogin !== "undefined" && httpFaceLogin.clearFaceQueue)
+                    httpFaceLogin.clearFaceQueue();
+                faceDialogJustOpened = true;
+                faceDialogJustOpenedTimer.start();
                 loginInput.sourceComponent = faceInputDelegate;
                 loginInput.visible = true;
             }
@@ -906,7 +1267,7 @@ Rectangle {
             width: 320
             height: 28
 
-            text: qsTr("您可以通过刷卡直接进行登录")
+            text: qsTr("请选择登录方式,您可以通过刷卡直接登录")
             color: loginTextColor
             font.pixelSize: 20
             // 普通文字不使用图标字体
@@ -942,21 +1303,22 @@ Rectangle {
             }
         }
     }
-    // 登录成功
+    // 登录成功(刷卡/密码/人脸统一:按刷卡成功逻辑跳转作业票)
     onLoginSuccessChanged: {
         if (loginSuccess) {
-            // 关闭可能存在的loading和错误提示
+            // 关闭可能存在的 loading、刷卡错误、人脸错误、账号密码错误弹窗
             cardLoginLoading = false;
+            passwordLoginLoading = false;
             closeCardLoginErrorDialog();
-            
+            closeFaceLoginErrorDialog();
+            closePasswordLoginErrorDialog();
+
             loginInput.visible = false;
-            appShowLogout = true
+            appShowLogout = true;
             loginInput.sourceComponent = null;
-            // TODO: 登录成功跳入作业页面
-            // appStackView.push("WorkingPage.qml")
             isRequestingJobTickets = true;
-            httpGetJobTickets.start();
-            //appStackView.push("components/NoJobTicketDialog.qml")
+            jobTicketsLoading = true;
+            appHttpGetJobTickets.start();
         }
     }
 
@@ -972,10 +1334,63 @@ Rectangle {
         }
     }
 
-    // 刷卡登录Loading组件
+    // 账号密码登录错误提示自动关闭定时器(3秒)
+    Timer {
+        id: passwordLoginErrorAutoCloseTimer
+        interval: 3000
+        running: false
+        repeat: false
+        onTriggered: {
+            closePasswordLoginErrorDialog();
+        }
+    }
+
+    // 超时弹窗 10 秒倒计时定时器
+    Timer {
+        id: timeoutAutoCloseTimer
+        interval: 1000
+        repeat: true
+        running: false
+        onTriggered: {
+            timeoutCountdown--;
+            if (timeoutDialog !== null)
+                timeoutDialog.confirmBtnText = "确认(" + Math.max(0, timeoutCountdown) + ")";
+            if (timeoutCountdown <= 0) {
+                if (typeof timeoutRefreshCallback === "function")
+                    timeoutRefreshCallback();
+                closeTimeoutDialog();
+            }
+        }
+    }
+
+    // 刷卡/账号密码/作业票 Loading
     LoadingDialog {
         id: cardLoginLoadingDialog
-        loadingText: "登录中,请稍后..."
-        showLoading: cardLoginLoading
+        loadingText: jobTicketsLoading ? "正在查询作业任务..." : (passwordLoginLoading ? "处理中..." : "登录中,请稍后...")
+        showLoading: cardLoginLoading || passwordLoginLoading || jobTicketsLoading
+    }
+
+    // 人脸登录成功提示:无全屏黑底,仅居中卡片+文字;先关人脸弹窗再关提示,避免黑闪
+    Item {
+        id: faceLoginSuccessOverlay
+        anchors.fill: parent
+        visible: showFaceLoginSuccessPrompt
+        z: 2000
+        Rectangle {
+            id: faceSuccessCard
+            anchors.centerIn: parent
+            width: faceSuccessLabel.implicitWidth + 48
+            height: faceSuccessLabel.implicitHeight + 24
+            radius: 12
+            color: "#E6181890"
+            Text {
+                id: faceSuccessLabel
+                anchors.centerIn: parent
+                text: (faceLoginSuccessNickName || "用户") + "、登录成功"
+                color: "white"
+                font.pixelSize: 36
+                font.bold: true
+            }
+        }
     }
 }

+ 63 - 0
src/qml/SettingPage.qml

@@ -14,6 +14,10 @@ Rectangle {
     property string userName: "李明"
     property string userType: "操作员"
     property string userGroup: "电气维护组"
+    property bool updatePasswordLoading: false
+    property var timeoutDialog: null
+    property int timeoutCountdown: 10
+    property var timeoutRefreshCallback: null
 
     function showUpdatePasswordDialog(cancelCallbackFunc, confirmCallbackFunc) {
         var component = Qt.createComponent("components/UpdatePasswordDialog.qml");
@@ -39,6 +43,7 @@ Rectangle {
         componentObj.visible = false;
         httpUpdateUserPassword.oldPassword = oldValue;
         httpUpdateUserPassword.newPassword = newValue;
+        updatePasswordLoading = true;
         httpUpdateUserPassword.start();
         componentObj.destroy();
     }
@@ -65,6 +70,36 @@ Rectangle {
         appAlertDialog.sourceComponent = null;
         componentObj.destroy();
     }
+    function closeTimeoutDialog() {
+        timeoutAutoCloseTimer.stop();
+        if (timeoutDialog !== null) {
+            timeoutDialog.visible = false;
+            timeoutDialog.destroy();
+            timeoutDialog = null;
+        }
+        timeoutRefreshCallback = null;
+    }
+    function showTimeoutDialog(refreshCallback) {
+        closeTimeoutDialog();
+        timeoutCountdown = 10;
+        timeoutRefreshCallback = refreshCallback || null;
+        var component = Qt.createComponent("components/AlertDialog.qml");
+        if (component.status === Component.Ready) {
+            timeoutDialog = component.createObject(appAlertDialog, {
+                messageTypeName: "",
+                messageValue: "处理超时,请稍后重试。",
+                messageIconCharacter: "\uf071",
+                confirmBtnText: "确认(10)"
+            });
+            timeoutDialog.parent = appAlertDialog;
+            timeoutDialog.confirm.connect(function () {
+                if (typeof timeoutRefreshCallback === "function")
+                    timeoutRefreshCallback();
+                closeTimeoutDialog();
+            });
+            timeoutAutoCloseTimer.restart();
+        }
+    }
 
     function opsitiveAlertDialogCallback(componentObj) {
         negativeAlertDialogCallback(componentObj);
@@ -127,6 +162,11 @@ Rectangle {
     Connections {
         target: httpUpdateUserPassword
         function onSignalUpdatePasswordReturnStat(stat, msg) {
+            updatePasswordLoading = false;
+            if (stat === 408) {
+                showTimeoutDialog(function () { });
+                return;
+            }
             if (stat !== 0) {
                 showAlertDialog("提示", msg, "\uf071", negativeAlertDialogCallback);
             } else {
@@ -135,6 +175,29 @@ Rectangle {
         }
     }
 
+    Timer {
+        id: timeoutAutoCloseTimer
+        interval: 1000
+        repeat: true
+        running: false
+        onTriggered: {
+            timeoutCountdown--;
+            if (timeoutDialog !== null)
+                timeoutDialog.confirmBtnText = "确认(" + Math.max(0, timeoutCountdown) + ")";
+            if (timeoutCountdown <= 0) {
+                if (typeof timeoutRefreshCallback === "function")
+                    timeoutRefreshCallback();
+                closeTimeoutDialog();
+            }
+        }
+    }
+
+    LoadingDialog {
+        id: updatePasswordLoadingDialog
+        loadingText: "处理中..."
+        showLoading: updatePasswordLoading
+    }
+
     Rectangle {
         id: __container
         x: 260

+ 78 - 4
src/qml/WorkingPage.qml

@@ -30,6 +30,11 @@ Rectangle {
     property string workingTimeText: ""
 
     property bool showNegativeBtn: true     // 是否显示消极按钮,如取消、审核不通过
+    property bool submitFormLoading: false   // 提交表单 API loading
+    property bool refreshListLoading: false  // 刷新作业列表 API loading
+    property var timeoutDialog: null
+    property int timeoutCountdown: 10
+    property var timeoutRefreshCallback: null
     property string textNegativeBtn: "审核不通过"
     property color colorNegativeBtn: "#CCF5222D"
     property bool showOpsitiveBtn: true     // 是否显示积极按钮,如确定、审核通过
@@ -58,15 +63,49 @@ Rectangle {
         }
     }
 
+    function closeTimeoutDialog() {
+        timeoutAutoCloseTimer.stop();
+        if (timeoutDialog !== null) {
+            timeoutDialog.visible = false;
+            timeoutDialog.destroy();
+            timeoutDialog = null;
+        }
+        timeoutRefreshCallback = null;
+    }
+    function showTimeoutDialog(refreshCallback) {
+        closeTimeoutDialog();
+        timeoutCountdown = 10;
+        timeoutRefreshCallback = refreshCallback || null;
+        var component = Qt.createComponent("components/AlertDialog.qml");
+        if (component.status === Component.Ready) {
+            timeoutDialog = component.createObject(appAlertDialog, {
+                messageTypeName: "",
+                messageValue: "处理超时,请稍后重试。",
+                messageIconCharacter: "\uf071",
+                confirmBtnText: "确认(10)"
+            });
+            timeoutDialog.parent = appAlertDialog;
+            timeoutDialog.confirm.connect(function () {
+                if (typeof timeoutRefreshCallback === "function")
+                    timeoutRefreshCallback();
+                closeTimeoutDialog();
+            });
+            timeoutAutoCloseTimer.restart();
+        }
+    }
+
     Connections {
         target: httpUpdateNodeApproval
         function onSignalUpdateNodeApprovalReturnStat(stat, msg) {
+            submitFormLoading = false;
+            if (stat === 408) {
+                showTimeoutDialog(function () { });
+                return;
+            }
             if (stat !== 0) {
                 showAlertDialog("提示", msg, "\uf071", negativeAlertDialogCallback);
             } else {
-                // 任务提交成功后,再次访问"我的任务"用于刷新作业页面
                 appHttpGetJobTickets.start();
-                // 提交成功统一提示
                 showSuccessConfirmDialog("提交成功,继续操作?");
             }
         }
@@ -84,10 +123,15 @@ Rectangle {
     Connections {
         target: httpRefreshJobTickets
         function onSignalJobTicketsReturnStat(stat, msg) {
-            console.log("[WorkingPage.qml] 刷新数据返回: stat=" + stat);
+            refreshListLoading = false;
+            if (stat === 408) {
+                showTimeoutDialog(function () {
+                    _doRefreshList();
+                });
+                return;
+            }
             if (isRefreshingData) {
                 isRefreshingData = false;
-                // 断开信号
                 try {
                     httpRefreshJobTickets.signalGetRequestData.disconnect(httpClientThread.slotGetRequestData);
                 } catch(e) {}
@@ -121,6 +165,7 @@ Rectangle {
     }
     
     function _doRefreshList() {
+        refreshListLoading = true;
         try {
             httpRefreshJobTickets.signalGetRequestData.disconnect(httpClientThread.slotGetRequestData);
         } catch(e) {}
@@ -131,6 +176,34 @@ Rectangle {
         httpClientThread.signalResponseGetJobTickets.connect(httpRefreshJobTickets.slotHttpResponseGetJobTickets);
         httpRefreshJobTickets.start();
     }
+
+    Timer {
+        id: timeoutAutoCloseTimer
+        interval: 1000
+        repeat: true
+        running: false
+        onTriggered: {
+            timeoutCountdown--;
+            if (timeoutDialog !== null)
+                timeoutDialog.confirmBtnText = "确认(" + Math.max(0, timeoutCountdown) + ")";
+            if (timeoutCountdown <= 0) {
+                if (typeof timeoutRefreshCallback === "function")
+                    timeoutRefreshCallback();
+                closeTimeoutDialog();
+            }
+        }
+    }
+
+    LoadingDialog {
+        id: submitFormLoadingDialog
+        loadingText: "处理中..."
+        showLoading: submitFormLoading
+    }
+    LoadingDialog {
+        id: refreshListLoadingDialog
+        loadingText: "处理中..."
+        showLoading: refreshListLoading
+    }
     
 
     Rectangle {
@@ -435,6 +508,7 @@ Rectangle {
                         onClicked: {
                             var formInfo = formCard.collectFormInputInfo();
                             if (validateFormInfo(formInfo)) {
+                                control.submitFormLoading = true;
                                 httpUpdateNodeApproval.nodeId = control.currentNodeId;
                                 httpUpdateNodeApproval.approvalStatus = "approved";
                                 httpUpdateNodeApproval.formData = formInfo;

+ 127 - 63
src/qml/components/FormCard.qml

@@ -6,9 +6,10 @@ Rectangle {
     id: control
     width: parent.width
     height: parent.height
-    border.color: "#40A9FF"
-    radius: 20
-    color: "transparent"
+    border.color: "#355a80"
+    border.width: 1
+    radius: 16
+    color: "#0a1520"
 
     signal signalSubmit(bool submit)
 
@@ -20,14 +21,14 @@ Rectangle {
     property int iconSize: 14
     property color textColor: "white"
 
-    property int padding: 20    // 边距
-    property int spacing: 28    // 间隔
+    property int padding: 24    // 边距
+    property int spacing: 32     // 表单项间隔,略加大留白
     property int itemWidth: 300
-    property int itemHeight: 90
+    property int itemHeight: 156  // 提高以容纳标题+选项,使背景边框完整包住内容
     property int delegateWidth: itemWidth + spacing
     property int delegateHeight: itemHeight + spacing
 
-    property int textAreaHeight: 160
+    property int textAreaHeight: 188
     property var returnFormInfo
     
     // 只读模式(已完成状态时表单不可编辑)
@@ -41,10 +42,10 @@ Rectangle {
         id: contentItem
         width: parent.width - 60
         anchors.fill: parent
-        anchors.leftMargin: 30
-        anchors.rightMargin: 30
-        anchors.topMargin: 30
-        anchors.bottomMargin: 20
+        anchors.leftMargin: 36
+        anchors.rightMargin: 36
+        anchors.topMargin: 36
+        anchors.bottomMargin: 28
 
         clip: true
         contentWidth: width
@@ -76,45 +77,116 @@ Rectangle {
 
                 Rectangle {
                     id: formArea
-                    color: "transparent"
+                    color: "#0d1a28"
                     radius: 12
+                    border.color: "#355a80"
+                    border.width: 1
+
+                    // 表单项之间的细小横线分割(非第一行时显示)
+                    Rectangle {
+                        id: topDivider
+                        anchors.top: parent.top
+                        anchors.left: parent.left
+                        anchors.right: parent.right
+                        height: 1
+                        color: "#2a4a6a"
+                        visible: groupIndex >= (gridColumns === 0 ? 1 : gridColumns)
+                    }
 
-                    // 标签行(必填标识 + 标签文字)
-                    Row {
-                        id: labelRow
-                        spacing: 4
-                        
-                        Text {
-                            id: formRequiredLabel
-                            width: required ? 12 : 0
-                            height: 22
-                            visible: required
-                            text: "*"
-                            color: "#ff4d4f"
-                            font.pixelSize: 16
-                            font.family: iconFont.name
-                            font.bold: true
-                            verticalAlignment: Text.AlignVCenter
+                    Item {
+                        id: contentContainer
+                        anchors.top: topDivider.bottom
+                        anchors.left: parent.left
+                        anchors.right: parent.right
+                        anchors.bottom: parent.bottom
+                        anchors.topMargin: 14
+                        anchors.leftMargin: 14
+                        anchors.rightMargin: 14
+                        anchors.bottomMargin: 14
+
+                        // 横向一行:标题 + 选项(radio/switch 用开关/分段形式)
+                        Row {
+                            id: rowLayout
+                            visible: type === "radio" || type === "switch"
+                            anchors.fill: parent
+                            spacing: 16
+                            Row {
+                                id: labelRow
+                                spacing: 6
+                                height: 48
+                                anchors.verticalCenter: parent.verticalCenter
+                                Text {
+                                    id: formRequiredLabel
+                                    width: required ? 14 : 0
+                                    height: 28
+                                    visible: required
+                                    text: "*"
+                                    color: "#ff6b6b"
+                                    font.pixelSize: 18
+                                    font.family: iconFont.name
+                                    font.bold: true
+                                    verticalAlignment: Text.AlignVCenter
+                                    anchors.verticalCenter: parent.verticalCenter
+                                }
+                                Text {
+                                    id: formLabel
+                                    height: 28
+                                    text: ""
+                                    color: "#e8e8e8"
+                                    font.pixelSize: 20
+                                    font.bold: true
+                                    verticalAlignment: Text.AlignVCenter
+                                    anchors.verticalCenter: parent.verticalCenter
+                                }
+                            }
+                            Loader {
+                                id: formControlRow
+                                width: parent.width - labelRow.width - 16
+                                height: 48
+                                anchors.verticalCenter: parent.verticalCenter
+                                visible: type === "radio" || type === "switch"
+                            }
                         }
 
-                        Text {
-                            id: formLabel
-                            height: 22
-                            text: ""
-                            color: "#e0e0e0"
-                            font.pixelSize: 15
-                            // 表单标签不使用图标字体
-                            verticalAlignment: Text.AlignVCenter
+                        // 纵向:标题在上、控件在下(input/textarea 等)
+                        Column {
+                            id: columnLayout
+                            visible: type !== "radio" && type !== "switch"
+                            anchors.fill: parent
+                            spacing: 12
+                            Row {
+                                id: labelRow2
+                                spacing: 6
+                                Text {
+                                    id: formRequiredLabel2
+                                    width: required ? 14 : 0
+                                    height: 28
+                                    visible: required
+                                    text: "*"
+                                    color: "#ff6b6b"
+                                    font.pixelSize: 18
+                                    font.family: iconFont.name
+                                    font.bold: true
+                                    verticalAlignment: Text.AlignVCenter
+                                }
+                                Text {
+                                    id: formLabel2
+                                    height: 28
+                                    text: ""
+                                    color: "#e8e8e8"
+                                    font.pixelSize: 20
+                                    font.bold: true
+                                    verticalAlignment: Text.AlignVCenter
+                                }
+                            }
+                            Loader {
+                                id: formControlColumn
+                                width: parent.width
+                                height: parent.height - labelRow2.height - 12
+                                visible: type !== "radio" && type !== "switch"
+                            }
                         }
                     }
-
-                    // 控件区域
-                    Loader {
-                        id: formControl
-                        anchors.top: labelRow.bottom
-                        anchors.topMargin: 10
-                        width: formArea.width
-                    }
                 }
 
                 Component.onCompleted: {
@@ -208,6 +280,8 @@ Rectangle {
                         formControl.item.required = required;
                         formControl.item.requiredMsg = requiredMessage !== "" ? requiredMessage : label.trim()+"不能为空";
                         formControl.item.enabled = !control.readOnly;
+                        formControl.item.fontSize = 15;
+                        formControl.item.backgroundVisible = false;
                         var radioOptions = [];
                         var selectedIdx = -1;
                         for (var i = 0; i < options.length; i++) {
@@ -256,30 +330,20 @@ Rectangle {
         }
     }
 
-    // 计算ListView中代理项的Y位置
+    // 计算ListView中代理项的Y位置(统一为 0,避免边框错位)
     function getDelegateY(index, gridColumns, groupIndex) {
-        var delegateY = 0;
-        if (index-gridColumns >= 0 && (groupIndex%(gridColumns===0?1:gridColumns))===0) {
-            for (var i = index-gridColumns; i < index; i++) {
-                var itemType = WorkNodeFormModel.getType(i);
-
-                if (itemType === "textarea") {
-                    delegateY = control.textAreaHeight-control.itemHeight-control.spacing;
-                }
-            }
-        }
-        return delegateY;
+        return 0;
     }
 
-    // 计算ListView中某行代理项的最大高度
+    // 计算ListView中某行代理项的最大高度(当前行若有 textarea 则增高,使边框包住内容)
     function getDelegateHeight(index, gridColumns, groupIndex) {
+        var columns = gridColumns === 0 ? 1 : gridColumns;
         var delegateHeight = control.delegateHeight;
-        if (index-gridColumns >= 0 && (groupIndex%(gridColumns===0?1:gridColumns))===0) {
-            for (var i = index-gridColumns; i < index; i++) {
-                var itemType = WorkNodeFormModel.getType(i);
-
-                if (itemType === "textarea") {
-                    delegateHeight = control.textAreaHeight;
+                if ((groupIndex % columns) === 0) {
+            for (var i = index; i < index + columns && i < WorkNodeFormModel.rowCount(); i++) {
+                if (WorkNodeFormModel.getType(i) === "textarea") {
+                    delegateHeight = Math.max(delegateHeight, control.textAreaHeight + 100);
+                    break;
                 }
             }
         }

+ 62 - 0
src/qml/components/JobTicketCard.qml

@@ -31,6 +31,9 @@ Rectangle {
     property string currentJobTicketName: "共锁取锁"
 
     property color cardTextColor: "white"
+    property var timeoutDialog: null
+    property int timeoutCountdown: 10
+    property var timeoutRefreshCallback: null
 
     function showAlertDialog(msgTypeName, msgValue, msgIconChar, callbackFunc) {
         var component = Qt.createComponent("AlertDialog.qml");
@@ -54,6 +57,36 @@ Rectangle {
         appAlertDialog.sourceComponent = null;
         componentObj.destroy();
     }
+    function closeTimeoutDialog() {
+        timeoutAutoCloseTimer.stop();
+        if (timeoutDialog !== null) {
+            timeoutDialog.visible = false;
+            timeoutDialog.destroy();
+            timeoutDialog = null;
+        }
+        timeoutRefreshCallback = null;
+    }
+    function showTimeoutDialog(refreshCallback) {
+        closeTimeoutDialog();
+        timeoutCountdown = 10;
+        timeoutRefreshCallback = refreshCallback || null;
+        var component = Qt.createComponent("AlertDialog.qml");
+        if (component.status === Component.Ready) {
+            timeoutDialog = component.createObject(appAlertDialog, {
+                messageTypeName: "",
+                messageValue: "处理超时,请稍后重试。",
+                messageIconCharacter: "\uf071",
+                confirmBtnText: "确认(10)"
+            });
+            timeoutDialog.parent = appAlertDialog;
+            timeoutDialog.confirm.connect(function () {
+                if (typeof timeoutRefreshCallback === "function")
+                    timeoutRefreshCallback();
+                closeTimeoutDialog();
+            });
+            timeoutAutoCloseTimer.restart();
+        }
+    }
 
     HttpGetWorkNodeDetail {
         id: httpGetWorkNodeDetail
@@ -68,6 +101,18 @@ Rectangle {
 
             __loadingDialog.showLoading = false;
 
+            if (state === 408) {
+                showTimeoutDialog(function () {
+                    __loadingDialog.showLoading = true;
+                    __loadingDialog.loadingText = "正在查询表单详情...";
+                    httpGetWorkNodeDetail.nodeId = control.jobNodeId;
+                    httpGetWorkNodeDetail.signalGetRequestData.connect(httpClientThread.slotGetRequestData);
+                    httpClientThread.signalResponseGetWorkNodeDetail.connect(httpGetWorkNodeDetail.slotHttpResponseGetWorkNodeDetail);
+                    httpClientThread.signalResponseGetFormById.connect(httpGetWorkNodeDetail.slotHttpResponseGetFormById);
+                    httpGetWorkNodeDetail.start();
+                });
+                return;
+            }
             var isShowNegativeBtn = false;
             var isShowOpsitiveBtn = false;
             var textNegativeBtnStr = "";
@@ -289,6 +334,23 @@ Rectangle {
         cancelMouseArea: true
     }
 
+    Timer {
+        id: timeoutAutoCloseTimer
+        interval: 1000
+        repeat: true
+        running: false
+        onTriggered: {
+            timeoutCountdown--;
+            if (timeoutDialog !== null)
+                timeoutDialog.confirmBtnText = "确认(" + Math.max(0, timeoutCountdown) + ")";
+            if (timeoutCountdown <= 0) {
+                if (typeof timeoutRefreshCallback === "function")
+                    timeoutRefreshCallback();
+                closeTimeoutDialog();
+            }
+        }
+    }
+
     LoadingDialog {
         id: __loadingDialog
         loadingText: ""

+ 21 - 0
src/qml/components/LoadingDialog.qml

@@ -1,5 +1,6 @@
 import QtQuick 2.12
 import QtQuick.Layouts 1.12
+import QtQuick.Window 2.12
 
 Rectangle {
     id: control
@@ -12,6 +13,26 @@ Rectangle {
 
     visible: showLoading
     color: "transparent"
+    z: 99997
+
+    // 显示时挂到窗口 contentItem,使 loading 在整个页面居中
+    property Item _windowContentItem: null
+    onShowLoadingChanged: {
+        if (showLoading && typeof Window !== "undefined" && Window.window && Window.window.contentItem) {
+            if (!_windowContentItem) {
+                _windowContentItem = Window.window.contentItem;
+                control.parent = _windowContentItem;
+                control.anchors.fill = _windowContentItem;
+            }
+        }
+    }
+    Component.onCompleted: {
+        if (showLoading && typeof Window !== "undefined" && Window.window && Window.window.contentItem) {
+            _windowContentItem = Window.window.contentItem;
+            control.parent = _windowContentItem;
+            control.anchors.fill = _windowContentItem;
+        }
+    }
 
     MBlurCard {
         id: __blurCard

+ 5 - 9
src/qml/components/MInput.qml

@@ -28,7 +28,7 @@ Item {
     property string showPasswordSymbol: "👁"
     property string hidePasswordSymbol: "🙈"
     property color textColor: "white"
-    property color bgBorderColor: "#40A9FF"
+    property color bgBorderColor: "#355a80"
 
     // === 状态属性 ===
     property bool enabled: true
@@ -50,15 +50,11 @@ Item {
         id: background
         anchors.fill: parent
         radius: control.radius
-        // 无背景时:选中用主题高亮色,未选中用次级色;有背景时沿用主题边框色
-        // border.color: control.backgroundVisible
-        //                ? theme.getBorderColor(textField.activeFocus)
-        //                : (textField.activeFocus ? theme.focusColor : "#40A9FF")
-        border.color: control.bgBorderColor
-        border.width: 1
-        // color: control.backgroundVisible ? theme.secondaryColor : "transparent"
-        color: "transparent"
+        border.color: textField.activeFocus ? "#1890FF" : control.bgBorderColor
+        border.width: textField.activeFocus ? 2 : 1
+        color: "#0d1a28"
         opacity: control.enabled ? 1.0 : 0.6
+        Behavior on border.color { ColorAnimation { duration: 150 } }
     }
 
     // === 内容布局 ===

+ 51 - 38
src/qml/components/MRadioButton.qml

@@ -22,22 +22,26 @@ Rectangle {
 
     // === 样式属性 ===
     property bool backgroundVisible: true
-    property real radius: 10
-    property int fontSize: 16
+    property real radius: 12
+    property int fontSize: 15
     property color buttonColor: "transparent"
     property color hoverColor: Qt.darker(buttonColor, 1.2)
     property color textColor: "white"
-    property color checkmarkColor: "white"
-    property real pressedScale: 0.96
+    property color checkmarkColor: "#5a8fc4"
+    property color borderColor: "#355a80"
+    property color selectedBgColor: "#1a3d2e"   // 选中项背景(偏绿与绿色勾一致)
+    property color selectedBorderColor: "#4CAF50"
+    property color checkmarkGreen: "#4CAF50"   // 选中时的绿色勾
+    property real pressedScale: 0.98
     property bool shadowEnabled: true
     property color shadowColor: theme.shadowColor
 
-    // 布局尺寸
-    property int verticalPadding: 6
-    property int boxSize: 14
-    property int spacingBetweenBoxAndText: 6
-    property int verticalSpacingBetweenButtons: 6
-    property int buttonHeight: 24
+    // 布局尺寸(按钮形式:大点击区,左侧留勾图标位)
+    property int verticalPadding: 12
+    property int iconSize: 24
+    property int spacingBetweenIconAndText: 10
+    property int verticalSpacingBetweenButtons: 10
+    property int buttonHeight: 48
 
     // === 隐藏文本用于测量最大宽度 ===
     Text {
@@ -59,15 +63,15 @@ Rectangle {
     Component.onCompleted: updateMaxTextWidth()
     onModelChanged: updateMaxTextWidth()
 
-    // === 尺寸计算 ===
-    implicitWidth: model.length > 0 ? model.length * (buttonHeight + verticalSpacingBetweenButtons) - verticalSpacingBetweenButtons + 20 : 0
-    implicitHeight: buttonHeight + 20
+    // === 尺寸计算(大点击区) ===
+    implicitWidth: model.length > 0 ? model.length * (buttonHeight + verticalSpacingBetweenButtons) - verticalSpacingBetweenButtons + 32 : 0
+    implicitHeight: buttonHeight + 24
 
     width: implicitWidth
     height: implicitHeight
     color: "transparent"
 
-    // === 背景 ===
+    // === 背景(仅填充,不画外框;选项各自保留边框) ===
     Rectangle {
         id: background
         anchors.fill: parent
@@ -75,7 +79,7 @@ Rectangle {
         radius: control.radius
         color: control.buttonColor
         visible: control.backgroundVisible
-        border.color: "#40A9FF"
+        border.width: 0
 
         layer.enabled: control.shadowEnabled && control.backgroundVisible
 //        layer.effect: MultiEffect {
@@ -105,21 +109,25 @@ Rectangle {
 
         delegate: Rectangle {
             id: btn
-            implicitWidth: verticalPadding * 2 + boxSize + spacingBetweenBoxAndText + label.implicitWidth + 10
+            implicitWidth: verticalPadding * 2 + iconSize + spacingBetweenIconAndText + label.implicitWidth + 24
             height: buttonHeight
-            radius: control.radius * 0.5
+            radius: 8
+            border.width: checked ? 2.5 : 1.5
+            border.color: checked ? (control.selectedBorderColor || theme.focusColor) : (hovered ? control.checkmarkColor : control.borderColor)
 
             // === 状态属性 ===
             property bool hovered: false
             property bool checked: control.selectedIndices.indexOf(index) !== -1
 
-            // 背景显隐控制颜色
+            // 工业感:选中整行有背景+边框,未选中时 hover 有轻微背景
             color: control.backgroundVisible
-                ? (hovered ? control.hoverColor : control.buttonColor)
+                ? (checked ? control.selectedBgColor : (hovered ? control.hoverColor : control.buttonColor))
                 : "transparent"
-            opacity: mouseArea.pressed ? 0.85 : 1.0
+            opacity: mouseArea.pressed ? 0.9 : 1.0
 
             Behavior on color { ColorAnimation { duration: 150 } }
+            Behavior on border.color { ColorAnimation { duration: 150 } }
+            Behavior on border.width { NumberAnimation { duration: 120 } }
             Behavior on opacity { NumberAnimation { duration: 100 } }
 
             // === 缩放动画 ===
@@ -134,32 +142,37 @@ Rectangle {
                 SpringAnimation { target: scale; property: "yScale"; to: 1.0; spring: 2.5; damping: 0.25 }
             }
 
-            // === 按钮内容布局 ===
+            // === 按钮内容布局:选中=变色+绿色勾,无圆形单选图标 ===
             RowLayout {
                 anchors.fill: parent
                 anchors.leftMargin: verticalPadding
                 anchors.rightMargin: verticalPadding
-                spacing: spacingBetweenBoxAndText
+                spacing: spacingBetweenIconAndText
                 Layout.alignment: Qt.AlignVCenter
 
-                // === 复选框圆圈 ===
+                // === 方框 + 勾在方框内(选中时显示绿色勾) ===
                 Rectangle {
-                    id: box
-                    width: boxSize
-                    height: boxSize
-                    radius: boxSize / 2
-                    border.color: control.checkmarkColor
-                    border.width: 2
-                    color: "transparent"
-                    Behavior on color { ColorAnimation { duration: 150 } }
-
-                    Rectangle {
+                    id: checkBox
+                    width: iconSize
+                    height: iconSize
+                    radius: 4
+                    border.width: checked ? 2 : 1
+                    border.color: checked ? control.checkmarkGreen : control.checkmarkColor
+                    color: checked ? "#404CAF50" : "transparent"
+                    Layout.alignment: Qt.AlignVCenter
+
+                    Behavior on border.color { ColorAnimation { duration: 120 } }
+                    Behavior on color { ColorAnimation { duration: 120 } }
+
+                    Text {
                         anchors.centerIn: parent
-                        width: boxSize * 0.5
-                        height: boxSize * 0.5
-                        radius: width / 2
-                        color: checked ? theme.focusColor : "transparent"
-                        Behavior on color { ColorAnimation { duration: 150 } }
+                        text: "\uf00c"
+                        color: control.checkmarkGreen
+                        font.pixelSize: iconSize - 6
+                        font.family: iconFont.name
+                        visible: checked
+                        opacity: checked ? 1 : 0
+                        Behavior on opacity { NumberAnimation { duration: 120 } }
                     }
                 }
 

+ 3 - 3
src/qml/components/MTextArea.qml

@@ -58,9 +58,9 @@ Item {
         id: background
         anchors.fill: parent
         radius: 12
-        color: "transparent"
+        color: "#0d1a28"
         visible: control.backgroundVisible
-        border.color: textArea.activeFocus ? "#1890FF" : "#40A9FF"
+        border.color: textArea.activeFocus ? "#1890FF" : "#355a80"
         border.width: textArea.activeFocus ? 2 : 1
         
         Behavior on border.color {
@@ -92,7 +92,7 @@ Item {
                 wrapMode: Text.Wrap
                 font.pixelSize: control.fontSize
                 placeholderText: "请输入内容"
-                placeholderTextColor: "#606060"
+                placeholderTextColor: "#708090"
                 background: null
                 color: control.textColor
                 leftPadding: 4

+ 93 - 9
src/qml/main.qml

@@ -52,6 +52,12 @@ ApplicationWindow {
     // ============ 导航栏刷新作业列表相关属性 ============
     property bool navBarRefreshJobTickets: false        // 从导航栏刷新作业列表标志
 
+    // ============ API 超时弹窗(408)与 loading ============
+    property var homeTimeoutDialog: null
+    property int homeTimeoutCountdown: 10
+    property var homeTimeoutRefreshCallback: null
+    property bool getUserInfoLoading: false             // 获取用户信息 loading
+
     // 首页刷卡登录:关闭错误提示对话框
     function closeHomeCardLoginErrorDialog() {
         console.log("[main.qml] 关闭首页刷卡登录错误提示");
@@ -63,6 +69,40 @@ ApplicationWindow {
         homeCardLoginErrorAutoCloseTimer.stop();
     }
 
+    // 关闭 API 超时弹窗并执行刷新回调
+    function closeHomeTimeoutDialog() {
+        homeTimeoutAutoCloseTimer.stop();
+        if (homeTimeoutDialog !== null) {
+            homeTimeoutDialog.visible = false;
+            homeTimeoutDialog.destroy();
+            homeTimeoutDialog = null;
+        }
+        homeTimeoutRefreshCallback = null;
+    }
+
+    // 显示 API 超时弹窗:处理超时,请稍后重试。确认(10) 倒计时,关闭后执行 refreshCallback
+    function showHomeTimeoutDialog(refreshCallback) {
+        closeHomeTimeoutDialog();
+        homeTimeoutCountdown = 10;
+        homeTimeoutRefreshCallback = refreshCallback || null;
+        var component = Qt.createComponent("components/AlertDialog.qml");
+        if (component.status === Component.Ready) {
+            homeTimeoutDialog = component.createObject(appAlertDialog, {
+                messageTypeName: "",
+                messageValue: "处理超时,请稍后重试。",
+                messageIconCharacter: "\uf071",
+                confirmBtnText: "确认(10)"
+            });
+            homeTimeoutDialog.parent = appAlertDialog;
+            homeTimeoutDialog.confirm.connect(function () {
+                if (typeof homeTimeoutRefreshCallback === "function")
+                    homeTimeoutRefreshCallback();
+                closeHomeTimeoutDialog();
+            });
+            homeTimeoutAutoCloseTimer.restart();
+        }
+    }
+
     // 首页刷卡登录:显示错误提示对话框
     function showHomeCardLoginErrorDialog(errorMsg) {
         console.log("[main.qml] 显示首页刷卡登录错误提示:", errorMsg);
@@ -174,15 +214,12 @@ ApplicationWindow {
     Connections {
         target: homeHttpCardLogin
         function onSignalLoginReturnStat(stat, data) {
-            console.log("[main.qml] 首页刷卡登录响应,stat:", stat, ",data:", data);
             homeCardLoginLoading = false;
-            console.log("[main.qml] 隐藏loading");
-
             if (stat === 0) {
-                console.log("[main.qml] 首页刷卡登录成功!");
                 homeCardLoginSuccess = true;
+            } else if (stat === 408) {
+                showHomeTimeoutDialog(function () { });
             } else {
-                console.log("[main.qml] 首页刷卡登录失败,显示错误提示");
                 showHomeCardLoginErrorDialog(data);
             }
         }
@@ -225,8 +262,14 @@ ApplicationWindow {
     Connections {
         target: homeHttpGetJobTickets
         function onSignalJobTicketsReturnStat(stat, msg) {
-            console.log("[main.qml] homeHttpGetJobTickets 返回: stat=" + stat + ", msg=" + msg + ", navBarRefresh=" + navBarRefreshJobTickets);
-            
+            // 408 超时:显示超时弹窗,确认后刷新(重新请求作业票)
+            if (stat === 408) {
+                showHomeTimeoutDialog(function () {
+                    connectHomeGetJobTicketsSignals();
+                    homeHttpGetJobTickets.start();
+                });
+                return;
+            }
             // 从导航栏刷新的情况
             if (navBarRefreshJobTickets) {
                 navBarRefreshJobTickets = false;
@@ -345,9 +388,24 @@ ApplicationWindow {
         }
     }
 
+    Connections {
+        target: InteractiveData
+        function onRequestTimedOut() {
+            showHomeTimeoutDialog(function () { });
+        }
+    }
+
     Connections {
         target: httpGetUserInfo
         function onSignalUserInfoReturnStat(stat, msg) {
+            getUserInfoLoading = false;
+            if (stat === 408) {
+                showHomeTimeoutDialog(function () {
+                    getUserInfoLoading = true;
+                    httpGetUserInfo.start();
+                });
+                return;
+            }
             if (stat !== 0) {
                 showAlertDialog("提示", msg, "\uf071", negativeAlertDialogCallback);
             } else {
@@ -464,6 +522,7 @@ ApplicationWindow {
                 iconColor: "#40a9ff"
 
                 onClicked: {
+                    getUserInfoLoading = true;
                     httpGetUserInfo.start();
                 }
             }
@@ -636,6 +695,7 @@ ApplicationWindow {
                 }
             }
         } else {
+            InteractiveData.clearToken();
             stackView.clear();
             stackView.push(adPage.createObject(appWindow, {adImageSource: advertisementImageModel[0]}));
             homeMouseArea.visible = true;
@@ -758,16 +818,40 @@ ApplicationWindow {
         running: false
         repeat: false
         onTriggered: {
-            console.log("[main.qml] 3秒超时,自动关闭错误提示");
             closeHomeCardLoginErrorDialog();
         }
     }
 
-    // 首页刷卡登录Loading组件
+    // API 超时弹窗 10 秒倒计时定时器
+    Timer {
+        id: homeTimeoutAutoCloseTimer
+        interval: 1000
+        repeat: true
+        running: false
+        onTriggered: {
+            homeTimeoutCountdown--;
+            if (homeTimeoutDialog !== null)
+                homeTimeoutDialog.confirmBtnText = "确认(" + Math.max(0, homeTimeoutCountdown) + ")";
+            if (homeTimeoutCountdown <= 0) {
+                if (typeof homeTimeoutRefreshCallback === "function")
+                    homeTimeoutRefreshCallback();
+                closeHomeTimeoutDialog();
+            }
+        }
+    }
+
+    // 首页刷卡登录 Loading
     LoadingDialog {
         id: homeCardLoginLoadingDialog
         loadingText: "登录中,请稍后..."
         showLoading: homeCardLoginLoading
     }
+
+    // 获取用户信息 Loading
+    LoadingDialog {
+        id: getUserInfoLoadingDialog
+        loadingText: "处理中..."
+        showLoading: getUserInfoLoading
+    }
     // ============================================
 }

+ 14 - 3
src/src.pro

@@ -6,9 +6,16 @@ CONFIG -= app_bundle
 QMAKE_CXXFLAGS += -std=c++11
 
 QT += core gui quick serialbus bluetooth dbus network
-QT += quickcontrols2 virtualkeyboard
+QT += quickcontrols2
 QT += multimedia multimediawidgets
 
+# OpenCV 人脸检测(本机 Ubuntu 开发:若已安装 opencv4 则启用)
+unix: packagesExist(opencv4) {
+    CONFIG += link_pkgconfig
+    PKGCONFIG += opencv4
+    DEFINES += INTERACTIVEFACE_OPENCV
+}
+
 BASE_DESTDIR = $$PWD/../build/bin
 CONFIG(debug, debug|release) {
     DESTDIR_COMMON = $${BASE_DESTDIR}/Debug
@@ -44,7 +51,8 @@ HEADERS += \
     httpclient/HttpGetUserInfo.h \
     httpclient/UserInfoModel.h \
     httpclient/HttpUpdateUserInfo.h \
-    httpclient/HttpUpdateUserPassword.h
+    httpclient/HttpUpdateUserPassword.h \
+    httpclient/HttpFaceLogin.h
 
 SOURCES += \
     httpclient/HttpClient.cpp \
@@ -58,7 +66,8 @@ SOURCES += \
     httpclient/HttpGetUserInfo.cpp \
     httpclient/UserInfoModel.cpp \
     httpclient/HttpUpdateUserInfo.cpp \
-    httpclient/HttpUpdateUserPassword.cpp
+    httpclient/HttpUpdateUserPassword.cpp \
+    httpclient/HttpFaceLogin.cpp
 
 # -------------------- interactive 目录 --------------------
 HEADERS += \
@@ -133,3 +142,5 @@ qmldir_install.path = $${target.path}/qml
 INSTALLS += qmldir_install
 
 DISTFILES += qml/qmldirs
+LIBS += -lc# 修复pthread线程库链接(原报错有pthread相关符号,显式链接)
+LIBS += -lpthread

+ 3 - 0
src/usr/LotoQmlPlugin.cpp

@@ -11,6 +11,7 @@
 #include "../httpclient/HttpUpdateNodeApproval.h"
 #include "../httpclient/HttpUpdateUserInfo.h"
 #include "../httpclient/HttpUpdateUserPassword.h"
+#include "../httpclient/HttpFaceLogin.h"
 #include "../httpclient/JobTicketsModel.h"
 #include "../httpclient/UserInfoModel.h"
 #include "../httpclient/WorkNodeFormModel.h"
@@ -74,6 +75,7 @@ void LotoQmlTypes::registerTypes()
     qmlRegisterType<HttpUpdateNodeApproval>(uri, majorVersion, minorVersion, "HttpUpdateNodeApproval");
     qmlRegisterType<HttpUpdateUserInfo>(uri, majorVersion, minorVersion, "HttpUpdateUserInfo");
     qmlRegisterType<HttpUpdateUserPassword>(uri, majorVersion, minorVersion, "HttpUpdateUserPassword");
+    qmlRegisterType<HttpFaceLogin>(uri, majorVersion, minorVersion, "HttpFaceLogin");
 
     qmlRegisterType<CANClient>(uri, majorVersion, minorVersion, "CANClient");
     qRegisterMetaType<CANKeyBaseStatus>("CANKeyBaseStatus");
@@ -90,6 +92,7 @@ void LotoQmlTypes::registerTypes()
     qmlRegisterSingletonType<JobTicketModel>(uri, majorVersion, minorVersion, "JobTicketModel", &JobTicketModel::create);
     qmlRegisterSingletonType<UserInfoModel>(uri, majorVersion, minorVersion, "UserInfoModel", &UserInfoModel::create);
     qmlRegisterSingletonType<WorkNodeFormModel>(uri, majorVersion, minorVersion, "WorkNodeFormModel", &WorkNodeFormModel::create);
+    qmlRegisterSingletonType<InteractiveData>(uri, majorVersion, minorVersion, "InteractiveData", &InteractiveData::create);
 
     qmlRegisterSingletonType<InteractiveCAN>(uri, majorVersion, minorVersion, "InteractiveCAN", &InteractiveCAN::create);
     qmlRegisterSingletonType<InteractiveFace>(uri, majorVersion, minorVersion, "InteractiveFace", &InteractiveFace::create);

+ 202 - 204
src/usr/config.h

@@ -1,204 +1,202 @@
-#ifndef CONFIG_H_
-#define CONFIG_H_
-#include <QString>
-#include <QObject>
-#include <QHash>
-#include <QVariantMap>
-#include <QtQml/qqml.h>
-
-class config : public QObject
-{
-    Q_OBJECT
-    QML_SINGLETON
-    QML_NAMED_ELEMENT(Config)
-
-    Q_PROPERTY(QString suserId READ suserId WRITE setSuserId)
-    Q_PROPERTY(QString sdevUuid READ sdevUuid WRITE setSdevUuid NOTIFY sdevUuidChanged)
-    Q_PROPERTY(QString shttpHost READ shttpHost WRITE setShttpHost NOTIFY shttpHostChanged)
-    Q_PROPERTY(int sloginTimeout READ sloginTimeout WRITE setSloginTimeout NOTIFY sloginTimeoutChanged)
-    Q_PROPERTY(QString susername READ susername)
-    Q_PROPERTY(QStringList sserialPortList READ sserialPortList)
-
-    Q_PROPERTY(int scurrentPlanId READ scurrentPlanId WRITE setScurrentPlanId)
-private:
-    config();
-
-    Q_DISABLE_COPY(config);
-public:
-    static config* pInstance;
-    static config* instance();
-    static config* create(QQmlEngine*, QJSEngine*);
-
-    void configWrite(void);
-    bool configRead(void);
-    QString getDeviceUUID(void);
-
-    enum xmlType {
-        XML_NULL = 0,
-        XML_CONFIG,
-        XML_DEVICE,
-        XML_SERVER,
-        XML_PARAM,
-    };
-    Q_ENUM(xmlType)
-#ifdef Q_OS_WIN
-    QString configfile = "config.xml";
-#else
-    QString configpath = "/storage/emulated/0/Android/data/com.cabinet/";
-    QString configfile = configpath + "config.xml";
-#endif
-    bool devInit = false;
-    QString devUuid = "CABINET_016";
-    bool logfileStat = false;
-    bool readBee = false;
-    int heartTime = 60;
-    int lockCloseTimeout = 10 * 60;
-    bool rfidInit = false;
-    bool lockInit = false;
-
-    int currentPlanId = 0;
-
-    const char* lotoQmlModuleName = "Loto";
-    int lotoQmlModuleMajorVersion = 1;
-    int lotoQmlModuleMinorVersion = 0;
-
-#ifdef Q_OS_WIN
-    QString rfidPort = "COM1";
-    QString lockPort = "COM2";
-#else
-    QString rfidPort = "/dev/ttyUSB1";
-    QString lockPort = "/dev/ttyUSB2";
-#endif
-    QStringList serialPortList;
-
-    QString m_systemMACAddr;
-
-    QString httpHost = "120.27.232.27:9292";
-//    QString httpHost = "192.168.0.10:48080";
-    QString tenant_id = "149";
-
-    QString userInfoUrl = "/admin-api/system/user/profile/get";                                             // 用户中心
-    QString updateUserInfoUrl = "/admin-api/system/user/profile/update";                                    // 更新用户信息
-    QString updatePasswordUrl = "/admin-api/system/user/profile/update-password";                           // 更新用户密码
-    QString userInfoByIdUrl = "/admin-api/system/user/get";                                                 // 根据用户ID获取用户头像等信息
-
-    QString usernameLogin_url = "/admin-api/system/auth/login";                                             //用户名登陆接口
-    QString cardLogin_url = "/admin-api/iscs/job-card/loginByCard";                                          //卡号登陆接口
-    QString logout_url = "/logout";                                                                         //登出接口
-
-    QString getInfo_url = "/getInfo";                                                                       // 获取当前用户信息
-    QString getSysUserCharacteristicPage_url = "/system/user/characteristic/getSysUserCharacteristicPage";  //获取当前用户特征
-    QString insertUserFace_url = "/system/user/characteristic/insertUserFace";   // 新增面部信息
-
-    QString loginByFace_url = "/loginByArcFace";   // 人脸登录
-
-    QString jobTicketsUrl = "/admin-api/iscs/workflow-work/getMyWorkPage";                                    // 获取我的作业页面
-    QString workNodeDetail = "/admin-api/iscs/workflow-work/getMyWorkNodeDetail";                               // 表单详情页面
-    QString workNodeDetailForm = "/admin-api/bpm/form/get";                                                   // 表单详情页中的组件信息
-    QString updateNodeApprovalUrl = "/admin-api/iscs/workflow-work/updateNodeApproval";                        // 表单提交
-
-    QString keyMACByNFC = "/admin-api/iscs/key/selectKeyByNfc";                                                 // 根据NFC信息获取钥匙MAC地址
-
-    QString isolationPointById = "/admin-api/iscs/isolation-point/selectIsolationPointById";                   // 根据隔离点ID获取隔离点具体信息
-
-    QString uploadJobTicketUrl = "/admin-api/isc/work-handle/insertWorkTicket";
-    QString updateWorkTicketUrl = "/admin-api/isc/work-handle/updateWorkTicket";
-    QString uploadPositionInfoUrl = "/admin-api/isc/work-handle/updatePointLock";
-
-    QString updateColockUrl = "/admin-api/isc/work-handle/updateUserLock";
-    QString updateUncolockUrl = "/admin-api/isc/work-handle/updateUserUnlock";
-    QString updatePointUnlock = "/admin-api/isc/work-handle/updatePointUnlock";
-    QString updateBackLock = "/admin-api/isc/work-handle/updateBackLock";
-    QString updateKeyBack = "/admin-api/isc/work-handle/updateKeyBack";
-
-    QString workTicketByNodeId = "/admin-api/isc/work-handle/getWorkTicketByNodeId";
-
-    QString ip = "0.0.0.0";
-    QString mask = "255.255.255.0";
-    QString dns = "0.0.0.0";
-
-    int loginTimeout = 99;
-
-    QString userId;
-    QString username;
-    QString nickName;
-    QString cardNo;
-    QString devId;
-    QString devName;
-
-    QString suserId()
-    {
-        return userId;
-    }
-    void setSuserId(const QString& suserId)
-    {
-        userId = suserId;
-    }
-
-    QString sdevUuid() const {
-        return devUuid;
-    }
-
-    void setSdevUuid(const QString &sdevUuid) {
-        if (sdevUuid == devUuid)
-            return;
-        devUuid = sdevUuid;
-        emit sdevUuidChanged();
-    }
-
-    QString shttpHost() const {
-        return httpHost;
-    }
-
-    void setShttpHost(const QString &shttpHost) {
-        if (shttpHost == httpHost)
-            return;
-        httpHost = shttpHost;
-        emit shttpHostChanged();
-    }
-
-    int sloginTimeout() const {
-        return loginTimeout;
-    }
-
-    void setSloginTimeout(const int &sloginTimeout) {
-        if (sloginTimeout == loginTimeout)
-            return;
-        loginTimeout = sloginTimeout;
-        emit sloginTimeoutChanged();
-    }
-
-    QString susername() const {
-        return username;
-    }
-
-    QStringList sserialPortList() const {
-        return serialPortList;
-    }
-
-    int scurrentPlanId()
-    {
-        return currentPlanId;
-    }
-
-    Q_INVOKABLE void setScurrentPlanId(int scurrentPlanId)
-    {
-        currentPlanId = scurrentPlanId;
-    }
-private:
-    void getDeviceValue(QString key, QString value);
-    void getParamValue(QString key, QString value);
-    void getServerValue(QString key, QString value);
-
-signals:
-    void sdevUuidChanged();
-    void shttpHostChanged();
-    void shttpPortChanged();
-    void sloginTimeoutChanged();
-};
-
-inline config* Config() {
-    return config::instance();
-}
-
-#endif // CONFIG_H
+#ifndef CONFIG_H_
+#define CONFIG_H_
+#include <QString>
+#include <QObject>
+#include <QHash>
+#include <QVariantMap>
+#include <QtQml/qqml.h>
+
+class config : public QObject
+{
+    Q_OBJECT
+    QML_SINGLETON
+    QML_NAMED_ELEMENT(Config)
+
+    Q_PROPERTY(QString suserId READ suserId WRITE setSuserId)
+    Q_PROPERTY(QString sdevUuid READ sdevUuid WRITE setSdevUuid NOTIFY sdevUuidChanged)
+    Q_PROPERTY(QString shttpHost READ shttpHost WRITE setShttpHost NOTIFY shttpHostChanged)
+    Q_PROPERTY(int sloginTimeout READ sloginTimeout WRITE setSloginTimeout NOTIFY sloginTimeoutChanged)
+    Q_PROPERTY(QString susername READ susername)
+    Q_PROPERTY(QStringList sserialPortList READ sserialPortList)
+
+    Q_PROPERTY(int scurrentPlanId READ scurrentPlanId WRITE setScurrentPlanId)
+private:
+    config();
+
+    Q_DISABLE_COPY(config);
+public:
+    static config* pInstance;
+    static config* instance();
+    static config* create(QQmlEngine*, QJSEngine*);
+
+    void configWrite(void);
+    bool configRead(void);
+    QString getDeviceUUID(void);
+
+    enum xmlType {
+        XML_NULL = 0,
+        XML_CONFIG,
+        XML_DEVICE,
+        XML_SERVER,
+        XML_PARAM,
+    };
+    Q_ENUM(xmlType)
+#ifdef Q_OS_WIN
+    QString configfile = "config.xml";
+#else
+    QString configpath = "/storage/emulated/0/Android/data/com.cabinet/";
+    QString configfile = configpath + "config.xml";
+#endif
+    bool devInit = false;
+    QString devUuid = "CABINET_016";
+    bool logfileStat = false;
+    bool readBee = false;
+    int heartTime = 60;
+    int lockCloseTimeout = 10 * 60;
+    bool rfidInit = false;
+    bool lockInit = false;
+
+    int currentPlanId = 0;
+
+    const char* lotoQmlModuleName = "Loto";
+    int lotoQmlModuleMajorVersion = 1;
+    int lotoQmlModuleMinorVersion = 0;
+
+#ifdef Q_OS_WIN
+    QString rfidPort = "COM1";
+    QString lockPort = "COM2";
+#else
+    QString rfidPort = "/dev/ttyUSB1";
+    QString lockPort = "/dev/ttyUSB2";
+#endif
+    QStringList serialPortList;
+
+    QString m_systemMACAddr;
+
+    QString httpHost = "120.27.232.27:9292";
+    //QString httpHost = "192.168.0.10:48080";
+    QString tenant_id = "149";
+
+    QString userInfoUrl = "/admin-api/system/user/profile/get";                                             // 用户中心
+    QString updateUserInfoUrl = "/admin-api/system/user/profile/update";                                    // 更新用户信息
+    QString updatePasswordUrl = "/admin-api/system/user/profile/update-password";                           // 更新用户密码
+    QString userInfoByIdUrl = "/admin-api/system/user/get";                                                 // 根据用户ID获取用户头像等信息
+
+    QString usernameLogin_url = "/admin-api/system/auth/login";                                             //用户名登陆接口
+    QString cardLogin_url = "/admin-api/iscs/job-card/loginByCard";                                          //卡号登陆接口
+    QString logout_url = "/logout";                                                                         //登出接口
+
+    QString getInfo_url = "/getInfo";                                                                       // 获取当前用户信息
+    QString getSysUserCharacteristicPage_url = "/system/user/characteristic/getSysUserCharacteristicPage";  //获取当前用户特征
+    QString insertUserFace_url = "/system/user/characteristic/insertUserFace";   // 新增面部信息
+
+    QString loginByFace_url = "/admin-api/system/auth/loginByArcFace";   // 人脸登录
+
+    QString jobTicketsUrl = "/admin-api/iscs/workflow-work/getMyWorkPage";                                    // 获取我的作业页面
+    QString workNodeDetail = "/admin-api/iscs/workflow-work/getMyWorkNodeDetail";                               // 表单详情页面
+    QString workNodeDetailForm = "/admin-api/bpm/form/get";                                                   // 表单详情页中的组件信息
+    QString updateNodeApprovalUrl = "/admin-api/iscs/workflow-work/updateNodeApproval";                        // 表单提交
+
+    QString keyMACByNFC = "/admin-api/iscs/key/selectKeyByNfc";                                                 // 根据NFC信息获取钥匙MAC地址
+
+    QString isolationPointById = "/admin-api/iscs/isolation-point/selectIsolationPointById";                   // 根据隔离点ID获取隔离点具体信息
+
+    QString uploadJobTicketUrl = "/admin-api/isc/work-handle/insertWorkTicket";
+    QString uploadPositionInfoUrl = "/admin-api/isc/work-handle/updatePointLock";
+
+    QString updateColockUrl = "/admin-api/isc/work-handle/updateUserLock";
+    QString updateUncolockUrl = "/admin-api/isc/work-handle/updateUserUnlock";
+    QString updatePointUnlock = "/admin-api/isc/work-handle/updatePointUnlock";
+    QString updateBackLock = "/admin-api/isc/work-handle/updateBackLock";
+
+    QString workTicketByNodeId = "/admin-api/isc/work-handle/getWorkTicketByNodeId";
+
+    QString ip = "0.0.0.0";
+    QString mask = "255.255.255.0";
+    QString dns = "0.0.0.0";
+
+    int loginTimeout = 99;
+
+    QString userId;
+    QString username;
+    QString nickName;
+    QString cardNo;
+    QString devId;
+    QString devName;
+
+    QString suserId()
+    {
+        return userId;
+    }
+    void setSuserId(const QString& suserId)
+    {
+        userId = suserId;
+    }
+
+    QString sdevUuid() const {
+        return devUuid;
+    }
+
+    void setSdevUuid(const QString &sdevUuid) {
+        if (sdevUuid == devUuid)
+            return;
+        devUuid = sdevUuid;
+        emit sdevUuidChanged();
+    }
+
+    QString shttpHost() const {
+        return httpHost;
+    }
+
+    void setShttpHost(const QString &shttpHost) {
+        if (shttpHost == httpHost)
+            return;
+        httpHost = shttpHost;
+        emit shttpHostChanged();
+    }
+
+    int sloginTimeout() const {
+        return loginTimeout;
+    }
+
+    void setSloginTimeout(const int &sloginTimeout) {
+        if (sloginTimeout == loginTimeout)
+            return;
+        loginTimeout = sloginTimeout;
+        emit sloginTimeoutChanged();
+    }
+
+    QString susername() const {
+        return username;
+    }
+
+    QStringList sserialPortList() const {
+        return serialPortList;
+    }
+
+    int scurrentPlanId()
+    {
+        return currentPlanId;
+    }
+
+    Q_INVOKABLE void setScurrentPlanId(int scurrentPlanId)
+    {
+        currentPlanId = scurrentPlanId;
+    }
+private:
+    void getDeviceValue(QString key, QString value);
+    void getParamValue(QString key, QString value);
+    void getServerValue(QString key, QString value);
+
+signals:
+    void sdevUuidChanged();
+    void shttpHostChanged();
+    void shttpPortChanged();
+    void sloginTimeoutChanged();
+};
+
+inline config* Config() {
+    return config::instance();
+}
+
+#endif // CONFIG_H