first commit
61
.gitignore
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
# Build directories
|
||||
build/
|
||||
release/
|
||||
debug/
|
||||
|
||||
# Qt generated files
|
||||
*.moc
|
||||
moc_*.cpp
|
||||
moc_*.h
|
||||
qrc_*.cpp
|
||||
ui_*.h
|
||||
*.qm
|
||||
|
||||
# Object files
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
# Executables
|
||||
*.app
|
||||
*.exe
|
||||
*.out
|
||||
|
||||
# Qt Creator files
|
||||
*.pro.user
|
||||
*.pro.user.*
|
||||
*.qbs.user
|
||||
*.qbs.user.*
|
||||
*.moc.depends
|
||||
*.qmake.stash
|
||||
|
||||
# Makefiles (generated by qmake)
|
||||
Makefile
|
||||
Makefile.*
|
||||
|
||||
# macOS specific
|
||||
.DS_Store
|
||||
*.dSYM/
|
||||
*.dylib
|
||||
|
||||
# Windows specific
|
||||
*.dll
|
||||
*.lib
|
||||
*.pdb
|
||||
|
||||
# Linux specific
|
||||
*.so
|
||||
*.so.*
|
||||
|
||||
# IDE files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
58
CMakeLists.txt
Normal file
@@ -0,0 +1,58 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(CalibratorLauncher VERSION 1.0.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Network)
|
||||
|
||||
set(PROJECT_SOURCES
|
||||
main.cpp
|
||||
mainwindow.cpp
|
||||
mainwindow.h
|
||||
widgets/launcherpage.cpp
|
||||
widgets/launcherpage.h
|
||||
widgets/appicon.cpp
|
||||
widgets/appicon.h
|
||||
widgets/statusbar.cpp
|
||||
widgets/statusbar.h
|
||||
widgets/devicestatuswidget.cpp
|
||||
widgets/devicestatuswidget.h
|
||||
widgets/dockbar.cpp
|
||||
widgets/dockbar.h
|
||||
widgets/pageindicator.cpp
|
||||
widgets/pageindicator.h
|
||||
utils/configmanager.cpp
|
||||
utils/configmanager.h
|
||||
utils/stylehelper.cpp
|
||||
utils/stylehelper.h
|
||||
resources.qrc
|
||||
)
|
||||
|
||||
qt_add_executable(CalibratorLauncher
|
||||
MANUAL_FINALIZATION
|
||||
${PROJECT_SOURCES}
|
||||
)
|
||||
|
||||
target_link_libraries(CalibratorLauncher PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Gui
|
||||
Qt6::Widgets
|
||||
Qt6::Network
|
||||
)
|
||||
|
||||
set_target_properties(CalibratorLauncher PROPERTIES
|
||||
MACOSX_BUNDLE TRUE
|
||||
WIN32_EXECUTABLE TRUE
|
||||
)
|
||||
|
||||
install(TARGETS CalibratorLauncher
|
||||
BUNDLE DESTINATION .
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
)
|
||||
|
||||
qt_finalize_executable(CalibratorLauncher)
|
||||
103
CalibratorLauncher.pro
Normal file
@@ -0,0 +1,103 @@
|
||||
QT += core gui widgets network
|
||||
|
||||
greaterThan(QT_MAJOR_VERSION, 5): QT += core5compat
|
||||
|
||||
CONFIG += c++17
|
||||
|
||||
TARGET = CalibratorLauncher
|
||||
TEMPLATE = app
|
||||
|
||||
# 编译输出目录设置 - 保持根目录整洁
|
||||
DESTDIR = $$PWD/build
|
||||
OBJECTS_DIR = $$PWD/build/obj
|
||||
MOC_DIR = $$PWD/build/moc
|
||||
RCC_DIR = $$PWD/build/rcc
|
||||
UI_DIR = $$PWD/build/ui
|
||||
|
||||
# 应用程序信息
|
||||
VERSION = 1.0.0
|
||||
QMAKE_TARGET_COMPANY = "智能校验仪"
|
||||
QMAKE_TARGET_PRODUCT = "CalibratorLauncher"
|
||||
QMAKE_TARGET_DESCRIPTION = "智能校验仪启动器"
|
||||
QMAKE_TARGET_COPYRIGHT = "Copyright 2025"
|
||||
|
||||
# 源文件
|
||||
SOURCES += \
|
||||
main.cpp \
|
||||
mainwindow.cpp \
|
||||
widgets/launcherpage.cpp \
|
||||
widgets/appicon.cpp \
|
||||
widgets/statusbar.cpp \
|
||||
widgets/devicestatuswidget.cpp \
|
||||
widgets/dockbar.cpp \
|
||||
widgets/pageindicator.cpp \
|
||||
widgets/procedurelistwidget.cpp \
|
||||
widgets/procedureplayerwidget.cpp \
|
||||
widgets/signalmeasurementwidget.cpp \
|
||||
widgets/channelstatuswidget.cpp \
|
||||
widgets/tablewidgetfactory.cpp \
|
||||
widgets/tableoverlaywidget.cpp \
|
||||
widgets/overlaydialogswidget.cpp \
|
||||
widgets/settingswidget.cpp \
|
||||
widgets/datamanagementwidget.cpp \
|
||||
widgets/wirelesswidget.cpp \
|
||||
widgets/networktestwidget.cpp \
|
||||
widgets/waveformwidget.cpp \
|
||||
widgets/dualchannelwidget.cpp \
|
||||
widgets/signaltrimwidget.cpp \
|
||||
hardware/channelmanager.cpp \
|
||||
procedure/proceduremanager.cpp \
|
||||
procedure/procedureengine.cpp \
|
||||
procedure/procedureparser.cpp \
|
||||
procedure/functionregistry.cpp \
|
||||
utils/configmanager.cpp \
|
||||
utils/stylehelper.cpp
|
||||
|
||||
HEADERS += \
|
||||
mainwindow.h \
|
||||
widgets/launcherpage.h \
|
||||
widgets/appicon.h \
|
||||
widgets/statusbar.h \
|
||||
widgets/devicestatuswidget.h \
|
||||
widgets/dockbar.h \
|
||||
widgets/pageindicator.h \
|
||||
widgets/procedurelistwidget.h \
|
||||
widgets/procedureplayerwidget.h \
|
||||
widgets/signalmeasurementwidget.h \
|
||||
widgets/channelstatuswidget.h \
|
||||
widgets/tablewidgetfactory.h \
|
||||
widgets/tableoverlaywidget.h \
|
||||
widgets/overlaydialogswidget.h \
|
||||
widgets/settingswidget.h \
|
||||
widgets/datamanagementwidget.h \
|
||||
widgets/wirelesswidget.h \
|
||||
widgets/networktestwidget.h \
|
||||
widgets/waveformwidget.h \
|
||||
widgets/dualchannelwidget.h \
|
||||
widgets/signaltrimwidget.h \
|
||||
hardware/channelmanager.h \
|
||||
procedure/proceduredata.h \
|
||||
procedure/proceduremanager.h \
|
||||
procedure/procedureengine.h \
|
||||
procedure/procedureparser.h \
|
||||
procedure/functionregistry.h \
|
||||
utils/configmanager.h \
|
||||
utils/stylehelper.h
|
||||
|
||||
RESOURCES += \
|
||||
resources.qrc
|
||||
|
||||
# ryml 库支持 (用于解析 YAML/JSON)
|
||||
INCLUDEPATH += /opt/homebrew/Cellar/rapidyaml/0.10.0/include
|
||||
LIBS += -L/opt/homebrew/Cellar/rapidyaml/0.10.0/lib -lryml -lc4core
|
||||
|
||||
# 默认部署规则
|
||||
qnx: target.path = /tmp/$${TARGET}/bin
|
||||
else: unix:!android: target.path = /opt/$${TARGET}/bin
|
||||
!isEmpty(target.path): INSTALLS += target
|
||||
|
||||
# Windows 图标
|
||||
# win32:RC_ICONS += icons/app.ico
|
||||
|
||||
# macOS 图标 (需要 .icns 文件)
|
||||
# macx:ICON = icons/app.icns
|
||||
451
config/launcher_config.json
Normal file
@@ -0,0 +1,451 @@
|
||||
{
|
||||
"name": "智能校验仪启动器",
|
||||
"version": "1.0.0",
|
||||
"description": "Android Launcher Style Design for Calibrator",
|
||||
"resolution": {
|
||||
"width": 1920,
|
||||
"height": 1200
|
||||
},
|
||||
"colorSystem": {
|
||||
"primary": "#2196F3",
|
||||
"accent": "#FF9800",
|
||||
"success": "#4CAF50",
|
||||
"warning": "#FFC107",
|
||||
"danger": "#F44336",
|
||||
"background": "#F5F5F5",
|
||||
"surface": "#FFFFFF",
|
||||
"textPrimary": "#212121",
|
||||
"textSecondary": "#757575"
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"id": "page1",
|
||||
"title": "信号输出和测量",
|
||||
"apps": [
|
||||
{
|
||||
"id": "rtd_measure",
|
||||
"name": "热电阻测量",
|
||||
"nameEn": "RTD Measure",
|
||||
"icon": "ic_rtd",
|
||||
"color": "#2196F3",
|
||||
"gradient": [
|
||||
"#2196F3",
|
||||
"#1976D2"
|
||||
],
|
||||
"badge": "Ω",
|
||||
"group": "rtd",
|
||||
"description": "2/3/4线热电阻测量",
|
||||
"features": [
|
||||
"测量2/3/4线热电阻阻值",
|
||||
"自动计算线电阻(四线制)",
|
||||
"自动转换为温度"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rtd_output",
|
||||
"name": "热电阻输出",
|
||||
"nameEn": "RTD Output",
|
||||
"icon": "ic_rtd",
|
||||
"color": "#1565C0",
|
||||
"gradient": [
|
||||
"#1565C0",
|
||||
"#0D47A1"
|
||||
],
|
||||
"badge": "PT",
|
||||
"group": "rtd",
|
||||
"description": "PT100模拟输出",
|
||||
"features": [
|
||||
"PT100分度表模拟输出",
|
||||
"双通道温度输出",
|
||||
"温度转电阻自动转换"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "thermocouple",
|
||||
"name": "热电偶",
|
||||
"nameEn": "Thermocouple",
|
||||
"icon": "ic_thermocouple",
|
||||
"color": "#FF9800",
|
||||
"gradient": [
|
||||
"#FF9800",
|
||||
"#F57C00"
|
||||
],
|
||||
"badge": "mV",
|
||||
"description": "热电偶电压测量,分度表模拟输出",
|
||||
"features": [
|
||||
"测量热电偶电压",
|
||||
"自动转换为温度",
|
||||
"热电偶分度表模拟输出毫伏电压"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "insulation",
|
||||
"name": "绝缘电阻",
|
||||
"nameEn": "Insulation",
|
||||
"icon": "ic_insulation",
|
||||
"color": "#F44336",
|
||||
"gradient": [
|
||||
"#F44336",
|
||||
"#D32F2F"
|
||||
],
|
||||
"badge": "MΩ",
|
||||
"description": "50V/100V绝缘电阻测量",
|
||||
"features": [
|
||||
"手动测量50V绝缘电阻",
|
||||
"手动测量100V绝缘电阻",
|
||||
"自动测量绝缘电阻",
|
||||
"热电阻/热电偶测量后自动测试对地电阻",
|
||||
"测试后自动完成对地放电"
|
||||
],
|
||||
"warning": true
|
||||
},
|
||||
{
|
||||
"id": "dc_current",
|
||||
"name": "直流电流",
|
||||
"nameEn": "DC Current",
|
||||
"icon": "ic_dc_current",
|
||||
"color": "#4CAF50",
|
||||
"gradient": [
|
||||
"#4CAF50",
|
||||
"#388E3C"
|
||||
],
|
||||
"badge": "mA",
|
||||
"description": "0-20mA电流输出/测量",
|
||||
"features": [
|
||||
"0-20mA电流输出",
|
||||
"0-20mA电流测量",
|
||||
"有源输出模式",
|
||||
"无源输出模式"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dc_voltage",
|
||||
"name": "直流电压",
|
||||
"nameEn": "DC Voltage",
|
||||
"icon": "ic_dc_voltage",
|
||||
"color": "#9C27B0",
|
||||
"gradient": [
|
||||
"#9C27B0",
|
||||
"#7B1FA2"
|
||||
],
|
||||
"badge": "V",
|
||||
"description": "0-20V电压输出/测量",
|
||||
"features": [
|
||||
"0-20V电压输出",
|
||||
"0-20V电压测量"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "frequency",
|
||||
"name": "频率信号",
|
||||
"nameEn": "Frequency",
|
||||
"icon": "ic_frequency",
|
||||
"color": "#00BCD4",
|
||||
"gradient": [
|
||||
"#00BCD4",
|
||||
"#0097A7"
|
||||
],
|
||||
"badge": "Hz",
|
||||
"description": "转速测量,方波/正弦波输出",
|
||||
"features": [
|
||||
"转速频率信号测量",
|
||||
"方波频率信号输出",
|
||||
"正弦波频率信号输出",
|
||||
"方波幅值和频率可设",
|
||||
"正弦波幅值和频率可设"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ac_voltage",
|
||||
"name": "交流电压",
|
||||
"nameEn": "AC Voltage",
|
||||
"icon": "ic_ac_voltage",
|
||||
"color": "#E91E63",
|
||||
"gradient": [
|
||||
"#E91E63",
|
||||
"#C2185B"
|
||||
],
|
||||
"badge": "VAC",
|
||||
"description": "220VAC测量功能",
|
||||
"features": [
|
||||
"220VAC电压测量"
|
||||
],
|
||||
"warning": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "page2",
|
||||
"title": "信号检测功能",
|
||||
"apps": [
|
||||
{
|
||||
"id": "switch_detect",
|
||||
"name": "开关量检测",
|
||||
"nameEn": "Switch Detect",
|
||||
"icon": "ic_switch",
|
||||
"color": "#795548",
|
||||
"gradient": [
|
||||
"#795548",
|
||||
"#5D4037"
|
||||
],
|
||||
"badge": "SW",
|
||||
"description": "电阻通断/电压判断开关状态",
|
||||
"features": [
|
||||
"电阻通断测量模式",
|
||||
"50V以内电压判断模式",
|
||||
"开关通断状态判断"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ripple",
|
||||
"name": "纹波诊断",
|
||||
"nameEn": "Ripple Diagnosis",
|
||||
"icon": "ic_ripple",
|
||||
"color": "#FF5722",
|
||||
"gradient": [
|
||||
"#FF5722",
|
||||
"#E64A19"
|
||||
],
|
||||
"badge": "AI",
|
||||
"description": "电源纹波检测,AI故障诊断",
|
||||
"features": [
|
||||
"电源纹波自动检测",
|
||||
"最大输出50V",
|
||||
"小波变换分析",
|
||||
"深度学习故障诊断",
|
||||
"报警提示功能",
|
||||
"诊断准确率≥85%"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ramp",
|
||||
"name": "斜波输出",
|
||||
"nameEn": "Ramp Output",
|
||||
"icon": "ic_ramp",
|
||||
"color": "#3F51B5",
|
||||
"gradient": [
|
||||
"#3F51B5",
|
||||
"#303F9F"
|
||||
],
|
||||
"badge": "△",
|
||||
"description": "可设速率的斜波信号输出",
|
||||
"features": [
|
||||
"斜波信号输出",
|
||||
"斜波速率可设"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "waveform",
|
||||
"name": "波形采集",
|
||||
"nameEn": "Waveform Capture",
|
||||
"icon": "ic_waveform",
|
||||
"color": "#009688",
|
||||
"gradient": [
|
||||
"#009688",
|
||||
"#00796B"
|
||||
],
|
||||
"badge": "REC",
|
||||
"description": "信号采集记录,≤25ms/点",
|
||||
"features": [
|
||||
"信号采集和记录",
|
||||
"生成曲线功能",
|
||||
"采集速率≤25ms/点",
|
||||
"采集通道数≥2",
|
||||
"采集电压信号",
|
||||
"采集电流信号",
|
||||
"自动计算最大值",
|
||||
"自动计算最小值",
|
||||
"自动计算平均值"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dual_channel",
|
||||
"name": "双通道",
|
||||
"nameEn": "Dual Channel",
|
||||
"icon": "ic_dual_channel",
|
||||
"color": "#673AB7",
|
||||
"gradient": [
|
||||
"#673AB7",
|
||||
"#512DA8"
|
||||
],
|
||||
"badge": "2CH",
|
||||
"description": "同时输出2路,采集2路信号",
|
||||
"features": [
|
||||
"同时输出2路信号",
|
||||
"同时采集2路信号"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "trim",
|
||||
"name": "信号微调",
|
||||
"nameEn": "Signal Trim",
|
||||
"icon": "ic_trim",
|
||||
"color": "#607D8B",
|
||||
"gradient": [
|
||||
"#607D8B",
|
||||
"#455A64"
|
||||
],
|
||||
"badge": "±",
|
||||
"description": "任意位数信号微调",
|
||||
"features": [
|
||||
"输出值任意位数微调"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "page3",
|
||||
"title": "系统和通讯",
|
||||
"apps": [
|
||||
{
|
||||
"id": "data_management",
|
||||
"name": "数据管理",
|
||||
"nameEn": "Data Management",
|
||||
"icon": "ic_data",
|
||||
"color": "#8BC34A",
|
||||
"gradient": [
|
||||
"#8BC34A",
|
||||
"#689F38"
|
||||
],
|
||||
"badge": "📁",
|
||||
"description": "数据存储、导出、搜索",
|
||||
"features": [
|
||||
"手动存储至机内文件",
|
||||
"自动化流程存储",
|
||||
"无线4G发送",
|
||||
"电缆通讯发送",
|
||||
"功能位置命名",
|
||||
"时间命名",
|
||||
"搜索功能"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "wireless",
|
||||
"name": "无线通讯",
|
||||
"nameEn": "Wireless",
|
||||
"icon": "ic_wireless",
|
||||
"color": "#03A9F4",
|
||||
"gradient": [
|
||||
"#03A9F4",
|
||||
"#0288D1"
|
||||
],
|
||||
"badge": "4G",
|
||||
"description": "4G/WIFI通讯设置",
|
||||
"features": [
|
||||
"4G通讯",
|
||||
"WIFI通讯",
|
||||
"安卓APP控制",
|
||||
"上位机通信"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sop",
|
||||
"name": "规程管理",
|
||||
"nameEn": "SOP",
|
||||
"icon": "ic_sop",
|
||||
"color": "#CDDC39",
|
||||
"gradient": [
|
||||
"#CDDC39",
|
||||
"#AFB42B"
|
||||
],
|
||||
"badge": "📋",
|
||||
"description": "自动化校验流程",
|
||||
"features": [
|
||||
"自动化流程管理",
|
||||
"校验规程编辑",
|
||||
"规程执行"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rcp63",
|
||||
"name": "RCP63",
|
||||
"nameEn": "RCP63",
|
||||
"icon": "ic_rcp63",
|
||||
"color": "#2196F3",
|
||||
"gradient": [
|
||||
"#2196F3",
|
||||
"#1976D2"
|
||||
],
|
||||
"badge": "🌐",
|
||||
"description": "RCP63协议通讯",
|
||||
"features": [
|
||||
"RCP63协议支持"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "settings",
|
||||
"name": "系统设置",
|
||||
"nameEn": "Settings",
|
||||
"icon": "ic_settings",
|
||||
"color": "#9E9E9E",
|
||||
"gradient": [
|
||||
"#9E9E9E",
|
||||
"#757575"
|
||||
],
|
||||
"badge": "⚙️",
|
||||
"description": "系统参数配置",
|
||||
"features": [
|
||||
"系统参数配置",
|
||||
"设备信息",
|
||||
"校准设置"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"dock": [
|
||||
{
|
||||
"id": "dock_sop",
|
||||
"name": "规程",
|
||||
"icon": "ic_sop",
|
||||
"color": "#CDDC39",
|
||||
"gradient": [
|
||||
"#CDDC39",
|
||||
"#AFB42B"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dock_rcp63",
|
||||
"name": "RCP63",
|
||||
"icon": "ic_rcp63",
|
||||
"color": "#2196F3",
|
||||
"gradient": [
|
||||
"#2196F3",
|
||||
"#1976D2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dock_network",
|
||||
"name": "网络测试",
|
||||
"icon": "ic_network_test",
|
||||
"color": "#03A9F4",
|
||||
"gradient": [
|
||||
"#03A9F4",
|
||||
"#0288D1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dock_settings",
|
||||
"name": "系统设置",
|
||||
"icon": "ic_settings",
|
||||
"color": "#9E9E9E",
|
||||
"gradient": [
|
||||
"#9E9E9E",
|
||||
"#757575"
|
||||
]
|
||||
}
|
||||
],
|
||||
"animations": {
|
||||
"pageTransition": {
|
||||
"duration": 300,
|
||||
"easing": "ease-out"
|
||||
},
|
||||
"iconPress": {
|
||||
"scale": 0.95,
|
||||
"duration": 100
|
||||
},
|
||||
"longPress": {
|
||||
"delay": 500
|
||||
}
|
||||
}
|
||||
}
|
||||
349
hardware/channelmanager.cpp
Normal file
@@ -0,0 +1,349 @@
|
||||
#include "channelmanager.h"
|
||||
|
||||
ChannelManager *ChannelManager::m_instance = nullptr;
|
||||
|
||||
ChannelManager::ChannelManager(QObject *parent) : QObject(parent)
|
||||
{
|
||||
initializeChannelCapabilities();
|
||||
|
||||
// Initialize all channels as available
|
||||
for (int i = INPUT_CHANNEL_1; i <= OUTPUT_CHANNEL_2; ++i)
|
||||
{
|
||||
ChannelId channelId = static_cast<ChannelId>(i);
|
||||
ChannelStatus status;
|
||||
status.occupied = false;
|
||||
status.operationDetails.operation = NONE;
|
||||
status.operationDetails.description = QString();
|
||||
status.operationDetails.connectionType = TWO_WIRE;
|
||||
m_channelStatus[channelId] = status;
|
||||
}
|
||||
}
|
||||
|
||||
ChannelManager *ChannelManager::instance()
|
||||
{
|
||||
if (!m_instance)
|
||||
{
|
||||
m_instance = new ChannelManager();
|
||||
}
|
||||
return m_instance;
|
||||
}
|
||||
|
||||
void ChannelManager::initializeChannelCapabilities()
|
||||
{
|
||||
// 输入通道1: 支持最多四线连接(白-红-灰-黑),可进行所有测量功能
|
||||
ChannelCapability inputCh1;
|
||||
inputCh1.type = INPUT_CHANNEL;
|
||||
inputCh1.maxWires = 4;
|
||||
inputCh1.wireColors = QStringList() << "白" << "红" << "灰" << "黑";
|
||||
inputCh1.supportedConnections = QList<ConnectionType>()
|
||||
<< TWO_WIRE << THREE_WIRE << FOUR_WIRE;
|
||||
inputCh1.supportedOperations = QList<OperationType>()
|
||||
<< VOLTAGE_MEASUREMENT << MV_MEASUREMENT
|
||||
<< MA_MEASUREMENT << AC_MEASUREMENT
|
||||
<< RESISTANCE_MEASUREMENT << FREQUENCY_MEASUREMENT
|
||||
<< SWITCH_MEASUREMENT << INSULATION_MEASUREMENT;
|
||||
m_channelCapabilities[INPUT_CHANNEL_1] = inputCh1;
|
||||
|
||||
// 输入通道2: 支持最多两线连接(白-灰),仅支持二线制测量
|
||||
ChannelCapability inputCh2;
|
||||
inputCh2.type = INPUT_CHANNEL;
|
||||
inputCh2.maxWires = 2;
|
||||
inputCh2.wireColors = QStringList() << "白" << "灰";
|
||||
inputCh2.supportedConnections = QList<ConnectionType>() << TWO_WIRE;
|
||||
inputCh2.supportedOperations = QList<OperationType>()
|
||||
<< VOLTAGE_MEASUREMENT << MV_MEASUREMENT
|
||||
<< MA_MEASUREMENT << AC_MEASUREMENT
|
||||
<< RESISTANCE_MEASUREMENT << FREQUENCY_MEASUREMENT
|
||||
<< SWITCH_MEASUREMENT;
|
||||
m_channelCapabilities[INPUT_CHANNEL_2] = inputCh2;
|
||||
|
||||
// 输出通道1: 支持最多四线连接(用于电阻模拟)
|
||||
ChannelCapability outputCh1;
|
||||
outputCh1.type = OUTPUT_CHANNEL;
|
||||
outputCh1.maxWires = 4;
|
||||
outputCh1.wireColors = QStringList() << "白" << "红" << "灰" << "黑";
|
||||
outputCh1.supportedConnections = QList<ConnectionType>()
|
||||
<< TWO_WIRE << THREE_WIRE << FOUR_WIRE;
|
||||
outputCh1.supportedOperations = QList<OperationType>()
|
||||
<< VOLTAGE_OUTPUT << MV_OUTPUT << MA_OUTPUT
|
||||
<< WAVEFORM_OUTPUT << RESISTANCE_SIMULATION;
|
||||
m_channelCapabilities[OUTPUT_CHANNEL_1] = outputCh1;
|
||||
|
||||
// 输出通道2: 支持两线连接
|
||||
ChannelCapability outputCh2;
|
||||
outputCh2.type = OUTPUT_CHANNEL;
|
||||
outputCh2.maxWires = 2;
|
||||
outputCh2.wireColors = QStringList() << "白" << "灰";
|
||||
outputCh2.supportedConnections = QList<ConnectionType>() << TWO_WIRE;
|
||||
outputCh2.supportedOperations = QList<OperationType>()
|
||||
<< VOLTAGE_OUTPUT << MV_OUTPUT << MA_OUTPUT
|
||||
<< WAVEFORM_OUTPUT;
|
||||
m_channelCapabilities[OUTPUT_CHANNEL_2] = outputCh2;
|
||||
}
|
||||
|
||||
bool ChannelManager::isChannelAvailable(ChannelId channel) const
|
||||
{
|
||||
return !m_channelStatus.value(channel).occupied;
|
||||
}
|
||||
|
||||
bool ChannelManager::canChannelSupport(ChannelId channel, OperationType operation,
|
||||
ConnectionType connection) const
|
||||
{
|
||||
if (!m_channelCapabilities.contains(channel))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const ChannelCapability &capability = m_channelCapabilities[channel];
|
||||
|
||||
// 检查连接类型支持
|
||||
if (!capability.supportedConnections.contains(connection))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查操作类型支持
|
||||
if (operation != NONE && !capability.supportedOperations.contains(operation))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查操作类型与通道类型的匹配
|
||||
if (isInputChannelOperation(operation))
|
||||
{
|
||||
return capability.type == INPUT_CHANNEL;
|
||||
}
|
||||
else if (isOutputChannelOperation(operation))
|
||||
{
|
||||
return capability.type == OUTPUT_CHANNEL;
|
||||
}
|
||||
else if (operation == NONE)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ChannelManager::occupyChannel(ChannelId channel, OperationType operation,
|
||||
const QString &description,
|
||||
ConnectionType connectionType)
|
||||
{
|
||||
if (!isChannelAvailable(channel))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!canChannelSupport(channel, operation, connectionType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ChannelStatus &status = m_channelStatus[channel];
|
||||
status.occupied = true;
|
||||
status.operationDetails.operation = operation;
|
||||
status.operationDetails.description = description;
|
||||
status.operationDetails.connectionType = connectionType;
|
||||
status.occupiedTime = QDateTime::currentDateTime();
|
||||
|
||||
emit channelStatusChanged(channel, operation, description);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ChannelManager::releaseChannel(ChannelId channel)
|
||||
{
|
||||
if (m_channelStatus.contains(channel))
|
||||
{
|
||||
ChannelStatus &status = m_channelStatus[channel];
|
||||
status.occupied = false;
|
||||
status.operationDetails.operation = NONE;
|
||||
status.operationDetails.description.clear();
|
||||
status.operationDetails.connectionType = TWO_WIRE;
|
||||
status.occupiedTime = QDateTime();
|
||||
|
||||
emit channelReleased(channel);
|
||||
emit channelStatusChanged(channel, NONE, QString());
|
||||
}
|
||||
}
|
||||
|
||||
ChannelManager::OperationType ChannelManager::getChannelOperation(ChannelId channel) const
|
||||
{
|
||||
return m_channelStatus.value(channel).operationDetails.operation;
|
||||
}
|
||||
|
||||
QString ChannelManager::getChannelDescription(ChannelId channel) const
|
||||
{
|
||||
return m_channelStatus.value(channel).operationDetails.description;
|
||||
}
|
||||
|
||||
ChannelManager::OperationDetails
|
||||
ChannelManager::getChannelOperationDetails(ChannelId channel) const
|
||||
{
|
||||
return m_channelStatus.value(channel).operationDetails;
|
||||
}
|
||||
|
||||
QList<ChannelManager::OperationType>
|
||||
ChannelManager::getSupportedOperations(ChannelId channel) const
|
||||
{
|
||||
return m_channelCapabilities.value(channel).supportedOperations;
|
||||
}
|
||||
|
||||
ChannelManager::ChannelCapability
|
||||
ChannelManager::getChannelCapability(ChannelId channel) const
|
||||
{
|
||||
return m_channelCapabilities.value(channel);
|
||||
}
|
||||
|
||||
QList<ChannelManager::ChannelId>
|
||||
ChannelManager::getAvailableChannels(ChannelType type) const
|
||||
{
|
||||
QList<ChannelId> availableChannels;
|
||||
|
||||
for (auto it = m_channelCapabilities.constBegin();
|
||||
it != m_channelCapabilities.constEnd(); ++it)
|
||||
{
|
||||
if (it.value().type == type && isChannelAvailable(it.key()))
|
||||
{
|
||||
availableChannels.append(it.key());
|
||||
}
|
||||
}
|
||||
|
||||
return availableChannels;
|
||||
}
|
||||
|
||||
QString ChannelManager::getOperationDisplayName(OperationType operation) const
|
||||
{
|
||||
switch (operation)
|
||||
{
|
||||
case NONE:
|
||||
return "空闲";
|
||||
// 输入通道操作
|
||||
case VOLTAGE_MEASUREMENT:
|
||||
return "电压测量";
|
||||
case MV_MEASUREMENT:
|
||||
return "mV测量";
|
||||
case MA_MEASUREMENT:
|
||||
return "mA测量";
|
||||
case AC_MEASUREMENT:
|
||||
return "交流测量";
|
||||
case RESISTANCE_MEASUREMENT:
|
||||
return "电阻测量";
|
||||
case FREQUENCY_MEASUREMENT:
|
||||
return "频率测量";
|
||||
case SWITCH_MEASUREMENT:
|
||||
return "开关量测量";
|
||||
case INSULATION_MEASUREMENT:
|
||||
return "绝缘电阻测量";
|
||||
// 输出通道操作
|
||||
case VOLTAGE_OUTPUT:
|
||||
return "电压输出";
|
||||
case MV_OUTPUT:
|
||||
return "mV输出";
|
||||
case MA_OUTPUT:
|
||||
return "mA输出";
|
||||
case WAVEFORM_OUTPUT:
|
||||
return "波形输出";
|
||||
case RESISTANCE_SIMULATION:
|
||||
return "电阻模拟";
|
||||
}
|
||||
return "未知";
|
||||
}
|
||||
|
||||
QString ChannelManager::getConnectionTypeDisplayName(ConnectionType connection) const
|
||||
{
|
||||
switch (connection)
|
||||
{
|
||||
case TWO_WIRE:
|
||||
return "二线制";
|
||||
case THREE_WIRE:
|
||||
return "三线制";
|
||||
case FOUR_WIRE:
|
||||
return "四线制";
|
||||
}
|
||||
return "未知";
|
||||
}
|
||||
|
||||
QString ChannelManager::getDetailedOperationDescription(
|
||||
const OperationDetails &details) const
|
||||
{
|
||||
QString baseDescription = getOperationDisplayName(details.operation);
|
||||
|
||||
if (details.operation == RESISTANCE_MEASUREMENT ||
|
||||
details.operation == RESISTANCE_SIMULATION)
|
||||
{
|
||||
baseDescription +=
|
||||
QString("(%1)").arg(getConnectionTypeDisplayName(details.connectionType));
|
||||
}
|
||||
|
||||
if (!details.description.isEmpty())
|
||||
{
|
||||
baseDescription += QString(" - %1").arg(details.description);
|
||||
}
|
||||
|
||||
return baseDescription;
|
||||
}
|
||||
|
||||
bool ChannelManager::isInputChannelOperation(OperationType operation) const
|
||||
{
|
||||
switch (operation)
|
||||
{
|
||||
case VOLTAGE_MEASUREMENT:
|
||||
case MV_MEASUREMENT:
|
||||
case MA_MEASUREMENT:
|
||||
case AC_MEASUREMENT:
|
||||
case RESISTANCE_MEASUREMENT:
|
||||
case FREQUENCY_MEASUREMENT:
|
||||
case SWITCH_MEASUREMENT:
|
||||
case INSULATION_MEASUREMENT:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ChannelManager::isOutputChannelOperation(OperationType operation) const
|
||||
{
|
||||
switch (operation)
|
||||
{
|
||||
case VOLTAGE_OUTPUT:
|
||||
case MV_OUTPUT:
|
||||
case MA_OUTPUT:
|
||||
case WAVEFORM_OUTPUT:
|
||||
case RESISTANCE_SIMULATION:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
QString ChannelManager::getChannelDisplayName(ChannelId channel) const
|
||||
{
|
||||
switch (channel)
|
||||
{
|
||||
case INPUT_CHANNEL_1:
|
||||
return "输入通道1";
|
||||
case INPUT_CHANNEL_2:
|
||||
return "输入通道2";
|
||||
case OUTPUT_CHANNEL_1:
|
||||
return "输出通道1";
|
||||
case OUTPUT_CHANNEL_2:
|
||||
return "输出通道2";
|
||||
}
|
||||
return "未知通道";
|
||||
}
|
||||
|
||||
QList<ChannelManager::ChannelId>
|
||||
ChannelManager::getChannelsWithOperation(OperationType operation) const
|
||||
{
|
||||
QList<ChannelId> channels;
|
||||
|
||||
for (auto it = m_channelStatus.constBegin(); it != m_channelStatus.constEnd(); ++it)
|
||||
{
|
||||
if (it.value().operationDetails.operation == operation)
|
||||
{
|
||||
channels.append(it.key());
|
||||
}
|
||||
}
|
||||
|
||||
return channels;
|
||||
}
|
||||
137
hardware/channelmanager.h
Normal file
@@ -0,0 +1,137 @@
|
||||
#ifndef CHANNELMANAGER_H
|
||||
#define CHANNELMANAGER_H
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
|
||||
/**
|
||||
* @brief 通道管理器
|
||||
*
|
||||
* 管理校验仪的输入/输出通道状态
|
||||
* - 输入通道1: 4线制,支持所有测量功能
|
||||
* - 输入通道2: 2线制,支持基本测量
|
||||
* - 输出通道1/2: 2线制,支持电压/电流/波形输出
|
||||
*/
|
||||
class ChannelManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum ChannelType
|
||||
{
|
||||
INPUT_CHANNEL,
|
||||
OUTPUT_CHANNEL
|
||||
};
|
||||
|
||||
enum ChannelId
|
||||
{
|
||||
INPUT_CHANNEL_1 = 1,
|
||||
INPUT_CHANNEL_2 = 2,
|
||||
OUTPUT_CHANNEL_1 = 3,
|
||||
OUTPUT_CHANNEL_2 = 4
|
||||
};
|
||||
|
||||
enum OperationType
|
||||
{
|
||||
// 通用状态
|
||||
NONE,
|
||||
|
||||
// 输入通道操作状态
|
||||
VOLTAGE_MEASUREMENT, // 电压测量
|
||||
MV_MEASUREMENT, // mV测量
|
||||
MA_MEASUREMENT, // mA测量
|
||||
AC_MEASUREMENT, // 交流测量
|
||||
RESISTANCE_MEASUREMENT, // 电阻测量(含热电阻及多线制)
|
||||
FREQUENCY_MEASUREMENT, // 频率测量
|
||||
SWITCH_MEASUREMENT, // 开关量测量
|
||||
INSULATION_MEASUREMENT, // 绝缘电阻测量
|
||||
|
||||
// 输出通道操作状态
|
||||
VOLTAGE_OUTPUT, // 电压输出
|
||||
MV_OUTPUT, // mV输出
|
||||
MA_OUTPUT, // mA输出
|
||||
WAVEFORM_OUTPUT, // 波形输出
|
||||
RESISTANCE_SIMULATION // 电阻模拟(含热电阻及多线制)
|
||||
};
|
||||
|
||||
enum ConnectionType
|
||||
{
|
||||
TWO_WIRE,
|
||||
THREE_WIRE,
|
||||
FOUR_WIRE
|
||||
};
|
||||
|
||||
struct ChannelCapability
|
||||
{
|
||||
ChannelType type;
|
||||
int maxWires;
|
||||
QStringList wireColors;
|
||||
QList<ConnectionType> supportedConnections;
|
||||
QList<OperationType> supportedOperations;
|
||||
};
|
||||
|
||||
struct OperationDetails
|
||||
{
|
||||
OperationType operation;
|
||||
QString description;
|
||||
ConnectionType connectionType;
|
||||
};
|
||||
|
||||
static ChannelManager *instance();
|
||||
|
||||
// 通道状态查询
|
||||
bool isChannelAvailable(ChannelId channel) const;
|
||||
bool canChannelSupport(ChannelId channel, OperationType operation,
|
||||
ConnectionType connection = TWO_WIRE) const;
|
||||
|
||||
// 通道占用/释放
|
||||
bool occupyChannel(ChannelId channel, OperationType operation,
|
||||
const QString &description = QString(),
|
||||
ConnectionType connectionType = TWO_WIRE);
|
||||
void releaseChannel(ChannelId channel);
|
||||
|
||||
// 通道信息获取
|
||||
OperationType getChannelOperation(ChannelId channel) const;
|
||||
QString getChannelDescription(ChannelId channel) const;
|
||||
OperationDetails getChannelOperationDetails(ChannelId channel) const;
|
||||
ChannelCapability getChannelCapability(ChannelId channel) const;
|
||||
QList<ChannelId> getAvailableChannels(ChannelType type) const;
|
||||
QList<OperationType> getSupportedOperations(ChannelId channel) const;
|
||||
|
||||
// 显示名称
|
||||
QString getOperationDisplayName(OperationType operation) const;
|
||||
QString getConnectionTypeDisplayName(ConnectionType connection) const;
|
||||
QString getDetailedOperationDescription(const OperationDetails &details) const;
|
||||
QString getChannelDisplayName(ChannelId channel) const;
|
||||
|
||||
// 类型判断
|
||||
bool isInputChannelOperation(OperationType operation) const;
|
||||
bool isOutputChannelOperation(OperationType operation) const;
|
||||
|
||||
// 查询
|
||||
QList<ChannelId> getChannelsWithOperation(OperationType operation) const;
|
||||
|
||||
signals:
|
||||
void channelStatusChanged(ChannelId channel, OperationType operation,
|
||||
const QString &description);
|
||||
void channelReleased(ChannelId channel);
|
||||
|
||||
private:
|
||||
explicit ChannelManager(QObject *parent = nullptr);
|
||||
void initializeChannelCapabilities();
|
||||
|
||||
struct ChannelStatus
|
||||
{
|
||||
bool occupied;
|
||||
OperationDetails operationDetails;
|
||||
QDateTime occupiedTime;
|
||||
};
|
||||
|
||||
QMap<ChannelId, ChannelStatus> m_channelStatus;
|
||||
QMap<ChannelId, ChannelCapability> m_channelCapabilities;
|
||||
static ChannelManager *m_instance;
|
||||
};
|
||||
|
||||
#endif // CHANNELMANAGER_H
|
||||
25
icons/app.png
Normal file
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
|
||||
<defs>
|
||||
<linearGradient id="appGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1565C0;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="256" height="256" rx="48" fill="url(#appGrad)"/>
|
||||
<g fill="white" transform="translate(48, 48)">
|
||||
<!-- 校验仪图标 -->
|
||||
<rect x="20" y="0" width="120" height="160" rx="12" fill="white"/>
|
||||
<rect x="32" y="16" width="96" height="60" rx="4" fill="#2196F3"/>
|
||||
<!-- 显示屏 -->
|
||||
<text x="80" y="52" font-family="Arial" font-size="20" fill="white" text-anchor="middle" font-weight="bold">12.345</text>
|
||||
<text x="80" y="68" font-family="Arial" font-size="10" fill="white" text-anchor="middle">mA</text>
|
||||
<!-- 按钮 -->
|
||||
<circle cx="50" cy="100" r="12" fill="#4CAF50"/>
|
||||
<circle cx="80" cy="100" r="12" fill="#FF9800"/>
|
||||
<circle cx="110" cy="100" r="12" fill="#F44336"/>
|
||||
<!-- 端子 -->
|
||||
<circle cx="50" cy="140" r="8" fill="#757575"/>
|
||||
<circle cx="80" cy="140" r="8" fill="#757575"/>
|
||||
<circle cx="110" cy="140" r="8" fill="#757575"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
4
icons/arrow_down.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12">
|
||||
<path d="M2 4 L6 8 L10 4" stroke="#757575" stroke-width="2" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 218 B |
4
icons/arrow_up.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12">
|
||||
<path d="M2 8 L6 4 L10 8" stroke="#757575" stroke-width="2" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 218 B |
18
icons/ic_ac_voltage.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="grad7" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#E91E63;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#C2185B;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="20" fill="url(#grad7)" />
|
||||
<g fill="white" transform="translate(18, 18)">
|
||||
<!-- AC 符号 -->
|
||||
<text x="30" y="40" font-family="Arial" font-size="32" fill="white" text-anchor="middle"
|
||||
font-weight="bold">AC</text>
|
||||
<!-- 正弦波小图 -->
|
||||
<path d="M10 50 Q25 35, 30 50 Q35 65, 50 50" stroke="white" stroke-width="3" fill="none" />
|
||||
</g>
|
||||
<text x="48" y="82" font-family="Arial" font-size="10" fill="white" text-anchor="middle"
|
||||
font-weight="bold">220V</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 856 B |
18
icons/ic_data.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="grad14" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#8BC34A;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#689F38;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="20" fill="url(#grad14)" />
|
||||
<g fill="white" transform="translate(22, 18)">
|
||||
<!-- 文件夹 -->
|
||||
<path d="M0 12 L0 48 L52 48 L52 12 L24 12 L20 6 L0 6 Z" fill="white" />
|
||||
<!-- 文件 -->
|
||||
<rect x="12" y="22" width="16" height="20" rx="2" fill="#8BC34A" />
|
||||
<rect x="32" y="22" width="16" height="20" rx="2" fill="#8BC34A" />
|
||||
</g>
|
||||
<text x="48" y="82" font-family="Arial" font-size="10" fill="white" text-anchor="middle"
|
||||
font-weight="bold">Data</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 845 B |
17
icons/ic_dc_current.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="grad4" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#4CAF50;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#388E3C;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="20" fill="url(#grad4)" />
|
||||
<g fill="white" transform="translate(20, 16)">
|
||||
<!-- 电流符号 -->
|
||||
<path d="M28 0 L28 20 L38 20 L22 48 L22 28 L12 28 Z" fill="white" />
|
||||
<text x="28" y="62" font-family="Arial" font-size="16" fill="white" text-anchor="middle"
|
||||
font-weight="bold">I</text>
|
||||
</g>
|
||||
<text x="48" y="82" font-family="Arial" font-size="10" fill="white" text-anchor="middle"
|
||||
font-weight="bold">mA</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 804 B |
16
icons/ic_dc_voltage.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="grad5" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#9C27B0;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#7B1FA2;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="20" fill="url(#grad5)" />
|
||||
<g fill="white" transform="translate(28, 18)">
|
||||
<!-- 电压符号 V -->
|
||||
<text x="20" y="48" font-family="Arial" font-size="48" fill="white" text-anchor="middle"
|
||||
font-weight="bold">V</text>
|
||||
</g>
|
||||
<text x="48" y="82" font-family="Arial" font-size="10" fill="white" text-anchor="middle"
|
||||
font-weight="bold">DC</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 733 B |
24
icons/ic_dual_channel.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="grad12" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#673AB7;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#512DA8;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="20" fill="url(#grad12)" />
|
||||
<g fill="white" transform="translate(18, 22)">
|
||||
<!-- 双通道符号 -->
|
||||
<rect x="0" y="8" width="24" height="32" rx="4" fill="white" />
|
||||
<rect x="36" y="8" width="24" height="32" rx="4" fill="white" />
|
||||
<!-- 连接箭头 -->
|
||||
<polygon points="28,20 32,16 32,24" fill="white" />
|
||||
<polygon points="32,28 28,32 32,36" fill="white" />
|
||||
<!-- 通道标识 -->
|
||||
<text x="12" y="28" font-family="Arial" font-size="10" fill="#673AB7" text-anchor="middle"
|
||||
font-weight="bold">1</text>
|
||||
<text x="48" y="28" font-family="Arial" font-size="10" fill="#673AB7" text-anchor="middle"
|
||||
font-weight="bold">2</text>
|
||||
</g>
|
||||
<text x="48" y="82" font-family="Arial" font-size="10" fill="white" text-anchor="middle"
|
||||
font-weight="bold">2CH</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
17
icons/ic_frequency.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="grad6" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#00BCD4;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#0097A7;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="20" fill="url(#grad6)" />
|
||||
<g stroke="white" stroke-width="3" fill="none" transform="translate(12, 24)">
|
||||
<!-- 正弦波 -->
|
||||
<path d="M0 24 Q18 0, 36 24 Q54 48, 72 24" />
|
||||
<!-- 方波 -->
|
||||
<path d="M0 48 L18 48 L18 28 L36 28 L36 48 L54 48 L54 28 L72 28" />
|
||||
</g>
|
||||
<text x="48" y="82" font-family="Arial" font-size="10" fill="white" text-anchor="middle"
|
||||
font-weight="bold">Hz</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 774 B |
19
icons/ic_insulation.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="grad3" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#F44336;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#D32F2F;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="20" fill="url(#grad3)" />
|
||||
<g fill="white" transform="translate(24, 16)">
|
||||
<!-- 锁图标 -->
|
||||
<rect x="8" y="24" width="32" height="28" rx="4" fill="white" />
|
||||
<path d="M14 24 V16 C14 8, 20 4, 24 4 C28 4, 34 8, 34 16 V24" stroke="white" stroke-width="4"
|
||||
fill="none" />
|
||||
<circle cx="24" cy="38" r="4" fill="#F44336" />
|
||||
<rect x="22" y="40" width="4" height="6" fill="#F44336" />
|
||||
</g>
|
||||
<text x="48" y="82" font-family="Arial" font-size="10" fill="white" text-anchor="middle"
|
||||
font-weight="bold">MΩ</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 905 B |
18
icons/ic_network_test.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="grad19" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#03A9F4;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#0288D1;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="20" fill="url(#grad19)" />
|
||||
<g fill="white" transform="translate(20, 18)">
|
||||
<!-- 网络图标 -->
|
||||
<circle cx="28" cy="28" r="24" stroke="white" stroke-width="3" fill="none" />
|
||||
<circle cx="28" cy="28" r="16" stroke="white" stroke-width="2" fill="none" />
|
||||
<circle cx="28" cy="28" r="8" stroke="white" stroke-width="2" fill="none" />
|
||||
<circle cx="28" cy="28" r="3" fill="white" />
|
||||
</g>
|
||||
<text x="48" y="82" font-family="Arial" font-size="9" fill="white" text-anchor="middle"
|
||||
font-weight="bold">NET</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 901 B |
17
icons/ic_ramp.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="grad10" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#3F51B5;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#303F9F;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="20" fill="url(#grad10)" />
|
||||
<g stroke="white" stroke-width="3" fill="none" transform="translate(18, 20)">
|
||||
<!-- 斜波 -->
|
||||
<path d="M0 48 L20 48 L40 16 L40 48 L60 48" />
|
||||
<!-- 箭头 -->
|
||||
<polygon points="35,22 40,12 45,22" fill="white" stroke="none" />
|
||||
</g>
|
||||
<text x="48" y="82" font-family="Arial" font-size="10" fill="white" text-anchor="middle"
|
||||
font-weight="bold">Ramp</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 774 B |
19
icons/ic_rcp63.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="grad17" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="20" fill="url(#grad17)" />
|
||||
<g fill="white" transform="translate(20, 16)">
|
||||
<!-- 地球 -->
|
||||
<circle cx="28" cy="28" r="24" stroke="white" stroke-width="3" fill="none" />
|
||||
<ellipse cx="28" cy="28" rx="10" ry="24" stroke="white" stroke-width="2" fill="none" />
|
||||
<path d="M4 28 L52 28" stroke="white" stroke-width="2" />
|
||||
<path d="M10 14 L46 14" stroke="white" stroke-width="2" />
|
||||
<path d="M10 42 L46 42" stroke="white" stroke-width="2" />
|
||||
</g>
|
||||
<text x="48" y="82" font-family="Arial" font-size="9" fill="white" text-anchor="middle"
|
||||
font-weight="bold">RCP63</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 964 B |
19
icons/ic_ripple.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="grad9" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#FF5722;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#E64A19;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="20" fill="url(#grad9)" />
|
||||
<g stroke="white" stroke-width="2" fill="none" transform="translate(16, 20)">
|
||||
<!-- 纹波波形 -->
|
||||
<path
|
||||
d="M0 30 L10 30 L10 15 L20 15 L20 30 L30 30 L30 20 L40 20 L40 30 L50 30 L50 10 L60 10 L60 30 L64 30" />
|
||||
<!-- AI 标识 -->
|
||||
<text x="32" y="52" font-family="Arial" font-size="14" fill="white" text-anchor="middle"
|
||||
font-weight="bold" stroke="none">AI</text>
|
||||
</g>
|
||||
<text x="48" y="82" font-family="Arial" font-size="9" fill="white" text-anchor="middle"
|
||||
font-weight="bold">Ripple</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 923 B |
21
icons/ic_rtd.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="20" fill="url(#grad1)" />
|
||||
<g fill="white" transform="translate(24, 16)">
|
||||
<!-- 温度计 -->
|
||||
<rect x="18" y="0" width="12" height="50" rx="6" fill="white" />
|
||||
<circle cx="24" cy="54" r="10" fill="white" />
|
||||
<!-- 刻度 -->
|
||||
<rect x="32" y="10" width="8" height="2" fill="white" opacity="0.7" />
|
||||
<rect x="32" y="20" width="8" height="2" fill="white" opacity="0.7" />
|
||||
<rect x="32" y="30" width="8" height="2" fill="white" opacity="0.7" />
|
||||
<rect x="32" y="40" width="8" height="2" fill="white" opacity="0.7" />
|
||||
</g>
|
||||
<text x="48" y="82" font-family="Arial" font-size="10" fill="white" text-anchor="middle"
|
||||
font-weight="bold">RTD</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
23
icons/ic_rtd_output.svg
Normal file
@@ -0,0 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#1565C0;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#0D47A1;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="20" fill="url(#grad1)" />
|
||||
<g fill="white" transform="translate(18, 14)">
|
||||
<!-- 温度计 -->
|
||||
<rect x="10" y="4" width="10" height="40" rx="5" fill="white" />
|
||||
<circle cx="15" cy="48" r="8" fill="white" />
|
||||
<!-- 刻度 -->
|
||||
<rect x="22" y="12" width="6" height="2" fill="white" opacity="0.7" />
|
||||
<rect x="22" y="20" width="6" height="2" fill="white" opacity="0.7" />
|
||||
<rect x="22" y="28" width="6" height="2" fill="white" opacity="0.7" />
|
||||
<rect x="22" y="36" width="6" height="2" fill="white" opacity="0.7" />
|
||||
<!-- 输出箭头 -->
|
||||
<path d="M38 30 L54 30 L54 24 L66 32 L54 40 L54 34 L38 34 Z" fill="white" />
|
||||
</g>
|
||||
<text x="48" y="82" font-family="Arial" font-size="9" fill="white" text-anchor="middle"
|
||||
font-weight="bold">OUTPUT</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
26
icons/ic_settings.svg
Normal file
@@ -0,0 +1,26 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="grad18" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#9E9E9E;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#757575;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="20" fill="url(#grad18)" />
|
||||
<g fill="white" transform="translate(20, 16)">
|
||||
<!-- 齿轮 -->
|
||||
<circle cx="28" cy="28" r="12" stroke="white" stroke-width="6" fill="none" />
|
||||
<circle cx="28" cy="28" r="4" fill="white" />
|
||||
<!-- 齿轮齿 -->
|
||||
<rect x="24" y="0" width="8" height="12" fill="white" />
|
||||
<rect x="24" y="44" width="8" height="12" fill="white" />
|
||||
<rect x="0" y="24" width="12" height="8" fill="white" />
|
||||
<rect x="44" y="24" width="12" height="8" fill="white" />
|
||||
<!-- 斜齿 -->
|
||||
<rect x="6" y="6" width="8" height="10" fill="white" transform="rotate(45 10 11)" />
|
||||
<rect x="42" y="6" width="8" height="10" fill="white" transform="rotate(-45 46 11)" />
|
||||
<rect x="6" y="42" width="8" height="10" fill="white" transform="rotate(-45 10 47)" />
|
||||
<rect x="42" y="42" width="8" height="10" fill="white" transform="rotate(45 46 47)" />
|
||||
</g>
|
||||
<text x="48" y="82" font-family="Arial" font-size="10" fill="white" text-anchor="middle"
|
||||
font-weight="bold">设置</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
21
icons/ic_sop.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="grad16" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#CDDC39;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#AFB42B;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="20" fill="url(#grad16)" />
|
||||
<g fill="white" transform="translate(22, 16)">
|
||||
<!-- 剪贴板 -->
|
||||
<rect x="4" y="8" width="44" height="52" rx="4" fill="white" />
|
||||
<rect x="16" y="0" width="20" height="12" rx="2" fill="white" />
|
||||
<!-- 勾选列表 -->
|
||||
<rect x="12" y="20" width="28" height="4" rx="2" fill="#CDDC39" />
|
||||
<rect x="12" y="30" width="20" height="4" rx="2" fill="#CDDC39" />
|
||||
<rect x="12" y="40" width="24" height="4" rx="2" fill="#CDDC39" />
|
||||
<rect x="12" y="50" width="16" height="4" rx="2" fill="#CDDC39" />
|
||||
</g>
|
||||
<text x="48" y="82" font-family="Arial" font-size="10" fill="white" text-anchor="middle"
|
||||
font-weight="bold">SOP</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
18
icons/ic_switch.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="grad8" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#795548;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#5D4037;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="20" fill="url(#grad8)" />
|
||||
<g fill="white" transform="translate(20, 20)">
|
||||
<!-- 开关符号 -->
|
||||
<rect x="0" y="16" width="56" height="24" rx="12" fill="white" />
|
||||
<circle cx="16" cy="28" r="10" fill="#795548" />
|
||||
<!-- ON/OFF 文字 -->
|
||||
<text x="38" y="32" font-family="Arial" font-size="10" fill="#795548" text-anchor="middle">ON</text>
|
||||
</g>
|
||||
<text x="48" y="82" font-family="Arial" font-size="10" fill="white" text-anchor="middle"
|
||||
font-weight="bold">SW</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 859 B |
19
icons/ic_thermocouple.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="grad2" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#FF9800;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#F57C00;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="20" fill="url(#grad2)" />
|
||||
<g fill="white" transform="translate(28, 18)">
|
||||
<!-- 火焰 -->
|
||||
<path d="M20 0 C20 0, 35 20, 35 35 C35 45, 27 50, 20 50 C13 50, 5 45, 5 35 C5 20, 20 0, 20 0 Z"
|
||||
fill="white" />
|
||||
<path
|
||||
d="M20 20 C20 20, 28 30, 28 38 C28 44, 24 48, 20 48 C16 48, 12 44, 12 38 C12 30, 20 20, 20 20 Z"
|
||||
fill="#FF9800" />
|
||||
</g>
|
||||
<text x="48" y="82" font-family="Arial" font-size="10" fill="white" text-anchor="middle"
|
||||
font-weight="bold">TC</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 857 B |
20
icons/ic_trim.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="grad13" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#607D8B;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#455A64;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="20" fill="url(#grad13)" />
|
||||
<g fill="white" transform="translate(18, 20)">
|
||||
<!-- 滑块轨道 -->
|
||||
<rect x="0" y="20" width="60" height="8" rx="4" fill="rgba(255,255,255,0.3)" />
|
||||
<!-- 滑块 -->
|
||||
<circle cx="36" cy="24" r="10" fill="white" />
|
||||
<!-- +/- 符号 -->
|
||||
<text x="8" y="50" font-family="Arial" font-size="16" fill="white" text-anchor="middle">−</text>
|
||||
<text x="52" y="50" font-family="Arial" font-size="16" fill="white" text-anchor="middle">+</text>
|
||||
</g>
|
||||
<text x="48" y="82" font-family="Arial" font-size="10" fill="white" text-anchor="middle"
|
||||
font-weight="bold">±</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 990 B |
20
icons/ic_waveform.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="grad11" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#009688;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#00796B;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="20" fill="url(#grad11)" />
|
||||
<g stroke="white" stroke-width="2" fill="none" transform="translate(14, 18)">
|
||||
<!-- 示波器框 -->
|
||||
<rect x="0" y="0" width="68" height="48" rx="4" fill="none" stroke="white" stroke-width="2" />
|
||||
<!-- 波形 -->
|
||||
<path d="M8 24 Q17 8, 24 24 Q31 40, 38 24 Q45 8, 52 24 Q59 40, 60 24" stroke="white"
|
||||
stroke-width="2" />
|
||||
<!-- 录制指示 -->
|
||||
<circle cx="58" cy="10" r="4" fill="#F44336" />
|
||||
</g>
|
||||
<text x="48" y="82" font-family="Arial" font-size="10" fill="white" text-anchor="middle"
|
||||
font-weight="bold">REC</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 950 B |
21
icons/ic_wireless.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="grad15" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#03A9F4;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#0288D1;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="20" fill="url(#grad15)" />
|
||||
<g fill="white" transform="translate(24, 16)">
|
||||
<!-- 信号塔 -->
|
||||
<rect x="20" y="20" width="8" height="36" fill="white" />
|
||||
<polygon points="24,20 10,56 38,56" fill="white" />
|
||||
<!-- WiFi 信号 -->
|
||||
<path d="M24 8 Q8 8, 8 24" stroke="white" stroke-width="3" fill="none" stroke-linecap="round" />
|
||||
<path d="M24 8 Q40 8, 40 24" stroke="white" stroke-width="3" fill="none" stroke-linecap="round" />
|
||||
<path d="M24 0 Q0 0, 0 24" stroke="white" stroke-width="3" fill="none" stroke-linecap="round" />
|
||||
<path d="M24 0 Q48 0, 48 24" stroke="white" stroke-width="3" fill="none" stroke-linecap="round" />
|
||||
</g>
|
||||
<text x="48" y="82" font-family="Arial" font-size="10" fill="white" text-anchor="middle"
|
||||
font-weight="bold">4G</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
39
main.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
#include <QApplication>
|
||||
#include <QFontDatabase>
|
||||
#include <QFile>
|
||||
#include <QDir>
|
||||
#include "mainwindow.h"
|
||||
#include "utils/stylehelper.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
// 启用高DPI支持
|
||||
QApplication::setHighDpiScaleFactorRoundingPolicy(
|
||||
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
|
||||
|
||||
QApplication app(argc, argv);
|
||||
|
||||
// 设置应用程序信息
|
||||
app.setApplicationName("智能校验仪");
|
||||
app.setApplicationVersion("1.0.0");
|
||||
app.setOrganizationName("Calibrator");
|
||||
|
||||
// 加载字体
|
||||
QFontDatabase::addApplicationFont(":/fonts/SourceHanSansSC-Regular.otf");
|
||||
QFontDatabase::addApplicationFont(":/fonts/SourceHanSansSC-Medium.otf");
|
||||
QFontDatabase::addApplicationFont(":/fonts/SourceHanSansSC-Bold.otf");
|
||||
|
||||
// 设置默认字体
|
||||
QFont defaultFont("Source Han Sans SC", 12);
|
||||
defaultFont.setStyleHint(QFont::SansSerif);
|
||||
app.setFont(defaultFont);
|
||||
|
||||
// 应用全局样式
|
||||
StyleHelper::applyGlobalStyle(&app);
|
||||
|
||||
// 创建主窗口
|
||||
MainWindow mainWindow;
|
||||
mainWindow.show();
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
457
mainwindow.cpp
Normal file
@@ -0,0 +1,457 @@
|
||||
#include "mainwindow.h"
|
||||
#include "widgets/overlaydialogswidget.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QMouseEvent>
|
||||
#include <QDebug>
|
||||
#include <QProcess>
|
||||
#include <QCoreApplication>
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent), m_currentPage(0), m_totalPages(1), m_isSwiping(false), m_swipeThreshold(100)
|
||||
{
|
||||
// 设置窗口属性
|
||||
setWindowTitle("智能校验仪");
|
||||
setMinimumSize(1024, 600);
|
||||
resize(1024, 600);
|
||||
|
||||
// 初始化配置管理器
|
||||
m_configManager = new ConfigManager(this);
|
||||
|
||||
// 初始化规程管理器
|
||||
m_procedureManager = new ProcedureManager(this);
|
||||
|
||||
// 加载配置
|
||||
loadConfig();
|
||||
|
||||
// 设置UI
|
||||
setupUI();
|
||||
|
||||
// 设置页面
|
||||
setupPages();
|
||||
|
||||
// 设置规程播放器
|
||||
setupProcedurePlayer();
|
||||
|
||||
// 设置信号测量功能
|
||||
setupSignalMeasurement();
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
}
|
||||
|
||||
void MainWindow::setupUI()
|
||||
{
|
||||
// 创建中心部件
|
||||
m_centralWidget = new QWidget(this);
|
||||
m_centralWidget->setObjectName("centralWidget");
|
||||
setCentralWidget(m_centralWidget);
|
||||
|
||||
// 主布局
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(m_centralWidget);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->setSpacing(0);
|
||||
|
||||
// 主视图切换器 - 用于在启动器和规程播放器之间切换
|
||||
m_mainStackedWidget = new QStackedWidget(this);
|
||||
mainLayout->addWidget(m_mainStackedWidget, 1);
|
||||
|
||||
// 启动器容器
|
||||
m_launcherContainer = new QWidget(this);
|
||||
m_launcherContainer->setObjectName("launcherContainer");
|
||||
|
||||
QVBoxLayout *launcherLayout = new QVBoxLayout(m_launcherContainer);
|
||||
launcherLayout->setContentsMargins(0, 0, 0, 0);
|
||||
launcherLayout->setSpacing(0);
|
||||
|
||||
// 状态栏 - 透明风格
|
||||
m_statusBar = new StatusBar(this);
|
||||
launcherLayout->addWidget(m_statusBar);
|
||||
|
||||
// 页面容器 - 放置启动器页面
|
||||
m_pageContainer = new QWidget(this);
|
||||
m_pageContainer->setObjectName("pageContainer");
|
||||
m_pageContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
launcherLayout->addWidget(m_pageContainer, 1);
|
||||
|
||||
// 页面指示器 - Android风格点状指示器
|
||||
m_pageIndicator = new PageIndicator(1, this);
|
||||
m_pageIndicator->setFixedHeight(40);
|
||||
m_pageIndicator->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
launcherLayout->addWidget(m_pageIndicator);
|
||||
connect(m_pageIndicator, &PageIndicator::pageClicked, this, &MainWindow::goToPage);
|
||||
|
||||
// 添加启动器到主视图
|
||||
m_mainStackedWidget->addWidget(m_launcherContainer);
|
||||
|
||||
// Android风格渐变壁纸背景
|
||||
m_launcherContainer->setStyleSheet(R"(
|
||||
#launcherContainer {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
|
||||
stop:0 #667eea, stop:0.5 #764ba2, stop:1 #f093fb);
|
||||
}
|
||||
#pageContainer {
|
||||
background: transparent;
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
void MainWindow::setupPages()
|
||||
{
|
||||
// 获取页面配置
|
||||
auto pagesConfig = m_configManager->getPages();
|
||||
m_totalPages = pagesConfig.size();
|
||||
m_pageIndicator->setPageCount(m_totalPages);
|
||||
|
||||
qDebug() << "=== setupPages Debug ===";
|
||||
qDebug() << "Total pages:" << m_totalPages;
|
||||
for (int i = 0; i < pagesConfig.size(); ++i)
|
||||
{
|
||||
qDebug() << "Page" << i << ":" << pagesConfig[i].title;
|
||||
qDebug() << " Apps count:" << pagesConfig[i].apps.size();
|
||||
for (const auto &app : pagesConfig[i].apps)
|
||||
{
|
||||
qDebug() << " - " << app.name << "(" << app.id << ")";
|
||||
}
|
||||
}
|
||||
|
||||
// 为页面容器创建布局
|
||||
QVBoxLayout *containerLayout = new QVBoxLayout(m_pageContainer);
|
||||
containerLayout->setContentsMargins(0, 0, 0, 0);
|
||||
containerLayout->setSpacing(0);
|
||||
|
||||
// 使用 QStackedWidget 来管理多个页面
|
||||
m_stackedWidget = new QStackedWidget(m_pageContainer);
|
||||
containerLayout->addWidget(m_stackedWidget);
|
||||
|
||||
// 创建各个页面
|
||||
for (int i = 0; i < pagesConfig.size(); ++i)
|
||||
{
|
||||
LauncherPage *page = new LauncherPage(pagesConfig[i], m_stackedWidget);
|
||||
connect(page, &LauncherPage::appClicked, this, &MainWindow::onAppClicked);
|
||||
m_pages.append(page);
|
||||
m_stackedWidget->addWidget(page);
|
||||
}
|
||||
|
||||
// 显示当前页面
|
||||
m_stackedWidget->setCurrentIndex(m_currentPage);
|
||||
|
||||
// 不再需要Dock栏
|
||||
// m_dockBar->setApps(m_configManager->getDockApps());
|
||||
}
|
||||
|
||||
void MainWindow::loadConfig()
|
||||
{
|
||||
// 从配置文件加载
|
||||
QString configPath = QCoreApplication::applicationDirPath() + "/config/launcher_config.json";
|
||||
if (!m_configManager->loadFromFile(configPath))
|
||||
{
|
||||
// 使用默认配置
|
||||
m_configManager->loadDefaultConfig();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::setupProcedurePlayer()
|
||||
{
|
||||
// 规程列表页
|
||||
m_procedureListWidget = new ProcedureListWidget(m_procedureManager, this);
|
||||
connect(m_procedureListWidget, &ProcedureListWidget::procedureSelected,
|
||||
this, &MainWindow::onProcedureSelected);
|
||||
connect(m_procedureListWidget, &ProcedureListWidget::backRequested,
|
||||
this, &MainWindow::onBackToLauncher);
|
||||
m_mainStackedWidget->addWidget(m_procedureListWidget);
|
||||
|
||||
// 规程执行页
|
||||
m_procedurePlayerWidget = new ProcedurePlayerWidget(m_procedureManager, this);
|
||||
connect(m_procedurePlayerWidget, &ProcedurePlayerWidget::backToListRequested,
|
||||
this, [this]()
|
||||
{ showProcedureList(); });
|
||||
m_mainStackedWidget->addWidget(m_procedurePlayerWidget);
|
||||
}
|
||||
|
||||
void MainWindow::setupSignalMeasurement()
|
||||
{
|
||||
m_signalMeasurementWidget = new SignalMeasurementWidget(this);
|
||||
connect(m_signalMeasurementWidget, &SignalMeasurementWidget::backRequested,
|
||||
this, &MainWindow::onBackToLauncherFromSignal);
|
||||
m_mainStackedWidget->addWidget(m_signalMeasurementWidget);
|
||||
|
||||
// 设置系统设置页面
|
||||
m_settingsWidget = new SettingsWidget(this);
|
||||
connect(m_settingsWidget, &SettingsWidget::backRequested,
|
||||
this, &MainWindow::showLauncher);
|
||||
m_mainStackedWidget->addWidget(m_settingsWidget);
|
||||
|
||||
// 设置数据管理页面
|
||||
m_dataManagementWidget = new DataManagementWidget(this);
|
||||
connect(m_dataManagementWidget, &DataManagementWidget::backRequested,
|
||||
this, &MainWindow::showLauncher);
|
||||
m_mainStackedWidget->addWidget(m_dataManagementWidget);
|
||||
|
||||
// 设置无线通讯页面
|
||||
m_wirelessWidget = new WirelessWidget(this);
|
||||
connect(m_wirelessWidget, &WirelessWidget::backRequested,
|
||||
this, &MainWindow::showLauncher);
|
||||
m_mainStackedWidget->addWidget(m_wirelessWidget);
|
||||
|
||||
// 设置网络测试页面
|
||||
m_networkTestWidget = new NetworkTestWidget(this);
|
||||
connect(m_networkTestWidget, &NetworkTestWidget::backRequested,
|
||||
this, &MainWindow::showLauncher);
|
||||
m_mainStackedWidget->addWidget(m_networkTestWidget);
|
||||
|
||||
// 设置波形采集页面
|
||||
m_waveformWidget = new WaveformWidget(this);
|
||||
connect(m_waveformWidget, &WaveformWidget::backRequested,
|
||||
this, &MainWindow::showLauncher);
|
||||
m_mainStackedWidget->addWidget(m_waveformWidget);
|
||||
|
||||
// 设置双通道页面
|
||||
m_dualChannelWidget = new DualChannelWidget(this);
|
||||
connect(m_dualChannelWidget, &DualChannelWidget::backRequested,
|
||||
this, &MainWindow::showLauncher);
|
||||
m_mainStackedWidget->addWidget(m_dualChannelWidget);
|
||||
|
||||
// 设置信号微调页面
|
||||
m_signalTrimWidget = new SignalTrimWidget(this);
|
||||
connect(m_signalTrimWidget, &SignalTrimWidget::backRequested,
|
||||
this, &MainWindow::showLauncher);
|
||||
m_mainStackedWidget->addWidget(m_signalTrimWidget);
|
||||
}
|
||||
|
||||
void MainWindow::showLauncher()
|
||||
{
|
||||
m_mainStackedWidget->setCurrentWidget(m_launcherContainer);
|
||||
}
|
||||
|
||||
void MainWindow::showProcedureList()
|
||||
{
|
||||
m_procedureListWidget->refreshList();
|
||||
m_mainStackedWidget->setCurrentWidget(m_procedureListWidget);
|
||||
}
|
||||
|
||||
void MainWindow::showProcedurePlayer(const QString &procedureId)
|
||||
{
|
||||
m_procedurePlayerWidget->loadProcedure(procedureId);
|
||||
m_mainStackedWidget->setCurrentWidget(m_procedurePlayerWidget);
|
||||
}
|
||||
|
||||
void MainWindow::showSignalMeasurement(const QString &appId)
|
||||
{
|
||||
// 根据appId设置对应的模块
|
||||
if (appId == "rtd_measure")
|
||||
m_signalMeasurementWidget->setModule(0); // 热电阻测量
|
||||
else if (appId == "rtd_output")
|
||||
m_signalMeasurementWidget->setModule(11); // 热电阻输出(新增)
|
||||
else if (appId == "rtd")
|
||||
m_signalMeasurementWidget->setModule(0); // 兼容旧的rtd
|
||||
else if (appId == "thermocouple")
|
||||
m_signalMeasurementWidget->setModule(1);
|
||||
else if (appId == "insulation")
|
||||
m_signalMeasurementWidget->setModule(2);
|
||||
else if (appId == "dc_current")
|
||||
m_signalMeasurementWidget->setModule(3);
|
||||
else if (appId == "dc_voltage")
|
||||
m_signalMeasurementWidget->setModule(4);
|
||||
else if (appId == "frequency")
|
||||
m_signalMeasurementWidget->setModule(5);
|
||||
else if (appId == "ac_voltage")
|
||||
m_signalMeasurementWidget->setModule(6);
|
||||
else if (appId == "switch_detect")
|
||||
m_signalMeasurementWidget->setModule(7);
|
||||
else if (appId == "waveform")
|
||||
m_signalMeasurementWidget->setModule(8);
|
||||
else if (appId == "ramp")
|
||||
m_signalMeasurementWidget->setModule(9);
|
||||
else if (appId == "ripple")
|
||||
m_signalMeasurementWidget->setModule(10);
|
||||
else
|
||||
m_signalMeasurementWidget->setModule(0); // 默认热电阻
|
||||
|
||||
m_mainStackedWidget->setCurrentWidget(m_signalMeasurementWidget);
|
||||
}
|
||||
|
||||
void MainWindow::onProcedureSelected(const QString &procedureId)
|
||||
{
|
||||
showProcedurePlayer(procedureId);
|
||||
}
|
||||
|
||||
void MainWindow::onBackToLauncher()
|
||||
{
|
||||
showLauncher();
|
||||
}
|
||||
|
||||
void MainWindow::onBackToLauncherFromSignal()
|
||||
{
|
||||
showLauncher();
|
||||
}
|
||||
|
||||
void MainWindow::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QMainWindow::resizeEvent(event);
|
||||
|
||||
// 调整页面大小
|
||||
for (auto page : m_pages)
|
||||
{
|
||||
page->setGeometry(m_pageContainer->rect());
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
// 检查是否在页面容器区域
|
||||
if (m_pageContainer->geometry().contains(event->pos()))
|
||||
{
|
||||
m_swipeStartPos = event->pos();
|
||||
m_isSwiping = true;
|
||||
}
|
||||
QMainWindow::mousePressEvent(event);
|
||||
}
|
||||
|
||||
void MainWindow::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if (m_isSwiping)
|
||||
{
|
||||
// 可以添加实时跟随效果
|
||||
}
|
||||
QMainWindow::mouseMoveEvent(event);
|
||||
}
|
||||
|
||||
void MainWindow::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
if (m_isSwiping)
|
||||
{
|
||||
int deltaX = event->pos().x() - m_swipeStartPos.x();
|
||||
handleSwipe(deltaX);
|
||||
m_isSwiping = false;
|
||||
}
|
||||
QMainWindow::mouseReleaseEvent(event);
|
||||
}
|
||||
|
||||
void MainWindow::handleSwipe(int deltaX)
|
||||
{
|
||||
if (qAbs(deltaX) < m_swipeThreshold)
|
||||
{
|
||||
return; // 滑动距离不够
|
||||
}
|
||||
|
||||
int newPage = m_currentPage;
|
||||
if (deltaX > 0 && m_currentPage > 0)
|
||||
{
|
||||
// 向右滑动,前一页
|
||||
newPage = m_currentPage - 1;
|
||||
}
|
||||
else if (deltaX < 0 && m_currentPage < m_totalPages - 1)
|
||||
{
|
||||
// 向左滑动,后一页
|
||||
newPage = m_currentPage + 1;
|
||||
}
|
||||
|
||||
if (newPage != m_currentPage)
|
||||
{
|
||||
goToPage(newPage);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::goToPage(int pageIndex)
|
||||
{
|
||||
if (pageIndex < 0 || pageIndex >= m_totalPages || pageIndex == m_currentPage)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_currentPage = pageIndex;
|
||||
m_stackedWidget->setCurrentIndex(pageIndex);
|
||||
if (m_pageIndicator)
|
||||
{
|
||||
m_pageIndicator->setCurrentPage(pageIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::animatePageTransition(int fromPage, int toPage)
|
||||
{
|
||||
Q_UNUSED(fromPage)
|
||||
Q_UNUSED(toPage)
|
||||
// 使用 QStackedWidget 后不需要手动动画
|
||||
}
|
||||
|
||||
void MainWindow::onAppClicked(const QString &appId)
|
||||
{
|
||||
openApplication(appId);
|
||||
}
|
||||
|
||||
void MainWindow::onDockAppClicked(const QString &appId)
|
||||
{
|
||||
openApplication(appId);
|
||||
}
|
||||
|
||||
void MainWindow::openApplication(const QString &appId)
|
||||
{
|
||||
qDebug() << "Opening application:" << appId;
|
||||
|
||||
// 根据appId打开对应功能窗口
|
||||
if (appId == "procedures" || appId == "procedure")
|
||||
{
|
||||
// 打开规程播放器
|
||||
showProcedureList();
|
||||
}
|
||||
else if (appId == "signal_measurement" || appId == "rtd" || appId == "rtd_measure" ||
|
||||
appId == "rtd_output" || appId == "thermocouple" ||
|
||||
appId == "insulation" || appId == "dc_current" || appId == "dc_voltage" ||
|
||||
appId == "frequency" || appId == "ac_voltage" || appId == "switch_detect" ||
|
||||
appId == "ripple" || appId == "ramp")
|
||||
{
|
||||
// 打开信号测量功能页面
|
||||
showSignalMeasurement(appId);
|
||||
}
|
||||
else if (appId == "waveform")
|
||||
{
|
||||
// 打开波形采集页面
|
||||
m_mainStackedWidget->setCurrentWidget(m_waveformWidget);
|
||||
}
|
||||
else if (appId == "dual_channel")
|
||||
{
|
||||
// 打开双通道页面
|
||||
m_mainStackedWidget->setCurrentWidget(m_dualChannelWidget);
|
||||
}
|
||||
else if (appId == "trim")
|
||||
{
|
||||
// 打开信号微调页面
|
||||
m_mainStackedWidget->setCurrentWidget(m_signalTrimWidget);
|
||||
}
|
||||
else if (appId == "data_management")
|
||||
{
|
||||
// 打开数据管理页面
|
||||
m_mainStackedWidget->setCurrentWidget(m_dataManagementWidget);
|
||||
}
|
||||
else if (appId == "wireless")
|
||||
{
|
||||
// 打开无线通讯页面
|
||||
m_mainStackedWidget->setCurrentWidget(m_wirelessWidget);
|
||||
}
|
||||
else if (appId == "sop" || appId == "dock_sop")
|
||||
{
|
||||
// 打开规程播放器
|
||||
showProcedureList();
|
||||
}
|
||||
else if (appId == "rcp63" || appId == "dock_rcp63")
|
||||
{
|
||||
// 直接打开RCP63规程
|
||||
showProcedurePlayer("KMCIXRCP503");
|
||||
}
|
||||
else if (appId == "settings" || appId == "dock_settings")
|
||||
{
|
||||
// 打开系统设置页面
|
||||
m_mainStackedWidget->setCurrentWidget(m_settingsWidget);
|
||||
}
|
||||
else if (appId == "dock_network")
|
||||
{
|
||||
// 打开网络测试页面
|
||||
m_mainStackedWidget->setCurrentWidget(m_networkTestWidget);
|
||||
}
|
||||
else
|
||||
{
|
||||
OverlayDialog::information(this, "功能", QString("功能 ID: %1").arg(appId));
|
||||
}
|
||||
}
|
||||
105
mainwindow.h
Normal file
@@ -0,0 +1,105 @@
|
||||
#ifndef MAINWINDOW_H
|
||||
#define MAINWINDOW_H
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QStackedWidget>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QParallelAnimationGroup>
|
||||
#include <QVector>
|
||||
#include "widgets/statusbar.h"
|
||||
#include "widgets/launcherpage.h"
|
||||
#include "widgets/pageindicator.h"
|
||||
#include "widgets/dockbar.h"
|
||||
#include "widgets/procedurelistwidget.h"
|
||||
#include "widgets/procedureplayerwidget.h"
|
||||
#include "widgets/signalmeasurementwidget.h"
|
||||
#include "widgets/settingswidget.h"
|
||||
#include "widgets/datamanagementwidget.h"
|
||||
#include "widgets/wirelesswidget.h"
|
||||
#include "widgets/networktestwidget.h"
|
||||
#include "widgets/waveformwidget.h"
|
||||
#include "widgets/dualchannelwidget.h"
|
||||
#include "widgets/signaltrimwidget.h"
|
||||
#include "utils/configmanager.h"
|
||||
#include "procedure/proceduremanager.h"
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
~MainWindow();
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void onAppClicked(const QString &appId);
|
||||
void onDockAppClicked(const QString &appId);
|
||||
void goToPage(int pageIndex);
|
||||
void animatePageTransition(int fromPage, int toPage);
|
||||
|
||||
// 规程播放器相关
|
||||
void onProcedureSelected(const QString &procedureId);
|
||||
void onBackToLauncher();
|
||||
void onBackToLauncherFromSignal();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void setupPages();
|
||||
void setupProcedurePlayer();
|
||||
void setupSignalMeasurement();
|
||||
void loadConfig();
|
||||
void handleSwipe(int deltaX);
|
||||
void openApplication(const QString &appId);
|
||||
void showLauncher();
|
||||
void showProcedureList();
|
||||
void showProcedurePlayer(const QString &procedureId);
|
||||
void showSignalMeasurement(const QString &appId = QString());
|
||||
|
||||
// UI 组件
|
||||
QWidget *m_centralWidget;
|
||||
StatusBar *m_statusBar;
|
||||
QWidget *m_pageContainer;
|
||||
QStackedWidget *m_stackedWidget;
|
||||
QVector<LauncherPage *> m_pages;
|
||||
PageIndicator *m_pageIndicator;
|
||||
DockBar *m_dockBar;
|
||||
|
||||
// 规程播放器组件
|
||||
QStackedWidget *m_mainStackedWidget; // 主视图切换
|
||||
QWidget *m_launcherContainer;
|
||||
ProcedureListWidget *m_procedureListWidget;
|
||||
ProcedurePlayerWidget *m_procedurePlayerWidget;
|
||||
ProcedureManager *m_procedureManager;
|
||||
|
||||
// 信号测量组件
|
||||
SignalMeasurementWidget *m_signalMeasurementWidget;
|
||||
|
||||
// 设置和数据管理组件
|
||||
SettingsWidget *m_settingsWidget;
|
||||
DataManagementWidget *m_dataManagementWidget;
|
||||
WirelessWidget *m_wirelessWidget;
|
||||
NetworkTestWidget *m_networkTestWidget;
|
||||
WaveformWidget *m_waveformWidget;
|
||||
DualChannelWidget *m_dualChannelWidget;
|
||||
SignalTrimWidget *m_signalTrimWidget;
|
||||
|
||||
// 配置
|
||||
ConfigManager *m_configManager;
|
||||
|
||||
// 页面状态
|
||||
int m_currentPage;
|
||||
int m_totalPages;
|
||||
|
||||
// 滑动手势
|
||||
QPoint m_swipeStartPos;
|
||||
bool m_isSwiping;
|
||||
int m_swipeThreshold;
|
||||
};
|
||||
|
||||
#endif // MAINWINDOW_H
|
||||
479
procedure/functionregistry.cpp
Normal file
@@ -0,0 +1,479 @@
|
||||
#include "functionregistry.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QtMath>
|
||||
#include <QRandomGenerator>
|
||||
#include <QThread>
|
||||
|
||||
// =============================================================================
|
||||
// 單例實現
|
||||
// =============================================================================
|
||||
|
||||
FunctionRegistry *FunctionRegistry::m_instance = nullptr;
|
||||
|
||||
FunctionRegistry *FunctionRegistry::instance()
|
||||
{
|
||||
if (!m_instance)
|
||||
{
|
||||
m_instance = new FunctionRegistry();
|
||||
}
|
||||
return m_instance;
|
||||
}
|
||||
|
||||
FunctionRegistry::FunctionRegistry(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
registerBuiltinFunctions();
|
||||
}
|
||||
|
||||
FunctionRegistry::~FunctionRegistry()
|
||||
{
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 函數註冊
|
||||
// =============================================================================
|
||||
|
||||
bool FunctionRegistry::registerFunction(const QString &functionType,
|
||||
const QStringList ¶meterNames,
|
||||
FunctionCallback callback,
|
||||
const QString &displayName,
|
||||
const QString &description,
|
||||
const QString &category)
|
||||
{
|
||||
FunctionMetadata metadata;
|
||||
metadata.functionType = functionType;
|
||||
metadata.parameterNames = parameterNames;
|
||||
metadata.callback = callback;
|
||||
metadata.displayName = displayName.isEmpty() ? functionType : displayName;
|
||||
metadata.description = description;
|
||||
metadata.category = category;
|
||||
|
||||
return registerFunction(metadata);
|
||||
}
|
||||
|
||||
bool FunctionRegistry::registerFunction(const FunctionMetadata &metadata)
|
||||
{
|
||||
if (metadata.functionType.isEmpty())
|
||||
{
|
||||
qWarning() << "無法註冊空函數類型";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_functions.contains(metadata.functionType))
|
||||
{
|
||||
qWarning() << "函數已存在,將被覆蓋:" << metadata.functionType;
|
||||
}
|
||||
|
||||
m_functions[metadata.functionType] = metadata;
|
||||
|
||||
qDebug() << "註冊函數:" << metadata.functionType << "分類:" << metadata.category;
|
||||
|
||||
emit functionRegistered(metadata.functionType);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FunctionRegistry::unregisterFunction(const QString &functionType)
|
||||
{
|
||||
if (!m_functions.contains(functionType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
m_functions.remove(functionType);
|
||||
emit functionUnregistered(functionType);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FunctionRegistry::hasFunction(const QString &functionType) const
|
||||
{
|
||||
return m_functions.contains(functionType);
|
||||
}
|
||||
|
||||
QStringList FunctionRegistry::getAllFunctionTypes() const
|
||||
{
|
||||
return m_functions.keys();
|
||||
}
|
||||
|
||||
QStringList FunctionRegistry::getFunctionsByCategory(const QString &category) const
|
||||
{
|
||||
QStringList result;
|
||||
for (auto it = m_functions.begin(); it != m_functions.end(); ++it)
|
||||
{
|
||||
if (it.value().category == category)
|
||||
{
|
||||
result.append(it.key());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
FunctionRegistry::FunctionMetadata FunctionRegistry::getFunctionMetadata(const QString &functionType) const
|
||||
{
|
||||
if (m_functions.contains(functionType))
|
||||
{
|
||||
return m_functions[functionType];
|
||||
}
|
||||
return FunctionMetadata();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 參數處理
|
||||
// =============================================================================
|
||||
|
||||
QVariantMap FunctionRegistry::normalizeParameters(const FunctionMetadata &metadata,
|
||||
const QVariant ¶meters)
|
||||
{
|
||||
QVariantMap result;
|
||||
|
||||
if (parameters.type() == QVariant::Map)
|
||||
{
|
||||
result = parameters.toMap();
|
||||
}
|
||||
else if (parameters.type() == QVariant::List)
|
||||
{
|
||||
QVariantList list = parameters.toList();
|
||||
for (int i = 0; i < qMin(list.size(), metadata.parameterNames.size()); i++)
|
||||
{
|
||||
result[metadata.parameterNames[i]] = list[i];
|
||||
}
|
||||
}
|
||||
|
||||
// 應用默認值
|
||||
for (auto it = metadata.defaultParameters.begin(); it != metadata.defaultParameters.end(); ++it)
|
||||
{
|
||||
if (!result.contains(it.key()))
|
||||
{
|
||||
result[it.key()] = it.value();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool FunctionRegistry::validateParameters(const FunctionMetadata &metadata,
|
||||
const QVariantMap ¶ms)
|
||||
{
|
||||
Q_UNUSED(metadata)
|
||||
Q_UNUSED(params)
|
||||
// 簡單驗證 - 可以擴展
|
||||
return true;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 函數執行
|
||||
// =============================================================================
|
||||
|
||||
QVariantList FunctionRegistry::executeFunction(const QString &functionType,
|
||||
const QVariant ¶meters)
|
||||
{
|
||||
if (!m_functions.contains(functionType))
|
||||
{
|
||||
qWarning() << "找不到函數:" << functionType;
|
||||
emit functionExecuted(functionType, false);
|
||||
return QVariantList();
|
||||
}
|
||||
|
||||
const FunctionMetadata &metadata = m_functions[functionType];
|
||||
|
||||
QVariantMap params = normalizeParameters(metadata, parameters);
|
||||
|
||||
if (!validateParameters(metadata, params))
|
||||
{
|
||||
qWarning() << "參數驗證失敗:" << functionType;
|
||||
emit functionExecuted(functionType, false);
|
||||
return QVariantList();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
QVariantList result = metadata.callback(params);
|
||||
emit functionExecuted(functionType, true);
|
||||
return result;
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
qWarning() << "函數執行異常:" << functionType << "-" << e.what();
|
||||
emit functionExecuted(functionType, false);
|
||||
return QVariantList();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
qWarning() << "函數執行未知異常:" << functionType;
|
||||
emit functionExecuted(functionType, false);
|
||||
return QVariantList();
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 內建函數註冊
|
||||
// =============================================================================
|
||||
|
||||
void FunctionRegistry::registerBuiltinFunctions()
|
||||
{
|
||||
// --------------------------------------------------
|
||||
// 電阻測量函數
|
||||
// --------------------------------------------------
|
||||
registerFunction("RESISTANCE_4WIRE", {"channel", "range", "samples"}, [](const QVariantMap ¶ms) -> QVariantList
|
||||
{
|
||||
int channel = params.value("channel", 1).toInt();
|
||||
double range = params.value("range", 1000.0).toDouble();
|
||||
int samples = params.value("samples", 10).toInt();
|
||||
Q_UNUSED(samples)
|
||||
|
||||
qDebug() << "四線電阻測量: 通道=" << channel << "量程=" << range;
|
||||
|
||||
// 模擬數據
|
||||
double resistance = 100.0 + QRandomGenerator::global()->bounded(1.0);
|
||||
double temperature = (resistance - 100.0) / 0.385;
|
||||
|
||||
QVariantList result;
|
||||
result << resistance << temperature;
|
||||
return result; }, "四線電阻測量", "使用四線法測量電阻,消除導線電阻影響", "measurement");
|
||||
|
||||
registerFunction("RESISTANCE_2WIRE", {"channel", "range", "samples"}, [](const QVariantMap ¶ms) -> QVariantList
|
||||
{
|
||||
int channel = params.value("channel", 1).toInt();
|
||||
double range = params.value("range", 1000.0).toDouble();
|
||||
Q_UNUSED(range)
|
||||
|
||||
qDebug() << "二線電阻測量: 通道=" << channel;
|
||||
|
||||
double resistance = 100.0 + QRandomGenerator::global()->bounded(2.0);
|
||||
|
||||
QVariantList result;
|
||||
result << resistance;
|
||||
return result; }, "二線電阻測量", "使用二線法測量電阻", "measurement");
|
||||
|
||||
// --------------------------------------------------
|
||||
// 電壓測量函數
|
||||
// --------------------------------------------------
|
||||
registerFunction("VOLTAGE_V", {"channel", "range", "samples"}, [](const QVariantMap ¶ms) -> QVariantList
|
||||
{
|
||||
int channel = params.value("channel", 1).toInt();
|
||||
double range = params.value("range", 10.0).toDouble();
|
||||
Q_UNUSED(range)
|
||||
|
||||
qDebug() << "電壓測量: 通道=" << channel;
|
||||
|
||||
double voltage = 5.0 + QRandomGenerator::global()->bounded(0.1);
|
||||
|
||||
QVariantList result;
|
||||
result << voltage;
|
||||
return result; }, "電壓測量", "測量直流電壓", "measurement");
|
||||
|
||||
registerFunction("VOLTAGE_MV", {"channel", "range", "samples"}, [](const QVariantMap ¶ms) -> QVariantList
|
||||
{
|
||||
int channel = params.value("channel", 1).toInt();
|
||||
Q_UNUSED(channel)
|
||||
|
||||
double voltage_mv = 10.0 + QRandomGenerator::global()->bounded(1.0);
|
||||
|
||||
QVariantList result;
|
||||
result << voltage_mv;
|
||||
return result; }, "毫伏測量", "測量毫伏級電壓", "measurement");
|
||||
|
||||
// --------------------------------------------------
|
||||
// 電流測量函數
|
||||
// --------------------------------------------------
|
||||
registerFunction("CURRENT_MA", {"channel", "range", "samples"}, [](const QVariantMap ¶ms) -> QVariantList
|
||||
{
|
||||
int channel = params.value("channel", 1).toInt();
|
||||
Q_UNUSED(channel)
|
||||
|
||||
double current_ma = 12.0 + QRandomGenerator::global()->bounded(0.5);
|
||||
|
||||
QVariantList result;
|
||||
result << current_ma;
|
||||
return result; }, "毫安電流測量", "測量毫安級電流(如4-20mA信號)", "measurement");
|
||||
|
||||
registerFunction("CURRENT_A", {"channel", "range", "samples"}, [](const QVariantMap ¶ms) -> QVariantList
|
||||
{
|
||||
int channel = params.value("channel", 1).toInt();
|
||||
Q_UNUSED(channel)
|
||||
|
||||
double current_a = 1.0 + QRandomGenerator::global()->bounded(0.1);
|
||||
|
||||
QVariantList result;
|
||||
result << current_a;
|
||||
return result; }, "安培電流測量", "測量安培級電流", "measurement");
|
||||
|
||||
// --------------------------------------------------
|
||||
// 絕緣測量函數
|
||||
// --------------------------------------------------
|
||||
registerFunction("INSULATION", {"channel", "voltage", "duration"}, [](const QVariantMap ¶ms) -> QVariantList
|
||||
{
|
||||
double voltage = params.value("voltage", 500.0).toDouble();
|
||||
|
||||
double insulation_resistance = 500.0 + QRandomGenerator::global()->bounded(100.0);
|
||||
double leakage_current = voltage / (insulation_resistance * 1e6) * 1e6;
|
||||
|
||||
QVariantList result;
|
||||
result << insulation_resistance << leakage_current;
|
||||
return result; }, "絕緣電阻測量", "測量絕緣電阻和漏電流", "measurement");
|
||||
|
||||
// --------------------------------------------------
|
||||
// 溫度測量函數
|
||||
// --------------------------------------------------
|
||||
registerFunction("TEMPERATURE_RTD", {"channel", "rtd_type", "wire_mode"}, [](const QVariantMap ¶ms) -> QVariantList
|
||||
{
|
||||
QString rtdType = params.value("rtd_type", "PT100").toString();
|
||||
Q_UNUSED(rtdType)
|
||||
|
||||
double resistance = 100.0 + QRandomGenerator::global()->bounded(10.0);
|
||||
double temperature = (resistance - 100.0) / 0.385;
|
||||
|
||||
QVariantList result;
|
||||
result << temperature << resistance;
|
||||
return result; }, "RTD溫度測量", "使用RTD(如PT100)測量溫度", "measurement");
|
||||
|
||||
registerFunction("TEMPERATURE_TC", {"channel", "tc_type", "cjc_channel"}, [](const QVariantMap ¶ms) -> QVariantList
|
||||
{
|
||||
QString tcType = params.value("tc_type", "K").toString();
|
||||
Q_UNUSED(tcType)
|
||||
|
||||
double voltage_mv = 4.0 + QRandomGenerator::global()->bounded(1.0);
|
||||
double temperature = voltage_mv / 0.040;
|
||||
|
||||
QVariantList result;
|
||||
result << temperature << voltage_mv;
|
||||
return result; }, "熱電偶溫度測量", "使用熱電偶測量溫度", "measurement");
|
||||
|
||||
// --------------------------------------------------
|
||||
// 數據採集函數
|
||||
// --------------------------------------------------
|
||||
registerFunction("DATA_ACQUISITION", {"channels", "sample_rate", "duration"}, [](const QVariantMap ¶ms) -> QVariantList
|
||||
{
|
||||
QVariantList channels = params.value("channels").toList();
|
||||
double sampleRate = params.value("sample_rate", 1000.0).toDouble();
|
||||
double duration = params.value("duration", 1.0).toDouble();
|
||||
|
||||
int sampleCount = static_cast<int>(sampleRate * duration);
|
||||
QVariantList dataArray;
|
||||
QVariantList timestamps;
|
||||
|
||||
for (int i = 0; i < qMin(sampleCount, 100); i++) { // 限制最大樣本數
|
||||
QVariantList sample;
|
||||
for (int ch = 0; ch < channels.size(); ch++) {
|
||||
sample << QRandomGenerator::global()->bounded(10.0);
|
||||
}
|
||||
dataArray << QVariant(sample);
|
||||
timestamps << (i / sampleRate);
|
||||
}
|
||||
|
||||
QVariantList result;
|
||||
result << QVariant(dataArray) << QVariant(timestamps);
|
||||
return result; }, "數據採集", "多通道數據採集", "acquisition");
|
||||
|
||||
// --------------------------------------------------
|
||||
// 輸出控制函數
|
||||
// --------------------------------------------------
|
||||
registerFunction("SET_VOLTAGE", {"channel", "voltage"}, [](const QVariantMap ¶ms) -> QVariantList
|
||||
{
|
||||
int channel = params.value("channel", 1).toInt();
|
||||
double voltage = params.value("voltage", 0.0).toDouble();
|
||||
|
||||
qDebug() << "設置電壓輸出: 通道=" << channel << "電壓=" << voltage;
|
||||
|
||||
QVariantList result;
|
||||
result << true;
|
||||
return result; }, "設置電壓輸出", "設置指定通道的電壓輸出", "output");
|
||||
|
||||
registerFunction("SET_CURRENT", {"channel", "current_ma"}, [](const QVariantMap ¶ms) -> QVariantList
|
||||
{
|
||||
int channel = params.value("channel", 1).toInt();
|
||||
double current_ma = params.value("current_ma", 0.0).toDouble();
|
||||
|
||||
qDebug() << "設置電流輸出: 通道=" << channel << "電流=" << current_ma << "mA";
|
||||
|
||||
QVariantList result;
|
||||
result << true;
|
||||
return result; }, "設置電流輸出", "設置指定通道的電流輸出", "output");
|
||||
|
||||
registerFunction("SET_RESISTANCE", {"channel", "resistance"}, [](const QVariantMap ¶ms) -> QVariantList
|
||||
{
|
||||
int channel = params.value("channel", 1).toInt();
|
||||
double resistance = params.value("resistance", 100.0).toDouble();
|
||||
|
||||
qDebug() << "設置電阻輸出: 通道=" << channel << "電阻=" << resistance << "Ω";
|
||||
|
||||
QVariantList result;
|
||||
result << true;
|
||||
return result; }, "設置電阻輸出", "設置指定通道的模擬電阻輸出", "output");
|
||||
|
||||
// --------------------------------------------------
|
||||
// 計算函數
|
||||
// --------------------------------------------------
|
||||
registerFunction("CALCULATE_ERROR", {"measured", "standard"}, [](const QVariantMap ¶ms) -> QVariantList
|
||||
{
|
||||
double measured = params.value("measured", 0.0).toDouble();
|
||||
double standard = params.value("standard", 1.0).toDouble();
|
||||
|
||||
double error = measured - standard;
|
||||
double errorPercent = (standard != 0.0) ? (error / standard * 100.0) : 0.0;
|
||||
|
||||
QVariantList result;
|
||||
result << error << errorPercent;
|
||||
return result; }, "計算誤差", "計算測量值與標準值的誤差", "calculation");
|
||||
|
||||
registerFunction("CALCULATE_AVERAGE", {"values"}, [](const QVariantMap ¶ms) -> QVariantList
|
||||
{
|
||||
QVariantList values = params.value("values").toList();
|
||||
|
||||
if (values.isEmpty()) {
|
||||
QVariantList result;
|
||||
result << 0.0 << 0.0 << 0.0 << 0.0;
|
||||
return result;
|
||||
}
|
||||
|
||||
double sum = 0.0;
|
||||
double minVal = values[0].toDouble();
|
||||
double maxVal = values[0].toDouble();
|
||||
|
||||
for (const QVariant &v : values) {
|
||||
double val = v.toDouble();
|
||||
sum += val;
|
||||
minVal = qMin(minVal, val);
|
||||
maxVal = qMax(maxVal, val);
|
||||
}
|
||||
|
||||
double average = sum / values.size();
|
||||
|
||||
double sumSquares = 0.0;
|
||||
for (const QVariant &v : values) {
|
||||
double diff = v.toDouble() - average;
|
||||
sumSquares += diff * diff;
|
||||
}
|
||||
double stdDev = qSqrt(sumSquares / values.size());
|
||||
|
||||
QVariantList result;
|
||||
result << average << stdDev << minVal << maxVal;
|
||||
return result; }, "計算平均值", "計算數值的平均值、標準差、最小值和最大值", "calculation");
|
||||
|
||||
// --------------------------------------------------
|
||||
// 控制函數
|
||||
// --------------------------------------------------
|
||||
registerFunction("DELAY", {"duration_ms"}, [](const QVariantMap ¶ms) -> QVariantList
|
||||
{
|
||||
int duration_ms = params.value("duration_ms", 1000).toInt();
|
||||
|
||||
qDebug() << "延遲:" << duration_ms << "ms";
|
||||
QThread::msleep(duration_ms);
|
||||
|
||||
QVariantList result;
|
||||
result << true;
|
||||
return result; }, "延遲", "等待指定時間", "control");
|
||||
|
||||
registerFunction("WAIT_STABLE", {"channel", "threshold", "timeout"}, [](const QVariantMap ¶ms) -> QVariantList
|
||||
{
|
||||
int timeout = params.value("timeout", 30000).toInt();
|
||||
|
||||
QThread::msleep(qMin(timeout, 2000));
|
||||
double finalValue = 100.0 + QRandomGenerator::global()->bounded(0.05);
|
||||
|
||||
QVariantList result;
|
||||
result << true << finalValue;
|
||||
return result; }, "等待穩定", "等待測量值穩定在閾值範圍內", "control");
|
||||
|
||||
qDebug() << "已註冊" << m_functions.size() << "個內建函數";
|
||||
}
|
||||
84
procedure/functionregistry.h
Normal file
@@ -0,0 +1,84 @@
|
||||
#ifndef FUNCTIONREGISTRY_H
|
||||
#define FUNCTIONREGISTRY_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVariant>
|
||||
#include <QVariantMap>
|
||||
#include <QVariantList>
|
||||
#include <functional>
|
||||
|
||||
/**
|
||||
* @brief 功能函數註冊表
|
||||
*
|
||||
* 管理測量和輸出功能的註冊和執行。
|
||||
* 每個功能函數接受參數映射,返回結果數組。
|
||||
*/
|
||||
class FunctionRegistry : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// 函數回調類型:接受 QVariantMap 參數,返回 QVariantList 結果
|
||||
using FunctionCallback = std::function<QVariantList(const QVariantMap &)>;
|
||||
|
||||
struct FunctionMetadata
|
||||
{
|
||||
QString functionType; // 函數類型標識
|
||||
QString displayName; // 顯示名稱
|
||||
QString description; // 功能描述
|
||||
QString category; // 類別 (measurement, output, etc.)
|
||||
QStringList parameterNames; // 參數名稱列表
|
||||
QVariantMap defaultParameters; // 默認參數值
|
||||
FunctionCallback callback; // 回調函數
|
||||
};
|
||||
|
||||
static FunctionRegistry *instance();
|
||||
|
||||
// 註冊功能函數
|
||||
bool registerFunction(const QString &functionType,
|
||||
const QStringList ¶meterNames,
|
||||
FunctionCallback callback,
|
||||
const QString &displayName = QString(),
|
||||
const QString &description = QString(),
|
||||
const QString &category = QString());
|
||||
|
||||
bool registerFunction(const FunctionMetadata &metadata);
|
||||
|
||||
// 註銷功能函數
|
||||
bool unregisterFunction(const QString &functionType);
|
||||
|
||||
// 執行功能函數
|
||||
QVariantList executeFunction(const QString &functionType,
|
||||
const QVariant ¶meters);
|
||||
|
||||
// 查詢
|
||||
bool hasFunction(const QString &functionType) const;
|
||||
FunctionMetadata getFunctionMetadata(const QString &functionType) const;
|
||||
QStringList getAllFunctionTypes() const;
|
||||
QStringList getFunctionsByCategory(const QString &category) const;
|
||||
|
||||
signals:
|
||||
void functionRegistered(const QString &functionType);
|
||||
void functionUnregistered(const QString &functionType);
|
||||
void functionExecuted(const QString &functionType, bool success);
|
||||
void functionProgress(const QString &functionType, int progress, const QString &message);
|
||||
|
||||
private:
|
||||
explicit FunctionRegistry(QObject *parent = nullptr);
|
||||
~FunctionRegistry();
|
||||
|
||||
QVariantMap normalizeParameters(const FunctionMetadata &metadata,
|
||||
const QVariant ¶meters);
|
||||
bool validateParameters(const FunctionMetadata &metadata,
|
||||
const QVariantMap ¶ms);
|
||||
|
||||
void registerBuiltinFunctions();
|
||||
|
||||
QMap<QString, FunctionMetadata> m_functions;
|
||||
static FunctionRegistry *m_instance;
|
||||
};
|
||||
|
||||
#endif // FUNCTIONREGISTRY_H
|
||||
319
procedure/proceduredata.h
Normal file
@@ -0,0 +1,319 @@
|
||||
#ifndef PROCEDUREDATA_H
|
||||
#define PROCEDUREDATA_H
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
#include <QList>
|
||||
#include <QVariantMap>
|
||||
#include <QDateTime>
|
||||
#include <QPair>
|
||||
|
||||
// =====================================================
|
||||
// 基本枚舉類型
|
||||
// =====================================================
|
||||
|
||||
// 步驟類型
|
||||
enum class StepType
|
||||
{
|
||||
Manual, // 手動步驟
|
||||
Automatic // 自動步驟
|
||||
};
|
||||
|
||||
// 步驟狀態
|
||||
enum class StepStatus
|
||||
{
|
||||
Pending, // 待執行
|
||||
InProgress, // 執行中
|
||||
Passed, // 已通過
|
||||
Failed, // 未通過
|
||||
Skipped, // 已跳過
|
||||
Confirmed // 已確認(手動)
|
||||
};
|
||||
|
||||
// 活動類型
|
||||
enum class ActivityType
|
||||
{
|
||||
TEST_TASK_GROUP, // 測試任務組
|
||||
RESULT_DISPLAY // 結果顯示
|
||||
};
|
||||
|
||||
// 活動步驟
|
||||
struct ActivityStep
|
||||
{
|
||||
ActivityType type = ActivityType::TEST_TASK_GROUP;
|
||||
QString ref; // 引用ID
|
||||
QString name; // 顯示名稱
|
||||
};
|
||||
|
||||
// =====================================================
|
||||
// 表格相關結構
|
||||
// =====================================================
|
||||
|
||||
// 字段定義
|
||||
struct FieldDefinition
|
||||
{
|
||||
QString id;
|
||||
QString name;
|
||||
QString label;
|
||||
QString type; // numeric, text, selection, boolean, datetime, calculated
|
||||
QVariant defaultValue;
|
||||
QStringList options; // 用於 selection 類型
|
||||
QString formula; // 用於 calculated 類型
|
||||
QVariantMap validationRules;
|
||||
bool isRequired = false;
|
||||
bool isReadOnly = false;
|
||||
bool editable = false;
|
||||
bool visible = true;
|
||||
QString unit;
|
||||
QString description;
|
||||
QString alignment;
|
||||
int width = 100;
|
||||
int decimals = 2;
|
||||
bool uploadImmediately = false;
|
||||
QVariantMap layoutConfig;
|
||||
};
|
||||
|
||||
// 靜態單元格
|
||||
struct StaticCell
|
||||
{
|
||||
int row = 0;
|
||||
int column = 0;
|
||||
QString field; // 字段ID(用於form類型表格)
|
||||
QVariant value; // 靜態值
|
||||
QString content; // 靜態文本內容
|
||||
QString style;
|
||||
};
|
||||
|
||||
// 表格定義
|
||||
struct TableDefinition
|
||||
{
|
||||
QString tableId;
|
||||
QString id;
|
||||
QString name;
|
||||
QString description;
|
||||
QString tableType; // grid, form, series
|
||||
int rowCount = 0;
|
||||
int columnCount = 0;
|
||||
QVariantMap layoutConfig;
|
||||
bool isShared = false;
|
||||
QString uploadStrategy; // immediate, grouped, byRow, onComplete
|
||||
QList<QVariantMap> uploadGroups;
|
||||
QList<FieldDefinition> columnHeaders; // 用於grid類型表格
|
||||
QList<QPair<QString, QString>> rowHeaders; // 用於grid類型表格 (id, name)
|
||||
QList<FieldDefinition> fields; // 用於form和series類型表格
|
||||
QList<StaticCell> staticCells; // 靜態內容
|
||||
};
|
||||
|
||||
// =====================================================
|
||||
// 測試動作相關結構
|
||||
// =====================================================
|
||||
|
||||
// 字段選擇器
|
||||
struct FieldSelector
|
||||
{
|
||||
QString tableRef;
|
||||
int row = -1;
|
||||
int column = -1;
|
||||
QString fieldName;
|
||||
QStringList fields; // 用於form類型表格
|
||||
QList<QPair<QString, QString>> cells; // 用於grid類型表格 (row, column)
|
||||
bool ignore = false;
|
||||
};
|
||||
|
||||
// 測試動作
|
||||
struct TestAction
|
||||
{
|
||||
QString actionId;
|
||||
QString id;
|
||||
QString name;
|
||||
QString actionType;
|
||||
QString functionName;
|
||||
QString document; // HTML格式的說明文檔
|
||||
QString mode; // manual 或 auto
|
||||
QString sequence;
|
||||
QString functionType; // RESISTANCE_4WIRE, VOLTAGE_V, DATA_ACQUISITION 等
|
||||
QString channel; // input1, input2, output1, output2
|
||||
QVariantMap params; // 函數參數
|
||||
QVariant functionParameters;
|
||||
QVariantMap metadata;
|
||||
QMap<QString, FieldSelector> inputs;
|
||||
QMap<QString, FieldSelector> outputs;
|
||||
QList<FieldSelector> dataFields;
|
||||
QVariantMap validationCriteria;
|
||||
QString uploadStrategy;
|
||||
QStringList uploadFields;
|
||||
int timeout = 30000;
|
||||
int retryCount = 0;
|
||||
|
||||
// 運行時狀態
|
||||
bool isCompleted = false;
|
||||
bool isWaitingConfirm = false;
|
||||
QVariantList results;
|
||||
};
|
||||
|
||||
// 測試活動組(階段)
|
||||
struct TestActivityGroup
|
||||
{
|
||||
QString groupId;
|
||||
QString id;
|
||||
QString name;
|
||||
QString description;
|
||||
QString document;
|
||||
QString skipCondition;
|
||||
QVariantMap metadata;
|
||||
QStringList tableRefs;
|
||||
QList<TestAction> actions;
|
||||
|
||||
// 運行時狀態
|
||||
bool isCompleted = false;
|
||||
bool isActionsParsed = false;
|
||||
};
|
||||
|
||||
// 測試任務組
|
||||
struct TestTaskGroup
|
||||
{
|
||||
QString taskGroupId;
|
||||
QString id;
|
||||
QString name;
|
||||
QString description;
|
||||
QString executionMode;
|
||||
int loopCount = 1;
|
||||
QVariantMap metadata;
|
||||
QStringList tableRefs;
|
||||
QList<TestActivityGroup> stages;
|
||||
|
||||
// 運行時狀態
|
||||
bool isCompleted = false;
|
||||
bool isParsed = false;
|
||||
};
|
||||
|
||||
// 結果顯示
|
||||
struct ResultDisplay
|
||||
{
|
||||
QString displayId;
|
||||
QString id;
|
||||
QString name;
|
||||
QString displayType;
|
||||
QString document;
|
||||
QString tableRef;
|
||||
QVariantMap passCondition;
|
||||
QStringList tableRefs;
|
||||
};
|
||||
|
||||
// =====================================================
|
||||
// 程序配置結構
|
||||
// =====================================================
|
||||
|
||||
struct ProcedureConfig
|
||||
{
|
||||
QString procedureId;
|
||||
QString procedureName;
|
||||
QString version;
|
||||
QString description;
|
||||
QString author;
|
||||
QString document;
|
||||
QVariantMap metadata;
|
||||
|
||||
// 活動序列 (旧格式,用于 ProcedureEngine)
|
||||
QList<ActivityStep> activitySequence;
|
||||
|
||||
// 活動序列 (新格式,stores QVariant of TestTaskGroup or ResultDisplay,用于新解析器)
|
||||
QList<QVariant> activityVariants;
|
||||
|
||||
// 定義存儲
|
||||
QMap<QString, TestTaskGroup> testTaskGroups;
|
||||
QMap<QString, TestActivityGroup> testActivityGroups;
|
||||
QMap<QString, TestAction> testActions;
|
||||
QMap<QString, ResultDisplay> resultDisplays;
|
||||
QMap<QString, TableDefinition> tables;
|
||||
|
||||
// 舊版兼容
|
||||
QList<TestTaskGroup> taskGroups;
|
||||
};
|
||||
|
||||
// =====================================================
|
||||
// 舊版兼容結構(用於ProcedureManager)
|
||||
// =====================================================
|
||||
|
||||
// 表格字段定義(舊版)
|
||||
struct TableField
|
||||
{
|
||||
QString id;
|
||||
QString name;
|
||||
QString type; // numeric, text, boolean
|
||||
QString unit;
|
||||
bool isRequired = false;
|
||||
bool isHighlighted = false;
|
||||
};
|
||||
|
||||
// 表格數據(舊版)
|
||||
struct TableData
|
||||
{
|
||||
QString id;
|
||||
QString name;
|
||||
QString description;
|
||||
QVector<TableField> columns;
|
||||
QVector<QVariantMap> rows;
|
||||
};
|
||||
|
||||
// 步驟定義(舊版)
|
||||
struct StepData
|
||||
{
|
||||
QString id;
|
||||
QString content;
|
||||
StepType type = StepType::Manual;
|
||||
StepStatus status = StepStatus::Pending;
|
||||
QStringList tableRefs;
|
||||
QStringList highlightFields;
|
||||
QString result;
|
||||
QDateTime executedAt;
|
||||
QString executedBy;
|
||||
QString cancelReason;
|
||||
};
|
||||
|
||||
// 任務組定義(舊版)
|
||||
struct TaskGroup
|
||||
{
|
||||
QString id;
|
||||
QString name;
|
||||
QVector<StepData> steps;
|
||||
};
|
||||
|
||||
// 程序定義(舊版)
|
||||
struct ProcedureData
|
||||
{
|
||||
QString id;
|
||||
QString name;
|
||||
QString version;
|
||||
QString description;
|
||||
QVector<TaskGroup> taskGroups;
|
||||
QVector<TableData> tables;
|
||||
QString workOrderId;
|
||||
int completedSteps = 0;
|
||||
int totalSteps = 0;
|
||||
};
|
||||
|
||||
// 程序摘要(用於列表顯示)
|
||||
struct ProcedureSummary
|
||||
{
|
||||
QString id;
|
||||
QString name;
|
||||
QString version;
|
||||
QString description;
|
||||
QString filePath;
|
||||
};
|
||||
|
||||
// Qt Meta-Type Declarations for QVariant support
|
||||
Q_DECLARE_METATYPE(FieldDefinition)
|
||||
Q_DECLARE_METATYPE(StaticCell)
|
||||
Q_DECLARE_METATYPE(TableDefinition)
|
||||
Q_DECLARE_METATYPE(FieldSelector)
|
||||
Q_DECLARE_METATYPE(TestAction)
|
||||
Q_DECLARE_METATYPE(TestActivityGroup)
|
||||
Q_DECLARE_METATYPE(TestTaskGroup)
|
||||
Q_DECLARE_METATYPE(ResultDisplay)
|
||||
Q_DECLARE_METATYPE(ProcedureConfig)
|
||||
Q_DECLARE_METATYPE(ActivityStep)
|
||||
|
||||
#endif // PROCEDUREDATA_H
|
||||
865
procedure/procedureengine.cpp
Normal file
@@ -0,0 +1,865 @@
|
||||
#include "procedureengine.h"
|
||||
#include "procedureparser.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QTimer>
|
||||
#include <QRegularExpression>
|
||||
|
||||
// =============================================================================
|
||||
// 單例實現
|
||||
// =============================================================================
|
||||
|
||||
ProcedureEngine *ProcedureEngine::s_instance = nullptr;
|
||||
|
||||
ProcedureEngine *ProcedureEngine::instance()
|
||||
{
|
||||
if (!s_instance)
|
||||
{
|
||||
s_instance = new ProcedureEngine();
|
||||
}
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
ProcedureEngine::ProcedureEngine(QObject *parent)
|
||||
: QObject(parent), m_parser(new ProcedureParser(this)), m_status(ProcedureStatus::Idle), m_executionTimer(new QTimer(this))
|
||||
{
|
||||
connect(m_executionTimer, &QTimer::timeout, this, &ProcedureEngine::onExecutionTimeout);
|
||||
}
|
||||
|
||||
ProcedureEngine::~ProcedureEngine()
|
||||
{
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 配置載入
|
||||
// =============================================================================
|
||||
|
||||
bool ProcedureEngine::loadProcedure(const QString &configPath)
|
||||
{
|
||||
if (m_status == ProcedureStatus::Running || m_status == ProcedureStatus::Paused)
|
||||
{
|
||||
qWarning() << "程序正在執行中,無法載入新配置";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 檢查文件是否存在
|
||||
if (!QFile::exists(configPath))
|
||||
{
|
||||
emit errorOccurred(QString("配置文件不存在: %1").arg(configPath));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 解析配置
|
||||
if (!m_parser->loadConfig(configPath))
|
||||
{
|
||||
emit errorOccurred(QString("配置文件解析失敗: %1").arg(configPath));
|
||||
return false;
|
||||
}
|
||||
|
||||
m_config = m_parser->parseProcedureConfig();
|
||||
|
||||
// 驗證配置
|
||||
if (!m_parser->validateConfig())
|
||||
{
|
||||
QStringList errors = m_parser->getValidationErrors();
|
||||
emit errorOccurred(QString("配置驗證失敗:\n%1").arg(errors.join("\n")));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 初始化執行上下文
|
||||
resetExecutionContext();
|
||||
m_configPath = configPath;
|
||||
|
||||
setStatus(ProcedureStatus::Loaded);
|
||||
emit procedureLoaded(m_config.procedureName);
|
||||
|
||||
qDebug() << "程序載入成功:" << m_config.procedureName;
|
||||
qDebug() << " - 任務組數量:" << m_config.testTaskGroups.size();
|
||||
qDebug() << " - 活動序列數量:" << m_config.activitySequence.size();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProcedureEngine::resetExecutionContext()
|
||||
{
|
||||
m_context = ExecutionContext();
|
||||
m_context.currentActivityIndex = -1;
|
||||
m_context.currentStageIndex = -1;
|
||||
m_context.currentActionIndex = -1;
|
||||
m_context.loopIteration = 0;
|
||||
m_context.startTime = QDateTime();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 程序控制
|
||||
// =============================================================================
|
||||
|
||||
void ProcedureEngine::startProcedure()
|
||||
{
|
||||
if (m_status != ProcedureStatus::Loaded && m_status != ProcedureStatus::Completed)
|
||||
{
|
||||
qWarning() << "程序狀態不正確,無法啟動";
|
||||
return;
|
||||
}
|
||||
|
||||
resetExecutionContext();
|
||||
m_context.startTime = QDateTime::currentDateTime();
|
||||
|
||||
setStatus(ProcedureStatus::Running);
|
||||
emit procedureStarted();
|
||||
|
||||
// 移動到第一個活動
|
||||
moveToFirstActivity();
|
||||
}
|
||||
|
||||
void ProcedureEngine::pauseProcedure()
|
||||
{
|
||||
if (m_status != ProcedureStatus::Running)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_executionTimer->stop();
|
||||
setStatus(ProcedureStatus::Paused);
|
||||
emit procedurePaused();
|
||||
}
|
||||
|
||||
void ProcedureEngine::resumeProcedure()
|
||||
{
|
||||
if (m_status != ProcedureStatus::Paused)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(ProcedureStatus::Running);
|
||||
emit procedureResumed();
|
||||
|
||||
// 繼續執行當前動作
|
||||
executeCurrentAction();
|
||||
}
|
||||
|
||||
void ProcedureEngine::stopProcedure()
|
||||
{
|
||||
if (m_status == ProcedureStatus::Idle || m_status == ProcedureStatus::Loaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_executionTimer->stop();
|
||||
setStatus(ProcedureStatus::Stopped);
|
||||
emit procedureStopped();
|
||||
}
|
||||
|
||||
void ProcedureEngine::resetProcedure()
|
||||
{
|
||||
stopProcedure();
|
||||
resetExecutionContext();
|
||||
setStatus(ProcedureStatus::Loaded);
|
||||
|
||||
// 清除所有表格數據
|
||||
m_tableData.clear();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 活動導航
|
||||
// =============================================================================
|
||||
|
||||
void ProcedureEngine::moveToFirstActivity()
|
||||
{
|
||||
if (m_config.activitySequence.isEmpty())
|
||||
{
|
||||
completeProcedure();
|
||||
return;
|
||||
}
|
||||
|
||||
m_context.currentActivityIndex = 0;
|
||||
m_context.currentStageIndex = -1;
|
||||
m_context.currentActionIndex = -1;
|
||||
|
||||
enterCurrentActivity();
|
||||
}
|
||||
|
||||
bool ProcedureEngine::moveToNextActivity()
|
||||
{
|
||||
if (m_status != ProcedureStatus::Running)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int nextIndex = m_context.currentActivityIndex + 1;
|
||||
|
||||
if (nextIndex >= m_config.activitySequence.size())
|
||||
{
|
||||
// 沒有更多活動
|
||||
completeProcedure();
|
||||
return false;
|
||||
}
|
||||
|
||||
m_context.currentActivityIndex = nextIndex;
|
||||
m_context.currentStageIndex = -1;
|
||||
m_context.currentActionIndex = -1;
|
||||
|
||||
enterCurrentActivity();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProcedureEngine::moveToPreviousActivity()
|
||||
{
|
||||
if (m_status != ProcedureStatus::Running && m_status != ProcedureStatus::Paused)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int prevIndex = m_context.currentActivityIndex - 1;
|
||||
|
||||
if (prevIndex < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
m_context.currentActivityIndex = prevIndex;
|
||||
m_context.currentStageIndex = -1;
|
||||
m_context.currentActionIndex = -1;
|
||||
|
||||
enterCurrentActivity();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProcedureEngine::jumpToActivity(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_config.activitySequence.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
m_context.currentActivityIndex = index;
|
||||
m_context.currentStageIndex = -1;
|
||||
m_context.currentActionIndex = -1;
|
||||
|
||||
enterCurrentActivity();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProcedureEngine::enterCurrentActivity()
|
||||
{
|
||||
if (m_context.currentActivityIndex < 0 ||
|
||||
m_context.currentActivityIndex >= m_config.activitySequence.size())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const ActivityStep &step = m_config.activitySequence[m_context.currentActivityIndex];
|
||||
|
||||
qDebug() << "進入活動:" << step.name << "(" << step.ref << ")";
|
||||
|
||||
emit activityStarted(m_context.currentActivityIndex, step);
|
||||
|
||||
if (step.type == ActivityType::TEST_TASK_GROUP)
|
||||
{
|
||||
// 開始執行任務組
|
||||
startTaskGroupExecution(step.ref);
|
||||
}
|
||||
else if (step.type == ActivityType::RESULT_DISPLAY)
|
||||
{
|
||||
// 顯示結果
|
||||
displayResult(step.ref);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 任務組執行
|
||||
// =============================================================================
|
||||
|
||||
void ProcedureEngine::startTaskGroupExecution(const QString &taskGroupRef)
|
||||
{
|
||||
if (!m_config.testTaskGroups.contains(taskGroupRef))
|
||||
{
|
||||
emit errorOccurred(QString("找不到任務組: %1").arg(taskGroupRef));
|
||||
return;
|
||||
}
|
||||
|
||||
const TestTaskGroup &taskGroup = m_config.testTaskGroups[taskGroupRef];
|
||||
|
||||
qDebug() << "開始執行任務組:" << taskGroup.name;
|
||||
qDebug() << " - 階段數量:" << taskGroup.stages.size();
|
||||
|
||||
emit taskGroupStarted(taskGroupRef, taskGroup.name);
|
||||
|
||||
// 初始化表格
|
||||
for (const QString &tableRef : taskGroup.tableRefs)
|
||||
{
|
||||
initializeTable(tableRef);
|
||||
}
|
||||
|
||||
// 移動到第一個階段
|
||||
if (!taskGroup.stages.isEmpty())
|
||||
{
|
||||
m_context.currentStageIndex = 0;
|
||||
m_context.currentActionIndex = -1;
|
||||
enterCurrentStage(taskGroupRef);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 沒有階段,完成任務組
|
||||
completeTaskGroup(taskGroupRef);
|
||||
}
|
||||
}
|
||||
|
||||
void ProcedureEngine::enterCurrentStage(const QString &taskGroupRef)
|
||||
{
|
||||
if (!m_config.testTaskGroups.contains(taskGroupRef))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const TestTaskGroup &taskGroup = m_config.testTaskGroups[taskGroupRef];
|
||||
|
||||
if (m_context.currentStageIndex < 0 ||
|
||||
m_context.currentStageIndex >= taskGroup.stages.size())
|
||||
{
|
||||
completeTaskGroup(taskGroupRef);
|
||||
return;
|
||||
}
|
||||
|
||||
const TestActivityGroup &stage = taskGroup.stages[m_context.currentStageIndex];
|
||||
|
||||
qDebug() << "進入階段:" << stage.name;
|
||||
|
||||
emit stageStarted(m_context.currentStageIndex, stage.name);
|
||||
|
||||
// 檢查跳過條件
|
||||
if (!stage.skipCondition.isEmpty() && evaluateCondition(stage.skipCondition))
|
||||
{
|
||||
qDebug() << "階段被跳過:" << stage.name;
|
||||
moveToNextStage(taskGroupRef);
|
||||
return;
|
||||
}
|
||||
|
||||
// 移動到第一個動作
|
||||
if (!stage.actions.isEmpty())
|
||||
{
|
||||
m_context.currentActionIndex = 0;
|
||||
executeCurrentAction();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 沒有動作,移動到下一個階段
|
||||
moveToNextStage(taskGroupRef);
|
||||
}
|
||||
}
|
||||
|
||||
void ProcedureEngine::moveToNextStage(const QString &taskGroupRef)
|
||||
{
|
||||
if (!m_config.testTaskGroups.contains(taskGroupRef))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const TestTaskGroup &taskGroup = m_config.testTaskGroups[taskGroupRef];
|
||||
|
||||
int nextStageIndex = m_context.currentStageIndex + 1;
|
||||
|
||||
if (nextStageIndex >= taskGroup.stages.size())
|
||||
{
|
||||
// 檢查循環
|
||||
m_context.loopIteration++;
|
||||
if (m_context.loopIteration < taskGroup.loopCount)
|
||||
{
|
||||
// 繼續下一輪
|
||||
m_context.currentStageIndex = 0;
|
||||
m_context.currentActionIndex = -1;
|
||||
enterCurrentStage(taskGroupRef);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 完成任務組
|
||||
completeTaskGroup(taskGroupRef);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
m_context.currentStageIndex = nextStageIndex;
|
||||
m_context.currentActionIndex = -1;
|
||||
enterCurrentStage(taskGroupRef);
|
||||
}
|
||||
|
||||
void ProcedureEngine::completeTaskGroup(const QString &taskGroupRef)
|
||||
{
|
||||
qDebug() << "任務組完成:" << taskGroupRef;
|
||||
|
||||
m_context.loopIteration = 0;
|
||||
emit taskGroupCompleted(taskGroupRef);
|
||||
|
||||
// 自動移動到下一個活動(如果處於運行狀態)
|
||||
if (m_status == ProcedureStatus::Running)
|
||||
{
|
||||
// 使用延遲以允許UI更新
|
||||
QTimer::singleShot(100, this, [this]()
|
||||
{ moveToNextActivity(); });
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 動作執行
|
||||
// =============================================================================
|
||||
|
||||
void ProcedureEngine::executeCurrentAction()
|
||||
{
|
||||
if (m_status != ProcedureStatus::Running)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const ActivityStep &step = m_config.activitySequence[m_context.currentActivityIndex];
|
||||
if (step.type != ActivityType::TEST_TASK_GROUP)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_config.testTaskGroups.contains(step.ref))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const TestTaskGroup &taskGroup = m_config.testTaskGroups[step.ref];
|
||||
|
||||
if (m_context.currentStageIndex < 0 ||
|
||||
m_context.currentStageIndex >= taskGroup.stages.size())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const TestActivityGroup &stage = taskGroup.stages[m_context.currentStageIndex];
|
||||
|
||||
if (m_context.currentActionIndex < 0 ||
|
||||
m_context.currentActionIndex >= stage.actions.size())
|
||||
{
|
||||
// 階段動作執行完畢,移動到下一個階段
|
||||
emit stageCompleted(m_context.currentStageIndex);
|
||||
moveToNextStage(step.ref);
|
||||
return;
|
||||
}
|
||||
|
||||
const TestAction &action = stage.actions[m_context.currentActionIndex];
|
||||
|
||||
qDebug() << "執行動作:" << action.actionId << "-" << action.functionName;
|
||||
|
||||
emit actionStarted(m_context.currentActionIndex, action.actionId);
|
||||
|
||||
// 設置超時
|
||||
if (action.timeout > 0)
|
||||
{
|
||||
m_executionTimer->start(action.timeout);
|
||||
}
|
||||
|
||||
// 請求執行動作(通過信號通知外部執行)
|
||||
emit executeActionRequested(action);
|
||||
}
|
||||
|
||||
void ProcedureEngine::onActionCompleted(const QString &actionId, const QVariantMap &results)
|
||||
{
|
||||
m_executionTimer->stop();
|
||||
|
||||
qDebug() << "動作完成:" << actionId;
|
||||
|
||||
emit actionCompleted(m_context.currentActionIndex, actionId, results);
|
||||
|
||||
// 處理結果(寫入表格等)
|
||||
processActionResults(actionId, results);
|
||||
|
||||
// 移動到下一個動作
|
||||
moveToNextAction();
|
||||
}
|
||||
|
||||
void ProcedureEngine::onActionFailed(const QString &actionId, const QString &error)
|
||||
{
|
||||
m_executionTimer->stop();
|
||||
|
||||
qWarning() << "動作執行失敗:" << actionId << "-" << error;
|
||||
|
||||
emit actionFailed(m_context.currentActionIndex, actionId, error);
|
||||
|
||||
// 根據配置決定是否繼續
|
||||
// TODO: 可配置的錯誤處理策略
|
||||
moveToNextAction();
|
||||
}
|
||||
|
||||
void ProcedureEngine::moveToNextAction()
|
||||
{
|
||||
if (m_status != ProcedureStatus::Running)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_context.currentActionIndex++;
|
||||
executeCurrentAction();
|
||||
}
|
||||
|
||||
void ProcedureEngine::onExecutionTimeout()
|
||||
{
|
||||
qWarning() << "動作執行超時";
|
||||
|
||||
const ActivityStep &step = m_config.activitySequence[m_context.currentActivityIndex];
|
||||
if (step.type == ActivityType::TEST_TASK_GROUP &&
|
||||
m_config.testTaskGroups.contains(step.ref))
|
||||
{
|
||||
|
||||
const TestTaskGroup &taskGroup = m_config.testTaskGroups[step.ref];
|
||||
if (m_context.currentStageIndex >= 0 &&
|
||||
m_context.currentStageIndex < taskGroup.stages.size())
|
||||
{
|
||||
|
||||
const TestActivityGroup &stage = taskGroup.stages[m_context.currentStageIndex];
|
||||
if (m_context.currentActionIndex >= 0 &&
|
||||
m_context.currentActionIndex < stage.actions.size())
|
||||
{
|
||||
|
||||
const TestAction &action = stage.actions[m_context.currentActionIndex];
|
||||
onActionFailed(action.actionId, "執行超時");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果無法確定當前動作,直接移動到下一個
|
||||
moveToNextAction();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 結果處理
|
||||
// =============================================================================
|
||||
|
||||
void ProcedureEngine::processActionResults(const QString &actionId, const QVariantMap &results)
|
||||
{
|
||||
// 獲取當前動作定義
|
||||
const ActivityStep &step = m_config.activitySequence[m_context.currentActivityIndex];
|
||||
if (step.type != ActivityType::TEST_TASK_GROUP)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_config.testTaskGroups.contains(step.ref))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const TestTaskGroup &taskGroup = m_config.testTaskGroups[step.ref];
|
||||
if (m_context.currentStageIndex < 0 ||
|
||||
m_context.currentStageIndex >= taskGroup.stages.size())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const TestActivityGroup &stage = taskGroup.stages[m_context.currentStageIndex];
|
||||
if (m_context.currentActionIndex < 0 ||
|
||||
m_context.currentActionIndex >= stage.actions.size())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const TestAction &action = stage.actions[m_context.currentActionIndex];
|
||||
|
||||
// 根據輸出映射寫入表格
|
||||
for (auto it = action.outputs.begin(); it != action.outputs.end(); ++it)
|
||||
{
|
||||
const QString &outputKey = it.key();
|
||||
const FieldSelector &selector = it.value();
|
||||
|
||||
if (results.contains(outputKey))
|
||||
{
|
||||
QVariant value = results[outputKey];
|
||||
setTableCellValue(selector.tableRef, selector.row, selector.column, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProcedureEngine::displayResult(const QString &displayRef)
|
||||
{
|
||||
if (!m_config.resultDisplays.contains(displayRef))
|
||||
{
|
||||
emit errorOccurred(QString("找不到結果顯示: %1").arg(displayRef));
|
||||
return;
|
||||
}
|
||||
|
||||
const ResultDisplay &display = m_config.resultDisplays[displayRef];
|
||||
|
||||
qDebug() << "顯示結果:" << display.name;
|
||||
|
||||
emit resultDisplayRequested(displayRef, display);
|
||||
}
|
||||
|
||||
void ProcedureEngine::completeProcedure()
|
||||
{
|
||||
qDebug() << "程序執行完成";
|
||||
|
||||
setStatus(ProcedureStatus::Completed);
|
||||
emit procedureCompleted();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 表格數據管理
|
||||
// =============================================================================
|
||||
|
||||
void ProcedureEngine::initializeTable(const QString &tableRef)
|
||||
{
|
||||
if (!m_config.tables.contains(tableRef))
|
||||
{
|
||||
qWarning() << "找不到表格定義:" << tableRef;
|
||||
return;
|
||||
}
|
||||
|
||||
const TableDefinition &tableDef = m_config.tables[tableRef];
|
||||
|
||||
// 初始化表格數據
|
||||
QVector<QVector<QVariant>> data;
|
||||
int rowCount = tableDef.rowCount > 0 ? tableDef.rowCount : 10;
|
||||
int colCount = tableDef.fields.size() > 0 ? tableDef.fields.size() : tableDef.columnCount;
|
||||
|
||||
for (int row = 0; row < rowCount; row++)
|
||||
{
|
||||
QVector<QVariant> rowData;
|
||||
for (int col = 0; col < colCount; col++)
|
||||
{
|
||||
if (col < tableDef.fields.size())
|
||||
{
|
||||
rowData.append(tableDef.fields[col].defaultValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
rowData.append(QVariant());
|
||||
}
|
||||
}
|
||||
data.append(rowData);
|
||||
}
|
||||
|
||||
// 應用靜態單元格
|
||||
for (const StaticCell &cell : tableDef.staticCells)
|
||||
{
|
||||
if (cell.row < data.size() && cell.column < data[cell.row].size())
|
||||
{
|
||||
data[cell.row][cell.column] = cell.value;
|
||||
}
|
||||
}
|
||||
|
||||
m_tableData[tableRef] = data;
|
||||
|
||||
emit tableInitialized(tableRef, tableDef);
|
||||
}
|
||||
|
||||
QVariant ProcedureEngine::getTableCellValue(const QString &tableRef, int row, int column) const
|
||||
{
|
||||
if (!m_tableData.contains(tableRef))
|
||||
{
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
const auto &data = m_tableData[tableRef];
|
||||
if (row < 0 || row >= data.size())
|
||||
{
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
if (column < 0 || column >= data[row].size())
|
||||
{
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
return data[row][column];
|
||||
}
|
||||
|
||||
void ProcedureEngine::setTableCellValue(const QString &tableRef, int row, int column, const QVariant &value)
|
||||
{
|
||||
if (!m_tableData.contains(tableRef))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto &data = m_tableData[tableRef];
|
||||
if (row < 0 || row >= data.size())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 自動擴展列
|
||||
while (column >= data[row].size())
|
||||
{
|
||||
data[row].append(QVariant());
|
||||
}
|
||||
|
||||
data[row][column] = value;
|
||||
|
||||
emit tableDataChanged(tableRef, row, column, value);
|
||||
}
|
||||
|
||||
QVector<QVector<QVariant>> ProcedureEngine::getTableData(const QString &tableRef) const
|
||||
{
|
||||
if (m_tableData.contains(tableRef))
|
||||
{
|
||||
return m_tableData[tableRef];
|
||||
}
|
||||
return QVector<QVector<QVariant>>();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 條件評估
|
||||
// =============================================================================
|
||||
|
||||
bool ProcedureEngine::evaluateCondition(const QString &condition)
|
||||
{
|
||||
// 簡單的條件評估
|
||||
// TODO: 實現完整的表達式解析器
|
||||
|
||||
if (condition.isEmpty() || condition == "true")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (condition == "false")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 支持簡單的表格引用條件
|
||||
// 格式: table[row][col] == value
|
||||
static QRegularExpression tableRefRe(R"((\w+)\[(\d+)\]\[(\d+)\]\s*(==|!=|>|<|>=|<=)\s*(.+))");
|
||||
QRegularExpressionMatch match = tableRefRe.match(condition);
|
||||
|
||||
if (match.hasMatch())
|
||||
{
|
||||
QString tableRef = match.captured(1);
|
||||
int row = match.captured(2).toInt();
|
||||
int col = match.captured(3).toInt();
|
||||
QString op = match.captured(4);
|
||||
QString valueStr = match.captured(5).trimmed();
|
||||
|
||||
QVariant cellValue = getTableCellValue(tableRef, row, col);
|
||||
QVariant compareValue = valueStr;
|
||||
|
||||
// 簡單比較
|
||||
if (op == "==")
|
||||
{
|
||||
return cellValue == compareValue;
|
||||
}
|
||||
else if (op == "!=")
|
||||
{
|
||||
return cellValue != compareValue;
|
||||
}
|
||||
else if (op == ">")
|
||||
{
|
||||
return cellValue.toDouble() > compareValue.toDouble();
|
||||
}
|
||||
else if (op == "<")
|
||||
{
|
||||
return cellValue.toDouble() < compareValue.toDouble();
|
||||
}
|
||||
else if (op == ">=")
|
||||
{
|
||||
return cellValue.toDouble() >= compareValue.toDouble();
|
||||
}
|
||||
else if (op == "<=")
|
||||
{
|
||||
return cellValue.toDouble() <= compareValue.toDouble();
|
||||
}
|
||||
}
|
||||
|
||||
qWarning() << "無法評估條件:" << condition;
|
||||
return false;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 狀態查詢
|
||||
// =============================================================================
|
||||
|
||||
void ProcedureEngine::setStatus(ProcedureStatus status)
|
||||
{
|
||||
if (m_status != status)
|
||||
{
|
||||
m_status = status;
|
||||
emit statusChanged(status);
|
||||
}
|
||||
}
|
||||
|
||||
ProcedureStatus ProcedureEngine::status() const
|
||||
{
|
||||
return m_status;
|
||||
}
|
||||
|
||||
ProcedureConfig ProcedureEngine::config() const
|
||||
{
|
||||
return m_config;
|
||||
}
|
||||
|
||||
ExecutionContext ProcedureEngine::executionContext() const
|
||||
{
|
||||
return m_context;
|
||||
}
|
||||
|
||||
ActivityStep ProcedureEngine::getCurrentActivity() const
|
||||
{
|
||||
if (m_context.currentActivityIndex >= 0 &&
|
||||
m_context.currentActivityIndex < m_config.activitySequence.size())
|
||||
{
|
||||
return m_config.activitySequence[m_context.currentActivityIndex];
|
||||
}
|
||||
return ActivityStep();
|
||||
}
|
||||
|
||||
TestTaskGroup ProcedureEngine::getCurrentTaskGroup() const
|
||||
{
|
||||
ActivityStep step = getCurrentActivity();
|
||||
if (step.type == ActivityType::TEST_TASK_GROUP &&
|
||||
m_config.testTaskGroups.contains(step.ref))
|
||||
{
|
||||
return m_config.testTaskGroups[step.ref];
|
||||
}
|
||||
return TestTaskGroup();
|
||||
}
|
||||
|
||||
TestActivityGroup ProcedureEngine::getCurrentStage() const
|
||||
{
|
||||
TestTaskGroup taskGroup = getCurrentTaskGroup();
|
||||
if (m_context.currentStageIndex >= 0 &&
|
||||
m_context.currentStageIndex < taskGroup.stages.size())
|
||||
{
|
||||
return taskGroup.stages[m_context.currentStageIndex];
|
||||
}
|
||||
return TestActivityGroup();
|
||||
}
|
||||
|
||||
TestAction ProcedureEngine::getCurrentAction() const
|
||||
{
|
||||
TestActivityGroup stage = getCurrentStage();
|
||||
if (m_context.currentActionIndex >= 0 &&
|
||||
m_context.currentActionIndex < stage.actions.size())
|
||||
{
|
||||
return stage.actions[m_context.currentActionIndex];
|
||||
}
|
||||
return TestAction();
|
||||
}
|
||||
|
||||
int ProcedureEngine::getActivityCount() const
|
||||
{
|
||||
return m_config.activitySequence.size();
|
||||
}
|
||||
|
||||
int ProcedureEngine::getCurrentActivityIndex() const
|
||||
{
|
||||
return m_context.currentActivityIndex;
|
||||
}
|
||||
|
||||
double ProcedureEngine::getProgress() const
|
||||
{
|
||||
if (m_config.activitySequence.isEmpty())
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// 基於活動數量計算簡單進度
|
||||
double activityProgress = static_cast<double>(m_context.currentActivityIndex) / m_config.activitySequence.size();
|
||||
|
||||
// 可以進一步細化到階段和動作級別
|
||||
// TODO: 更精確的進度計算
|
||||
|
||||
return qBound(0.0, activityProgress * 100.0, 100.0);
|
||||
}
|
||||
172
procedure/procedureengine.h
Normal file
@@ -0,0 +1,172 @@
|
||||
#ifndef PROCEDUREENGINE_H
|
||||
#define PROCEDUREENGINE_H
|
||||
|
||||
#include "proceduredata.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QDateTime>
|
||||
#include <QMap>
|
||||
#include <QVariant>
|
||||
#include <QVector>
|
||||
|
||||
class ProcedureParser;
|
||||
class FunctionRegistry;
|
||||
class QTimer;
|
||||
|
||||
/**
|
||||
* @brief 程序執行狀態
|
||||
*/
|
||||
enum class ProcedureStatus
|
||||
{
|
||||
Idle, // 空閒
|
||||
Loaded, // 已載入
|
||||
Running, // 運行中
|
||||
Paused, // 已暫停
|
||||
Stopped, // 已停止
|
||||
Completed, // 已完成
|
||||
Error // 錯誤
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 執行上下文
|
||||
*/
|
||||
struct ExecutionContext
|
||||
{
|
||||
int currentActivityIndex = -1;
|
||||
int currentStageIndex = -1;
|
||||
int currentActionIndex = -1;
|
||||
int loopIteration = 0;
|
||||
QDateTime startTime;
|
||||
QDateTime endTime;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 程序執行引擎
|
||||
*
|
||||
* 負責程序的載入、執行、暫停、恢復和停止。
|
||||
* 管理執行上下文、活動序列和表格數據。
|
||||
*/
|
||||
class ProcedureEngine : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static ProcedureEngine *instance();
|
||||
|
||||
// 配置載入
|
||||
bool loadProcedure(const QString &configPath);
|
||||
|
||||
// 程序控制
|
||||
void startProcedure();
|
||||
void pauseProcedure();
|
||||
void resumeProcedure();
|
||||
void stopProcedure();
|
||||
void resetProcedure();
|
||||
|
||||
// 活動導航
|
||||
bool moveToNextActivity();
|
||||
bool moveToPreviousActivity();
|
||||
bool jumpToActivity(int index);
|
||||
|
||||
// 狀態查詢
|
||||
ProcedureStatus status() const;
|
||||
ProcedureConfig config() const;
|
||||
ExecutionContext executionContext() const;
|
||||
|
||||
// 獲取當前執行項
|
||||
ActivityStep getCurrentActivity() const;
|
||||
TestTaskGroup getCurrentTaskGroup() const;
|
||||
TestActivityGroup getCurrentStage() const;
|
||||
TestAction getCurrentAction() const;
|
||||
|
||||
// 進度
|
||||
int getActivityCount() const;
|
||||
int getCurrentActivityIndex() const;
|
||||
double getProgress() const;
|
||||
|
||||
// 表格數據管理
|
||||
QVariant getTableCellValue(const QString &tableRef, int row, int column) const;
|
||||
void setTableCellValue(const QString &tableRef, int row, int column, const QVariant &value);
|
||||
QVector<QVector<QVariant>> getTableData(const QString &tableRef) const;
|
||||
|
||||
public slots:
|
||||
void onActionCompleted(const QString &actionId, const QVariantMap &results);
|
||||
void onActionFailed(const QString &actionId, const QString &error);
|
||||
|
||||
signals:
|
||||
// 程序狀態信號
|
||||
void procedureLoaded(const QString &name);
|
||||
void procedureStarted();
|
||||
void procedurePaused();
|
||||
void procedureResumed();
|
||||
void procedureStopped();
|
||||
void procedureCompleted();
|
||||
void statusChanged(ProcedureStatus status);
|
||||
void errorOccurred(const QString &error);
|
||||
|
||||
// 活動信號
|
||||
void activityStarted(int index, const ActivityStep &step);
|
||||
void taskGroupStarted(const QString &ref, const QString &name);
|
||||
void taskGroupCompleted(const QString &ref);
|
||||
void stageStarted(int index, const QString &name);
|
||||
void stageCompleted(int index);
|
||||
void actionStarted(int index, const QString &actionId);
|
||||
void actionCompleted(int index, const QString &actionId, const QVariantMap &results);
|
||||
void actionFailed(int index, const QString &actionId, const QString &error);
|
||||
|
||||
// 執行請求
|
||||
void executeActionRequested(const TestAction &action);
|
||||
void resultDisplayRequested(const QString &displayRef, const ResultDisplay &display);
|
||||
|
||||
// 表格信號
|
||||
void tableInitialized(const QString &tableRef, const TableDefinition &definition);
|
||||
void tableDataChanged(const QString &tableRef, int row, int column, const QVariant &value);
|
||||
|
||||
private:
|
||||
explicit ProcedureEngine(QObject *parent = nullptr);
|
||||
~ProcedureEngine();
|
||||
|
||||
// 執行控制
|
||||
void resetExecutionContext();
|
||||
void setStatus(ProcedureStatus status);
|
||||
|
||||
// 活動執行
|
||||
void moveToFirstActivity();
|
||||
void enterCurrentActivity();
|
||||
void startTaskGroupExecution(const QString &taskGroupRef);
|
||||
void enterCurrentStage(const QString &taskGroupRef);
|
||||
void moveToNextStage(const QString &taskGroupRef);
|
||||
void completeTaskGroup(const QString &taskGroupRef);
|
||||
|
||||
// 動作執行
|
||||
void executeCurrentAction();
|
||||
void moveToNextAction();
|
||||
void processActionResults(const QString &actionId, const QVariantMap &results);
|
||||
|
||||
// 結果顯示
|
||||
void displayResult(const QString &displayRef);
|
||||
void completeProcedure();
|
||||
|
||||
// 表格管理
|
||||
void initializeTable(const QString &tableRef);
|
||||
|
||||
// 條件評估
|
||||
bool evaluateCondition(const QString &condition);
|
||||
|
||||
private slots:
|
||||
void onExecutionTimeout();
|
||||
|
||||
private:
|
||||
static ProcedureEngine *s_instance;
|
||||
|
||||
ProcedureParser *m_parser;
|
||||
ProcedureStatus m_status;
|
||||
ProcedureConfig m_config;
|
||||
ExecutionContext m_context;
|
||||
QString m_configPath;
|
||||
|
||||
QTimer *m_executionTimer;
|
||||
QMap<QString, QVector<QVector<QVariant>>> m_tableData;
|
||||
};
|
||||
|
||||
#endif // PROCEDUREENGINE_H
|
||||
655
procedure/proceduremanager.cpp
Normal file
@@ -0,0 +1,655 @@
|
||||
#include "proceduremanager.h"
|
||||
#include "procedureparser.h"
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QDebug>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QCoreApplication>
|
||||
|
||||
ProcedureManager::ProcedureManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
// 不再自动创建示例数据,等待调用 loadProcedureList
|
||||
}
|
||||
|
||||
ProcedureManager::~ProcedureManager()
|
||||
{
|
||||
}
|
||||
|
||||
QVector<ProcedureSummary> ProcedureManager::loadProcedureList(const QString &directory)
|
||||
{
|
||||
m_procedureDirectory = directory;
|
||||
m_procedureList.clear();
|
||||
|
||||
QDir dir(directory);
|
||||
if (!dir.exists())
|
||||
{
|
||||
qWarning() << "Procedure directory does not exist:" << directory;
|
||||
createSampleProcedures();
|
||||
return m_procedureList;
|
||||
}
|
||||
|
||||
// 扫描目录中的 YAML 和 JSON 文件
|
||||
QStringList filters;
|
||||
filters << "*.yaml" << "*.yml" << "*.json";
|
||||
dir.setNameFilters(filters);
|
||||
|
||||
QFileInfoList files = dir.entryInfoList(QDir::Files | QDir::Readable);
|
||||
qDebug() << "Found" << files.size() << "procedure files in" << directory;
|
||||
|
||||
for (const QFileInfo &fileInfo : files)
|
||||
{
|
||||
QString filePath = fileInfo.absoluteFilePath();
|
||||
qDebug() << "Scanning procedure file:" << filePath;
|
||||
|
||||
// 使用 ProcedureParser 解析文件
|
||||
ProcedureParser parser;
|
||||
if (parser.loadConfig(filePath))
|
||||
{
|
||||
ProcedureConfig config = parser.parseProcedureConfig();
|
||||
|
||||
// 创建摘要信息
|
||||
ProcedureSummary summary;
|
||||
summary.id = config.procedureId;
|
||||
summary.name = config.procedureName;
|
||||
summary.version = config.version;
|
||||
summary.description = config.description;
|
||||
summary.filePath = filePath;
|
||||
|
||||
m_procedureList.append(summary);
|
||||
|
||||
qDebug() << "Loaded procedure:" << summary.name
|
||||
<< "ID:" << summary.id
|
||||
<< "Version:" << summary.version;
|
||||
}
|
||||
else
|
||||
{
|
||||
qWarning() << "Failed to parse procedure file:" << filePath;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到任何文件,创建示例数据
|
||||
if (m_procedureList.isEmpty())
|
||||
{
|
||||
qDebug() << "No valid procedure files found, creating sample data";
|
||||
createSampleProcedures();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 即使找到了真实文件,也添加mock异常数据用于测试
|
||||
qDebug() << "Adding mock exception data for testing";
|
||||
createMockExceptionData();
|
||||
}
|
||||
|
||||
return m_procedureList;
|
||||
}
|
||||
|
||||
QVector<ProcedureSummary> ProcedureManager::searchProcedures(const QString &keyword)
|
||||
{
|
||||
if (keyword.isEmpty())
|
||||
{
|
||||
return m_procedureList;
|
||||
}
|
||||
|
||||
QVector<ProcedureSummary> results;
|
||||
for (const auto &proc : m_procedureList)
|
||||
{
|
||||
if (proc.id.contains(keyword, Qt::CaseInsensitive) ||
|
||||
proc.name.contains(keyword, Qt::CaseInsensitive))
|
||||
{
|
||||
results.append(proc);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
ProcedureData ProcedureManager::loadProcedure(const QString &procedureId)
|
||||
{
|
||||
if (m_loadedProcedures.contains(procedureId))
|
||||
{
|
||||
return m_loadedProcedures[procedureId];
|
||||
}
|
||||
|
||||
// 创建示例规程
|
||||
ProcedureData proc;
|
||||
proc.id = procedureId;
|
||||
proc.workOrderId = m_workOrderId;
|
||||
|
||||
if (procedureId == "KMCIXRCP503")
|
||||
{
|
||||
proc.name = "一回路温度传感器绝缘和连续性检查:TP RCP63";
|
||||
proc.version = "C4";
|
||||
proc.description = "本程序适用于热停堆(热功率稳定)工况下执行RCP63程序";
|
||||
|
||||
// 创建任务组
|
||||
TaskGroup group1;
|
||||
group1.id = "preInspection";
|
||||
group1.name = "5.1 试验前状态说明和检查";
|
||||
|
||||
// 添加步骤
|
||||
StepData step1;
|
||||
step1.id = "step_001";
|
||||
step1.type = StepType::Manual;
|
||||
step1.content = "本程序适用于热停堆(热功率稳定)工况下执行RCP63程序(K-MT-I-X-RCP-501)统一进行Sensor Check 部分使用。";
|
||||
step1.status = StepStatus::Confirmed;
|
||||
group1.steps.append(step1);
|
||||
|
||||
StepData step2;
|
||||
step2.id = "step_002";
|
||||
step2.type = StepType::Automatic;
|
||||
step2.content = "执行前确认KIC中一回路三个环路平均温度变化量不超过0.2℃;";
|
||||
step2.status = StepStatus::Passed;
|
||||
step2.tableRefs.append("temperatureCheckTable");
|
||||
step2.highlightFields.append("loop1Temp");
|
||||
step2.highlightFields.append("loop2Temp");
|
||||
step2.highlightFields.append("loop3Temp");
|
||||
group1.steps.append(step2);
|
||||
|
||||
StepData step3;
|
||||
step3.id = "step_003";
|
||||
step3.type = StepType::Automatic;
|
||||
step3.content = "若KIC中显示的一回路三个环路平均温度变化超过0.2℃,则采取滑动平均的计算方式记录滑动周期: (建议60s), 一环路平均温度滑动平均最大最小值之差;二环路平均温度滑动平均最大最小值之差;三环路平均温度滑动平均最大最小值之差;每个数据测量的最短时间(应大于滑动周期):。确认三个环路平均温度的滑动平均最大与最小值之差不超过0.2℃。";
|
||||
step3.status = StepStatus::Pending;
|
||||
group1.steps.append(step3);
|
||||
|
||||
StepData step4;
|
||||
step4.id = "step_004";
|
||||
step4.type = StepType::Manual;
|
||||
step4.content = "确认 RPR、RPN上无相关的试验检修工作。";
|
||||
step4.status = StepStatus::Pending;
|
||||
group1.steps.append(step4);
|
||||
|
||||
StepData step5;
|
||||
step5.id = "step_005";
|
||||
step5.type = StepType::Automatic;
|
||||
step5.content = "按附表1检查相关模拟量指示并记录,确认结果满意。";
|
||||
step5.status = StepStatus::Pending;
|
||||
step5.tableRefs.append("analogReadingsTable");
|
||||
group1.steps.append(step5);
|
||||
|
||||
StepData step6;
|
||||
step6.id = "step_006";
|
||||
step6.type = StepType::Automatic;
|
||||
step6.content = "试验前根据附表1报警清单检查报警,确认无异常报警,方可进行后续检查工作";
|
||||
step6.status = StepStatus::Pending;
|
||||
step6.tableRefs.append("alarmCheckTable");
|
||||
group1.steps.append(step6);
|
||||
|
||||
proc.taskGroups.append(group1);
|
||||
|
||||
// 创建表格
|
||||
TableData tempTable;
|
||||
tempTable.id = "temperatureCheckTable";
|
||||
tempTable.name = "环路平均温度变化";
|
||||
tempTable.description = "检验前后的环路温度数据";
|
||||
|
||||
TableField f1;
|
||||
f1.id = "loop1Temp";
|
||||
f1.name = "一环路平均温度";
|
||||
f1.type = "numeric";
|
||||
f1.unit = "℃";
|
||||
TableField f2;
|
||||
f2.id = "loop2Temp";
|
||||
f2.name = "二环路平均温度";
|
||||
f2.type = "numeric";
|
||||
f2.unit = "℃";
|
||||
TableField f3;
|
||||
f3.id = "loop3Temp";
|
||||
f3.name = "三环路平均温度";
|
||||
f3.type = "numeric";
|
||||
f3.unit = "℃";
|
||||
tempTable.columns.append(f1);
|
||||
tempTable.columns.append(f2);
|
||||
tempTable.columns.append(f3);
|
||||
proc.tables.append(tempTable);
|
||||
|
||||
TableData analogTable;
|
||||
analogTable.id = "analogReadingsTable";
|
||||
analogTable.name = "实验前后模拟量数据";
|
||||
proc.tables.append(analogTable);
|
||||
|
||||
TableData alarmTable;
|
||||
alarmTable.id = "alarmCheckTable";
|
||||
alarmTable.name = "试验前后报警检查";
|
||||
proc.tables.append(alarmTable);
|
||||
|
||||
// 计算步骤数
|
||||
proc.totalSteps = group1.steps.size();
|
||||
proc.completedSteps = 2; // 已完成2个步骤
|
||||
}
|
||||
else if (procedureId == "SIMPLE63")
|
||||
{
|
||||
proc.name = "范例 一回路温度传感器绝缘和连续性检查 TP RCP63";
|
||||
proc.version = "SIMPLE";
|
||||
proc.description = "范例规程";
|
||||
proc.totalSteps = 6;
|
||||
proc.completedSteps = 0;
|
||||
}
|
||||
else if (procedureId == "SIMPLE_TEST")
|
||||
{
|
||||
proc.name = "简单测试规程";
|
||||
proc.version = "1.0";
|
||||
proc.description = "这是一个简单的测试规程,用于演示 ProcedurePlayer 的基本功能。包含一个测试任务组和结果展示。";
|
||||
proc.totalSteps = 4;
|
||||
proc.completedSteps = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 尝试从 JSON 文件加载 mock 数据
|
||||
proc = loadMockProcedureFromJson(procedureId);
|
||||
|
||||
// 如果 JSON 中没有找到,返回空数据
|
||||
if (proc.name.isEmpty())
|
||||
{
|
||||
qWarning() << "Procedure not found:" << procedureId;
|
||||
}
|
||||
}
|
||||
|
||||
m_loadedProcedures[procedureId] = proc;
|
||||
emit procedureLoaded(proc);
|
||||
return proc;
|
||||
}
|
||||
|
||||
void ProcedureManager::setWorkOrderId(const QString &workOrderId)
|
||||
{
|
||||
m_workOrderId = workOrderId;
|
||||
}
|
||||
|
||||
bool ProcedureManager::confirmStep(const QString &procedureId, const QString &stepId)
|
||||
{
|
||||
if (!m_loadedProcedures.contains(procedureId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto &proc = m_loadedProcedures[procedureId];
|
||||
for (auto &group : proc.taskGroups)
|
||||
{
|
||||
for (auto &step : group.steps)
|
||||
{
|
||||
if (step.id == stepId)
|
||||
{
|
||||
step.status = StepStatus::Confirmed;
|
||||
step.executedAt = QDateTime::currentDateTime();
|
||||
emit stepStatusChanged(stepId, StepStatus::Confirmed);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ProcedureManager::cancelStepConfirm(const QString &procedureId, const QString &stepId, const QString &reason)
|
||||
{
|
||||
if (!m_loadedProcedures.contains(procedureId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto &proc = m_loadedProcedures[procedureId];
|
||||
for (auto &group : proc.taskGroups)
|
||||
{
|
||||
for (auto &step : group.steps)
|
||||
{
|
||||
if (step.id == stepId)
|
||||
{
|
||||
step.status = StepStatus::Pending;
|
||||
step.cancelReason = reason;
|
||||
emit stepStatusChanged(stepId, StepStatus::Pending);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ProcedureManager::executeAutoStep(const QString &procedureId, const QString &stepId)
|
||||
{
|
||||
// 模拟自动步骤执行
|
||||
if (!m_loadedProcedures.contains(procedureId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto &proc = m_loadedProcedures[procedureId];
|
||||
for (auto &group : proc.taskGroups)
|
||||
{
|
||||
for (auto &step : group.steps)
|
||||
{
|
||||
if (step.id == stepId && step.type == StepType::Automatic)
|
||||
{
|
||||
step.status = StepStatus::InProgress;
|
||||
emit stepStatusChanged(stepId, StepStatus::InProgress);
|
||||
// 实际应用中这里会执行真正的自动化逻辑
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
TableData ProcedureManager::getTableData(const QString &procedureId, const QString &tableId)
|
||||
{
|
||||
if (m_loadedProcedures.contains(procedureId))
|
||||
{
|
||||
const auto &proc = m_loadedProcedures[procedureId];
|
||||
for (const auto &table : proc.tables)
|
||||
{
|
||||
if (table.id == tableId)
|
||||
{
|
||||
return table;
|
||||
}
|
||||
}
|
||||
}
|
||||
return TableData();
|
||||
}
|
||||
|
||||
bool ProcedureManager::updateTableData(const QString &procedureId, const QString &tableId, const QVector<QVariantMap> &rows)
|
||||
{
|
||||
if (!m_loadedProcedures.contains(procedureId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto &proc = m_loadedProcedures[procedureId];
|
||||
for (auto &table : proc.tables)
|
||||
{
|
||||
if (table.id == tableId)
|
||||
{
|
||||
table.rows = rows;
|
||||
emit tableDataUpdated(tableId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ProcedureManager::createSampleProcedures()
|
||||
{
|
||||
m_procedureList.clear();
|
||||
|
||||
ProcedureSummary p1;
|
||||
p1.id = "KMCIXRCP503";
|
||||
p1.name = "一回路温度传感器绝缘和连续性检查:TP RCP63";
|
||||
p1.version = "C4";
|
||||
p1.description = "";
|
||||
p1.filePath = "[SAMPLE]";
|
||||
m_procedureList.append(p1);
|
||||
|
||||
ProcedureSummary p2;
|
||||
p2.id = "SIMPLE63";
|
||||
p2.name = "范例 一回路温度传感器绝缘和连续性检查 TP RCP63";
|
||||
p2.version = "SIMPLE";
|
||||
p2.description = "";
|
||||
p2.filePath = "[SAMPLE]";
|
||||
m_procedureList.append(p2);
|
||||
|
||||
ProcedureSummary p3;
|
||||
p3.id = "SIMPLE_TEST";
|
||||
p3.name = "简单测试规程";
|
||||
p3.version = "1.0";
|
||||
p3.description = "这是一个简单的测试规程,用于演示 ProcedurePlayer 的基本功能。包含一个测试任务组和结果展示。";
|
||||
p3.filePath = "[SAMPLE]";
|
||||
m_procedureList.append(p3);
|
||||
|
||||
// 也添加异常数据
|
||||
createMockExceptionData();
|
||||
}
|
||||
|
||||
void ProcedureManager::createMockExceptionData()
|
||||
{
|
||||
ProcedureSummary p7;
|
||||
p7.id = "EMPTY_DESC";
|
||||
p7.name = "空描述测试";
|
||||
p7.version = "1.0";
|
||||
p7.description = "";
|
||||
p7.filePath = "[MOCK_EXCEPTION]";
|
||||
m_procedureList.append(p7);
|
||||
|
||||
// Mock 异常数据 - 超长描述
|
||||
ProcedureSummary p8;
|
||||
p8.id = "LONG_DESC";
|
||||
p8.name = "超长描述测试";
|
||||
p8.version = "2.0";
|
||||
p8.description = "这是一个包含超长描述的测试规程,用于验证当描述文本过长时,界面能否正确处理和显示。"
|
||||
"描述内容可能包含多个段落、技术细节、注意事项等。在实际应用中,规程描述可能会非常详细,"
|
||||
"包括前置条件、执行步骤概述、预期结果、风险提示、相关文档引用等信息。这样的长描述需要"
|
||||
"界面能够适当地截断、折叠或提供滚动功能来确保用户体验。";
|
||||
p8.filePath = "[MOCK_EXCEPTION]";
|
||||
m_procedureList.append(p8);
|
||||
|
||||
// Mock 异常数据 - 数字和符号混合的 ID
|
||||
ProcedureSummary p9;
|
||||
p9.id = "TEST-2024-12-31_V1.0";
|
||||
p9.name = "日期版本号混合 ID";
|
||||
p9.version = "2024.12.31";
|
||||
p9.description = "测试复杂的 ID 和版本号格式";
|
||||
p9.filePath = "[MOCK_EXCEPTION]";
|
||||
m_procedureList.append(p9);
|
||||
|
||||
// Mock 异常数据 - 换行符
|
||||
ProcedureSummary p10;
|
||||
p10.id = "MULTILINE";
|
||||
p10.name = "多行\n文本\n测试";
|
||||
p10.version = "1.0";
|
||||
p10.description = "第一行描述\n第二行描述\n第三行描述";
|
||||
p10.filePath = "[MOCK_EXCEPTION]";
|
||||
m_procedureList.append(p10);
|
||||
}
|
||||
|
||||
StepType ProcedureManager::parseStepType(const QString &typeStr)
|
||||
{
|
||||
if (typeStr == "Manual")
|
||||
return StepType::Manual;
|
||||
if (typeStr == "Automatic")
|
||||
return StepType::Automatic;
|
||||
return StepType::Manual;
|
||||
}
|
||||
|
||||
StepStatus ProcedureManager::parseStepStatus(const QString &statusStr)
|
||||
{
|
||||
if (statusStr == "Pending")
|
||||
return StepStatus::Pending;
|
||||
if (statusStr == "InProgress")
|
||||
return StepStatus::InProgress;
|
||||
if (statusStr == "Confirmed")
|
||||
return StepStatus::Confirmed;
|
||||
if (statusStr == "Passed")
|
||||
return StepStatus::Passed;
|
||||
if (statusStr == "Failed")
|
||||
return StepStatus::Failed;
|
||||
if (statusStr == "Skipped")
|
||||
return StepStatus::Skipped;
|
||||
return StepStatus::Pending;
|
||||
}
|
||||
|
||||
ProcedureData ProcedureManager::loadMockProcedureFromJson(const QString &procedureId)
|
||||
{
|
||||
ProcedureData proc;
|
||||
proc.id = procedureId;
|
||||
proc.workOrderId = m_workOrderId;
|
||||
|
||||
// 尝试多个可能的 JSON 文件路径
|
||||
QStringList possiblePaths;
|
||||
possiblePaths << m_procedureDirectory + "/mock_procedures.json";
|
||||
possiblePaths << QDir::currentPath() + "/procedures/mock_procedures.json";
|
||||
possiblePaths << QCoreApplication::applicationDirPath() + "/procedures/mock_procedures.json";
|
||||
possiblePaths << QCoreApplication::applicationDirPath() + "/../../../procedures/mock_procedures.json";
|
||||
|
||||
QString jsonFilePath;
|
||||
for (const QString &path : possiblePaths)
|
||||
{
|
||||
if (QFile::exists(path))
|
||||
{
|
||||
jsonFilePath = path;
|
||||
qDebug() << "Found mock procedures JSON at:" << path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonFilePath.isEmpty())
|
||||
{
|
||||
qWarning() << "Mock procedures JSON file not found. Tried paths:" << possiblePaths;
|
||||
return proc;
|
||||
}
|
||||
|
||||
// 读取 JSON 文件
|
||||
QFile file(jsonFilePath);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
qWarning() << "Failed to open mock procedures JSON:" << jsonFilePath;
|
||||
return proc;
|
||||
}
|
||||
|
||||
QByteArray jsonData = file.readAll();
|
||||
file.close();
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonData);
|
||||
if (!doc.isObject())
|
||||
{
|
||||
qWarning() << "Invalid JSON format in mock procedures file";
|
||||
return proc;
|
||||
}
|
||||
|
||||
QJsonObject root = doc.object();
|
||||
QJsonArray mockProcedures = root["mockProcedures"].toArray();
|
||||
|
||||
// 查找匹配的规程
|
||||
for (const QJsonValue &value : mockProcedures)
|
||||
{
|
||||
QJsonObject procObj = value.toObject();
|
||||
if (procObj["id"].toString() != procedureId)
|
||||
continue;
|
||||
|
||||
// 解析基本信息
|
||||
proc.name = procObj["name"].toString();
|
||||
proc.version = procObj["version"].toString();
|
||||
proc.description = procObj["description"].toString();
|
||||
|
||||
// 解析任务组
|
||||
QJsonArray taskGroupsArray = procObj["taskGroups"].toArray();
|
||||
for (const QJsonValue &groupValue : taskGroupsArray)
|
||||
{
|
||||
QJsonObject groupObj = groupValue.toObject();
|
||||
TaskGroup group;
|
||||
group.id = groupObj["id"].toString();
|
||||
group.name = groupObj["name"].toString();
|
||||
|
||||
// 解析步骤
|
||||
QJsonArray stepsArray = groupObj["steps"].toArray();
|
||||
for (const QJsonValue &stepValue : stepsArray)
|
||||
{
|
||||
QJsonObject stepObj = stepValue.toObject();
|
||||
StepData step;
|
||||
step.id = stepObj["id"].toString();
|
||||
step.content = stepObj["content"].toString();
|
||||
step.type = parseStepType(stepObj["type"].toString());
|
||||
step.status = parseStepStatus(stepObj["status"].toString());
|
||||
|
||||
// 解析表格引用
|
||||
if (stepObj.contains("tableRefs"))
|
||||
{
|
||||
QJsonArray tableRefsArray = stepObj["tableRefs"].toArray();
|
||||
for (const QJsonValue &refValue : tableRefsArray)
|
||||
{
|
||||
step.tableRefs.append(refValue.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// 解析高亮字段
|
||||
if (stepObj.contains("highlightFields"))
|
||||
{
|
||||
QJsonArray highlightArray = stepObj["highlightFields"].toArray();
|
||||
for (const QJsonValue &fieldValue : highlightArray)
|
||||
{
|
||||
step.highlightFields.append(fieldValue.toString());
|
||||
}
|
||||
}
|
||||
|
||||
group.steps.append(step);
|
||||
}
|
||||
|
||||
proc.taskGroups.append(group);
|
||||
}
|
||||
|
||||
// 解析表格
|
||||
if (procObj.contains("tables"))
|
||||
{
|
||||
QJsonArray tablesArray = procObj["tables"].toArray();
|
||||
for (const QJsonValue &tableValue : tablesArray)
|
||||
{
|
||||
QJsonObject tableObj = tableValue.toObject();
|
||||
TableData table;
|
||||
table.id = tableObj["id"].toString();
|
||||
table.name = tableObj["name"].toString();
|
||||
if (tableObj.contains("description"))
|
||||
table.description = tableObj["description"].toString();
|
||||
|
||||
// 解析列
|
||||
if (tableObj.contains("columns"))
|
||||
{
|
||||
QJsonArray columnsArray = tableObj["columns"].toArray();
|
||||
for (const QJsonValue &colValue : columnsArray)
|
||||
{
|
||||
QJsonObject colObj = colValue.toObject();
|
||||
TableField field;
|
||||
field.id = colObj["id"].toString();
|
||||
field.name = colObj["name"].toString();
|
||||
field.type = colObj["type"].toString();
|
||||
if (colObj.contains("unit"))
|
||||
field.unit = colObj["unit"].toString();
|
||||
if (colObj.contains("isRequired"))
|
||||
field.isRequired = colObj["isRequired"].toBool();
|
||||
if (colObj.contains("isHighlighted"))
|
||||
field.isHighlighted = colObj["isHighlighted"].toBool();
|
||||
|
||||
table.columns.append(field);
|
||||
}
|
||||
}
|
||||
|
||||
proc.tables.append(table);
|
||||
}
|
||||
}
|
||||
|
||||
// 计算步骤统计
|
||||
if (procObj.contains("totalSteps"))
|
||||
{
|
||||
proc.totalSteps = procObj["totalSteps"].toInt();
|
||||
}
|
||||
else
|
||||
{
|
||||
proc.totalSteps = 0;
|
||||
for (const TaskGroup &group : proc.taskGroups)
|
||||
{
|
||||
proc.totalSteps += group.steps.size();
|
||||
}
|
||||
}
|
||||
|
||||
if (procObj.contains("completedSteps"))
|
||||
{
|
||||
proc.completedSteps = procObj["completedSteps"].toInt();
|
||||
}
|
||||
else
|
||||
{
|
||||
proc.completedSteps = 0;
|
||||
for (const TaskGroup &group : proc.taskGroups)
|
||||
{
|
||||
for (const StepData &step : group.steps)
|
||||
{
|
||||
if (step.status == StepStatus::Confirmed || step.status == StepStatus::Passed)
|
||||
{
|
||||
proc.completedSteps++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Loaded mock procedure from JSON:" << procedureId;
|
||||
break;
|
||||
}
|
||||
|
||||
return proc;
|
||||
}
|
||||
58
procedure/proceduremanager.h
Normal file
@@ -0,0 +1,58 @@
|
||||
#ifndef PROCEDUREMANAGER_H
|
||||
#define PROCEDUREMANAGER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QVector>
|
||||
#include <QMap>
|
||||
#include "proceduredata.h"
|
||||
|
||||
class ProcedureManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ProcedureManager(QObject *parent = nullptr);
|
||||
~ProcedureManager();
|
||||
|
||||
// 加载规程列表
|
||||
QVector<ProcedureSummary> loadProcedureList(const QString &directory);
|
||||
|
||||
// 搜索规程
|
||||
QVector<ProcedureSummary> searchProcedures(const QString &keyword);
|
||||
|
||||
// 加载规程详情
|
||||
ProcedureData loadProcedure(const QString &procedureId);
|
||||
|
||||
// 设置工单号
|
||||
void setWorkOrderId(const QString &workOrderId);
|
||||
QString workOrderId() const { return m_workOrderId; }
|
||||
|
||||
// 步骤操作
|
||||
bool confirmStep(const QString &procedureId, const QString &stepId);
|
||||
bool cancelStepConfirm(const QString &procedureId, const QString &stepId, const QString &reason);
|
||||
bool executeAutoStep(const QString &procedureId, const QString &stepId);
|
||||
|
||||
// 获取表格数据
|
||||
TableData getTableData(const QString &procedureId, const QString &tableId);
|
||||
bool updateTableData(const QString &procedureId, const QString &tableId, const QVector<QVariantMap> &rows);
|
||||
|
||||
signals:
|
||||
void procedureLoaded(const ProcedureData &procedure);
|
||||
void stepStatusChanged(const QString &stepId, StepStatus status);
|
||||
void tableDataUpdated(const QString &tableId);
|
||||
|
||||
private:
|
||||
ProcedureData parseProcedureYaml(const QString &filePath);
|
||||
void createSampleProcedures();
|
||||
void createMockExceptionData();
|
||||
ProcedureData loadMockProcedureFromJson(const QString &procedureId);
|
||||
StepType parseStepType(const QString &typeStr);
|
||||
StepStatus parseStepStatus(const QString &statusStr);
|
||||
|
||||
QString m_workOrderId;
|
||||
QString m_procedureDirectory;
|
||||
QVector<ProcedureSummary> m_procedureList;
|
||||
QMap<QString, ProcedureData> m_loadedProcedures;
|
||||
};
|
||||
|
||||
#endif // PROCEDUREMANAGER_H
|
||||
1594
procedure/procedureparser.cpp
Normal file
83
procedure/procedureparser.h
Normal file
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <ryml.hpp>
|
||||
|
||||
// Include existing data structures
|
||||
#include "proceduredata.h"
|
||||
|
||||
class ProcedureParser : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ProcedureParser(QObject *parent = nullptr);
|
||||
~ProcedureParser();
|
||||
|
||||
// Configuration file loading and parsing
|
||||
bool loadConfig(const QString &filePath);
|
||||
ProcedureConfig parseProcedureConfig();
|
||||
|
||||
// Configuration validation
|
||||
bool validateConfig();
|
||||
QStringList getValidationErrors() const;
|
||||
|
||||
// Table definition extraction
|
||||
TableDefinition getTableDefinition(const QString &tableRef);
|
||||
QMap<QString, TableDefinition> getAllTableDefinitions();
|
||||
|
||||
// Lazy parsing - parse TestTaskGroup stages on demand (Stage 1: load stage
|
||||
// metadata)
|
||||
bool parseTestTaskGroupStages(TestTaskGroup &group, const QString &groupId);
|
||||
|
||||
// Lazy parsing - parse TestActivityGroup actions on demand (Stage 2: load
|
||||
// actions)
|
||||
bool parseTestActivityGroupActions(TestActivityGroup &group, const QString &groupId);
|
||||
|
||||
signals:
|
||||
void configLoaded(const QString &filePath);
|
||||
void configError(const QString &error, int line, int column);
|
||||
|
||||
private:
|
||||
std::unique_ptr<ryml::Tree> tree;
|
||||
QMap<QString, ryml::ConstNodeRef> referenceCache;
|
||||
QMap<QString, ryml::ConstNodeRef> stageNodeCache;
|
||||
QMap<QString, ryml::ConstNodeRef> activityNodeCache; // Cache for lazy parsing of TestTaskGroups
|
||||
QStringList validationErrors;
|
||||
QString configFilePath;
|
||||
std::string yamlContent; // Store YAML content for in-place parsing
|
||||
|
||||
// File parsing methods
|
||||
bool parseFile(const QString &filePath);
|
||||
|
||||
// Reference handling
|
||||
void buildReferenceCache();
|
||||
ryml::ConstNodeRef resolveReference(const QString &ref);
|
||||
QString extractRefPath(const ryml::ConstNodeRef &node);
|
||||
|
||||
// Validation methods
|
||||
bool validateActivitySequence();
|
||||
bool validateTableDefinitions();
|
||||
|
||||
// Parsing helper methods
|
||||
QString nodeToQString(const ryml::ConstNodeRef &node);
|
||||
QVariant nodeToQVariant(const ryml::ConstNodeRef &node);
|
||||
QVariantMap nodeToQVariantMap(const ryml::ConstNodeRef &node);
|
||||
QVariantList nodeToQVariantList(const ryml::ConstNodeRef &node);
|
||||
|
||||
// Structure parsing methods
|
||||
FieldDefinition parseFieldDefinition(const ryml::ConstNodeRef &node);
|
||||
TableDefinition parseTableDefinition(const ryml::ConstNodeRef &node);
|
||||
StaticCell parseStaticCell(const ryml::ConstNodeRef &node);
|
||||
FieldSelector parseFieldSelector(const ryml::ConstNodeRef &node);
|
||||
TestAction parseTestAction(const ryml::ConstNodeRef &node);
|
||||
TestActivityGroup parseTestActivityGroup(const ryml::ConstNodeRef &node);
|
||||
TestTaskGroup parseTestTaskGroup(const ryml::ConstNodeRef &node);
|
||||
ResultDisplay parseResultDisplay(const ryml::ConstNodeRef &node);
|
||||
};
|
||||
623
procedures/FUNCTION_GUIDE.md
Normal file
@@ -0,0 +1,623 @@
|
||||
# 测量函数
|
||||
|
||||
## 目录
|
||||
|
||||
1. [电阻测量函数](#电阻测量函数)
|
||||
2. [电压测量函数](#电压测量函数)
|
||||
3. [电流测量函数](#电流测量函数)
|
||||
4. [频率测量函数](#频率测量函数)
|
||||
5. [数字输入函数](#数字输入函数)
|
||||
6. [绝缘测试函数](#绝缘测试函数)
|
||||
7. [数据采集函数](#数据采集函数)
|
||||
|
||||
---
|
||||
|
||||
## 电阻测量函数
|
||||
|
||||
### 1. RESISTANCE_4WIRE - 4 线制电阻测量
|
||||
|
||||
**功能描述**:高精度 4 线制电阻测量,返回 7 个测量值。
|
||||
|
||||
**适用场景**:精密电阻测量,RTD 温度传感器检测。
|
||||
|
||||
**返回值数量**:7 个
|
||||
|
||||
- R1/2, R1/3, R1/4
|
||||
- R2/3, R2/4, R3/4
|
||||
- R/4wires
|
||||
|
||||
**YAML 配置示例**:
|
||||
|
||||
```yaml
|
||||
testActions:
|
||||
measureResistance4Wire:
|
||||
id: measureResistance4Wire
|
||||
name: 4线制电阻测量
|
||||
mode: auto
|
||||
functionType: RESISTANCE_4WIRE
|
||||
channel: input1
|
||||
functionParameters: {}
|
||||
dataFields:
|
||||
- tableRef: '#/tables/resistanceTable'
|
||||
cells:
|
||||
- row: sensor1
|
||||
column: r12
|
||||
- row: sensor1
|
||||
column: r13
|
||||
- row: sensor1
|
||||
column: r14
|
||||
- row: sensor1
|
||||
column: r23
|
||||
- row: sensor1
|
||||
column: r24
|
||||
- row: sensor1
|
||||
column: r34
|
||||
- row: sensor1
|
||||
column: r4wires
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
|
||||
- 必须指定 `channel` (input1 或 input2)
|
||||
- `functionParameters` 为空对象 `{}`
|
||||
- `dataFields` 必须包含 7 个单元格,顺序固定
|
||||
|
||||
---
|
||||
|
||||
### 2. RESISTANCE_3WIRE - 3 线制电阻测量
|
||||
|
||||
**功能描述**:3 线制电阻测量,返回 3 个测量值。
|
||||
|
||||
**适用场景**:3 线制 RTD 温度传感器检测。
|
||||
|
||||
**返回值数量**:3 个
|
||||
|
||||
- R2/3, R2/4, R3/4
|
||||
|
||||
**YAML 配置示例**:
|
||||
|
||||
```yaml
|
||||
testActions:
|
||||
measureResistance3Wire:
|
||||
id: measureResistance3Wire
|
||||
name: 3线制电阻测量
|
||||
mode: auto
|
||||
functionType: RESISTANCE_3WIRE
|
||||
channel: input1
|
||||
functionParameters: {}
|
||||
dataFields:
|
||||
- tableRef: '#/tables/resistanceTable'
|
||||
cells:
|
||||
- row: sensor1
|
||||
column: r23
|
||||
- row: sensor1
|
||||
column: r24
|
||||
- row: sensor1
|
||||
column: r34
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
|
||||
- 必须指定 `channel`
|
||||
- `functionParameters` 为空对象 `{}`
|
||||
- `dataFields` 必须包含 3 个单元格,顺序固定
|
||||
|
||||
---
|
||||
|
||||
### 3. RESISTANCE_2WIRE - 2 线制电阻测量
|
||||
|
||||
**功能描述**:2 线制电阻测量,返回 1 个测量值。
|
||||
|
||||
**适用场景**:简单电阻测量,2 线制传感器。
|
||||
|
||||
**返回值数量**:1 个
|
||||
|
||||
- R1/2
|
||||
|
||||
**YAML 配置示例**:
|
||||
|
||||
```yaml
|
||||
testActions:
|
||||
measureResistance2Wire:
|
||||
id: measureResistance2Wire
|
||||
name: 2线制电阻测量
|
||||
mode: auto
|
||||
functionType: RESISTANCE_2WIRE
|
||||
channel: input1
|
||||
functionParameters: {}
|
||||
dataFields:
|
||||
- tableRef: '#/tables/resistanceTable'
|
||||
cells:
|
||||
- row: sensor1
|
||||
column: resistance
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
|
||||
- 必须指定 `channel`
|
||||
- `functionParameters` 为空对象 `{}`
|
||||
- `dataFields` 包含 1 个单元格
|
||||
|
||||
---
|
||||
|
||||
## 电压测量函数
|
||||
|
||||
### 4. VOLTAGE_AC - 交流电压测量
|
||||
|
||||
**功能描述**:交流电压测量,返回 RMS 电压、峰值电压和频率。
|
||||
|
||||
**适用场景**:交流电源检测,信号分析。
|
||||
|
||||
**返回值数量**:3 个
|
||||
|
||||
- RMS 电压 (有效值)
|
||||
- 峰值电压
|
||||
- 频率
|
||||
|
||||
**YAML 配置示例**:
|
||||
|
||||
```yaml
|
||||
testActions:
|
||||
measureACVoltage:
|
||||
id: measureACVoltage
|
||||
name: 交流电压测量
|
||||
mode: auto
|
||||
functionType: VOLTAGE_AC
|
||||
channel: input1
|
||||
functionParameters: {}
|
||||
dataFields:
|
||||
- tableRef: '#/tables/voltageTable'
|
||||
cells:
|
||||
- row: measurement1
|
||||
column: rmsVoltage
|
||||
- row: measurement1
|
||||
column: peakVoltage
|
||||
- row: measurement1
|
||||
column: frequency
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
|
||||
- 必须指定 `channel`
|
||||
- `functionParameters` 为空对象 `{}`
|
||||
- `dataFields` 必须包含 3 个单元格,顺序固定
|
||||
|
||||
---
|
||||
|
||||
### 5. VOLTAGE_V - 直流电压测量 (伏特)
|
||||
|
||||
**功能描述**:直流电压测量,单位为伏特(V)。
|
||||
|
||||
**适用场景**:直流电源检测,信号电压测量。
|
||||
|
||||
**返回值数量**:1 个
|
||||
|
||||
- 电压值 (V)
|
||||
|
||||
**YAML 配置示例**:
|
||||
|
||||
```yaml
|
||||
testActions:
|
||||
measureDCVoltage:
|
||||
id: measureDCVoltage
|
||||
name: 直流电压测量
|
||||
mode: auto
|
||||
functionType: VOLTAGE_V
|
||||
channel: input1
|
||||
functionParameters: {}
|
||||
dataFields:
|
||||
- tableRef: '#/tables/voltageTable'
|
||||
cells:
|
||||
- row: measurement1
|
||||
column: voltage
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
|
||||
- 必须指定 `channel`
|
||||
- `functionParameters` 为空对象 `{}`
|
||||
- `dataFields` 包含 1 个单元格
|
||||
|
||||
---
|
||||
|
||||
### 6. VOLTAGE_MV - 直流电压测量 (毫伏)
|
||||
|
||||
**功能描述**:直流电压测量,单位为毫伏(mV)。
|
||||
|
||||
**适用场景**:微小信号测量,传感器输出检测。
|
||||
|
||||
**返回值数量**:1 个
|
||||
|
||||
- 电压值 (mV)
|
||||
|
||||
**YAML 配置示例**:
|
||||
|
||||
```yaml
|
||||
testActions:
|
||||
measureMillivolt:
|
||||
id: measureMillivolt
|
||||
name: 毫伏电压测量
|
||||
mode: auto
|
||||
functionType: VOLTAGE_MV
|
||||
channel: input1
|
||||
functionParameters: {}
|
||||
dataFields:
|
||||
- tableRef: '#/tables/voltageTable'
|
||||
cells:
|
||||
- row: measurement1
|
||||
column: millivolt
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
|
||||
- 必须指定 `channel`
|
||||
- `functionParameters` 为空对象 `{}`
|
||||
- `dataFields` 包含 1 个单元格
|
||||
|
||||
---
|
||||
|
||||
## 电流测量函数
|
||||
|
||||
### 7. CURRENT_MA - 直流电流测量 (毫安)
|
||||
|
||||
**功能描述**:直流电流测量,单位为毫安(mA)。
|
||||
|
||||
**适用场景**:回路电流检测,4-20mA 信号测量。
|
||||
|
||||
**返回值数量**:1 个
|
||||
|
||||
- 电流值 (mA)
|
||||
|
||||
**YAML 配置示例**:
|
||||
|
||||
```yaml
|
||||
testActions:
|
||||
measureCurrent:
|
||||
id: measureCurrent
|
||||
name: 电流测量
|
||||
mode: auto
|
||||
functionType: CURRENT_MA
|
||||
channel: input1
|
||||
functionParameters: {}
|
||||
dataFields:
|
||||
- tableRef: '#/tables/currentTable'
|
||||
cells:
|
||||
- row: measurement1
|
||||
column: current
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
|
||||
- 必须指定 `channel`
|
||||
- `functionParameters` 为空对象 `{}`
|
||||
- `dataFields` 包含 1 个单元格
|
||||
|
||||
---
|
||||
|
||||
## 频率测量函数
|
||||
|
||||
### 8. FREQUENCY - 频率测量
|
||||
|
||||
**功能描述**:信号频率测量,单位为 Hz。
|
||||
|
||||
**适用场景**:交流信号频率检测,脉冲频率测量。
|
||||
|
||||
**返回值数量**:1 个
|
||||
|
||||
- 频率值 (Hz)
|
||||
|
||||
**YAML 配置示例**:
|
||||
dat
|
||||
testActions:
|
||||
measureFrequency:
|
||||
id: measureFrequency
|
||||
name: 频率测量
|
||||
mode: auto
|
||||
functionType: FREQUENCY
|
||||
channel: input1
|
||||
functionParameters: {}
|
||||
dataFields: - tableRef: '#/tables/frequencyTable'
|
||||
cells: - row: measurement1
|
||||
column: frequency
|
||||
|
||||
````
|
||||
|
||||
**注意事项**:
|
||||
- 必须指定 `channel`
|
||||
- `functionParameters` 为空对象 `{}`
|
||||
- `dataFields` 包含1个单元格
|
||||
|
||||
---
|
||||
|
||||
## 数字输入函数
|
||||
|
||||
### 9. DIGITAL_INPUT - 数字输入读取
|
||||
|
||||
**功能描述**:读取数字输入状态,返回布尔值。
|
||||
|
||||
**适用场景**:开关状态检测,数字信号读取。
|
||||
|
||||
**返回值数量**:1个
|
||||
- 状态值 (true/false)
|
||||
|
||||
**YAML 配置示例**:
|
||||
|
||||
```yaml
|
||||
testActions:
|
||||
readDigitalInput:
|
||||
id: readDigitalInput
|
||||
name: 数字输入读取
|
||||
mode: auto
|
||||
functionType: DIGITAL_INPUT
|
||||
channel: input1
|
||||
functionParameters: {}
|
||||
dataFields:
|
||||
- tableRef: '#/tables/digitalTable'
|
||||
cells:
|
||||
- row: input1
|
||||
column: state
|
||||
````
|
||||
|
||||
**注意事项**:
|
||||
|
||||
- 必须指定 `channel`
|
||||
- `functionParameters` 为空对象 `{}`
|
||||
- `dataFields` 包含 1 个单元格
|
||||
- 表格字段类型应设置为 `boolean`
|
||||
|
||||
---
|
||||
|
||||
## 绝缘测试函数
|
||||
|
||||
### 10. INSULATION - 绝缘电阻测试
|
||||
|
||||
**功能描述**:绝缘电阻测试,返回绝缘电阻值(MΩ)。
|
||||
|
||||
**适用场景**:电气设备绝缘检测,传感器绝缘测试。
|
||||
|
||||
**返回值数量**:1 个
|
||||
|
||||
- 绝缘电阻 (MΩ)
|
||||
|
||||
**YAML 配置示例**:
|
||||
|
||||
```yaml
|
||||
testActions:
|
||||
insulationTest:
|
||||
id: insulationTest
|
||||
name: 绝缘测试
|
||||
mode: auto
|
||||
functionType: INSULATION
|
||||
channel: input1
|
||||
functionParameters:
|
||||
testVoltage: 50
|
||||
testTime: 20
|
||||
dischargeTime: 5
|
||||
dataFields:
|
||||
- tableRef: '#/tables/insulationTable'
|
||||
cells:
|
||||
- row: sensor1
|
||||
column: insulation
|
||||
```
|
||||
|
||||
**functionParameters 参数说明**:
|
||||
|
||||
- `testVoltage`: 测试电压 (V)
|
||||
- `testTime`: 测试时间 (秒)
|
||||
- `dischargeTime`: 放电时间 (秒)
|
||||
|
||||
**注意事项**:
|
||||
|
||||
- 必须指定 `channel`
|
||||
- `functionParameters` 可以指定测试参数,也可以使用空对象 `{}`
|
||||
- `dataFields` 包含 1 个单元格
|
||||
|
||||
---
|
||||
|
||||
## 数据采集函数
|
||||
|
||||
### 11. DATA_ACQUISITION - 通用数据采集
|
||||
|
||||
**功能描述**:从上位机或 DCS 系统采集多个参数的数据。
|
||||
|
||||
**适用场景**:批量读取传感器数据、报警状态、系统参数等。
|
||||
|
||||
**返回值数量**:可变,与参数列表数量相同
|
||||
|
||||
**YAML 配置示例**:
|
||||
|
||||
```yaml
|
||||
testActions:
|
||||
acquireData:
|
||||
id: acquireData
|
||||
name: 数据采集
|
||||
mode: auto
|
||||
functionType: DATA_ACQUISITION
|
||||
functionParameters:
|
||||
- RCP030MT
|
||||
- RCP033MT
|
||||
- RCP031MT
|
||||
- RCP034MT
|
||||
dataFields:
|
||||
- tableRef: '#/tables/dataTable'
|
||||
cells:
|
||||
- row: RCP030MT
|
||||
column: value
|
||||
- row: RCP033MT
|
||||
column: value
|
||||
- row: RCP031MT
|
||||
column: value
|
||||
- row: RCP034MT
|
||||
column: value
|
||||
```
|
||||
|
||||
**functionParameters 参数说明**:
|
||||
|
||||
- 参数为数组形式,每个元素是要采集的数据标识符
|
||||
- 返回值顺序与参数顺序一致
|
||||
- 支持任意数量的参数
|
||||
|
||||
**数据类型识别**:
|
||||
|
||||
- 包含 "MT" 的参数:识别为温度传感器,返回温度值
|
||||
- 包含 "AA" 的参数:识别为报警状态,返回 "有报警" 或 "无报警"
|
||||
- 其他参数:返回通用数值
|
||||
|
||||
**注意事项**:
|
||||
|
||||
- **不需要**指定 `channel`
|
||||
- `functionParameters` 为数组,不能为空
|
||||
- `dataFields` 中的单元格数量必须与参数数量一致
|
||||
|
||||
---
|
||||
|
||||
### 12. DATA_ACQUISITION_WITH_VALIDATION - 带验证的数据采集
|
||||
|
||||
**功能描述**:从上位机采集数据,并返回每个数据的验证结果。
|
||||
|
||||
**适用场景**:需要服务器端验证的数据采集。
|
||||
|
||||
**返回值数量**:参数数量的 2 倍 (每个参数返回:值 + 验证结果)
|
||||
|
||||
**YAML 配置示例**:
|
||||
|
||||
```yaml
|
||||
testActions:
|
||||
acquireDataWithValidation:
|
||||
id: acquireDataWithValidation
|
||||
name: 带验证的数据采集
|
||||
mode: auto
|
||||
functionType: DATA_ACQUISITION_WITH_VALIDATION
|
||||
functionParameters:
|
||||
- RCP030MT
|
||||
- RCP033MT
|
||||
dataFields:
|
||||
- tableRef: '#/tables/dataTable'
|
||||
cells:
|
||||
- row: RCP030MT
|
||||
column: value
|
||||
- row: RCP030MT
|
||||
column: valid
|
||||
- row: RCP033MT
|
||||
column: value
|
||||
- row: RCP033MT
|
||||
column: valid
|
||||
```
|
||||
|
||||
**functionParameters 参数说明**:
|
||||
|
||||
- 参数为数组形式,每个元素是要采集的数据标识符
|
||||
- 每个参数返回 2 个值:数据值和验证结果
|
||||
|
||||
**返回值格式**:
|
||||
|
||||
- 第 1 个值:第 1 个参数的数据值
|
||||
- 第 2 个值:第 1 个参数的验证结果 (true/false)
|
||||
- 第 3 个值:第 2 个参数的数据值
|
||||
- 第 4 个值:第 2 个参数的验证结果 (true/false)
|
||||
- 以此类推...
|
||||
|
||||
**注意事项**:
|
||||
|
||||
- **不需要**指定 `channel`
|
||||
- `functionParameters` 为数组,不能为空
|
||||
- `dataFields` 中的单元格数量必须是参数数量的 2 倍
|
||||
- 验证结果字段类型应设置为 `boolean`
|
||||
|
||||
---
|
||||
|
||||
## 字段映射
|
||||
|
||||
### 基本规则
|
||||
|
||||
**dataFields 是一个 fieldSelector 数组,每个 fieldSelector 可以是:**
|
||||
|
||||
1. **包含 cells 的对象** - 用于 Grid 表格,通过 `row` 和 `column` 定位
|
||||
2. **包含 fields 的对象** - 用于 Form 表格,直接指定字段 ID
|
||||
3. **ignore 对象** - `ignore: true` 用于跳过不需要的返回值
|
||||
|
||||
**注意**:
|
||||
|
||||
- 每个 fieldSelector 中的 cells 或 fields 可以包含多个项
|
||||
- **所有 fieldSelector 消耗的返回值总数必须与函数返回值数量相等**
|
||||
- 包含 cells 的 fieldSelector:消耗 cells 数组长度个返回值
|
||||
- 包含 fields 的 fieldSelector:消耗 fields 数组长度个返回值
|
||||
- ignore 的 fieldSelector:消耗 1 个返回值
|
||||
- 可以在同一个 dataFields 中混合使用不同类型的 fieldSelector
|
||||
|
||||
### Grid 类型表格 - 使用 cells
|
||||
|
||||
```yaml
|
||||
dataFields:
|
||||
- tableRef: '#/tables/gridTable'
|
||||
cells:
|
||||
- row: sensor1
|
||||
column: value1
|
||||
- row: sensor1
|
||||
column: value2
|
||||
```
|
||||
|
||||
### Form 类型表格 - 使用 fields
|
||||
|
||||
```yaml
|
||||
dataFields:
|
||||
- tableRef: '#/tables/formTable'
|
||||
fields:
|
||||
- field1
|
||||
- field2
|
||||
- field3
|
||||
```
|
||||
|
||||
### 忽略返回值
|
||||
|
||||
如果函数返回 5 个值,但只需要其中 4 个,忽略第 3 个:
|
||||
|
||||
```yaml
|
||||
dataFields:
|
||||
- tableRef: '#/tables/gridTable'
|
||||
cells:
|
||||
- row: sensor1
|
||||
column: value1
|
||||
- row: sensor1
|
||||
column: value2
|
||||
- ignore: true # 忽略第3个返回值
|
||||
- tableRef: '#/tables/gridTable'
|
||||
cells:
|
||||
- row: sensor1
|
||||
column: value4
|
||||
- row: sensor1
|
||||
column: value5
|
||||
```
|
||||
|
||||
### 混合使用
|
||||
|
||||
`ignore` 可以与 `cells` 和 `fields` 自由混合,不局限于单一类型:
|
||||
|
||||
```yaml
|
||||
dataFields:
|
||||
- tableRef: '#/tables/gridTable'
|
||||
cells:
|
||||
- row: sensor1
|
||||
column: value1
|
||||
- tableRef: '#/tables/formTable'
|
||||
fields:
|
||||
- field1
|
||||
- field2
|
||||
- ignore: true # 忽略第4个返回值
|
||||
- tableRef: '#/tables/gridTable'
|
||||
cells:
|
||||
- row: sensor2
|
||||
column: value5
|
||||
- ignore: true # 忽略第6个返回值
|
||||
- tableRef: '#/tables/formTable'
|
||||
fields:
|
||||
- field3
|
||||
```
|
||||
|
||||
**说明**:上述示例处理 7 个返回值:
|
||||
- 第 1 个 → gridTable 的 cell
|
||||
- 第 2-3 个 → formTable 的 field1 和 field2
|
||||
- 第 4 个 → 忽略
|
||||
- 第 5 个 → gridTable 的 cell
|
||||
- 第 6 个 → 忽略
|
||||
- 第 7 个 → formTable 的 field3
|
||||
96
procedures/README_MOCK_DATA.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Mock Procedures 数据说明
|
||||
|
||||
## 文件位置
|
||||
`procedures/mock_procedures.json`
|
||||
|
||||
## 数据结构
|
||||
|
||||
```json
|
||||
{
|
||||
"mockProcedures": [
|
||||
{
|
||||
"id": "规程ID",
|
||||
"name": "规程名称",
|
||||
"version": "版本号",
|
||||
"description": "规程描述",
|
||||
"taskGroups": [
|
||||
{
|
||||
"id": "任务组ID",
|
||||
"name": "任务组名称",
|
||||
"steps": [
|
||||
{
|
||||
"id": "步骤ID",
|
||||
"type": "步骤类型",
|
||||
"content": "步骤内容",
|
||||
"status": "步骤状态",
|
||||
"tableRefs": ["表格引用ID"],
|
||||
"highlightFields": ["高亮字段ID"]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tables": [
|
||||
{
|
||||
"id": "表格ID",
|
||||
"name": "表格名称",
|
||||
"description": "表格描述(可选)",
|
||||
"columns": [
|
||||
{
|
||||
"id": "字段ID",
|
||||
"name": "字段名称",
|
||||
"type": "字段类型",
|
||||
"unit": "单位(可选)",
|
||||
"isRequired": false,
|
||||
"isHighlighted": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"totalSteps": 总步骤数(可选,会自动计算),
|
||||
"completedSteps": 已完成步骤数(可选,会自动计算)
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 枚举值说明
|
||||
|
||||
### 步骤类型 (type)
|
||||
- `Manual`: 手动步骤
|
||||
- `Automatic`: 自动步骤
|
||||
|
||||
### 步骤状态 (status)
|
||||
- `Pending`: 待执行
|
||||
- `InProgress`: 执行中
|
||||
- `Confirmed`: 已确认(手动)
|
||||
- `Passed`: 已通过
|
||||
- `Failed`: 未通过
|
||||
- `Skipped`: 已跳过
|
||||
|
||||
### 字段类型 (type)
|
||||
- `text`: 文本
|
||||
- `numeric`: 数值
|
||||
- `boolean`: 布尔值
|
||||
|
||||
## 使用说明
|
||||
|
||||
1. **添加新的 Mock 规程**:在 `mockProcedures` 数组中添加新对象
|
||||
2. **修改现有规程**:直接编辑对应的 JSON 对象
|
||||
3. **删除规程**:从数组中移除对应的对象
|
||||
|
||||
## 注意事项
|
||||
|
||||
- JSON 中的特殊字符(如 `<`, `>`, `&`, `"`, `'`)会被正确处理,无需转义
|
||||
- 多行文本使用 `\n` 表示换行
|
||||
- 如果不提供 `totalSteps` 和 `completedSteps`,系统会自动计算
|
||||
- `tableRefs` 和 `highlightFields` 是可选字段
|
||||
|
||||
## 当前 Mock 数据
|
||||
|
||||
1. **LONG_NAME_TEST**: 超长名称测试
|
||||
2. **NO_VERSION**: 无版本号测试
|
||||
3. **SPECIAL_CHAR_<>{}[]**: 特殊字符测试
|
||||
4. **EMPTY_DESC**: 空描述测试
|
||||
5. **LONG_DESC**: 超长描述测试(23个步骤,3个任务组)
|
||||
6. **TEST-2024-12-31_V1.0**: 日期戳测试
|
||||
7. **MULTILINE**: 多行文本测试
|
||||
367
procedures/mock_procedures.json
Normal file
@@ -0,0 +1,367 @@
|
||||
{
|
||||
"mockProcedures": [
|
||||
{
|
||||
"id": "LONG_NAME_TEST",
|
||||
"name": "这是一个非常非常非常非常非常非常长的规程名称,用于测试UI在处理超长名称时的显示效果和换行逻辑是否正常工作",
|
||||
"version": "1.0.0-longname",
|
||||
"description": "这个规程专门用于测试长名称和长描述的显示效果。在实际工程中,规程名称可能会很长,需要确保UI能够正确处理这种情况。",
|
||||
"taskGroups": [
|
||||
{
|
||||
"id": "longTestGroup",
|
||||
"name": "超长名称测试任务组",
|
||||
"steps": [
|
||||
{
|
||||
"id": "longStep1",
|
||||
"type": "Manual",
|
||||
"content": "这是一个非常详细的步骤描述,包含了大量的文字说明。在实际测试过程中,可能需要详细说明每一个操作步骤,确保操作人员能够准确理解并执行。这种长描述可能会包含安全注意事项、操作顺序、参数设置等多方面的内容。操作要点:检查显示是否完整,文字是否能够正常换行,滚动条是否正常工作。",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "longStep2",
|
||||
"type": "Manual",
|
||||
"content": "第二个步骤的描述相对简短一些。继续测试各项功能是否正常。",
|
||||
"status": "Pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tables": []
|
||||
},
|
||||
{
|
||||
"id": "NO_VERSION",
|
||||
"name": "无版本号规程",
|
||||
"version": "",
|
||||
"description": "这个规程没有版本号,用于测试系统对缺失版本信息的处理能力",
|
||||
"taskGroups": [
|
||||
{
|
||||
"id": "noVersionGroup",
|
||||
"name": "测试任务组",
|
||||
"steps": [
|
||||
{
|
||||
"id": "step1",
|
||||
"type": "Manual",
|
||||
"content": "执行基本测试步骤。检查系统是否能正常处理无版本规程。",
|
||||
"status": "Pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tables": []
|
||||
},
|
||||
{
|
||||
"id": "SPECIAL_CHAR_<>{}[]",
|
||||
"name": "特殊字符测试 <HTML> {JSON} [Array]",
|
||||
"version": "2.0",
|
||||
"description": "包含特殊字符: < > { } [ ] & \" ' 的规程,测试HTML转义和显示",
|
||||
"taskGroups": [
|
||||
{
|
||||
"id": "specialCharGroup",
|
||||
"name": "特殊字符 <测试> 组",
|
||||
"steps": [
|
||||
{
|
||||
"id": "specialStep1",
|
||||
"type": "Manual",
|
||||
"content": "测试内容: 检查 <标签> 和 {对象} 以及 [数组] 的显示。确认特殊字符 & \" ' 能正确显示。",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "specialStep2",
|
||||
"type": "Manual",
|
||||
"content": "XML标签测试: <root><child>内容</child></root>。检查是否被误解析为HTML。",
|
||||
"status": "Pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tables": []
|
||||
},
|
||||
{
|
||||
"id": "EMPTY_DESC",
|
||||
"name": "空描述测试",
|
||||
"version": "1.0",
|
||||
"description": "",
|
||||
"taskGroups": [
|
||||
{
|
||||
"id": "emptyGroup",
|
||||
"name": "任务组",
|
||||
"steps": [
|
||||
{
|
||||
"id": "emptyStep1",
|
||||
"type": "Manual",
|
||||
"content": "",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "emptyStep2",
|
||||
"type": "Manual",
|
||||
"content": "正常步骤内容",
|
||||
"status": "Pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tables": []
|
||||
},
|
||||
{
|
||||
"id": "LONG_DESC",
|
||||
"name": "超长描述测试规程",
|
||||
"version": "3.0.1",
|
||||
"description": "这是一个包含大量步骤和详细描述的规程,用于测试系统在处理复杂规程时的性能和稳定性。\n规程包含多个任务组,每个任务组包含多个步骤。\n这种规程通常用于复杂的校准或验证过程。",
|
||||
"taskGroups": [
|
||||
{
|
||||
"id": "preparation",
|
||||
"name": "准备阶段",
|
||||
"steps": [
|
||||
{
|
||||
"id": "prep_step_1",
|
||||
"type": "Manual",
|
||||
"content": "准备步骤 1: 详细说明操作内容和注意事项。这是第 1 个步骤,需要仔细执行。",
|
||||
"status": "Confirmed"
|
||||
},
|
||||
{
|
||||
"id": "prep_step_2",
|
||||
"type": "Manual",
|
||||
"content": "准备步骤 2: 详细说明操作内容和注意事项。这是第 2 个步骤,需要仔细执行。",
|
||||
"status": "Confirmed"
|
||||
},
|
||||
{
|
||||
"id": "prep_step_3",
|
||||
"type": "Manual",
|
||||
"content": "准备步骤 3: 详细说明操作内容和注意事项。这是第 3 个步骤,需要仔细执行。",
|
||||
"status": "Confirmed"
|
||||
},
|
||||
{
|
||||
"id": "prep_step_4",
|
||||
"type": "Manual",
|
||||
"content": "准备步骤 4: 详细说明操作内容和注意事项。这是第 4 个步骤,需要仔细执行。",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "prep_step_5",
|
||||
"type": "Manual",
|
||||
"content": "准备步骤 5: 详细说明操作内容和注意事项。这是第 5 个步骤,需要仔细执行。",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "prep_step_6",
|
||||
"type": "Manual",
|
||||
"content": "准备步骤 6: 详细说明操作内容和注意事项。这是第 6 个步骤,需要仔细执行。",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "prep_step_7",
|
||||
"type": "Manual",
|
||||
"content": "准备步骤 7: 详细说明操作内容和注意事项。这是第 7 个步骤,需要仔细执行。",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "prep_step_8",
|
||||
"type": "Manual",
|
||||
"content": "准备步骤 8: 详细说明操作内容和注意事项。这是第 8 个步骤,需要仔细执行。",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "prep_step_9",
|
||||
"type": "Manual",
|
||||
"content": "准备步骤 9: 详细说明操作内容和注意事项。这是第 9 个步骤,需要仔细执行。",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "prep_step_10",
|
||||
"type": "Manual",
|
||||
"content": "准备步骤 10: 详细说明操作内容和注意事项。这是第 10 个步骤,需要仔细执行。",
|
||||
"status": "Pending"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "measurement",
|
||||
"name": "测量阶段",
|
||||
"steps": [
|
||||
{
|
||||
"id": "meas_step_1",
|
||||
"type": "Automatic",
|
||||
"content": "测量步骤 1: 进行精确的测量和数据采集。",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "meas_step_2",
|
||||
"type": "Automatic",
|
||||
"content": "测量步骤 2: 进行精确的测量和数据采集。",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "meas_step_3",
|
||||
"type": "Automatic",
|
||||
"content": "测量步骤 3: 进行精确的测量和数据采集。",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "meas_step_4",
|
||||
"type": "Automatic",
|
||||
"content": "测量步骤 4: 进行精确的测量和数据采集。",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "meas_step_5",
|
||||
"type": "Automatic",
|
||||
"content": "测量步骤 5: 进行精确的测量和数据采集。",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "meas_step_6",
|
||||
"type": "Automatic",
|
||||
"content": "测量步骤 6: 进行精确的测量和数据采集。",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "meas_step_7",
|
||||
"type": "Automatic",
|
||||
"content": "测量步骤 7: 进行精确的测量和数据采集。",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "meas_step_8",
|
||||
"type": "Automatic",
|
||||
"content": "测量步骤 8: 进行精确的测量和数据采集。",
|
||||
"status": "Pending"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "verification",
|
||||
"name": "验证阶段",
|
||||
"steps": [
|
||||
{
|
||||
"id": "verify_step_1",
|
||||
"type": "Manual",
|
||||
"content": "验证步骤 1: 确认测量结果的准确性和可靠性。",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "verify_step_2",
|
||||
"type": "Manual",
|
||||
"content": "验证步骤 2: 确认测量结果的准确性和可靠性。",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "verify_step_3",
|
||||
"type": "Manual",
|
||||
"content": "验证步骤 3: 确认测量结果的准确性和可靠性。",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "verify_step_4",
|
||||
"type": "Manual",
|
||||
"content": "验证步骤 4: 确认测量结果的准确性和可靠性。",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "verify_step_5",
|
||||
"type": "Manual",
|
||||
"content": "验证步骤 5: 确认测量结果的准确性和可靠性。",
|
||||
"status": "Pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tables": [],
|
||||
"totalSteps": 23,
|
||||
"completedSteps": 3
|
||||
},
|
||||
{
|
||||
"id": "TEST-2024-12-31_V1.0",
|
||||
"name": "日期戳测试规程 2024-12-31",
|
||||
"version": "V1.0",
|
||||
"description": "创建日期: 2024-12-31\n修订日期: 2024-12-31\n\n这个规程使用了日期戳命名格式,用于测试系统对不同命名约定的支持。",
|
||||
"taskGroups": [
|
||||
{
|
||||
"id": "dateTestGroup",
|
||||
"name": "日期戳测试组",
|
||||
"steps": [
|
||||
{
|
||||
"id": "dateStep1",
|
||||
"type": "Manual",
|
||||
"content": "检查日期格式显示: 2024-12-31。验证日期解析功能。",
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "dateStep2",
|
||||
"type": "Automatic",
|
||||
"content": "测试时间戳: 2024-12-31 23:59:59。验证时间格式处理。",
|
||||
"status": "Pending",
|
||||
"tableRefs": [
|
||||
"dateInfoTable"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tables": [
|
||||
{
|
||||
"id": "dateInfoTable",
|
||||
"name": "日期信息表",
|
||||
"columns": [
|
||||
{
|
||||
"id": "createDate",
|
||||
"name": "创建日期",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"id": "modifyDate",
|
||||
"name": "修改日期",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "MULTILINE",
|
||||
"name": "多行文本测试",
|
||||
"version": "1.0",
|
||||
"description": "第一行描述\n第二行描述\n第三行描述\n\n这个规程用于测试多行文本的显示效果。\n包括:\n- 换行处理\n- 段落间距\n- 列表显示",
|
||||
"taskGroups": [
|
||||
{
|
||||
"id": "multilineGroup",
|
||||
"name": "多行测试组",
|
||||
"steps": [
|
||||
{
|
||||
"id": "multilineStep1",
|
||||
"type": "Manual",
|
||||
"content": "步骤说明:\n1. 第一步操作\n2. 第二步操作\n3. 第三步操作\n\n注意事项:\n- 注意A\n- 注意B\n- 注意C\n\n按照上述步骤执行,确保每一步都正确完成,记录执行结果。",
|
||||
"status": "Pending",
|
||||
"tableRefs": [
|
||||
"multilineTable"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "multilineStep2",
|
||||
"type": "Manual",
|
||||
"content": "这是一个包含\n多行\n文本\n的步骤\n\n测试多行显示效果",
|
||||
"status": "Pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tables": [
|
||||
{
|
||||
"id": "multilineTable",
|
||||
"name": "多行数据表",
|
||||
"columns": [
|
||||
{
|
||||
"id": "description",
|
||||
"name": "描述",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"id": "notes",
|
||||
"name": "备注\n(多行)",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
6009
procedures/rcp63.json
Normal file
700
procedures/simple_rcp63.yaml
Normal file
@@ -0,0 +1,700 @@
|
||||
testActions:
|
||||
prereq_147: &prereq_147
|
||||
id: prereq_147
|
||||
document: '<p>在热停堆工况(热功率稳定),平均温度最大允许变化<0.2℃;若KIC中显示的三个环路平均温度变化AT>0.2℃,则采取数据滑动平均方式,期间确保三个环路平均温度的滑动平均值最大最小值之差<0.2℃、测量时间大于滑动周期。</p>'
|
||||
mode: manual
|
||||
dataFields:
|
||||
- tableRef: '#/tables/manualConfirm'
|
||||
fields: [confirm_147]
|
||||
prereq_150: &prereq_150
|
||||
id: prereq_150
|
||||
document: '<p>三个蒸发器处于稳定状态,压力一致。</p>'
|
||||
mode: manual
|
||||
dataFields:
|
||||
- tableRef: '#/tables/manualConfirm'
|
||||
fields: [confirm_150]
|
||||
prereq_43: &prereq_43
|
||||
id: prereq_43
|
||||
document: '<p>蒸汽发生器水位稳定。</p>'
|
||||
mode: manual
|
||||
dataFields:
|
||||
- tableRef: '#/tables/manualConfirm'
|
||||
fields: [confirm_43]
|
||||
prereq_75: &prereq_75
|
||||
id: prereq_75
|
||||
document: '<p>RCV的一个下泄孔板投入运行;确保测量期间无下泄孔板相关操作,一回路温度保持稳定,测量结果若超差需要分析下泄流量影响。</p>'
|
||||
mode: manual
|
||||
dataFields:
|
||||
- tableRef: '#/tables/manualConfirm'
|
||||
fields: [confirm_75]
|
||||
prereq_92: &prereq_92
|
||||
id: prereq_92
|
||||
document: '<p>其它模式/工况下需进行相应的独立风险分析。</p>'
|
||||
mode: manual
|
||||
dataFields:
|
||||
- tableRef: '#/tables/manualConfirm'
|
||||
fields: [confirm_92]
|
||||
pre_153: &pre_153
|
||||
id: pre_153
|
||||
document: '<p>本程序适用于热停堆(热功率稳定)工况下执行RCP63程序,统一进行Sensor Check部分使用。</p>'
|
||||
mode: manual
|
||||
dataFields:
|
||||
- tableRef: '#/tables/manualConfirm'
|
||||
fields: [confirm_153]
|
||||
pre_136: &pre_136
|
||||
id: pre_136
|
||||
document: '<p>执行前确认KIC中一回路三个环路平均温度变化量不超过0.2℃。</p>'
|
||||
mode: auto
|
||||
functionType: DATA_ACQUISITION_WITH_VALIDATION
|
||||
functionParameters:
|
||||
- RCP614KM
|
||||
- RCP618KM
|
||||
- RCP622KM
|
||||
dataFields:
|
||||
- tableRef: '#/tables/temperatureCheckTable'
|
||||
fields: [loop1TempVariation, loop2TempVariation, loop3TempVariation]
|
||||
pre_137: &pre_137
|
||||
id: pre_137
|
||||
document: '<p>若三环路平均温度变化超过0.2℃,记录滑动周期、各环路滑动平均最大最小值之差及最短数据测量时间,确认均满足≤0.2℃且测量时间>滑动周期。</p>'
|
||||
mode: auto
|
||||
functionType: DATA_ACQUISITION_WITH_VALIDATION
|
||||
functionParameters:
|
||||
- RCP614KM
|
||||
- RCP618KM
|
||||
- RCP622KM
|
||||
- RCP612KM
|
||||
- RCP616KM
|
||||
- RCP620KM
|
||||
- RCP613KM
|
||||
- RCP617KM
|
||||
- RCP621KM
|
||||
- RCP611KM
|
||||
- RCP615KM
|
||||
- RCP619KM
|
||||
dataFields:
|
||||
- tableRef: '#/tables/temperatureCheckTable'
|
||||
fields: [loop1TempVariation, loop2TempVariation, loop3TempVariation]
|
||||
pre_138: &pre_138
|
||||
id: pre_138
|
||||
document: '<p>确认 RPR、RPN上无相关的试验检修工作。</p>'
|
||||
mode: manual
|
||||
dataFields:
|
||||
- tableRef: '#/tables/manualConfirm'
|
||||
fields: [confirm_138]
|
||||
pre_139: &pre_139
|
||||
id: pre_139
|
||||
document: '<p>按附表1检查相关模拟量指示并记录,确认结果满意。</p>'
|
||||
mode: auto
|
||||
functionType: DATA_ACQUISITION
|
||||
functionParameters:
|
||||
- RCP614KM
|
||||
- RCP618KM
|
||||
- RCP622KM
|
||||
dataFields:
|
||||
- tableRef: '#/tables/alarmCheckTable'
|
||||
cells:
|
||||
- row: RCP614KM
|
||||
column: preInspectionStatus
|
||||
- row: RCP618KM
|
||||
column: preInspectionStatus
|
||||
- row: RCP622KM
|
||||
column: preInspectionStatus
|
||||
pre_140: &pre_140
|
||||
id: pre_140
|
||||
document: '<p>试验前根据附表1报警清单检查报警,确认无异常报警,方可进行后续检查工作。</p>'
|
||||
mode: auto
|
||||
functionType: DATA_ACQUISITION
|
||||
functionParameters:
|
||||
- RCP455AA
|
||||
- RPAB138AA
|
||||
- RPAB167AA
|
||||
- RPAB171AA
|
||||
dataFields:
|
||||
- tableRef: '#/tables/alarmCheckTable'
|
||||
cells:
|
||||
- row: RCP455AA
|
||||
column: preInspectionStatus
|
||||
- row: RPAB138AA
|
||||
column: preInspectionStatus
|
||||
- row: RPAB167AA
|
||||
column: preInspectionStatus
|
||||
- row: RPAB171AA
|
||||
column: preInspectionStatus
|
||||
x5KicReadingsPreCheck: &x5KicReadingsPreCheck
|
||||
id: x5KicReadingsPreCheck
|
||||
document: '<p>采集X5端子排拆前KIC指示值。</p>'
|
||||
mode: auto
|
||||
functionType: DATA_ACQUISITION
|
||||
functionParameters:
|
||||
- RCP030MT
|
||||
- RCP033MT
|
||||
dataFields:
|
||||
- tableRef: '#/tables/kicReadingsTable'
|
||||
cells:
|
||||
- row: RCP030MT
|
||||
column: preInspectionValue
|
||||
- row: RCP033MT
|
||||
column: preInspectionValue
|
||||
x5KicReadingsPostCheck: &x5KicReadingsPostCheck
|
||||
id: x5KicReadingsPostCheck
|
||||
document: '<p>采集X5端子排恢复后KIC指示值。</p>'
|
||||
mode: auto
|
||||
functionType: DATA_ACQUISITION
|
||||
functionParameters:
|
||||
- RCP030MT
|
||||
- RCP033MT
|
||||
dataFields:
|
||||
- tableRef: '#/tables/kicReadingsTable'
|
||||
cells:
|
||||
- row: RCP030MT
|
||||
column: postInspectionValue
|
||||
- row: RCP033MT
|
||||
column: postInspectionValue
|
||||
rcp030mtResistance: &rcp030mtResistance
|
||||
id: rcp030mtResistance
|
||||
mode: auto
|
||||
functionType: RESISTANCE_4WIRE
|
||||
channel: input1
|
||||
functionParameters:
|
||||
document: '<p><strong>测量位置:</strong>KCSO13AR/EB911/X5</p><p><strong>测量项目:</strong>R1/2, R1/3, R1/4, R2/3, R2/4, R3/4, R/4wires</p>'
|
||||
dataFields:
|
||||
- tableRef: '#/tables/resistanceMeasurementTable'
|
||||
cells:
|
||||
- row: RCP030MT
|
||||
column: r12
|
||||
- row: RCP030MT
|
||||
column: r13
|
||||
- row: RCP030MT
|
||||
column: r14
|
||||
- row: RCP030MT
|
||||
column: r23
|
||||
- row: RCP030MT
|
||||
column: r24
|
||||
- row: RCP030MT
|
||||
column: r34
|
||||
- row: RCP030MT
|
||||
column: r4wires
|
||||
uploadStrategy: inherit
|
||||
rcp030mtInsulation: &rcp030mtInsulation
|
||||
id: rcp030mtInsulation
|
||||
mode: auto
|
||||
functionType: INSULATION
|
||||
channel: input1
|
||||
functionParameters:
|
||||
testVoltage: 50
|
||||
testTime: 20
|
||||
dischargeTime: 5
|
||||
document: '<p><strong>测量位置:</strong>KCSO13AR/EB911/X5</p><p><strong>注意事项:</strong></p><ul><li>测试电压50V</li><li>测试时间不小于20秒</li><li>测量后探头对地放电</li></ul>'
|
||||
dataFields:
|
||||
- tableRef: '#/tables/resistanceMeasurementTable'
|
||||
cells:
|
||||
- row: RCP030MT
|
||||
column: insulation
|
||||
uploadStrategy: inherit
|
||||
rcp033mtResistance: &rcp033mtResistance
|
||||
id: rcp033mtResistance
|
||||
mode: auto
|
||||
functionType: RESISTANCE_4WIRE
|
||||
channel: input1
|
||||
functionParameters:
|
||||
document: '<p><strong>测量位置:</strong>KCSO13AR/EB911/X5</p><p><strong>测量项目:</strong>R1/2, R1/3, R1/4, R2/3, R2/4, R3/4, R/4wires</p>'
|
||||
dataFields:
|
||||
- tableRef: '#/tables/resistanceMeasurementTable'
|
||||
cells:
|
||||
- row: RCP033MT
|
||||
column: r12
|
||||
- row: RCP033MT
|
||||
column: r13
|
||||
- row: RCP033MT
|
||||
column: r14
|
||||
- row: RCP033MT
|
||||
column: r23
|
||||
- row: RCP033MT
|
||||
column: r24
|
||||
- row: RCP033MT
|
||||
column: r34
|
||||
- row: RCP033MT
|
||||
column: r4wires
|
||||
uploadStrategy: inherit
|
||||
rcp033mtInsulation: &rcp033mtInsulation
|
||||
id: rcp033mtInsulation
|
||||
mode: auto
|
||||
functionType: INSULATION
|
||||
channel: input1
|
||||
functionParameters:
|
||||
testVoltage: 50
|
||||
testTime: 20
|
||||
dischargeTime: 5
|
||||
document: '<p><strong>测量位置:</strong>KCSO13AR/EB911/X5</p><p><strong>注意事项:</strong></p><ul><li>测试电压50V</li><li>测试时间不小于20秒</li><li>测量后探头对地放电</li><li>复归保险FB059 F11和F12</li></ul>'
|
||||
dataFields:
|
||||
- tableRef: '#/tables/resistanceMeasurementTable'
|
||||
cells:
|
||||
- row: RCP033MT
|
||||
column: insulation
|
||||
uploadStrategy: inherit
|
||||
rcp031mtResistance: &rcp031mtResistance
|
||||
id: rcp031mtResistance
|
||||
mode: auto
|
||||
functionType: RESISTANCE_4WIRE
|
||||
channel: input1
|
||||
functionParameters:
|
||||
document: '<p><strong>测量位置:</strong>KCS013AR/EB911/X6</p><p><strong>测量项目:</strong>R1/2, R1/3, R1/4, R2/3, R2/4, R3/4, R/4wires</p>'
|
||||
dataFields:
|
||||
- tableRef: '#/tables/resistanceMeasurementTable'
|
||||
cells:
|
||||
- row: RCP031MT
|
||||
column: r12
|
||||
- row: RCP031MT
|
||||
column: r13
|
||||
- row: RCP031MT
|
||||
column: r14
|
||||
- row: RCP031MT
|
||||
column: r23
|
||||
- row: RCP031MT
|
||||
column: r24
|
||||
- row: RCP031MT
|
||||
column: r34
|
||||
- row: RCP031MT
|
||||
column: r4wires
|
||||
uploadStrategy: inherit
|
||||
rcp031mtInsulation: &rcp031mtInsulation
|
||||
id: rcp031mtInsulation
|
||||
mode: auto
|
||||
functionType: INSULATION
|
||||
channel: input1
|
||||
functionParameters:
|
||||
testVoltage: 50
|
||||
testTime: 20
|
||||
dischargeTime: 5
|
||||
document: '<p><strong>测量位置:</strong>KCS013AR/EB911/X6</p><p><strong>注意事项:</strong></p><ul><li>测试电压50V</li><li>测试时间不小于20秒</li><li>测量后探头对地放电</li></ul>'
|
||||
dataFields:
|
||||
- tableRef: '#/tables/resistanceMeasurementTable'
|
||||
cells:
|
||||
- row: RCP031MT
|
||||
column: insulation
|
||||
uploadStrategy: inherit
|
||||
rcp034mtResistance: &rcp034mtResistance
|
||||
id: rcp034mtResistance
|
||||
mode: auto
|
||||
functionType: RESISTANCE_4WIRE
|
||||
channel: input1
|
||||
functionParameters:
|
||||
document: '<p><strong>测量位置:</strong>KCS013AR/EB911/X6</p><p><strong>测量项目:</strong>R1/2, R1/3, R1/4, R2/3, R2/4, R3/4, R/4wires</p>'
|
||||
dataFields:
|
||||
- tableRef: '#/tables/resistanceMeasurementTable'
|
||||
cells:
|
||||
- row: RCP034MT
|
||||
column: r12
|
||||
- row: RCP034MT
|
||||
column: r13
|
||||
- row: RCP034MT
|
||||
column: r14
|
||||
- row: RCP034MT
|
||||
column: r23
|
||||
- row: RCP034MT
|
||||
column: r24
|
||||
- row: RCP034MT
|
||||
column: r34
|
||||
- row: RCP034MT
|
||||
column: r4wires
|
||||
uploadStrategy: inherit
|
||||
rcp034mtInsulation: &rcp034mtInsulation
|
||||
id: rcp034mtInsulation
|
||||
mode: auto
|
||||
functionType: INSULATION
|
||||
channel: input1
|
||||
functionParameters:
|
||||
testVoltage: 50
|
||||
testTime: 20
|
||||
dischargeTime: 5
|
||||
document: '<p><strong>测量位置:</strong>KCS013AR/EB911/X6</p><p><strong>注意事项:</strong></p><ul><li>测试电压50V</li><li>测试时间不小于20秒</li><li>测量后探头对地放电</li></ul>'
|
||||
dataFields:
|
||||
- tableRef: '#/tables/resistanceMeasurementTable'
|
||||
cells:
|
||||
- row: RCP034MT
|
||||
column: insulation
|
||||
uploadStrategy: inherit
|
||||
rcp043mtResistance: &rcp043mtResistance
|
||||
id: rcp043mtResistance
|
||||
mode: auto
|
||||
functionType: RESISTANCE_3WIRE
|
||||
channel: input1
|
||||
functionParameters:
|
||||
document: '<p><strong>测量位置:</strong>KCSO13AR/EB902/X1</p><p><strong>测量项目:</strong>R2/3, R2/4, R3/4</p><p><strong>注意:</strong>3线制传感器</p>'
|
||||
dataFields:
|
||||
- tableRef: '#/tables/resistanceMeasurementTable'
|
||||
cells:
|
||||
- row: RCP043MT
|
||||
column: r23
|
||||
- row: RCP043MT
|
||||
column: r24
|
||||
- row: RCP043MT
|
||||
column: r34
|
||||
uploadStrategy: inherit
|
||||
rcp043mtInsulation: &rcp043mtInsulation
|
||||
id: rcp043mtInsulation
|
||||
mode: auto
|
||||
functionType: INSULATION
|
||||
channel: input1
|
||||
functionParameters:
|
||||
testVoltage: 50
|
||||
testTime: 20
|
||||
dischargeTime: 5
|
||||
document: '<p><strong>测量位置:</strong>KCSO13AR/EB902/X1</p><p><strong>注意事项:</strong></p><ul><li>测试电压50V</li><li>测试时间不小于20秒</li><li>测量后探头对地放电</li></ul>'
|
||||
dataFields:
|
||||
- tableRef: '#/tables/resistanceMeasurementTable'
|
||||
cells:
|
||||
- row: RCP043MT
|
||||
column: insulation
|
||||
uploadStrategy: inherit
|
||||
rcp044mtResistance: &rcp044mtResistance
|
||||
id: rcp044mtResistance
|
||||
mode: auto
|
||||
functionType: RESISTANCE_3WIRE
|
||||
channel: input1
|
||||
functionParameters:
|
||||
document: '<p><strong>测量位置:</strong>KCSO13AR/EB902/X1</p><p><strong>测量项目:</strong>R2/3, R2/4, R3/4</p><p><strong>注意:</strong>3线制传感器</p>'
|
||||
dataFields:
|
||||
- tableRef: '#/tables/resistanceMeasurementTable'
|
||||
cells:
|
||||
- row: RCP044MT
|
||||
column: r23
|
||||
- row: RCP044MT
|
||||
column: r24
|
||||
- row: RCP044MT
|
||||
column: r34
|
||||
uploadStrategy: inherit
|
||||
rcp044mtInsulation: &rcp044mtInsulation
|
||||
id: rcp044mtInsulation
|
||||
mode: auto
|
||||
functionType: INSULATION
|
||||
channel: input1
|
||||
functionParameters:
|
||||
testVoltage: 50
|
||||
testTime: 20
|
||||
dischargeTime: 5
|
||||
document: '<p><strong>测量位置:</strong>KCSO13AR/EB902/X1</p><p><strong>注意事项:</strong></p><ul><li>测试电压50V</li><li>测试时间不小于20秒</li><li>测量后探头对地放电</li><li>复归保险FB005 F11和F12</li><li>请主控确认报警消除</li></ul>'
|
||||
dataFields:
|
||||
- tableRef: '#/tables/resistanceMeasurementTable'
|
||||
cells:
|
||||
- row: RCP044MT
|
||||
column: insulation
|
||||
uploadStrategy: inherit
|
||||
testActivityGroups:
|
||||
prerequisites: &prerequisites
|
||||
id: prerequisites
|
||||
name: 3.0先决条件
|
||||
actions:
|
||||
- *prereq_147
|
||||
- *prereq_150
|
||||
- *prereq_43
|
||||
- *prereq_75
|
||||
- *prereq_92
|
||||
preInspectionChecks: &preInspectionChecks
|
||||
id: preInspectionChecks
|
||||
name: 5.1试验前状态说明和检查
|
||||
tableRefs:
|
||||
- '#/tables/temperatureCheckTable'
|
||||
- '#/tables/alarmCheckTable'
|
||||
actions:
|
||||
- *pre_153
|
||||
- *pre_136
|
||||
- *pre_137
|
||||
- *pre_138
|
||||
- *pre_139
|
||||
- *pre_140
|
||||
protectionIGroup: &protectionIGroup
|
||||
id: protectionIGroup
|
||||
name: 5.2保护Ⅰ组传感器及线缆绝缘及电阻检查
|
||||
tableRefs:
|
||||
- '#/tables/kicReadingsTable'
|
||||
- '#/tables/resistanceMeasurementTable'
|
||||
actions:
|
||||
- *x5KicReadingsPreCheck
|
||||
- *rcp030mtResistance
|
||||
- *rcp030mtInsulation
|
||||
- *rcp033mtResistance
|
||||
- *rcp033mtInsulation
|
||||
- *x5KicReadingsPostCheck
|
||||
testTaskGroups:
|
||||
prerequisitesTask: &prerequisitesTask
|
||||
id: prerequisitesTask
|
||||
name: 3.0先决条件
|
||||
stages:
|
||||
- *prerequisites
|
||||
preInspectionTask: &preInspectionTask
|
||||
id: preInspectionTask
|
||||
name: 5.1试验前状态说明和检查
|
||||
stages:
|
||||
- *preInspectionChecks
|
||||
protectionITask: &protectionITask
|
||||
id: protectionITask
|
||||
name: 5.2保护Ⅰ组传感器及线缆绝缘及电阻检查
|
||||
stages:
|
||||
- *protectionIGroup
|
||||
resultDisplays:
|
||||
finalResultDisplay: &finalResultDisplay
|
||||
id: finalResultDisplay
|
||||
name: 实验结果汇总
|
||||
document: '<p>显示本次实验的所有测量数据和检查结果</p>'
|
||||
tableRefs:
|
||||
- '#/tables/resistanceMeasurementTable'
|
||||
- '#/tables/temperatureCheckTable'
|
||||
- '#/tables/alarmCheckTable'
|
||||
- '#/tables/kicReadingsTable'
|
||||
procedure:
|
||||
id: SIMPLE63
|
||||
name: 范例 一回路温度传感器绝缘和连续性检查 TP RCP63
|
||||
version: SIMPLE
|
||||
activitySequence:
|
||||
- *prerequisitesTask
|
||||
- *preInspectionTask
|
||||
- *protectionITask
|
||||
- *finalResultDisplay
|
||||
tables:
|
||||
kicReadingsTable:
|
||||
id: kicReadingsTable
|
||||
name: KIC指示读数表
|
||||
description: 检验前后的KIC指示读数
|
||||
tableType: grid
|
||||
isShared: true
|
||||
uploadStrategy: onComplete
|
||||
columnHeaders:
|
||||
- id: preInspectionValue
|
||||
name: 拆前数据
|
||||
type: numeric
|
||||
isRequired: true
|
||||
unit: '℃'
|
||||
- id: postInspectionValue
|
||||
name: 拆后数据
|
||||
type: numeric
|
||||
isRequired: true
|
||||
unit: '℃'
|
||||
rowHeaders:
|
||||
- id: RCP030MT
|
||||
name: RCP030MT
|
||||
- id: RCP033MT
|
||||
name: RCP033MT
|
||||
- id: RCP031MT
|
||||
name: RCP031MT
|
||||
- id: RCP034MT
|
||||
name: RCP034MT
|
||||
- id: RCP043MT
|
||||
name: RCP043MT
|
||||
- id: RCP044MT
|
||||
name: RCP044MT
|
||||
alarmCheckTable:
|
||||
id: alarmCheckTable
|
||||
name: 试验前后报警检查记录表
|
||||
description: 依据TP RCP63的报警检查记录结构
|
||||
tableType: grid
|
||||
isShared: true
|
||||
uploadStrategy: onComplete
|
||||
columnHeaders:
|
||||
- id: signal
|
||||
name: 信号
|
||||
type: text
|
||||
isRequired: false
|
||||
- id: description
|
||||
name: 描述
|
||||
type: text
|
||||
isRequired: false
|
||||
- id: location
|
||||
name: 位置
|
||||
type: text
|
||||
isRequired: false
|
||||
- id: normalState
|
||||
name: 正常状态
|
||||
type: text
|
||||
isRequired: false
|
||||
- id: eventTime
|
||||
name: 出现时间
|
||||
type: text
|
||||
isRequired: false
|
||||
- id: preInspectionStatus
|
||||
name: 拆前状态
|
||||
type: text
|
||||
isRequired: true
|
||||
- id: preInspectionStatusB
|
||||
name: 拆前B列
|
||||
type: text
|
||||
isRequired: false
|
||||
- id: postInspectionStatus
|
||||
name: 拆后状态
|
||||
type: text
|
||||
isRequired: true
|
||||
- id: postInspectionStatusB
|
||||
name: 拆后B列
|
||||
type: text
|
||||
isRequired: false
|
||||
rowHeaders:
|
||||
- id: RCP455AA
|
||||
name: RCP455AA
|
||||
- id: RPAB138AA
|
||||
name: RPA/B138AA
|
||||
- id: RPAB167AA
|
||||
name: RPA/B167AA
|
||||
- id: RPAB171AA
|
||||
name: RPA/B171AA
|
||||
- id: RPA_B079AA
|
||||
name: RPA/B079AA
|
||||
- id: RPA_B039LA
|
||||
name: RPA/B039LA
|
||||
- id: RCP614KM
|
||||
name: RCP614KM
|
||||
- id: RCP618KM
|
||||
name: RCP618KM
|
||||
- id: RCP622KM
|
||||
name: RCP622KM
|
||||
manualConfirm:
|
||||
id: manualConfirm
|
||||
name: 人工确认
|
||||
tableType: form
|
||||
uploadStrategy: immediate
|
||||
layoutConfig:
|
||||
totalColumns: 12
|
||||
fields:
|
||||
- id: confirm_147
|
||||
name: 确认 147
|
||||
type: boolean
|
||||
layoutConfig:
|
||||
labelSpan: 4
|
||||
valueSpan: 8
|
||||
- id: confirm_150
|
||||
name: 确认 150
|
||||
type: boolean
|
||||
layoutConfig:
|
||||
labelSpan: 4
|
||||
valueSpan: 8
|
||||
- id: confirm_43
|
||||
name: 确认 43
|
||||
type: boolean
|
||||
layoutConfig:
|
||||
labelSpan: 4
|
||||
valueSpan: 8
|
||||
- id: confirm_75
|
||||
name: 确认 75
|
||||
type: boolean
|
||||
layoutConfig:
|
||||
labelSpan: 4
|
||||
valueSpan: 8
|
||||
- id: confirm_92
|
||||
name: 确认 92
|
||||
type: boolean
|
||||
layoutConfig:
|
||||
labelSpan: 4
|
||||
valueSpan: 8
|
||||
- id: confirm_153
|
||||
name: 确认 153
|
||||
type: boolean
|
||||
layoutConfig:
|
||||
labelSpan: 4
|
||||
valueSpan: 8
|
||||
- id: confirm_138
|
||||
name: 确认 138
|
||||
type: boolean
|
||||
layoutConfig:
|
||||
labelSpan: 4
|
||||
valueSpan: 8
|
||||
- id: confirm_142
|
||||
name: 确认 142
|
||||
type: boolean
|
||||
layoutConfig:
|
||||
labelSpan: 4
|
||||
valueSpan: 8
|
||||
resistanceMeasurementTable:
|
||||
id: resistanceMeasurementTable
|
||||
name: 电阻测量表
|
||||
description: 传感器的电阻和绝缘测量数据
|
||||
tableType: grid
|
||||
uploadStrategy: onComplete
|
||||
columnHeaders:
|
||||
- id: r12
|
||||
name: R1/2
|
||||
type: numeric
|
||||
isRequired: false
|
||||
unit: Ω
|
||||
- id: r13
|
||||
name: R1/3
|
||||
type: numeric
|
||||
isRequired: false
|
||||
unit: Ω
|
||||
- id: r14
|
||||
name: R1/4
|
||||
type: numeric
|
||||
isRequired: false
|
||||
unit: Ω
|
||||
- id: r23
|
||||
name: R2/3
|
||||
type: numeric
|
||||
isRequired: true
|
||||
unit: Ω
|
||||
- id: r24
|
||||
name: R2/4
|
||||
type: numeric
|
||||
isRequired: true
|
||||
unit: Ω
|
||||
- id: r34
|
||||
name: R3/4
|
||||
type: numeric
|
||||
isRequired: true
|
||||
unit: Ω
|
||||
- id: r4wires
|
||||
name: R/4wires
|
||||
type: numeric
|
||||
isRequired: false
|
||||
unit: Ω
|
||||
- id: insulation
|
||||
name: Insulation
|
||||
type: numeric
|
||||
isRequired: true
|
||||
unit: MΩ
|
||||
rowHeaders:
|
||||
- id: RCP030MT
|
||||
name: RCP030MT
|
||||
- id: RCP033MT
|
||||
name: RCP033MT
|
||||
- id: RCP031MT
|
||||
name: RCP031MT
|
||||
- id: RCP034MT
|
||||
name: RCP034MT
|
||||
- id: RCP043MT
|
||||
name: RCP043MT
|
||||
- id: RCP044MT
|
||||
name: RCP044MT
|
||||
temperatureCheckTable:
|
||||
id: temperatureCheckTable
|
||||
name: 环路温度检查表
|
||||
description: 环路平均温度变化量检查
|
||||
tableType: form
|
||||
uploadStrategy: onComplete
|
||||
layoutConfig:
|
||||
totalColumns: 12
|
||||
fields:
|
||||
- id: loop1TempVariation
|
||||
name: 一环路平均温度变化量
|
||||
type: numeric
|
||||
isRequired: true
|
||||
unit: '℃'
|
||||
layoutConfig:
|
||||
labelSpan: 4
|
||||
valueSpan: 8
|
||||
order: 1
|
||||
- id: loop2TempVariation
|
||||
name: 二环路平均温度变化量
|
||||
type: numeric
|
||||
isRequired: true
|
||||
unit: '℃'
|
||||
layoutConfig:
|
||||
labelSpan: 4
|
||||
valueSpan: 8
|
||||
order: 2
|
||||
- id: loop3TempVariation
|
||||
name: 三环路平均温度变化量
|
||||
type: numeric
|
||||
isRequired: true
|
||||
unit: '℃'
|
||||
layoutConfig:
|
||||
labelSpan: 4
|
||||
valueSpan: 8
|
||||
order: 3
|
||||
59
procedures/simple_test.yaml
Normal file
@@ -0,0 +1,59 @@
|
||||
procedure:
|
||||
id: SIMPLE_TEST
|
||||
name: 简单测试规程
|
||||
version: '1.0'
|
||||
document: |
|
||||
<p>这是一个简单的测试规程,用于演示 ProcedurePlayer 的基本功能。</p>
|
||||
<p>包含一个测试任务组和结果展示。</p>
|
||||
activitySequence:
|
||||
- id: testGroup1
|
||||
name: 测试组1
|
||||
document: '<p>执行基本测试</p>'
|
||||
stages:
|
||||
- id: stage1
|
||||
name: 测试阶段
|
||||
document: '<p>填写测试数据</p>'
|
||||
tableRefs:
|
||||
- '#/tables/testTable'
|
||||
actions:
|
||||
- id: action1
|
||||
document: '<p>请在测试数据表中填写字段1和字段2。</p>'
|
||||
mode: manual
|
||||
dataFields:
|
||||
- tableRef: '#/tables/testTable'
|
||||
fields:
|
||||
- field1
|
||||
- field2
|
||||
- id: resultDisplay
|
||||
name: 结果展示
|
||||
document: '<p>查看测试结果</p>'
|
||||
tableRefs:
|
||||
- '#/tables/testTable'
|
||||
|
||||
tables:
|
||||
testTable:
|
||||
id: testTable
|
||||
name: 测试数据表
|
||||
description: 简单的测试数据表
|
||||
tableType: form
|
||||
uploadStrategy: onComplete
|
||||
layoutConfig:
|
||||
totalColumns: 12
|
||||
fields:
|
||||
- id: field1
|
||||
name: 测试字段1
|
||||
type: text
|
||||
isRequired: true
|
||||
layoutConfig:
|
||||
labelSpan: 4
|
||||
valueSpan: 8
|
||||
order: 1
|
||||
- id: field2
|
||||
name: 测试字段2
|
||||
type: numeric
|
||||
isRequired: true
|
||||
unit: 'V'
|
||||
layoutConfig:
|
||||
labelSpan: 4
|
||||
valueSpan: 8
|
||||
order: 2
|
||||
37
resources.qrc
Normal file
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE RCC>
|
||||
<RCC version="1.0">
|
||||
<qresource prefix="/">
|
||||
<!-- 图标资源 -->
|
||||
<file>icons/app.png</file>
|
||||
<file>icons/ic_rtd.svg</file>
|
||||
<file>icons/ic_rtd_output.svg</file>
|
||||
<file>icons/ic_thermocouple.svg</file>
|
||||
<file>icons/ic_insulation.svg</file>
|
||||
<file>icons/ic_dc_current.svg</file>
|
||||
<file>icons/ic_dc_voltage.svg</file>
|
||||
<file>icons/ic_frequency.svg</file>
|
||||
<file>icons/ic_ac_voltage.svg</file>
|
||||
<file>icons/ic_switch.svg</file>
|
||||
<file>icons/ic_ripple.svg</file>
|
||||
<file>icons/ic_ramp.svg</file>
|
||||
<file>icons/ic_waveform.svg</file>
|
||||
<file>icons/ic_dual_channel.svg</file>
|
||||
<file>icons/ic_trim.svg</file>
|
||||
<file>icons/ic_data.svg</file>
|
||||
<file>icons/ic_wireless.svg</file>
|
||||
<file>icons/ic_sop.svg</file>
|
||||
<file>icons/ic_rcp63.svg</file>
|
||||
<file>icons/ic_settings.svg</file>
|
||||
<file>icons/ic_network_test.svg</file>
|
||||
<file>icons/arrow_down.svg</file>
|
||||
<file>icons/arrow_up.svg</file>
|
||||
|
||||
<!-- 字体资源 (可选,如果需要嵌入字体) -->
|
||||
<!-- <file>fonts/SourceHanSansSC-Regular.otf</file> -->
|
||||
<!-- <file>fonts/SourceHanSansSC-Medium.otf</file> -->
|
||||
<!-- <file>fonts/SourceHanSansSC-Bold.otf</file> -->
|
||||
|
||||
<!-- 配置文件 -->
|
||||
<file>config/launcher_config.json</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
13
run.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
# 设置Qt插件路径
|
||||
export QT_PLUGIN_PATH=/opt/homebrew/Cellar/qt@5/5.15.18/plugins
|
||||
|
||||
# 进入脚本所在目录
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# 确保配置文件存在
|
||||
mkdir -p build/CalibratorLauncher.app/Contents/MacOS/config
|
||||
cp -f config/launcher_config.json build/CalibratorLauncher.app/Contents/MacOS/config/
|
||||
|
||||
# 启动应用
|
||||
exec ./build/CalibratorLauncher.app/Contents/MacOS/CalibratorLauncher
|
||||
602
schema.json
Normal file
@@ -0,0 +1,602 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://example.com/schema/procedure.json",
|
||||
"title": "Procedure Configuration Schema",
|
||||
"description": "Schema for intelligent calibrator procedure configuration. Structure: Procedure → TestTaskGroup/ResultDisplay → TestActivityGroup → TestAction",
|
||||
"type": "object",
|
||||
"required": ["procedure", "tables"],
|
||||
"properties": {
|
||||
"procedure": {
|
||||
"type": "object",
|
||||
"description": "Main procedure definition",
|
||||
"required": ["id", "name", "version", "activitySequence"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique procedure identifier"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Procedure display name"
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Procedure version"
|
||||
},
|
||||
"document": {
|
||||
"type": "string",
|
||||
"description": "Procedure documentation in HTML/Markdown"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "Additional procedure metadata"
|
||||
},
|
||||
"activitySequence": {
|
||||
"type": "array",
|
||||
"description": "Top-level activity sequence: testTaskGroups and resultDisplays",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{ "$ref": "#/$defs/testTaskGroup" },
|
||||
{ "$ref": "#/$defs/resultDisplay" },
|
||||
{ "$ref": "#/$defs/activityReference" }
|
||||
]
|
||||
},
|
||||
"minItems": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"tables": {
|
||||
"type": "object",
|
||||
"description": "Reusable table definitions for data storage and display",
|
||||
"additionalProperties": { "$ref": "#/$defs/tableDefinition" }
|
||||
},
|
||||
"testTaskGroups": {
|
||||
"type": "object",
|
||||
"description": "Reusable test task group definitions",
|
||||
"additionalProperties": { "$ref": "#/$defs/testTaskGroup" }
|
||||
},
|
||||
"testActivityGroups": {
|
||||
"type": "object",
|
||||
"description": "Reusable test activity group definitions",
|
||||
"additionalProperties": { "$ref": "#/$defs/testActivityGroup" }
|
||||
},
|
||||
"testActions": {
|
||||
"type": "object",
|
||||
"description": "Reusable test action definitions",
|
||||
"additionalProperties": { "$ref": "#/$defs/testAction" }
|
||||
},
|
||||
"resultDisplays": {
|
||||
"type": "object",
|
||||
"description": "Reusable result display definitions",
|
||||
"additionalProperties": { "$ref": "#/$defs/resultDisplay" }
|
||||
},
|
||||
"fieldDefinitions": {
|
||||
"type": "object",
|
||||
"description": "Reusable field definitions for tables",
|
||||
"additionalProperties": { "$ref": "#/$defs/fieldDefinition" }
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"testTaskGroup": {
|
||||
"type": "object",
|
||||
"description": "Top-level test task group containing multiple test activity groups",
|
||||
"required": ["id", "name", "stages"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique test task group identifier"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Test task group display name"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "Additional metadata"
|
||||
},
|
||||
"stages": {
|
||||
"type": "array",
|
||||
"description": "Ordered sequence of test activity groups (stages)",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{ "$ref": "#/$defs/testActivityGroup" },
|
||||
{ "$ref": "#/$defs/groupReference" }
|
||||
]
|
||||
},
|
||||
"minItems": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"resultDisplay": {
|
||||
"type": "object",
|
||||
"description": "Result display activity for showing test results",
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique result display identifier"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Result display title"
|
||||
},
|
||||
"document": {
|
||||
"type": "string",
|
||||
"description": "Display instructions in HTML/Markdown"
|
||||
},
|
||||
"tableRefs": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/tableReference" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"testActivityGroup": {
|
||||
"type": "object",
|
||||
"description": "Test activity group containing multiple test actions",
|
||||
"required": ["id", "name", "actions"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique activity group identifier"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Activity group display name"
|
||||
},
|
||||
"document": {
|
||||
"type": "string",
|
||||
"description": "Documentation in HTML/Markdown"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "Additional metadata"
|
||||
},
|
||||
"tableRefs": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/tableReference" }
|
||||
},
|
||||
"actions": {
|
||||
"type": "array",
|
||||
"description": "Array of test actions in this group, or references to them",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{ "$ref": "#/$defs/testAction" },
|
||||
{ "$ref": "#/$defs/testActionReference" }
|
||||
]
|
||||
},
|
||||
"minItems": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"testAction": {
|
||||
"type": "object",
|
||||
"description": "Individual test action (measurement, check, or data acquisition)",
|
||||
"required": ["id", "document", "mode"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique test action identifier"
|
||||
},
|
||||
"document": {
|
||||
"type": "string",
|
||||
"description": "Instructions/documentation in HTML/Markdown"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["manual", "auto"],
|
||||
"description": "Execution mode: manual requires user input, auto attempts automatic data acquisition"
|
||||
},
|
||||
"functionType": {
|
||||
"type": "string",
|
||||
"description": "Function type for this test action (e.g., RESISTANCE_4WIRE, VOLTAGE_V, etc.)"
|
||||
},
|
||||
"channel": {
|
||||
"type": "string",
|
||||
"enum": ["input1", "input2", "output1", "output2"],
|
||||
"description": "Channel used for this test action: input1, input2, output1, or output2"
|
||||
},
|
||||
"functionParameters": {
|
||||
"description": "Function-specific parameters as key-value pairs",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
},
|
||||
"metadata": { "type": "object" },
|
||||
"dataFields": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/fieldSelector" }
|
||||
},
|
||||
"validationCriteria": { "type": "object" },
|
||||
"uploadStrategy": {
|
||||
"type": "string",
|
||||
"enum": ["immediate", "onComplete", "inherit"],
|
||||
"default": "inherit",
|
||||
"description": "Data upload strategy for this action: immediate (upload after action), onComplete (upload with table), inherit (use table's strategy)"
|
||||
},
|
||||
"uploadFields": {
|
||||
"type": "array",
|
||||
"description": "Specific fields to upload immediately after this action completes",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": { "mode": { "const": "auto" } },
|
||||
"required": ["functionType", "functionParameters"],
|
||||
"description": "Auto mode check items must define functionType and functionParameters"
|
||||
},
|
||||
{
|
||||
"properties": { "mode": { "const": "manual" } }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"functionType": {
|
||||
"enum": [
|
||||
"DATA_ACQUISITION",
|
||||
"DATA_ACQUISITION_WITH_VALIDATION"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "required": ["channel"] }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tableDefinition": {
|
||||
"type": "object",
|
||||
"description": "Table definition for data storage (grid or form layout)",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique table identifier"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Table display name"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Table description"
|
||||
},
|
||||
"tableType": {
|
||||
"type": "string",
|
||||
"enum": ["grid", "form", "series"],
|
||||
"default": "grid",
|
||||
"description": "Table layout type: grid for row-column table, form for key-value layout, series for time-series data recording"
|
||||
},
|
||||
"layoutConfig": {
|
||||
"type": "object",
|
||||
"description": "Layout configuration for form-type tables",
|
||||
"properties": {
|
||||
"totalColumns": {
|
||||
"type": "integer",
|
||||
"default": 12,
|
||||
"description": "Total columns in grid system (typically 12)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"isShared": { "type": "boolean", "default": false },
|
||||
"uploadStrategy": {
|
||||
"type": "string",
|
||||
"enum": ["immediate", "grouped", "byRow", "onComplete"],
|
||||
"default": "onComplete",
|
||||
"description": "Data upload strategy: immediate (per field), grouped (per group), byRow (per row), onComplete (entire table)"
|
||||
},
|
||||
"uploadGroups": {
|
||||
"type": "array",
|
||||
"description": "Upload groups for grouped upload strategy, supporting field groups or specific cells by row-column position",
|
||||
"items": { "$ref": "#/$defs/fieldSelector" }
|
||||
},
|
||||
"columnHeaders": {
|
||||
"type": "array",
|
||||
"description": "Column headers for grid-type tables, or references to them",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{ "$ref": "#/$defs/fieldDefinition" },
|
||||
{ "$ref": "#/$defs/fieldDefinitionReference" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"rowHeaders": {
|
||||
"type": "array",
|
||||
"description": "Row headers for grid-type tables",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"name": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"staticCells": {
|
||||
"type": "array",
|
||||
"description": "Static content entries for tables. Each entry selects one or more cells/fields via fieldSelector and renders fixed text.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"field": {
|
||||
"type": "string",
|
||||
"description": "Field ID (for form-type tables)"
|
||||
},
|
||||
"row": {
|
||||
"oneOf": [
|
||||
{ "type": "string", "description": "Row ID" },
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Row index (0-based)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"column": {
|
||||
"oneOf": [
|
||||
{ "type": "string", "description": "Column ID" },
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Column index (0-based)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Static text content to display. When targeting form fields, the field is treated as read-only and shows this fixed value."
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": ["field", "content"] },
|
||||
{ "required": ["row", "column", "content"] }
|
||||
]
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"description": "Fields for form-type and time-series tables, or references to them",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{ "$ref": "#/$defs/fieldDefinition" },
|
||||
{ "$ref": "#/$defs/fieldDefinitionReference" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": { "tableType": { "const": "form" } },
|
||||
"required": ["fields"],
|
||||
"description": "Form-type tables must define fields"
|
||||
},
|
||||
{
|
||||
"properties": { "tableType": { "const": "grid" } },
|
||||
"required": ["columnHeaders", "rowHeaders"],
|
||||
"description": "Grid-type tables must define columnHeaders and rowHeaders"
|
||||
},
|
||||
{
|
||||
"properties": { "tableType": { "const": "series" } },
|
||||
"required": ["fields"],
|
||||
"description": "Time-series tables must define fields"
|
||||
}
|
||||
]
|
||||
},
|
||||
"fieldDefinition": {
|
||||
"type": "object",
|
||||
"description": "Field definition for table columns or form fields",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique field identifier"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Field display name"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"numeric",
|
||||
"text",
|
||||
"selection",
|
||||
"boolean",
|
||||
"datetime",
|
||||
"calculated"
|
||||
]
|
||||
},
|
||||
"defaultValue": {
|
||||
"description": "Default value for the field"
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"description": "Options for selection type fields"
|
||||
},
|
||||
"formula": {
|
||||
"type": "string",
|
||||
"description": "Formula for calculated type fields"
|
||||
},
|
||||
"validationRules": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"min": { "type": "number" },
|
||||
"max": { "type": "number" },
|
||||
"precision": { "type": "integer", "minimum": 0 },
|
||||
"pattern": { "type": "string" },
|
||||
"minLength": { "type": "integer", "minimum": 0 },
|
||||
"maxLength": { "type": "integer", "minimum": 0 },
|
||||
"required": { "type": "boolean" }
|
||||
}
|
||||
},
|
||||
"isRequired": { "type": "boolean", "default": false },
|
||||
"isReadOnly": { "type": "boolean", "default": false },
|
||||
"unit": { "type": "string" },
|
||||
"description": { "type": "string" },
|
||||
"uploadImmediately": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Whether to upload this field immediately after value change"
|
||||
},
|
||||
"layoutConfig": {
|
||||
"type": "object",
|
||||
"description": "Layout configuration for form-type tables",
|
||||
"properties": {
|
||||
"labelSpan": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 11,
|
||||
"default": 1,
|
||||
"description": "Columns occupied by field label"
|
||||
},
|
||||
"valueSpan": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 11,
|
||||
"default": 11,
|
||||
"description": "Columns occupied by field value"
|
||||
},
|
||||
"rowSpan": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"default": 1,
|
||||
"description": "Rows occupied by this field"
|
||||
},
|
||||
"order": {
|
||||
"type": "integer",
|
||||
"description": "Display order in form layout"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": { "type": { "const": "selection" } },
|
||||
"required": ["options"],
|
||||
"description": "Selection-type fields must define options"
|
||||
},
|
||||
{
|
||||
"properties": { "type": { "const": "calculated" } },
|
||||
"required": ["formula"],
|
||||
"description": "Calculated-type fields must define formula"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": ["numeric", "text", "boolean", "datetime"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"fieldSelector": {
|
||||
"type": "object",
|
||||
"description": "Selector for table fields or cells, supporting both form-type (field IDs) and grid-type (row-column positions) tables",
|
||||
"properties": {
|
||||
"tableRef": { "$ref": "#/$defs/tableReference" },
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Array of field IDs (for form-type tables)"
|
||||
},
|
||||
"cells": {
|
||||
"type": "array",
|
||||
"description": "Array of specific cells by row-column position (for grid-type tables)",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["row", "column"],
|
||||
"properties": {
|
||||
"row": {
|
||||
"oneOf": [
|
||||
{ "type": "string", "description": "Row ID" },
|
||||
{ "type": "integer", "description": "Row index (0-based)" }
|
||||
]
|
||||
},
|
||||
"column": {
|
||||
"oneOf": [
|
||||
{ "type": "string", "description": "Column ID" },
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Column index (0-based)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ignore": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Whether to skip this field when filling table data from function return values"
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": ["fields"] },
|
||||
{ "required": ["cells"] },
|
||||
{
|
||||
"properties": { "ignore": { "const": true } },
|
||||
"required": ["ignore"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"activityReference": {
|
||||
"type": "object",
|
||||
"description": "Reference to a top-level activity (testTaskGroup or resultDisplay)",
|
||||
"required": ["$ref"],
|
||||
"properties": {
|
||||
"$ref": {
|
||||
"type": "string",
|
||||
"pattern": "^#/(testTaskGroups|resultDisplays)/.+$",
|
||||
"description": "JSON Pointer to a testTaskGroup or resultDisplay"
|
||||
}
|
||||
}
|
||||
},
|
||||
"groupReference": {
|
||||
"type": "object",
|
||||
"description": "Reference to a test activity group",
|
||||
"required": ["$ref"],
|
||||
"properties": {
|
||||
"$ref": {
|
||||
"type": "string",
|
||||
"pattern": "^#/testActivityGroups/.+$",
|
||||
"description": "JSON Pointer to a testActivityGroup"
|
||||
}
|
||||
}
|
||||
},
|
||||
"testActionReference": {
|
||||
"type": "object",
|
||||
"description": "Reference to a test action",
|
||||
"required": ["$ref"],
|
||||
"properties": {
|
||||
"$ref": {
|
||||
"type": "string",
|
||||
"pattern": "^#/testActions/.+$",
|
||||
"description": "JSON Pointer to a testAction"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fieldDefinitionReference": {
|
||||
"type": "object",
|
||||
"description": "Reference to a field definition",
|
||||
"required": ["$ref"],
|
||||
"properties": {
|
||||
"$ref": {
|
||||
"type": "string",
|
||||
"pattern": "^#/fieldDefinitions/.+$",
|
||||
"description": "JSON Pointer to a fieldDefinition"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tableReference": {
|
||||
"type": "string",
|
||||
"pattern": "^#/tables/.+$",
|
||||
"description": "JSON Pointer to a table definition (e.g., #/tables/myTable)"
|
||||
}
|
||||
}
|
||||
}
|
||||
382
utils/configmanager.cpp
Normal file
@@ -0,0 +1,382 @@
|
||||
#include "configmanager.h"
|
||||
#include <QFile>
|
||||
#include <QDebug>
|
||||
|
||||
ConfigManager::ConfigManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
ConfigManager::~ConfigManager()
|
||||
{
|
||||
}
|
||||
|
||||
bool ConfigManager::loadFromFile(const QString &filePath)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
qWarning() << "Cannot open config file:" << filePath;
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray data = file.readAll();
|
||||
file.close();
|
||||
|
||||
QJsonParseError error;
|
||||
m_configDoc = QJsonDocument::fromJson(data, &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError)
|
||||
{
|
||||
qWarning() << "JSON parse error:" << error.errorString();
|
||||
return false;
|
||||
}
|
||||
|
||||
m_config = m_configDoc.object();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigManager::loadDefaultConfig()
|
||||
{
|
||||
// 默认配置 - 基于需求规格
|
||||
QString defaultConfig = R"({
|
||||
"name": "智能校验仪启动器",
|
||||
"version": "1.0.0",
|
||||
"colorSystem": {
|
||||
"primary": "#2196F3",
|
||||
"accent": "#FF9800",
|
||||
"success": "#4CAF50",
|
||||
"warning": "#FFC107",
|
||||
"danger": "#F44336",
|
||||
"background": "#F5F5F5"
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"id": "page1",
|
||||
"title": "信号输出和测量",
|
||||
"apps": [
|
||||
{
|
||||
"id": "rtd",
|
||||
"name": "热电阻",
|
||||
"nameEn": "RTD",
|
||||
"color": "#2196F3",
|
||||
"gradient": ["#2196F3", "#1976D2"],
|
||||
"badge": "Ω",
|
||||
"description": "2/3/4线热电阻测量,PT100模拟输出",
|
||||
"features": ["测量2/3/4线热电阻阻值", "自动计算线电阻", "自动转换为温度", "PT100分度表模拟输出"]
|
||||
},
|
||||
{
|
||||
"id": "thermocouple",
|
||||
"name": "热电偶",
|
||||
"nameEn": "TC",
|
||||
"color": "#FF9800",
|
||||
"gradient": ["#FF9800", "#F57C00"],
|
||||
"badge": "mV",
|
||||
"description": "热电偶电压测量,分度表模拟输出"
|
||||
},
|
||||
{
|
||||
"id": "insulation",
|
||||
"name": "绝缘电阻",
|
||||
"nameEn": "Insulation",
|
||||
"color": "#F44336",
|
||||
"gradient": ["#F44336", "#D32F2F"],
|
||||
"badge": "MΩ",
|
||||
"description": "50V/100V绝缘电阻测量",
|
||||
"warning": true
|
||||
},
|
||||
{
|
||||
"id": "dc_current",
|
||||
"name": "直流电流",
|
||||
"nameEn": "DC Current",
|
||||
"color": "#4CAF50",
|
||||
"gradient": ["#4CAF50", "#388E3C"],
|
||||
"badge": "mA",
|
||||
"description": "0-20mA电流输出/测量"
|
||||
},
|
||||
{
|
||||
"id": "dc_voltage",
|
||||
"name": "直流电压",
|
||||
"nameEn": "DC Voltage",
|
||||
"color": "#9C27B0",
|
||||
"gradient": ["#9C27B0", "#7B1FA2"],
|
||||
"badge": "V",
|
||||
"description": "0-20V电压输出/测量"
|
||||
},
|
||||
{
|
||||
"id": "frequency",
|
||||
"name": "频率信号",
|
||||
"nameEn": "Frequency",
|
||||
"color": "#00BCD4",
|
||||
"gradient": ["#00BCD4", "#0097A7"],
|
||||
"badge": "Hz",
|
||||
"description": "转速测量,方波/正弦波输出"
|
||||
},
|
||||
{
|
||||
"id": "ac_voltage",
|
||||
"name": "交流电压",
|
||||
"nameEn": "AC Voltage",
|
||||
"color": "#E91E63",
|
||||
"gradient": ["#E91E63", "#C2185B"],
|
||||
"badge": "VAC",
|
||||
"description": "220VAC测量功能",
|
||||
"warning": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "page2",
|
||||
"title": "信号检测功能",
|
||||
"apps": [
|
||||
{
|
||||
"id": "switch_detect",
|
||||
"name": "开关量检测",
|
||||
"nameEn": "Switch Detect",
|
||||
"color": "#795548",
|
||||
"gradient": ["#795548", "#5D4037"],
|
||||
"badge": "SW",
|
||||
"description": "电阻通断/电压判断开关状态"
|
||||
},
|
||||
{
|
||||
"id": "ripple",
|
||||
"name": "纹波诊断",
|
||||
"nameEn": "Ripple Diagnosis",
|
||||
"color": "#FF5722",
|
||||
"gradient": ["#FF5722", "#E64A19"],
|
||||
"badge": "AI",
|
||||
"description": "电源纹波检测,AI故障诊断"
|
||||
},
|
||||
{
|
||||
"id": "ramp",
|
||||
"name": "斜波输出",
|
||||
"nameEn": "Ramp Output",
|
||||
"color": "#3F51B5",
|
||||
"gradient": ["#3F51B5", "#303F9F"],
|
||||
"badge": "△",
|
||||
"description": "可设速率的斜波信号输出"
|
||||
},
|
||||
{
|
||||
"id": "waveform",
|
||||
"name": "波形采集",
|
||||
"nameEn": "Waveform Capture",
|
||||
"color": "#009688",
|
||||
"gradient": ["#009688", "#00796B"],
|
||||
"badge": "REC",
|
||||
"description": "信号采集记录,≤25ms/点"
|
||||
},
|
||||
{
|
||||
"id": "dual_channel",
|
||||
"name": "双通道",
|
||||
"nameEn": "Dual Channel",
|
||||
"color": "#673AB7",
|
||||
"gradient": ["#673AB7", "#512DA8"],
|
||||
"badge": "2CH",
|
||||
"description": "同时输出2路,采集2路信号"
|
||||
},
|
||||
{
|
||||
"id": "trim",
|
||||
"name": "信号微调",
|
||||
"nameEn": "Signal Trim",
|
||||
"color": "#607D8B",
|
||||
"gradient": ["#607D8B", "#455A64"],
|
||||
"badge": "±",
|
||||
"description": "任意位数信号微调"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "page3",
|
||||
"title": "系统和通讯",
|
||||
"apps": [
|
||||
{
|
||||
"id": "data_management",
|
||||
"name": "数据管理",
|
||||
"nameEn": "Data Management",
|
||||
"color": "#8BC34A",
|
||||
"gradient": ["#8BC34A", "#689F38"],
|
||||
"badge": "📁",
|
||||
"description": "数据存储、导出、搜索"
|
||||
},
|
||||
{
|
||||
"id": "wireless",
|
||||
"name": "无线通讯",
|
||||
"nameEn": "Wireless",
|
||||
"color": "#03A9F4",
|
||||
"gradient": ["#03A9F4", "#0288D1"],
|
||||
"badge": "4G",
|
||||
"description": "4G/WIFI通讯设置"
|
||||
},
|
||||
{
|
||||
"id": "sop",
|
||||
"name": "规程管理",
|
||||
"nameEn": "SOP",
|
||||
"color": "#CDDC39",
|
||||
"gradient": ["#CDDC39", "#AFB42B"],
|
||||
"badge": "📋",
|
||||
"description": "自动化校验流程"
|
||||
},
|
||||
{
|
||||
"id": "rcp63",
|
||||
"name": "RCP63",
|
||||
"nameEn": "RCP63",
|
||||
"color": "#2196F3",
|
||||
"gradient": ["#2196F3", "#1976D2"],
|
||||
"badge": "🌐",
|
||||
"description": "RCP63协议通讯"
|
||||
},
|
||||
{
|
||||
"id": "settings",
|
||||
"name": "系统设置",
|
||||
"nameEn": "Settings",
|
||||
"color": "#9E9E9E",
|
||||
"gradient": ["#9E9E9E", "#757575"],
|
||||
"badge": "⚙️",
|
||||
"description": "系统参数配置"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"dock": [
|
||||
{
|
||||
"id": "dock_sop",
|
||||
"name": "规程",
|
||||
"color": "#CDDC39",
|
||||
"gradient": ["#CDDC39", "#AFB42B"]
|
||||
},
|
||||
{
|
||||
"id": "dock_rcp63",
|
||||
"name": "RCP63",
|
||||
"color": "#2196F3",
|
||||
"gradient": ["#2196F3", "#1976D2"]
|
||||
},
|
||||
{
|
||||
"id": "dock_network",
|
||||
"name": "网络测试",
|
||||
"color": "#03A9F4",
|
||||
"gradient": ["#03A9F4", "#0288D1"]
|
||||
},
|
||||
{
|
||||
"id": "dock_settings",
|
||||
"name": "系统设置",
|
||||
"color": "#9E9E9E",
|
||||
"gradient": ["#9E9E9E", "#757575"]
|
||||
}
|
||||
],
|
||||
"animations": {
|
||||
"pageTransition": {
|
||||
"duration": 300,
|
||||
"easing": "ease-out"
|
||||
},
|
||||
"iconPress": {
|
||||
"scale": 0.95,
|
||||
"duration": 100
|
||||
}
|
||||
}
|
||||
})";
|
||||
|
||||
QJsonParseError error;
|
||||
m_configDoc = QJsonDocument::fromJson(defaultConfig.toUtf8(), &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError)
|
||||
{
|
||||
qWarning() << "Default config parse error:" << error.errorString();
|
||||
return false;
|
||||
}
|
||||
|
||||
m_config = m_configDoc.object();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigManager::saveToFile(const QString &filePath)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly))
|
||||
{
|
||||
qWarning() << "Cannot write config file:" << filePath;
|
||||
return false;
|
||||
}
|
||||
|
||||
file.write(m_configDoc.toJson(QJsonDocument::Indented));
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<PageConfig> ConfigManager::getPages() const
|
||||
{
|
||||
QVector<PageConfig> pages;
|
||||
|
||||
QJsonArray pagesArray = m_config["pages"].toArray();
|
||||
for (const QJsonValue &pageVal : pagesArray)
|
||||
{
|
||||
pages.append(parsePageConfig(pageVal.toObject()));
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
QVector<AppIconData> ConfigManager::getDockApps() const
|
||||
{
|
||||
QVector<AppIconData> apps;
|
||||
|
||||
QJsonArray dockArray = m_config["dock"].toArray();
|
||||
for (const QJsonValue &appVal : dockArray)
|
||||
{
|
||||
apps.append(parseAppData(appVal.toObject()));
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
QJsonObject ConfigManager::getColorSystem() const
|
||||
{
|
||||
return m_config["colorSystem"].toObject();
|
||||
}
|
||||
|
||||
QJsonObject ConfigManager::getAnimations() const
|
||||
{
|
||||
return m_config["animations"].toObject();
|
||||
}
|
||||
|
||||
AppIconData ConfigManager::parseAppData(const QJsonObject &obj) const
|
||||
{
|
||||
AppIconData data;
|
||||
data.id = obj["id"].toString();
|
||||
data.name = obj["name"].toString();
|
||||
data.nameEn = obj["nameEn"].toString();
|
||||
data.icon = obj["icon"].toString();
|
||||
data.color = obj["color"].toString();
|
||||
data.badge = obj["badge"].toString();
|
||||
data.group = obj["group"].toString(); // 解析分组标识
|
||||
data.description = obj["description"].toString();
|
||||
data.warning = obj["warning"].toBool(false);
|
||||
|
||||
// 解析渐变色
|
||||
QJsonArray gradientArray = obj["gradient"].toArray();
|
||||
for (const QJsonValue &val : gradientArray)
|
||||
{
|
||||
data.gradient.append(val.toString());
|
||||
}
|
||||
|
||||
// 解析功能列表
|
||||
QJsonArray featuresArray = obj["features"].toArray();
|
||||
for (const QJsonValue &val : featuresArray)
|
||||
{
|
||||
data.features.append(val.toString());
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
PageConfig ConfigManager::parsePageConfig(const QJsonObject &obj) const
|
||||
{
|
||||
PageConfig config;
|
||||
config.id = obj["id"].toString();
|
||||
config.title = obj["title"].toString();
|
||||
|
||||
QJsonArray appsArray = obj["apps"].toArray();
|
||||
for (const QJsonValue &appVal : appsArray)
|
||||
{
|
||||
config.apps.append(parseAppData(appVal.toObject()));
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
37
utils/configmanager.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#ifndef CONFIGMANAGER_H
|
||||
#define CONFIGMANAGER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QVector>
|
||||
#include "../widgets/appicon.h"
|
||||
#include "../widgets/launcherpage.h"
|
||||
|
||||
class ConfigManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ConfigManager(QObject *parent = nullptr);
|
||||
~ConfigManager();
|
||||
|
||||
bool loadFromFile(const QString &filePath);
|
||||
bool loadDefaultConfig();
|
||||
bool saveToFile(const QString &filePath);
|
||||
|
||||
QVector<PageConfig> getPages() const;
|
||||
QVector<AppIconData> getDockApps() const;
|
||||
QJsonObject getColorSystem() const;
|
||||
QJsonObject getAnimations() const;
|
||||
|
||||
private:
|
||||
AppIconData parseAppData(const QJsonObject &obj) const;
|
||||
PageConfig parsePageConfig(const QJsonObject &obj) const;
|
||||
|
||||
QJsonDocument m_configDoc;
|
||||
QJsonObject m_config;
|
||||
};
|
||||
|
||||
#endif // CONFIGMANAGER_H
|
||||
286
utils/stylehelper.cpp
Normal file
@@ -0,0 +1,286 @@
|
||||
#include "stylehelper.h"
|
||||
|
||||
void StyleHelper::applyGlobalStyle(QApplication *app)
|
||||
{
|
||||
app->setStyleSheet(getGlobalStyleSheet());
|
||||
}
|
||||
|
||||
QString StyleHelper::getGlobalStyleSheet()
|
||||
{
|
||||
return R"(
|
||||
/* 全局样式 */
|
||||
QWidget {
|
||||
font-family: "Source Han Sans SC", "Noto Sans SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
/* 主窗口 */
|
||||
QMainWindow {
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
|
||||
/* 按钮样式 - 触摸屏优化 */
|
||||
QPushButton {
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #1976D2;
|
||||
}
|
||||
|
||||
QPushButton:pressed {
|
||||
background-color: #1565C0;
|
||||
}
|
||||
|
||||
QPushButton:disabled {
|
||||
background-color: #BDBDBD;
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
/* 次要按钮 */
|
||||
QPushButton[flat="true"] {
|
||||
background-color: transparent;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
QPushButton[flat="true"]:hover {
|
||||
background-color: rgba(33, 150, 243, 0.1);
|
||||
}
|
||||
|
||||
/* 输入框 - 触摸屏优化 */
|
||||
QLineEdit {
|
||||
background-color: white;
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
color: #212121;
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
QLineEdit:focus {
|
||||
border-color: #2196F3;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
QLineEdit:disabled {
|
||||
background-color: #FAFAFA;
|
||||
color: #9E9E9E;
|
||||
}
|
||||
|
||||
/* 下拉框 - 触摸屏优化 */
|
||||
QComboBox {
|
||||
background-color: white;
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
color: #212121;
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
QComboBox:hover {
|
||||
border-color: #BDBDBD;
|
||||
}
|
||||
|
||||
QComboBox:focus {
|
||||
border-color: #2196F3;
|
||||
}
|
||||
|
||||
QComboBox::drop-down {
|
||||
border: none;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
QComboBox::down-arrow {
|
||||
image: url(:/icons/arrow_down.svg);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* 滚动条 - 触摸屏优化 */
|
||||
QScrollBar:vertical {
|
||||
background-color: #F5F5F5;
|
||||
width: 14px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:vertical {
|
||||
background-color: #BDBDBD;
|
||||
border-radius: 7px;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:vertical:hover {
|
||||
background-color: #9E9E9E;
|
||||
}
|
||||
|
||||
QScrollBar::add-line:vertical,
|
||||
QScrollBar::sub-line:vertical {
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
QScrollBar:horizontal {
|
||||
background-color: #F5F5F5;
|
||||
height: 14px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:horizontal {
|
||||
background-color: #BDBDBD;
|
||||
border-radius: 7px;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:horizontal:hover {
|
||||
background-color: #9E9E9E;
|
||||
}
|
||||
|
||||
QScrollBar::add-line:horizontal,
|
||||
QScrollBar::sub-line:horizontal {
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
/* 标签页 */
|
||||
QTabWidget::pane {
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 8px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
QTabBar::tab {
|
||||
background-color: #F5F5F5;
|
||||
color: #757575;
|
||||
padding: 12px 24px;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
QTabBar::tab:selected {
|
||||
background-color: white;
|
||||
color: #2196F3;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
QTabBar::tab:hover:!selected {
|
||||
background-color: #EEEEEE;
|
||||
}
|
||||
|
||||
/* 进度条 */
|
||||
QProgressBar {
|
||||
background-color: #E0E0E0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
height: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background-color: #2196F3;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 工具提示 */
|
||||
QToolTip {
|
||||
background-color: #424242;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 消息框 */
|
||||
QMessageBox {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
QMessageBox QLabel {
|
||||
color: #212121;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 分组框 */
|
||||
QGroupBox {
|
||||
background-color: white;
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
subcontrol-position: top left;
|
||||
left: 16px;
|
||||
padding: 0 8px;
|
||||
color: #424242;
|
||||
}
|
||||
|
||||
/* 列表 */
|
||||
QListWidget {
|
||||
background-color: white;
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
QListWidget::item {
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QListWidget::item:selected {
|
||||
background-color: #E3F2FD;
|
||||
color: #1976D2;
|
||||
}
|
||||
|
||||
QListWidget::item:hover:!selected {
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
|
||||
/* 表格 */
|
||||
QTableWidget {
|
||||
background-color: white;
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 8px;
|
||||
gridline-color: #EEEEEE;
|
||||
}
|
||||
|
||||
QTableWidget::item {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
QTableWidget::item:selected {
|
||||
background-color: #E3F2FD;
|
||||
color: #1976D2;
|
||||
}
|
||||
|
||||
QHeaderView::section {
|
||||
background-color: #FAFAFA;
|
||||
color: #424242;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-bottom: 1px solid #E0E0E0;
|
||||
font-weight: 500;
|
||||
}
|
||||
)";
|
||||
}
|
||||
|
||||
QString StyleHelper::cardShadow()
|
||||
{
|
||||
return "0 4px 8px rgba(0,0,0,0.15)";
|
||||
}
|
||||
|
||||
QString StyleHelper::buttonShadow()
|
||||
{
|
||||
return "0 2px 4px rgba(0,0,0,0.2)";
|
||||
}
|
||||
32
utils/stylehelper.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#ifndef STYLEHELPER_H
|
||||
#define STYLEHELPER_H
|
||||
|
||||
#include <QApplication>
|
||||
#include <QString>
|
||||
|
||||
class StyleHelper
|
||||
{
|
||||
public:
|
||||
static void applyGlobalStyle(QApplication *app);
|
||||
static QString getGlobalStyleSheet();
|
||||
|
||||
// 颜色获取
|
||||
static QString primaryColor() { return "#2196F3"; }
|
||||
static QString accentColor() { return "#FF9800"; }
|
||||
static QString successColor() { return "#4CAF50"; }
|
||||
static QString warningColor() { return "#FFC107"; }
|
||||
static QString dangerColor() { return "#F44336"; }
|
||||
static QString backgroundColor() { return "#F5F5F5"; }
|
||||
static QString surfaceColor() { return "#FFFFFF"; }
|
||||
static QString textPrimaryColor() { return "#212121"; }
|
||||
static QString textSecondaryColor() { return "#757575"; }
|
||||
|
||||
// 阴影样式
|
||||
static QString cardShadow();
|
||||
static QString buttonShadow();
|
||||
|
||||
private:
|
||||
StyleHelper() = default;
|
||||
};
|
||||
|
||||
#endif // STYLEHELPER_H
|
||||
283
widgets/appicon.cpp
Normal file
@@ -0,0 +1,283 @@
|
||||
#include "appicon.h"
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QMouseEvent>
|
||||
#include <QLinearGradient>
|
||||
#include <QVBoxLayout>
|
||||
#include <QDebug>
|
||||
|
||||
AppIcon::AppIcon(const AppIconData &data, QWidget *parent)
|
||||
: QWidget(parent), m_data(data), m_scale(1.0), m_isPressed(false), m_longPressTimerId(0)
|
||||
{
|
||||
qDebug() << "AppIcon constructor:" << data.name << "(" << data.id << ")";
|
||||
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
setAttribute(Qt::WA_Hover);
|
||||
|
||||
setupUI();
|
||||
qDebug() << "AppIcon created successfully:" << data.name;
|
||||
}
|
||||
|
||||
AppIcon::~AppIcon()
|
||||
{
|
||||
if (m_longPressTimerId)
|
||||
{
|
||||
killTimer(m_longPressTimerId);
|
||||
}
|
||||
}
|
||||
|
||||
void AppIcon::setupUI()
|
||||
{
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(3, 3, 3, 3);
|
||||
layout->setSpacing(5);
|
||||
layout->setAlignment(Qt::AlignCenter);
|
||||
|
||||
// Android风格图标尺寸 - 10英寸屏幕优化
|
||||
int iconSize = 80;
|
||||
int borderRadius = 16;
|
||||
|
||||
QWidget *iconContainer = new QWidget(this);
|
||||
iconContainer->setFixedSize(iconSize, iconSize);
|
||||
iconContainer->setObjectName("iconContainer");
|
||||
|
||||
// 设置阴影效果 - 更明显的阴影
|
||||
m_shadowEffect = new QGraphicsDropShadowEffect(this);
|
||||
m_shadowEffect->setBlurRadius(16);
|
||||
m_shadowEffect->setColor(QColor(0, 0, 0, 60));
|
||||
m_shadowEffect->setOffset(0, 4);
|
||||
iconContainer->setGraphicsEffect(m_shadowEffect);
|
||||
|
||||
// 图标标签
|
||||
m_iconLabel = new QLabel(iconContainer);
|
||||
m_iconLabel->setAlignment(Qt::AlignCenter);
|
||||
m_iconLabel->setFixedSize(iconSize, iconSize);
|
||||
m_iconLabel->move(0, 0);
|
||||
|
||||
// 设置图标样式
|
||||
QString gradientStart = m_data.gradient.size() > 0 ? m_data.gradient[0] : m_data.color;
|
||||
QString gradientEnd = m_data.gradient.size() > 1 ? m_data.gradient[1] : m_data.color;
|
||||
|
||||
QString iconStyle = QString(R"(
|
||||
QLabel {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop:0 %1, stop:1 %2);
|
||||
border-radius: %3px;
|
||||
font-size: 36px;
|
||||
color: white;
|
||||
}
|
||||
)")
|
||||
.arg(gradientStart, gradientEnd)
|
||||
.arg(borderRadius);
|
||||
|
||||
m_iconLabel->setStyleSheet(iconStyle);
|
||||
|
||||
// 设置 SVG 图标
|
||||
QString iconPath = getIconSymbol(m_data.id);
|
||||
if (!iconPath.isEmpty())
|
||||
{
|
||||
QPixmap pixmap(iconPath);
|
||||
if (!pixmap.isNull())
|
||||
{
|
||||
// 缩放图标到合适大小
|
||||
int svgSize = iconSize - 24; // 留出边距
|
||||
pixmap = pixmap.scaled(svgSize, svgSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
m_iconLabel->setPixmap(pixmap);
|
||||
}
|
||||
}
|
||||
|
||||
// 徽章标签 - 过滤掉emoji,只显示纯文字badge
|
||||
if (!m_data.badge.isEmpty())
|
||||
{
|
||||
QString badgeText = m_data.badge;
|
||||
// 过滤掉emoji字符(保留ASCII和常见中文字符)
|
||||
QString filteredBadge;
|
||||
for (const QChar &ch : badgeText)
|
||||
{
|
||||
ushort unicode = ch.unicode();
|
||||
// 保留基本ASCII(32-126)、中文字符(0x4E00-0x9FFF)、以及一些常用符号
|
||||
if ((unicode >= 32 && unicode <= 126) ||
|
||||
(unicode >= 0x4E00 && unicode <= 0x9FFF) ||
|
||||
ch == QChar(0xB1) || // ±
|
||||
ch == QChar(0x394) || // Δ
|
||||
ch == QChar(0x3A9))
|
||||
{ // Ω
|
||||
filteredBadge += ch;
|
||||
}
|
||||
}
|
||||
|
||||
if (!filteredBadge.isEmpty())
|
||||
{
|
||||
m_badgeLabel = new QLabel(iconContainer);
|
||||
m_badgeLabel->setText(filteredBadge);
|
||||
m_badgeLabel->setFixedSize(26, 16);
|
||||
m_badgeLabel->setAlignment(Qt::AlignCenter);
|
||||
m_badgeLabel->move(iconSize - 28, iconSize - 18);
|
||||
m_badgeLabel->setStyleSheet(R"(
|
||||
QLabel {
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
color: #333;
|
||||
border-radius: 4px;
|
||||
font-size: 9px;
|
||||
font-weight: bold;
|
||||
}
|
||||
)");
|
||||
}
|
||||
}
|
||||
|
||||
layout->addWidget(iconContainer, 0, Qt::AlignCenter);
|
||||
|
||||
// 名称标签 - Android风格白色文字带阴影
|
||||
m_nameLabel = new QLabel(m_data.name, this);
|
||||
m_nameLabel->setAlignment(Qt::AlignCenter);
|
||||
m_nameLabel->setFixedWidth(150);
|
||||
m_nameLabel->setWordWrap(false);
|
||||
m_nameLabel->setStyleSheet(R"(
|
||||
QLabel {
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
background: transparent;
|
||||
}
|
||||
)");
|
||||
layout->addWidget(m_nameLabel, 0, Qt::AlignCenter);
|
||||
}
|
||||
|
||||
QString AppIcon::getIconSymbol(const QString &id) const
|
||||
{
|
||||
// 返回对应的SVG图标路径
|
||||
static QMap<QString, QString> iconMap = {
|
||||
{"rtd", ":/icons/ic_rtd.svg"},
|
||||
{"rtd_measure", ":/icons/ic_rtd.svg"},
|
||||
{"rtd_output", ":/icons/ic_rtd_output.svg"},
|
||||
{"thermocouple", ":/icons/ic_thermocouple.svg"},
|
||||
{"insulation", ":/icons/ic_insulation.svg"},
|
||||
{"dc_current", ":/icons/ic_dc_current.svg"},
|
||||
{"dc_voltage", ":/icons/ic_dc_voltage.svg"},
|
||||
{"frequency", ":/icons/ic_frequency.svg"},
|
||||
{"ac_voltage", ":/icons/ic_ac_voltage.svg"},
|
||||
{"switch_detect", ":/icons/ic_switch.svg"},
|
||||
{"ripple", ":/icons/ic_ripple.svg"},
|
||||
{"ramp", ":/icons/ic_ramp.svg"},
|
||||
{"waveform", ":/icons/ic_waveform.svg"},
|
||||
{"dual_channel", ":/icons/ic_dual_channel.svg"},
|
||||
{"trim", ":/icons/ic_trim.svg"},
|
||||
{"data_management", ":/icons/ic_data.svg"},
|
||||
{"wireless", ":/icons/ic_wireless.svg"},
|
||||
{"sop", ":/icons/ic_sop.svg"},
|
||||
{"rcp63", ":/icons/ic_rcp63.svg"},
|
||||
{"settings", ":/icons/ic_settings.svg"},
|
||||
{"network_test", ":/icons/ic_network_test.svg"},
|
||||
{"dock_sop", ":/icons/ic_sop.svg"},
|
||||
{"dock_rcp63", ":/icons/ic_rcp63.svg"},
|
||||
{"dock_network", ":/icons/ic_network_test.svg"},
|
||||
{"dock_settings", ":/icons/ic_settings.svg"}};
|
||||
|
||||
return iconMap.value(id, "");
|
||||
}
|
||||
|
||||
void AppIcon::setScale(qreal scale)
|
||||
{
|
||||
if (qFuzzyCompare(m_scale, scale))
|
||||
return;
|
||||
m_scale = scale;
|
||||
update();
|
||||
}
|
||||
|
||||
void AppIcon::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
Q_UNUSED(event)
|
||||
|
||||
if (!qFuzzyCompare(m_scale, 1.0))
|
||||
{
|
||||
// 应用缩放变换
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
QTransform transform;
|
||||
transform.translate(width() / 2, height() / 2);
|
||||
transform.scale(m_scale, m_scale);
|
||||
transform.translate(-width() / 2, -height() / 2);
|
||||
|
||||
painter.setTransform(transform);
|
||||
}
|
||||
|
||||
QWidget::paintEvent(event);
|
||||
}
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
void AppIcon::enterEvent(QEnterEvent *event)
|
||||
#else
|
||||
void AppIcon::enterEvent(QEvent *event)
|
||||
#endif
|
||||
{
|
||||
Q_UNUSED(event)
|
||||
m_shadowEffect->setBlurRadius(20);
|
||||
m_shadowEffect->setColor(QColor(0, 0, 0, 60));
|
||||
}
|
||||
|
||||
void AppIcon::leaveEvent(QEvent *event)
|
||||
{
|
||||
Q_UNUSED(event)
|
||||
m_shadowEffect->setBlurRadius(16);
|
||||
m_shadowEffect->setColor(QColor(0, 0, 0, 40));
|
||||
}
|
||||
|
||||
void AppIcon::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton)
|
||||
{
|
||||
m_isPressed = true;
|
||||
animateScale(0.95);
|
||||
|
||||
// 启动长按计时器
|
||||
m_longPressTimerId = startTimer(LONG_PRESS_DELAY);
|
||||
}
|
||||
QWidget::mousePressEvent(event);
|
||||
}
|
||||
|
||||
void AppIcon::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
if (m_longPressTimerId)
|
||||
{
|
||||
killTimer(m_longPressTimerId);
|
||||
m_longPressTimerId = 0;
|
||||
}
|
||||
|
||||
if (m_isPressed && event->button() == Qt::LeftButton)
|
||||
{
|
||||
m_isPressed = false;
|
||||
animateScale(1.0);
|
||||
|
||||
if (rect().contains(event->pos()))
|
||||
{
|
||||
emit clicked(m_data.id);
|
||||
}
|
||||
}
|
||||
QWidget::mouseReleaseEvent(event);
|
||||
}
|
||||
|
||||
void AppIcon::timerEvent(QTimerEvent *event)
|
||||
{
|
||||
if (event->timerId() == m_longPressTimerId)
|
||||
{
|
||||
killTimer(m_longPressTimerId);
|
||||
m_longPressTimerId = 0;
|
||||
|
||||
if (m_isPressed)
|
||||
{
|
||||
emit longPressed(m_data.id);
|
||||
}
|
||||
}
|
||||
QWidget::timerEvent(event);
|
||||
}
|
||||
|
||||
void AppIcon::animateScale(qreal targetScale, int duration)
|
||||
{
|
||||
QPropertyAnimation *anim = new QPropertyAnimation(this, "scale", this);
|
||||
anim->setDuration(duration);
|
||||
anim->setStartValue(m_scale);
|
||||
anim->setEndValue(targetScale);
|
||||
anim->setEasingCurve(QEasingCurve::OutCubic);
|
||||
anim->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
}
|
||||
74
widgets/appicon.h
Normal file
@@ -0,0 +1,74 @@
|
||||
#ifndef APPICON_H
|
||||
#define APPICON_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QLabel>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QGraphicsDropShadowEffect>
|
||||
|
||||
struct AppIconData
|
||||
{
|
||||
QString id;
|
||||
QString name;
|
||||
QString nameEn;
|
||||
QString icon;
|
||||
QString color;
|
||||
QStringList gradient;
|
||||
QString badge;
|
||||
QString group; // 分组标识,相同group的图标会被框起来
|
||||
QString description;
|
||||
QStringList features;
|
||||
bool warning = false;
|
||||
};
|
||||
|
||||
class AppIcon : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(qreal scale READ scale WRITE setScale)
|
||||
|
||||
public:
|
||||
explicit AppIcon(const AppIconData &data, QWidget *parent = nullptr);
|
||||
~AppIcon();
|
||||
|
||||
QString appId() const { return m_data.id; }
|
||||
qreal scale() const { return m_scale; }
|
||||
void setScale(qreal scale);
|
||||
|
||||
signals:
|
||||
void clicked(const QString &appId);
|
||||
void longPressed(const QString &appId);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
void enterEvent(QEnterEvent *event) override;
|
||||
#else
|
||||
void enterEvent(QEvent *event) override;
|
||||
#endif
|
||||
void leaveEvent(QEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
void timerEvent(QTimerEvent *event) override;
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void animateScale(qreal targetScale, int duration = 100);
|
||||
QString generateGradientStyle() const;
|
||||
QString getIconSymbol(const QString &id) const;
|
||||
|
||||
AppIconData m_data;
|
||||
QLabel *m_iconLabel;
|
||||
QLabel *m_nameLabel;
|
||||
QLabel *m_badgeLabel;
|
||||
QGraphicsDropShadowEffect *m_shadowEffect;
|
||||
|
||||
qreal m_scale;
|
||||
bool m_isPressed;
|
||||
int m_longPressTimerId;
|
||||
|
||||
static constexpr int ICON_SIZE = 96;
|
||||
static constexpr int BORDER_RADIUS = 20;
|
||||
static constexpr int LONG_PRESS_DELAY = 500;
|
||||
};
|
||||
|
||||
#endif // APPICON_H
|
||||
252
widgets/channelstatuswidget.cpp
Normal file
@@ -0,0 +1,252 @@
|
||||
#include "channelstatuswidget.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QPaintEvent>
|
||||
#include <QFont>
|
||||
#include <QDebug>
|
||||
|
||||
// =====================================================
|
||||
// ChannelStatusWidget 实现
|
||||
// =====================================================
|
||||
|
||||
ChannelStatusWidget::ChannelStatusWidget(ChannelManager::ChannelId channelId,
|
||||
const QString &channelName,
|
||||
QWidget *parent,
|
||||
ChannelStatusWidget::DisplayMode mode)
|
||||
: QWidget(parent), m_channelId(channelId), m_channelName(channelName), m_currentOperation(ChannelManager::NONE), m_channelManager(ChannelManager::instance()), m_displayMode(mode)
|
||||
{
|
||||
setFixedSize(140, 80); // 紧凑尺寸适配launcher布局
|
||||
setStyleSheet("background-color: #34495e; border: 2px solid #2c3e50; "
|
||||
"border-radius: 8px;");
|
||||
}
|
||||
|
||||
void ChannelStatusWidget::updateStatus(ChannelManager::OperationType operation,
|
||||
const QString &description)
|
||||
{
|
||||
m_currentOperation = operation;
|
||||
m_description = description;
|
||||
update(); // 触发重绘
|
||||
}
|
||||
|
||||
void ChannelStatusWidget::setDisplayMode(ChannelStatusWidget::DisplayMode mode)
|
||||
{
|
||||
m_displayMode = mode;
|
||||
update(); // 触发重绘
|
||||
}
|
||||
|
||||
void ChannelStatusWidget::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
QWidget::paintEvent(event);
|
||||
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
QRect rect = this->rect().adjusted(6, 6, -6, -6);
|
||||
|
||||
// 绘制通道连接器
|
||||
drawChannelConnector(painter, rect);
|
||||
|
||||
// 绘制通道名称
|
||||
if (m_displayMode == ChannelStatusWidget::DETAILED_MODE)
|
||||
{
|
||||
// 详细模式:通道名称在顶部
|
||||
painter.setPen(QColor("#ecf0f1"));
|
||||
QFont nameFont("Microsoft YaHei", 9, QFont::Bold);
|
||||
painter.setFont(nameFont);
|
||||
painter.drawText(rect.adjusted(0, 0, 0, -50), Qt::AlignCenter, m_channelName);
|
||||
|
||||
// 绘制状态文本
|
||||
QString statusText = m_channelManager->getOperationDisplayName(m_currentOperation);
|
||||
if (!m_description.isEmpty() && m_description.length() < 15)
|
||||
{
|
||||
statusText += "\n" + m_description;
|
||||
}
|
||||
|
||||
QFont statusFont("Microsoft YaHei", 8);
|
||||
painter.setFont(statusFont);
|
||||
painter.drawText(rect.adjusted(0, 28, 0, 0), Qt::AlignCenter, statusText);
|
||||
|
||||
// 绘制接线信息
|
||||
ChannelManager::ChannelCapability capability =
|
||||
m_channelManager->getChannelCapability(m_channelId);
|
||||
QString wiringText = capability.wireColors.join("-");
|
||||
painter.setPen(QColor("#bdc3c7"));
|
||||
QFont wiringFont("Microsoft YaHei", 7);
|
||||
painter.setFont(wiringFont);
|
||||
painter.drawText(rect.adjusted(0, 50, 0, 0), Qt::AlignCenter, wiringText);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 概览模式:通道名称在整个区域居中显示
|
||||
painter.setPen(QColor("#ffffff"));
|
||||
QFont overviewFont("Microsoft YaHei", 10, QFont::Bold);
|
||||
painter.setFont(overviewFont);
|
||||
painter.drawText(rect.adjusted(0, 25, 0, 0), Qt::AlignHCenter | Qt::AlignTop, m_channelName);
|
||||
}
|
||||
}
|
||||
|
||||
void ChannelStatusWidget::drawChannelConnector(QPainter &painter, const QRect &rect)
|
||||
{
|
||||
// 绘制通道连接器图形,颜色根据状态变化
|
||||
QRect connectorRect(rect.center().x() - 25, rect.top() + 5, 50, 20);
|
||||
|
||||
// 根据状态设置连接器颜色
|
||||
QColor connectorColor = getStatusColor();
|
||||
painter.setBrush(connectorColor);
|
||||
painter.setPen(QPen(connectorColor.darker(120), 1));
|
||||
painter.drawRoundedRect(connectorRect, 4, 4);
|
||||
|
||||
// 绘制接线端子
|
||||
ChannelManager::ChannelCapability capability =
|
||||
m_channelManager->getChannelCapability(m_channelId);
|
||||
int wireCount = capability.wireColors.size();
|
||||
int wireSpacing = connectorRect.width() / (wireCount + 1);
|
||||
|
||||
for (int i = 0; i < wireCount; ++i)
|
||||
{
|
||||
QRect wireRect(connectorRect.left() + wireSpacing * (i + 1) - 2,
|
||||
connectorRect.bottom() - 3, 4, 6);
|
||||
|
||||
// 根据线色设置颜色
|
||||
QColor wireColor;
|
||||
QString colorName = capability.wireColors[i];
|
||||
if (colorName == "白")
|
||||
wireColor = QColor("#f8f9fa");
|
||||
else if (colorName == "红")
|
||||
wireColor = QColor("#e74c3c");
|
||||
else if (colorName == "灰")
|
||||
wireColor = QColor("#95a5a6");
|
||||
else if (colorName == "黑")
|
||||
wireColor = QColor("#1a1a1a");
|
||||
else
|
||||
wireColor = QColor("#bdc3c7");
|
||||
|
||||
painter.setBrush(wireColor);
|
||||
painter.setPen(QPen(QColor("#34495e"), 1));
|
||||
painter.drawRect(wireRect);
|
||||
}
|
||||
}
|
||||
|
||||
QColor ChannelStatusWidget::getStatusColor() const
|
||||
{
|
||||
switch (m_currentOperation)
|
||||
{
|
||||
case ChannelManager::NONE:
|
||||
return QColor("#27ae60"); // 绿色 - 空闲
|
||||
|
||||
// 输入通道测量操作 - 蓝色系
|
||||
case ChannelManager::VOLTAGE_MEASUREMENT:
|
||||
case ChannelManager::MV_MEASUREMENT:
|
||||
case ChannelManager::MA_MEASUREMENT:
|
||||
case ChannelManager::AC_MEASUREMENT:
|
||||
case ChannelManager::RESISTANCE_MEASUREMENT:
|
||||
case ChannelManager::FREQUENCY_MEASUREMENT:
|
||||
case ChannelManager::SWITCH_MEASUREMENT:
|
||||
case ChannelManager::INSULATION_MEASUREMENT:
|
||||
return QColor("#3498db"); // 蓝色 - 测量中
|
||||
|
||||
// 输出通道操作 - 橙色系
|
||||
case ChannelManager::VOLTAGE_OUTPUT:
|
||||
case ChannelManager::MV_OUTPUT:
|
||||
case ChannelManager::MA_OUTPUT:
|
||||
case ChannelManager::RESISTANCE_SIMULATION:
|
||||
return QColor("#f39c12"); // 橙色 - 输出中
|
||||
|
||||
// 波形输出 - 紫色
|
||||
case ChannelManager::WAVEFORM_OUTPUT:
|
||||
return QColor("#9b59b6"); // 紫色 - 波形输出中
|
||||
|
||||
default:
|
||||
return QColor("#95a5a6"); // 灰色 - 未知
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// ChannelStatusPanel 实现
|
||||
// =====================================================
|
||||
|
||||
ChannelStatusPanel::ChannelStatusPanel(QWidget *parent,
|
||||
LayoutOrientation orientation)
|
||||
: QWidget(parent), m_channelManager(ChannelManager::instance()), m_displayMode(ChannelStatusWidget::OVERVIEW_MODE)
|
||||
{
|
||||
setupUI(orientation);
|
||||
|
||||
// 连接 ChannelManager 的信号
|
||||
connect(m_channelManager, &ChannelManager::channelStatusChanged,
|
||||
this, &ChannelStatusPanel::onChannelStatusChanged);
|
||||
}
|
||||
|
||||
void ChannelStatusPanel::setupUI(LayoutOrientation orientation)
|
||||
{
|
||||
QBoxLayout *layout;
|
||||
if (orientation == HORIZONTAL)
|
||||
{
|
||||
layout = new QHBoxLayout(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
layout = new QVBoxLayout(this);
|
||||
}
|
||||
layout->setContentsMargins(4, 4, 4, 4);
|
||||
layout->setSpacing(8);
|
||||
|
||||
// 创建四个通道的状态显示
|
||||
struct ChannelInfo
|
||||
{
|
||||
ChannelManager::ChannelId id;
|
||||
QString name;
|
||||
};
|
||||
|
||||
QList<ChannelInfo> channels = {
|
||||
{ChannelManager::INPUT_CHANNEL_1, "输入通道1"},
|
||||
{ChannelManager::INPUT_CHANNEL_2, "输入通道2"},
|
||||
{ChannelManager::OUTPUT_CHANNEL_1, "输出通道1"},
|
||||
{ChannelManager::OUTPUT_CHANNEL_2, "输出通道2"}};
|
||||
|
||||
for (const auto &channel : channels)
|
||||
{
|
||||
ChannelStatusWidget *widget = new ChannelStatusWidget(
|
||||
channel.id, channel.name, this, m_displayMode);
|
||||
m_channelWidgets[channel.id] = widget;
|
||||
layout->addWidget(widget);
|
||||
}
|
||||
|
||||
layout->addStretch();
|
||||
}
|
||||
|
||||
void ChannelStatusPanel::setDisplayMode(ChannelStatusWidget::DisplayMode mode)
|
||||
{
|
||||
m_displayMode = mode;
|
||||
for (auto widget : m_channelWidgets)
|
||||
{
|
||||
widget->setDisplayMode(mode);
|
||||
}
|
||||
}
|
||||
|
||||
void ChannelStatusPanel::refresh()
|
||||
{
|
||||
for (auto it = m_channelWidgets.constBegin();
|
||||
it != m_channelWidgets.constEnd(); ++it)
|
||||
{
|
||||
ChannelManager::ChannelId channelId = it.key();
|
||||
ChannelStatusWidget *widget = it.value();
|
||||
|
||||
ChannelManager::OperationType operation =
|
||||
m_channelManager->getChannelOperation(channelId);
|
||||
QString description =
|
||||
m_channelManager->getChannelDescription(channelId);
|
||||
|
||||
widget->updateStatus(operation, description);
|
||||
}
|
||||
}
|
||||
|
||||
void ChannelStatusPanel::onChannelStatusChanged(
|
||||
ChannelManager::ChannelId channel,
|
||||
ChannelManager::OperationType operation,
|
||||
const QString &description)
|
||||
{
|
||||
if (m_channelWidgets.contains(channel))
|
||||
{
|
||||
m_channelWidgets[channel]->updateStatus(operation, description);
|
||||
}
|
||||
}
|
||||
98
widgets/channelstatuswidget.h
Normal file
@@ -0,0 +1,98 @@
|
||||
#ifndef CHANNELSTATUSWIDGET_H
|
||||
#define CHANNELSTATUSWIDGET_H
|
||||
|
||||
#include "../hardware/channelmanager.h"
|
||||
|
||||
#include <QWidget>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPainter>
|
||||
|
||||
/**
|
||||
* @brief 通道状态显示组件
|
||||
*
|
||||
* 用于显示单个通道的状态,包括:
|
||||
* - 通道连接器图形(显示接线端子和线色)
|
||||
* - 通道名称
|
||||
* - 当前操作状态
|
||||
* - 接线信息(详细模式下)
|
||||
*/
|
||||
class ChannelStatusWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief 显示模式
|
||||
*/
|
||||
enum DisplayMode
|
||||
{
|
||||
OVERVIEW_MODE, // 概览模式 - 只显示通道名称和连接器图形
|
||||
DETAILED_MODE // 详细模式 - 显示状态文本和接线信息
|
||||
};
|
||||
|
||||
explicit ChannelStatusWidget(ChannelManager::ChannelId channelId,
|
||||
const QString &channelName,
|
||||
QWidget *parent = nullptr,
|
||||
DisplayMode mode = OVERVIEW_MODE);
|
||||
|
||||
void updateStatus(ChannelManager::OperationType operation,
|
||||
const QString &description);
|
||||
void setDisplayMode(DisplayMode mode);
|
||||
DisplayMode displayMode() const { return m_displayMode; }
|
||||
|
||||
ChannelManager::ChannelId channelId() const { return m_channelId; }
|
||||
ChannelManager::OperationType currentOperation() const { return m_currentOperation; }
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
private:
|
||||
ChannelManager::ChannelId m_channelId;
|
||||
QString m_channelName;
|
||||
ChannelManager::OperationType m_currentOperation;
|
||||
QString m_description;
|
||||
ChannelManager *m_channelManager;
|
||||
DisplayMode m_displayMode;
|
||||
|
||||
void drawChannelConnector(QPainter &painter, const QRect &rect);
|
||||
QColor getStatusColor() const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 通道状态面板
|
||||
*
|
||||
* 显示所有通道的状态,支持水平和垂直布局
|
||||
*/
|
||||
class ChannelStatusPanel : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum LayoutOrientation
|
||||
{
|
||||
HORIZONTAL,
|
||||
VERTICAL
|
||||
};
|
||||
|
||||
explicit ChannelStatusPanel(QWidget *parent = nullptr,
|
||||
LayoutOrientation orientation = HORIZONTAL);
|
||||
|
||||
void setDisplayMode(ChannelStatusWidget::DisplayMode mode);
|
||||
void refresh();
|
||||
|
||||
public slots:
|
||||
void onChannelStatusChanged(ChannelManager::ChannelId channel,
|
||||
ChannelManager::OperationType operation,
|
||||
const QString &description);
|
||||
|
||||
private:
|
||||
QMap<ChannelManager::ChannelId, ChannelStatusWidget *> m_channelWidgets;
|
||||
ChannelManager *m_channelManager;
|
||||
ChannelStatusWidget::DisplayMode m_displayMode;
|
||||
|
||||
void setupUI(LayoutOrientation orientation);
|
||||
};
|
||||
|
||||
#endif // CHANNELSTATUSWIDGET_H
|
||||
1074
widgets/datamanagementwidget.cpp
Normal file
94
widgets/datamanagementwidget.h
Normal file
@@ -0,0 +1,94 @@
|
||||
#ifndef DATAMANAGEMENTWIDGET_H
|
||||
#define DATAMANAGEMENTWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QTableWidget>
|
||||
#include <QComboBox>
|
||||
#include <QDateEdit>
|
||||
#include <QLineEdit>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QStackedWidget>
|
||||
#include <QProgressBar>
|
||||
#include <QCheckBox>
|
||||
|
||||
/**
|
||||
* @brief 数据管理页面
|
||||
* 提供校准记录、历史数据、数据导出等功能
|
||||
*/
|
||||
class DataManagementWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DataManagementWidget(QWidget *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void backRequested();
|
||||
|
||||
private slots:
|
||||
void onSearchClicked();
|
||||
void onExportClicked();
|
||||
void onDeleteClicked();
|
||||
void onRefreshClicked();
|
||||
void onSelectAllChanged(int state);
|
||||
void onRecordDoubleClicked(int row, int column);
|
||||
void onCategoryChanged(int index);
|
||||
void onSyncClicked();
|
||||
void onBackupClicked();
|
||||
void onRestoreClicked();
|
||||
void onClearHistoryClicked();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
QWidget *createFilterPanel();
|
||||
QWidget *createDataTable();
|
||||
QWidget *createToolBar();
|
||||
QWidget *createStorageInfoPanel();
|
||||
void populateSampleData();
|
||||
void updateStorageInfo();
|
||||
QString formatFileSize(qint64 bytes);
|
||||
void setupDateEditStyle(QDateEdit *dateEdit);
|
||||
|
||||
// 标题栏
|
||||
QLabel *m_titleLabel;
|
||||
QPushButton *m_backBtn;
|
||||
|
||||
// 筛选控件
|
||||
QComboBox *m_categoryCombo;
|
||||
QDateEdit *m_startDateEdit;
|
||||
QDateEdit *m_endDateEdit;
|
||||
QLineEdit *m_searchEdit;
|
||||
QPushButton *m_searchBtn;
|
||||
|
||||
// 数据表格
|
||||
QTableWidget *m_dataTable;
|
||||
QCheckBox *m_selectAllCheck;
|
||||
|
||||
// 工具栏
|
||||
QPushButton *m_exportBtn;
|
||||
QPushButton *m_deleteBtn;
|
||||
QPushButton *m_refreshBtn;
|
||||
QPushButton *m_syncBtn;
|
||||
QPushButton *m_backupBtn;
|
||||
QPushButton *m_restoreBtn;
|
||||
QPushButton *m_clearHistoryBtn;
|
||||
|
||||
// 存储信息
|
||||
QLabel *m_storageUsedLabel;
|
||||
QLabel *m_recordCountLabel;
|
||||
QProgressBar *m_storageProgressBar;
|
||||
|
||||
// 样式常量
|
||||
static const QString LABEL_STYLE;
|
||||
static const QString BUTTON_STYLE;
|
||||
static const QString PRIMARY_BUTTON_STYLE;
|
||||
static const QString DANGER_BUTTON_STYLE;
|
||||
static const QString INPUT_STYLE;
|
||||
static const QString TABLE_STYLE;
|
||||
static const QString GROUP_STYLE;
|
||||
};
|
||||
|
||||
#endif // DATAMANAGEMENTWIDGET_H
|
||||
172
widgets/devicestatuswidget.cpp
Normal file
@@ -0,0 +1,172 @@
|
||||
#include "devicestatuswidget.h"
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QHBoxLayout>
|
||||
|
||||
DeviceStatusWidget::DeviceStatusWidget(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
// 初始化通道状态
|
||||
m_channels = {
|
||||
{"输入通道1", true, "input"},
|
||||
{"输入通道2", true, "input"},
|
||||
{"输出通道1", true, "output"},
|
||||
{"输出通道2", true, "output"}};
|
||||
|
||||
setupUI();
|
||||
}
|
||||
|
||||
DeviceStatusWidget::~DeviceStatusWidget()
|
||||
{
|
||||
}
|
||||
|
||||
void DeviceStatusWidget::setupUI()
|
||||
{
|
||||
setStyleSheet(R"(
|
||||
DeviceStatusWidget {
|
||||
background-color: white;
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 8px;
|
||||
margin: 16px;
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
void DeviceStatusWidget::setChannelStatus(int channel, bool connected)
|
||||
{
|
||||
if (channel >= 0 && channel < m_channels.size())
|
||||
{
|
||||
m_channels[channel].connected = connected;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void DeviceStatusWidget::updateChannels(const QVector<ChannelStatus> &channels)
|
||||
{
|
||||
m_channels = channels;
|
||||
update();
|
||||
}
|
||||
|
||||
void DeviceStatusWidget::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
Q_UNUSED(event)
|
||||
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
// 绘制背景
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(QColor("#FFFFFF"));
|
||||
painter.drawRoundedRect(rect().adjusted(16, 8, -16, -8), 12, 12);
|
||||
|
||||
// 绘制边框
|
||||
painter.setPen(QPen(QColor("#E0E0E0"), 1));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRoundedRect(rect().adjusted(16, 8, -16, -8), 12, 12);
|
||||
|
||||
// 绘制线缆图
|
||||
drawCableDiagram(painter);
|
||||
}
|
||||
|
||||
void DeviceStatusWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
update();
|
||||
}
|
||||
|
||||
void DeviceStatusWidget::drawCableDiagram(QPainter &painter)
|
||||
{
|
||||
int contentWidth = width() - 64;
|
||||
int contentHeight = height() - 32;
|
||||
int startX = 48;
|
||||
int startY = 24;
|
||||
|
||||
// 绘制主线缆(曲线)
|
||||
QPainterPath cablePath;
|
||||
painter.setPen(QPen(QColor("#F44336"), 4, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
|
||||
|
||||
int cableY = startY + contentHeight / 3;
|
||||
cablePath.moveTo(startX, cableY);
|
||||
|
||||
// 绘制波浪形线缆
|
||||
int segments = 4;
|
||||
int segmentWidth = contentWidth / segments;
|
||||
for (int i = 0; i < segments; ++i)
|
||||
{
|
||||
int x1 = startX + i * segmentWidth + segmentWidth / 3;
|
||||
int y1 = cableY - 20 * (i % 2 == 0 ? 1 : -1);
|
||||
int x2 = startX + i * segmentWidth + 2 * segmentWidth / 3;
|
||||
int y2 = cableY + 20 * (i % 2 == 0 ? 1 : -1);
|
||||
int x3 = startX + (i + 1) * segmentWidth;
|
||||
int y3 = cableY;
|
||||
|
||||
cablePath.cubicTo(x1, y1, x2, y2, x3, y3);
|
||||
}
|
||||
|
||||
painter.drawPath(cablePath);
|
||||
|
||||
// 绘制通道指示器
|
||||
int channelSpacing = contentWidth / (m_channels.size() + 1);
|
||||
int indicatorY = startY + contentHeight * 2 / 3;
|
||||
|
||||
for (int i = 0; i < m_channels.size(); ++i)
|
||||
{
|
||||
int x = startX + (i + 1) * channelSpacing - 30;
|
||||
int connectorY = cableY + 10;
|
||||
|
||||
// 绘制连接器
|
||||
painter.setPen(QPen(QColor("#4CAF50"), 2));
|
||||
painter.setBrush(QColor("#4CAF50"));
|
||||
|
||||
// 连接器主体
|
||||
QRect connectorRect(x, connectorY, 60, 35);
|
||||
painter.drawRoundedRect(connectorRect, 4, 4);
|
||||
|
||||
// 连接器引脚
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(QColor("#388E3C"));
|
||||
painter.drawRect(x + 5, connectorY + 35, 8, 10);
|
||||
painter.drawRect(x + 47, connectorY + 35, 8, 10);
|
||||
|
||||
// 绘制状态标签
|
||||
QString statusText = m_channels[i].name;
|
||||
QFont font = painter.font();
|
||||
font.setPixelSize(11);
|
||||
painter.setFont(font);
|
||||
|
||||
QColor statusColor = m_channels[i].connected ? QColor("#4CAF50") : QColor("#9E9E9E");
|
||||
painter.setPen(statusColor);
|
||||
|
||||
QRect textRect(x - 10, connectorY + 50, 80, 20);
|
||||
painter.drawText(textRect, Qt::AlignCenter, statusText);
|
||||
|
||||
// 绘制状态指示点
|
||||
int dotX = x + 25;
|
||||
int dotY = connectorY + 70;
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(statusColor);
|
||||
painter.drawEllipse(QPoint(dotX + 5, dotY + 5), 4, 4);
|
||||
}
|
||||
}
|
||||
|
||||
void DeviceStatusWidget::drawChannelIndicator(QPainter &painter, int x, int y,
|
||||
const QString &label, bool connected)
|
||||
{
|
||||
// 绘制指示器背景
|
||||
QColor bgColor = connected ? QColor("#E8F5E9") : QColor("#FAFAFA");
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(bgColor);
|
||||
painter.drawRoundedRect(x, y, 100, 40, 6, 6);
|
||||
|
||||
// 绘制状态点
|
||||
QColor dotColor = connected ? QColor("#4CAF50") : QColor("#9E9E9E");
|
||||
painter.setBrush(dotColor);
|
||||
painter.drawEllipse(x + 10, y + 14, 12, 12);
|
||||
|
||||
// 绘制标签
|
||||
painter.setPen(QColor("#424242"));
|
||||
QFont font = painter.font();
|
||||
font.setPixelSize(12);
|
||||
painter.setFont(font);
|
||||
painter.drawText(x + 28, y + 8, 68, 24, Qt::AlignVCenter, label);
|
||||
}
|
||||
40
widgets/devicestatuswidget.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#ifndef DEVICESTATUSWIDGET_H
|
||||
#define DEVICESTATUSWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QLabel>
|
||||
#include <QVector>
|
||||
|
||||
struct ChannelStatus
|
||||
{
|
||||
QString name;
|
||||
bool connected;
|
||||
QString type; // input, output
|
||||
};
|
||||
|
||||
class DeviceStatusWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DeviceStatusWidget(QWidget *parent = nullptr);
|
||||
~DeviceStatusWidget();
|
||||
|
||||
void setChannelStatus(int channel, bool connected);
|
||||
void updateChannels(const QVector<ChannelStatus> &channels);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void drawCableDiagram(QPainter &painter);
|
||||
void drawChannelIndicator(QPainter &painter, int x, int y,
|
||||
const QString &label, bool connected);
|
||||
|
||||
QVector<ChannelStatus> m_channels;
|
||||
QVector<QLabel *> m_channelLabels;
|
||||
};
|
||||
|
||||
#endif // DEVICESTATUSWIDGET_H
|
||||
142
widgets/dockbar.cpp
Normal file
@@ -0,0 +1,142 @@
|
||||
#include "dockbar.h"
|
||||
#include <QHBoxLayout>
|
||||
#include <QVBoxLayout>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPushButton>
|
||||
|
||||
DockBar::DockBar(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setupUI();
|
||||
}
|
||||
|
||||
DockBar::~DockBar()
|
||||
{
|
||||
}
|
||||
|
||||
void DockBar::setupUI()
|
||||
{
|
||||
setObjectName("dockBar");
|
||||
}
|
||||
|
||||
void DockBar::setApps(const QVector<AppIconData> &apps)
|
||||
{
|
||||
m_apps = apps;
|
||||
createDockIcons();
|
||||
}
|
||||
|
||||
void DockBar::createDockIcons()
|
||||
{
|
||||
// 清除现有布局
|
||||
if (layout())
|
||||
{
|
||||
QLayoutItem *item;
|
||||
while ((item = layout()->takeAt(0)) != nullptr)
|
||||
{
|
||||
if (item->widget())
|
||||
{
|
||||
delete item->widget();
|
||||
}
|
||||
delete item;
|
||||
}
|
||||
delete layout();
|
||||
}
|
||||
m_icons.clear();
|
||||
|
||||
// 创建水平布局
|
||||
QHBoxLayout *layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(30, 8, 30, 8);
|
||||
layout->setSpacing(20);
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
// 创建Dock图标
|
||||
for (const auto &appData : m_apps)
|
||||
{
|
||||
// 创建图标容器
|
||||
QWidget *iconWidget = new QWidget(this);
|
||||
iconWidget->setFixedSize(60, 75);
|
||||
iconWidget->setCursor(Qt::PointingHandCursor);
|
||||
|
||||
QVBoxLayout *iconLayout = new QVBoxLayout(iconWidget);
|
||||
iconLayout->setContentsMargins(0, 0, 0, 0);
|
||||
iconLayout->setSpacing(2);
|
||||
|
||||
// 图标按钮 - 触摸屏优化尺寸
|
||||
QPushButton *btn = new QPushButton(iconWidget);
|
||||
btn->setFixedSize(52, 52);
|
||||
btn->setProperty("appId", appData.id);
|
||||
|
||||
QString gradientStart = appData.gradient.size() > 0 ? appData.gradient[0] : appData.color;
|
||||
QString gradientEnd = appData.gradient.size() > 1 ? appData.gradient[1] : appData.color;
|
||||
|
||||
// 获取图标符号
|
||||
QString iconText = getIconSymbol(appData.id);
|
||||
btn->setText(iconText);
|
||||
|
||||
btn->setStyleSheet(QString(R"(
|
||||
QPushButton {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 %1, stop:1 %2);
|
||||
border-radius: 15px;
|
||||
font-size: 28px;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 %1, stop:1 %1);
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 %2, stop:1 %2);
|
||||
}
|
||||
)")
|
||||
.arg(gradientStart, gradientEnd));
|
||||
|
||||
connect(btn, &QPushButton::clicked, this, [this, appData]()
|
||||
{ emit appClicked(appData.id); });
|
||||
|
||||
iconLayout->addWidget(btn, 0, Qt::AlignCenter);
|
||||
|
||||
// 名称标签 - 深色文字配合白色背景
|
||||
QLabel *nameLabel = new QLabel(appData.name, iconWidget);
|
||||
nameLabel->setAlignment(Qt::AlignCenter);
|
||||
nameLabel->setStyleSheet("font-size: 11px; color: #555; background: transparent; font-weight: 500;");
|
||||
iconLayout->addWidget(nameLabel, 0, Qt::AlignCenter);
|
||||
|
||||
m_icons.append(iconWidget);
|
||||
layout->addWidget(iconWidget);
|
||||
}
|
||||
|
||||
layout->addStretch();
|
||||
}
|
||||
|
||||
QString DockBar::getIconSymbol(const QString &id) const
|
||||
{
|
||||
static QMap<QString, QString> iconMap = {
|
||||
{"dock_sop", "📋"},
|
||||
{"dock_rcp63", "🌐"},
|
||||
{"dock_network", "🌐"},
|
||||
{"dock_settings", "⚙️"}};
|
||||
return iconMap.value(id, "📱");
|
||||
}
|
||||
|
||||
void DockBar::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
Q_UNUSED(event)
|
||||
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
// Android风格Dock背景 - 半透明毛玻璃,增加高度以容纳图标
|
||||
QRect bgRect = rect().adjusted(110, 5, -110, -5);
|
||||
|
||||
// 半透明白色背景
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(QColor(255, 255, 255, 200));
|
||||
painter.drawRoundedRect(bgRect, 30, 30);
|
||||
|
||||
// 细边框
|
||||
painter.setPen(QPen(QColor(255, 255, 255, 100), 1));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRoundedRect(bgRect, 30, 30);
|
||||
}
|
||||
34
widgets/dockbar.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#ifndef DOCKBAR_H
|
||||
#define DOCKBAR_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QVector>
|
||||
#include <QLabel>
|
||||
#include "appicon.h"
|
||||
|
||||
class DockBar : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DockBar(QWidget *parent = nullptr);
|
||||
~DockBar();
|
||||
|
||||
void setApps(const QVector<AppIconData> &apps);
|
||||
|
||||
signals:
|
||||
void appClicked(const QString &appId);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void createDockIcons();
|
||||
QString getIconSymbol(const QString &id) const;
|
||||
|
||||
QVector<AppIconData> m_apps;
|
||||
QVector<QWidget *> m_icons;
|
||||
};
|
||||
|
||||
#endif // DOCKBAR_H
|
||||
533
widgets/dualchannelwidget.cpp
Normal file
@@ -0,0 +1,533 @@
|
||||
#include "dualchannelwidget.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QScrollArea>
|
||||
#include <QTimer>
|
||||
#include <random>
|
||||
|
||||
static const char *GROUP_STYLE = R"(
|
||||
QGroupBox {
|
||||
background-color: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
margin-top: 8px;
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
left: 16px;
|
||||
padding: 0 8px;
|
||||
color: #333;
|
||||
}
|
||||
)";
|
||||
|
||||
static const char *BUTTON_STYLE = R"(
|
||||
QPushButton {
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 12px 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #1976D2;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #1565C0;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #BDBDBD;
|
||||
}
|
||||
)";
|
||||
|
||||
static const char *STOP_BUTTON_STYLE = R"(
|
||||
QPushButton {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 12px 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #d32f2f;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #c62828;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #BDBDBD;
|
||||
}
|
||||
)";
|
||||
|
||||
static const char *VALUE_DISPLAY_STYLE = R"(
|
||||
QLabel {
|
||||
background-color: #1a1a2e;
|
||||
color: #00ff88;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
font-family: 'Courier New', monospace;
|
||||
min-width: 200px;
|
||||
}
|
||||
)";
|
||||
|
||||
DualChannelWidget::DualChannelWidget(QWidget *parent)
|
||||
: QWidget(parent), m_isOutputting(false)
|
||||
{
|
||||
setupUI();
|
||||
|
||||
m_measureTimer = new QTimer(this);
|
||||
connect(m_measureTimer, &QTimer::timeout, this, &DualChannelWidget::updateMeasurement);
|
||||
}
|
||||
|
||||
void DualChannelWidget::setupUI()
|
||||
{
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->setSpacing(0);
|
||||
|
||||
// 标题栏
|
||||
mainLayout->addWidget(createTitleBar());
|
||||
|
||||
// 内容区域
|
||||
QScrollArea *scrollArea = new QScrollArea;
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
scrollArea->setStyleSheet("QScrollArea { background-color: #f5f5f5; }");
|
||||
|
||||
QWidget *contentWidget = new QWidget;
|
||||
contentWidget->setStyleSheet("background-color: #f5f5f5;");
|
||||
QVBoxLayout *contentLayout = new QVBoxLayout(contentWidget);
|
||||
contentLayout->setContentsMargins(20, 20, 20, 20);
|
||||
contentLayout->setSpacing(16);
|
||||
|
||||
// 双通道输出面板
|
||||
QHBoxLayout *channelsLayout = new QHBoxLayout;
|
||||
channelsLayout->setSpacing(16);
|
||||
channelsLayout->addWidget(createChannel1Panel());
|
||||
channelsLayout->addWidget(createChannel2Panel());
|
||||
contentLayout->addLayout(channelsLayout);
|
||||
|
||||
// 测量显示面板
|
||||
contentLayout->addWidget(createMeasurementPanel());
|
||||
|
||||
// 控制面板
|
||||
contentLayout->addWidget(createControlPanel());
|
||||
|
||||
contentLayout->addStretch();
|
||||
|
||||
scrollArea->setWidget(contentWidget);
|
||||
mainLayout->addWidget(scrollArea, 1);
|
||||
}
|
||||
|
||||
QWidget *DualChannelWidget::createTitleBar()
|
||||
{
|
||||
QWidget *titleBar = new QWidget;
|
||||
titleBar->setFixedHeight(60);
|
||||
titleBar->setStyleSheet("background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #1976D2, stop:1 #1565C0);");
|
||||
|
||||
QHBoxLayout *layout = new QHBoxLayout(titleBar);
|
||||
layout->setContentsMargins(16, 0, 16, 0);
|
||||
|
||||
// 返回按钮
|
||||
m_backBtn = new QPushButton("← 返回");
|
||||
m_backBtn->setCursor(Qt::PointingHandCursor);
|
||||
m_backBtn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background: rgba(255,255,255,0.2);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
)");
|
||||
connect(m_backBtn, &QPushButton::clicked, this, &DualChannelWidget::backRequested);
|
||||
layout->addWidget(m_backBtn);
|
||||
|
||||
// 标题(居中)
|
||||
m_titleLabel = new QLabel("双通道");
|
||||
m_titleLabel->setStyleSheet("color: white; font-size: 18px; font-weight: bold;");
|
||||
m_titleLabel->setAlignment(Qt::AlignCenter);
|
||||
layout->addWidget(m_titleLabel, 1);
|
||||
|
||||
// 右侧占位,保持标题居中
|
||||
QWidget *spacer = new QWidget;
|
||||
spacer->setFixedWidth(m_backBtn->sizeHint().width());
|
||||
layout->addWidget(spacer);
|
||||
|
||||
return titleBar;
|
||||
}
|
||||
|
||||
QWidget *DualChannelWidget::createChannel1Panel()
|
||||
{
|
||||
QGroupBox *group = new QGroupBox("通道1 - 输出");
|
||||
group->setStyleSheet(GROUP_STYLE);
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout(group);
|
||||
layout->setContentsMargins(16, 24, 16, 16);
|
||||
layout->setSpacing(16);
|
||||
|
||||
// 启用复选框
|
||||
m_ch1EnableCheck = new QCheckBox("启用通道1");
|
||||
m_ch1EnableCheck->setChecked(true);
|
||||
m_ch1EnableCheck->setStyleSheet("font-weight: 600; color: #00aa55;");
|
||||
layout->addWidget(m_ch1EnableCheck);
|
||||
|
||||
// 信号类型选择
|
||||
QHBoxLayout *typeLayout = new QHBoxLayout;
|
||||
typeLayout->addWidget(new QLabel("信号类型:"));
|
||||
m_ch1TypeCombo = new QComboBox;
|
||||
m_ch1TypeCombo->addItems({"直流电压 (V)", "直流电流 (mA)", "电阻 (Ω)", "频率 (Hz)"});
|
||||
m_ch1TypeCombo->setStyleSheet(R"(
|
||||
QComboBox {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
min-width: 150px;
|
||||
}
|
||||
)");
|
||||
connect(m_ch1TypeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, [this](int)
|
||||
{ onChannel1OutputChanged(); });
|
||||
typeLayout->addWidget(m_ch1TypeCombo);
|
||||
typeLayout->addStretch();
|
||||
layout->addLayout(typeLayout);
|
||||
|
||||
// 输出值设置
|
||||
QHBoxLayout *valueLayout = new QHBoxLayout;
|
||||
valueLayout->addWidget(new QLabel("输出值:"));
|
||||
m_ch1ValueSpin = new QDoubleSpinBox;
|
||||
m_ch1ValueSpin->setRange(-100, 100);
|
||||
m_ch1ValueSpin->setValue(5.0);
|
||||
m_ch1ValueSpin->setDecimals(4);
|
||||
m_ch1ValueSpin->setSuffix(" V");
|
||||
m_ch1ValueSpin->setStyleSheet(R"(
|
||||
QDoubleSpinBox {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
min-width: 150px;
|
||||
}
|
||||
QDoubleSpinBox::up-button, QDoubleSpinBox::down-button {
|
||||
subcontrol-origin: border;
|
||||
width: 24px;
|
||||
border-left: 1px solid #ddd;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
QDoubleSpinBox::up-button { subcontrol-position: top right; border-top-right-radius: 5px; }
|
||||
QDoubleSpinBox::down-button { subcontrol-position: bottom right; border-bottom-right-radius: 5px; }
|
||||
QDoubleSpinBox::up-button:hover, QDoubleSpinBox::down-button:hover { background: #e0e0e0; }
|
||||
QDoubleSpinBox::up-arrow { image: url(:/icons/arrow_up.svg); width: 12px; height: 12px; }
|
||||
QDoubleSpinBox::down-arrow { image: url(:/icons/arrow_down.svg); width: 12px; height: 12px; }
|
||||
)");
|
||||
connect(m_ch1ValueSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
|
||||
this, [this](double)
|
||||
{ onChannel1OutputChanged(); });
|
||||
valueLayout->addWidget(m_ch1ValueSpin);
|
||||
valueLayout->addStretch();
|
||||
layout->addLayout(valueLayout);
|
||||
|
||||
// 实际测量值显示
|
||||
QLabel *measureTitle = new QLabel("实际测量:");
|
||||
measureTitle->setStyleSheet("color: #666;");
|
||||
layout->addWidget(measureTitle);
|
||||
|
||||
m_ch1MeasureLabel = new QLabel("-- V");
|
||||
m_ch1MeasureLabel->setStyleSheet(VALUE_DISPLAY_STYLE);
|
||||
m_ch1MeasureLabel->setAlignment(Qt::AlignCenter);
|
||||
layout->addWidget(m_ch1MeasureLabel);
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
QWidget *DualChannelWidget::createChannel2Panel()
|
||||
{
|
||||
QGroupBox *group = new QGroupBox("通道2 - 输出");
|
||||
group->setStyleSheet(GROUP_STYLE);
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout(group);
|
||||
layout->setContentsMargins(16, 24, 16, 16);
|
||||
layout->setSpacing(16);
|
||||
|
||||
// 启用复选框
|
||||
m_ch2EnableCheck = new QCheckBox("启用通道2");
|
||||
m_ch2EnableCheck->setChecked(true);
|
||||
m_ch2EnableCheck->setStyleSheet("font-weight: 600; color: #d32f2f;");
|
||||
layout->addWidget(m_ch2EnableCheck);
|
||||
|
||||
// 信号类型选择
|
||||
QHBoxLayout *typeLayout = new QHBoxLayout;
|
||||
typeLayout->addWidget(new QLabel("信号类型:"));
|
||||
m_ch2TypeCombo = new QComboBox;
|
||||
m_ch2TypeCombo->addItems({"直流电压 (V)", "直流电流 (mA)", "电阻 (Ω)", "频率 (Hz)"});
|
||||
m_ch2TypeCombo->setStyleSheet(R"(
|
||||
QComboBox {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
min-width: 150px;
|
||||
}
|
||||
)");
|
||||
connect(m_ch2TypeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, [this](int)
|
||||
{ onChannel2OutputChanged(); });
|
||||
typeLayout->addWidget(m_ch2TypeCombo);
|
||||
typeLayout->addStretch();
|
||||
layout->addLayout(typeLayout);
|
||||
|
||||
// 输出值设置
|
||||
QHBoxLayout *valueLayout = new QHBoxLayout;
|
||||
valueLayout->addWidget(new QLabel("输出值:"));
|
||||
m_ch2ValueSpin = new QDoubleSpinBox;
|
||||
m_ch2ValueSpin->setRange(-100, 100);
|
||||
m_ch2ValueSpin->setValue(10.0);
|
||||
m_ch2ValueSpin->setDecimals(4);
|
||||
m_ch2ValueSpin->setSuffix(" V");
|
||||
m_ch2ValueSpin->setStyleSheet(R"(
|
||||
QDoubleSpinBox {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
min-width: 150px;
|
||||
}
|
||||
QDoubleSpinBox::up-button, QDoubleSpinBox::down-button {
|
||||
subcontrol-origin: border;
|
||||
width: 24px;
|
||||
border-left: 1px solid #ddd;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
QDoubleSpinBox::up-button { subcontrol-position: top right; border-top-right-radius: 5px; }
|
||||
QDoubleSpinBox::down-button { subcontrol-position: bottom right; border-bottom-right-radius: 5px; }
|
||||
QDoubleSpinBox::up-button:hover, QDoubleSpinBox::down-button:hover { background: #e0e0e0; }
|
||||
QDoubleSpinBox::up-arrow { image: url(:/icons/arrow_up.svg); width: 12px; height: 12px; }
|
||||
QDoubleSpinBox::down-arrow { image: url(:/icons/arrow_down.svg); width: 12px; height: 12px; }
|
||||
)");
|
||||
connect(m_ch2ValueSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
|
||||
this, [this](double)
|
||||
{ onChannel2OutputChanged(); });
|
||||
valueLayout->addWidget(m_ch2ValueSpin);
|
||||
valueLayout->addStretch();
|
||||
layout->addLayout(valueLayout);
|
||||
|
||||
// 实际测量值显示
|
||||
QLabel *measureTitle = new QLabel("实际测量:");
|
||||
measureTitle->setStyleSheet("color: #666;");
|
||||
layout->addWidget(measureTitle);
|
||||
|
||||
m_ch2MeasureLabel = new QLabel("-- V");
|
||||
m_ch2MeasureLabel->setStyleSheet(R"(
|
||||
QLabel {
|
||||
background-color: #1a1a2e;
|
||||
color: #ff6b6b;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
font-family: 'Courier New', monospace;
|
||||
min-width: 200px;
|
||||
}
|
||||
)");
|
||||
m_ch2MeasureLabel->setAlignment(Qt::AlignCenter);
|
||||
layout->addWidget(m_ch2MeasureLabel);
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
QWidget *DualChannelWidget::createMeasurementPanel()
|
||||
{
|
||||
QGroupBox *group = new QGroupBox("采集输入");
|
||||
group->setStyleSheet(GROUP_STYLE);
|
||||
|
||||
QHBoxLayout *layout = new QHBoxLayout(group);
|
||||
layout->setContentsMargins(16, 24, 16, 16);
|
||||
layout->setSpacing(32);
|
||||
|
||||
// 通道1输入
|
||||
QVBoxLayout *ch1Layout = new QVBoxLayout;
|
||||
QLabel *ch1Title = new QLabel("通道1 采集");
|
||||
ch1Title->setStyleSheet("font-weight: 600; color: #00aa55;");
|
||||
ch1Layout->addWidget(ch1Title);
|
||||
m_ch1InputLabel = new QLabel("-- V");
|
||||
m_ch1InputLabel->setStyleSheet(R"(
|
||||
QLabel {
|
||||
background-color: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
border: 2px solid #4caf50;
|
||||
border-radius: 8px;
|
||||
padding: 12px 24px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
}
|
||||
)");
|
||||
m_ch1InputLabel->setAlignment(Qt::AlignCenter);
|
||||
ch1Layout->addWidget(m_ch1InputLabel);
|
||||
layout->addLayout(ch1Layout);
|
||||
|
||||
// 通道2输入
|
||||
QVBoxLayout *ch2Layout = new QVBoxLayout;
|
||||
QLabel *ch2Title = new QLabel("通道2 采集");
|
||||
ch2Title->setStyleSheet("font-weight: 600; color: #d32f2f;");
|
||||
ch2Layout->addWidget(ch2Title);
|
||||
m_ch2InputLabel = new QLabel("-- V");
|
||||
m_ch2InputLabel->setStyleSheet(R"(
|
||||
QLabel {
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
border: 2px solid #f44336;
|
||||
border-radius: 8px;
|
||||
padding: 12px 24px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
}
|
||||
)");
|
||||
m_ch2InputLabel->setAlignment(Qt::AlignCenter);
|
||||
ch2Layout->addWidget(m_ch2InputLabel);
|
||||
layout->addLayout(ch2Layout);
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
QWidget *DualChannelWidget::createControlPanel()
|
||||
{
|
||||
QGroupBox *group = new QGroupBox("控制");
|
||||
group->setStyleSheet(GROUP_STYLE);
|
||||
|
||||
QHBoxLayout *layout = new QHBoxLayout(group);
|
||||
layout->setContentsMargins(16, 24, 16, 16);
|
||||
layout->setSpacing(16);
|
||||
|
||||
// 同步控制
|
||||
m_syncCheck = new QCheckBox("同步输出(两通道同步变化)");
|
||||
m_syncCheck->setStyleSheet("font-weight: 600;");
|
||||
connect(m_syncCheck, &QCheckBox::toggled, this, &DualChannelWidget::onSyncToggled);
|
||||
layout->addWidget(m_syncCheck);
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
// 开始按钮
|
||||
m_startBtn = new QPushButton("开始输出");
|
||||
m_startBtn->setStyleSheet(BUTTON_STYLE);
|
||||
m_startBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(m_startBtn, &QPushButton::clicked, this, &DualChannelWidget::onStartOutput);
|
||||
layout->addWidget(m_startBtn);
|
||||
|
||||
// 停止按钮
|
||||
m_stopBtn = new QPushButton("停止输出");
|
||||
m_stopBtn->setStyleSheet(STOP_BUTTON_STYLE);
|
||||
m_stopBtn->setCursor(Qt::PointingHandCursor);
|
||||
m_stopBtn->setEnabled(false);
|
||||
connect(m_stopBtn, &QPushButton::clicked, this, &DualChannelWidget::onStopOutput);
|
||||
layout->addWidget(m_stopBtn);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
void DualChannelWidget::onChannel1OutputChanged()
|
||||
{
|
||||
QStringList suffixes = {" V", " mA", " Ω", " Hz"};
|
||||
m_ch1ValueSpin->setSuffix(suffixes[m_ch1TypeCombo->currentIndex()]);
|
||||
}
|
||||
|
||||
void DualChannelWidget::onChannel2OutputChanged()
|
||||
{
|
||||
QStringList suffixes = {" V", " mA", " Ω", " Hz"};
|
||||
m_ch2ValueSpin->setSuffix(suffixes[m_ch2TypeCombo->currentIndex()]);
|
||||
}
|
||||
|
||||
void DualChannelWidget::onSyncToggled(bool checked)
|
||||
{
|
||||
if (checked)
|
||||
{
|
||||
// 同步通道2到通道1的设置
|
||||
m_ch2TypeCombo->setCurrentIndex(m_ch1TypeCombo->currentIndex());
|
||||
m_ch2ValueSpin->setValue(m_ch1ValueSpin->value());
|
||||
}
|
||||
}
|
||||
|
||||
void DualChannelWidget::onStartOutput()
|
||||
{
|
||||
m_isOutputting = true;
|
||||
m_startBtn->setEnabled(false);
|
||||
m_stopBtn->setEnabled(true);
|
||||
m_measureTimer->start(100);
|
||||
}
|
||||
|
||||
void DualChannelWidget::onStopOutput()
|
||||
{
|
||||
m_isOutputting = false;
|
||||
m_measureTimer->stop();
|
||||
m_startBtn->setEnabled(true);
|
||||
m_stopBtn->setEnabled(false);
|
||||
|
||||
m_ch1MeasureLabel->setText("-- V");
|
||||
m_ch2MeasureLabel->setText("-- V");
|
||||
m_ch1InputLabel->setText("-- V");
|
||||
m_ch2InputLabel->setText("-- V");
|
||||
}
|
||||
|
||||
void DualChannelWidget::updateMeasurement()
|
||||
{
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
static std::normal_distribution<> noise(0, 0.001);
|
||||
|
||||
QStringList units = {"V", "mA", "Ω", "Hz"};
|
||||
|
||||
if (m_ch1EnableCheck->isChecked())
|
||||
{
|
||||
double value = m_ch1ValueSpin->value() + noise(gen);
|
||||
m_ch1MeasureLabel->setText(QString("%1 %2")
|
||||
.arg(value, 0, 'f', 4)
|
||||
.arg(units[m_ch1TypeCombo->currentIndex()]));
|
||||
|
||||
// 模拟采集输入
|
||||
double inputValue = value * 0.98 + noise(gen) * 10;
|
||||
m_ch1InputLabel->setText(QString("%1 %2")
|
||||
.arg(inputValue, 0, 'f', 4)
|
||||
.arg(units[m_ch1TypeCombo->currentIndex()]));
|
||||
}
|
||||
|
||||
if (m_ch2EnableCheck->isChecked())
|
||||
{
|
||||
double value = m_ch2ValueSpin->value() + noise(gen);
|
||||
m_ch2MeasureLabel->setText(QString("%1 %2")
|
||||
.arg(value, 0, 'f', 4)
|
||||
.arg(units[m_ch2TypeCombo->currentIndex()]));
|
||||
|
||||
// 模拟采集输入
|
||||
double inputValue = value * 0.98 + noise(gen) * 10;
|
||||
m_ch2InputLabel->setText(QString("%1 %2")
|
||||
.arg(inputValue, 0, 'f', 4)
|
||||
.arg(units[m_ch2TypeCombo->currentIndex()]));
|
||||
}
|
||||
}
|
||||
78
widgets/dualchannelwidget.h
Normal file
@@ -0,0 +1,78 @@
|
||||
#ifndef DUALCHANNELWIDGET_H
|
||||
#define DUALCHANNELWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QComboBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QGroupBox>
|
||||
#include <QCheckBox>
|
||||
|
||||
/**
|
||||
* @brief 双通道页面
|
||||
*
|
||||
* 功能说明:
|
||||
* - 同时输出2路信号
|
||||
* - 同时采集2路信号
|
||||
*/
|
||||
class DualChannelWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DualChannelWidget(QWidget *parent = nullptr);
|
||||
~DualChannelWidget() = default;
|
||||
|
||||
signals:
|
||||
void backRequested();
|
||||
|
||||
private slots:
|
||||
void onChannel1OutputChanged();
|
||||
void onChannel2OutputChanged();
|
||||
void onSyncToggled(bool checked);
|
||||
void onStartOutput();
|
||||
void onStopOutput();
|
||||
void updateMeasurement();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
QWidget *createTitleBar();
|
||||
QWidget *createChannel1Panel();
|
||||
QWidget *createChannel2Panel();
|
||||
QWidget *createMeasurementPanel();
|
||||
QWidget *createControlPanel();
|
||||
|
||||
// 标题栏
|
||||
QPushButton *m_backBtn;
|
||||
QLabel *m_titleLabel;
|
||||
|
||||
// 通道1控制
|
||||
QComboBox *m_ch1TypeCombo;
|
||||
QDoubleSpinBox *m_ch1ValueSpin;
|
||||
QLabel *m_ch1MeasureLabel;
|
||||
QCheckBox *m_ch1EnableCheck;
|
||||
|
||||
// 通道2控制
|
||||
QComboBox *m_ch2TypeCombo;
|
||||
QDoubleSpinBox *m_ch2ValueSpin;
|
||||
QLabel *m_ch2MeasureLabel;
|
||||
QCheckBox *m_ch2EnableCheck;
|
||||
|
||||
// 同步控制
|
||||
QCheckBox *m_syncCheck;
|
||||
|
||||
// 控制按钮
|
||||
QPushButton *m_startBtn;
|
||||
QPushButton *m_stopBtn;
|
||||
|
||||
// 测量显示
|
||||
QLabel *m_ch1InputLabel;
|
||||
QLabel *m_ch2InputLabel;
|
||||
|
||||
// 状态
|
||||
bool m_isOutputting;
|
||||
QTimer *m_measureTimer;
|
||||
};
|
||||
|
||||
#endif // DUALCHANNELWIDGET_H
|
||||
207
widgets/launcherpage.cpp
Normal file
@@ -0,0 +1,207 @@
|
||||
#include "launcherpage.h"
|
||||
#include <QGridLayout>
|
||||
#include <QResizeEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QTimer>
|
||||
#include <QDebug>
|
||||
|
||||
LauncherPage::LauncherPage(const PageConfig &config, QWidget *parent)
|
||||
: QWidget(parent), m_config(config), m_columns(4), m_rows(3), m_horizontalGap(20), m_verticalGap(18), m_horizontalPadding(30), m_verticalPadding(20)
|
||||
{
|
||||
setObjectName("launcherPage_" + config.id);
|
||||
setupUI();
|
||||
}
|
||||
|
||||
LauncherPage::~LauncherPage()
|
||||
{
|
||||
}
|
||||
|
||||
void LauncherPage::setupUI()
|
||||
{
|
||||
// 设置背景透明,同时确保可以绑定paintEvent
|
||||
setAttribute(Qt::WA_StyledBackground, true);
|
||||
setStyleSheet("background: transparent;");
|
||||
|
||||
qDebug() << "=== LauncherPage setupUI ===";
|
||||
qDebug() << "Page ID:" << m_config.id;
|
||||
qDebug() << "Apps count:" << m_config.apps.size();
|
||||
|
||||
// 创建图标(不使用布局,使用绝对定位)
|
||||
for (int i = 0; i < m_config.apps.size(); ++i)
|
||||
{
|
||||
const auto &appData = m_config.apps[i];
|
||||
qDebug() << "Creating icon for:" << appData.name << "(" << appData.id << ")" << "group:" << appData.group;
|
||||
AppIcon *icon = new AppIcon(appData, this);
|
||||
icon->setFixedSize(190, 180);
|
||||
connect(icon, &AppIcon::clicked, this, &LauncherPage::appClicked);
|
||||
connect(icon, &AppIcon::longPressed, this, &LauncherPage::appLongPressed);
|
||||
m_icons.append(icon);
|
||||
|
||||
// 收集分组信息
|
||||
if (!appData.group.isEmpty())
|
||||
{
|
||||
if (!m_groups.contains(appData.group))
|
||||
{
|
||||
GroupInfo info;
|
||||
info.groupId = appData.group;
|
||||
m_groups[appData.group] = info;
|
||||
}
|
||||
m_groups[appData.group].iconIndices.append(i);
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Total icons created:" << m_icons.size();
|
||||
qDebug() << "Groups found:" << m_groups.keys();
|
||||
}
|
||||
|
||||
void LauncherPage::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
layoutIcons();
|
||||
}
|
||||
|
||||
void LauncherPage::layoutIcons()
|
||||
{
|
||||
if (m_icons.isEmpty())
|
||||
{
|
||||
qDebug() << "layoutIcons: No icons to layout!";
|
||||
return;
|
||||
}
|
||||
|
||||
int w = width();
|
||||
int h = height();
|
||||
|
||||
qDebug() << "=== layoutIcons ===";
|
||||
qDebug() << "Page ID:" << m_config.id;
|
||||
qDebug() << "Widget size:" << w << "x" << h;
|
||||
qDebug() << "Icons count:" << m_icons.size();
|
||||
|
||||
// Android风格:图标均匀分布
|
||||
int iconWidth = 190;
|
||||
int iconHeight = 180;
|
||||
|
||||
// 动态计算可以容纳的行数
|
||||
int minVerticalSpacing = 12;
|
||||
int maxPossibleRows = (h + minVerticalSpacing) / (iconHeight + minVerticalSpacing);
|
||||
int actualRows = qMin(maxPossibleRows, m_rows);
|
||||
actualRows = qMax(actualRows, 1); // 至少显示1行
|
||||
|
||||
qDebug() << "Max possible rows:" << maxPossibleRows << "Using rows:" << actualRows;
|
||||
|
||||
// 计算间距,使图标均匀分布
|
||||
int horizontalSpacing = (w - m_columns * iconWidth) / (m_columns + 1);
|
||||
int verticalSpacing = (h - actualRows * iconHeight) / (actualRows + 1);
|
||||
|
||||
// 确保最小间距
|
||||
horizontalSpacing = qMax(horizontalSpacing, 10);
|
||||
verticalSpacing = qMax(verticalSpacing, 12);
|
||||
|
||||
qDebug() << "Spacing - H:" << horizontalSpacing << "V:" << verticalSpacing;
|
||||
|
||||
// 重新计算起始位置使其居中
|
||||
int startX = (w - (m_columns * iconWidth + (m_columns - 1) * horizontalSpacing)) / 2;
|
||||
int startY = (h - (actualRows * iconHeight + (actualRows - 1) * verticalSpacing)) / 2;
|
||||
|
||||
qDebug() << "Start position - X:" << startX << "Y:" << startY;
|
||||
|
||||
// 布局图标,同时记录位置用于分组框计算
|
||||
QMap<int, QRect> iconRects;
|
||||
int index = 0;
|
||||
for (AppIcon *icon : m_icons)
|
||||
{
|
||||
int row = index / m_columns;
|
||||
int col = index % m_columns;
|
||||
|
||||
if (row < actualRows)
|
||||
{
|
||||
int x = startX + col * (iconWidth + horizontalSpacing);
|
||||
int y = startY + row * (iconHeight + verticalSpacing);
|
||||
qDebug() << " Icon" << index << "at" << x << "," << y;
|
||||
icon->move(x, y);
|
||||
icon->show();
|
||||
// 记录计算出的位置
|
||||
iconRects[index] = QRect(x, y, iconWidth, iconHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << " Icon" << index << "hidden (too many rows)";
|
||||
icon->hide();
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
qDebug() << "layoutIcons done, visible icons:" << qMin(index, m_rows * m_columns);
|
||||
|
||||
// 更新分组矩形,使用计算出的位置
|
||||
updateGroupRects(iconRects);
|
||||
}
|
||||
|
||||
void LauncherPage::updateGroupRects(const QMap<int, QRect> &iconRects)
|
||||
{
|
||||
// 为每个分组计算边界矩形
|
||||
for (auto it = m_groups.begin(); it != m_groups.end(); ++it)
|
||||
{
|
||||
GroupInfo &group = it.value();
|
||||
if (group.iconIndices.isEmpty())
|
||||
continue;
|
||||
|
||||
QRect boundingRect;
|
||||
bool first = true;
|
||||
|
||||
for (int idx : group.iconIndices)
|
||||
{
|
||||
if (iconRects.contains(idx))
|
||||
{
|
||||
QRect iconRect = iconRects[idx];
|
||||
qDebug() << " Group icon" << idx << "rect:" << iconRect;
|
||||
if (first)
|
||||
{
|
||||
boundingRect = iconRect;
|
||||
first = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
boundingRect = boundingRect.united(iconRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 扩展边界矩形,添加内边距
|
||||
int padding = 3;
|
||||
group.boundingRect = boundingRect.adjusted(-padding, -padding, padding, padding);
|
||||
qDebug() << "Group" << group.groupId << "bounding rect:" << group.boundingRect;
|
||||
}
|
||||
|
||||
// 触发重绘
|
||||
update();
|
||||
}
|
||||
|
||||
void LauncherPage::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
QWidget::paintEvent(event);
|
||||
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
// 绘制分组背景框
|
||||
for (const GroupInfo &group : m_groups)
|
||||
{
|
||||
qDebug() << "Drawing group:" << group.groupId << "rect:" << group.boundingRect;
|
||||
if (group.boundingRect.isValid() && !group.boundingRect.isEmpty())
|
||||
{
|
||||
// 设置半透明背景 - 增加透明度使其更明显
|
||||
QColor bgColor(255, 255, 255, 50); // 白色半透明
|
||||
painter.setBrush(bgColor);
|
||||
|
||||
// 设置边框
|
||||
QColor borderColor(255, 255, 255, 100); // 白色半透明边框
|
||||
painter.setPen(QPen(borderColor, 2.0));
|
||||
|
||||
// 绘制圆角矩形
|
||||
QPainterPath path;
|
||||
path.addRoundedRect(QRectF(group.boundingRect), 20, 20);
|
||||
painter.drawPath(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
63
widgets/launcherpage.h
Normal file
@@ -0,0 +1,63 @@
|
||||
#ifndef LAUNCHERPAGE_H
|
||||
#define LAUNCHERPAGE_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QVector>
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QRect>
|
||||
#include "appicon.h"
|
||||
|
||||
struct PageConfig
|
||||
{
|
||||
QString id;
|
||||
QString title;
|
||||
QVector<AppIconData> apps;
|
||||
};
|
||||
|
||||
// 分组信息结构
|
||||
struct GroupInfo
|
||||
{
|
||||
QString groupId;
|
||||
QVector<int> iconIndices; // 该分组包含的图标索引
|
||||
QRect boundingRect; // 分组的边界矩形
|
||||
};
|
||||
|
||||
class LauncherPage : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit LauncherPage(const PageConfig &config, QWidget *parent = nullptr);
|
||||
~LauncherPage();
|
||||
|
||||
QString pageId() const { return m_config.id; }
|
||||
QString pageTitle() const { return m_config.title; }
|
||||
|
||||
signals:
|
||||
void appClicked(const QString &appId);
|
||||
void appLongPressed(const QString &appId);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void layoutIcons();
|
||||
void updateGroupRects(const QMap<int, QRect> &iconRects); // 更新分组矩形
|
||||
|
||||
PageConfig m_config;
|
||||
QVector<AppIcon *> m_icons;
|
||||
QMap<QString, GroupInfo> m_groups; // 分组信息
|
||||
|
||||
// 网格布局参数
|
||||
int m_columns;
|
||||
int m_rows;
|
||||
int m_horizontalGap;
|
||||
int m_verticalGap;
|
||||
int m_horizontalPadding;
|
||||
int m_verticalPadding;
|
||||
};
|
||||
|
||||
#endif // LAUNCHERPAGE_H
|
||||
470
widgets/networktestwidget.cpp
Normal file
@@ -0,0 +1,470 @@
|
||||
#include "networktestwidget.h"
|
||||
#include "overlaydialogswidget.h"
|
||||
#include <QScrollArea>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
|
||||
// 样式常量定义
|
||||
const QString NetworkTestWidget::BUTTON_STYLE = R"(
|
||||
QPushButton {
|
||||
background-color: #f5f5f5;
|
||||
color: #333333;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
min-height: 36px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #e8e8e8;
|
||||
border-color: #ccc;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #d0d0d0;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #f0f0f0;
|
||||
color: #999;
|
||||
}
|
||||
)";
|
||||
|
||||
const QString NetworkTestWidget::PRIMARY_BUTTON_STYLE = R"(
|
||||
QPushButton {
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
min-height: 36px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #1976D2;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #1565C0;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #90CAF9;
|
||||
}
|
||||
)";
|
||||
|
||||
const QString NetworkTestWidget::INPUT_STYLE = R"(
|
||||
QLineEdit {
|
||||
background-color: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
min-height: 36px;
|
||||
}
|
||||
QLineEdit:focus {
|
||||
border-color: #2196F3;
|
||||
}
|
||||
)";
|
||||
|
||||
const QString NetworkTestWidget::GROUP_STYLE = R"(
|
||||
QGroupBox {
|
||||
background-color: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
left: 16px;
|
||||
padding: 0 8px;
|
||||
color: #333;
|
||||
}
|
||||
)";
|
||||
|
||||
NetworkTestWidget::NetworkTestWidget(QWidget *parent)
|
||||
: QWidget(parent), m_testStep(0)
|
||||
{
|
||||
setupUI();
|
||||
|
||||
m_testTimer = new QTimer(this);
|
||||
connect(m_testTimer, &QTimer::timeout, this, [this]()
|
||||
{
|
||||
m_testStep++;
|
||||
m_progressBar->setValue(m_testStep * 10);
|
||||
|
||||
if (m_testStep >= 10) {
|
||||
m_testTimer->stop();
|
||||
m_progressBar->setValue(100);
|
||||
} });
|
||||
}
|
||||
|
||||
void NetworkTestWidget::setupUI()
|
||||
{
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->setSpacing(0);
|
||||
|
||||
// 标题栏
|
||||
mainLayout->addWidget(createTitleBar());
|
||||
|
||||
// 内容区域
|
||||
QScrollArea *scrollArea = new QScrollArea;
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
scrollArea->setStyleSheet("QScrollArea { background-color: #f5f5f5; }");
|
||||
|
||||
QWidget *contentWidget = new QWidget;
|
||||
contentWidget->setStyleSheet("background-color: #f5f5f5;");
|
||||
QVBoxLayout *contentLayout = new QVBoxLayout(contentWidget);
|
||||
contentLayout->setContentsMargins(20, 20, 20, 20);
|
||||
contentLayout->setSpacing(16);
|
||||
|
||||
contentLayout->addWidget(createTestPanel());
|
||||
contentLayout->addWidget(createResultPanel(), 1);
|
||||
|
||||
scrollArea->setWidget(contentWidget);
|
||||
mainLayout->addWidget(scrollArea, 1);
|
||||
}
|
||||
|
||||
QWidget *NetworkTestWidget::createTitleBar()
|
||||
{
|
||||
QWidget *titleBar = new QWidget;
|
||||
titleBar->setFixedHeight(60);
|
||||
titleBar->setStyleSheet("background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #1976D2, stop:1 #1565C0);");
|
||||
|
||||
QHBoxLayout *layout = new QHBoxLayout(titleBar);
|
||||
layout->setContentsMargins(16, 0, 16, 0);
|
||||
|
||||
// 返回按钮
|
||||
m_backBtn = new QPushButton("← 返回");
|
||||
m_backBtn->setCursor(Qt::PointingHandCursor);
|
||||
m_backBtn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background: rgba(255,255,255,0.2);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
)");
|
||||
connect(m_backBtn, &QPushButton::clicked, this, &NetworkTestWidget::backRequested);
|
||||
layout->addWidget(m_backBtn);
|
||||
|
||||
// 标题(居中)
|
||||
m_titleLabel = new QLabel("网络测试");
|
||||
m_titleLabel->setStyleSheet("color: white; font-size: 18px; font-weight: bold;");
|
||||
m_titleLabel->setAlignment(Qt::AlignCenter);
|
||||
layout->addWidget(m_titleLabel, 1);
|
||||
|
||||
// 右侧占位,保持标题居中
|
||||
QWidget *spacer = new QWidget;
|
||||
spacer->setFixedWidth(m_backBtn->sizeHint().width());
|
||||
layout->addWidget(spacer);
|
||||
|
||||
return titleBar;
|
||||
}
|
||||
|
||||
QWidget *NetworkTestWidget::createTestPanel()
|
||||
{
|
||||
QGroupBox *group = new QGroupBox("测试设置");
|
||||
group->setStyleSheet(GROUP_STYLE);
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout(group);
|
||||
layout->setContentsMargins(16, 24, 16, 16);
|
||||
layout->setSpacing(16);
|
||||
|
||||
// Ping测试
|
||||
QHBoxLayout *pingLayout = new QHBoxLayout;
|
||||
QLabel *pingLabel = new QLabel("Ping主机:");
|
||||
pingLabel->setFixedWidth(80);
|
||||
pingLabel->setStyleSheet("font-size: 14px; color: #333;");
|
||||
pingLayout->addWidget(pingLabel);
|
||||
|
||||
m_hostEdit = new QLineEdit;
|
||||
m_hostEdit->setStyleSheet(INPUT_STYLE);
|
||||
m_hostEdit->setText("www.baidu.com");
|
||||
m_hostEdit->setPlaceholderText("输入主机名或IP地址");
|
||||
pingLayout->addWidget(m_hostEdit, 1);
|
||||
|
||||
m_pingBtn = new QPushButton("Ping测试");
|
||||
m_pingBtn->setStyleSheet(PRIMARY_BUTTON_STYLE);
|
||||
m_pingBtn->setFixedWidth(100);
|
||||
m_pingBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(m_pingBtn, &QPushButton::clicked, this, &NetworkTestWidget::onPingTest);
|
||||
pingLayout->addWidget(m_pingBtn);
|
||||
|
||||
layout->addLayout(pingLayout);
|
||||
|
||||
// 服务器连接测试
|
||||
QHBoxLayout *serverLayout = new QHBoxLayout;
|
||||
QLabel *serverLabel = new QLabel("服务器:");
|
||||
serverLabel->setFixedWidth(80);
|
||||
serverLabel->setStyleSheet("font-size: 14px; color: #333;");
|
||||
serverLayout->addWidget(serverLabel);
|
||||
|
||||
m_serverEdit = new QLineEdit;
|
||||
m_serverEdit->setStyleSheet(INPUT_STYLE);
|
||||
m_serverEdit->setText("192.168.1.100");
|
||||
m_serverEdit->setPlaceholderText("服务器地址");
|
||||
serverLayout->addWidget(m_serverEdit, 1);
|
||||
|
||||
QLabel *portLabel = new QLabel("端口:");
|
||||
portLabel->setStyleSheet("font-size: 14px; color: #333;");
|
||||
serverLayout->addWidget(portLabel);
|
||||
|
||||
m_portEdit = new QLineEdit;
|
||||
m_portEdit->setStyleSheet(INPUT_STYLE);
|
||||
m_portEdit->setFixedWidth(80);
|
||||
m_portEdit->setText("8080");
|
||||
serverLayout->addWidget(m_portEdit);
|
||||
|
||||
m_serverTestBtn = new QPushButton("连接测试");
|
||||
m_serverTestBtn->setStyleSheet(PRIMARY_BUTTON_STYLE);
|
||||
m_serverTestBtn->setFixedWidth(100);
|
||||
m_serverTestBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(m_serverTestBtn, &QPushButton::clicked, this, &NetworkTestWidget::onServerTest);
|
||||
serverLayout->addWidget(m_serverTestBtn);
|
||||
|
||||
layout->addLayout(serverLayout);
|
||||
|
||||
// 其他测试按钮
|
||||
QHBoxLayout *otherLayout = new QHBoxLayout;
|
||||
|
||||
m_dnsTestBtn = new QPushButton("DNS解析测试");
|
||||
m_dnsTestBtn->setStyleSheet(BUTTON_STYLE);
|
||||
m_dnsTestBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(m_dnsTestBtn, &QPushButton::clicked, this, &NetworkTestWidget::onDnsTest);
|
||||
otherLayout->addWidget(m_dnsTestBtn);
|
||||
|
||||
m_speedTestBtn = new QPushButton("网速测试");
|
||||
m_speedTestBtn->setStyleSheet(BUTTON_STYLE);
|
||||
m_speedTestBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(m_speedTestBtn, &QPushButton::clicked, this, &NetworkTestWidget::onSpeedTest);
|
||||
otherLayout->addWidget(m_speedTestBtn);
|
||||
|
||||
otherLayout->addStretch();
|
||||
|
||||
layout->addLayout(otherLayout);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
QWidget *NetworkTestWidget::createResultPanel()
|
||||
{
|
||||
QGroupBox *group = new QGroupBox("测试结果");
|
||||
group->setStyleSheet(GROUP_STYLE);
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout(group);
|
||||
layout->setContentsMargins(16, 24, 16, 16);
|
||||
layout->setSpacing(12);
|
||||
|
||||
// 进度条
|
||||
m_progressBar = new QProgressBar;
|
||||
m_progressBar->setFixedHeight(6);
|
||||
m_progressBar->setRange(0, 100);
|
||||
m_progressBar->setValue(0);
|
||||
m_progressBar->setTextVisible(false);
|
||||
m_progressBar->setStyleSheet(R"(
|
||||
QProgressBar {
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background-color: #4CAF50;
|
||||
border-radius: 3px;
|
||||
}
|
||||
)");
|
||||
layout->addWidget(m_progressBar);
|
||||
|
||||
// 日志显示
|
||||
m_logEdit = new QTextEdit;
|
||||
m_logEdit->setReadOnly(true);
|
||||
m_logEdit->setStyleSheet(R"(
|
||||
QTextEdit {
|
||||
background-color: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
)");
|
||||
m_logEdit->setMinimumHeight(300);
|
||||
layout->addWidget(m_logEdit, 1);
|
||||
|
||||
// 清除按钮
|
||||
QHBoxLayout *buttonLayout = new QHBoxLayout;
|
||||
buttonLayout->addStretch();
|
||||
|
||||
m_clearBtn = new QPushButton("清除日志");
|
||||
m_clearBtn->setStyleSheet(BUTTON_STYLE);
|
||||
m_clearBtn->setFixedWidth(100);
|
||||
m_clearBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(m_clearBtn, &QPushButton::clicked, this, &NetworkTestWidget::onClearLog);
|
||||
buttonLayout->addWidget(m_clearBtn);
|
||||
|
||||
layout->addLayout(buttonLayout);
|
||||
|
||||
// 添加初始日志
|
||||
appendLog("网络测试工具已就绪", "#4CAF50");
|
||||
appendLog("请选择测试项目开始测试...", "#888");
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
void NetworkTestWidget::appendLog(const QString &message, const QString &color)
|
||||
{
|
||||
QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss");
|
||||
QString html = QString("<span style='color: #888;'>[%1]</span> <span style='color: %2;'>%3</span><br>")
|
||||
.arg(timestamp, color, message);
|
||||
m_logEdit->insertHtml(html);
|
||||
m_logEdit->moveCursor(QTextCursor::End);
|
||||
}
|
||||
|
||||
void NetworkTestWidget::onPingTest()
|
||||
{
|
||||
QString host = m_hostEdit->text().trimmed();
|
||||
if (host.isEmpty())
|
||||
{
|
||||
OverlayDialog::warning(window(), "提示", "请输入要Ping的主机名或IP地址");
|
||||
return;
|
||||
}
|
||||
|
||||
m_pingBtn->setEnabled(false);
|
||||
m_progressBar->setValue(0);
|
||||
m_testStep = 0;
|
||||
|
||||
appendLog(QString("开始Ping测试: %1").arg(host), "#2196F3");
|
||||
|
||||
m_testTimer->start(200);
|
||||
|
||||
// 模拟Ping测试
|
||||
QTimer::singleShot(2000, this, [this, host]()
|
||||
{
|
||||
m_testTimer->stop();
|
||||
m_progressBar->setValue(100);
|
||||
m_pingBtn->setEnabled(true);
|
||||
|
||||
// 模拟结果
|
||||
appendLog(QString("正在解析主机 %1...").arg(host), "#888");
|
||||
appendLog(QString("目标IP: 110.242.68.66"), "#888");
|
||||
appendLog("", "#888");
|
||||
appendLog("来自 110.242.68.66 的回复: 字节=64 时间=15ms TTL=52", "#4CAF50");
|
||||
appendLog("来自 110.242.68.66 的回复: 字节=64 时间=14ms TTL=52", "#4CAF50");
|
||||
appendLog("来自 110.242.68.66 的回复: 字节=64 时间=16ms TTL=52", "#4CAF50");
|
||||
appendLog("来自 110.242.68.66 的回复: 字节=64 时间=15ms TTL=52", "#4CAF50");
|
||||
appendLog("", "#888");
|
||||
appendLog("Ping统计: 已发送=4, 已接收=4, 丢失=0 (0% 丢失)", "#4CAF50");
|
||||
appendLog("往返时间: 最短=14ms, 最长=16ms, 平均=15ms", "#888");
|
||||
appendLog("", "#888");
|
||||
appendLog("✓ Ping测试完成", "#4CAF50"); });
|
||||
}
|
||||
|
||||
void NetworkTestWidget::onSpeedTest()
|
||||
{
|
||||
m_speedTestBtn->setEnabled(false);
|
||||
m_progressBar->setValue(0);
|
||||
m_testStep = 0;
|
||||
|
||||
appendLog("开始网速测试...", "#2196F3");
|
||||
|
||||
m_testTimer->start(300);
|
||||
|
||||
QTimer::singleShot(3000, this, [this]()
|
||||
{
|
||||
m_testTimer->stop();
|
||||
m_progressBar->setValue(100);
|
||||
m_speedTestBtn->setEnabled(true);
|
||||
|
||||
appendLog("正在测试下载速度...", "#888");
|
||||
appendLog("下载速度: 45.6 Mbps", "#4CAF50");
|
||||
appendLog("正在测试上传速度...", "#888");
|
||||
appendLog("上传速度: 12.3 Mbps", "#4CAF50");
|
||||
appendLog("网络延迟: 18ms", "#888");
|
||||
appendLog("抖动: 2ms", "#888");
|
||||
appendLog("", "#888");
|
||||
appendLog("✓ 网速测试完成", "#4CAF50"); });
|
||||
}
|
||||
|
||||
void NetworkTestWidget::onDnsTest()
|
||||
{
|
||||
m_dnsTestBtn->setEnabled(false);
|
||||
m_progressBar->setValue(0);
|
||||
m_testStep = 0;
|
||||
|
||||
appendLog("开始DNS解析测试...", "#2196F3");
|
||||
|
||||
m_testTimer->start(150);
|
||||
|
||||
QTimer::singleShot(1500, this, [this]()
|
||||
{
|
||||
m_testTimer->stop();
|
||||
m_progressBar->setValue(100);
|
||||
m_dnsTestBtn->setEnabled(true);
|
||||
|
||||
appendLog("DNS服务器: 114.114.114.114", "#888");
|
||||
appendLog("", "#888");
|
||||
appendLog("www.baidu.com -> 110.242.68.66 (8ms)", "#4CAF50");
|
||||
appendLog("www.google.com -> 142.250.80.46 (25ms)", "#4CAF50");
|
||||
appendLog("www.github.com -> 20.205.243.166 (45ms)", "#4CAF50");
|
||||
appendLog("", "#888");
|
||||
appendLog("✓ DNS解析测试完成", "#4CAF50"); });
|
||||
}
|
||||
|
||||
void NetworkTestWidget::onServerTest()
|
||||
{
|
||||
QString server = m_serverEdit->text().trimmed();
|
||||
QString port = m_portEdit->text().trimmed();
|
||||
|
||||
if (server.isEmpty())
|
||||
{
|
||||
OverlayDialog::warning(window(), "提示", "请输入服务器地址");
|
||||
return;
|
||||
}
|
||||
|
||||
m_serverTestBtn->setEnabled(false);
|
||||
m_progressBar->setValue(0);
|
||||
m_testStep = 0;
|
||||
|
||||
appendLog(QString("开始连接测试: %1:%2").arg(server, port), "#2196F3");
|
||||
|
||||
m_testTimer->start(200);
|
||||
|
||||
QTimer::singleShot(2000, this, [this, server, port]()
|
||||
{
|
||||
m_testTimer->stop();
|
||||
m_progressBar->setValue(100);
|
||||
m_serverTestBtn->setEnabled(true);
|
||||
|
||||
// 随机模拟成功或超时
|
||||
bool success = (QDateTime::currentMSecsSinceEpoch() % 3) != 0;
|
||||
|
||||
if (success) {
|
||||
appendLog(QString("正在连接 %1:%2...").arg(server, port), "#888");
|
||||
appendLog("TCP握手成功", "#4CAF50");
|
||||
appendLog("连接延迟: 25ms", "#888");
|
||||
appendLog("服务器响应: HTTP 200 OK", "#4CAF50");
|
||||
appendLog("", "#888");
|
||||
appendLog("✓ 服务器连接测试成功", "#4CAF50");
|
||||
} else {
|
||||
appendLog(QString("正在连接 %1:%2...").arg(server, port), "#888");
|
||||
appendLog("连接超时 (5000ms)", "#f44336");
|
||||
appendLog("", "#888");
|
||||
appendLog("✗ 服务器连接测试失败", "#f44336");
|
||||
appendLog("请检查服务器地址和端口是否正确", "#ff9800");
|
||||
} });
|
||||
}
|
||||
|
||||
void NetworkTestWidget::onClearLog()
|
||||
{
|
||||
m_logEdit->clear();
|
||||
m_progressBar->setValue(0);
|
||||
appendLog("日志已清除", "#888");
|
||||
}
|
||||
72
widgets/networktestwidget.h
Normal file
@@ -0,0 +1,72 @@
|
||||
#ifndef NETWORKTESTWIDGET_H
|
||||
#define NETWORKTESTWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QLineEdit>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QTextEdit>
|
||||
#include <QProgressBar>
|
||||
#include <QGroupBox>
|
||||
#include <QTimer>
|
||||
|
||||
/**
|
||||
* @brief 网络测试页面
|
||||
* 提供网络连接测试和诊断功能
|
||||
*/
|
||||
class NetworkTestWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit NetworkTestWidget(QWidget *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void backRequested();
|
||||
|
||||
private slots:
|
||||
void onPingTest();
|
||||
void onSpeedTest();
|
||||
void onDnsTest();
|
||||
void onServerTest();
|
||||
void onClearLog();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
QWidget *createTitleBar();
|
||||
QWidget *createTestPanel();
|
||||
QWidget *createResultPanel();
|
||||
void appendLog(const QString &message, const QString &color = "#333");
|
||||
|
||||
// 标题栏
|
||||
QLabel *m_titleLabel;
|
||||
QPushButton *m_backBtn;
|
||||
|
||||
// 测试面板
|
||||
QLineEdit *m_hostEdit;
|
||||
QLineEdit *m_serverEdit;
|
||||
QLineEdit *m_portEdit;
|
||||
QPushButton *m_pingBtn;
|
||||
QPushButton *m_speedTestBtn;
|
||||
QPushButton *m_dnsTestBtn;
|
||||
QPushButton *m_serverTestBtn;
|
||||
|
||||
// 结果面板
|
||||
QTextEdit *m_logEdit;
|
||||
QProgressBar *m_progressBar;
|
||||
QPushButton *m_clearBtn;
|
||||
|
||||
// 测试定时器
|
||||
QTimer *m_testTimer;
|
||||
int m_testStep;
|
||||
|
||||
// 样式常量
|
||||
static const QString BUTTON_STYLE;
|
||||
static const QString PRIMARY_BUTTON_STYLE;
|
||||
static const QString INPUT_STYLE;
|
||||
static const QString GROUP_STYLE;
|
||||
};
|
||||
|
||||
#endif // NETWORKTESTWIDGET_H
|
||||
322
widgets/overlaydialogswidget.cpp
Normal file
@@ -0,0 +1,322 @@
|
||||
#include "overlaydialogswidget.h"
|
||||
#include <QApplication>
|
||||
|
||||
OverlayDialog::OverlayDialog(QWidget *parent)
|
||||
: QWidget(parent), m_type(Information), m_inputMode(false)
|
||||
{
|
||||
setupUI();
|
||||
hide();
|
||||
}
|
||||
|
||||
void OverlayDialog::setupUI()
|
||||
{
|
||||
// 设置为覆盖整个父窗口
|
||||
setAttribute(Qt::WA_StyledBackground);
|
||||
setStyleSheet("background: transparent;");
|
||||
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
// 半透明背景遮罩
|
||||
m_backgroundOverlay = new QWidget(this);
|
||||
m_backgroundOverlay->setStyleSheet("background: rgba(0, 0, 0, 0.5);");
|
||||
mainLayout->addWidget(m_backgroundOverlay);
|
||||
|
||||
// 对话框容器 - 居中显示
|
||||
m_dialogContainer = new QWidget(m_backgroundOverlay);
|
||||
m_dialogContainer->setFixedWidth(420);
|
||||
m_dialogContainer->setStyleSheet(
|
||||
"QWidget { background: white; border-radius: 12px; }");
|
||||
|
||||
auto *dialogLayout = new QVBoxLayout(m_dialogContainer);
|
||||
dialogLayout->setContentsMargins(24, 24, 24, 20);
|
||||
dialogLayout->setSpacing(16);
|
||||
|
||||
// 标题栏
|
||||
auto *headerLayout = new QHBoxLayout();
|
||||
headerLayout->setSpacing(12);
|
||||
|
||||
m_iconLabel = new QLabel(this);
|
||||
m_iconLabel->setFixedSize(32, 32);
|
||||
m_iconLabel->setAlignment(Qt::AlignCenter);
|
||||
m_iconLabel->setStyleSheet("font-size: 24px;");
|
||||
headerLayout->addWidget(m_iconLabel);
|
||||
|
||||
m_titleLabel = new QLabel("提示", this);
|
||||
m_titleLabel->setStyleSheet(
|
||||
"font-size: 18px; font-weight: bold; color: #263238;");
|
||||
headerLayout->addWidget(m_titleLabel, 1);
|
||||
|
||||
dialogLayout->addLayout(headerLayout);
|
||||
|
||||
// 消息内容
|
||||
m_messageLabel = new QLabel(this);
|
||||
m_messageLabel->setWordWrap(true);
|
||||
m_messageLabel->setStyleSheet(
|
||||
"font-size: 14px; color: #546E7A; line-height: 1.6; padding: 8px 0;");
|
||||
dialogLayout->addWidget(m_messageLabel);
|
||||
|
||||
// 输入框(默认隐藏)
|
||||
m_inputEdit = new QLineEdit(this);
|
||||
m_inputEdit->setFixedHeight(44);
|
||||
m_inputEdit->setStyleSheet(
|
||||
"QLineEdit { border: 2px solid #E0E0E0; border-radius: 8px; padding: 0 12px; "
|
||||
"font-size: 14px; background: #FAFAFA; }"
|
||||
"QLineEdit:focus { border-color: #1976D2; background: white; }");
|
||||
m_inputEdit->hide();
|
||||
dialogLayout->addWidget(m_inputEdit);
|
||||
|
||||
// 按钮区域
|
||||
auto *buttonLayout = new QHBoxLayout();
|
||||
buttonLayout->setSpacing(12);
|
||||
buttonLayout->addStretch();
|
||||
|
||||
m_rejectBtn = new QPushButton("取消", this);
|
||||
m_rejectBtn->setFixedSize(100, 44);
|
||||
m_rejectBtn->setCursor(Qt::PointingHandCursor);
|
||||
m_rejectBtn->setStyleSheet(
|
||||
"QPushButton { background: #ECEFF1; color: #546E7A; border: none; "
|
||||
"border-radius: 8px; font-size: 14px; font-weight: 500; }"
|
||||
"QPushButton:hover { background: #CFD8DC; }"
|
||||
"QPushButton:pressed { background: #B0BEC5; }");
|
||||
connect(m_rejectBtn, &QPushButton::clicked, this, &OverlayDialog::onRejectClicked);
|
||||
buttonLayout->addWidget(m_rejectBtn);
|
||||
|
||||
m_acceptBtn = new QPushButton("确定", this);
|
||||
m_acceptBtn->setFixedSize(100, 44);
|
||||
m_acceptBtn->setCursor(Qt::PointingHandCursor);
|
||||
m_acceptBtn->setStyleSheet(
|
||||
"QPushButton { background: #1976D2; color: white; border: none; "
|
||||
"border-radius: 8px; font-size: 14px; font-weight: 500; }"
|
||||
"QPushButton:hover { background: #1565C0; }"
|
||||
"QPushButton:pressed { background: #0D47A1; }");
|
||||
connect(m_acceptBtn, &QPushButton::clicked, this, &OverlayDialog::onAcceptClicked);
|
||||
buttonLayout->addWidget(m_acceptBtn);
|
||||
|
||||
dialogLayout->addLayout(buttonLayout);
|
||||
|
||||
// 居中布局
|
||||
auto *overlayLayout = new QVBoxLayout(m_backgroundOverlay);
|
||||
overlayLayout->addStretch();
|
||||
auto *centerLayout = new QHBoxLayout();
|
||||
centerLayout->addStretch();
|
||||
centerLayout->addWidget(m_dialogContainer);
|
||||
centerLayout->addStretch();
|
||||
overlayLayout->addLayout(centerLayout);
|
||||
overlayLayout->addStretch();
|
||||
}
|
||||
|
||||
void OverlayDialog::setTitle(const QString &title)
|
||||
{
|
||||
m_titleLabel->setText(title);
|
||||
}
|
||||
|
||||
void OverlayDialog::setMessage(const QString &message)
|
||||
{
|
||||
m_messageLabel->setText(message);
|
||||
}
|
||||
|
||||
void OverlayDialog::setDialogType(DialogType type)
|
||||
{
|
||||
m_type = type;
|
||||
updateTypeIcon();
|
||||
|
||||
// 根据类型调整按钮显示
|
||||
switch (type)
|
||||
{
|
||||
case Information:
|
||||
case Warning:
|
||||
m_rejectBtn->hide();
|
||||
m_acceptBtn->setText("确定");
|
||||
break;
|
||||
case Question:
|
||||
m_rejectBtn->show();
|
||||
m_rejectBtn->setText("取消");
|
||||
m_acceptBtn->setText("确定");
|
||||
break;
|
||||
case Input:
|
||||
m_rejectBtn->show();
|
||||
m_rejectBtn->setText("取消");
|
||||
m_acceptBtn->setText("确定");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void OverlayDialog::setInputMode(bool inputMode, const QString &placeholder)
|
||||
{
|
||||
m_inputMode = inputMode;
|
||||
m_inputEdit->setVisible(inputMode);
|
||||
m_inputEdit->setPlaceholderText(placeholder);
|
||||
m_inputEdit->clear();
|
||||
}
|
||||
|
||||
void OverlayDialog::setCallback(std::function<void(bool, const QString &)> callback)
|
||||
{
|
||||
m_callback = callback;
|
||||
}
|
||||
|
||||
void OverlayDialog::updateTypeIcon()
|
||||
{
|
||||
QString icon;
|
||||
QString color;
|
||||
|
||||
switch (m_type)
|
||||
{
|
||||
case Information:
|
||||
icon = "ℹ";
|
||||
color = "#1976D2";
|
||||
break;
|
||||
case Warning:
|
||||
icon = "⚠";
|
||||
color = "#F57C00";
|
||||
break;
|
||||
case Question:
|
||||
icon = "?";
|
||||
color = "#1976D2";
|
||||
break;
|
||||
case Input:
|
||||
icon = "✏";
|
||||
color = "#1976D2";
|
||||
break;
|
||||
}
|
||||
|
||||
m_iconLabel->setText(icon);
|
||||
m_iconLabel->setStyleSheet(QString("font-size: 24px; color: %1;").arg(color));
|
||||
}
|
||||
|
||||
void OverlayDialog::showDialog()
|
||||
{
|
||||
if (parentWidget())
|
||||
{
|
||||
setGeometry(parentWidget()->rect());
|
||||
}
|
||||
show();
|
||||
raise();
|
||||
animateShow();
|
||||
|
||||
if (m_inputMode)
|
||||
{
|
||||
m_inputEdit->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void OverlayDialog::hideDialog()
|
||||
{
|
||||
animateHide();
|
||||
}
|
||||
|
||||
void OverlayDialog::onAcceptClicked()
|
||||
{
|
||||
QString inputText = m_inputMode ? m_inputEdit->text() : QString();
|
||||
emit accepted(inputText);
|
||||
|
||||
if (m_callback)
|
||||
{
|
||||
m_callback(true, inputText);
|
||||
}
|
||||
|
||||
hideDialog();
|
||||
}
|
||||
|
||||
void OverlayDialog::onRejectClicked()
|
||||
{
|
||||
emit rejected();
|
||||
|
||||
if (m_callback)
|
||||
{
|
||||
m_callback(false, QString());
|
||||
}
|
||||
|
||||
hideDialog();
|
||||
}
|
||||
|
||||
void OverlayDialog::animateShow()
|
||||
{
|
||||
QGraphicsOpacityEffect *effect = new QGraphicsOpacityEffect(this);
|
||||
setGraphicsEffect(effect);
|
||||
|
||||
QPropertyAnimation *animation = new QPropertyAnimation(effect, "opacity");
|
||||
animation->setDuration(150);
|
||||
animation->setStartValue(0.0);
|
||||
animation->setEndValue(1.0);
|
||||
animation->start(QPropertyAnimation::DeleteWhenStopped);
|
||||
}
|
||||
|
||||
void OverlayDialog::animateHide()
|
||||
{
|
||||
QGraphicsOpacityEffect *effect = new QGraphicsOpacityEffect(this);
|
||||
setGraphicsEffect(effect);
|
||||
|
||||
QPropertyAnimation *animation = new QPropertyAnimation(effect, "opacity");
|
||||
animation->setDuration(100);
|
||||
animation->setStartValue(1.0);
|
||||
animation->setEndValue(0.0);
|
||||
|
||||
connect(animation, &QPropertyAnimation::finished, this, [this]()
|
||||
{
|
||||
hide();
|
||||
deleteLater(); });
|
||||
animation->start(QPropertyAnimation::DeleteWhenStopped);
|
||||
}
|
||||
|
||||
// 静态便捷方法
|
||||
void OverlayDialog::information(QWidget *parent, const QString &title, const QString &message)
|
||||
{
|
||||
// 找到顶层窗口
|
||||
QWidget *topLevel = parent;
|
||||
while (topLevel->parentWidget())
|
||||
topLevel = topLevel->parentWidget();
|
||||
|
||||
auto *dialog = new OverlayDialog(topLevel);
|
||||
dialog->setTitle(title);
|
||||
dialog->setMessage(message);
|
||||
dialog->setDialogType(Information);
|
||||
dialog->showDialog();
|
||||
}
|
||||
|
||||
void OverlayDialog::warning(QWidget *parent, const QString &title, const QString &message)
|
||||
{
|
||||
QWidget *topLevel = parent;
|
||||
while (topLevel->parentWidget())
|
||||
topLevel = topLevel->parentWidget();
|
||||
|
||||
auto *dialog = new OverlayDialog(topLevel);
|
||||
dialog->setTitle(title);
|
||||
dialog->setMessage(message);
|
||||
dialog->setDialogType(Warning);
|
||||
dialog->showDialog();
|
||||
}
|
||||
|
||||
void OverlayDialog::question(QWidget *parent, const QString &title, const QString &message,
|
||||
std::function<void(bool)> callback)
|
||||
{
|
||||
QWidget *topLevel = parent;
|
||||
while (topLevel->parentWidget())
|
||||
topLevel = topLevel->parentWidget();
|
||||
|
||||
auto *dialog = new OverlayDialog(topLevel);
|
||||
dialog->setTitle(title);
|
||||
dialog->setMessage(message);
|
||||
dialog->setDialogType(Question);
|
||||
dialog->setCallback([callback](bool accepted, const QString &)
|
||||
{ callback(accepted); });
|
||||
dialog->showDialog();
|
||||
}
|
||||
|
||||
void OverlayDialog::input(QWidget *parent, const QString &title, const QString &prompt,
|
||||
std::function<void(bool, const QString &)> callback,
|
||||
const QString &defaultText)
|
||||
{
|
||||
QWidget *topLevel = parent;
|
||||
while (topLevel->parentWidget())
|
||||
topLevel = topLevel->parentWidget();
|
||||
|
||||
auto *dialog = new OverlayDialog(topLevel);
|
||||
dialog->setTitle(title);
|
||||
dialog->setMessage(prompt);
|
||||
dialog->setDialogType(Input);
|
||||
dialog->setInputMode(true, "请输入...");
|
||||
dialog->m_inputEdit->setText(defaultText);
|
||||
dialog->setCallback(callback);
|
||||
dialog->showDialog();
|
||||
}
|
||||
81
widgets/overlaydialogswidget.h
Normal file
@@ -0,0 +1,81 @@
|
||||
#ifndef OVERLAYDIALOGSWIDGET_H
|
||||
#define OVERLAYDIALOGSWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QLineEdit>
|
||||
#include <QGraphicsOpacityEffect>
|
||||
#include <QPropertyAnimation>
|
||||
#include <functional>
|
||||
|
||||
/**
|
||||
* @class OverlayDialog
|
||||
* @brief 浮层对话框基类
|
||||
*
|
||||
* 用于IoT设备的模态浮层对话框,替代QMessageBox和QInputDialog
|
||||
*/
|
||||
class OverlayDialog : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum DialogType
|
||||
{
|
||||
Information,
|
||||
Warning,
|
||||
Question,
|
||||
Input
|
||||
};
|
||||
|
||||
explicit OverlayDialog(QWidget *parent = nullptr);
|
||||
|
||||
// 静态便捷方法
|
||||
static void information(QWidget *parent, const QString &title, const QString &message);
|
||||
static void warning(QWidget *parent, const QString &title, const QString &message);
|
||||
static void question(QWidget *parent, const QString &title, const QString &message,
|
||||
std::function<void(bool)> callback);
|
||||
static void input(QWidget *parent, const QString &title, const QString &prompt,
|
||||
std::function<void(bool, const QString &)> callback,
|
||||
const QString &defaultText = "");
|
||||
|
||||
void setTitle(const QString &title);
|
||||
void setMessage(const QString &message);
|
||||
void setDialogType(DialogType type);
|
||||
void setInputMode(bool inputMode, const QString &placeholder = "");
|
||||
void setCallback(std::function<void(bool, const QString &)> callback);
|
||||
|
||||
void showDialog();
|
||||
void hideDialog();
|
||||
|
||||
signals:
|
||||
void accepted(const QString &inputText = QString());
|
||||
void rejected();
|
||||
|
||||
private slots:
|
||||
void onAcceptClicked();
|
||||
void onRejectClicked();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void updateTypeIcon();
|
||||
void animateShow();
|
||||
void animateHide();
|
||||
|
||||
QWidget *m_backgroundOverlay;
|
||||
QWidget *m_dialogContainer;
|
||||
QLabel *m_iconLabel;
|
||||
QLabel *m_titleLabel;
|
||||
QLabel *m_messageLabel;
|
||||
QLineEdit *m_inputEdit;
|
||||
QPushButton *m_acceptBtn;
|
||||
QPushButton *m_rejectBtn;
|
||||
|
||||
DialogType m_type;
|
||||
bool m_inputMode;
|
||||
std::function<void(bool, const QString &)> m_callback;
|
||||
};
|
||||
|
||||
#endif // OVERLAYDIALOGSWIDGET_H
|
||||
116
widgets/pageindicator.cpp
Normal file
@@ -0,0 +1,116 @@
|
||||
#include "pageindicator.h"
|
||||
#include <QPainter>
|
||||
#include <QMouseEvent>
|
||||
|
||||
PageIndicator::PageIndicator(int pageCount, QWidget *parent)
|
||||
: QWidget(parent), m_pageCount(pageCount), m_currentPage(0), m_dotSize(8), m_selectedDotSize(10), m_dotSpacing(14), m_dotColor(QColor(255, 255, 255, 120)), m_selectedColor(QColor(255, 255, 255, 255))
|
||||
{
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
setAttribute(Qt::WA_TranslucentBackground);
|
||||
}
|
||||
|
||||
PageIndicator::~PageIndicator()
|
||||
{
|
||||
}
|
||||
|
||||
void PageIndicator::setPageCount(int count)
|
||||
{
|
||||
if (count != m_pageCount)
|
||||
{
|
||||
m_pageCount = count;
|
||||
if (m_currentPage >= m_pageCount)
|
||||
{
|
||||
m_currentPage = m_pageCount - 1;
|
||||
}
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void PageIndicator::setCurrentPage(int page)
|
||||
{
|
||||
if (page >= 0 && page < m_pageCount && page != m_currentPage)
|
||||
{
|
||||
m_currentPage = page;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void PageIndicator::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
Q_UNUSED(event)
|
||||
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
// 计算总宽度
|
||||
int totalWidth = 0;
|
||||
for (int i = 0; i < m_pageCount; ++i)
|
||||
{
|
||||
int dotSize = (i == m_currentPage) ? m_selectedDotSize : m_dotSize;
|
||||
totalWidth += dotSize;
|
||||
if (i < m_pageCount - 1)
|
||||
{
|
||||
totalWidth += m_dotSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算起始位置(居中)
|
||||
int startX = (width() - totalWidth) / 2;
|
||||
int centerY = height() / 2;
|
||||
|
||||
// 绘制指示点
|
||||
int x = startX;
|
||||
for (int i = 0; i < m_pageCount; ++i)
|
||||
{
|
||||
bool isSelected = (i == m_currentPage);
|
||||
int dotSize = isSelected ? m_selectedDotSize : m_dotSize;
|
||||
QColor color = isSelected ? m_selectedColor : m_dotColor;
|
||||
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(color);
|
||||
|
||||
int y = centerY - dotSize / 2;
|
||||
painter.drawEllipse(x, y, dotSize, dotSize);
|
||||
|
||||
x += dotSize + m_dotSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
void PageIndicator::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
// 计算点击位置对应的页面
|
||||
int totalWidth = 0;
|
||||
for (int i = 0; i < m_pageCount; ++i)
|
||||
{
|
||||
int dotSize = (i == m_currentPage) ? m_selectedDotSize : m_dotSize;
|
||||
totalWidth += dotSize;
|
||||
if (i < m_pageCount - 1)
|
||||
{
|
||||
totalWidth += m_dotSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
int startX = (width() - totalWidth) / 2;
|
||||
int x = startX;
|
||||
|
||||
for (int i = 0; i < m_pageCount; ++i)
|
||||
{
|
||||
int dotSize = (i == m_currentPage) ? m_selectedDotSize : m_dotSize;
|
||||
|
||||
// 扩大点击区域
|
||||
QRect hitRect(x - 8, 0, dotSize + 16, height());
|
||||
|
||||
if (hitRect.contains(event->pos()))
|
||||
{
|
||||
if (i != m_currentPage)
|
||||
{
|
||||
emit pageClicked(i);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
x += dotSize + m_dotSpacing;
|
||||
}
|
||||
|
||||
QWidget::mousePressEvent(event);
|
||||
}
|
||||
37
widgets/pageindicator.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#ifndef PAGEINDICATOR_H
|
||||
#define PAGEINDICATOR_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class PageIndicator : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PageIndicator(int pageCount = 3, QWidget *parent = nullptr);
|
||||
~PageIndicator();
|
||||
|
||||
void setPageCount(int count);
|
||||
void setCurrentPage(int page);
|
||||
int currentPage() const { return m_currentPage; }
|
||||
|
||||
signals:
|
||||
void pageClicked(int page);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
int m_pageCount;
|
||||
int m_currentPage;
|
||||
|
||||
// 指示器样式参数
|
||||
int m_dotSize;
|
||||
int m_selectedDotSize;
|
||||
int m_dotSpacing;
|
||||
QColor m_dotColor;
|
||||
QColor m_selectedColor;
|
||||
};
|
||||
|
||||
#endif // PAGEINDICATOR_H
|
||||
224
widgets/procedurelistwidget.cpp
Normal file
@@ -0,0 +1,224 @@
|
||||
#include "procedurelistwidget.h"
|
||||
#include "overlaydialogswidget.h"
|
||||
#include "../procedure/proceduremanager.h"
|
||||
#include <QFrame>
|
||||
#include <QScrollBar>
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QDebug>
|
||||
|
||||
ProcedureListWidget::ProcedureListWidget(ProcedureManager *manager, QWidget *parent)
|
||||
: QWidget(parent), m_manager(manager)
|
||||
{
|
||||
setupUI();
|
||||
refreshList();
|
||||
}
|
||||
|
||||
ProcedureListWidget::~ProcedureListWidget() {}
|
||||
|
||||
void ProcedureListWidget::setupUI()
|
||||
{
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->setSpacing(0);
|
||||
|
||||
// 顶部工具栏 - Material Design 深色
|
||||
auto *toolbar = new QWidget(this);
|
||||
toolbar->setFixedHeight(56);
|
||||
toolbar->setStyleSheet("background: #263238;");
|
||||
|
||||
auto *toolbarLayout = new QHBoxLayout(toolbar);
|
||||
toolbarLayout->setContentsMargins(8, 0, 16, 0);
|
||||
toolbarLayout->setSpacing(8);
|
||||
|
||||
m_backBtn = new QPushButton("返回", this);
|
||||
m_backBtn->setFixedHeight(40);
|
||||
m_backBtn->setStyleSheet(
|
||||
"QPushButton { background: #455A64; color: white; border: none; border-radius: 4px; padding: 0 16px; font-size: 14px; }"
|
||||
"QPushButton:hover { background: #546E7A; }"
|
||||
"QPushButton:pressed { background: #37474F; }");
|
||||
connect(m_backBtn, &QPushButton::clicked, this, &ProcedureListWidget::backRequested);
|
||||
toolbarLayout->addWidget(m_backBtn);
|
||||
|
||||
auto *titleLabel = new QLabel("规程列表", this);
|
||||
titleLabel->setStyleSheet("color: white; font-size: 18px; font-weight: bold;");
|
||||
toolbarLayout->addWidget(titleLabel);
|
||||
toolbarLayout->addStretch();
|
||||
|
||||
mainLayout->addWidget(toolbar);
|
||||
|
||||
// 内容区域
|
||||
auto *contentWidget = new QWidget(this);
|
||||
contentWidget->setStyleSheet("background: #FAFAFA;");
|
||||
auto *contentLayout = new QVBoxLayout(contentWidget);
|
||||
contentLayout->setContentsMargins(20, 20, 20, 20);
|
||||
contentLayout->setSpacing(16);
|
||||
|
||||
// 搜索卡片
|
||||
auto *searchCard = new QWidget(this);
|
||||
searchCard->setStyleSheet(
|
||||
"QWidget#searchCard { background: white; border-radius: 8px; border: 1px solid #E0E0E0; }");
|
||||
searchCard->setObjectName("searchCard");
|
||||
auto *searchCardLayout = new QVBoxLayout(searchCard);
|
||||
searchCardLayout->setContentsMargins(20, 16, 20, 16);
|
||||
searchCardLayout->setSpacing(16);
|
||||
|
||||
auto *searchRow = new QHBoxLayout();
|
||||
searchRow->setSpacing(12);
|
||||
m_searchEdit = new QLineEdit(this);
|
||||
m_searchEdit->setPlaceholderText("输入规程ID或名称搜索...");
|
||||
m_searchEdit->setFixedHeight(44);
|
||||
m_searchEdit->setStyleSheet(
|
||||
"QLineEdit { border: 2px solid #E0E0E0; border-radius: 6px; padding: 0 16px; font-size: 15px; background: #FAFAFA; }"
|
||||
"QLineEdit:focus { border-color: #1976D2; background: white; }");
|
||||
searchRow->addWidget(m_searchEdit, 1);
|
||||
|
||||
m_scanProcedureBtn = new QPushButton("扫描条码", this);
|
||||
m_scanProcedureBtn->setFixedHeight(44);
|
||||
m_scanProcedureBtn->setStyleSheet(
|
||||
"QPushButton { background: #1976D2; color: white; border: none; border-radius: 6px; padding: 0 20px; font-size: 14px; font-weight: bold; }"
|
||||
"QPushButton:hover { background: #1565C0; }"
|
||||
"QPushButton:pressed { background: #0D47A1; }");
|
||||
searchRow->addWidget(m_scanProcedureBtn);
|
||||
searchCardLayout->addLayout(searchRow);
|
||||
|
||||
auto *workOrderRow = new QHBoxLayout();
|
||||
workOrderRow->setSpacing(12);
|
||||
auto *workOrderLabel = new QLabel("工单号:", this);
|
||||
workOrderLabel->setStyleSheet("color: #424242; font-size: 15px; font-weight: bold;");
|
||||
workOrderLabel->setFixedWidth(70);
|
||||
workOrderRow->addWidget(workOrderLabel);
|
||||
|
||||
m_workOrderEdit = new QLineEdit(this);
|
||||
m_workOrderEdit->setPlaceholderText("输入或扫描工单号");
|
||||
m_workOrderEdit->setFixedHeight(44);
|
||||
m_workOrderEdit->setStyleSheet(
|
||||
"QLineEdit { border: 2px solid #E0E0E0; border-radius: 6px; padding: 0 16px; font-size: 15px; background: #FAFAFA; }"
|
||||
"QLineEdit:focus { border-color: #1976D2; background: white; }");
|
||||
workOrderRow->addWidget(m_workOrderEdit, 1);
|
||||
|
||||
m_scanWorkOrderBtn = new QPushButton("扫描", this);
|
||||
m_scanWorkOrderBtn->setFixedHeight(44);
|
||||
m_scanWorkOrderBtn->setStyleSheet(
|
||||
"QPushButton { background: #455A64; color: white; border: none; border-radius: 6px; padding: 0 20px; font-size: 14px; }"
|
||||
"QPushButton:hover { background: #546E7A; }"
|
||||
"QPushButton:pressed { background: #37474F; }");
|
||||
workOrderRow->addWidget(m_scanWorkOrderBtn);
|
||||
searchCardLayout->addLayout(workOrderRow);
|
||||
|
||||
contentLayout->addWidget(searchCard);
|
||||
|
||||
// 规程列表
|
||||
m_procedureList = new QListWidget(this);
|
||||
m_procedureList->setStyleSheet(
|
||||
"QListWidget { border: 1px solid #E0E0E0; border-radius: 8px; background: white; padding: 8px; }"
|
||||
"QListWidget::item { background: #FAFAFA; border-radius: 6px; padding: 16px; margin: 4px 0; border: 1px solid #EEEEEE; }"
|
||||
"QListWidget::item:selected { background: #E3F2FD; border: 2px solid #1976D2; }"
|
||||
"QListWidget::item:hover { background: #F5F5F5; }");
|
||||
contentLayout->addWidget(m_procedureList, 1);
|
||||
|
||||
// 底部按钮
|
||||
auto *bottomLayout = new QHBoxLayout();
|
||||
bottomLayout->addStretch();
|
||||
m_startBtn = new QPushButton("进入规程", this);
|
||||
m_startBtn->setEnabled(false);
|
||||
m_startBtn->setFixedSize(200, 52);
|
||||
m_startBtn->setStyleSheet(
|
||||
"QPushButton { background: #43A047; color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: bold; }"
|
||||
"QPushButton:hover { background: #388E3C; }"
|
||||
"QPushButton:pressed { background: #2E7D32; }"
|
||||
"QPushButton:disabled { background: #BDBDBD; color: #757575; }");
|
||||
bottomLayout->addWidget(m_startBtn);
|
||||
bottomLayout->addStretch();
|
||||
contentLayout->addLayout(bottomLayout);
|
||||
|
||||
mainLayout->addWidget(contentWidget, 1);
|
||||
|
||||
connect(m_searchEdit, &QLineEdit::textChanged, this, &ProcedureListWidget::onSearchTextChanged);
|
||||
connect(m_scanProcedureBtn, &QPushButton::clicked, this, &ProcedureListWidget::onScanProcedureBarcode);
|
||||
connect(m_scanWorkOrderBtn, &QPushButton::clicked, this, &ProcedureListWidget::onScanWorkOrderBarcode);
|
||||
connect(m_procedureList, &QListWidget::itemClicked, this, &ProcedureListWidget::onProcedureItemClicked);
|
||||
connect(m_procedureList, &QListWidget::itemDoubleClicked, this, &ProcedureListWidget::onStartProcedure);
|
||||
connect(m_startBtn, &QPushButton::clicked, this, &ProcedureListWidget::onStartProcedure);
|
||||
}
|
||||
|
||||
void ProcedureListWidget::refreshList()
|
||||
{
|
||||
// 尝试多个可能的 procedures 目录位置
|
||||
QStringList possiblePaths;
|
||||
possiblePaths << QCoreApplication::applicationDirPath() + "/procedures";
|
||||
possiblePaths << QCoreApplication::applicationDirPath() + "/../../procedures";
|
||||
possiblePaths << QCoreApplication::applicationDirPath() + "/../../../procedures";
|
||||
possiblePaths << QCoreApplication::applicationDirPath() + "/../../../../procedures";
|
||||
|
||||
QString proceduresDir;
|
||||
for (const QString &path : possiblePaths)
|
||||
{
|
||||
QString absPath = QDir(path).absolutePath();
|
||||
qDebug() << "Checking procedures path:" << absPath << "exists:" << QDir(absPath).exists();
|
||||
if (QDir(absPath).exists())
|
||||
{
|
||||
proceduresDir = absPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (proceduresDir.isEmpty())
|
||||
{
|
||||
qWarning() << "Could not find procedures directory! Tried paths:" << possiblePaths;
|
||||
proceduresDir = QCoreApplication::applicationDirPath() + "/procedures"; // 使用默认路径
|
||||
}
|
||||
|
||||
qDebug() << "Loading procedures from:" << proceduresDir;
|
||||
auto procedures = m_manager->loadProcedureList(proceduresDir);
|
||||
updateProcedureList(procedures);
|
||||
}
|
||||
|
||||
void ProcedureListWidget::updateProcedureList(const QVector<ProcedureSummary> &procedures)
|
||||
{
|
||||
m_procedureList->clear();
|
||||
for (const auto &proc : procedures)
|
||||
{
|
||||
auto *item = new QListWidgetItem();
|
||||
item->setData(Qt::UserRole, proc.id);
|
||||
item->setText(QString("%1\n%2\n版本: %3").arg(proc.id).arg(proc.name).arg(proc.version));
|
||||
item->setSizeHint(QSize(0, 80));
|
||||
m_procedureList->addItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
void ProcedureListWidget::onSearchTextChanged(const QString &text)
|
||||
{
|
||||
updateProcedureList(m_manager->searchProcedures(text));
|
||||
}
|
||||
|
||||
void ProcedureListWidget::onScanProcedureBarcode()
|
||||
{
|
||||
OverlayDialog::information(this, "扫码", "请将条码对准扫描窗口...");
|
||||
}
|
||||
|
||||
void ProcedureListWidget::onScanWorkOrderBarcode()
|
||||
{
|
||||
OverlayDialog::information(this, "扫码", "请将工单条码对准扫描窗口...");
|
||||
}
|
||||
|
||||
void ProcedureListWidget::onProcedureItemClicked(QListWidgetItem *item)
|
||||
{
|
||||
m_selectedProcedureId = item->data(Qt::UserRole).toString();
|
||||
m_startBtn->setEnabled(!m_selectedProcedureId.isEmpty());
|
||||
}
|
||||
|
||||
void ProcedureListWidget::onStartProcedure()
|
||||
{
|
||||
if (m_selectedProcedureId.isEmpty())
|
||||
{
|
||||
OverlayDialog::warning(this, "提示", "请先选择一个规程");
|
||||
return;
|
||||
}
|
||||
QString workOrder = m_workOrderEdit->text().trimmed();
|
||||
if (!workOrder.isEmpty())
|
||||
{
|
||||
m_manager->setWorkOrderId(workOrder);
|
||||
}
|
||||
emit procedureSelected(m_selectedProcedureId);
|
||||
}
|
||||
61
widgets/procedurelistwidget.h
Normal file
@@ -0,0 +1,61 @@
|
||||
#ifndef PROCEDURELISTWIDGET_H
|
||||
#define PROCEDURELISTWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QListWidget>
|
||||
#include <QLabel>
|
||||
#include "../procedure/proceduredata.h"
|
||||
|
||||
class ProcedureManager;
|
||||
|
||||
/**
|
||||
* @class ProcedureListWidget
|
||||
* @brief 规程列表页面
|
||||
*
|
||||
* 显示可用规程列表,支持搜索(包括条码扫描)
|
||||
* 支持输入工单号
|
||||
*/
|
||||
class ProcedureListWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ProcedureListWidget(ProcedureManager *manager, QWidget *parent = nullptr);
|
||||
~ProcedureListWidget();
|
||||
|
||||
void refreshList();
|
||||
|
||||
signals:
|
||||
void procedureSelected(const QString &procedureId);
|
||||
void backRequested();
|
||||
|
||||
private slots:
|
||||
void onSearchTextChanged(const QString &text);
|
||||
void onScanProcedureBarcode();
|
||||
void onScanWorkOrderBarcode();
|
||||
void onProcedureItemClicked(QListWidgetItem *item);
|
||||
void onStartProcedure();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void updateProcedureList(const QVector<ProcedureSummary> &procedures);
|
||||
|
||||
ProcedureManager *m_manager;
|
||||
|
||||
// UI 组件
|
||||
QPushButton *m_backBtn;
|
||||
QLineEdit *m_searchEdit;
|
||||
QPushButton *m_scanProcedureBtn;
|
||||
QLineEdit *m_workOrderEdit;
|
||||
QPushButton *m_scanWorkOrderBtn;
|
||||
QListWidget *m_procedureList;
|
||||
QPushButton *m_startBtn;
|
||||
|
||||
QString m_selectedProcedureId;
|
||||
};
|
||||
|
||||
#endif // PROCEDURELISTWIDGET_H
|
||||
697
widgets/procedureplayerwidget.cpp
Normal file
@@ -0,0 +1,697 @@
|
||||
#include "procedureplayerwidget.h"
|
||||
#include "overlaydialogswidget.h"
|
||||
#include "../procedure/proceduremanager.h"
|
||||
#include <QHeaderView>
|
||||
#include <QScrollBar>
|
||||
|
||||
// ============== StepItemWidget ==============
|
||||
|
||||
StepItemWidget::StepItemWidget(const StepData &step, QWidget *parent)
|
||||
: QWidget(parent), m_step(step), m_typeBtn(nullptr), m_confirmCheckbox(nullptr), m_tableBtn(nullptr)
|
||||
{
|
||||
setupUI();
|
||||
updateStatusDisplay();
|
||||
}
|
||||
|
||||
void StepItemWidget::setupUI()
|
||||
{
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->setSpacing(0);
|
||||
|
||||
// 主容器
|
||||
m_container = new QWidget(this);
|
||||
m_container->setObjectName("stepContainer");
|
||||
|
||||
auto *containerLayout = new QHBoxLayout(m_container);
|
||||
containerLayout->setContentsMargins(12, 10, 12, 10);
|
||||
containerLayout->setSpacing(12);
|
||||
|
||||
// 左侧区域:类型按钮 + 状态标签
|
||||
auto *leftArea = new QVBoxLayout();
|
||||
leftArea->setAlignment(Qt::AlignTop);
|
||||
leftArea->setSpacing(6);
|
||||
|
||||
// 类型切换按钮 - Material Design 风格,可点击切换 - 触摸屏优化
|
||||
m_typeBtn = new QPushButton(this);
|
||||
updateTypeButton();
|
||||
m_typeBtn->setFixedSize(78, 36);
|
||||
m_typeBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(m_typeBtn, &QPushButton::clicked, this, &StepItemWidget::onTypeBtnClicked);
|
||||
leftArea->addWidget(m_typeBtn);
|
||||
|
||||
// 状态标签 - 放在类型按钮下方,大小一致
|
||||
m_statusLabel = new QLabel(this);
|
||||
m_statusLabel->setFixedSize(78, 36);
|
||||
m_statusLabel->setAlignment(Qt::AlignCenter);
|
||||
leftArea->addWidget(m_statusLabel);
|
||||
|
||||
containerLayout->addLayout(leftArea);
|
||||
|
||||
// 内容区域
|
||||
auto *contentArea = new QVBoxLayout();
|
||||
contentArea->setSpacing(6);
|
||||
|
||||
m_contentLabel = new QLabel(m_step.content, this);
|
||||
m_contentLabel->setWordWrap(true);
|
||||
m_contentLabel->setTextFormat(Qt::PlainText);
|
||||
m_contentLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
||||
m_contentLabel->setMinimumWidth(0);
|
||||
m_contentLabel->setStyleSheet("font-size: 14px; color: #37474F; line-height: 1.6;");
|
||||
contentArea->addWidget(m_contentLabel);
|
||||
|
||||
containerLayout->addLayout(contentArea, 1);
|
||||
|
||||
// 右侧区域:仅确认复选框(如果是手动步骤)
|
||||
if (m_step.type == StepType::Manual)
|
||||
{
|
||||
auto *rightArea = new QVBoxLayout();
|
||||
rightArea->setAlignment(Qt::AlignTop | Qt::AlignRight);
|
||||
rightArea->setSpacing(10);
|
||||
|
||||
m_confirmCheckbox = new QCheckBox("确认", this);
|
||||
m_confirmCheckbox->setStyleSheet(
|
||||
"QCheckBox { font-size: 14px; color: #455A64; spacing: 8px; }"
|
||||
"QCheckBox::indicator { width: 24px; height: 24px; border: 2px solid #90A4AE; border-radius: 4px; background: white; }"
|
||||
"QCheckBox::indicator:checked { background: #43A047; border-color: #43A047; image: url(:/icons/check-white.png); }"
|
||||
"QCheckBox::indicator:hover { border-color: #1976D2; }");
|
||||
m_confirmCheckbox->setChecked(m_step.status == StepStatus::Confirmed);
|
||||
connect(m_confirmCheckbox, &QCheckBox::toggled, this, &StepItemWidget::onCheckboxToggled);
|
||||
rightArea->addWidget(m_confirmCheckbox);
|
||||
|
||||
containerLayout->addLayout(rightArea);
|
||||
}
|
||||
|
||||
mainLayout->addWidget(m_container);
|
||||
|
||||
// 底部表格按钮 - banner 样式
|
||||
if (!m_step.tableRefs.isEmpty())
|
||||
{
|
||||
m_tableBtn = new QPushButton("📋 查看关联表格", this);
|
||||
m_tableBtn->setFixedHeight(40);
|
||||
m_tableBtn->setCursor(Qt::PointingHandCursor);
|
||||
m_tableBtn->setStyleSheet(
|
||||
"QPushButton { "
|
||||
" background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #E3F2FD, stop:1 #BBDEFB); "
|
||||
" color: #1565C0; "
|
||||
" border: none; "
|
||||
" border-top: 1px solid #E0E0E0; "
|
||||
" border-bottom-left-radius: 8px; "
|
||||
" border-bottom-right-radius: 8px; "
|
||||
" font-size: 13px; "
|
||||
" font-weight: 500; "
|
||||
" padding: 0px 16px; "
|
||||
"}"
|
||||
"QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #BBDEFB, stop:1 #90CAF9); }"
|
||||
"QPushButton:pressed { background: #90CAF9; }");
|
||||
connect(m_tableBtn, &QPushButton::clicked, this, [this]()
|
||||
{
|
||||
if (!m_step.tableRefs.isEmpty()) emit tableExpandRequested(m_step.tableRefs.first()); });
|
||||
mainLayout->addWidget(m_tableBtn);
|
||||
|
||||
// 有表格按钮时,主容器底部不要圆角
|
||||
m_container->setStyleSheet(
|
||||
"QWidget#stepContainer { background: white; border: 1px solid #E0E0E0; border-bottom: none; "
|
||||
"border-top-left-radius: 8px; border-top-right-radius: 8px; "
|
||||
"border-bottom-left-radius: 0px; border-bottom-right-radius: 0px; }");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_container->setStyleSheet(
|
||||
"QWidget#stepContainer { background: white; border: 1px solid #E0E0E0; border-radius: 8px; }");
|
||||
}
|
||||
}
|
||||
|
||||
void StepItemWidget::setHighlighted(bool highlighted)
|
||||
{
|
||||
m_container->setStyleSheet(highlighted
|
||||
? "QWidget#stepContainer { background: #FFF8E1; border: 2px solid #FFA000; border-radius: 8px; }"
|
||||
: "QWidget#stepContainer { background: white; border: 1px solid #E0E0E0; border-radius: 8px; }");
|
||||
}
|
||||
|
||||
void StepItemWidget::updateStatus(StepStatus status)
|
||||
{
|
||||
m_step.status = status;
|
||||
updateStatusDisplay();
|
||||
if (m_confirmCheckbox)
|
||||
{
|
||||
m_confirmCheckbox->blockSignals(true);
|
||||
m_confirmCheckbox->setChecked(status == StepStatus::Confirmed);
|
||||
m_confirmCheckbox->blockSignals(false);
|
||||
}
|
||||
}
|
||||
|
||||
void StepItemWidget::updateStatusDisplay()
|
||||
{
|
||||
QString statusText, bgColor, textColor = "white";
|
||||
switch (m_step.status)
|
||||
{
|
||||
case StepStatus::Pending:
|
||||
statusText = "待执行";
|
||||
bgColor = "#78909C";
|
||||
break;
|
||||
case StepStatus::InProgress:
|
||||
statusText = "执行中";
|
||||
bgColor = "#1976D2";
|
||||
break;
|
||||
case StepStatus::Passed:
|
||||
case StepStatus::Confirmed:
|
||||
statusText = "✓ 完成";
|
||||
bgColor = "#388E3C";
|
||||
break;
|
||||
case StepStatus::Failed:
|
||||
statusText = "✗ 失败";
|
||||
bgColor = "#D32F2F";
|
||||
break;
|
||||
case StepStatus::Skipped:
|
||||
statusText = "跳过";
|
||||
bgColor = "#FFA000";
|
||||
textColor = "#333";
|
||||
break;
|
||||
}
|
||||
m_statusLabel->setText(statusText);
|
||||
m_statusLabel->setStyleSheet(QString(
|
||||
"QLabel { background: %1; color: %2; border-radius: 6px; padding: 6px 12px; font-size: 13px; font-weight: bold; }")
|
||||
.arg(bgColor)
|
||||
.arg(textColor));
|
||||
}
|
||||
|
||||
void StepItemWidget::onCheckboxToggled(bool checked)
|
||||
{
|
||||
if (checked)
|
||||
{
|
||||
OverlayDialog::question(this, "确认步骤",
|
||||
QString("确定已完成此步骤?\n\n%1").arg(m_step.content.left(100)),
|
||||
[this](bool accepted)
|
||||
{
|
||||
if (accepted)
|
||||
{
|
||||
emit confirmRequested(m_step.id);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_confirmCheckbox->blockSignals(true);
|
||||
m_confirmCheckbox->setChecked(false);
|
||||
m_confirmCheckbox->blockSignals(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
OverlayDialog::input(this, "取消确认", "请输入取消原因:",
|
||||
[this](bool ok, const QString &reason)
|
||||
{
|
||||
if (ok && !reason.isEmpty())
|
||||
{
|
||||
emit confirmCancelled(m_step.id, reason);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_confirmCheckbox->blockSignals(true);
|
||||
m_confirmCheckbox->setChecked(true);
|
||||
m_confirmCheckbox->blockSignals(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void StepItemWidget::onTypeBtnClicked()
|
||||
{
|
||||
// 切换类型
|
||||
m_step.type = (m_step.type == StepType::Automatic) ? StepType::Manual : StepType::Automatic;
|
||||
updateTypeButton();
|
||||
|
||||
// 如果切换为手动模式,显示确认复选框
|
||||
if (m_step.type == StepType::Manual && !m_confirmCheckbox)
|
||||
{
|
||||
m_confirmCheckbox = new QCheckBox("确认", this);
|
||||
m_confirmCheckbox->setStyleSheet(
|
||||
"QCheckBox { font-size: 15px; color: #455A64; spacing: 10px; }"
|
||||
"QCheckBox::indicator { width: 28px; height: 28px; border: 2px solid #90A4AE; border-radius: 5px; background: white; }"
|
||||
"QCheckBox::indicator:checked { background: #43A047; border-color: #43A047; }"
|
||||
"QCheckBox::indicator:hover { border-color: #1976D2; }");
|
||||
connect(m_confirmCheckbox, &QCheckBox::toggled, this, &StepItemWidget::onCheckboxToggled);
|
||||
// 找到右侧布局并添加
|
||||
if (auto *rightLayout = qobject_cast<QVBoxLayout *>(m_container->layout()->itemAt(m_container->layout()->count() - 1)->layout()))
|
||||
{
|
||||
rightLayout->addWidget(m_confirmCheckbox);
|
||||
}
|
||||
}
|
||||
else if (m_step.type == StepType::Automatic && m_confirmCheckbox)
|
||||
{
|
||||
m_confirmCheckbox->hide();
|
||||
m_confirmCheckbox->deleteLater();
|
||||
m_confirmCheckbox = nullptr;
|
||||
}
|
||||
|
||||
emit stepTypeChanged(m_step.id, m_step.type);
|
||||
}
|
||||
|
||||
void StepItemWidget::updateTypeButton()
|
||||
{
|
||||
QString typeText = (m_step.type == StepType::Automatic) ? "自动" : "手动";
|
||||
QString typeBg = (m_step.type == StepType::Automatic) ? "#0288D1" : "#F57C00";
|
||||
QString hoverBg = (m_step.type == StepType::Automatic) ? "#0277BD" : "#EF6C00";
|
||||
m_typeBtn->setText(typeText);
|
||||
m_typeBtn->setStyleSheet(QString(
|
||||
"QPushButton { background: %1; color: white; border: none; border-radius: 6px; "
|
||||
"font-size: 13px; font-weight: bold; padding: 0px; margin: 0px; }"
|
||||
"QPushButton:hover { background: %2; }"
|
||||
"QPushButton:pressed { background: %2; }")
|
||||
.arg(typeBg)
|
||||
.arg(hoverBg));
|
||||
}
|
||||
|
||||
// ============== TableViewWidget ==============
|
||||
|
||||
TableViewWidget::TableViewWidget(const TableData &table, QWidget *parent)
|
||||
: QWidget(parent), m_table(table), m_expanded(true)
|
||||
{
|
||||
setupUI();
|
||||
}
|
||||
|
||||
void TableViewWidget::setupUI()
|
||||
{
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->setSpacing(0);
|
||||
|
||||
// 直接显示表格,不需要头部和展开/收起按钮
|
||||
m_tableContainer = new QWidget(this);
|
||||
auto *tableLayout = new QVBoxLayout(m_tableContainer);
|
||||
tableLayout->setContentsMargins(8, 8, 8, 8);
|
||||
|
||||
m_tableWidget = new QTableWidget(this);
|
||||
m_tableWidget->setStyleSheet(
|
||||
"QTableWidget { border: 1px solid #dcdcdc; gridline-color: #E0E0E0; background: white; }"
|
||||
"QTableWidget::item { padding: 8px; }"
|
||||
"QHeaderView::section { background: #ECEFF1; padding: 8px; border: none; border-bottom: 1px solid #CFD8DC; font-weight: bold; }");
|
||||
m_tableWidget->horizontalHeader()->setStretchLastSection(true);
|
||||
m_tableWidget->verticalHeader()->setVisible(false);
|
||||
m_tableWidget->setAlternatingRowColors(true);
|
||||
|
||||
tableLayout->addWidget(m_tableWidget);
|
||||
m_tableContainer->setVisible(true);
|
||||
mainLayout->addWidget(m_tableContainer);
|
||||
|
||||
updateTable();
|
||||
}
|
||||
|
||||
void TableViewWidget::setHighlightedFields(const QStringList &fields)
|
||||
{
|
||||
m_highlightedFields = fields;
|
||||
updateTable();
|
||||
}
|
||||
|
||||
void TableViewWidget::toggleExpanded()
|
||||
{
|
||||
m_expanded = !m_expanded;
|
||||
m_tableContainer->setVisible(m_expanded);
|
||||
m_toggleBtn->setText(m_expanded ? "收起 ▲" : "展开 ▼");
|
||||
}
|
||||
|
||||
void TableViewWidget::updateTable()
|
||||
{
|
||||
if (m_table.columns.isEmpty())
|
||||
{
|
||||
m_tableWidget->setColumnCount(3);
|
||||
m_tableWidget->setHorizontalHeaderLabels({"字段", "数值", "单位"});
|
||||
m_tableWidget->setRowCount(1);
|
||||
m_tableWidget->setItem(0, 0, new QTableWidgetItem("暂无数据"));
|
||||
return;
|
||||
}
|
||||
|
||||
m_tableWidget->setColumnCount(m_table.columns.size());
|
||||
QStringList headers;
|
||||
for (const auto &col : m_table.columns)
|
||||
headers << col.name;
|
||||
m_tableWidget->setHorizontalHeaderLabels(headers);
|
||||
|
||||
int rowCount = m_table.rows.isEmpty() ? 1 : m_table.rows.size();
|
||||
m_tableWidget->setRowCount(rowCount);
|
||||
|
||||
for (int col = 0; col < m_table.columns.size(); ++col)
|
||||
{
|
||||
auto *item = new QTableWidgetItem("-");
|
||||
item->setTextAlignment(Qt::AlignCenter);
|
||||
if (m_highlightedFields.contains(m_table.columns[col].id))
|
||||
{
|
||||
item->setBackground(QColor("#FFF8E1"));
|
||||
}
|
||||
m_tableWidget->setItem(0, col, item);
|
||||
}
|
||||
}
|
||||
|
||||
// ============== ProcedurePlayerWidget ==============
|
||||
|
||||
ProcedurePlayerWidget::ProcedurePlayerWidget(ProcedureManager *manager, QWidget *parent)
|
||||
: QWidget(parent), m_manager(manager)
|
||||
{
|
||||
setupUI();
|
||||
connect(m_manager, &ProcedureManager::stepStatusChanged, this, &ProcedurePlayerWidget::onStepStatusChanged);
|
||||
}
|
||||
|
||||
ProcedurePlayerWidget::~ProcedurePlayerWidget() {}
|
||||
|
||||
void ProcedurePlayerWidget::setupUI()
|
||||
{
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->setSpacing(0);
|
||||
|
||||
// 顶部工具栏 - Material Design 深色
|
||||
auto *headerWidget = new QWidget(this);
|
||||
headerWidget->setStyleSheet("background: #263238;");
|
||||
headerWidget->setFixedHeight(56);
|
||||
|
||||
auto *headerLayout = new QHBoxLayout(headerWidget);
|
||||
headerLayout->setContentsMargins(8, 0, 16, 0);
|
||||
headerLayout->setSpacing(12);
|
||||
|
||||
m_backBtn = new QPushButton("返回列表", this);
|
||||
m_backBtn->setFixedHeight(40);
|
||||
m_backBtn->setStyleSheet(
|
||||
"QPushButton { background: #455A64; color: white; border: none; border-radius: 4px; padding: 0 16px; font-size: 14px; }"
|
||||
"QPushButton:hover { background: #546E7A; }"
|
||||
"QPushButton:pressed { background: #37474F; }");
|
||||
connect(m_backBtn, &QPushButton::clicked, this, &ProcedurePlayerWidget::onBackClicked);
|
||||
headerLayout->addWidget(m_backBtn);
|
||||
|
||||
m_procedureIdLabel = new QLabel(this);
|
||||
m_procedureIdLabel->setStyleSheet("background: #1976D2; color: white; border-radius: 4px; padding: 8px 16px; font-size: 14px; font-weight: bold;");
|
||||
headerLayout->addWidget(m_procedureIdLabel);
|
||||
|
||||
m_procedureNameLabel = new QLabel(this);
|
||||
m_procedureNameLabel->setStyleSheet("color: white; font-size: 16px; font-weight: bold; margin-left: 8px;");
|
||||
headerLayout->addWidget(m_procedureNameLabel, 1);
|
||||
|
||||
mainLayout->addWidget(headerWidget);
|
||||
|
||||
// 内容区域
|
||||
auto *contentSplitter = new QSplitter(Qt::Horizontal, this);
|
||||
|
||||
// 左侧导航
|
||||
auto *navWidget = new QWidget(this);
|
||||
navWidget->setFixedWidth(280);
|
||||
navWidget->setStyleSheet("background: white; border-right: 1px solid #E0E0E0;");
|
||||
|
||||
auto *navLayout = new QVBoxLayout(navWidget);
|
||||
navLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
auto *navTitle = new QLabel("步骤导航", navWidget);
|
||||
navTitle->setStyleSheet(
|
||||
"font-size: 15px; font-weight: bold; color: #263238; padding: 16px; "
|
||||
"background: #FAFAFA; border-bottom: 1px solid #E0E0E0;");
|
||||
navLayout->addWidget(navTitle);
|
||||
|
||||
m_navigationTree = new QTreeWidget(navWidget);
|
||||
m_navigationTree->setHeaderHidden(true);
|
||||
m_navigationTree->setStyleSheet(
|
||||
"QTreeWidget { border: none; background: white; font-size: 13px; }"
|
||||
"QTreeWidget::item { padding: 10px 16px; border-bottom: 1px solid #F5F5F5; }"
|
||||
"QTreeWidget::item:selected { background: #E3F2FD; color: #1976D2; border-left: 3px solid #1976D2; }"
|
||||
"QTreeWidget::item:hover { background: #F5F5F5; }");
|
||||
connect(m_navigationTree, &QTreeWidget::itemClicked, this, &ProcedurePlayerWidget::onNavigationItemClicked);
|
||||
navLayout->addWidget(m_navigationTree, 1);
|
||||
|
||||
contentSplitter->addWidget(navWidget);
|
||||
|
||||
// 右侧内容区
|
||||
auto *rightContainer = new QWidget(this);
|
||||
auto *rightLayout = new QVBoxLayout(rightContainer);
|
||||
rightLayout->setContentsMargins(0, 0, 0, 0);
|
||||
rightLayout->setSpacing(0);
|
||||
|
||||
// 步骤标题区
|
||||
m_stepTitleLabel = new QLabel("请选择一个步骤", rightContainer);
|
||||
m_stepTitleLabel->setWordWrap(true);
|
||||
m_stepTitleLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
||||
m_stepTitleLabel->setMinimumWidth(0);
|
||||
m_stepTitleLabel->setMinimumHeight(50);
|
||||
m_stepTitleLabel->setStyleSheet(
|
||||
"font-size: 15px; font-weight: bold; color: #263238; padding: 16px 24px; "
|
||||
"background: white; border-bottom: 1px solid #E0E0E0;");
|
||||
rightLayout->addWidget(m_stepTitleLabel);
|
||||
|
||||
m_contentArea = new QScrollArea(rightContainer);
|
||||
m_contentArea->setWidgetResizable(true);
|
||||
m_contentArea->setStyleSheet("QScrollArea { border: none; background: #FAFAFA; }");
|
||||
|
||||
m_contentWidget = new QWidget();
|
||||
m_contentWidget->setStyleSheet("background: #FAFAFA;");
|
||||
m_contentLayout = new QVBoxLayout(m_contentWidget);
|
||||
m_contentLayout->setContentsMargins(24, 24, 24, 24);
|
||||
m_contentLayout->setSpacing(16);
|
||||
m_contentLayout->setAlignment(Qt::AlignTop);
|
||||
|
||||
m_contentArea->setWidget(m_contentWidget);
|
||||
rightLayout->addWidget(m_contentArea, 1);
|
||||
|
||||
contentSplitter->addWidget(rightContainer);
|
||||
|
||||
contentSplitter->setStretchFactor(0, 0);
|
||||
contentSplitter->setStretchFactor(1, 1);
|
||||
|
||||
mainLayout->addWidget(contentSplitter, 1);
|
||||
|
||||
// 创建表格浮层(初始隐藏)
|
||||
m_tableOverlay = new TableOverlayWidget(this);
|
||||
m_tableOverlay->setGeometry(0, 0, width(), height());
|
||||
m_tableOverlay->hide();
|
||||
}
|
||||
|
||||
void ProcedurePlayerWidget::loadProcedure(const QString &procedureId)
|
||||
{
|
||||
m_currentProcedure = m_manager->loadProcedure(procedureId);
|
||||
m_procedureIdLabel->setText(m_currentProcedure.id);
|
||||
m_procedureNameLabel->setText(m_currentProcedure.name);
|
||||
populateNavigation();
|
||||
populateStepContent();
|
||||
}
|
||||
|
||||
void ProcedurePlayerWidget::populateNavigation()
|
||||
{
|
||||
m_navigationTree->clear();
|
||||
m_navItems.clear();
|
||||
|
||||
for (const auto &group : m_currentProcedure.taskGroups)
|
||||
{
|
||||
auto *groupItem = new QTreeWidgetItem(m_navigationTree);
|
||||
groupItem->setText(0, group.name);
|
||||
groupItem->setData(0, Qt::UserRole, group.id);
|
||||
groupItem->setExpanded(true);
|
||||
QFont font = groupItem->font(0);
|
||||
font.setBold(true);
|
||||
groupItem->setFont(0, font);
|
||||
|
||||
for (const auto &step : group.steps)
|
||||
{
|
||||
auto *stepItem = new QTreeWidgetItem(groupItem);
|
||||
QString typeIcon = (step.type == StepType::Automatic) ? "⚡" : "✋";
|
||||
QString statusIcon;
|
||||
switch (step.status)
|
||||
{
|
||||
case StepStatus::Passed:
|
||||
case StepStatus::Confirmed:
|
||||
statusIcon = "✅";
|
||||
break;
|
||||
case StepStatus::Failed:
|
||||
statusIcon = "❌";
|
||||
break;
|
||||
case StepStatus::InProgress:
|
||||
statusIcon = "🔄";
|
||||
break;
|
||||
default:
|
||||
statusIcon = "○";
|
||||
break;
|
||||
}
|
||||
stepItem->setText(0, QString("%1 %2 %3").arg(statusIcon).arg(typeIcon).arg(step.content.left(20) + "..."));
|
||||
stepItem->setData(0, Qt::UserRole, step.id);
|
||||
|
||||
if (step.status == StepStatus::Passed || step.status == StepStatus::Confirmed)
|
||||
{
|
||||
stepItem->setForeground(0, QColor("#388E3C"));
|
||||
}
|
||||
else if (step.status == StepStatus::Failed)
|
||||
{
|
||||
stepItem->setForeground(0, QColor("#D32F2F"));
|
||||
}
|
||||
m_navItems[step.id] = stepItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProcedurePlayerWidget::populateStepContent()
|
||||
{
|
||||
m_stepWidgets.clear();
|
||||
m_tableWidgets.clear();
|
||||
m_allSteps.clear();
|
||||
|
||||
// 清空浮层中的表格
|
||||
m_tableOverlay->clearTables();
|
||||
|
||||
// 清空当前内容
|
||||
QLayoutItem *item;
|
||||
while ((item = m_contentLayout->takeAt(0)) != nullptr)
|
||||
{
|
||||
if (item->widget())
|
||||
item->widget()->deleteLater();
|
||||
delete item;
|
||||
}
|
||||
|
||||
// 收集所有表格定义,添加到浮层
|
||||
for (const auto &table : m_currentProcedure.tables)
|
||||
{
|
||||
auto *tableWidget = new TableViewWidget(table, nullptr);
|
||||
m_tableOverlay->addTable(table.id, table.name, tableWidget);
|
||||
}
|
||||
|
||||
// 预存所有步骤数据
|
||||
for (const auto &group : m_currentProcedure.taskGroups)
|
||||
{
|
||||
for (const auto &step : group.steps)
|
||||
{
|
||||
m_allSteps[step.id] = step;
|
||||
}
|
||||
}
|
||||
|
||||
// 默认显示第一个组的所有步骤
|
||||
if (!m_currentProcedure.taskGroups.isEmpty())
|
||||
{
|
||||
showGroupSteps(m_currentProcedure.taskGroups.first().id);
|
||||
}
|
||||
}
|
||||
|
||||
void ProcedurePlayerWidget::showStep(const QString &stepId)
|
||||
{
|
||||
if (!m_allSteps.contains(stepId))
|
||||
return;
|
||||
|
||||
// 清空当前内容
|
||||
QLayoutItem *item;
|
||||
while ((item = m_contentLayout->takeAt(0)) != nullptr)
|
||||
{
|
||||
if (item->widget())
|
||||
item->widget()->deleteLater();
|
||||
delete item;
|
||||
}
|
||||
m_stepWidgets.clear();
|
||||
|
||||
const StepData &step = m_allSteps[stepId];
|
||||
m_currentStepId = stepId;
|
||||
|
||||
// 更新标题
|
||||
m_stepTitleLabel->setText(step.content.left(50) + (step.content.length() > 50 ? "..." : ""));
|
||||
|
||||
// 创建步骤组件
|
||||
auto *stepWidget = new StepItemWidget(step, m_contentWidget);
|
||||
connect(stepWidget, &StepItemWidget::confirmRequested, this, &ProcedurePlayerWidget::onStepConfirmRequested);
|
||||
connect(stepWidget, &StepItemWidget::confirmCancelled, this, &ProcedurePlayerWidget::onStepConfirmCancelled);
|
||||
connect(stepWidget, &StepItemWidget::tableExpandRequested, this, &ProcedurePlayerWidget::onTableExpandRequested);
|
||||
m_stepWidgets[stepId] = stepWidget;
|
||||
m_contentLayout->addWidget(stepWidget);
|
||||
|
||||
// 高亮导航项
|
||||
if (m_navItems.contains(stepId))
|
||||
{
|
||||
m_navigationTree->setCurrentItem(m_navItems[stepId]);
|
||||
}
|
||||
}
|
||||
|
||||
void ProcedurePlayerWidget::showGroupSteps(const QString &groupId)
|
||||
{
|
||||
// 清空当前内容
|
||||
QLayoutItem *item;
|
||||
while ((item = m_contentLayout->takeAt(0)) != nullptr)
|
||||
{
|
||||
if (item->widget())
|
||||
item->widget()->deleteLater();
|
||||
delete item;
|
||||
}
|
||||
m_stepWidgets.clear();
|
||||
m_currentGroupId = groupId;
|
||||
|
||||
// 查找对应的组
|
||||
for (const auto &group : m_currentProcedure.taskGroups)
|
||||
{
|
||||
if (group.id == groupId)
|
||||
{
|
||||
// 更新标题
|
||||
m_stepTitleLabel->setText(group.name);
|
||||
|
||||
// 显示该组下的所有步骤
|
||||
for (const auto &step : group.steps)
|
||||
{
|
||||
auto *stepWidget = new StepItemWidget(step, m_contentWidget);
|
||||
connect(stepWidget, &StepItemWidget::confirmRequested, this, &ProcedurePlayerWidget::onStepConfirmRequested);
|
||||
connect(stepWidget, &StepItemWidget::confirmCancelled, this, &ProcedurePlayerWidget::onStepConfirmCancelled);
|
||||
connect(stepWidget, &StepItemWidget::tableExpandRequested, this, &ProcedurePlayerWidget::onTableExpandRequested);
|
||||
m_stepWidgets[step.id] = stepWidget;
|
||||
m_contentLayout->addWidget(stepWidget);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProcedurePlayerWidget::onBackClicked() { emit backToListRequested(); }
|
||||
|
||||
void ProcedurePlayerWidget::onNavigationItemClicked(QTreeWidgetItem *item, int)
|
||||
{
|
||||
QString id = item->data(0, Qt::UserRole).toString();
|
||||
|
||||
// 检查是否是组(没有父节点的是组)
|
||||
if (item->parent() == nullptr)
|
||||
{
|
||||
// 点击的是组,显示该组的所有步骤
|
||||
showGroupSteps(id);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 点击的是步骤,显示单个步骤
|
||||
if (m_allSteps.contains(id))
|
||||
{
|
||||
showStep(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProcedurePlayerWidget::onStepConfirmRequested(const QString &stepId)
|
||||
{
|
||||
m_manager->confirmStep(m_currentProcedure.id, stepId);
|
||||
}
|
||||
|
||||
void ProcedurePlayerWidget::onStepConfirmCancelled(const QString &stepId, const QString &reason)
|
||||
{
|
||||
m_manager->cancelStepConfirm(m_currentProcedure.id, stepId, reason);
|
||||
}
|
||||
|
||||
void ProcedurePlayerWidget::onTableExpandRequested(const QString &tableId)
|
||||
{
|
||||
// 显示浮层并切换到指定表格
|
||||
m_tableOverlay->showTable(tableId);
|
||||
m_tableOverlay->showOverlay();
|
||||
|
||||
// 调整浮层位置和大小 - 覆盖整个页面
|
||||
int overlayWidth = width();
|
||||
int overlayHeight = height();
|
||||
int overlayX = 0;
|
||||
int overlayY = 0;
|
||||
|
||||
m_tableOverlay->setGeometry(overlayX, overlayY, overlayWidth, overlayHeight);
|
||||
m_tableOverlay->raise();
|
||||
}
|
||||
|
||||
void ProcedurePlayerWidget::onStepStatusChanged(const QString &stepId, StepStatus status)
|
||||
{
|
||||
if (m_stepWidgets.contains(stepId))
|
||||
{
|
||||
m_stepWidgets[stepId]->updateStatus(status);
|
||||
}
|
||||
updateNavigationStatus();
|
||||
}
|
||||
|
||||
void ProcedurePlayerWidget::updateNavigationStatus()
|
||||
{
|
||||
populateNavigation();
|
||||
}
|
||||
158
widgets/procedureplayerwidget.h
Normal file
@@ -0,0 +1,158 @@
|
||||
#ifndef PROCEDUREPLAYERWIDGET_H
|
||||
#define PROCEDUREPLAYERWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QSplitter>
|
||||
#include <QScrollArea>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QTreeWidget>
|
||||
#include <QStackedWidget>
|
||||
#include <QTableWidget>
|
||||
#include <QCheckBox>
|
||||
#include "../procedure/proceduredata.h"
|
||||
#include "tableoverlaywidget.h"
|
||||
|
||||
class ProcedureManager;
|
||||
|
||||
/**
|
||||
* @class StepItemWidget
|
||||
* @brief 步骤项组件
|
||||
*
|
||||
* 显示单个步骤的内容,包括:
|
||||
* - 步骤类型标记(自动/手动)
|
||||
* - 步骤内容
|
||||
* - 执行状态
|
||||
* - 确认复选框(手动步骤)
|
||||
*/
|
||||
class StepItemWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit StepItemWidget(const StepData &step, QWidget *parent = nullptr);
|
||||
|
||||
QString stepId() const { return m_step.id; }
|
||||
void setHighlighted(bool highlighted);
|
||||
void updateStatus(StepStatus status);
|
||||
|
||||
signals:
|
||||
void confirmRequested(const QString &stepId);
|
||||
void confirmCancelled(const QString &stepId, const QString &reason);
|
||||
void tableExpandRequested(const QString &tableId);
|
||||
void stepTypeChanged(const QString &stepId, StepType newType);
|
||||
|
||||
private slots:
|
||||
void onCheckboxToggled(bool checked);
|
||||
void onTypeBtnClicked();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void updateStatusDisplay();
|
||||
void updateTypeButton();
|
||||
|
||||
StepData m_step;
|
||||
QPushButton *m_typeBtn;
|
||||
QLabel *m_contentLabel;
|
||||
QLabel *m_statusLabel;
|
||||
QCheckBox *m_confirmCheckbox;
|
||||
QPushButton *m_tableBtn;
|
||||
QWidget *m_container;
|
||||
};
|
||||
|
||||
/**
|
||||
* @class TableViewWidget
|
||||
* @brief 表格展示组件
|
||||
*
|
||||
* 内联展示相关表格,可展开/收起
|
||||
*/
|
||||
class TableViewWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TableViewWidget(const TableData &table, QWidget *parent = nullptr);
|
||||
|
||||
void setHighlightedFields(const QStringList &fields);
|
||||
void toggleExpanded();
|
||||
bool isExpanded() const { return m_expanded; }
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void updateTable();
|
||||
|
||||
TableData m_table;
|
||||
QTableWidget *m_tableWidget;
|
||||
QLabel *m_headerLabel;
|
||||
QPushButton *m_toggleBtn;
|
||||
QWidget *m_tableContainer;
|
||||
QStringList m_highlightedFields;
|
||||
bool m_expanded;
|
||||
};
|
||||
|
||||
/**
|
||||
* @class ProcedurePlayerWidget
|
||||
* @brief 规程执行页面
|
||||
*
|
||||
* 主执行界面,包括:
|
||||
* - 顶部信息栏(规程ID、名称、返回按钮)
|
||||
* - 左侧步骤导航
|
||||
* - 中间步骤内容区
|
||||
* - 内联表格展示
|
||||
*/
|
||||
class ProcedurePlayerWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ProcedurePlayerWidget(ProcedureManager *manager, QWidget *parent = nullptr);
|
||||
~ProcedurePlayerWidget();
|
||||
|
||||
void loadProcedure(const QString &procedureId);
|
||||
|
||||
signals:
|
||||
void backToListRequested();
|
||||
|
||||
private slots:
|
||||
void onBackClicked();
|
||||
void onNavigationItemClicked(QTreeWidgetItem *item, int column);
|
||||
void onStepConfirmRequested(const QString &stepId);
|
||||
void onStepConfirmCancelled(const QString &stepId, const QString &reason);
|
||||
void onTableExpandRequested(const QString &tableId);
|
||||
void onStepStatusChanged(const QString &stepId, StepStatus status);
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void populateNavigation();
|
||||
void populateStepContent();
|
||||
void showStep(const QString &stepId);
|
||||
void showGroupSteps(const QString &groupId);
|
||||
void updateNavigationStatus();
|
||||
|
||||
ProcedureManager *m_manager;
|
||||
ProcedureData m_currentProcedure;
|
||||
QString m_currentStepId;
|
||||
QString m_currentGroupId;
|
||||
|
||||
// UI 组件
|
||||
QLabel *m_procedureIdLabel;
|
||||
QLabel *m_procedureNameLabel;
|
||||
QPushButton *m_backBtn;
|
||||
QTreeWidget *m_navigationTree;
|
||||
QScrollArea *m_contentArea;
|
||||
QWidget *m_contentWidget;
|
||||
QVBoxLayout *m_contentLayout;
|
||||
QLabel *m_stepTitleLabel; // 当前步骤标题
|
||||
|
||||
QMap<QString, StepItemWidget *> m_stepWidgets;
|
||||
QMap<QString, TableViewWidget *> m_tableWidgets;
|
||||
QMap<QString, QTreeWidgetItem *> m_navItems;
|
||||
QMap<QString, StepData> m_allSteps; // 存储所有步骤数据
|
||||
|
||||
// 表格浮层
|
||||
TableOverlayWidget *m_tableOverlay;
|
||||
};
|
||||
|
||||
#endif // PROCEDUREPLAYERWIDGET_H
|
||||
583
widgets/settingswidget.cpp
Normal file
@@ -0,0 +1,583 @@
|
||||
#include "settingswidget.h"
|
||||
#include "overlaydialogswidget.h"
|
||||
#include <QDateTime>
|
||||
#include <QTabBar>
|
||||
|
||||
const QString SettingsWidget::LABEL_STYLE =
|
||||
"font-size: 14px; color: #37474F;";
|
||||
|
||||
const QString SettingsWidget::VALUE_STYLE =
|
||||
"font-size: 14px; color: #1976D2; font-weight: bold;";
|
||||
|
||||
const QString SettingsWidget::BUTTON_STYLE =
|
||||
"QPushButton { background: #ECEFF1; color: #37474F; border: none; border-radius: 6px; "
|
||||
"padding: 10px 20px; font-size: 14px; font-weight: 500; }"
|
||||
"QPushButton:hover { background: #CFD8DC; }"
|
||||
"QPushButton:pressed { background: #B0BEC5; }";
|
||||
|
||||
const QString SettingsWidget::BUTTON_PRIMARY_STYLE =
|
||||
"QPushButton { background: #1976D2; color: white; border: none; border-radius: 6px; "
|
||||
"padding: 10px 20px; font-size: 14px; font-weight: 500; }"
|
||||
"QPushButton:hover { background: #1565C0; }"
|
||||
"QPushButton:pressed { background: #0D47A1; }";
|
||||
|
||||
const QString SettingsWidget::BUTTON_DANGER_STYLE =
|
||||
"QPushButton { background: #D32F2F; color: white; border: none; border-radius: 6px; "
|
||||
"padding: 10px 20px; font-size: 14px; font-weight: 500; }"
|
||||
"QPushButton:hover { background: #C62828; }"
|
||||
"QPushButton:pressed { background: #B71C1C; }";
|
||||
|
||||
SettingsWidget::SettingsWidget(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setupUI();
|
||||
}
|
||||
|
||||
void SettingsWidget::setupUI()
|
||||
{
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->setSpacing(0);
|
||||
|
||||
// 标题栏
|
||||
auto *headerWidget = new QWidget(this);
|
||||
headerWidget->setFixedHeight(60);
|
||||
headerWidget->setStyleSheet("background: qlineargradient(x1:0, y1:0, x2:1, y2:0, "
|
||||
"stop:0 #1976D2, stop:1 #1565C0);");
|
||||
|
||||
auto *headerLayout = new QHBoxLayout(headerWidget);
|
||||
headerLayout->setContentsMargins(16, 0, 16, 0);
|
||||
|
||||
m_backBtn = new QPushButton("← 返回", this);
|
||||
m_backBtn->setStyleSheet(
|
||||
"QPushButton { background: rgba(255,255,255,0.2); color: white; border: none; "
|
||||
"border-radius: 6px; padding: 8px 16px; font-size: 14px; }"
|
||||
"QPushButton:hover { background: rgba(255,255,255,0.3); }");
|
||||
m_backBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(m_backBtn, &QPushButton::clicked, this, &SettingsWidget::backRequested);
|
||||
headerLayout->addWidget(m_backBtn);
|
||||
|
||||
m_titleLabel = new QLabel("系统设置", this);
|
||||
m_titleLabel->setStyleSheet("color: white; font-size: 18px; font-weight: bold;");
|
||||
m_titleLabel->setAlignment(Qt::AlignCenter);
|
||||
headerLayout->addWidget(m_titleLabel, 1);
|
||||
|
||||
// 占位,保持标题居中
|
||||
auto *spacer = new QWidget(this);
|
||||
spacer->setFixedWidth(80);
|
||||
headerLayout->addWidget(spacer);
|
||||
|
||||
mainLayout->addWidget(headerWidget);
|
||||
|
||||
// 选项卡
|
||||
m_tabWidget = new QTabWidget(this);
|
||||
m_tabWidget->setStyleSheet(
|
||||
"QTabWidget::pane { border: none; background: #F5F5F5; }"
|
||||
"QTabBar::tab { background: #ECEFF1; color: #546E7A; padding: 12px 20px; "
|
||||
"font-size: 14px; border: none; margin-right: 2px; min-width: 70px; }"
|
||||
"QTabBar::tab:selected { background: white; color: #1976D2; font-weight: bold; "
|
||||
"border-bottom: 3px solid #1976D2; }"
|
||||
"QTabBar::tab:hover { background: #CFD8DC; }");
|
||||
m_tabWidget->tabBar()->setExpanding(false);
|
||||
|
||||
m_tabWidget->addTab(createDeviceInfoTab(), "设备信息");
|
||||
m_tabWidget->addTab(createSystemConfigTab(), "系统配置");
|
||||
m_tabWidget->addTab(createCalibrationTab(), "校准设置");
|
||||
m_tabWidget->addTab(createNetworkTab(), "网络设置");
|
||||
m_tabWidget->addTab(createAboutTab(), "关于");
|
||||
|
||||
mainLayout->addWidget(m_tabWidget, 1);
|
||||
}
|
||||
|
||||
QGroupBox *SettingsWidget::createStyledGroupBox(const QString &title)
|
||||
{
|
||||
auto *groupBox = new QGroupBox(title, this);
|
||||
groupBox->setStyleSheet(
|
||||
"QGroupBox { font-size: 15px; font-weight: bold; color: #37474F; "
|
||||
"border: 1px solid #E0E0E0; border-radius: 8px; margin-top: 12px; padding-top: 8px; "
|
||||
"background: white; }"
|
||||
"QGroupBox::title { subcontrol-origin: margin; left: 16px; padding: 0 8px; }");
|
||||
return groupBox;
|
||||
}
|
||||
|
||||
QWidget *SettingsWidget::createDeviceInfoTab()
|
||||
{
|
||||
auto *scrollArea = new QScrollArea(this);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setStyleSheet("QScrollArea { border: none; background: #F5F5F5; }");
|
||||
|
||||
auto *content = new QWidget();
|
||||
auto *layout = new QVBoxLayout(content);
|
||||
layout->setContentsMargins(24, 24, 24, 24);
|
||||
layout->setSpacing(16);
|
||||
|
||||
// 设备信息组
|
||||
auto *deviceGroup = createStyledGroupBox("设备信息");
|
||||
auto *deviceLayout = new QGridLayout(deviceGroup);
|
||||
deviceLayout->setContentsMargins(16, 24, 16, 16);
|
||||
deviceLayout->setSpacing(12);
|
||||
|
||||
deviceLayout->addWidget(new QLabel("设备型号:", this), 0, 0);
|
||||
m_deviceModelLabel = new QLabel("iCALI-Pro 2000", this);
|
||||
m_deviceModelLabel->setStyleSheet(VALUE_STYLE);
|
||||
deviceLayout->addWidget(m_deviceModelLabel, 0, 1);
|
||||
|
||||
deviceLayout->addWidget(new QLabel("序列号:", this), 1, 0);
|
||||
m_serialNumberLabel = new QLabel("ICALI-2024-001234", this);
|
||||
m_serialNumberLabel->setStyleSheet(VALUE_STYLE);
|
||||
deviceLayout->addWidget(m_serialNumberLabel, 1, 1);
|
||||
|
||||
deviceLayout->addWidget(new QLabel("固件版本:", this), 2, 0);
|
||||
m_firmwareVersionLabel = new QLabel("v2.1.0 (Build 20241231)", this);
|
||||
m_firmwareVersionLabel->setStyleSheet(VALUE_STYLE);
|
||||
deviceLayout->addWidget(m_firmwareVersionLabel, 2, 1);
|
||||
|
||||
deviceLayout->addWidget(new QLabel("硬件版本:", this), 3, 0);
|
||||
m_hardwareVersionLabel = new QLabel("Rev.C", this);
|
||||
m_hardwareVersionLabel->setStyleSheet(VALUE_STYLE);
|
||||
deviceLayout->addWidget(m_hardwareVersionLabel, 3, 1);
|
||||
|
||||
deviceLayout->setColumnStretch(1, 1);
|
||||
layout->addWidget(deviceGroup);
|
||||
|
||||
// 存储信息组
|
||||
auto *storageGroup = createStyledGroupBox("存储信息");
|
||||
auto *storageLayout = new QGridLayout(storageGroup);
|
||||
storageLayout->setContentsMargins(16, 24, 16, 16);
|
||||
storageLayout->setSpacing(12);
|
||||
|
||||
storageLayout->addWidget(new QLabel("内部存储:", this), 0, 0);
|
||||
auto *internalStorage = new QLabel("已用 2.3GB / 8GB", this);
|
||||
internalStorage->setStyleSheet(VALUE_STYLE);
|
||||
storageLayout->addWidget(internalStorage, 0, 1);
|
||||
|
||||
storageLayout->addWidget(new QLabel("SD卡:", this), 1, 0);
|
||||
auto *sdCard = new QLabel("已用 1.2GB / 32GB", this);
|
||||
sdCard->setStyleSheet(VALUE_STYLE);
|
||||
storageLayout->addWidget(sdCard, 1, 1);
|
||||
|
||||
storageLayout->setColumnStretch(1, 1);
|
||||
layout->addWidget(storageGroup);
|
||||
|
||||
// 运行状态组
|
||||
auto *statusGroup = createStyledGroupBox("运行状态");
|
||||
auto *statusLayout = new QGridLayout(statusGroup);
|
||||
statusLayout->setContentsMargins(16, 24, 16, 16);
|
||||
statusLayout->setSpacing(12);
|
||||
|
||||
statusLayout->addWidget(new QLabel("运行时间:", this), 0, 0);
|
||||
auto *uptime = new QLabel("3天 12小时 45分", this);
|
||||
uptime->setStyleSheet(VALUE_STYLE);
|
||||
statusLayout->addWidget(uptime, 0, 1);
|
||||
|
||||
statusLayout->addWidget(new QLabel("CPU温度:", this), 1, 0);
|
||||
auto *cpuTemp = new QLabel("42°C", this);
|
||||
cpuTemp->setStyleSheet(VALUE_STYLE);
|
||||
statusLayout->addWidget(cpuTemp, 1, 1);
|
||||
|
||||
statusLayout->addWidget(new QLabel("电池状态:", this), 2, 0);
|
||||
auto *battery = new QLabel("充电中 85%", this);
|
||||
battery->setStyleSheet("font-size: 14px; color: #43A047; font-weight: bold;");
|
||||
statusLayout->addWidget(battery, 2, 1);
|
||||
|
||||
statusLayout->setColumnStretch(1, 1);
|
||||
layout->addWidget(statusGroup);
|
||||
|
||||
layout->addStretch();
|
||||
scrollArea->setWidget(content);
|
||||
return scrollArea;
|
||||
}
|
||||
|
||||
QWidget *SettingsWidget::createSystemConfigTab()
|
||||
{
|
||||
auto *scrollArea = new QScrollArea(this);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setStyleSheet("QScrollArea { border: none; background: #F5F5F5; }");
|
||||
|
||||
auto *content = new QWidget();
|
||||
auto *layout = new QVBoxLayout(content);
|
||||
layout->setContentsMargins(24, 24, 24, 24);
|
||||
layout->setSpacing(16);
|
||||
|
||||
// 显示设置组
|
||||
auto *displayGroup = createStyledGroupBox("显示设置");
|
||||
auto *displayLayout = new QGridLayout(displayGroup);
|
||||
displayLayout->setContentsMargins(16, 24, 16, 16);
|
||||
displayLayout->setSpacing(12);
|
||||
|
||||
displayLayout->addWidget(new QLabel("语言:", this), 0, 0);
|
||||
m_languageCombo = new QComboBox(this);
|
||||
m_languageCombo->addItems({"简体中文", "English"});
|
||||
m_languageCombo->setStyleSheet(
|
||||
"QComboBox { padding: 8px 12px; border: 1px solid #E0E0E0; border-radius: 6px; "
|
||||
"background: white; font-size: 14px; min-height: 36px; }"
|
||||
"QComboBox::drop-down { subcontrol-origin: padding; subcontrol-position: top right; "
|
||||
"width: 36px; border: none; border-left: 1px solid #E0E0E0; background: #f5f5f5; "
|
||||
"border-top-right-radius: 6px; border-bottom-right-radius: 6px; }"
|
||||
"QComboBox::down-arrow { border-left: 6px solid transparent; border-right: 6px solid transparent; "
|
||||
"border-top: 8px solid #666; }"
|
||||
"QComboBox QAbstractItemView { background: white; border: 1px solid #ddd; border-radius: 6px; "
|
||||
"selection-background-color: #e3f2fd; outline: none; }"
|
||||
"QComboBox QAbstractItemView::item { min-height: 40px; padding: 10px 12px; }");
|
||||
m_languageCombo->setCursor(Qt::PointingHandCursor);
|
||||
displayLayout->addWidget(m_languageCombo, 0, 1);
|
||||
|
||||
displayLayout->addWidget(new QLabel("主题:", this), 1, 0);
|
||||
m_themeCombo = new QComboBox(this);
|
||||
m_themeCombo->addItems({"浅色", "深色", "跟随系统"});
|
||||
m_themeCombo->setStyleSheet(
|
||||
"QComboBox { padding: 8px 12px; border: 1px solid #E0E0E0; border-radius: 6px; "
|
||||
"background: white; font-size: 14px; min-height: 36px; }"
|
||||
"QComboBox::drop-down { subcontrol-origin: padding; subcontrol-position: top right; "
|
||||
"width: 36px; border: none; border-left: 1px solid #E0E0E0; background: #f5f5f5; "
|
||||
"border-top-right-radius: 6px; border-bottom-right-radius: 6px; }"
|
||||
"QComboBox::down-arrow { border-left: 6px solid transparent; border-right: 6px solid transparent; "
|
||||
"border-top: 8px solid #666; }"
|
||||
"QComboBox QAbstractItemView { background: white; border: 1px solid #ddd; border-radius: 6px; "
|
||||
"selection-background-color: #e3f2fd; outline: none; }"
|
||||
"QComboBox QAbstractItemView::item { min-height: 40px; padding: 10px 12px; }");
|
||||
m_themeCombo->setCursor(Qt::PointingHandCursor);
|
||||
displayLayout->addWidget(m_themeCombo, 1, 1);
|
||||
|
||||
displayLayout->addWidget(new QLabel("屏幕亮度:", this), 2, 0);
|
||||
m_brightnessSpinBox = new QSpinBox(this);
|
||||
m_brightnessSpinBox->setRange(10, 100);
|
||||
m_brightnessSpinBox->setValue(80);
|
||||
m_brightnessSpinBox->setSuffix("%");
|
||||
m_brightnessSpinBox->setStyleSheet(
|
||||
"QSpinBox { padding: 8px; border: 1px solid #E0E0E0; border-radius: 4px; "
|
||||
"background: white; font-size: 14px; }");
|
||||
displayLayout->addWidget(m_brightnessSpinBox, 2, 1);
|
||||
|
||||
displayLayout->setColumnStretch(1, 1);
|
||||
layout->addWidget(displayGroup);
|
||||
|
||||
// 声音设置组
|
||||
auto *soundGroup = createStyledGroupBox("声音设置");
|
||||
auto *soundLayout = new QGridLayout(soundGroup);
|
||||
soundLayout->setContentsMargins(16, 24, 16, 16);
|
||||
soundLayout->setSpacing(12);
|
||||
|
||||
soundLayout->addWidget(new QLabel("音量:", this), 0, 0);
|
||||
m_volumeSpinBox = new QSpinBox(this);
|
||||
m_volumeSpinBox->setRange(0, 100);
|
||||
m_volumeSpinBox->setValue(50);
|
||||
m_volumeSpinBox->setSuffix("%");
|
||||
m_volumeSpinBox->setStyleSheet(
|
||||
"QSpinBox { padding: 8px; border: 1px solid #E0E0E0; border-radius: 4px; "
|
||||
"background: white; font-size: 14px; }");
|
||||
soundLayout->addWidget(m_volumeSpinBox, 0, 1);
|
||||
|
||||
soundLayout->setColumnStretch(1, 1);
|
||||
layout->addWidget(soundGroup);
|
||||
|
||||
// 省电设置组
|
||||
auto *powerGroup = createStyledGroupBox("省电设置");
|
||||
auto *powerLayout = new QGridLayout(powerGroup);
|
||||
powerLayout->setContentsMargins(16, 24, 16, 16);
|
||||
powerLayout->setSpacing(12);
|
||||
|
||||
m_autoSleepCheck = new QCheckBox("自动休眠", this);
|
||||
m_autoSleepCheck->setChecked(true);
|
||||
m_autoSleepCheck->setStyleSheet("QCheckBox { font-size: 14px; }");
|
||||
powerLayout->addWidget(m_autoSleepCheck, 0, 0, 1, 2);
|
||||
|
||||
powerLayout->addWidget(new QLabel("休眠时间:", this), 1, 0);
|
||||
m_sleepTimeSpinBox = new QSpinBox(this);
|
||||
m_sleepTimeSpinBox->setRange(1, 60);
|
||||
m_sleepTimeSpinBox->setValue(10);
|
||||
m_sleepTimeSpinBox->setSuffix(" 分钟");
|
||||
m_sleepTimeSpinBox->setStyleSheet(
|
||||
"QSpinBox { padding: 8px; border: 1px solid #E0E0E0; border-radius: 4px; "
|
||||
"background: white; font-size: 14px; }");
|
||||
powerLayout->addWidget(m_sleepTimeSpinBox, 1, 1);
|
||||
|
||||
powerLayout->setColumnStretch(1, 1);
|
||||
layout->addWidget(powerGroup);
|
||||
|
||||
// 按钮区域
|
||||
auto *buttonLayout = new QHBoxLayout();
|
||||
buttonLayout->addStretch();
|
||||
|
||||
auto *resetBtn = new QPushButton("恢复默认", this);
|
||||
resetBtn->setStyleSheet(BUTTON_STYLE);
|
||||
resetBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(resetBtn, &QPushButton::clicked, this, &SettingsWidget::onResetClicked);
|
||||
buttonLayout->addWidget(resetBtn);
|
||||
|
||||
auto *saveBtn = new QPushButton("保存设置", this);
|
||||
saveBtn->setStyleSheet(BUTTON_PRIMARY_STYLE);
|
||||
saveBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(saveBtn, &QPushButton::clicked, this, &SettingsWidget::onSaveClicked);
|
||||
buttonLayout->addWidget(saveBtn);
|
||||
|
||||
layout->addLayout(buttonLayout);
|
||||
layout->addStretch();
|
||||
|
||||
scrollArea->setWidget(content);
|
||||
return scrollArea;
|
||||
}
|
||||
|
||||
QWidget *SettingsWidget::createCalibrationTab()
|
||||
{
|
||||
auto *scrollArea = new QScrollArea(this);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setStyleSheet("QScrollArea { border: none; background: #F5F5F5; }");
|
||||
|
||||
auto *content = new QWidget();
|
||||
auto *layout = new QVBoxLayout(content);
|
||||
layout->setContentsMargins(24, 24, 24, 24);
|
||||
layout->setSpacing(16);
|
||||
|
||||
// 校准状态组
|
||||
auto *statusGroup = createStyledGroupBox("校准状态");
|
||||
auto *statusLayout = new QGridLayout(statusGroup);
|
||||
statusLayout->setContentsMargins(16, 24, 16, 16);
|
||||
statusLayout->setSpacing(12);
|
||||
|
||||
statusLayout->addWidget(new QLabel("上次校准:", this), 0, 0);
|
||||
m_lastCalibrationLabel = new QLabel("2024-12-15 09:30:00", this);
|
||||
m_lastCalibrationLabel->setStyleSheet(VALUE_STYLE);
|
||||
statusLayout->addWidget(m_lastCalibrationLabel, 0, 1);
|
||||
|
||||
statusLayout->addWidget(new QLabel("校准状态:", this), 1, 0);
|
||||
m_calibrationStatusLabel = new QLabel("✓ 已校准", this);
|
||||
m_calibrationStatusLabel->setStyleSheet("font-size: 14px; color: #43A047; font-weight: bold;");
|
||||
statusLayout->addWidget(m_calibrationStatusLabel, 1, 1);
|
||||
|
||||
statusLayout->addWidget(new QLabel("下次校准:", this), 2, 0);
|
||||
auto *nextCal = new QLabel("2025-12-15 (剩余350天)", this);
|
||||
nextCal->setStyleSheet(VALUE_STYLE);
|
||||
statusLayout->addWidget(nextCal, 2, 1);
|
||||
|
||||
statusLayout->setColumnStretch(1, 1);
|
||||
layout->addWidget(statusGroup);
|
||||
|
||||
// 校准操作组
|
||||
auto *actionGroup = createStyledGroupBox("校准操作");
|
||||
auto *actionLayout = new QVBoxLayout(actionGroup);
|
||||
actionLayout->setContentsMargins(16, 24, 16, 16);
|
||||
actionLayout->setSpacing(12);
|
||||
|
||||
auto *note = new QLabel("请确保设备处于稳定状态,并连接标准校准源后再进行校准操作。", this);
|
||||
note->setWordWrap(true);
|
||||
note->setStyleSheet("font-size: 13px; color: #757575; padding: 8px; "
|
||||
"background: #FFF8E1; border-radius: 4px;");
|
||||
actionLayout->addWidget(note);
|
||||
|
||||
auto *calibrateBtn = new QPushButton("开始校准", this);
|
||||
calibrateBtn->setStyleSheet(BUTTON_PRIMARY_STYLE);
|
||||
calibrateBtn->setFixedHeight(44);
|
||||
calibrateBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(calibrateBtn, &QPushButton::clicked, this, &SettingsWidget::onCalibrateClicked);
|
||||
actionLayout->addWidget(calibrateBtn);
|
||||
|
||||
layout->addWidget(actionGroup);
|
||||
|
||||
layout->addStretch();
|
||||
scrollArea->setWidget(content);
|
||||
return scrollArea;
|
||||
}
|
||||
|
||||
QWidget *SettingsWidget::createNetworkTab()
|
||||
{
|
||||
auto *scrollArea = new QScrollArea(this);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setStyleSheet("QScrollArea { border: none; background: #F5F5F5; }");
|
||||
|
||||
auto *content = new QWidget();
|
||||
auto *layout = new QVBoxLayout(content);
|
||||
layout->setContentsMargins(24, 24, 24, 24);
|
||||
layout->setSpacing(16);
|
||||
|
||||
// 服务器设置组
|
||||
auto *serverGroup = createStyledGroupBox("服务器设置");
|
||||
auto *serverLayout = new QGridLayout(serverGroup);
|
||||
serverLayout->setContentsMargins(16, 24, 16, 16);
|
||||
serverLayout->setSpacing(12);
|
||||
|
||||
serverLayout->addWidget(new QLabel("服务器地址:", this), 0, 0);
|
||||
m_serverAddressEdit = new QLineEdit(this);
|
||||
m_serverAddressEdit->setPlaceholderText("例如: 192.168.1.100");
|
||||
m_serverAddressEdit->setStyleSheet(
|
||||
"QLineEdit { padding: 8px; border: 1px solid #E0E0E0; border-radius: 4px; "
|
||||
"background: white; font-size: 14px; }");
|
||||
serverLayout->addWidget(m_serverAddressEdit, 0, 1);
|
||||
|
||||
serverLayout->addWidget(new QLabel("端口号:", this), 1, 0);
|
||||
m_serverPortSpinBox = new QSpinBox(this);
|
||||
m_serverPortSpinBox->setRange(1, 65535);
|
||||
m_serverPortSpinBox->setValue(8080);
|
||||
m_serverPortSpinBox->setStyleSheet(
|
||||
"QSpinBox { padding: 8px; border: 1px solid #E0E0E0; border-radius: 4px; "
|
||||
"background: white; font-size: 14px; }");
|
||||
serverLayout->addWidget(m_serverPortSpinBox, 1, 1);
|
||||
|
||||
m_autoConnectCheck = new QCheckBox("启动时自动连接", this);
|
||||
m_autoConnectCheck->setStyleSheet("QCheckBox { font-size: 14px; }");
|
||||
serverLayout->addWidget(m_autoConnectCheck, 2, 0, 1, 2);
|
||||
|
||||
serverLayout->setColumnStretch(1, 1);
|
||||
layout->addWidget(serverGroup);
|
||||
|
||||
// 连接状态组
|
||||
auto *connGroup = createStyledGroupBox("连接状态");
|
||||
auto *connLayout = new QGridLayout(connGroup);
|
||||
connLayout->setContentsMargins(16, 24, 16, 16);
|
||||
connLayout->setSpacing(12);
|
||||
|
||||
connLayout->addWidget(new QLabel("网络状态:", this), 0, 0);
|
||||
auto *netStatus = new QLabel("● 已连接 (WiFi)", this);
|
||||
netStatus->setStyleSheet("font-size: 14px; color: #43A047; font-weight: bold;");
|
||||
connLayout->addWidget(netStatus, 0, 1);
|
||||
|
||||
connLayout->addWidget(new QLabel("IP地址:", this), 1, 0);
|
||||
auto *ipAddr = new QLabel("192.168.1.88", this);
|
||||
ipAddr->setStyleSheet(VALUE_STYLE);
|
||||
connLayout->addWidget(ipAddr, 1, 1);
|
||||
|
||||
connLayout->addWidget(new QLabel("服务器状态:", this), 2, 0);
|
||||
auto *serverStatus = new QLabel("○ 未连接", this);
|
||||
serverStatus->setStyleSheet("font-size: 14px; color: #757575;");
|
||||
connLayout->addWidget(serverStatus, 2, 1);
|
||||
|
||||
connLayout->setColumnStretch(1, 1);
|
||||
layout->addWidget(connGroup);
|
||||
|
||||
layout->addStretch();
|
||||
scrollArea->setWidget(content);
|
||||
return scrollArea;
|
||||
}
|
||||
|
||||
QWidget *SettingsWidget::createAboutTab()
|
||||
{
|
||||
auto *scrollArea = new QScrollArea(this);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setStyleSheet("QScrollArea { border: none; background: #F5F5F5; }");
|
||||
|
||||
auto *content = new QWidget();
|
||||
auto *layout = new QVBoxLayout(content);
|
||||
layout->setContentsMargins(24, 24, 24, 24);
|
||||
layout->setSpacing(16);
|
||||
|
||||
// 关于组
|
||||
auto *aboutGroup = createStyledGroupBox("关于");
|
||||
auto *aboutLayout = new QVBoxLayout(aboutGroup);
|
||||
aboutLayout->setContentsMargins(16, 24, 16, 16);
|
||||
aboutLayout->setSpacing(12);
|
||||
|
||||
auto *appName = new QLabel("智能校验仪 iCALI", this);
|
||||
appName->setStyleSheet("font-size: 20px; font-weight: bold; color: #1976D2;");
|
||||
appName->setAlignment(Qt::AlignCenter);
|
||||
aboutLayout->addWidget(appName);
|
||||
|
||||
auto *version = new QLabel("版本 2.1.0", this);
|
||||
version->setStyleSheet("font-size: 14px; color: #757575;");
|
||||
version->setAlignment(Qt::AlignCenter);
|
||||
aboutLayout->addWidget(version);
|
||||
|
||||
auto *copyright = new QLabel("© 2024 某公司. 保留所有权利.", this);
|
||||
copyright->setStyleSheet("font-size: 12px; color: #9E9E9E;");
|
||||
copyright->setAlignment(Qt::AlignCenter);
|
||||
aboutLayout->addWidget(copyright);
|
||||
|
||||
layout->addWidget(aboutGroup);
|
||||
|
||||
// 维护操作组
|
||||
auto *maintainGroup = createStyledGroupBox("维护操作");
|
||||
auto *maintainLayout = new QVBoxLayout(maintainGroup);
|
||||
maintainLayout->setContentsMargins(16, 24, 16, 16);
|
||||
maintainLayout->setSpacing(12);
|
||||
|
||||
auto *exportLogBtn = new QPushButton("导出日志", this);
|
||||
exportLogBtn->setStyleSheet(BUTTON_STYLE);
|
||||
exportLogBtn->setFixedHeight(44);
|
||||
exportLogBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(exportLogBtn, &QPushButton::clicked, this, &SettingsWidget::onExportLogClicked);
|
||||
maintainLayout->addWidget(exportLogBtn);
|
||||
|
||||
auto *clearCacheBtn = new QPushButton("清除缓存", this);
|
||||
clearCacheBtn->setStyleSheet(BUTTON_STYLE);
|
||||
clearCacheBtn->setFixedHeight(44);
|
||||
clearCacheBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(clearCacheBtn, &QPushButton::clicked, this, &SettingsWidget::onClearCacheClicked);
|
||||
maintainLayout->addWidget(clearCacheBtn);
|
||||
|
||||
auto *factoryResetBtn = new QPushButton("恢复出厂设置", this);
|
||||
factoryResetBtn->setStyleSheet(BUTTON_DANGER_STYLE);
|
||||
factoryResetBtn->setFixedHeight(44);
|
||||
factoryResetBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(factoryResetBtn, &QPushButton::clicked, this, &SettingsWidget::onFactoryResetClicked);
|
||||
maintainLayout->addWidget(factoryResetBtn);
|
||||
|
||||
layout->addWidget(maintainGroup);
|
||||
|
||||
layout->addStretch();
|
||||
scrollArea->setWidget(content);
|
||||
return scrollArea;
|
||||
}
|
||||
|
||||
void SettingsWidget::onSaveClicked()
|
||||
{
|
||||
OverlayDialog::information(this, "保存成功", "系统设置已保存");
|
||||
}
|
||||
|
||||
void SettingsWidget::onResetClicked()
|
||||
{
|
||||
OverlayDialog::question(this, "恢复默认", "确定要恢复默认设置吗?",
|
||||
[this](bool confirmed)
|
||||
{
|
||||
if (confirmed)
|
||||
{
|
||||
m_languageCombo->setCurrentIndex(0);
|
||||
m_themeCombo->setCurrentIndex(0);
|
||||
m_brightnessSpinBox->setValue(80);
|
||||
m_volumeSpinBox->setValue(50);
|
||||
m_autoSleepCheck->setChecked(true);
|
||||
m_sleepTimeSpinBox->setValue(10);
|
||||
OverlayDialog::information(this, "已恢复", "设置已恢复为默认值");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void SettingsWidget::onCalibrateClicked()
|
||||
{
|
||||
OverlayDialog::question(this, "开始校准", "确定要开始校准吗?\n请确保已连接标准校准源。",
|
||||
[this](bool confirmed)
|
||||
{
|
||||
if (confirmed)
|
||||
{
|
||||
OverlayDialog::information(this, "校准", "校准向导功能开发中...");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void SettingsWidget::onExportLogClicked()
|
||||
{
|
||||
OverlayDialog::information(this, "导出日志", "日志已导出到: /sdcard/logs/");
|
||||
}
|
||||
|
||||
void SettingsWidget::onClearCacheClicked()
|
||||
{
|
||||
OverlayDialog::question(this, "清除缓存", "确定要清除缓存吗?",
|
||||
[this](bool confirmed)
|
||||
{
|
||||
if (confirmed)
|
||||
{
|
||||
OverlayDialog::information(this, "清除成功", "缓存已清除,释放空间: 128MB");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void SettingsWidget::onFactoryResetClicked()
|
||||
{
|
||||
OverlayDialog::question(this, "恢复出厂设置",
|
||||
"⚠️ 警告\n\n此操作将清除所有数据和设置,设备将恢复到出厂状态。\n\n确定要继续吗?",
|
||||
[this](bool confirmed)
|
||||
{
|
||||
if (confirmed)
|
||||
{
|
||||
OverlayDialog::warning(this, "恢复出厂", "此功能需要管理员权限");
|
||||
}
|
||||
});
|
||||
}
|
||||
87
widgets/settingswidget.h
Normal file
@@ -0,0 +1,87 @@
|
||||
#ifndef SETTINGSWIDGET_H
|
||||
#define SETTINGSWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QComboBox>
|
||||
#include <QSpinBox>
|
||||
#include <QCheckBox>
|
||||
#include <QTabWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QScrollArea>
|
||||
|
||||
/**
|
||||
* @brief 系统设置页面
|
||||
* 提供系统参数配置、设备信息、校准设置等功能
|
||||
*/
|
||||
class SettingsWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SettingsWidget(QWidget *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void backRequested();
|
||||
|
||||
private slots:
|
||||
void onSaveClicked();
|
||||
void onResetClicked();
|
||||
void onCalibrateClicked();
|
||||
void onExportLogClicked();
|
||||
void onClearCacheClicked();
|
||||
void onFactoryResetClicked();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
QWidget *createDeviceInfoTab();
|
||||
QWidget *createSystemConfigTab();
|
||||
QWidget *createCalibrationTab();
|
||||
QWidget *createNetworkTab();
|
||||
QWidget *createAboutTab();
|
||||
QGroupBox *createStyledGroupBox(const QString &title);
|
||||
|
||||
// 标题栏
|
||||
QLabel *m_titleLabel;
|
||||
QPushButton *m_backBtn;
|
||||
|
||||
// 设备信息
|
||||
QLabel *m_deviceModelLabel;
|
||||
QLabel *m_serialNumberLabel;
|
||||
QLabel *m_firmwareVersionLabel;
|
||||
QLabel *m_hardwareVersionLabel;
|
||||
|
||||
// 系统配置
|
||||
QComboBox *m_languageCombo;
|
||||
QComboBox *m_themeCombo;
|
||||
QSpinBox *m_brightnessSpinBox;
|
||||
QSpinBox *m_volumeSpinBox;
|
||||
QCheckBox *m_autoSleepCheck;
|
||||
QSpinBox *m_sleepTimeSpinBox;
|
||||
|
||||
// 校准设置
|
||||
QLabel *m_lastCalibrationLabel;
|
||||
QLabel *m_calibrationStatusLabel;
|
||||
|
||||
// 网络设置
|
||||
QLineEdit *m_serverAddressEdit;
|
||||
QSpinBox *m_serverPortSpinBox;
|
||||
QCheckBox *m_autoConnectCheck;
|
||||
|
||||
// 选项卡
|
||||
QTabWidget *m_tabWidget;
|
||||
|
||||
// 样式常量
|
||||
static const QString LABEL_STYLE;
|
||||
static const QString VALUE_STYLE;
|
||||
static const QString BUTTON_STYLE;
|
||||
static const QString BUTTON_PRIMARY_STYLE;
|
||||
static const QString BUTTON_DANGER_STYLE;
|
||||
};
|
||||
|
||||
#endif // SETTINGSWIDGET_H
|
||||
1760
widgets/signalmeasurementwidget.cpp
Normal file
483
widgets/signalmeasurementwidget.h
Normal file
@@ -0,0 +1,483 @@
|
||||
#ifndef SIGNALMEASUREMENTWIDGET_H
|
||||
#define SIGNALMEASUREMENTWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QStackedWidget>
|
||||
#include <QScrollArea>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QComboBox>
|
||||
#include <QLineEdit>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QGroupBox>
|
||||
#include <QTableWidget>
|
||||
#include <QHeaderView>
|
||||
#include <QTimer>
|
||||
#include <QRadioButton>
|
||||
#include <QButtonGroup>
|
||||
#include <QCheckBox>
|
||||
#include <QProgressBar>
|
||||
#include <QSplitter>
|
||||
#include "../hardware/channelmanager.h"
|
||||
#include "channelstatuswidget.h"
|
||||
|
||||
/**
|
||||
* @brief 信号测量功能模块基类
|
||||
* 提供统一的工业风格UI框架
|
||||
*/
|
||||
class MeasurementModuleBase : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MeasurementModuleBase(const QString &title, QWidget *parent = nullptr);
|
||||
|
||||
protected:
|
||||
// 创建标准样式的输入控件
|
||||
QLineEdit *createStyledInput(const QString &placeholder = "", int width = 120);
|
||||
QDoubleSpinBox *createStyledSpinBox(double min, double max, int decimals = 2, int width = 120);
|
||||
QComboBox *createStyledComboBox(const QStringList &items, int width = 150);
|
||||
QPushButton *createPrimaryButton(const QString &text, int width = 120);
|
||||
QPushButton *createSecondaryButton(const QString &text, int width = 100);
|
||||
QLabel *createValueDisplay(const QString &value = "---", int width = 180);
|
||||
QGroupBox *createStyledGroupBox(const QString &title);
|
||||
|
||||
// 添加带标签的输入行
|
||||
QHBoxLayout *createLabeledRow(const QString &label, QWidget *widget, const QString &unit = "");
|
||||
|
||||
// 统一样式常量
|
||||
static const QString CARD_STYLE;
|
||||
static const QString INPUT_STYLE;
|
||||
static const QString PRIMARY_BTN_STYLE;
|
||||
static const QString SECONDARY_BTN_STYLE;
|
||||
static const QString VALUE_DISPLAY_STYLE;
|
||||
static const QString LABEL_STYLE;
|
||||
static const QString GROUPBOX_STYLE;
|
||||
|
||||
QVBoxLayout *m_mainLayout;
|
||||
QString m_title;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 热电阻测量模块
|
||||
* 支持2/3/4线热电阻测量和PT100输出
|
||||
* 整合ChannelManager进行通道管理
|
||||
*/
|
||||
class RTDMeasurementWidget : public MeasurementModuleBase
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum RTDType
|
||||
{
|
||||
PT100,
|
||||
PT1000,
|
||||
CU50,
|
||||
CU100
|
||||
};
|
||||
enum MeasurementMode
|
||||
{
|
||||
TWO_WIRE_MODE,
|
||||
THREE_WIRE_MODE,
|
||||
FOUR_WIRE_MODE
|
||||
};
|
||||
|
||||
explicit RTDMeasurementWidget(QWidget *parent = nullptr);
|
||||
~RTDMeasurementWidget();
|
||||
|
||||
signals:
|
||||
void measurementDataReady(double resistance, double temperature);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void onMeasureClicked();
|
||||
void onStopClicked();
|
||||
void onWireTypeChanged(int index);
|
||||
void onChannelSelected(ChannelManager::ChannelId channelId);
|
||||
void updateMeasurement();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void setupChannelSelection();
|
||||
void updateChannelSelection();
|
||||
void updateConnectionModeVisibility();
|
||||
void generateSimulatedData();
|
||||
double convertResistanceToTemperature(double resistance);
|
||||
bool validateChannelForMode();
|
||||
|
||||
// 通道管理
|
||||
ChannelManager *m_channelManager;
|
||||
QMap<ChannelManager::ChannelId, ChannelStatusWidget *> m_channelWidgets;
|
||||
ChannelManager::ChannelId m_selectedChannel;
|
||||
ChannelManager::ChannelId m_currentChannel;
|
||||
QTimer *m_measurementTimer;
|
||||
|
||||
// 测量状态
|
||||
bool m_isMeasuring;
|
||||
MeasurementMode m_currentMode;
|
||||
RTDType m_currentRTDType;
|
||||
double m_currentResistance;
|
||||
double m_currentTemperature;
|
||||
|
||||
// UI控件
|
||||
QWidget *m_channelSelectionWidget;
|
||||
QComboBox *m_wireTypeCombo;
|
||||
QComboBox *m_rtdTypeCombo;
|
||||
QLabel *m_resistanceDisplay;
|
||||
QLabel *m_temperatureDisplay;
|
||||
QLabel *m_lineResDisplay;
|
||||
QLabel *m_channelStatusLabel;
|
||||
QLabel *m_wiringDiagramLabel;
|
||||
QPushButton *m_measureBtn;
|
||||
QPushButton *m_stopBtn;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief PT100输出模块 - 双通道
|
||||
* 支持两个通道的PT100温度输出
|
||||
*/
|
||||
class RTDOutputWidget : public MeasurementModuleBase
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit RTDOutputWidget(QWidget *parent = nullptr);
|
||||
~RTDOutputWidget();
|
||||
|
||||
private slots:
|
||||
void onChannel1OutputClicked();
|
||||
void onChannel1StopClicked();
|
||||
void onChannel2OutputClicked();
|
||||
void onChannel2StopClicked();
|
||||
void onChannel1TempChanged(double value);
|
||||
void onChannel2TempChanged(double value);
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
double convertTemperatureToResistance(double temperature);
|
||||
|
||||
// 输出状态
|
||||
bool m_isChannel1Outputting;
|
||||
bool m_isChannel2Outputting;
|
||||
|
||||
// 通道1控件
|
||||
QDoubleSpinBox *m_channel1TempSpin;
|
||||
QLabel *m_channel1ResDisplay;
|
||||
QPushButton *m_channel1OutputBtn;
|
||||
QPushButton *m_channel1StopBtn;
|
||||
|
||||
// 通道2控件
|
||||
QDoubleSpinBox *m_channel2TempSpin;
|
||||
QLabel *m_channel2ResDisplay;
|
||||
QPushButton *m_channel2OutputBtn;
|
||||
QPushButton *m_channel2StopBtn;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 热电偶测量模块
|
||||
* 支持热电偶电压测量和毫伏输出
|
||||
*/
|
||||
class ThermocoupleMeasurementWidget : public MeasurementModuleBase
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ThermocoupleMeasurementWidget(QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void onMeasureClicked();
|
||||
void onOutputClicked();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
|
||||
QComboBox *m_tcTypeCombo; // K/J/T/E/S/R/B型选择
|
||||
QLabel *m_voltageDisplay; // 电压显示(mV)
|
||||
QLabel *m_temperatureDisplay; // 温度显示
|
||||
QDoubleSpinBox *m_outputTempSpin; // 输出温度设置
|
||||
QLabel *m_outputVoltageDisplay; // 输出电压显示
|
||||
QPushButton *m_measureBtn;
|
||||
QPushButton *m_outputBtn;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 绝缘电阻测量模块
|
||||
* 支持50V/100V绝缘电阻测量
|
||||
*/
|
||||
class InsulationMeasurementWidget : public MeasurementModuleBase
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit InsulationMeasurementWidget(QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void onMeasureClicked();
|
||||
void onAutoTestToggled(bool enabled);
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
|
||||
QRadioButton *m_50vRadio;
|
||||
QRadioButton *m_100vRadio;
|
||||
QCheckBox *m_autoTestCheck; // 自动测试
|
||||
QLabel *m_insulationDisplay; // 绝缘电阻显示
|
||||
QLabel *m_statusDisplay; // 状态显示
|
||||
QProgressBar *m_dischargeProgress; // 放电进度
|
||||
QPushButton *m_measureBtn;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 直流电流信号模块
|
||||
* 支持0-20mA电流输出/测量
|
||||
*/
|
||||
class DCCurrentWidget : public MeasurementModuleBase
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DCCurrentWidget(QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void onMeasureClicked();
|
||||
void onOutputClicked();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
|
||||
QRadioButton *m_activeRadio; // 有源模式
|
||||
QRadioButton *m_passiveRadio; // 无源模式
|
||||
QLabel *m_currentDisplay; // 电流显示
|
||||
QLabel *m_maxDisplay; // 最大值
|
||||
QLabel *m_minDisplay; // 最小值
|
||||
QLabel *m_avgDisplay; // 平均值
|
||||
QDoubleSpinBox *m_outputSpin; // 输出设置
|
||||
QPushButton *m_measureBtn;
|
||||
QPushButton *m_outputBtn;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 直流电压信号模块
|
||||
* 支持0-20V电压输出/测量
|
||||
*/
|
||||
class DCVoltageWidget : public MeasurementModuleBase
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DCVoltageWidget(QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void onMeasureClicked();
|
||||
void onOutputClicked();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
|
||||
QLabel *m_voltageDisplay;
|
||||
QLabel *m_maxDisplay;
|
||||
QLabel *m_minDisplay;
|
||||
QLabel *m_avgDisplay;
|
||||
QDoubleSpinBox *m_outputSpin;
|
||||
QPushButton *m_measureBtn;
|
||||
QPushButton *m_outputBtn;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 频率信号模块
|
||||
* 支持频率测量和方波/正弦波输出
|
||||
*/
|
||||
class FrequencyWidget : public MeasurementModuleBase
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FrequencyWidget(QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void onMeasureClicked();
|
||||
void onOutputClicked();
|
||||
void onWaveTypeChanged(int index);
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
|
||||
QLabel *m_frequencyDisplay;
|
||||
QComboBox *m_waveTypeCombo; // 方波/正弦波
|
||||
QDoubleSpinBox *m_freqSpin; // 频率设置
|
||||
QDoubleSpinBox *m_amplitudeSpin; // 幅值设置
|
||||
QPushButton *m_measureBtn;
|
||||
QPushButton *m_outputBtn;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 220VAC测量模块
|
||||
*/
|
||||
class ACVoltageWidget : public MeasurementModuleBase
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ACVoltageWidget(QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void onMeasureClicked();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
|
||||
QLabel *m_voltageDisplay;
|
||||
QLabel *m_frequencyDisplay;
|
||||
QPushButton *m_measureBtn;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 开关量检测模块
|
||||
*/
|
||||
class SwitchDetectionWidget : public MeasurementModuleBase
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SwitchDetectionWidget(QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void onDetectClicked();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
|
||||
QRadioButton *m_resistanceMode; // 电阻通断模式
|
||||
QRadioButton *m_voltageMode; // 电压模式
|
||||
QLabel *m_statusDisplay; // 通断状态
|
||||
QLabel *m_valueDisplay; // 测量值
|
||||
QPushButton *m_detectBtn;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 信号采集记录模块
|
||||
*/
|
||||
class SignalAcquisitionWidget : public MeasurementModuleBase
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SignalAcquisitionWidget(QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void onStartClicked();
|
||||
void onStopClicked();
|
||||
void onExportClicked();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
|
||||
QComboBox *m_channel1Type; // 通道1类型
|
||||
QComboBox *m_channel2Type; // 通道2类型
|
||||
QDoubleSpinBox *m_rateSpin; // 采集速率
|
||||
QTableWidget *m_dataTable; // 数据表格
|
||||
QPushButton *m_startBtn;
|
||||
QPushButton *m_stopBtn;
|
||||
QPushButton *m_exportBtn;
|
||||
QTimer *m_acquisitionTimer;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 斜波信号输出模块
|
||||
*/
|
||||
class RampSignalWidget : public MeasurementModuleBase
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit RampSignalWidget(QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void onStartClicked();
|
||||
void onStopClicked();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
|
||||
QComboBox *m_signalType; // 电压/电流
|
||||
QDoubleSpinBox *m_startValue; // 起始值
|
||||
QDoubleSpinBox *m_endValue; // 结束值
|
||||
QDoubleSpinBox *m_rampRate; // 斜波速率
|
||||
QLabel *m_currentValueDisplay; // 当前值
|
||||
QProgressBar *m_rampProgress;
|
||||
QPushButton *m_startBtn;
|
||||
QPushButton *m_stopBtn;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 电源纹波检测模块
|
||||
*/
|
||||
class RippleDetectionWidget : public MeasurementModuleBase
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit RippleDetectionWidget(QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void onDetectClicked();
|
||||
void onDiagnoseClicked();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
|
||||
QLabel *m_rippleDisplay; // 纹波值
|
||||
QLabel *m_diagnosisDisplay; // 诊断结果
|
||||
QLabel *m_confidenceDisplay; // 置信度
|
||||
QPushButton *m_detectBtn;
|
||||
QPushButton *m_diagnoseBtn;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 信号测量主页面
|
||||
* 包含所有测量功能模块的导航
|
||||
*/
|
||||
class SignalMeasurementWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SignalMeasurementWidget(QWidget *parent = nullptr);
|
||||
|
||||
void setModule(int moduleIndex);
|
||||
|
||||
signals:
|
||||
void backRequested();
|
||||
|
||||
private slots:
|
||||
void onBackClicked();
|
||||
void onModuleSelected(int index);
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void setupContentArea();
|
||||
|
||||
QStackedWidget *m_contentStack;
|
||||
QLabel *m_titleLabel;
|
||||
int m_currentModuleIndex;
|
||||
|
||||
// 各功能模块
|
||||
RTDMeasurementWidget *m_rtdWidget;
|
||||
RTDOutputWidget *m_rtdOutputWidget;
|
||||
ThermocoupleMeasurementWidget *m_tcWidget;
|
||||
InsulationMeasurementWidget *m_insulationWidget;
|
||||
DCCurrentWidget *m_dcCurrentWidget;
|
||||
DCVoltageWidget *m_dcVoltageWidget;
|
||||
FrequencyWidget *m_frequencyWidget;
|
||||
ACVoltageWidget *m_acVoltageWidget;
|
||||
SwitchDetectionWidget *m_switchWidget;
|
||||
SignalAcquisitionWidget *m_acquisitionWidget;
|
||||
RampSignalWidget *m_rampWidget;
|
||||
RippleDetectionWidget *m_rippleWidget;
|
||||
};
|
||||
|
||||
#endif // SIGNALMEASUREMENTWIDGET_H
|
||||
618
widgets/signaltrimwidget.cpp
Normal file
@@ -0,0 +1,618 @@
|
||||
#include "signaltrimwidget.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QScrollArea>
|
||||
#include <QFrame>
|
||||
#include <cmath>
|
||||
|
||||
static const char *GROUP_STYLE = R"(
|
||||
QGroupBox {
|
||||
background-color: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
margin-top: 8px;
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
left: 16px;
|
||||
padding: 0 8px;
|
||||
color: #333;
|
||||
}
|
||||
)";
|
||||
|
||||
static const char *BUTTON_STYLE = R"(
|
||||
QPushButton {
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 12px 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #1976D2;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #1565C0;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #BDBDBD;
|
||||
}
|
||||
)";
|
||||
|
||||
static const char *TRIM_BUTTON_STYLE = R"(
|
||||
QPushButton {
|
||||
background-color: #ff9800;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 12px 20px;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
min-width: 60px;
|
||||
min-height: 50px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #f57c00;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #ef6c00;
|
||||
}
|
||||
)";
|
||||
|
||||
SignalTrimWidget::SignalTrimWidget(QWidget *parent)
|
||||
: QWidget(parent), m_currentValue(0), m_minValue(-100), m_maxValue(100), m_decimals(4)
|
||||
{
|
||||
setupUI();
|
||||
updateTotalValue();
|
||||
}
|
||||
|
||||
void SignalTrimWidget::setupUI()
|
||||
{
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->setSpacing(0);
|
||||
|
||||
// 标题栏
|
||||
mainLayout->addWidget(createTitleBar());
|
||||
|
||||
// 内容区域
|
||||
QScrollArea *scrollArea = new QScrollArea;
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
scrollArea->setStyleSheet("QScrollArea { background-color: #f5f5f5; }");
|
||||
|
||||
QWidget *contentWidget = new QWidget;
|
||||
contentWidget->setStyleSheet("background-color: #f5f5f5;");
|
||||
QVBoxLayout *contentLayout = new QVBoxLayout(contentWidget);
|
||||
contentLayout->setContentsMargins(20, 20, 20, 20);
|
||||
contentLayout->setSpacing(16);
|
||||
|
||||
// 信号选择面板
|
||||
contentLayout->addWidget(createSignalSelectPanel());
|
||||
|
||||
// 粗调面板
|
||||
contentLayout->addWidget(createCoarseAdjustPanel());
|
||||
|
||||
// 微调面板
|
||||
contentLayout->addWidget(createFineAdjustPanel());
|
||||
|
||||
// 输出面板
|
||||
contentLayout->addWidget(createOutputPanel());
|
||||
|
||||
contentLayout->addStretch();
|
||||
|
||||
scrollArea->setWidget(contentWidget);
|
||||
mainLayout->addWidget(scrollArea, 1);
|
||||
}
|
||||
|
||||
QWidget *SignalTrimWidget::createTitleBar()
|
||||
{
|
||||
QWidget *titleBar = new QWidget;
|
||||
titleBar->setFixedHeight(60);
|
||||
titleBar->setStyleSheet("background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #1976D2, stop:1 #1565C0);");
|
||||
|
||||
QHBoxLayout *layout = new QHBoxLayout(titleBar);
|
||||
layout->setContentsMargins(16, 0, 16, 0);
|
||||
|
||||
// 返回按钮
|
||||
m_backBtn = new QPushButton("← 返回");
|
||||
m_backBtn->setCursor(Qt::PointingHandCursor);
|
||||
m_backBtn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background: rgba(255,255,255,0.2);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
)");
|
||||
connect(m_backBtn, &QPushButton::clicked, this, &SignalTrimWidget::backRequested);
|
||||
layout->addWidget(m_backBtn);
|
||||
|
||||
// 标题(居中)
|
||||
m_titleLabel = new QLabel("信号微调");
|
||||
m_titleLabel->setStyleSheet("color: white; font-size: 18px; font-weight: bold;");
|
||||
m_titleLabel->setAlignment(Qt::AlignCenter);
|
||||
layout->addWidget(m_titleLabel, 1);
|
||||
|
||||
// 右侧占位,保持标题居中
|
||||
QWidget *spacer = new QWidget;
|
||||
spacer->setFixedWidth(m_backBtn->sizeHint().width());
|
||||
layout->addWidget(spacer);
|
||||
|
||||
return titleBar;
|
||||
}
|
||||
|
||||
QWidget *SignalTrimWidget::createSignalSelectPanel()
|
||||
{
|
||||
QGroupBox *group = new QGroupBox("信号选择");
|
||||
group->setStyleSheet(GROUP_STYLE);
|
||||
|
||||
QHBoxLayout *layout = new QHBoxLayout(group);
|
||||
layout->setContentsMargins(16, 24, 16, 16);
|
||||
layout->setSpacing(24);
|
||||
|
||||
// 信号类型
|
||||
QVBoxLayout *typeLayout = new QVBoxLayout;
|
||||
QLabel *typeLabel = new QLabel("信号类型");
|
||||
typeLabel->setStyleSheet("color: #666; font-size: 12px; font-weight: normal;");
|
||||
m_signalTypeCombo = new QComboBox;
|
||||
m_signalTypeCombo->addItems({"直流电压 (DC V)",
|
||||
"直流电流 (DC mA)",
|
||||
"交流电压 (AC V)",
|
||||
"电阻 (Ω)",
|
||||
"热电偶 (°C)",
|
||||
"热电阻 (°C)",
|
||||
"频率 (Hz)"});
|
||||
m_signalTypeCombo->setStyleSheet(R"(
|
||||
QComboBox {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 10px 16px;
|
||||
min-width: 180px;
|
||||
font-size: 14px;
|
||||
}
|
||||
)");
|
||||
connect(m_signalTypeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &SignalTrimWidget::onSignalTypeChanged);
|
||||
typeLayout->addWidget(typeLabel);
|
||||
typeLayout->addWidget(m_signalTypeCombo);
|
||||
layout->addLayout(typeLayout);
|
||||
|
||||
// 单位选择
|
||||
QVBoxLayout *unitLayout = new QVBoxLayout;
|
||||
QLabel *unitLabel = new QLabel("单位");
|
||||
unitLabel->setStyleSheet("color: #666; font-size: 12px; font-weight: normal;");
|
||||
m_unitCombo = new QComboBox;
|
||||
m_unitCombo->addItems({"V", "mV", "µV"});
|
||||
m_unitCombo->setStyleSheet(R"(
|
||||
QComboBox {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 10px 16px;
|
||||
min-width: 100px;
|
||||
font-size: 14px;
|
||||
}
|
||||
)");
|
||||
unitLayout->addWidget(unitLabel);
|
||||
unitLayout->addWidget(m_unitCombo);
|
||||
layout->addLayout(unitLayout);
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
QWidget *SignalTrimWidget::createCoarseAdjustPanel()
|
||||
{
|
||||
QGroupBox *group = new QGroupBox("粗调");
|
||||
group->setStyleSheet(GROUP_STYLE);
|
||||
|
||||
QHBoxLayout *layout = new QHBoxLayout(group);
|
||||
layout->setContentsMargins(16, 24, 16, 16);
|
||||
layout->setSpacing(16);
|
||||
|
||||
QLabel *valueLabel = new QLabel("设定值:");
|
||||
valueLabel->setStyleSheet("font-size: 16px; font-weight: 600;");
|
||||
layout->addWidget(valueLabel);
|
||||
|
||||
m_coarseValueSpin = new QDoubleSpinBox;
|
||||
m_coarseValueSpin->setRange(m_minValue, m_maxValue);
|
||||
m_coarseValueSpin->setValue(0);
|
||||
m_coarseValueSpin->setDecimals(m_decimals);
|
||||
m_coarseValueSpin->setSingleStep(1.0);
|
||||
m_coarseValueSpin->setStyleSheet(R"(
|
||||
QDoubleSpinBox {
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #2196F3;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
min-width: 200px;
|
||||
}
|
||||
QDoubleSpinBox::up-button, QDoubleSpinBox::down-button {
|
||||
subcontrol-origin: border;
|
||||
width: 32px;
|
||||
border-left: 2px solid #2196F3;
|
||||
background: #e3f2fd;
|
||||
}
|
||||
QDoubleSpinBox::up-button { subcontrol-position: top right; border-top-right-radius: 6px; }
|
||||
QDoubleSpinBox::down-button { subcontrol-position: bottom right; border-bottom-right-radius: 6px; }
|
||||
QDoubleSpinBox::up-button:hover, QDoubleSpinBox::down-button:hover { background: #bbdefb; }
|
||||
QDoubleSpinBox::up-arrow { image: url(:/icons/arrow_up.svg); width: 14px; height: 14px; }
|
||||
QDoubleSpinBox::down-arrow { image: url(:/icons/arrow_down.svg); width: 14px; height: 14px; }
|
||||
)");
|
||||
connect(m_coarseValueSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
|
||||
this, &SignalTrimWidget::onCoarseValueChanged);
|
||||
layout->addWidget(m_coarseValueSpin);
|
||||
|
||||
m_coarseUnitLabel = new QLabel("V");
|
||||
m_coarseUnitLabel->setStyleSheet("font-size: 20px; font-weight: bold; color: #2196F3;");
|
||||
layout->addWidget(m_coarseUnitLabel);
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
// 快捷设置按钮
|
||||
QVBoxLayout *presetLayout = new QVBoxLayout;
|
||||
QLabel *presetLabel = new QLabel("快捷设置:");
|
||||
presetLabel->setStyleSheet("color: #666; font-size: 12px;");
|
||||
presetLayout->addWidget(presetLabel);
|
||||
|
||||
QHBoxLayout *presetBtnLayout = new QHBoxLayout;
|
||||
QStringList presets = {"0", "1", "5", "10", "20"};
|
||||
for (const QString &preset : presets)
|
||||
{
|
||||
QPushButton *btn = new QPushButton(preset);
|
||||
btn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #e3f2fd;
|
||||
color: #1976D2;
|
||||
border: 1px solid #90caf9;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #bbdefb;
|
||||
}
|
||||
)");
|
||||
btn->setCursor(Qt::PointingHandCursor);
|
||||
connect(btn, &QPushButton::clicked, this, [this, preset]()
|
||||
{ m_coarseValueSpin->setValue(preset.toDouble()); });
|
||||
presetBtnLayout->addWidget(btn);
|
||||
}
|
||||
presetLayout->addLayout(presetBtnLayout);
|
||||
layout->addLayout(presetLayout);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
QWidget *SignalTrimWidget::createFineAdjustPanel()
|
||||
{
|
||||
QGroupBox *group = new QGroupBox("微调");
|
||||
group->setStyleSheet(GROUP_STYLE);
|
||||
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(group);
|
||||
mainLayout->setContentsMargins(16, 24, 16, 16);
|
||||
mainLayout->setSpacing(16);
|
||||
|
||||
// 步进选择
|
||||
QHBoxLayout *stepLayout = new QHBoxLayout;
|
||||
QLabel *stepLabel = new QLabel("微调步进:");
|
||||
stepLabel->setStyleSheet("font-weight: 600;");
|
||||
stepLayout->addWidget(stepLabel);
|
||||
|
||||
m_stepSizeCombo = new QComboBox;
|
||||
m_stepSizeCombo->addItems({"0.0001", "0.001", "0.01", "0.1", "1"});
|
||||
m_stepSizeCombo->setCurrentIndex(1); // 默认 0.001
|
||||
m_stepSizeCombo->setStyleSheet(R"(
|
||||
QComboBox {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
min-width: 100px;
|
||||
}
|
||||
)");
|
||||
connect(m_stepSizeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, [this](int)
|
||||
{ updateSliderRange(); });
|
||||
stepLayout->addWidget(m_stepSizeCombo);
|
||||
stepLayout->addStretch();
|
||||
mainLayout->addLayout(stepLayout);
|
||||
|
||||
// 微调控制
|
||||
QHBoxLayout *trimLayout = new QHBoxLayout;
|
||||
trimLayout->setSpacing(16);
|
||||
|
||||
// 减少按钮
|
||||
m_decrementBtn = new QPushButton("-");
|
||||
m_decrementBtn->setStyleSheet(TRIM_BUTTON_STYLE);
|
||||
m_decrementBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(m_decrementBtn, &QPushButton::clicked, this, &SignalTrimWidget::onDecrementClicked);
|
||||
trimLayout->addWidget(m_decrementBtn);
|
||||
|
||||
// 微调值显示
|
||||
m_fineValueSpin = new QDoubleSpinBox;
|
||||
m_fineValueSpin->setRange(-10, 10);
|
||||
m_fineValueSpin->setValue(0);
|
||||
m_fineValueSpin->setDecimals(4);
|
||||
m_fineValueSpin->setSingleStep(0.001);
|
||||
m_fineValueSpin->setPrefix("微调: ");
|
||||
m_fineValueSpin->setStyleSheet(R"(
|
||||
QDoubleSpinBox {
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #ff9800;
|
||||
border-radius: 8px;
|
||||
background: #fff3e0;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
min-width: 180px;
|
||||
}
|
||||
QDoubleSpinBox::up-button, QDoubleSpinBox::down-button {
|
||||
subcontrol-origin: border;
|
||||
width: 28px;
|
||||
border-left: 2px solid #ff9800;
|
||||
background: #ffe0b2;
|
||||
}
|
||||
QDoubleSpinBox::up-button { subcontrol-position: top right; border-top-right-radius: 6px; }
|
||||
QDoubleSpinBox::down-button { subcontrol-position: bottom right; border-bottom-right-radius: 6px; }
|
||||
QDoubleSpinBox::up-button:hover, QDoubleSpinBox::down-button:hover { background: #ffcc80; }
|
||||
QDoubleSpinBox::up-arrow { image: url(:/icons/arrow_up.svg); width: 12px; height: 12px; }
|
||||
QDoubleSpinBox::down-arrow { image: url(:/icons/arrow_down.svg); width: 12px; height: 12px; }
|
||||
)");
|
||||
connect(m_fineValueSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
|
||||
this, &SignalTrimWidget::onFineValueChanged);
|
||||
trimLayout->addWidget(m_fineValueSpin);
|
||||
|
||||
// 增加按钮
|
||||
m_incrementBtn = new QPushButton("+");
|
||||
m_incrementBtn->setStyleSheet(TRIM_BUTTON_STYLE);
|
||||
m_incrementBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(m_incrementBtn, &QPushButton::clicked, this, &SignalTrimWidget::onIncrementClicked);
|
||||
trimLayout->addWidget(m_incrementBtn);
|
||||
|
||||
trimLayout->addStretch();
|
||||
mainLayout->addLayout(trimLayout);
|
||||
|
||||
// 滑块微调
|
||||
QHBoxLayout *sliderLayout = new QHBoxLayout;
|
||||
QLabel *minLabel = new QLabel("-1.000");
|
||||
minLabel->setStyleSheet("color: #666; font-family: monospace;");
|
||||
sliderLayout->addWidget(minLabel);
|
||||
|
||||
m_fineSlider = new QSlider(Qt::Horizontal);
|
||||
m_fineSlider->setRange(-1000, 1000);
|
||||
m_fineSlider->setValue(0);
|
||||
m_fineSlider->setStyleSheet(R"(
|
||||
QSlider::groove:horizontal {
|
||||
border: 1px solid #bbb;
|
||||
background: #e0e0e0;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QSlider::handle:horizontal {
|
||||
background: #ff9800;
|
||||
border: 2px solid #f57c00;
|
||||
width: 24px;
|
||||
margin: -8px 0;
|
||||
border-radius: 12px;
|
||||
}
|
||||
QSlider::handle:horizontal:hover {
|
||||
background: #ffa726;
|
||||
}
|
||||
QSlider::sub-page:horizontal {
|
||||
background: #ffcc80;
|
||||
border-radius: 4px;
|
||||
}
|
||||
)");
|
||||
connect(m_fineSlider, &QSlider::valueChanged, this, &SignalTrimWidget::onSliderChanged);
|
||||
sliderLayout->addWidget(m_fineSlider, 1);
|
||||
|
||||
QLabel *maxLabel = new QLabel("+1.000");
|
||||
maxLabel->setStyleSheet("color: #666; font-family: monospace;");
|
||||
sliderLayout->addWidget(maxLabel);
|
||||
|
||||
mainLayout->addLayout(sliderLayout);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
QWidget *SignalTrimWidget::createOutputPanel()
|
||||
{
|
||||
QGroupBox *group = new QGroupBox("最终输出");
|
||||
group->setStyleSheet(GROUP_STYLE);
|
||||
|
||||
QHBoxLayout *layout = new QHBoxLayout(group);
|
||||
layout->setContentsMargins(16, 24, 16, 16);
|
||||
layout->setSpacing(24);
|
||||
|
||||
// 总值显示
|
||||
m_totalValueLabel = new QLabel("0.0000 V");
|
||||
m_totalValueLabel->setStyleSheet(R"(
|
||||
QLabel {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop:0 #1a1a2e, stop:1 #16213e);
|
||||
color: #00ff88;
|
||||
border: 2px solid #00aa55;
|
||||
border-radius: 12px;
|
||||
padding: 20px 40px;
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
font-family: 'Courier New', monospace;
|
||||
min-width: 300px;
|
||||
}
|
||||
)");
|
||||
m_totalValueLabel->setAlignment(Qt::AlignCenter);
|
||||
layout->addWidget(m_totalValueLabel);
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
// 控制按钮
|
||||
QVBoxLayout *btnLayout = new QVBoxLayout;
|
||||
btnLayout->setSpacing(12);
|
||||
|
||||
m_applyBtn = new QPushButton("应用输出");
|
||||
m_applyBtn->setStyleSheet(BUTTON_STYLE);
|
||||
m_applyBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(m_applyBtn, &QPushButton::clicked, this, &SignalTrimWidget::onApplyOutput);
|
||||
btnLayout->addWidget(m_applyBtn);
|
||||
|
||||
m_resetBtn = new QPushButton("重置归零");
|
||||
m_resetBtn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #757575;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 12px 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #616161;
|
||||
}
|
||||
)");
|
||||
m_resetBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(m_resetBtn, &QPushButton::clicked, this, &SignalTrimWidget::onResetValue);
|
||||
btnLayout->addWidget(m_resetBtn);
|
||||
|
||||
layout->addLayout(btnLayout);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
void SignalTrimWidget::onSignalTypeChanged(int index)
|
||||
{
|
||||
// 根据信号类型更新单位选项
|
||||
m_unitCombo->clear();
|
||||
switch (index)
|
||||
{
|
||||
case 0: // DC V
|
||||
m_unitCombo->addItems({"V", "mV", "µV"});
|
||||
m_coarseUnitLabel->setText("V");
|
||||
m_minValue = -100;
|
||||
m_maxValue = 100;
|
||||
m_decimals = 4;
|
||||
break;
|
||||
case 1: // DC mA
|
||||
m_unitCombo->addItems({"mA", "µA"});
|
||||
m_coarseUnitLabel->setText("mA");
|
||||
m_minValue = -30;
|
||||
m_maxValue = 30;
|
||||
m_decimals = 4;
|
||||
break;
|
||||
case 2: // AC V
|
||||
m_unitCombo->addItems({"V", "mV"});
|
||||
m_coarseUnitLabel->setText("V");
|
||||
m_minValue = 0;
|
||||
m_maxValue = 300;
|
||||
m_decimals = 3;
|
||||
break;
|
||||
case 3: // Ω
|
||||
m_unitCombo->addItems({"Ω", "kΩ", "MΩ"});
|
||||
m_coarseUnitLabel->setText("Ω");
|
||||
m_minValue = 0;
|
||||
m_maxValue = 10000;
|
||||
m_decimals = 2;
|
||||
break;
|
||||
case 4: // 热电偶
|
||||
case 5: // 热电阻
|
||||
m_unitCombo->addItems({"°C"});
|
||||
m_coarseUnitLabel->setText("°C");
|
||||
m_minValue = -200;
|
||||
m_maxValue = 1800;
|
||||
m_decimals = 2;
|
||||
break;
|
||||
case 6: // Hz
|
||||
m_unitCombo->addItems({"Hz", "kHz", "MHz"});
|
||||
m_coarseUnitLabel->setText("Hz");
|
||||
m_minValue = 0;
|
||||
m_maxValue = 100000;
|
||||
m_decimals = 2;
|
||||
break;
|
||||
}
|
||||
|
||||
m_coarseValueSpin->setRange(m_minValue, m_maxValue);
|
||||
m_coarseValueSpin->setDecimals(m_decimals);
|
||||
updateTotalValue();
|
||||
}
|
||||
|
||||
void SignalTrimWidget::onCoarseValueChanged(double value)
|
||||
{
|
||||
Q_UNUSED(value)
|
||||
updateTotalValue();
|
||||
}
|
||||
|
||||
void SignalTrimWidget::onFineValueChanged(double value)
|
||||
{
|
||||
// 更新滑块位置
|
||||
m_fineSlider->blockSignals(true);
|
||||
double step = m_stepSizeCombo->currentText().toDouble();
|
||||
m_fineSlider->setValue(static_cast<int>(value / step));
|
||||
m_fineSlider->blockSignals(false);
|
||||
|
||||
updateTotalValue();
|
||||
}
|
||||
|
||||
void SignalTrimWidget::onSliderChanged(int value)
|
||||
{
|
||||
double step = m_stepSizeCombo->currentText().toDouble();
|
||||
m_fineValueSpin->setValue(value * step);
|
||||
}
|
||||
|
||||
void SignalTrimWidget::onIncrementClicked()
|
||||
{
|
||||
double step = m_stepSizeCombo->currentText().toDouble();
|
||||
m_fineValueSpin->setValue(m_fineValueSpin->value() + step);
|
||||
}
|
||||
|
||||
void SignalTrimWidget::onDecrementClicked()
|
||||
{
|
||||
double step = m_stepSizeCombo->currentText().toDouble();
|
||||
m_fineValueSpin->setValue(m_fineValueSpin->value() - step);
|
||||
}
|
||||
|
||||
void SignalTrimWidget::onApplyOutput()
|
||||
{
|
||||
// 应用输出值到设备
|
||||
m_currentValue = m_coarseValueSpin->value() + m_fineValueSpin->value();
|
||||
// TODO: 发送到硬件
|
||||
}
|
||||
|
||||
void SignalTrimWidget::onResetValue()
|
||||
{
|
||||
m_coarseValueSpin->setValue(0);
|
||||
m_fineValueSpin->setValue(0);
|
||||
m_fineSlider->setValue(0);
|
||||
}
|
||||
|
||||
void SignalTrimWidget::updateTotalValue()
|
||||
{
|
||||
double total = m_coarseValueSpin->value() + m_fineValueSpin->value();
|
||||
QString unit = m_coarseUnitLabel->text();
|
||||
m_totalValueLabel->setText(QString("%1 %2").arg(total, 0, 'f', m_decimals).arg(unit));
|
||||
}
|
||||
|
||||
void SignalTrimWidget::updateSliderRange()
|
||||
{
|
||||
double step = m_stepSizeCombo->currentText().toDouble();
|
||||
int range = static_cast<int>(1.0 / step);
|
||||
m_fineSlider->setRange(-range, range);
|
||||
m_fineValueSpin->setSingleStep(step);
|
||||
}
|
||||
81
widgets/signaltrimwidget.h
Normal file
@@ -0,0 +1,81 @@
|
||||
#ifndef SIGNALTRIMWIDGET_H
|
||||
#define SIGNALTRIMWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QComboBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QSlider>
|
||||
#include <QGroupBox>
|
||||
|
||||
/**
|
||||
* @brief 信号微调页面
|
||||
*
|
||||
* 功能说明:
|
||||
* - 输出值任意位数微调
|
||||
* - 精确调节信号输出
|
||||
*/
|
||||
class SignalTrimWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SignalTrimWidget(QWidget *parent = nullptr);
|
||||
~SignalTrimWidget() = default;
|
||||
|
||||
signals:
|
||||
void backRequested();
|
||||
|
||||
private slots:
|
||||
void onSignalTypeChanged(int index);
|
||||
void onCoarseValueChanged(double value);
|
||||
void onFineValueChanged(double value);
|
||||
void onSliderChanged(int value);
|
||||
void onIncrementClicked();
|
||||
void onDecrementClicked();
|
||||
void onApplyOutput();
|
||||
void onResetValue();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
QWidget *createTitleBar();
|
||||
QWidget *createSignalSelectPanel();
|
||||
QWidget *createCoarseAdjustPanel();
|
||||
QWidget *createFineAdjustPanel();
|
||||
QWidget *createOutputPanel();
|
||||
void updateTotalValue();
|
||||
void updateSliderRange();
|
||||
|
||||
// 标题栏
|
||||
QPushButton *m_backBtn;
|
||||
QLabel *m_titleLabel;
|
||||
|
||||
// 信号选择
|
||||
QComboBox *m_signalTypeCombo;
|
||||
QComboBox *m_unitCombo;
|
||||
|
||||
// 粗调
|
||||
QDoubleSpinBox *m_coarseValueSpin;
|
||||
QLabel *m_coarseUnitLabel;
|
||||
|
||||
// 微调
|
||||
QDoubleSpinBox *m_fineValueSpin;
|
||||
QSlider *m_fineSlider;
|
||||
QPushButton *m_incrementBtn;
|
||||
QPushButton *m_decrementBtn;
|
||||
QComboBox *m_stepSizeCombo;
|
||||
|
||||
// 输出显示
|
||||
QLabel *m_totalValueLabel;
|
||||
QPushButton *m_applyBtn;
|
||||
QPushButton *m_resetBtn;
|
||||
|
||||
// 当前值
|
||||
double m_currentValue;
|
||||
double m_minValue;
|
||||
double m_maxValue;
|
||||
int m_decimals;
|
||||
};
|
||||
|
||||
#endif // SIGNALTRIMWIDGET_H
|
||||
156
widgets/statusbar.cpp
Normal file
@@ -0,0 +1,156 @@
|
||||
#include "statusbar.h"
|
||||
#include <QHBoxLayout>
|
||||
#include <QDateTime>
|
||||
#include <QPalette>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
|
||||
StatusBar::StatusBar(QWidget *parent)
|
||||
: QWidget(parent), m_batteryLevel(100), m_networkConnected(true), m_networkType("WiFi")
|
||||
{
|
||||
setFixedHeight(40);
|
||||
setupUI();
|
||||
|
||||
// 定时更新时间
|
||||
m_timer = new QTimer(this);
|
||||
connect(m_timer, &QTimer::timeout, this, &StatusBar::updateTime);
|
||||
m_timer->start(1000);
|
||||
updateTime();
|
||||
|
||||
// 定时更新电池和WiFi状态(每10秒)
|
||||
QTimer *statusTimer = new QTimer(this);
|
||||
connect(statusTimer, &QTimer::timeout, this, &StatusBar::updateBatteryLevel);
|
||||
connect(statusTimer, &QTimer::timeout, this, &StatusBar::updateNetworkStatus);
|
||||
statusTimer->start(10000);
|
||||
updateBatteryLevel();
|
||||
updateNetworkStatus();
|
||||
}
|
||||
|
||||
StatusBar::~StatusBar()
|
||||
{
|
||||
}
|
||||
|
||||
void StatusBar::setupUI()
|
||||
{
|
||||
// 透明背景 - Android风格
|
||||
setAttribute(Qt::WA_TranslucentBackground);
|
||||
setStyleSheet("background: transparent;");
|
||||
|
||||
QHBoxLayout *layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(24, 8, 24, 8);
|
||||
layout->setSpacing(20);
|
||||
|
||||
// 标题 - 使用白色带阴影效果
|
||||
m_titleLabel = new QLabel("智能校验仪", this);
|
||||
m_titleLabel->setStyleSheet(R"(
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
background: transparent;
|
||||
)");
|
||||
layout->addWidget(m_titleLabel);
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
// 网络状态
|
||||
m_networkLabel = new QLabel("\u25CF", this); // 实心圆点
|
||||
m_networkLabel->setStyleSheet("font-size: 16px; color: rgba(255,255,255,0.9); background: transparent;");
|
||||
layout->addWidget(m_networkLabel);
|
||||
|
||||
// 电池状态
|
||||
m_batteryLabel = new QLabel("100%", this);
|
||||
m_batteryLabel->setStyleSheet("font-size: 13px; color: rgba(255,255,255,0.9); background: transparent;");
|
||||
layout->addWidget(m_batteryLabel);
|
||||
|
||||
// 时间
|
||||
m_timeLabel = new QLabel(this);
|
||||
m_timeLabel->setStyleSheet("font-size: 13px; min-width: 50px; color: rgba(255,255,255,0.9); background: transparent;");
|
||||
layout->addWidget(m_timeLabel);
|
||||
}
|
||||
|
||||
void StatusBar::setTitle(const QString &title)
|
||||
{
|
||||
m_titleLabel->setText(title);
|
||||
}
|
||||
|
||||
void StatusBar::setBatteryLevel(int level)
|
||||
{
|
||||
m_batteryLevel = qBound(0, level, 100);
|
||||
QString icon = m_batteryLevel > 20 ? "🔋" : "🪫";
|
||||
m_batteryLabel->setText(QString("%1 %2%").arg(icon).arg(m_batteryLevel));
|
||||
}
|
||||
|
||||
void StatusBar::setNetworkStatus(bool connected, const QString &type)
|
||||
{
|
||||
m_networkConnected = connected;
|
||||
m_networkType = type;
|
||||
|
||||
QString icon;
|
||||
if (!connected)
|
||||
{
|
||||
icon = "📵";
|
||||
}
|
||||
else if (type == "4G")
|
||||
{
|
||||
icon = "📶";
|
||||
}
|
||||
else
|
||||
{
|
||||
icon = "📶";
|
||||
}
|
||||
m_networkLabel->setText(icon);
|
||||
}
|
||||
|
||||
void StatusBar::updateTime()
|
||||
{
|
||||
QString timeStr = QDateTime::currentDateTime().toString("HH:mm");
|
||||
m_timeLabel->setText(timeStr);
|
||||
}
|
||||
|
||||
int StatusBar::getBatteryPercentage()
|
||||
{
|
||||
QProcess process;
|
||||
process.start("pmset", QStringList() << "-g" << "batt");
|
||||
process.waitForFinished(1000);
|
||||
QString output = process.readAllStandardOutput();
|
||||
|
||||
// 解析输出,格式如:Now drawing from 'Battery Power' -InternalBattery-0 (id=123456) 85%; discharging; 3:45 remaining
|
||||
QRegularExpression re("(\\d+)%");
|
||||
QRegularExpressionMatch match = re.match(output);
|
||||
if (match.hasMatch())
|
||||
{
|
||||
return match.captured(1).toInt();
|
||||
}
|
||||
return 100; // 默认值
|
||||
}
|
||||
|
||||
bool StatusBar::isWiFiConnected()
|
||||
{
|
||||
QProcess process;
|
||||
process.start("/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport", QStringList() << "-I");
|
||||
process.waitForFinished(1000);
|
||||
QString output = process.readAllStandardOutput();
|
||||
|
||||
// 如果输出包含SSID,说明WiFi已连接
|
||||
return output.contains("SSID:");
|
||||
}
|
||||
|
||||
void StatusBar::updateBatteryLevel()
|
||||
{
|
||||
int level = getBatteryPercentage();
|
||||
setBatteryLevel(level);
|
||||
}
|
||||
|
||||
void StatusBar::updateNetworkStatus()
|
||||
{
|
||||
bool connected = isWiFiConnected();
|
||||
|
||||
if (connected)
|
||||
{
|
||||
m_networkLabel->setText("\u25CF"); // 实心圆点表示已连接
|
||||
}
|
||||
else
|
||||
{
|
||||
m_networkLabel->setText("\u25CB"); // 空心圆点表示未连接
|
||||
}
|
||||
}
|
||||
41
widgets/statusbar.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#ifndef STATUSBAR_H
|
||||
#define STATUSBAR_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QLabel>
|
||||
#include <QTimer>
|
||||
|
||||
class StatusBar : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit StatusBar(QWidget *parent = nullptr);
|
||||
~StatusBar();
|
||||
|
||||
void setTitle(const QString &title);
|
||||
void setBatteryLevel(int level);
|
||||
void setNetworkStatus(bool connected, const QString &type = "WiFi");
|
||||
|
||||
private slots:
|
||||
void updateTime();
|
||||
void updateBatteryLevel();
|
||||
void updateNetworkStatus();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
int getBatteryPercentage();
|
||||
bool isWiFiConnected();
|
||||
|
||||
QLabel *m_titleLabel;
|
||||
QLabel *m_timeLabel;
|
||||
QLabel *m_batteryLabel;
|
||||
QLabel *m_networkLabel;
|
||||
QTimer *m_timer;
|
||||
|
||||
int m_batteryLevel;
|
||||
bool m_networkConnected;
|
||||
QString m_networkType;
|
||||
};
|
||||
|
||||
#endif // STATUSBAR_H
|
||||
361
widgets/tableoverlaywidget.cpp
Normal file
@@ -0,0 +1,361 @@
|
||||
#include "tableoverlaywidget.h"
|
||||
#include <QPropertyAnimation>
|
||||
#include <QGraphicsOpacityEffect>
|
||||
#include <QScrollBar>
|
||||
#include <QDebug>
|
||||
|
||||
TableOverlayWidget::TableOverlayWidget(QWidget *parent)
|
||||
: QWidget(parent), m_isVisible(false)
|
||||
{
|
||||
setupUI();
|
||||
hide(); // 初始隐藏
|
||||
}
|
||||
|
||||
void TableOverlayWidget::setupUI()
|
||||
{
|
||||
// 设置浮层属性
|
||||
setAttribute(Qt::WA_StyledBackground);
|
||||
setStyleSheet("TableOverlayWidget { background: transparent; }");
|
||||
|
||||
// 主布局
|
||||
auto *mainLayout = new QHBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->setSpacing(0);
|
||||
|
||||
// 左侧标签栏滚动区域
|
||||
m_tabScrollArea = new QScrollArea(this);
|
||||
m_tabScrollArea->setFixedWidth(130);
|
||||
m_tabScrollArea->setWidgetResizable(true);
|
||||
m_tabScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_tabScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
m_tabScrollArea->setStyleSheet(
|
||||
"QScrollArea { "
|
||||
" background: #34495e; "
|
||||
" border-right: 1px solid #2c3e50; "
|
||||
" border: none; "
|
||||
"}"
|
||||
"QScrollBar:vertical { "
|
||||
" background: #2c3e50; "
|
||||
" width: 10px; "
|
||||
" margin: 0px; "
|
||||
"}"
|
||||
"QScrollBar::handle:vertical { "
|
||||
" background: #546e7a; "
|
||||
" min-height: 20px; "
|
||||
" border-radius: 5px; "
|
||||
"}"
|
||||
"QScrollBar::handle:vertical:hover { "
|
||||
" background: #607d8b; "
|
||||
"}"
|
||||
"QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { "
|
||||
" height: 0px; "
|
||||
"}"
|
||||
"QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { "
|
||||
" background: none; "
|
||||
"}");
|
||||
|
||||
// 标签栏容器
|
||||
m_tabBar = new QWidget();
|
||||
m_tabBar->setStyleSheet(
|
||||
"QWidget { "
|
||||
" background: #34495e; "
|
||||
"}");
|
||||
|
||||
m_tabLayout = new QVBoxLayout(m_tabBar);
|
||||
m_tabLayout->setContentsMargins(8, 12, 8, 12);
|
||||
m_tabLayout->setSpacing(6);
|
||||
m_tabLayout->setAlignment(Qt::AlignTop);
|
||||
|
||||
m_tabScrollArea->setWidget(m_tabBar);
|
||||
mainLayout->addWidget(m_tabScrollArea);
|
||||
|
||||
// 内容容器(右侧)
|
||||
m_container = new QWidget(this);
|
||||
m_container->setStyleSheet(
|
||||
"QWidget { "
|
||||
" background: #f5f5f5; "
|
||||
" border: none; "
|
||||
" border-top-right-radius: 10px; "
|
||||
" border-bottom-right-radius: 10px; "
|
||||
"}");
|
||||
|
||||
auto *containerLayout = new QVBoxLayout(m_container);
|
||||
containerLayout->setContentsMargins(0, 0, 0, 0);
|
||||
containerLayout->setSpacing(0);
|
||||
|
||||
// 标题栏
|
||||
auto *headerWidget = new QWidget(this);
|
||||
headerWidget->setStyleSheet(
|
||||
"QWidget { "
|
||||
" background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #3498db, stop:1 #2980b9); "
|
||||
" border-top-left-radius: 0px; "
|
||||
" border-top-right-radius: 10px; "
|
||||
"}");
|
||||
auto *headerLayout = new QHBoxLayout(headerWidget);
|
||||
headerLayout->setContentsMargins(20, 10, 10, 10);
|
||||
headerLayout->setSpacing(8);
|
||||
|
||||
m_titleLabel = new QLabel("表格", this);
|
||||
m_titleLabel->setStyleSheet(
|
||||
"QLabel { "
|
||||
" color: white; "
|
||||
" font-size: 14pt; "
|
||||
" font-weight: bold; "
|
||||
" background: transparent; "
|
||||
"}");
|
||||
headerLayout->addWidget(m_titleLabel);
|
||||
headerLayout->addStretch();
|
||||
|
||||
// 关闭按钮 - 正方形圆角,图标居中
|
||||
m_closeBtn = new QPushButton("×", this);
|
||||
m_closeBtn->setFixedSize(32, 32);
|
||||
m_closeBtn->setCursor(Qt::PointingHandCursor);
|
||||
m_closeBtn->setStyleSheet(
|
||||
"QPushButton { "
|
||||
" background: rgba(255,255,255,0.2); "
|
||||
" color: white; "
|
||||
" border: none; "
|
||||
" border-radius: 6px; "
|
||||
" font-size: 20px; "
|
||||
" font-weight: normal; "
|
||||
" text-align: center; "
|
||||
" padding: 0px; "
|
||||
" margin: 0px; "
|
||||
"}"
|
||||
"QPushButton:hover { background: rgba(255,255,255,0.35); }"
|
||||
"QPushButton:pressed { background: rgba(255,255,255,0.5); }");
|
||||
connect(m_closeBtn, &QPushButton::clicked, this, &TableOverlayWidget::onCloseButtonClicked);
|
||||
headerLayout->addWidget(m_closeBtn);
|
||||
|
||||
containerLayout->addWidget(headerWidget);
|
||||
|
||||
// 内容堆栈
|
||||
m_contentStack = new QStackedWidget(this);
|
||||
m_contentStack->setStyleSheet(
|
||||
"QStackedWidget { "
|
||||
" background: white; "
|
||||
" border: none; "
|
||||
"}");
|
||||
containerLayout->addWidget(m_contentStack, 1);
|
||||
|
||||
mainLayout->addWidget(m_container, 1);
|
||||
|
||||
// 设置默认大小(占父容器的70%宽度,90%高度)
|
||||
setMinimumWidth(600);
|
||||
setMaximumWidth(1200);
|
||||
}
|
||||
|
||||
void TableOverlayWidget::addTable(const QString &tableId, const QString &tableName, QWidget *tableWidget)
|
||||
{
|
||||
if (m_tables.contains(tableId))
|
||||
{
|
||||
qDebug() << "Table already exists:" << tableId;
|
||||
return;
|
||||
}
|
||||
|
||||
TableTab tab;
|
||||
tab.id = tableId;
|
||||
tab.name = tableName;
|
||||
tab.widget = tableWidget;
|
||||
|
||||
// 创建标签按钮
|
||||
tab.tabButton = new QPushButton(tableName, m_tabBar);
|
||||
tab.tabButton->setFixedHeight(48);
|
||||
tab.tabButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
tab.tabButton->setProperty("tableId", tableId);
|
||||
tab.tabButton->setCursor(Qt::PointingHandCursor);
|
||||
tab.tabButton->setStyleSheet(
|
||||
"QPushButton {"
|
||||
" background: #455a64;"
|
||||
" color: #ecf0f1;"
|
||||
" border: none;"
|
||||
" border-radius: 6px;"
|
||||
" padding: 6px 6px;"
|
||||
" font-size: 9.5pt;"
|
||||
" font-weight: 500;"
|
||||
" text-align: center;"
|
||||
" border-left: 3px solid transparent;"
|
||||
"}"
|
||||
"QPushButton:hover {"
|
||||
" background: #546e7a;"
|
||||
" color: white;"
|
||||
"}"
|
||||
"QPushButton:checked {"
|
||||
" background: #2980b9;"
|
||||
" color: white;"
|
||||
" font-weight: bold;"
|
||||
" border-left: 3px solid #1976d2;"
|
||||
"}"
|
||||
"QPushButton:pressed {"
|
||||
" background: #21618c;"
|
||||
"}");
|
||||
tab.tabButton->setCheckable(true);
|
||||
connect(tab.tabButton, &QPushButton::clicked, this, &TableOverlayWidget::onTabButtonClicked);
|
||||
|
||||
// 添加到布局
|
||||
m_tabLayout->addWidget(tab.tabButton);
|
||||
|
||||
// 添加到内容堆栈
|
||||
m_contentStack->addWidget(tableWidget);
|
||||
|
||||
m_tables[tableId] = tab;
|
||||
|
||||
// 如果是第一个表格,自动显示
|
||||
if (m_tables.size() == 1)
|
||||
{
|
||||
showTable(tableId);
|
||||
}
|
||||
}
|
||||
|
||||
void TableOverlayWidget::showTable(const QString &tableId)
|
||||
{
|
||||
if (!m_tables.contains(tableId))
|
||||
{
|
||||
qDebug() << "Table not found:" << tableId;
|
||||
return;
|
||||
}
|
||||
|
||||
// 取消之前的选中状态
|
||||
if (!m_currentTableId.isEmpty() && m_tables.contains(m_currentTableId))
|
||||
{
|
||||
m_tables[m_currentTableId].tabButton->setChecked(false);
|
||||
}
|
||||
|
||||
// 切换到新表格
|
||||
m_currentTableId = tableId;
|
||||
const TableTab &tab = m_tables[tableId];
|
||||
|
||||
tab.tabButton->setChecked(true);
|
||||
m_contentStack->setCurrentWidget(tab.widget);
|
||||
m_titleLabel->setText(tab.name);
|
||||
|
||||
emit tableChanged(tableId);
|
||||
}
|
||||
|
||||
void TableOverlayWidget::removeTable(const QString &tableId)
|
||||
{
|
||||
if (!m_tables.contains(tableId))
|
||||
return;
|
||||
|
||||
TableTab tab = m_tables.take(tableId);
|
||||
|
||||
// 移除按钮
|
||||
m_tabLayout->removeWidget(tab.tabButton);
|
||||
tab.tabButton->deleteLater();
|
||||
|
||||
// 移除内容
|
||||
m_contentStack->removeWidget(tab.widget);
|
||||
|
||||
// 如果删除的是当前表格,切换到第一个
|
||||
if (m_currentTableId == tableId && !m_tables.isEmpty())
|
||||
{
|
||||
showTable(m_tables.firstKey());
|
||||
}
|
||||
else if (m_tables.isEmpty())
|
||||
{
|
||||
m_currentTableId.clear();
|
||||
hideOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
void TableOverlayWidget::clearTables()
|
||||
{
|
||||
// 清除所有表格
|
||||
for (const QString &tableId : m_tables.keys())
|
||||
{
|
||||
removeTable(tableId);
|
||||
}
|
||||
m_currentTableId.clear();
|
||||
}
|
||||
|
||||
void TableOverlayWidget::showOverlay()
|
||||
{
|
||||
if (m_isVisible || m_tables.isEmpty())
|
||||
return;
|
||||
|
||||
// 如果当前没有选中的表格,显示第一个
|
||||
if (m_currentTableId.isEmpty() && !m_tables.isEmpty())
|
||||
{
|
||||
showTable(m_tables.firstKey());
|
||||
}
|
||||
|
||||
m_isVisible = true;
|
||||
m_container->show();
|
||||
m_tabBar->show();
|
||||
show();
|
||||
animateShow();
|
||||
}
|
||||
|
||||
void TableOverlayWidget::hideOverlay()
|
||||
{
|
||||
if (!m_isVisible)
|
||||
return;
|
||||
|
||||
m_isVisible = false;
|
||||
animateHide();
|
||||
}
|
||||
|
||||
void TableOverlayWidget::toggleOverlay()
|
||||
{
|
||||
if (m_isVisible)
|
||||
hideOverlay();
|
||||
else
|
||||
showOverlay();
|
||||
}
|
||||
|
||||
void TableOverlayWidget::onTabButtonClicked()
|
||||
{
|
||||
QPushButton *btn = qobject_cast<QPushButton *>(sender());
|
||||
if (!btn)
|
||||
return;
|
||||
|
||||
QString tableId = btn->property("tableId").toString();
|
||||
if (!tableId.isEmpty())
|
||||
{
|
||||
showTable(tableId);
|
||||
}
|
||||
}
|
||||
|
||||
void TableOverlayWidget::onCloseButtonClicked()
|
||||
{
|
||||
hideOverlay();
|
||||
emit closed();
|
||||
}
|
||||
|
||||
void TableOverlayWidget::updateTabButtons()
|
||||
{
|
||||
for (const TableTab &tab : m_tables)
|
||||
{
|
||||
tab.tabButton->setChecked(tab.id == m_currentTableId);
|
||||
}
|
||||
}
|
||||
|
||||
void TableOverlayWidget::animateShow()
|
||||
{
|
||||
// 淡入动画
|
||||
QGraphicsOpacityEffect *effect = new QGraphicsOpacityEffect(this);
|
||||
setGraphicsEffect(effect);
|
||||
|
||||
QPropertyAnimation *animation = new QPropertyAnimation(effect, "opacity");
|
||||
animation->setDuration(200);
|
||||
animation->setStartValue(0.0);
|
||||
animation->setEndValue(1.0);
|
||||
animation->setEasingCurve(QEasingCurve::OutCubic);
|
||||
animation->start(QPropertyAnimation::DeleteWhenStopped);
|
||||
}
|
||||
|
||||
void TableOverlayWidget::animateHide()
|
||||
{
|
||||
// 淡出动画
|
||||
QGraphicsOpacityEffect *effect = new QGraphicsOpacityEffect(this);
|
||||
setGraphicsEffect(effect);
|
||||
|
||||
QPropertyAnimation *animation = new QPropertyAnimation(effect, "opacity");
|
||||
animation->setDuration(150);
|
||||
animation->setStartValue(1.0);
|
||||
animation->setEndValue(0.0);
|
||||
animation->setEasingCurve(QEasingCurve::InCubic);
|
||||
|
||||
connect(animation, &QPropertyAnimation::finished, this, &TableOverlayWidget::hide);
|
||||
animation->start(QPropertyAnimation::DeleteWhenStopped);
|
||||
}
|
||||
111
widgets/tableoverlaywidget.h
Normal file
@@ -0,0 +1,111 @@
|
||||
#ifndef TABLEOVERLAYWIDGET_H
|
||||
#define TABLEOVERLAYWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QStackedWidget>
|
||||
#include <QMap>
|
||||
#include <QScrollArea>
|
||||
#include <QLabel>
|
||||
#include "../procedure/proceduredata.h"
|
||||
|
||||
/**
|
||||
* @class TableOverlayWidget
|
||||
* @brief 表格浮层组件
|
||||
*
|
||||
* 在页面上显示浮层,支持多个表格的标签页切换
|
||||
* 特点:
|
||||
* - 浮层形式,不阻塞主界面
|
||||
* - 左侧垂直标签页导航
|
||||
* - 可关闭
|
||||
*/
|
||||
class TableOverlayWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TableOverlayWidget(QWidget *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief 添加表格到浮层
|
||||
* @param tableId 表格ID
|
||||
* @param tableName 表格名称
|
||||
* @param tableWidget 表格组件
|
||||
*/
|
||||
void addTable(const QString &tableId, const QString &tableName, QWidget *tableWidget);
|
||||
|
||||
/**
|
||||
* @brief 显示特定表格
|
||||
* @param tableId 表格ID
|
||||
*/
|
||||
void showTable(const QString &tableId);
|
||||
|
||||
/**
|
||||
* @brief 移除表格
|
||||
* @param tableId 表格ID
|
||||
*/
|
||||
void removeTable(const QString &tableId);
|
||||
|
||||
/**
|
||||
* @brief 清除所有表格
|
||||
*/
|
||||
void clearTables();
|
||||
|
||||
/**
|
||||
* @brief 显示浮层
|
||||
*/
|
||||
void showOverlay();
|
||||
|
||||
/**
|
||||
* @brief 隐藏浮层
|
||||
*/
|
||||
void hideOverlay();
|
||||
|
||||
/**
|
||||
* @brief 切换浮层显示状态
|
||||
*/
|
||||
void toggleOverlay();
|
||||
|
||||
/**
|
||||
* @brief 是否显示
|
||||
*/
|
||||
bool isOverlayVisible() const { return m_isVisible; }
|
||||
|
||||
signals:
|
||||
void closed();
|
||||
void tableChanged(const QString &tableId);
|
||||
|
||||
private slots:
|
||||
void onTabButtonClicked();
|
||||
void onCloseButtonClicked();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void updateTabButtons();
|
||||
void animateShow();
|
||||
void animateHide();
|
||||
|
||||
struct TableTab
|
||||
{
|
||||
QString id;
|
||||
QString name;
|
||||
QWidget *widget;
|
||||
QPushButton *tabButton;
|
||||
};
|
||||
|
||||
QWidget *m_container; // 浮层容器
|
||||
QScrollArea *m_tabScrollArea; // 标签栏滚动区域
|
||||
QWidget *m_tabBar; // 标签栏容器
|
||||
QVBoxLayout *m_tabLayout; // 标签栏布局
|
||||
QStackedWidget *m_contentStack; // 内容堆栈
|
||||
QPushButton *m_closeBtn; // 关闭按钮
|
||||
QLabel *m_titleLabel; // 标题标签
|
||||
|
||||
QMap<QString, TableTab> m_tables; // 表格映射
|
||||
QString m_currentTableId; // 当前显示的表格ID
|
||||
bool m_isVisible; // 是否可见
|
||||
};
|
||||
|
||||
#endif // TABLEOVERLAYWIDGET_H
|
||||
1407
widgets/tablewidgetfactory.cpp
Normal file
200
widgets/tablewidgetfactory.h
Normal file
@@ -0,0 +1,200 @@
|
||||
#ifndef TABLEWIDGETFACTORY_H
|
||||
#define TABLEWIDGETFACTORY_H
|
||||
|
||||
#include "../procedure/proceduredata.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QTableWidget>
|
||||
#include <QVariant>
|
||||
#include <QWidget>
|
||||
|
||||
/**
|
||||
* @class TableWidgetFactory
|
||||
* @brief 表格組件工廠類
|
||||
*
|
||||
* 根據 YAML 配置動態創建不同類型的表格組件:
|
||||
* - Grid 表格:數據矩陣(行列形式)
|
||||
* - Form 表格:表單字段佈局
|
||||
* - Series 表格:時序數據記錄
|
||||
*/
|
||||
class TableWidgetFactory : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TableWidgetFactory(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief 創建表格組件
|
||||
* @param tableDef 表格定義
|
||||
* @return 創建的表格組件
|
||||
*/
|
||||
QWidget *createTableWidget(const TableDefinition &tableDef);
|
||||
|
||||
// =====================================================
|
||||
// Form 表格數據訪問
|
||||
// =====================================================
|
||||
|
||||
/**
|
||||
* @brief 設置字段值
|
||||
* @param tableWidget 表格組件
|
||||
* @param fieldId 字段ID
|
||||
* @param value 值
|
||||
* @return 是否成功
|
||||
*/
|
||||
bool setFieldValue(QWidget *tableWidget, const QString &fieldId, const QVariant &value);
|
||||
|
||||
/**
|
||||
* @brief 獲取字段值
|
||||
* @param tableWidget 表格組件
|
||||
* @param fieldId 字段ID
|
||||
* @return 字段值
|
||||
*/
|
||||
QVariant getFieldValue(QWidget *tableWidget, const QString &fieldId) const;
|
||||
|
||||
// =====================================================
|
||||
// Grid 表格數據訪問
|
||||
// =====================================================
|
||||
|
||||
/**
|
||||
* @brief 設置單元格值
|
||||
* @param tableWidget 表格組件
|
||||
* @param rowId 行ID
|
||||
* @param columnId 列ID
|
||||
* @param value 值
|
||||
* @return 是否成功
|
||||
*/
|
||||
bool setCellValue(QWidget *tableWidget, const QString &rowId,
|
||||
const QString &columnId, const QVariant &value);
|
||||
|
||||
/**
|
||||
* @brief 獲取單元格值
|
||||
* @param tableWidget 表格組件
|
||||
* @param rowId 行ID
|
||||
* @param columnId 列ID
|
||||
* @return 單元格值
|
||||
*/
|
||||
QVariant getCellValue(QWidget *tableWidget, const QString &rowId,
|
||||
const QString &columnId) const;
|
||||
|
||||
// =====================================================
|
||||
// 通用方法
|
||||
// =====================================================
|
||||
|
||||
/**
|
||||
* @brief 獲取表格定義
|
||||
* @param tableWidget 表格組件
|
||||
* @return 表格定義
|
||||
*/
|
||||
TableDefinition getTableDefinition(QWidget *tableWidget) const;
|
||||
|
||||
/**
|
||||
* @brief 從引擎加載數據到表格
|
||||
* @param tableWidget 表格組件
|
||||
* @param tableRef 表格引用ID
|
||||
* @param engineData 引擎數據
|
||||
* @return 是否成功
|
||||
*/
|
||||
bool loadDataFromEngine(QWidget *tableWidget, const QString &tableRef,
|
||||
const QVariantMap &engineData);
|
||||
|
||||
// =====================================================
|
||||
// Series 表格特有方法
|
||||
// =====================================================
|
||||
|
||||
/**
|
||||
* @brief 添加時序數據點
|
||||
* @param tableWidget 表格組件
|
||||
* @param timestamp 時間戳
|
||||
* @param fieldValues 字段值映射
|
||||
* @return 是否成功
|
||||
*/
|
||||
bool addSeriesDataPoint(QWidget *tableWidget, const QDateTime ×tamp,
|
||||
const QVariantMap &fieldValues);
|
||||
|
||||
/**
|
||||
* @brief 清除時序數據
|
||||
* @param tableWidget 表格組件
|
||||
*/
|
||||
void clearSeriesData(QWidget *tableWidget);
|
||||
|
||||
// =====================================================
|
||||
// 驗證與控制
|
||||
// =====================================================
|
||||
|
||||
/**
|
||||
* @brief 驗證字段
|
||||
* @param tableWidget 表格組件
|
||||
* @param fieldId 字段ID
|
||||
* @return 是否有效
|
||||
*/
|
||||
bool validateField(QWidget *tableWidget, const QString &fieldId) const;
|
||||
|
||||
/**
|
||||
* @brief 啟用/禁用手動輸入
|
||||
* @param tableWidget 表格組件
|
||||
* @param enabled 是否啟用
|
||||
*/
|
||||
void enableManualInput(QWidget *tableWidget, bool enabled);
|
||||
|
||||
signals:
|
||||
/**
|
||||
* @brief 字段值變更信號(Form 表格)
|
||||
*/
|
||||
void fieldValueChanged(QWidget *tableWidget, const QString &fieldId,
|
||||
const QVariant &value);
|
||||
|
||||
/**
|
||||
* @brief 單元格值變更信號(Grid 表格)
|
||||
*/
|
||||
void cellValueChanged(QWidget *tableWidget, const QString &rowId,
|
||||
const QString &columnId, const QVariant &value);
|
||||
|
||||
private:
|
||||
// =====================================================
|
||||
// 表格創建方法
|
||||
// =====================================================
|
||||
|
||||
QWidget *createGridTable(const TableDefinition &tableDef);
|
||||
QWidget *createFormTable(const TableDefinition &tableDef);
|
||||
QWidget *createSeriesTable(const TableDefinition &tableDef);
|
||||
|
||||
// =====================================================
|
||||
// 字段組件創建方法
|
||||
// =====================================================
|
||||
|
||||
QWidget *createFieldWidget(const FieldDefinition &fieldDef, QWidget *parent);
|
||||
QWidget *createNumericField(const FieldDefinition &fieldDef, QWidget *parent);
|
||||
QWidget *createTextField(const FieldDefinition &fieldDef, QWidget *parent);
|
||||
QWidget *createSelectionField(const FieldDefinition &fieldDef, QWidget *parent);
|
||||
QWidget *createBooleanField(const FieldDefinition &fieldDef, QWidget *parent);
|
||||
QWidget *createDateTimeField(const FieldDefinition &fieldDef, QWidget *parent);
|
||||
QWidget *createCalculatedField(const FieldDefinition &fieldDef, QWidget *parent);
|
||||
|
||||
// =====================================================
|
||||
// 輔助方法
|
||||
// =====================================================
|
||||
|
||||
void connectFieldSignals(QWidget *fieldWidget, const QString &fieldId,
|
||||
QWidget *tableWidget);
|
||||
void applyStaticCells(QWidget *tableWidget, const TableDefinition &tableDef);
|
||||
|
||||
// =====================================================
|
||||
// 內部數據結構
|
||||
// =====================================================
|
||||
|
||||
struct TableWidgetData
|
||||
{
|
||||
TableDefinition definition;
|
||||
QMap<QString, QWidget *> fieldWidgets; // fieldId -> widget (for form/series)
|
||||
QTableWidget *gridWidget = nullptr; // for grid tables
|
||||
QTableWidget *seriesWidget = nullptr; // for series tables
|
||||
};
|
||||
|
||||
QMap<QWidget *, TableWidgetData> tableData;
|
||||
};
|
||||
|
||||
#endif // TABLEWIDGETFACTORY_H
|
||||
617
widgets/waveformwidget.cpp
Normal file
@@ -0,0 +1,617 @@
|
||||
#include "waveformwidget.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHeaderView>
|
||||
#include <QScrollArea>
|
||||
#include <QPainter>
|
||||
#include <QDateTime>
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
|
||||
static const char *GROUP_STYLE = R"(
|
||||
QGroupBox {
|
||||
background-color: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
margin-top: 8px;
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
left: 16px;
|
||||
padding: 0 8px;
|
||||
color: #333;
|
||||
}
|
||||
)";
|
||||
|
||||
static const char *BUTTON_STYLE = R"(
|
||||
QPushButton {
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 12px 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #1976D2;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #1565C0;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #BDBDBD;
|
||||
}
|
||||
)";
|
||||
|
||||
static const char *STOP_BUTTON_STYLE = R"(
|
||||
QPushButton {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 12px 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #d32f2f;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #c62828;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #BDBDBD;
|
||||
}
|
||||
)";
|
||||
|
||||
WaveformWidget::WaveformWidget(QWidget *parent)
|
||||
: QWidget(parent), m_isCapturing(false), m_dataIndex(0)
|
||||
{
|
||||
setupUI();
|
||||
|
||||
m_captureTimer = new QTimer(this);
|
||||
connect(m_captureTimer, &QTimer::timeout, this, &WaveformWidget::onTimerTick);
|
||||
}
|
||||
|
||||
void WaveformWidget::setupUI()
|
||||
{
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->setSpacing(0);
|
||||
|
||||
// 标题栏
|
||||
mainLayout->addWidget(createTitleBar());
|
||||
|
||||
// 内容区域
|
||||
QScrollArea *scrollArea = new QScrollArea;
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
scrollArea->setStyleSheet("QScrollArea { background-color: #f5f5f5; }");
|
||||
|
||||
QWidget *contentWidget = new QWidget;
|
||||
contentWidget->setStyleSheet("background-color: #f5f5f5;");
|
||||
QVBoxLayout *contentLayout = new QVBoxLayout(contentWidget);
|
||||
contentLayout->setContentsMargins(20, 20, 20, 20);
|
||||
contentLayout->setSpacing(16);
|
||||
|
||||
// 配置面板
|
||||
contentLayout->addWidget(createConfigPanel());
|
||||
|
||||
// 波形显示区域
|
||||
contentLayout->addWidget(createWaveformDisplay());
|
||||
|
||||
// 底部区域:数据表格和统计
|
||||
QHBoxLayout *bottomLayout = new QHBoxLayout;
|
||||
bottomLayout->setSpacing(16);
|
||||
bottomLayout->addWidget(createDataPanel(), 2);
|
||||
bottomLayout->addWidget(createStatisticsPanel(), 1);
|
||||
contentLayout->addLayout(bottomLayout);
|
||||
|
||||
scrollArea->setWidget(contentWidget);
|
||||
mainLayout->addWidget(scrollArea, 1);
|
||||
}
|
||||
|
||||
QWidget *WaveformWidget::createTitleBar()
|
||||
{
|
||||
QWidget *titleBar = new QWidget;
|
||||
titleBar->setFixedHeight(60);
|
||||
titleBar->setStyleSheet("background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #1976D2, stop:1 #1565C0);");
|
||||
|
||||
QHBoxLayout *layout = new QHBoxLayout(titleBar);
|
||||
layout->setContentsMargins(16, 0, 16, 0);
|
||||
|
||||
// 返回按钮
|
||||
m_backBtn = new QPushButton("← 返回");
|
||||
m_backBtn->setCursor(Qt::PointingHandCursor);
|
||||
m_backBtn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background: rgba(255,255,255,0.2);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
)");
|
||||
connect(m_backBtn, &QPushButton::clicked, this, &WaveformWidget::backRequested);
|
||||
layout->addWidget(m_backBtn);
|
||||
|
||||
// 标题(居中)
|
||||
m_titleLabel = new QLabel("波形采集");
|
||||
m_titleLabel->setStyleSheet("color: white; font-size: 18px; font-weight: bold;");
|
||||
m_titleLabel->setAlignment(Qt::AlignCenter);
|
||||
layout->addWidget(m_titleLabel, 1);
|
||||
|
||||
// 右侧占位,保持标题居中
|
||||
QWidget *spacer = new QWidget;
|
||||
spacer->setFixedWidth(m_backBtn->sizeHint().width());
|
||||
layout->addWidget(spacer);
|
||||
|
||||
return titleBar;
|
||||
}
|
||||
|
||||
QWidget *WaveformWidget::createConfigPanel()
|
||||
{
|
||||
QGroupBox *group = new QGroupBox("采集配置");
|
||||
group->setStyleSheet(GROUP_STYLE);
|
||||
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(group);
|
||||
mainLayout->setContentsMargins(16, 24, 16, 16);
|
||||
mainLayout->setSpacing(12);
|
||||
|
||||
// 第一行:配置选项
|
||||
QHBoxLayout *configLayout = new QHBoxLayout;
|
||||
configLayout->setSpacing(16);
|
||||
|
||||
// 信号类型
|
||||
QVBoxLayout *typeLayout = new QVBoxLayout;
|
||||
QLabel *typeLabel = new QLabel("信号类型");
|
||||
typeLabel->setStyleSheet("color: #666; font-size: 12px; font-weight: normal;");
|
||||
m_signalTypeCombo = new QComboBox;
|
||||
m_signalTypeCombo->addItems({"直流电压", "直流电流", "交流电压", "交流电流"});
|
||||
m_signalTypeCombo->setStyleSheet(R"(
|
||||
QComboBox {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
min-width: 100px;
|
||||
}
|
||||
)");
|
||||
typeLayout->addWidget(typeLabel);
|
||||
typeLayout->addWidget(m_signalTypeCombo);
|
||||
configLayout->addLayout(typeLayout);
|
||||
|
||||
// 采样率
|
||||
QVBoxLayout *rateLayout = new QVBoxLayout;
|
||||
QLabel *rateLabel = new QLabel("采样率");
|
||||
rateLabel->setStyleSheet("color: #666; font-size: 12px; font-weight: normal;");
|
||||
m_sampleRateCombo = new QComboBox;
|
||||
m_sampleRateCombo->addItems({"25ms/点", "50ms/点", "100ms/点", "200ms/点", "500ms/点", "1s/点"});
|
||||
m_sampleRateCombo->setStyleSheet(R"(
|
||||
QComboBox {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
min-width: 80px;
|
||||
}
|
||||
)");
|
||||
rateLayout->addWidget(rateLabel);
|
||||
rateLayout->addWidget(m_sampleRateCombo);
|
||||
configLayout->addLayout(rateLayout);
|
||||
|
||||
// 采集时长
|
||||
QVBoxLayout *durationLayout = new QVBoxLayout;
|
||||
QLabel *durationLabel = new QLabel("时长(秒)");
|
||||
durationLabel->setStyleSheet("color: #666; font-size: 12px; font-weight: normal;");
|
||||
m_durationSpin = new QSpinBox;
|
||||
m_durationSpin->setRange(1, 3600);
|
||||
m_durationSpin->setValue(60);
|
||||
m_durationSpin->setStyleSheet(R"(
|
||||
QSpinBox {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
min-width: 60px;
|
||||
}
|
||||
QSpinBox::up-button, QSpinBox::down-button {
|
||||
subcontrol-origin: border;
|
||||
width: 20px;
|
||||
border-left: 1px solid #ddd;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
QSpinBox::up-button { subcontrol-position: top right; border-top-right-radius: 5px; }
|
||||
QSpinBox::down-button { subcontrol-position: bottom right; border-bottom-right-radius: 5px; }
|
||||
QSpinBox::up-button:hover, QSpinBox::down-button:hover { background: #e0e0e0; }
|
||||
QSpinBox::up-arrow { image: url(:/icons/arrow_up.svg); width: 10px; height: 10px; }
|
||||
QSpinBox::down-arrow { image: url(:/icons/arrow_down.svg); width: 10px; height: 10px; }
|
||||
)");
|
||||
;
|
||||
durationLayout->addWidget(durationLabel);
|
||||
durationLayout->addWidget(m_durationSpin);
|
||||
configLayout->addLayout(durationLayout);
|
||||
|
||||
// 通道选择
|
||||
QVBoxLayout *channelLayout = new QVBoxLayout;
|
||||
QLabel *channelLabel = new QLabel("采集通道");
|
||||
channelLabel->setStyleSheet("color: #666; font-size: 12px; font-weight: normal;");
|
||||
QHBoxLayout *checkLayout = new QHBoxLayout;
|
||||
checkLayout->setSpacing(8);
|
||||
m_ch1Enable = new QCheckBox("CH1");
|
||||
m_ch1Enable->setChecked(true);
|
||||
m_ch2Enable = new QCheckBox("CH2");
|
||||
m_ch2Enable->setChecked(true);
|
||||
checkLayout->addWidget(m_ch1Enable);
|
||||
checkLayout->addWidget(m_ch2Enable);
|
||||
channelLayout->addWidget(channelLabel);
|
||||
channelLayout->addLayout(checkLayout);
|
||||
configLayout->addLayout(channelLayout);
|
||||
|
||||
configLayout->addStretch();
|
||||
mainLayout->addLayout(configLayout);
|
||||
|
||||
// 第二行:控制按钮
|
||||
QHBoxLayout *buttonLayout = new QHBoxLayout;
|
||||
buttonLayout->setSpacing(12);
|
||||
|
||||
m_startBtn = new QPushButton("开始采集");
|
||||
m_startBtn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QPushButton:hover { background-color: #1976D2; }
|
||||
QPushButton:disabled { background-color: #BDBDBD; }
|
||||
)");
|
||||
m_startBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(m_startBtn, &QPushButton::clicked, this, &WaveformWidget::onStartCapture);
|
||||
buttonLayout->addWidget(m_startBtn);
|
||||
|
||||
m_stopBtn = new QPushButton("停止");
|
||||
m_stopBtn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QPushButton:hover { background-color: #d32f2f; }
|
||||
QPushButton:disabled { background-color: #BDBDBD; }
|
||||
)");
|
||||
m_stopBtn->setCursor(Qt::PointingHandCursor);
|
||||
m_stopBtn->setEnabled(false);
|
||||
connect(m_stopBtn, &QPushButton::clicked, this, &WaveformWidget::onStopCapture);
|
||||
buttonLayout->addWidget(m_stopBtn);
|
||||
|
||||
m_clearBtn = new QPushButton("清除");
|
||||
m_clearBtn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #757575;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QPushButton:hover { background-color: #616161; }
|
||||
)");
|
||||
m_clearBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(m_clearBtn, &QPushButton::clicked, this, &WaveformWidget::onClearData);
|
||||
buttonLayout->addWidget(m_clearBtn);
|
||||
|
||||
m_exportBtn = new QPushButton("导出");
|
||||
m_exportBtn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QPushButton:hover { background-color: #43A047; }
|
||||
)");
|
||||
m_exportBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(m_exportBtn, &QPushButton::clicked, this, &WaveformWidget::onExportData);
|
||||
buttonLayout->addWidget(m_exportBtn);
|
||||
|
||||
buttonLayout->addStretch();
|
||||
mainLayout->addLayout(buttonLayout);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
QWidget *WaveformWidget::createWaveformDisplay()
|
||||
{
|
||||
QGroupBox *group = new QGroupBox("波形显示");
|
||||
group->setStyleSheet(GROUP_STYLE);
|
||||
group->setMinimumHeight(250);
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout(group);
|
||||
layout->setContentsMargins(16, 24, 16, 16);
|
||||
|
||||
// 波形显示区域(简化版,使用渐变背景模拟)
|
||||
m_waveformArea = new QWidget;
|
||||
m_waveformArea->setMinimumHeight(180);
|
||||
m_waveformArea->setStyleSheet(R"(
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop:0 #1a1a2e, stop:1 #16213e);
|
||||
border-radius: 8px;
|
||||
)");
|
||||
|
||||
QHBoxLayout *waveLayout = new QHBoxLayout(m_waveformArea);
|
||||
waveLayout->setContentsMargins(20, 20, 20, 20);
|
||||
|
||||
// 通道标签
|
||||
QVBoxLayout *labelLayout = new QVBoxLayout;
|
||||
m_ch1Label = new QLabel("CH1: -- V");
|
||||
m_ch1Label->setStyleSheet("color: #00ff88; font-size: 16px; font-weight: bold; font-family: monospace;");
|
||||
m_ch2Label = new QLabel("CH2: -- V");
|
||||
m_ch2Label->setStyleSheet("color: #ff6b6b; font-size: 16px; font-weight: bold; font-family: monospace;");
|
||||
labelLayout->addWidget(m_ch1Label);
|
||||
labelLayout->addWidget(m_ch2Label);
|
||||
labelLayout->addStretch();
|
||||
waveLayout->addLayout(labelLayout);
|
||||
|
||||
waveLayout->addStretch();
|
||||
|
||||
// 状态提示
|
||||
QLabel *hintLabel = new QLabel("点击\"开始采集\"开始记录波形数据");
|
||||
hintLabel->setStyleSheet("color: #888; font-size: 14px;");
|
||||
hintLabel->setAlignment(Qt::AlignCenter);
|
||||
waveLayout->addWidget(hintLabel);
|
||||
|
||||
waveLayout->addStretch();
|
||||
|
||||
layout->addWidget(m_waveformArea);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
QWidget *WaveformWidget::createDataPanel()
|
||||
{
|
||||
QGroupBox *group = new QGroupBox("采集数据");
|
||||
group->setStyleSheet(GROUP_STYLE);
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout(group);
|
||||
layout->setContentsMargins(16, 24, 16, 16);
|
||||
|
||||
m_dataTable = new QTableWidget;
|
||||
m_dataTable->setColumnCount(4);
|
||||
m_dataTable->setHorizontalHeaderLabels({"序号", "时间", "通道1", "通道2"});
|
||||
m_dataTable->horizontalHeader()->setStretchLastSection(true);
|
||||
m_dataTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
||||
m_dataTable->setStyleSheet(R"(
|
||||
QTableWidget {
|
||||
background-color: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
gridline-color: #f0f0f0;
|
||||
}
|
||||
QHeaderView::section {
|
||||
background-color: #f5f5f5;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-weight: 600;
|
||||
}
|
||||
)");
|
||||
m_dataTable->setMinimumHeight(200);
|
||||
layout->addWidget(m_dataTable);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
QWidget *WaveformWidget::createStatisticsPanel()
|
||||
{
|
||||
QGroupBox *group = new QGroupBox("统计信息");
|
||||
group->setStyleSheet(GROUP_STYLE);
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout(group);
|
||||
layout->setContentsMargins(16, 24, 16, 16);
|
||||
layout->setSpacing(12);
|
||||
|
||||
// 通道1统计
|
||||
QLabel *ch1Title = new QLabel("通道1 (绿色)");
|
||||
ch1Title->setStyleSheet("color: #00aa55; font-weight: bold;");
|
||||
layout->addWidget(ch1Title);
|
||||
|
||||
QHBoxLayout *ch1MaxLayout = new QHBoxLayout;
|
||||
ch1MaxLayout->addWidget(new QLabel("最大值:"));
|
||||
m_ch1MaxLabel = new QLabel("-- V");
|
||||
m_ch1MaxLabel->setStyleSheet("font-weight: bold; color: #333;");
|
||||
ch1MaxLayout->addWidget(m_ch1MaxLabel);
|
||||
ch1MaxLayout->addStretch();
|
||||
layout->addLayout(ch1MaxLayout);
|
||||
|
||||
QHBoxLayout *ch1MinLayout = new QHBoxLayout;
|
||||
ch1MinLayout->addWidget(new QLabel("最小值:"));
|
||||
m_ch1MinLabel = new QLabel("-- V");
|
||||
m_ch1MinLabel->setStyleSheet("font-weight: bold; color: #333;");
|
||||
ch1MinLayout->addWidget(m_ch1MinLabel);
|
||||
ch1MinLayout->addStretch();
|
||||
layout->addLayout(ch1MinLayout);
|
||||
|
||||
QHBoxLayout *ch1AvgLayout = new QHBoxLayout;
|
||||
ch1AvgLayout->addWidget(new QLabel("平均值:"));
|
||||
m_ch1AvgLabel = new QLabel("-- V");
|
||||
m_ch1AvgLabel->setStyleSheet("font-weight: bold; color: #333;");
|
||||
ch1AvgLayout->addWidget(m_ch1AvgLabel);
|
||||
ch1AvgLayout->addStretch();
|
||||
layout->addLayout(ch1AvgLayout);
|
||||
|
||||
layout->addSpacing(16);
|
||||
|
||||
// 通道2统计
|
||||
QLabel *ch2Title = new QLabel("通道2 (红色)");
|
||||
ch2Title->setStyleSheet("color: #d32f2f; font-weight: bold;");
|
||||
layout->addWidget(ch2Title);
|
||||
|
||||
QHBoxLayout *ch2MaxLayout = new QHBoxLayout;
|
||||
ch2MaxLayout->addWidget(new QLabel("最大值:"));
|
||||
m_ch2MaxLabel = new QLabel("-- V");
|
||||
m_ch2MaxLabel->setStyleSheet("font-weight: bold; color: #333;");
|
||||
ch2MaxLayout->addWidget(m_ch2MaxLabel);
|
||||
ch2MaxLayout->addStretch();
|
||||
layout->addLayout(ch2MaxLayout);
|
||||
|
||||
QHBoxLayout *ch2MinLayout = new QHBoxLayout;
|
||||
ch2MinLayout->addWidget(new QLabel("最小值:"));
|
||||
m_ch2MinLabel = new QLabel("-- V");
|
||||
m_ch2MinLabel->setStyleSheet("font-weight: bold; color: #333;");
|
||||
ch2MinLayout->addWidget(m_ch2MinLabel);
|
||||
ch2MinLayout->addStretch();
|
||||
layout->addLayout(ch2MinLayout);
|
||||
|
||||
QHBoxLayout *ch2AvgLayout = new QHBoxLayout;
|
||||
ch2AvgLayout->addWidget(new QLabel("平均值:"));
|
||||
m_ch2AvgLabel = new QLabel("-- V");
|
||||
m_ch2AvgLabel->setStyleSheet("font-weight: bold; color: #333;");
|
||||
ch2AvgLayout->addWidget(m_ch2AvgLabel);
|
||||
ch2AvgLayout->addStretch();
|
||||
layout->addLayout(ch2AvgLayout);
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
void WaveformWidget::onStartCapture()
|
||||
{
|
||||
m_isCapturing = true;
|
||||
m_startBtn->setEnabled(false);
|
||||
m_stopBtn->setEnabled(true);
|
||||
m_dataIndex = 0;
|
||||
|
||||
// 根据采样率设置定时器间隔
|
||||
QStringList rates = {"25", "50", "100", "200", "500", "1000"};
|
||||
int interval = rates[m_sampleRateCombo->currentIndex()].toInt();
|
||||
m_captureTimer->start(interval);
|
||||
}
|
||||
|
||||
void WaveformWidget::onStopCapture()
|
||||
{
|
||||
m_isCapturing = false;
|
||||
m_captureTimer->stop();
|
||||
m_startBtn->setEnabled(true);
|
||||
m_stopBtn->setEnabled(false);
|
||||
}
|
||||
|
||||
void WaveformWidget::onClearData()
|
||||
{
|
||||
m_ch1Data.clear();
|
||||
m_ch2Data.clear();
|
||||
m_dataTable->setRowCount(0);
|
||||
m_dataIndex = 0;
|
||||
|
||||
m_ch1Label->setText("CH1: -- V");
|
||||
m_ch2Label->setText("CH2: -- V");
|
||||
m_ch1MaxLabel->setText("-- V");
|
||||
m_ch1MinLabel->setText("-- V");
|
||||
m_ch1AvgLabel->setText("-- V");
|
||||
m_ch2MaxLabel->setText("-- V");
|
||||
m_ch2MinLabel->setText("-- V");
|
||||
m_ch2AvgLabel->setText("-- V");
|
||||
}
|
||||
|
||||
void WaveformWidget::onExportData()
|
||||
{
|
||||
if (m_ch1Data.isEmpty() && m_ch2Data.isEmpty())
|
||||
{
|
||||
QMessageBox::warning(this, "导出", "没有可导出的数据");
|
||||
return;
|
||||
}
|
||||
|
||||
QString fileName = QFileDialog::getSaveFileName(this, "导出数据",
|
||||
QString("waveform_%1.csv").arg(QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss")),
|
||||
"CSV文件 (*.csv)");
|
||||
|
||||
if (!fileName.isEmpty())
|
||||
{
|
||||
QMessageBox::information(this, "导出", "数据已导出到:\n" + fileName);
|
||||
}
|
||||
}
|
||||
|
||||
void WaveformWidget::onTimerTick()
|
||||
{
|
||||
// 模拟采集数据
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
static std::normal_distribution<> dis(5.0, 0.5);
|
||||
|
||||
double ch1Value = m_ch1Enable->isChecked() ? dis(gen) : 0;
|
||||
double ch2Value = m_ch2Enable->isChecked() ? dis(gen) + 2.0 : 0;
|
||||
|
||||
addDataPoint(ch1Value, ch2Value);
|
||||
}
|
||||
|
||||
void WaveformWidget::addDataPoint(double ch1Value, double ch2Value)
|
||||
{
|
||||
m_dataIndex++;
|
||||
|
||||
if (m_ch1Enable->isChecked())
|
||||
m_ch1Data.append(ch1Value);
|
||||
if (m_ch2Enable->isChecked())
|
||||
m_ch2Data.append(ch2Value);
|
||||
|
||||
// 更新实时显示
|
||||
m_ch1Label->setText(QString("CH1: %1 V").arg(ch1Value, 0, 'f', 4));
|
||||
m_ch2Label->setText(QString("CH2: %1 V").arg(ch2Value, 0, 'f', 4));
|
||||
|
||||
// 添加到表格
|
||||
int row = m_dataTable->rowCount();
|
||||
m_dataTable->insertRow(row);
|
||||
m_dataTable->setItem(row, 0, new QTableWidgetItem(QString::number(m_dataIndex)));
|
||||
m_dataTable->setItem(row, 1, new QTableWidgetItem(QDateTime::currentDateTime().toString("HH:mm:ss.zzz")));
|
||||
m_dataTable->setItem(row, 2, new QTableWidgetItem(QString::number(ch1Value, 'f', 4)));
|
||||
m_dataTable->setItem(row, 3, new QTableWidgetItem(QString::number(ch2Value, 'f', 4)));
|
||||
m_dataTable->scrollToBottom();
|
||||
|
||||
// 更新统计
|
||||
updateStatistics();
|
||||
}
|
||||
|
||||
void WaveformWidget::updateStatistics()
|
||||
{
|
||||
if (!m_ch1Data.isEmpty())
|
||||
{
|
||||
double maxVal = *std::max_element(m_ch1Data.begin(), m_ch1Data.end());
|
||||
double minVal = *std::min_element(m_ch1Data.begin(), m_ch1Data.end());
|
||||
double avgVal = std::accumulate(m_ch1Data.begin(), m_ch1Data.end(), 0.0) / m_ch1Data.size();
|
||||
m_ch1MaxLabel->setText(QString("%1 V").arg(maxVal, 0, 'f', 4));
|
||||
m_ch1MinLabel->setText(QString("%1 V").arg(minVal, 0, 'f', 4));
|
||||
m_ch1AvgLabel->setText(QString("%1 V").arg(avgVal, 0, 'f', 4));
|
||||
}
|
||||
|
||||
if (!m_ch2Data.isEmpty())
|
||||
{
|
||||
double maxVal = *std::max_element(m_ch2Data.begin(), m_ch2Data.end());
|
||||
double minVal = *std::min_element(m_ch2Data.begin(), m_ch2Data.end());
|
||||
double avgVal = std::accumulate(m_ch2Data.begin(), m_ch2Data.end(), 0.0) / m_ch2Data.size();
|
||||
m_ch2MaxLabel->setText(QString("%1 V").arg(maxVal, 0, 'f', 4));
|
||||
m_ch2MinLabel->setText(QString("%1 V").arg(minVal, 0, 'f', 4));
|
||||
m_ch2AvgLabel->setText(QString("%1 V").arg(avgVal, 0, 'f', 4));
|
||||
}
|
||||
}
|
||||
95
widgets/waveformwidget.h
Normal file
@@ -0,0 +1,95 @@
|
||||
#ifndef WAVEFORMWIDGET_H
|
||||
#define WAVEFORMWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QComboBox>
|
||||
#include <QSpinBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QTableWidget>
|
||||
#include <QTimer>
|
||||
#include <QVector>
|
||||
#include <QCheckBox>
|
||||
|
||||
/**
|
||||
* @brief 波形采集页面
|
||||
*
|
||||
* 功能说明:
|
||||
* - 信号采集和记录
|
||||
* - 生成曲线功能
|
||||
* - 采集速率≤25ms/点
|
||||
* - 采集通道数≥2
|
||||
* - 采集电压/电流信号
|
||||
* - 自动计算最大/最小/平均值
|
||||
*/
|
||||
class WaveformWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit WaveformWidget(QWidget *parent = nullptr);
|
||||
~WaveformWidget() = default;
|
||||
|
||||
signals:
|
||||
void backRequested();
|
||||
|
||||
private slots:
|
||||
void onStartCapture();
|
||||
void onStopCapture();
|
||||
void onClearData();
|
||||
void onExportData();
|
||||
void onTimerTick();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
QWidget *createTitleBar();
|
||||
QWidget *createConfigPanel();
|
||||
QWidget *createWaveformDisplay();
|
||||
QWidget *createDataPanel();
|
||||
QWidget *createStatisticsPanel();
|
||||
void updateStatistics();
|
||||
void addDataPoint(double ch1Value, double ch2Value);
|
||||
|
||||
// 标题栏
|
||||
QPushButton *m_backBtn;
|
||||
QLabel *m_titleLabel;
|
||||
|
||||
// 配置面板
|
||||
QComboBox *m_signalTypeCombo;
|
||||
QComboBox *m_sampleRateCombo;
|
||||
QSpinBox *m_durationSpin;
|
||||
QCheckBox *m_ch1Enable;
|
||||
QCheckBox *m_ch2Enable;
|
||||
|
||||
// 控制按钮
|
||||
QPushButton *m_startBtn;
|
||||
QPushButton *m_stopBtn;
|
||||
QPushButton *m_clearBtn;
|
||||
QPushButton *m_exportBtn;
|
||||
|
||||
// 波形显示
|
||||
QWidget *m_waveformArea;
|
||||
QLabel *m_ch1Label;
|
||||
QLabel *m_ch2Label;
|
||||
|
||||
// 数据表格
|
||||
QTableWidget *m_dataTable;
|
||||
|
||||
// 统计信息
|
||||
QLabel *m_ch1MaxLabel;
|
||||
QLabel *m_ch1MinLabel;
|
||||
QLabel *m_ch1AvgLabel;
|
||||
QLabel *m_ch2MaxLabel;
|
||||
QLabel *m_ch2MinLabel;
|
||||
QLabel *m_ch2AvgLabel;
|
||||
|
||||
// 采集状态
|
||||
QTimer *m_captureTimer;
|
||||
bool m_isCapturing;
|
||||
QVector<double> m_ch1Data;
|
||||
QVector<double> m_ch2Data;
|
||||
int m_dataIndex;
|
||||
};
|
||||
|
||||
#endif // WAVEFORMWIDGET_H
|
||||
790
widgets/wirelesswidget.cpp
Normal file
@@ -0,0 +1,790 @@
|
||||
#include "wirelesswidget.h"
|
||||
#include "overlaydialogswidget.h"
|
||||
#include <QScrollArea>
|
||||
#include <QDebug>
|
||||
|
||||
// 样式常量定义
|
||||
const QString WirelessWidget::BUTTON_STYLE = R"(
|
||||
QPushButton {
|
||||
background-color: #f5f5f5;
|
||||
color: #333333;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
min-height: 36px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #e8e8e8;
|
||||
border-color: #ccc;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #d0d0d0;
|
||||
}
|
||||
)";
|
||||
|
||||
const QString WirelessWidget::PRIMARY_BUTTON_STYLE = R"(
|
||||
QPushButton {
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
min-height: 36px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #1976D2;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #1565C0;
|
||||
}
|
||||
)";
|
||||
|
||||
const QString WirelessWidget::DANGER_BUTTON_STYLE = R"(
|
||||
QPushButton {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
min-height: 36px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #d32f2f;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #c62828;
|
||||
}
|
||||
)";
|
||||
|
||||
const QString WirelessWidget::INPUT_STYLE = R"(
|
||||
QLineEdit, QComboBox {
|
||||
background-color: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
min-height: 36px;
|
||||
}
|
||||
QLineEdit:focus, QComboBox:focus {
|
||||
border-color: #2196F3;
|
||||
}
|
||||
QComboBox::drop-down {
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: center right;
|
||||
width: 30px;
|
||||
border: none;
|
||||
}
|
||||
QComboBox::down-arrow {
|
||||
image: url(:/icons/arrow_down.svg);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
)";
|
||||
|
||||
const QString WirelessWidget::GROUP_STYLE = R"(
|
||||
QGroupBox {
|
||||
background-color: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
left: 16px;
|
||||
padding: 0 8px;
|
||||
color: #333;
|
||||
}
|
||||
)";
|
||||
|
||||
const QString WirelessWidget::TAB_STYLE = R"(
|
||||
QPushButton {
|
||||
background-color: transparent;
|
||||
color: #666;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
padding: 12px 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
min-width: 100px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
color: #2196F3;
|
||||
background-color: rgba(33, 150, 243, 0.05);
|
||||
}
|
||||
)";
|
||||
|
||||
const QString WirelessWidget::TAB_ACTIVE_STYLE = R"(
|
||||
QPushButton {
|
||||
background-color: transparent;
|
||||
color: #2196F3;
|
||||
border: none;
|
||||
border-bottom: 2px solid #2196F3;
|
||||
padding: 12px 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
min-width: 100px;
|
||||
}
|
||||
)";
|
||||
|
||||
WirelessWidget::WirelessWidget(QWidget *parent)
|
||||
: QWidget(parent), m_currentTab(0)
|
||||
{
|
||||
setupUI();
|
||||
|
||||
// 初始化状态刷新定时器
|
||||
m_statusTimer = new QTimer(this);
|
||||
connect(m_statusTimer, &QTimer::timeout, this, &WirelessWidget::onRefreshStatus);
|
||||
m_statusTimer->start(5000); // 每5秒刷新一次
|
||||
|
||||
// 初始刷新
|
||||
onRefreshStatus();
|
||||
}
|
||||
|
||||
void WirelessWidget::setupUI()
|
||||
{
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->setSpacing(0);
|
||||
|
||||
// 标题栏
|
||||
mainLayout->addWidget(createTitleBar());
|
||||
|
||||
// Tab栏
|
||||
mainLayout->addWidget(createTabBar());
|
||||
|
||||
// 内容区域(滚动区域)
|
||||
QScrollArea *scrollArea = new QScrollArea;
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
scrollArea->setStyleSheet("QScrollArea { background-color: #f5f5f5; }");
|
||||
|
||||
m_contentStack = new QStackedWidget;
|
||||
m_contentStack->addWidget(createWifiPanel());
|
||||
m_contentStack->addWidget(create4GPanel());
|
||||
m_contentStack->addWidget(createStatusPanel());
|
||||
|
||||
scrollArea->setWidget(m_contentStack);
|
||||
mainLayout->addWidget(scrollArea, 1);
|
||||
|
||||
// 默认显示WiFi面板
|
||||
onTabChanged(0);
|
||||
}
|
||||
|
||||
QWidget *WirelessWidget::createTitleBar()
|
||||
{
|
||||
QWidget *titleBar = new QWidget;
|
||||
titleBar->setFixedHeight(60);
|
||||
titleBar->setStyleSheet("background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #1976D2, stop:1 #1565C0);");
|
||||
|
||||
QHBoxLayout *layout = new QHBoxLayout(titleBar);
|
||||
layout->setContentsMargins(16, 0, 16, 0);
|
||||
|
||||
// 返回按钮
|
||||
m_backBtn = new QPushButton("← 返回");
|
||||
m_backBtn->setCursor(Qt::PointingHandCursor);
|
||||
m_backBtn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background: rgba(255,255,255,0.2);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
)");
|
||||
connect(m_backBtn, &QPushButton::clicked, this, &WirelessWidget::backRequested);
|
||||
layout->addWidget(m_backBtn);
|
||||
|
||||
// 标题(居中)
|
||||
m_titleLabel = new QLabel("无线通讯");
|
||||
m_titleLabel->setStyleSheet("color: white; font-size: 18px; font-weight: bold;");
|
||||
m_titleLabel->setAlignment(Qt::AlignCenter);
|
||||
layout->addWidget(m_titleLabel, 1);
|
||||
|
||||
// 右侧占位,保持标题居中
|
||||
QWidget *spacer = new QWidget;
|
||||
spacer->setFixedWidth(m_backBtn->sizeHint().width());
|
||||
layout->addWidget(spacer);
|
||||
|
||||
return titleBar;
|
||||
}
|
||||
|
||||
QWidget *WirelessWidget::createTabBar()
|
||||
{
|
||||
QWidget *tabBar = new QWidget;
|
||||
tabBar->setFixedHeight(48);
|
||||
tabBar->setStyleSheet("background-color: white; border-bottom: 1px solid #e0e0e0;");
|
||||
|
||||
QHBoxLayout *layout = new QHBoxLayout(tabBar);
|
||||
layout->setContentsMargins(16, 0, 16, 0);
|
||||
layout->setSpacing(0);
|
||||
|
||||
QStringList tabNames = {"WiFi", "4G网络", "连接状态"};
|
||||
for (int i = 0; i < tabNames.size(); ++i)
|
||||
{
|
||||
QPushButton *tabBtn = new QPushButton(tabNames[i]);
|
||||
tabBtn->setStyleSheet(i == 0 ? TAB_ACTIVE_STYLE : TAB_STYLE);
|
||||
tabBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(tabBtn, &QPushButton::clicked, this, [this, i]()
|
||||
{ onTabChanged(i); });
|
||||
layout->addWidget(tabBtn);
|
||||
m_tabButtons.append(tabBtn);
|
||||
}
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
return tabBar;
|
||||
}
|
||||
|
||||
QWidget *WirelessWidget::createWifiPanel()
|
||||
{
|
||||
QWidget *panel = new QWidget;
|
||||
panel->setStyleSheet("background-color: #f5f5f5;");
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout(panel);
|
||||
layout->setContentsMargins(20, 20, 20, 20);
|
||||
layout->setSpacing(16);
|
||||
|
||||
// WiFi列表区域
|
||||
QGroupBox *listGroup = new QGroupBox("可用网络");
|
||||
listGroup->setStyleSheet(GROUP_STYLE);
|
||||
QVBoxLayout *listLayout = new QVBoxLayout(listGroup);
|
||||
listLayout->setContentsMargins(16, 24, 16, 16);
|
||||
listLayout->setSpacing(12);
|
||||
|
||||
// 扫描按钮
|
||||
QHBoxLayout *scanLayout = new QHBoxLayout;
|
||||
m_wifiScanBtn = new QPushButton("扫描网络");
|
||||
m_wifiScanBtn->setStyleSheet(PRIMARY_BUTTON_STYLE);
|
||||
m_wifiScanBtn->setFixedWidth(120);
|
||||
m_wifiScanBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(m_wifiScanBtn, &QPushButton::clicked, this, &WirelessWidget::onWifiScan);
|
||||
scanLayout->addWidget(m_wifiScanBtn);
|
||||
scanLayout->addStretch();
|
||||
listLayout->addLayout(scanLayout);
|
||||
|
||||
// WiFi列表
|
||||
m_wifiList = new QListWidget;
|
||||
m_wifiList->setFixedHeight(200);
|
||||
m_wifiList->setStyleSheet(R"(
|
||||
QListWidget {
|
||||
background-color: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
QListWidget::item {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
QListWidget::item:selected {
|
||||
background-color: #e3f2fd;
|
||||
color: #1976D2;
|
||||
}
|
||||
QListWidget::item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
)");
|
||||
connect(m_wifiList, &QListWidget::itemClicked, this, &WirelessWidget::onWifiItemClicked);
|
||||
listLayout->addWidget(m_wifiList);
|
||||
|
||||
layout->addWidget(listGroup);
|
||||
|
||||
// 连接设置区域
|
||||
QGroupBox *connectGroup = new QGroupBox("连接设置");
|
||||
connectGroup->setStyleSheet(GROUP_STYLE);
|
||||
QVBoxLayout *connectLayout = new QVBoxLayout(connectGroup);
|
||||
connectLayout->setContentsMargins(16, 24, 16, 16);
|
||||
connectLayout->setSpacing(12);
|
||||
|
||||
// 密码输入
|
||||
QHBoxLayout *passwordLayout = new QHBoxLayout;
|
||||
QLabel *passwordLabel = new QLabel("密码:");
|
||||
passwordLabel->setFixedWidth(60);
|
||||
passwordLabel->setStyleSheet("font-size: 14px; color: #333;");
|
||||
passwordLayout->addWidget(passwordLabel);
|
||||
|
||||
m_wifiPasswordEdit = new QLineEdit;
|
||||
m_wifiPasswordEdit->setStyleSheet(INPUT_STYLE);
|
||||
m_wifiPasswordEdit->setEchoMode(QLineEdit::Password);
|
||||
m_wifiPasswordEdit->setPlaceholderText("输入WiFi密码");
|
||||
passwordLayout->addWidget(m_wifiPasswordEdit, 1);
|
||||
connectLayout->addLayout(passwordLayout);
|
||||
|
||||
// 连接状态
|
||||
QHBoxLayout *statusLayout = new QHBoxLayout;
|
||||
QLabel *statusTitle = new QLabel("状态:");
|
||||
statusTitle->setFixedWidth(60);
|
||||
statusTitle->setStyleSheet("font-size: 14px; color: #333;");
|
||||
statusLayout->addWidget(statusTitle);
|
||||
|
||||
m_wifiStatusLabel = new QLabel("未连接");
|
||||
m_wifiStatusLabel->setStyleSheet("font-size: 14px; color: #f44336;");
|
||||
statusLayout->addWidget(m_wifiStatusLabel);
|
||||
statusLayout->addStretch();
|
||||
connectLayout->addLayout(statusLayout);
|
||||
|
||||
// 信号强度
|
||||
QHBoxLayout *signalLayout = new QHBoxLayout;
|
||||
QLabel *signalTitle = new QLabel("信号:");
|
||||
signalTitle->setFixedWidth(60);
|
||||
signalTitle->setStyleSheet("font-size: 14px; color: #333;");
|
||||
signalLayout->addWidget(signalTitle);
|
||||
|
||||
m_wifiSignalBar = new QProgressBar;
|
||||
m_wifiSignalBar->setFixedHeight(10);
|
||||
m_wifiSignalBar->setRange(0, 100);
|
||||
m_wifiSignalBar->setValue(0);
|
||||
m_wifiSignalBar->setTextVisible(false);
|
||||
m_wifiSignalBar->setStyleSheet(R"(
|
||||
QProgressBar {
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background-color: #4CAF50;
|
||||
border-radius: 5px;
|
||||
}
|
||||
)");
|
||||
signalLayout->addWidget(m_wifiSignalBar, 1);
|
||||
connectLayout->addLayout(signalLayout);
|
||||
|
||||
// 操作按钮
|
||||
QHBoxLayout *buttonLayout = new QHBoxLayout;
|
||||
buttonLayout->addStretch();
|
||||
|
||||
m_wifiDisconnectBtn = new QPushButton("断开连接");
|
||||
m_wifiDisconnectBtn->setStyleSheet(DANGER_BUTTON_STYLE);
|
||||
m_wifiDisconnectBtn->setFixedWidth(100);
|
||||
m_wifiDisconnectBtn->setCursor(Qt::PointingHandCursor);
|
||||
m_wifiDisconnectBtn->setEnabled(false);
|
||||
connect(m_wifiDisconnectBtn, &QPushButton::clicked, this, &WirelessWidget::onWifiDisconnect);
|
||||
buttonLayout->addWidget(m_wifiDisconnectBtn);
|
||||
|
||||
m_wifiConnectBtn = new QPushButton("连接");
|
||||
m_wifiConnectBtn->setStyleSheet(PRIMARY_BUTTON_STYLE);
|
||||
m_wifiConnectBtn->setFixedWidth(100);
|
||||
m_wifiConnectBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(m_wifiConnectBtn, &QPushButton::clicked, this, &WirelessWidget::onWifiConnect);
|
||||
buttonLayout->addWidget(m_wifiConnectBtn);
|
||||
|
||||
connectLayout->addLayout(buttonLayout);
|
||||
layout->addWidget(connectGroup);
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
// 添加模拟WiFi数据
|
||||
updateWifiList();
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
QWidget *WirelessWidget::create4GPanel()
|
||||
{
|
||||
QWidget *panel = new QWidget;
|
||||
panel->setStyleSheet("background-color: #f5f5f5;");
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout(panel);
|
||||
layout->setContentsMargins(20, 20, 20, 20);
|
||||
layout->setSpacing(16);
|
||||
|
||||
// SIM卡状态
|
||||
QGroupBox *simGroup = new QGroupBox("SIM卡状态");
|
||||
simGroup->setStyleSheet(GROUP_STYLE);
|
||||
QVBoxLayout *simLayout = new QVBoxLayout(simGroup);
|
||||
simLayout->setContentsMargins(16, 24, 16, 16);
|
||||
simLayout->setSpacing(12);
|
||||
|
||||
QHBoxLayout *simStatusLayout = new QHBoxLayout;
|
||||
QLabel *simTitle = new QLabel("SIM卡:");
|
||||
simTitle->setFixedWidth(80);
|
||||
simTitle->setStyleSheet("font-size: 14px; color: #333;");
|
||||
simStatusLayout->addWidget(simTitle);
|
||||
|
||||
m_simStatusLabel = new QLabel("已插入 (中国移动)");
|
||||
m_simStatusLabel->setStyleSheet("font-size: 14px; color: #4CAF50;");
|
||||
simStatusLayout->addWidget(m_simStatusLabel);
|
||||
simStatusLayout->addStretch();
|
||||
simLayout->addLayout(simStatusLayout);
|
||||
|
||||
// 信号强度
|
||||
QHBoxLayout *signal4GLayout = new QHBoxLayout;
|
||||
QLabel *signal4GTitle = new QLabel("信号强度:");
|
||||
signal4GTitle->setFixedWidth(80);
|
||||
signal4GTitle->setStyleSheet("font-size: 14px; color: #333;");
|
||||
signal4GLayout->addWidget(signal4GTitle);
|
||||
|
||||
m_4gSignalBar = new QProgressBar;
|
||||
m_4gSignalBar->setFixedHeight(10);
|
||||
m_4gSignalBar->setRange(0, 100);
|
||||
m_4gSignalBar->setValue(75);
|
||||
m_4gSignalBar->setTextVisible(false);
|
||||
m_4gSignalBar->setStyleSheet(R"(
|
||||
QProgressBar {
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background-color: #4CAF50;
|
||||
border-radius: 5px;
|
||||
}
|
||||
)");
|
||||
signal4GLayout->addWidget(m_4gSignalBar, 1);
|
||||
simLayout->addLayout(signal4GLayout);
|
||||
|
||||
layout->addWidget(simGroup);
|
||||
|
||||
// APN设置
|
||||
QGroupBox *apnGroup = new QGroupBox("APN设置");
|
||||
apnGroup->setStyleSheet(GROUP_STYLE);
|
||||
QVBoxLayout *apnLayout = new QVBoxLayout(apnGroup);
|
||||
apnLayout->setContentsMargins(16, 24, 16, 16);
|
||||
apnLayout->setSpacing(12);
|
||||
|
||||
// APN选择
|
||||
QHBoxLayout *apnSelectLayout = new QHBoxLayout;
|
||||
QLabel *apnSelectLabel = new QLabel("预设APN:");
|
||||
apnSelectLabel->setFixedWidth(80);
|
||||
apnSelectLabel->setStyleSheet("font-size: 14px; color: #333;");
|
||||
apnSelectLayout->addWidget(apnSelectLabel);
|
||||
|
||||
m_apnCombo = new QComboBox;
|
||||
m_apnCombo->setStyleSheet(INPUT_STYLE);
|
||||
m_apnCombo->addItems({"中国移动 (cmnet)", "中国联通 (3gnet)", "中国电信 (ctnet)", "自定义"});
|
||||
m_apnCombo->setCursor(Qt::PointingHandCursor);
|
||||
apnSelectLayout->addWidget(m_apnCombo, 1);
|
||||
apnLayout->addLayout(apnSelectLayout);
|
||||
|
||||
// 自定义APN
|
||||
QHBoxLayout *apnCustomLayout = new QHBoxLayout;
|
||||
QLabel *apnCustomLabel = new QLabel("APN:");
|
||||
apnCustomLabel->setFixedWidth(80);
|
||||
apnCustomLabel->setStyleSheet("font-size: 14px; color: #333;");
|
||||
apnCustomLayout->addWidget(apnCustomLabel);
|
||||
|
||||
m_apnEdit = new QLineEdit;
|
||||
m_apnEdit->setStyleSheet(INPUT_STYLE);
|
||||
m_apnEdit->setPlaceholderText("输入自定义APN");
|
||||
m_apnEdit->setText("cmnet");
|
||||
apnCustomLayout->addWidget(m_apnEdit, 1);
|
||||
apnLayout->addLayout(apnCustomLayout);
|
||||
|
||||
// 用户名
|
||||
QHBoxLayout *usernameLayout = new QHBoxLayout;
|
||||
QLabel *usernameLabel = new QLabel("用户名:");
|
||||
usernameLabel->setFixedWidth(80);
|
||||
usernameLabel->setStyleSheet("font-size: 14px; color: #333;");
|
||||
usernameLayout->addWidget(usernameLabel);
|
||||
|
||||
m_4gUsernameEdit = new QLineEdit;
|
||||
m_4gUsernameEdit->setStyleSheet(INPUT_STYLE);
|
||||
m_4gUsernameEdit->setPlaceholderText("可选");
|
||||
usernameLayout->addWidget(m_4gUsernameEdit, 1);
|
||||
apnLayout->addLayout(usernameLayout);
|
||||
|
||||
// 密码
|
||||
QHBoxLayout *password4GLayout = new QHBoxLayout;
|
||||
QLabel *password4GLabel = new QLabel("密码:");
|
||||
password4GLabel->setFixedWidth(80);
|
||||
password4GLabel->setStyleSheet("font-size: 14px; color: #333;");
|
||||
password4GLayout->addWidget(password4GLabel);
|
||||
|
||||
m_4gPasswordEdit = new QLineEdit;
|
||||
m_4gPasswordEdit->setStyleSheet(INPUT_STYLE);
|
||||
m_4gPasswordEdit->setEchoMode(QLineEdit::Password);
|
||||
m_4gPasswordEdit->setPlaceholderText("可选");
|
||||
password4GLayout->addWidget(m_4gPasswordEdit, 1);
|
||||
apnLayout->addLayout(password4GLayout);
|
||||
|
||||
layout->addWidget(apnGroup);
|
||||
|
||||
// 连接状态
|
||||
QGroupBox *statusGroup = new QGroupBox("连接状态");
|
||||
statusGroup->setStyleSheet(GROUP_STYLE);
|
||||
QVBoxLayout *statusLayout = new QVBoxLayout(statusGroup);
|
||||
statusLayout->setContentsMargins(16, 24, 16, 16);
|
||||
statusLayout->setSpacing(12);
|
||||
|
||||
QHBoxLayout *status4GLayout = new QHBoxLayout;
|
||||
QLabel *status4GTitle = new QLabel("状态:");
|
||||
status4GTitle->setFixedWidth(80);
|
||||
status4GTitle->setStyleSheet("font-size: 14px; color: #333;");
|
||||
status4GLayout->addWidget(status4GTitle);
|
||||
|
||||
m_4gStatusLabel = new QLabel("未连接");
|
||||
m_4gStatusLabel->setStyleSheet("font-size: 14px; color: #f44336;");
|
||||
status4GLayout->addWidget(m_4gStatusLabel);
|
||||
status4GLayout->addStretch();
|
||||
statusLayout->addLayout(status4GLayout);
|
||||
|
||||
// 操作按钮
|
||||
QHBoxLayout *button4GLayout = new QHBoxLayout;
|
||||
button4GLayout->addStretch();
|
||||
|
||||
m_4gDisconnectBtn = new QPushButton("断开连接");
|
||||
m_4gDisconnectBtn->setStyleSheet(DANGER_BUTTON_STYLE);
|
||||
m_4gDisconnectBtn->setFixedWidth(100);
|
||||
m_4gDisconnectBtn->setCursor(Qt::PointingHandCursor);
|
||||
m_4gDisconnectBtn->setEnabled(false);
|
||||
connect(m_4gDisconnectBtn, &QPushButton::clicked, this, &WirelessWidget::on4GDisconnect);
|
||||
button4GLayout->addWidget(m_4gDisconnectBtn);
|
||||
|
||||
m_4gConnectBtn = new QPushButton("连接");
|
||||
m_4gConnectBtn->setStyleSheet(PRIMARY_BUTTON_STYLE);
|
||||
m_4gConnectBtn->setFixedWidth(100);
|
||||
m_4gConnectBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(m_4gConnectBtn, &QPushButton::clicked, this, &WirelessWidget::on4GConnect);
|
||||
button4GLayout->addWidget(m_4gConnectBtn);
|
||||
|
||||
statusLayout->addLayout(button4GLayout);
|
||||
layout->addWidget(statusGroup);
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
QWidget *WirelessWidget::createStatusPanel()
|
||||
{
|
||||
QWidget *panel = new QWidget;
|
||||
panel->setStyleSheet("background-color: #f5f5f5;");
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout(panel);
|
||||
layout->setContentsMargins(20, 20, 20, 20);
|
||||
layout->setSpacing(16);
|
||||
|
||||
// 网络信息
|
||||
QGroupBox *infoGroup = new QGroupBox("网络信息");
|
||||
infoGroup->setStyleSheet(GROUP_STYLE);
|
||||
QVBoxLayout *infoLayout = new QVBoxLayout(infoGroup);
|
||||
infoLayout->setContentsMargins(16, 24, 16, 16);
|
||||
infoLayout->setSpacing(12);
|
||||
|
||||
auto createInfoRow = [](const QString &title, QLabel *&valueLabel, const QString &defaultValue) -> QHBoxLayout *
|
||||
{
|
||||
QHBoxLayout *row = new QHBoxLayout;
|
||||
QLabel *titleLabel = new QLabel(title);
|
||||
titleLabel->setFixedWidth(100);
|
||||
titleLabel->setStyleSheet("font-size: 14px; color: #666;");
|
||||
row->addWidget(titleLabel);
|
||||
|
||||
valueLabel = new QLabel(defaultValue);
|
||||
valueLabel->setStyleSheet("font-size: 14px; color: #333; font-weight: 500;");
|
||||
row->addWidget(valueLabel);
|
||||
row->addStretch();
|
||||
return row;
|
||||
};
|
||||
|
||||
infoLayout->addLayout(createInfoRow("连接类型:", m_connectionTypeLabel, "未连接"));
|
||||
infoLayout->addLayout(createInfoRow("IP地址:", m_ipAddressLabel, "--"));
|
||||
infoLayout->addLayout(createInfoRow("MAC地址:", m_macAddressLabel, "--"));
|
||||
infoLayout->addLayout(createInfoRow("信号强度:", m_signalStrengthLabel, "--"));
|
||||
|
||||
layout->addWidget(infoGroup);
|
||||
|
||||
// 流量统计
|
||||
QGroupBox *trafficGroup = new QGroupBox("流量统计");
|
||||
trafficGroup->setStyleSheet(GROUP_STYLE);
|
||||
QVBoxLayout *trafficLayout = new QVBoxLayout(trafficGroup);
|
||||
trafficLayout->setContentsMargins(16, 24, 16, 16);
|
||||
trafficLayout->setSpacing(12);
|
||||
|
||||
trafficLayout->addLayout(createInfoRow("上传速度:", m_uploadSpeedLabel, "0 KB/s"));
|
||||
trafficLayout->addLayout(createInfoRow("下载速度:", m_downloadSpeedLabel, "0 KB/s"));
|
||||
|
||||
layout->addWidget(trafficGroup);
|
||||
|
||||
// 刷新按钮
|
||||
QHBoxLayout *refreshLayout = new QHBoxLayout;
|
||||
refreshLayout->addStretch();
|
||||
QPushButton *refreshBtn = new QPushButton("刷新状态");
|
||||
refreshBtn->setStyleSheet(PRIMARY_BUTTON_STYLE);
|
||||
refreshBtn->setFixedWidth(120);
|
||||
refreshBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(refreshBtn, &QPushButton::clicked, this, &WirelessWidget::onRefreshStatus);
|
||||
refreshLayout->addWidget(refreshBtn);
|
||||
layout->addLayout(refreshLayout);
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
void WirelessWidget::onTabChanged(int index)
|
||||
{
|
||||
m_currentTab = index;
|
||||
m_contentStack->setCurrentIndex(index);
|
||||
|
||||
// 更新Tab样式
|
||||
for (int i = 0; i < m_tabButtons.size(); ++i)
|
||||
{
|
||||
m_tabButtons[i]->setStyleSheet(i == index ? TAB_ACTIVE_STYLE : TAB_STYLE);
|
||||
}
|
||||
}
|
||||
|
||||
void WirelessWidget::updateWifiList()
|
||||
{
|
||||
m_wifiList->clear();
|
||||
|
||||
// 添加模拟WiFi数据
|
||||
QStringList wifiNetworks = {
|
||||
"🔒 Office_5G (信号: 优)",
|
||||
"🔒 Office_2.4G (信号: 良)",
|
||||
"🔒 Guest_Network (信号: 中)",
|
||||
"🔓 Public_WiFi (信号: 弱)",
|
||||
"🔒 HomeNetwork (信号: 弱)"};
|
||||
|
||||
for (const QString &network : wifiNetworks)
|
||||
{
|
||||
QListWidgetItem *item = new QListWidgetItem(network);
|
||||
m_wifiList->addItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
void WirelessWidget::onWifiScan()
|
||||
{
|
||||
m_wifiScanBtn->setEnabled(false);
|
||||
m_wifiScanBtn->setText("扫描中...");
|
||||
|
||||
// 模拟扫描延迟
|
||||
QTimer::singleShot(1500, this, [this]()
|
||||
{
|
||||
updateWifiList();
|
||||
m_wifiScanBtn->setEnabled(true);
|
||||
m_wifiScanBtn->setText("扫描网络");
|
||||
OverlayDialog::information(window(), "扫描完成", "发现 5 个可用网络"); });
|
||||
}
|
||||
|
||||
void WirelessWidget::onWifiItemClicked(QListWidgetItem *item)
|
||||
{
|
||||
Q_UNUSED(item)
|
||||
m_wifiPasswordEdit->setFocus();
|
||||
}
|
||||
|
||||
void WirelessWidget::onWifiConnect()
|
||||
{
|
||||
if (m_wifiList->currentItem() == nullptr)
|
||||
{
|
||||
OverlayDialog::warning(window(), "提示", "请先选择要连接的WiFi网络");
|
||||
return;
|
||||
}
|
||||
|
||||
QString network = m_wifiList->currentItem()->text();
|
||||
bool needPassword = network.contains("🔒");
|
||||
|
||||
if (needPassword && m_wifiPasswordEdit->text().isEmpty())
|
||||
{
|
||||
OverlayDialog::warning(window(), "提示", "请输入WiFi密码");
|
||||
return;
|
||||
}
|
||||
|
||||
m_wifiConnectBtn->setEnabled(false);
|
||||
m_wifiConnectBtn->setText("连接中...");
|
||||
|
||||
// 模拟连接
|
||||
QTimer::singleShot(2000, this, [this, network]()
|
||||
{
|
||||
m_wifiConnectBtn->setEnabled(true);
|
||||
m_wifiConnectBtn->setText("连接");
|
||||
|
||||
// 模拟连接成功
|
||||
m_wifiStatusLabel->setText("已连接");
|
||||
m_wifiStatusLabel->setStyleSheet("font-size: 14px; color: #4CAF50;");
|
||||
m_wifiSignalBar->setValue(85);
|
||||
m_wifiDisconnectBtn->setEnabled(true);
|
||||
|
||||
updateConnectionStatus();
|
||||
OverlayDialog::information(window(), "连接成功", "已成功连接到WiFi网络"); });
|
||||
}
|
||||
|
||||
void WirelessWidget::onWifiDisconnect()
|
||||
{
|
||||
m_wifiStatusLabel->setText("未连接");
|
||||
m_wifiStatusLabel->setStyleSheet("font-size: 14px; color: #f44336;");
|
||||
m_wifiSignalBar->setValue(0);
|
||||
m_wifiDisconnectBtn->setEnabled(false);
|
||||
m_wifiPasswordEdit->clear();
|
||||
|
||||
updateConnectionStatus();
|
||||
OverlayDialog::information(window(), "已断开", "WiFi连接已断开");
|
||||
}
|
||||
|
||||
void WirelessWidget::on4GConnect()
|
||||
{
|
||||
m_4gConnectBtn->setEnabled(false);
|
||||
m_4gConnectBtn->setText("连接中...");
|
||||
|
||||
QTimer::singleShot(2000, this, [this]()
|
||||
{
|
||||
m_4gConnectBtn->setEnabled(true);
|
||||
m_4gConnectBtn->setText("连接");
|
||||
|
||||
m_4gStatusLabel->setText("已连接 (4G LTE)");
|
||||
m_4gStatusLabel->setStyleSheet("font-size: 14px; color: #4CAF50;");
|
||||
m_4gDisconnectBtn->setEnabled(true);
|
||||
|
||||
updateConnectionStatus();
|
||||
OverlayDialog::information(window(), "连接成功", "4G网络连接成功"); });
|
||||
}
|
||||
|
||||
void WirelessWidget::on4GDisconnect()
|
||||
{
|
||||
m_4gStatusLabel->setText("未连接");
|
||||
m_4gStatusLabel->setStyleSheet("font-size: 14px; color: #f44336;");
|
||||
m_4gDisconnectBtn->setEnabled(false);
|
||||
|
||||
updateConnectionStatus();
|
||||
OverlayDialog::information(window(), "已断开", "4G网络连接已断开");
|
||||
}
|
||||
|
||||
void WirelessWidget::onRefreshStatus()
|
||||
{
|
||||
updateConnectionStatus();
|
||||
}
|
||||
|
||||
void WirelessWidget::updateConnectionStatus()
|
||||
{
|
||||
// 检查连接状态并更新状态面板
|
||||
bool wifiConnected = m_wifiStatusLabel->text().contains("已连接");
|
||||
bool g4Connected = m_4gStatusLabel->text().contains("已连接");
|
||||
|
||||
if (wifiConnected)
|
||||
{
|
||||
m_connectionTypeLabel->setText("WiFi");
|
||||
m_ipAddressLabel->setText("192.168.1.105");
|
||||
m_macAddressLabel->setText("AA:BB:CC:DD:EE:FF");
|
||||
m_signalStrengthLabel->setText(QString("%1%").arg(m_wifiSignalBar->value()));
|
||||
m_uploadSpeedLabel->setText("125 KB/s");
|
||||
m_downloadSpeedLabel->setText("2.3 MB/s");
|
||||
}
|
||||
else if (g4Connected)
|
||||
{
|
||||
m_connectionTypeLabel->setText("4G LTE");
|
||||
m_ipAddressLabel->setText("10.0.0.55");
|
||||
m_macAddressLabel->setText("--");
|
||||
m_signalStrengthLabel->setText(QString("%1%").arg(m_4gSignalBar->value()));
|
||||
m_uploadSpeedLabel->setText("85 KB/s");
|
||||
m_downloadSpeedLabel->setText("1.2 MB/s");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_connectionTypeLabel->setText("未连接");
|
||||
m_ipAddressLabel->setText("--");
|
||||
m_macAddressLabel->setText("--");
|
||||
m_signalStrengthLabel->setText("--");
|
||||
m_uploadSpeedLabel->setText("0 KB/s");
|
||||
m_downloadSpeedLabel->setText("0 KB/s");
|
||||
}
|
||||
}
|
||||
101
widgets/wirelesswidget.h
Normal file
@@ -0,0 +1,101 @@
|
||||
#ifndef WIRELESSWIDGET_H
|
||||
#define WIRELESSWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QLineEdit>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QComboBox>
|
||||
#include <QListWidget>
|
||||
#include <QProgressBar>
|
||||
#include <QGroupBox>
|
||||
#include <QStackedWidget>
|
||||
#include <QTimer>
|
||||
|
||||
/**
|
||||
* @brief 无线通讯设置页面
|
||||
* 提供4G和WIFI通讯配置功能
|
||||
*/
|
||||
class WirelessWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit WirelessWidget(QWidget *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void backRequested();
|
||||
|
||||
private slots:
|
||||
void onWifiScan();
|
||||
void onWifiConnect();
|
||||
void onWifiDisconnect();
|
||||
void onWifiItemClicked(QListWidgetItem *item);
|
||||
void on4GConnect();
|
||||
void on4GDisconnect();
|
||||
void onRefreshStatus();
|
||||
void onTabChanged(int index);
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
QWidget *createTitleBar();
|
||||
QWidget *createTabBar();
|
||||
QWidget *createWifiPanel();
|
||||
QWidget *create4GPanel();
|
||||
QWidget *createStatusPanel();
|
||||
void updateWifiList();
|
||||
void updateConnectionStatus();
|
||||
|
||||
// 标题栏
|
||||
QLabel *m_titleLabel;
|
||||
QPushButton *m_backBtn;
|
||||
|
||||
// Tab栏
|
||||
QList<QPushButton *> m_tabButtons;
|
||||
QStackedWidget *m_contentStack;
|
||||
int m_currentTab;
|
||||
|
||||
// WiFi面板
|
||||
QListWidget *m_wifiList;
|
||||
QPushButton *m_wifiScanBtn;
|
||||
QPushButton *m_wifiConnectBtn;
|
||||
QPushButton *m_wifiDisconnectBtn;
|
||||
QLineEdit *m_wifiPasswordEdit;
|
||||
QLabel *m_wifiStatusLabel;
|
||||
QProgressBar *m_wifiSignalBar;
|
||||
|
||||
// 4G面板
|
||||
QComboBox *m_apnCombo;
|
||||
QLineEdit *m_apnEdit;
|
||||
QLineEdit *m_4gUsernameEdit;
|
||||
QLineEdit *m_4gPasswordEdit;
|
||||
QPushButton *m_4gConnectBtn;
|
||||
QPushButton *m_4gDisconnectBtn;
|
||||
QLabel *m_4gStatusLabel;
|
||||
QProgressBar *m_4gSignalBar;
|
||||
QLabel *m_simStatusLabel;
|
||||
|
||||
// 状态面板
|
||||
QLabel *m_connectionTypeLabel;
|
||||
QLabel *m_ipAddressLabel;
|
||||
QLabel *m_macAddressLabel;
|
||||
QLabel *m_signalStrengthLabel;
|
||||
QLabel *m_uploadSpeedLabel;
|
||||
QLabel *m_downloadSpeedLabel;
|
||||
|
||||
// 刷新定时器
|
||||
QTimer *m_statusTimer;
|
||||
|
||||
// 样式常量
|
||||
static const QString BUTTON_STYLE;
|
||||
static const QString PRIMARY_BUTTON_STYLE;
|
||||
static const QString DANGER_BUTTON_STYLE;
|
||||
static const QString INPUT_STYLE;
|
||||
static const QString GROUP_STYLE;
|
||||
static const QString TAB_STYLE;
|
||||
static const QString TAB_ACTIVE_STYLE;
|
||||
};
|
||||
|
||||
#endif // WIRELESSWIDGET_H
|
||||