1.新增服务端UI界面,显示服务基础信息。2.修改设备显示配置。3.监控页面储能系统显示储能模式,模式设置新增'手动'

This commit is contained in:
lixiaoyuan
2025-09-25 19:20:25 +08:00
parent d7888c2be4
commit 8aba56f47d
39 changed files with 2954 additions and 408 deletions

View File

@@ -1,35 +1,16 @@
#include "MainApp.h"
#include "common/Spdlogger.h"
#include <QtCore/QVariant>
#include <QtWidgets/QApplication>
#include <QtWidgets/QComboBox>
#include <QtWidgets/QGridLayout>
#include <QtWidgets/QLabel>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QWidget>
#include <QPalette>
#include <QGroupBox>
#include <QTableWidget>
#include <QHeaderView>
#include "app/Config.h"
#include "common/Utils.h"
#include "QUI.h"
#include "MyQUI.h"
#include "app/Application.h"
#include "app/AppData.h"
#include "app/Station.h"
#include "protocol/MqttEntity.h"
static const std::string QSS_BTN_MENU =
"QPushButton {background:rgba(50,128,218,200);color:white;border-radius:5px;border:2px solid rgb(10,120,215);font:bold 18px;}"
"QPushButton:hover {background-color:rgb(32,164,128);}"
"QPushButton:pressed {border-width:3px 0 0 3px;background-color:rgb(1,32,54);border-style:inset;}"
"QPushButton:disabled {color:rgb(150,150,150);}";
static const std::string QSS_LINE =
"QLineEdit { background-color: rgb(14, 49, 66); color: #ffffff; border: 1px solid gray; border-radius: 5px; font: bold 13px; }";
MainApp::MainApp()
{
@@ -101,168 +82,8 @@ public:
QGridLayout layout;
};
void PairLine(QWidget* parent, int x, int y, string k, string v)
{
auto key = new QLabel(parent);
key->setText(k.c_str());
key->setGeometry(x, y, 80, 26);
auto value = new QLineEdit(parent);
value->setText(v.c_str());
value->setGeometry(x+80, y, 260, 26);
value->setStyleSheet(QSS_LINE.c_str());
value->setReadOnly(true);
}
class MyWorkspace : public QWidget
{
public:
MyWorkspace(QWidget* parent) : QWidget(parent)
{
this->setObjectName("workspace");
this->setStyleSheet("#workspace { background-color:rgba(100,100,100,50); }");
int x=10, y=10;
{
this->groupSys = UI::GroupBox(this, x, y, 1190, 120, "系统");
auto pw = groupSys.get();
}
{
x = 10, y += 130;
this->groupHttp = UI::GroupBox(this, x, y, 390, 120, "HTTP");
auto pw = groupHttp.get();
PairLine(pw, 20, 20, "服务类型: ", "服务端");
PairLine(pw, 20, 50, "服务端口: ", Utils::toStr(Config::option.http.port));
PairLine(pw, 20, 80, "服务状态: ", "运行");
}
{
x += 400;
this->groupMqtt = UI::GroupBox(this, x, y, 390, 120, "MQTT");
auto pw = groupMqtt.get();
PairLine(pw, 20, 20, "服务类型: ", "客户端");
PairLine(pw, 20, 50, "服务地址: ", Config::option.mqtt.host);
PairLine(pw, 20, 80, "服务状态: ", "---");
}
{
x += 400;
this->groupDB = UI::GroupBox(this, x, y, 390, 120, "数据库");
auto pw = groupDB.get();
PairLine(pw, 20, 20, "数据库名: ", Config::option.database.dbname);
PairLine(pw, 20, 50, "主机地址: ", Config::option.database.host);
PairLine(pw, 20, 80, "用 户 名: ", Config::option.database.user);
const std::string QSS_TABLE = // 表格整体样式
"QTableWidget {"
" background-color: transparent;" // 背景色
" gridline-color: #C0C0C0;" // 网格线颜色
" border: 1px solid gray;" // 边框
" color: white;" // 文字颜色
"}"
// 表头样式
"QHeaderView::section {"
" background-color: #404040;" // 表头背景
" padding: 4px;" // 内边距
" border: 1px solid #505050;" // 边框
" min-height: 25px;" // 最小高度
"}"
// 单元格样式
"QTableWidget::item {"
" padding-left: 5px;"
" border-bottom: 1px solid gray;" // 底部边框
"}"
// 选中状态
"QTableWidget::item:selected {"
" background-color: #B8D6FF;" // 选中背景色
" color: black;" // 选中文字颜色
"}";
table = std::make_shared<QTableWidget>(this);
table->setGeometry(10, y += 130, 1190, 300);
table->setStyleSheet(QSS_TABLE.c_str());
table->horizontalHeader()->setStretchLastSection(true); // 最后一列占满
table->verticalHeader()->setVisible(false); // 不显示垂直表头
table->setEditTriggers(QAbstractItemView::NoEditTriggers); // 单元格不可编辑
table->setSelectionMode(QAbstractItemView::SingleSelection); // 设置为单选模式
table->setSelectionBehavior(QAbstractItemView::SelectRows); // 设置为整行选中
QTableWidgetItem* headerItem;
QStringList headerText_Row, headerText_Col;
headerText_Row << "站ID" << "站名称" << "站编号" << "站状态" << "MQTT状态" << "召测(秒)" << "说明";
// 设置为水平表头
table->setColumnCount(headerText_Row.size());
table->setHorizontalHeaderLabels(headerText_Row);
}
textLog = std::make_shared<QTextEdit>(this);
textLog->setGeometry(10, y += 310, 1190, 280);
textLog->setStyleSheet("background-color: transparent; border: 1px solid gray; font-weight: 400;");
textLog->setReadOnly(true);
{
// 第二个参数是方法函数名称,即调用 QTextEdit的appeng函数
auto qtSink = std::make_shared<spdlog::sinks::qt_sink_mt>(textLog.get(), "append");
spdlog::default_logger()->sinks().push_back(qtSink);
}
{
//qtSink = std::make_shared<QtTextSink>(textLog.get());
//connect(qtSink.get(), &QtTextSink::appendText, textLog.get(), &QTextEdit::append);
}
}
void setTableCell(int row, int col, std::string text, std::string style="")
{
auto item = table->item(row, col);
if (!item)
{
item = new QTableWidgetItem();
table->setItem(row, col, item);
}
item->setText(text.c_str());
if (style == "OK") { item->setForeground(QBrush(Qt::green)); }
else if (style == "ERR") { item->setForeground(QBrush(Qt::red)); }
}
void onTimer()
{
auto& appdata = Application().data();
int rowNo = 0;
int tsNow = Utils::time();
for (auto& item : appdata.mapStation)
{
auto& station = item.second;
if (rowNo >= table->rowCount())
{
table->insertRow(rowNo);
}
bool isOpen = station->status > 0;
bool isConnected = station->mqttCli->isConnected;
setTableCell(rowNo, 0, std::to_string(station->stationId));
setTableCell(rowNo, 0, std::to_string(station->stationId));
setTableCell(rowNo, 1, station->name);
setTableCell(rowNo, 2, station->code);
setTableCell(rowNo, 3, isOpen ? "启用" : "未启用", isOpen ? "OK" : "ERR");
setTableCell(rowNo, 4, isConnected ? "连接成功" : "未连接", isConnected ? "OK" : "ERR");
int tsPolling = station->getPollingTS();
setTableCell(rowNo, 5, tsPolling > 0 ? std::to_string(tsNow - tsPolling) + "/" + std::to_string(Config::option.mqtt.interval) : "--");
rowNo++;
}
}
// 创建自定义sink
std::shared_ptr<QtTextSink> qtSink;
std::shared_ptr<QGroupBox> groupSys;
std::shared_ptr<QGroupBox> groupHttp;
std::shared_ptr<QGroupBox> groupMqtt;
std::shared_ptr<QGroupBox> groupDB;
std::shared_ptr<QTableWidget> table {};
std::shared_ptr<QTextEdit> textLog;
};
void MainApp::setMyLayout()
{
@@ -284,9 +105,9 @@ void MainApp::setMyLayout()
menu->setSizePolicy(sizePolicy);
layout.main->addWidget(menu, 0, 0, 1, 1);
ui.workspace = std::make_shared<MyWorkspace>(this);
ui.workspace->setSizePolicy(sizePolicy);
layout.main->addWidget(ui.workspace.get(), 0, 1, 1, 1);
ui.wigetHome = std::make_shared<QWHome>(this);
ui.wigetHome->setSizePolicy(sizePolicy);
layout.main->addWidget(ui.wigetHome.get(), 0, 1, 1, 1);
// 设置列宽和行高
layout.main->setColumnMinimumWidth(0, 200); // 设置第0列的最小宽度为100像素
@@ -303,5 +124,5 @@ void MainApp::setMyLayout()
void MainApp::onTimer()
{
ui.workspace->onTimer();
ui.wigetHome->onTimer();
}

View File

@@ -1,10 +1,9 @@
#include <QMainWindow>
#include <QLabel>
#include <QPushButton>
#include <QHBoxLayout>
#include <QTableWidget>
#include <QTimer>
#include <QTextEdit>
#include <string>
using namespace std;
@@ -14,73 +13,8 @@ using namespace std;
#include <QTextEdit>
#include <mutex>
class QtTextSink : public QObject, public spdlog::sinks::base_sink<std::mutex>
{
Q_OBJECT
public:
// 构造函数接收一个 QTextEdit*,但用 QPointer 来包装它
explicit QtTextSink(QTextEdit* textEdit, QObject* parent = nullptr)
: QObject(parent), textEdit(textEdit) {
}
#include "widgets/QWHome.h"
signals:
// 定义一个信号,用于在主线程中追加文本
void appendText(const QString& message);
protected:
// 重写 sink_it_ 方法,处理每条日志
void sink_it_(const spdlog::details::log_msg& msg) override {
if (textEdit.isNull()) {
// 如果 QTextEdit 已经被删除,则忽略这条日志
return;
}
spdlog::memory_buf_t formatted;
formatter_->format(msg, formatted);
QString logMessage = QString::fromStdString(fmt::to_string(formatted));
// 发射信号通过Qt的事件循环在主线程中安全地更新UI
emit appendText(logMessage);
}
void flush_() override {}
private:
QPointer<QTextEdit> textEdit; // 关键:使用 QPointer
};
class LabelPair
{
public:
LabelPair(QWidget* parent, int x, int y, int w, int h)
{
title.setParent(parent);
value.setParent(&title);
title.setGeometry(x, y, w, h);
value.setGeometry(80, 0, w-80, h);
title.show();
value.show();
}
void setTitle(std::string text)
{
title.setText(text.c_str());
}
void setValue(std::string text)
{
value.setText(text.c_str());
}
QLabel title;
QLabel value;
};
class MyWorkspace;
class MainApp : public QWidget
{
@@ -97,7 +31,7 @@ private slots:
public:
struct {
std::shared_ptr<LabelPair> weburl {};
std::shared_ptr<MyWorkspace> workspace;
std::shared_ptr<MyWidget> wigetHome;
} ui;
struct {

35
src/qt/MyQUI.cpp Normal file
View File

@@ -0,0 +1,35 @@
#include "MyQUI.h"
static const std::string QSS_GROUP =
"QGroupBox { border: 1px solid gray; margin-top: 8px; border-radius: 5px;}"
"QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; left:10px; margin-left: 0px; padding:0 1px; }";
static const std::string QSS_BTN_MENU =
"QPushButton {background:rgba(50,128,218,200);color:white;border-radius:5px;border:2px solid rgb(10,120,215);font:bold 18px;}"
"QPushButton:hover {background-color:rgb(32,164,128);}"
"QPushButton:pressed {border-width:3px 0 0 3px;background-color:rgb(1,32,54);border-style:inset;}"
"QPushButton:disabled {color:rgb(150,150,150);}";
static const std::string QSS_LINE =
"QLineEdit { background-color: rgb(14, 49, 66); color: #ffffff; border: 1px solid gray; border-radius: 5px; font: bold 13px; }";
std::shared_ptr<QGroupBox> MyQUI::GroupBox(QWidget* parent, int x, int y, int w, int h, std::string title)
{
auto groupBox = std::make_shared<QGroupBox>(title.c_str(), parent);
groupBox->setGeometry(x, y, w, h);
groupBox->setStyleSheet(QSS_GROUP.c_str());
return groupBox;
}
void MyQUI::PairLine(QWidget* parent, int x, int y, string k, string v)
{
auto key = new QLabel(parent);
key->setText(k.c_str());
key->setGeometry(x, y, 80, 26);
auto value = new QLineEdit(parent);
value->setText(v.c_str());
value->setGeometry(x+80, y, 260, 26);
value->setStyleSheet(QSS_LINE.c_str());
value->setReadOnly(true);
}

76
src/qt/MyQUI.h Normal file
View File

@@ -0,0 +1,76 @@
#pragma once
#include <QGroupBox>
#include <QMainWindow>
#include <QtWebEngineWidgets/QWebEngineView>
#include <QSplashScreen>
#include <QLabel>
#include <QPushButton>
#include <QHBoxLayout>
#include <QTextEdit>
#include <QtCore/QVariant>
#include <QtWidgets/QApplication>
#include <QtWidgets/QComboBox>
#include <QtWidgets/QGridLayout>
#include <QtWidgets/QLabel>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QWidget>
#include <QPalette>
#include <QGroupBox>
#include <QTableWidget>
#include <QHeaderView>
#include <string>
using namespace std;
extern const std::string QSS_GROUP;
extern const std::string QSS_BTN_MENU;
extern const std::string QSS_LINE;
class LabelPair
{
public:
LabelPair(QWidget* parent, int x, int y, int w, int h)
{
title.setParent(parent);
value.setParent(&title);
title.setGeometry(x, y, w, h);
value.setGeometry(80, 0, w-80, h);
title.show();
value.show();
}
void setTitle(std::string text)
{
title.setText(text.c_str());
}
void setValue(std::string text)
{
value.setText(text.c_str());
}
QLabel title;
QLabel value;
};
class MyWidget : public QWidget
{
Q_OBJECT
public:
MyWidget(QWidget* parent) : QWidget(parent) {}
virtual void onTimer() {};
};
class MyQUI
{
public:
static std::shared_ptr<QGroupBox> GroupBox(QWidget* parent, int x, int y, int w, int h, std::string title);
static void PairLine(QWidget* parent, int x, int y, string k, string v);
};

View File

@@ -1,14 +0,0 @@
#include "QUI.h"
static const std::string QSS_GROUP =
"QGroupBox { border: 1px solid gray; margin-top: 8px; border-radius: 5px;}"
"QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; left:10px; margin-left: 0px; padding:0 1px; }";
std::shared_ptr<QGroupBox> UI::GroupBox(QWidget* parent, int x, int y, int w, int h, std::string title)
{
auto groupBox = std::make_shared<QGroupBox>(title.c_str(), parent);
groupBox->setGeometry(x, y, w, h);
groupBox->setStyleSheet(QSS_GROUP.c_str());
return groupBox;
}

View File

@@ -1,10 +0,0 @@
#pragma once
#include <QGroupBox>
class UI
{
public:
static std::shared_ptr<QGroupBox> GroupBox(QWidget* parent, int x, int y, int w, int h, std::string title);
};

161
src/qt/widgets/QWHome.cpp Normal file
View File

@@ -0,0 +1,161 @@
#include "QWHome.h"
#include "common/Spdlogger.h"
#include "app/Config.h"
#include "common/Utils.h"
#include "app/Application.h"
#include "app/AppData.h"
#include "app/Station.h"
#include "protocol/MqttEntity.h"
QWHome::QWHome(QWidget* parent) : MyWidget(parent)
{
this->setObjectName("workspace");
this->setStyleSheet("#workspace { background-color:rgba(100,100,100,50); }");
int x = 10, y = 10;
{
this->groupSys = MyQUI::GroupBox(this, x, y, 1190, 120, "系统");
auto pw = groupSys.get();
}
{
x = 10, y += 130;
this->groupHttp = MyQUI::GroupBox(this, x, y, 390, 120, "HTTP");
auto pw = groupHttp.get();
MyQUI::PairLine(pw, 20, 20, "服务类型: ", "服务端");
MyQUI::PairLine(pw, 20, 50, "服务端口: ", Utils::toStr(Config::option.http.port));
MyQUI::PairLine(pw, 20, 80, "服务状态: ", "运行");
}
{
x += 400;
this->groupMqtt = MyQUI::GroupBox(this, x, y, 390, 120, "MQTT");
auto pw = groupMqtt.get();
MyQUI::PairLine(pw, 20, 20, "服务类型: ", "客户端");
MyQUI::PairLine(pw, 20, 50, "服务地址: ", Config::option.mqtt.host);
MyQUI::PairLine(pw, 20, 80, "服务状态: ", "---");
}
{
x += 400;
this->groupDB = MyQUI::GroupBox(this, x, y, 390, 120, "数据库");
auto pw = groupDB.get();
MyQUI::PairLine(pw, 20, 20, "数据库名: ", Config::option.database.dbname);
MyQUI::PairLine(pw, 20, 50, "主机地址: ", Config::option.database.host);
MyQUI::PairLine(pw, 20, 80, "用 户 名: ", Config::option.database.user);
const std::string QSS_TABLE = // 表格整体样式
"QTableWidget {"
" background-color: transparent;" // 背景色
" gridline-color: #C0C0C0;" // 网格线颜色
" border: 1px solid gray;" // 边框
" color: white;" // 文字颜色
"}"
// 表头样式
"QHeaderView::section {"
" background-color: #404040;" // 表头背景
" padding: 4px;" // 内边距
" border: 1px solid #505050;" // 边框
" min-height: 25px;" // 最小高度
"}"
// 单元格样式
"QTableWidget::item {"
" padding-left: 5px;"
" border-bottom: 1px solid gray;" // 底部边框
"}"
// 选中状态
"QTableWidget::item:selected {"
" background-color: #B8D6FF;" // 选中背景色
" color: black;" // 选中文字颜色
"}";
table = std::make_shared<QTableWidget>(this);
table->setGeometry(10, y += 130, 1190, 300);
table->setStyleSheet(QSS_TABLE.c_str());
table->horizontalHeader()->setStretchLastSection(true); // 最后一列占满
table->verticalHeader()->setVisible(false); // 不显示垂直表头
table->setEditTriggers(QAbstractItemView::NoEditTriggers); // 单元格不可编辑
table->setSelectionMode(QAbstractItemView::SingleSelection); // 设置为单选模式
table->setSelectionBehavior(QAbstractItemView::SelectRows); // 设置为整行选中
table->horizontalHeader()->setFixedHeight(50);
table->horizontalHeader()->setDefaultSectionSize(60);
QTableWidgetItem* headerItem;
QStringList headerText_Row, headerText_Col;
headerText_Row << "ID" << "站名" << "编号" << "状态" << "MQTT状态" << "召测(秒)"
<< "日充电\n电量" << "日放电\n电量" << "总充电\n电量" << "总放电\n电量"
<< "日充电\n费用" << "日放电\n费用" << "总充电\n费用" << "总放电\n费用"
<< "日收益" << "总收益" << "--";
// 设置为水平表头
table->setColumnCount(headerText_Row.size());
table->setHorizontalHeaderLabels(headerText_Row);
table->setColumnWidth(0, 50);
table->setColumnWidth(1, 120);
table->setColumnWidth(2, 50);
table->setColumnWidth(4, 80);
}
textLog = std::make_shared<QTextEdit>(this);
textLog->setGeometry(10, y += 310, 1190, 280);
textLog->setStyleSheet("background-color: transparent; border: 1px solid gray; font-weight: 400;");
textLog->setReadOnly(true);
{
// 第二个参数是方法函数名称,即调用 QTextEdit的appeng函数
auto qtSink = std::make_shared<spdlog::sinks::qt_sink_mt>(textLog.get(), "append");
spdlog::default_logger()->sinks().push_back(qtSink);
}
}
void QWHome::setTableCell(int row, int col, std::string text, std::string style /*= ""*/)
{
auto item = table->item(row, col);
if (!item)
{
item = new QTableWidgetItem();
table->setItem(row, col, item);
}
item->setText(text.c_str());
if (style == "OK") { item->setForeground(QBrush(Qt::green)); }
else if (style == "ERR") { item->setForeground(QBrush(Qt::red)); }
}
void QWHome::onTimer()
{
auto& appdata = Application().data();
int rowNo = 0;
int tsNow = Utils::time();
for (auto& item : appdata.mapStation)
{
auto& station = item.second;
if (rowNo >= table->rowCount())
{
table->insertRow(rowNo);
}
bool isOpen = station->status > 0;
bool isConnected = station->mqttCli->isConnected;
setTableCell(rowNo, 0, std::to_string(station->stationId));
setTableCell(rowNo, 1, station->name);
setTableCell(rowNo, 2, station->code);
setTableCell(rowNo, 3, isOpen ? "启用" : "未启用", isOpen ? "OK" : "ERR");
setTableCell(rowNo, 4, isConnected ? "连接成功" : "未连接", isConnected ? "OK" : "ERR");
int tsPolling = station->getPollingTS();
setTableCell(rowNo, 5, tsPolling > 0 ? std::to_string(tsNow - tsPolling) + "/" + std::to_string(Config::option.mqtt.interval) : "--");
setTableCell(rowNo, 6, Utils::toStr(station->statData.dayElectIn, 0));
setTableCell(rowNo, 7, Utils::toStr(station->statData.dayElectOut, 0));
setTableCell(rowNo, 8, Utils::toStr(station->statData.totalElectIn, 0));
setTableCell(rowNo, 9, Utils::toStr(station->statData.totalElectOut, 0));
setTableCell(rowNo, 10, Utils::toStr(station->statData.dayFeeIn, 0));
setTableCell(rowNo, 11, Utils::toStr(station->statData.dayFeeOut, 0));
setTableCell(rowNo, 12, Utils::toStr(station->statData.totalFeeIn, 0));
setTableCell(rowNo, 13, Utils::toStr(station->statData.totalFeeOut, 0));
setTableCell(rowNo, 14, Utils::toStr(station->statData.dayIncome, 0));
setTableCell(rowNo, 15, Utils::toStr(station->statData.totalIncome, 0));
setTableCell(rowNo, 16, station->statData.ts > 0 ? Utils::timeStr(station->statData.ts) : "--");
rowNo++;
}
}

28
src/qt/widgets/QWHome.h Normal file
View File

@@ -0,0 +1,28 @@
#pragma once
#include "qt/MyQUI.h"
#include <spdlog/sinks/base_sink.h>
#include <QHBoxLayout>
#include <QTableWidget>
class QWHome : public MyWidget
{
Q_OBJECT
public:
QWHome(QWidget* parent);
// 创建自定义sink
//std::shared_ptr<QtTextSink> qtSink;
std::shared_ptr<QGroupBox> groupSys;
std::shared_ptr<QGroupBox> groupHttp;
std::shared_ptr<QGroupBox> groupMqtt;
std::shared_ptr<QGroupBox> groupDB;
std::shared_ptr<QTableWidget> table {};
std::shared_ptr<QTextEdit> textLog;
void onTimer() override;
void setTableCell(int row, int col, std::string text, std::string style = "");
};

View File

View File