diff --git a/UI/models/dut_model.py b/UI/models/dut_model.py index 9330d93..1dab65c 100644 --- a/UI/models/dut_model.py +++ b/UI/models/dut_model.py @@ -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, diff --git a/UI/views/dut_form_dialog.py b/UI/views/dut_form_dialog.py index a43a6c9..1e0d51f 100644 --- a/UI/views/dut_form_dialog.py +++ b/UI/views/dut_form_dialog.py @@ -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 的结构返回数据 + # 项目阶段:优先获取 itemData(PN值),如果没有则使用用户输入的文本 + 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(), diff --git a/UI/views/login_dialog.py b/UI/views/login_dialog.py index 70882da..dd2b021 100644 --- a/UI/views/login_dialog.py +++ b/UI/views/login_dialog.py @@ -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" diff --git a/UI/views/test_plan_view.py b/UI/views/test_plan_view.py index 4c94915..be8833f 100644 --- a/UI/views/test_plan_view.py +++ b/UI/views/test_plan_view.py @@ -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', '')))) # 测试设备(带颜色) diff --git a/UI/views/test_result_view.py b/UI/views/test_result_view.py index 044c7ab..5bfd483 100644 --- a/UI/views/test_result_view.py +++ b/UI/views/test_result_view.py @@ -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 diff --git a/db/dtmgtDb.db-shm b/db/dtmgtDb.db-shm index ac0437c..30d3a21 100644 Binary files a/db/dtmgtDb.db-shm and b/db/dtmgtDb.db-shm differ diff --git a/db/dtmgtDb.db-wal b/db/dtmgtDb.db-wal index ad8a604..4cce233 100644 Binary files a/db/dtmgtDb.db-wal and b/db/dtmgtDb.db-wal differ diff --git a/dtmgtApp-debug.spec b/dtmgtApp-debug.spec index 91eed6f..1e3c6da 100644 --- a/dtmgtApp-debug.spec +++ b/dtmgtApp-debug.spec @@ -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', diff --git a/dtmgtApp.spec b/dtmgtApp.spec index fd3f10d..1e4cc8a 100644 --- a/dtmgtApp.spec +++ b/dtmgtApp.spec @@ -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', diff --git a/requirements.txt b/requirements.txt index 3c4576d..23e5524 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,5 @@ gevent~=23.9.1 websockets~=12.0 pyinstaller==6.3.0 setuptools==65.5.1 -pywebview==5.3.2 \ No newline at end of file +pywebview==5.3.2 +openpyxl~=3.1.0 \ No newline at end of file