这次增加了测试结果导出到EXCEL、项目阶段需要安装定义的表格中的name PN来处理

This commit is contained in:
2025-12-14 18:19:29 +08:00
parent 872b181703
commit 5e94d17212
10 changed files with 434 additions and 17 deletions

View File

@@ -97,6 +97,7 @@ class DUTModel(QObject):
"SN": row.get("SN", ""),
"name": row.get("name", ""),
"project": row.get("project", ""),
"projectPhase": row.get("projectPhase", ""),
"projectType": row.get("projectType", ""),
"phase": row.get("phase", ""),
"weeks": row.get("weeks", 0) if row.get("weeks") is not None else 0,
@@ -141,7 +142,7 @@ class DUTModel(QObject):
# 查询 dutList 表中的所有 DUT
cursor.execute("""
SELECT SN, name, project, projectType, phase, weeks, stationAssigned,
SELECT SN, name, project,projectPhase, projectType, phase, weeks, stationAssigned,
itemOnGoing, itemsFinished, status, testReq, inspector, id,
description, deadLine, createdate, workOrder, direction_codes
FROM dutList
@@ -174,6 +175,7 @@ class DUTModel(QObject):
"SN": row[0] if row[0] else "",
"name": row[1] if row[1] else "",
"project": row[2] if row[2] else "",
"projectPhase": row[3] if row[3] else "",
"projectType": row[3] if row[3] else "",
"phase": row[4] if row[4] else "",
"weeks": row[5] if row[5] is not None else 0,

View File

@@ -13,6 +13,14 @@ from PyQt5.QtWidgets import (
QTextEdit, QPushButton, QLabel, QWidget
)
from PyQt5.QtCore import Qt
import requests
# 尝试导入配置
try:
from config import USE_HTTP_API, HTTP_API_BASE_URL
except ImportError:
USE_HTTP_API = False
HTTP_API_BASE_URL = "http://127.0.0.1:5050"
class DUTFormDialog(QDialog):
@@ -26,12 +34,20 @@ class DUTFormDialog(QDialog):
self.setModal(True)
self.resize(800, 480)
# HTTP API 配置
self.use_http_api = USE_HTTP_API
self.api_base_url = HTTP_API_BASE_URL
# 机台数据缓存
self.machines = []
self.current_machine = None
# 项目阶段数据缓存
self.project_phases = [] # 存储从后端获取的项目阶段列表
self._build_ui()
self._load_machines() # 加载机台数据
self._load_project_phases() # 加载项目阶段数据
self._fill_form(self.dut_form)
self._apply_lock()
@@ -62,7 +78,7 @@ class DUTFormDialog(QDialog):
self.input_testReq = QLineEdit()
self.input_phase = QComboBox()
self.input_phase.setEditable(True)
self.input_phase.addItems(["A Sample", "B Sample", "C Sample", "Work Sample", "阶段1", "阶段2"]) # 示例项
# 项目阶段选项将在 _load_project_phases() 中从后端加载
# 右列控件 - 设备分配(二级选择)
self.input_dtMachine = QComboBox() # 机台选择
@@ -143,6 +159,58 @@ class DUTFormDialog(QDialog):
except Exception as e:
print(f"加载机台数据失败: {e}")
def _load_project_phases(self):
"""从后端加载项目阶段数据"""
try:
# 尝试使用 HTTP API
if self.use_http_api:
try:
url = f"{self.api_base_url}/dbTableAccess"
response = requests.get(
url,
params={"table": "projectPhase"},
timeout=5
)
response.raise_for_status()
result = response.json()
data = result.get("data", [])
if data:
self.project_phases = data
print(f"从 HTTP API 加载了 {len(data)} 个项目阶段")
else:
print("后端返回的项目阶段数据为空")
self.project_phases = []
except Exception as e:
print(f"HTTP API 加载项目阶段失败: {e},回退到 SystemModel")
# 回退到使用 SystemModel
if self.system_model:
self.project_phases = self.system_model.load_project_phases()
else:
self.project_phases = []
else:
# 直接使用 SystemModel
if self.system_model:
self.project_phases = self.system_model.load_project_phases()
else:
self.project_phases = []
# 填充下拉框:显示 name存储 PN 作为 itemData
self.input_phase.clear()
for phase in self.project_phases:
name = phase.get("name", "")
pn = phase.get("PN", "")
if name: # 只添加有名称的项
self.input_phase.addItem(name, pn) # 显示 name数据存储 PN
print(f"项目阶段下拉框已填充 {self.input_phase.count()} 个选项")
except Exception as e:
print(f"加载项目阶段数据失败: {e}")
import traceback
traceback.print_exc()
def _on_machine_changed(self, index):
"""机台选择变化时更新通道列表"""
self.input_station.clear()
@@ -181,13 +249,22 @@ class DUTFormDialog(QDialog):
self.input_SN.setText(str(f.get("SN", "")))
self.input_project.setText(str(f.get("project", "")))
self.input_testReq.setText(str(f.get("testReq", "")))
phase_val = str(f.get("phase", ""))
if phase_val:
idx = self.input_phase.findText(phase_val)
if idx >= 0:
self.input_phase.setCurrentIndex(idx)
else:
self.input_phase.setEditText(phase_val)
# 项目阶段f.get("phase") 存储的是 PN 值,需要匹配 itemData
phase_pn = str(f.get("phase", ""))
if phase_pn:
# 查找匹配的 PN
found = False
for i in range(self.input_phase.count()):
item_pn = self.input_phase.itemData(i)
if item_pn == phase_pn:
self.input_phase.setCurrentIndex(i)
found = True
break
# 如果没有找到匹配项,使用可编辑功能填充
if not found:
self.input_phase.setEditText(phase_pn)
# 设备分配:使用 Model 解析后的 dtMachine 和 station 字段
dtm = f.get("dtMachine", "").strip()
@@ -224,11 +301,16 @@ class DUTFormDialog(QDialog):
def get_form_dict(self):
# 以 dut_form 的结构返回数据
# 项目阶段:优先获取 itemDataPN值如果没有则使用用户输入的文本
phase_pn = self.input_phase.currentData() # 获取当前选中项的 PN
if phase_pn is None: # 如果是用户手动输入的文本
phase_pn = self.input_phase.currentText().strip()
dut_form = {
"SN": self.input_SN.text().strip(),
"project": self.input_project.text().strip(),
"testReq": self.input_testReq.text().strip(),
"phase": self.input_phase.currentText().strip(),
"phase": phase_pn, # 存储 PN 值
"weeks": int(self.input_weeks.text() or 0),
"workOrder": self.input_workOrder.text().strip(),
"inspector": self.input_inspector.text().strip(),

View File

@@ -75,7 +75,7 @@ class LoginDialog(QDialog):
title_label = QLabel("跌落试验管理系统")
title_label.setAlignment(Qt.AlignCenter)
title_font = QFont()
title_font.setPointSize(18)
title_font.setPointSize(16)
title_font.setBold(True)
title_label.setFont(title_font)
title_label.setStyleSheet("color: #333; margin-bottom: 20px;")
@@ -92,7 +92,7 @@ class LoginDialog(QDialog):
self.username_combo = QComboBox()
self.username_combo.setEditable(True)
self.username_combo.addItems(['test', 'admin', 'topAdmin123'])
self.username_combo.setCurrentText('') # 默认为空
self.username_combo.setCurrentText('test') # 默认test
# 保存箭头图标以便在样式表中引用
arrow_path = "e:/WORK/DTM-PY-ALL/UI/temp_arrow.png"

View File

@@ -302,7 +302,7 @@ class TestPlanView(QWidget):
self.table.setItem(idx, 0, QTableWidgetItem(str(plan.get('index', idx+1))))
self.table.setItem(idx, 1, QTableWidgetItem(plan.get('SN', '')))
self.table.setItem(idx, 2, QTableWidgetItem(plan.get('project', '')))
self.table.setItem(idx, 3, QTableWidgetItem(plan.get('phase', '')))
self.table.setItem(idx, 3, QTableWidgetItem(plan.get('projectPhase', '')))
self.table.setItem(idx, 4, QTableWidgetItem(plan.get('testReq', '')))
self.table.setItem(idx, 5, QTableWidgetItem(str(plan.get('weeks', ''))))
# 测试设备(带颜色)

View File

@@ -7,10 +7,15 @@
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QTableWidget, QTableWidgetItem,
QHeaderView, QLineEdit, QComboBox, QDateTimeEdit, QMessageBox, QGridLayout)
QHeaderView, QLineEdit, QComboBox, QDateTimeEdit, QMessageBox, QGridLayout,
QFileDialog, QProgressDialog, QApplication)
from PyQt5.QtCore import Qt, QDateTime, QSize
import requests
from config import HTTP_API_BASE_URL
import datetime
import openpyxl
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from openpyxl.utils import get_column_letter
class TestResultView(QWidget):
@@ -235,10 +240,61 @@ class TestResultView(QWidget):
self.page_size_combo.currentTextChanged.connect(self.on_page_size_changed)
pager_layout.addWidget(self.page_size_combo)
# 上一页按钮
self.prev_btn = QPushButton("上一页")
self.prev_btn.clicked.connect(self.go_prev_page)
pager_layout.addWidget(self.prev_btn)
# 页码跳转
goto_label = QLabel("跳转至")
pager_layout.addWidget(goto_label)
self.page_input = QLineEdit()
self.page_input.setFixedWidth(60)
self.page_input.setPlaceholderText("页码")
self.page_input.setAlignment(Qt.AlignCenter)
self.page_input.setStyleSheet("""
QLineEdit {
padding: 4px;
border: 1px solid #ddd;
border-radius: 3px;
}
QLineEdit:focus {
border: 1px solid #3A84FF;
}
""")
# 按回车键跳转
self.page_input.returnPressed.connect(self.go_to_page)
pager_layout.addWidget(self.page_input)
goto_page_label = QLabel("")
pager_layout.addWidget(goto_page_label)
# 跳转按钮
self.goto_btn = QPushButton("跳转")
self.goto_btn.setFixedWidth(60)
self.goto_btn.clicked.connect(self.go_to_page)
self.goto_btn.setStyleSheet("""
QPushButton {
background-color: #3A84FF;
color: white;
border: none;
border-radius: 3px;
padding: 4px 8px;
}
QPushButton:hover {
background-color: #2B6FE6;
}
QPushButton:pressed {
background-color: #1F5BD1;
}
QPushButton:disabled {
background-color: #cccccc;
}
""")
pager_layout.addWidget(self.goto_btn)
# 下一页按钮
self.next_btn = QPushButton("下一页")
self.next_btn.clicked.connect(self.go_next_page)
pager_layout.addWidget(self.next_btn)
@@ -444,7 +500,261 @@ class TestResultView(QWidget):
if self.current_page < total_pages:
self.current_page += 1
self.load_data()
def go_to_page(self):
"""跳转到指定页码"""
try:
# 获取输入的页码
page_text = self.page_input.text().strip()
if not page_text:
QMessageBox.warning(self, "提示", "请输入页码")
return
# 尝试转换为整数
try:
target_page = int(page_text)
except ValueError:
QMessageBox.warning(self, "错误", "页码必须是整数")
self.page_input.clear()
return
# 计算总页数
total_pages = max(1, (self.total_count + self.page_size - 1) // self.page_size)
# 检查页码有效性
if target_page < 1:
QMessageBox.warning(self, "错误", f"页码不能小于 1")
self.page_input.clear()
return
if target_page > total_pages:
QMessageBox.warning(
self,
"错误",
f"页码不能大于总页数 {total_pages}"
)
self.page_input.clear()
return
# 如果已经在当前页,不需要重复加载
if target_page == self.current_page:
QMessageBox.information(self, "提示", f"当前已在第 {target_page}")
self.page_input.clear()
return
# 跳转到目标页
self.current_page = target_page
self.page_input.clear()
self.load_data()
except Exception as e:
print(f"页码跳转失败: {e}")
QMessageBox.critical(self, "错误", f"跳转失败:{str(e)}")
self.page_input.clear()
def on_export(self):
"""导出结果"""
QMessageBox.information(self, "导出", "导出功能开发中...")
"""导出结果到Excel"""
try:
# 获取满足筛选条件的总数
total_count = self.total_count
if total_count == 0:
QMessageBox.information(self, "提示", "没有数据可以导出")
return
# 生成默认文件名testResult+日期
default_filename = f"testResult_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
# 显示另存为对话框
file_path, _ = QFileDialog.getSaveFileName(
self,
"导出测试结果",
default_filename,
"Excel Files (*.xlsx);;All Files (*)"
)
if not file_path:
return # 用户取消
# 确保文件后缀名
if not file_path.endswith('.xlsx'):
file_path += '.xlsx'
# 显示进度对话框
progress = QProgressDialog("正在导出数据...", "取消", 0, 100, self)
progress.setWindowModality(Qt.WindowModal)
progress.setWindowTitle("导出进度")
progress.setMinimumDuration(0)
progress.setValue(0)
QApplication.processEvents()
# 分批获取策略每次最多获取500条数据
batch_size = 500
total_batches = (total_count + batch_size - 1) // batch_size
# 创建Excel工作簿
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "测试结果"
# 设置表头
headers = [
"序号", "样品编号", "项目代码", "项目阶段", "项目方案", "测试周次",
"工单名称", "产品序列号", "测试设备", "测试通道", "跌落方向",
"跌落高度(mm)", "跌落速度(次/Min)", "样品跌落次数", "方向当前次数",
"木板累计跌落次数", "试验开始时间", "试验结束时间"
]
ws.append(headers)
# 设置表头样式
header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
header_font = Font(bold=True, color="FFFFFF", size=11)
header_alignment = Alignment(horizontal="center", vertical="center")
for col_num, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col_num)
cell.fill = header_fill
cell.font = header_font
cell.alignment = header_alignment
# 分批获取并写入数据
all_data = []
for batch_num in range(total_batches):
if progress.wasCanceled():
QMessageBox.information(self, "提示", "导出已取消")
return
# 更新进度
progress_value = int((batch_num / total_batches) * 80) # 0-80%用于数据获取
progress.setValue(progress_value)
progress.setLabelText(f"正在获取数据... ({batch_num + 1}/{total_batches})")
QApplication.processEvents()
# 获取当前批次数据
batch_data = self._fetch_batch_data(batch_num + 1, batch_size)
if batch_data:
all_data.extend(batch_data)
# 应用前端筛选(测试人员)
progress.setValue(85)
progress.setLabelText("正在筛选数据...")
QApplication.processEvents()
filtered_data = self._filter_data_by_inspector(all_data)
# 写入数据到Excel
progress.setValue(90)
progress.setLabelText(f"正在写入Excel... 共{len(filtered_data)}条数据")
QApplication.processEvents()
for idx, item in enumerate(filtered_data):
row_data = [
self._format_cell_value(item.get('mysequence', idx + 1)),
self._format_cell_value(item.get('SN')),
self._format_cell_value(item.get('dutProject')),
self._format_cell_value(item.get('dutPhase')),
self._format_cell_value(item.get('dutProjectType')),
self._format_cell_value(item.get('dutWeeks')),
self._format_cell_value(item.get('dutWorkOrder')),
self._format_cell_value(item.get('SN')),
self._format_cell_value(item.get('machine_sn')),
self._format_cell_value(item.get('station_no')),
self._format_cell_value(item.get('dropDirection')),
self._format_cell_value(item.get('dropHeight_int')),
self._format_cell_value(item.get('dropSpeed')),
self._format_cell_value(item.get('dropCycles')),
self._format_cell_value(item.get('itemCurrentCycles')),
self._format_cell_value(item.get('stationDropCycles')),
self._format_cell_value(item.get('startTime')),
self._format_cell_value(item.get('endTime'))
]
ws.append(row_data)
# 每100行更新一次进度
if (idx + 1) % 100 == 0:
progress_value = 90 + int((idx + 1) / len(filtered_data) * 8) # 90-98%
progress.setValue(progress_value)
QApplication.processEvents()
# 设置列宽
for col_num in range(1, len(headers) + 1):
column_letter = get_column_letter(col_num)
ws.column_dimensions[column_letter].width = 15
# 保存文件
progress.setValue(99)
progress.setLabelText("正在保存文件...")
QApplication.processEvents()
wb.save(file_path)
progress.setValue(100)
progress.close()
QMessageBox.information(
self,
"成功",
f"导出成功!\n共导出 {len(filtered_data)} 条数据\n文件保存在:{file_path}"
)
except Exception as e:
print(f"导出失败: {e}")
import traceback
traceback.print_exc()
QMessageBox.critical(self, "错误", f"导出失败:{str(e)}")
def _fetch_batch_data(self, page, page_size):
"""获取指定页的数据"""
try:
url = f"{self.api_base_url}/dbTableAccess"
# 构建请求参数(与当前筛选条件一致)
params = {
'table': 'testReqResult',
'pageSize': page_size,
'currentPage': page,
}
# 添加时间筛选
start_dt = self.start_datetime_edit.dateTime().toString("yyyy/MM/dd HH:mm:ss")
end_dt = self.end_datetime_edit.dateTime().toString("yyyy/MM/dd HH:mm:ss")
params['date1'] = start_dt
params['date2'] = end_dt
# 添加样品编号筛选
sn_filter = self.sn_input.text().strip()
if sn_filter:
params['SN'] = sn_filter
# 添加工单筛选
workorder_filter = self.workorder_input.text().strip()
if workorder_filter:
params['dutWorkOrder'] = workorder_filter
# 调用后端API
response = requests.get(url, params=params, timeout=30)
response.raise_for_status()
result = response.json()
if 'data' in result:
return result['data']
return []
except Exception as e:
print(f"获取第{page}页数据失败: {e}")
return []
def _filter_data_by_inspector(self, data):
"""根据测试人员筛选数据(前端筛选)"""
inspector_filter = self.inspector_input.text().strip().lower()
if not inspector_filter:
return data
filtered = []
for item in data:
inspector = str(item.get('tester', '')).lower()
if inspector_filter in inspector:
filtered.append(item)
return filtered

Binary file not shown.

Binary file not shown.

View File

@@ -127,6 +127,17 @@ a = Analysis(
'tkinter.constants',
'tkinter.filedialog',
# openpyxl 相关Excel导出功能
'openpyxl',
'openpyxl.styles',
'openpyxl.utils',
'openpyxl.cell',
'openpyxl.worksheet',
'openpyxl.workbook',
'openpyxl.reader',
'openpyxl.writer',
'et_xmlfile',
# 其他依赖
'websockets',
'serial',

View File

@@ -127,6 +127,17 @@ a = Analysis(
'tkinter.constants',
'tkinter.filedialog',
# openpyxl 相关Excel导出功能
'openpyxl',
'openpyxl.styles',
'openpyxl.utils',
'openpyxl.cell',
'openpyxl.worksheet',
'openpyxl.workbook',
'openpyxl.reader',
'openpyxl.writer',
'et_xmlfile',
# 其他依赖
'websockets',
'serial',

View File

@@ -10,4 +10,5 @@ gevent~=23.9.1
websockets~=12.0
pyinstaller==6.3.0
setuptools==65.5.1
pywebview==5.3.2
pywebview==5.3.2
openpyxl~=3.1.0