#include "procedureplayerwidget.h" #include "overlaydialogswidget.h" #include "../procedure/proceduremanager.h" #include #include // ============== 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(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(); }