364 lines
14 KiB
Python
364 lines
14 KiB
Python
from PySide6.QtWidgets import (
|
||
QWidget, QTableWidget, QTableWidgetItem, QHeaderView, QLineEdit, QAbstractItemView
|
||
)
|
||
from PySide6.QtCore import Qt, QTimer
|
||
from openpyxl.styles.builtins import headline_1
|
||
|
||
from ui.base_page import BaseInspectionPage
|
||
from src.db_manager import DatabaseManager
|
||
from utils.logger import get_logger, log_timing_context
|
||
logger = get_logger("incoming_inspection")
|
||
|
||
class IncomingInspectionPage(BaseInspectionPage):
|
||
def __init__(self, parent=None):
|
||
super().__init__("入检", parent)
|
||
self.inputs = {}
|
||
self.db_manager = DatabaseManager()
|
||
self.scan_timer = QTimer()
|
||
self.scan_timer.setSingleShot(True)
|
||
self.scan_timer.timeout.connect(self.query_data)
|
||
self.last_gch = ""
|
||
self.init_ui()
|
||
|
||
def init_ui(self):
|
||
self.content_layout.setContentsMargins(5, 5, 5, 5)
|
||
|
||
# Initialize Table - 使用4列
|
||
self.table = QTableWidget()
|
||
self.table.setColumnCount(4)
|
||
self.table.verticalHeader().setVisible(False)
|
||
self.table.horizontalHeader().setVisible(False)
|
||
|
||
# Style
|
||
self.table.setStyleSheet("""
|
||
QTableWidget {
|
||
border: 1px solid #999;
|
||
gridline-color: #999;
|
||
background-color: white;
|
||
font-family: 'Microsoft YaHei';
|
||
font-size: 18px;
|
||
}
|
||
QTableWidget::item {
|
||
padding: 5px;
|
||
}
|
||
QLineEdit {
|
||
border: none;
|
||
background-color: transparent;
|
||
font-family: 'Microsoft YaHei';
|
||
font-size: 18px;
|
||
}
|
||
""")
|
||
|
||
header = self.table.horizontalHeader()
|
||
header.setSectionResizeMode(QHeaderView.Stretch)
|
||
|
||
self.table.setSelectionMode(QAbstractItemView.NoSelection)
|
||
self.table.setFocusPolicy(Qt.NoFocus)
|
||
self.content_layout.addWidget(self.table)
|
||
self.render_form()
|
||
# 默认光标定位到工程号
|
||
if "batch_no" in self.inputs:
|
||
self.inputs["batch_no"].setFocus()
|
||
|
||
def render_form(self):
|
||
# 1. 第一行 - 工程号
|
||
self.add_row_label_input("工程号", "batch_no", span_input=3)
|
||
self.inputs["batch_no"].setPlaceholderText("扫描/输入")
|
||
self.inputs["batch_no"].returnPressed.connect(self.query_data)
|
||
self.inputs["batch_no"].textChanged.connect(self.on_text_changed)
|
||
|
||
# 2. 第二行 - 产地和规格
|
||
row = self.table.rowCount()
|
||
self.table.insertRow(row)
|
||
self.table.setRowHeight(row, 50)
|
||
self.set_label_cell(row, 0, "产地")
|
||
self.table.setCellWidget(row, 1, self.create_input("material"))
|
||
self.set_label_cell(row, 2, "规格")
|
||
self.table.setCellWidget(row, 3, self.create_input("spec"))
|
||
|
||
# 3. 分组标题 - 第一组
|
||
self.add_group_header("━ 第一组 ━")
|
||
|
||
# 4. 炉号和材质(第一组)
|
||
row = self.table.rowCount()
|
||
self.table.insertRow(row)
|
||
self.table.setRowHeight(row, 50)
|
||
self.set_label_cell(row, 0, "炉号", align_left=True)
|
||
inp1 = self.create_input("material1")
|
||
self.table.setCellWidget(row, 1, inp1)
|
||
self.set_label_cell(row, 2, "材质")
|
||
inp2 = self.create_input("spec1")
|
||
self.table.setCellWidget(row, 3, inp2)
|
||
|
||
# 5. 检验信息(第一组)
|
||
self.add_row_label_input("检验信息", "insp_info_1", span_input=3, align_left=True)
|
||
|
||
# 6. 分组标题 - 第二组
|
||
self.add_group_header("━ 第二组 ━")
|
||
|
||
# 7. 炉号和材质(第二组)
|
||
row = self.table.rowCount()
|
||
self.table.insertRow(row)
|
||
self.table.setRowHeight(row, 50)
|
||
self.set_label_cell(row, 0, "炉号", align_left=True)
|
||
inp3 = self.create_input("info1")
|
||
self.table.setCellWidget(row, 1, inp3)
|
||
self.set_label_cell(row, 2, "材质")
|
||
inp4 = self.create_input("spec2")
|
||
self.table.setCellWidget(row, 3, inp4)
|
||
|
||
# 8. 检验信息(第二组)
|
||
self.add_row_label_input("检验信息", "insp_info_2", span_input=3, align_left=True)
|
||
|
||
self.add_group_header("━ 检验值 ━")
|
||
# 9. 元素行 - 固定顺序显示所有18个元素
|
||
element_order = ["C", "Si", "Mn", "P", "S", "Cr", "Ni", "Mo", "Cu", "Ti", "Nb", "V", "Al", "W", "Co", "N", "Fe", "Se"]
|
||
for i in range(0, len(element_order), 2):
|
||
name1 = element_order[i]
|
||
name2 = element_order[i+1] if i + 1 < len(element_order) else None
|
||
|
||
row = self.table.rowCount()
|
||
self.table.insertRow(row)
|
||
self.table.setRowHeight(row, 50)
|
||
|
||
self.set_label_cell(row, 0, name1)
|
||
self.table.setCellWidget(row, 1, self.create_input(name1))
|
||
|
||
if name2:
|
||
self.set_label_cell(row, 2, name2)
|
||
self.table.setCellWidget(row, 3, self.create_input(name2))
|
||
|
||
def add_row_label_input(self, label_text, key, span_input=1, bg_color=None, group_col=True, align_left=False):
|
||
row = self.table.rowCount()
|
||
self.table.insertRow(row)
|
||
self.table.setRowHeight(row, 50)
|
||
|
||
if not group_col:
|
||
# 不需要分组列,第一列空白
|
||
self.set_empty_cell(row, 0)
|
||
self.set_label_cell(row, 1, label_text, bg_color=bg_color, align_left=align_left)
|
||
inp = self.create_input(key)
|
||
self.table.setCellWidget(row, 2, inp)
|
||
if span_input > 1:
|
||
self.table.setSpan(row, 2, 1, span_input)
|
||
else:
|
||
self.set_label_cell(row, 0, label_text, bg_color=bg_color, align_left=align_left)
|
||
inp = self.create_input(key)
|
||
self.table.setCellWidget(row, 1, inp)
|
||
if span_input > 1:
|
||
self.table.setSpan(row, 1, 1, span_input)
|
||
|
||
if bg_color:
|
||
col_start = 2 if not group_col else 1
|
||
for col in range(col_start, col_start + span_input):
|
||
self.set_cell_background(row, col, bg_color)
|
||
|
||
def set_group_cell(self, row, col, text, rowspan=1, bg_color=None):
|
||
"""设置分组标识单元格"""
|
||
item = QTableWidgetItem(text)
|
||
item.setTextAlignment(Qt.AlignCenter)
|
||
item.setFlags(Qt.ItemIsEnabled)
|
||
from PySide6.QtGui import QColor, QBrush, QFont
|
||
if bg_color:
|
||
brush = QBrush(QColor(bg_color))
|
||
else:
|
||
brush = QBrush(QColor("#f5f5f5"))
|
||
item.setBackground(brush)
|
||
font = QFont()
|
||
font.setBold(True)
|
||
font.setPointSize(16)
|
||
item.setFont(font)
|
||
self.table.setItem(row, col, item)
|
||
if rowspan > 1:
|
||
self.table.setSpan(row, col, rowspan, 1)
|
||
|
||
def set_empty_cell(self, row, col):
|
||
"""设置空白单元格"""
|
||
item = QTableWidgetItem("")
|
||
item.setFlags(Qt.ItemIsEnabled)
|
||
self.table.setItem(row, col, item)
|
||
|
||
def add_group_header(self, text):
|
||
"""添加分组标题行"""
|
||
row = self.table.rowCount()
|
||
self.table.insertRow(row)
|
||
self.table.setRowHeight(row, 40)
|
||
|
||
item = QTableWidgetItem(text)
|
||
item.setTextAlignment(Qt.AlignCenter)
|
||
item.setFlags(Qt.ItemIsEnabled)
|
||
from PySide6.QtGui import QColor, QBrush, QFont
|
||
brush = QBrush(QColor("#f0f0f0"))
|
||
item.setBackground(brush)
|
||
font = QFont()
|
||
font.setBold(True)
|
||
font.setPointSize(14)
|
||
item.setFont(font)
|
||
self.table.setItem(row, 0, item)
|
||
self.table.setSpan(row, 0, 1, 4)
|
||
|
||
def set_cell_background(self, row, col, color):
|
||
"""设置单元格背景色"""
|
||
item = self.table.item(row, col)
|
||
if not item:
|
||
item = QTableWidgetItem("")
|
||
self.table.setItem(row, col, item)
|
||
from PySide6.QtGui import QColor, QBrush
|
||
brush = QBrush(QColor(color))
|
||
item.setBackground(brush)
|
||
|
||
def set_label_cell(self, row, col, text, bg_color=None, align_left=False):
|
||
item = QTableWidgetItem(text)
|
||
# 根据参数设置对齐方式
|
||
if align_left:
|
||
item.setTextAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||
else:
|
||
item.setTextAlignment(Qt.AlignCenter)
|
||
item.setFlags(Qt.ItemIsEnabled)
|
||
from PySide6.QtGui import QColor, QBrush
|
||
if bg_color:
|
||
brush = QBrush(QColor(bg_color))
|
||
else:
|
||
brush = QBrush(QColor("#f5f5f5"))
|
||
item.setBackground(brush)
|
||
self.table.setItem(row, col, item)
|
||
|
||
def create_input(self, key):
|
||
inp = QLineEdit()
|
||
if key in self.inputs:
|
||
key = f"{key}_2"
|
||
self.inputs[key] = inp
|
||
return inp
|
||
|
||
def on_text_changed(self, text):
|
||
"""PDA扫码输入检测,延时300ms后触发查询"""
|
||
self.scan_timer.stop()
|
||
if text.strip() and text.strip() != self.last_gch:
|
||
self.scan_timer.start(300)
|
||
|
||
def query_data(self):
|
||
self.scan_timer.stop()
|
||
gch = self.inputs["batch_no"].text().strip()
|
||
if not gch or gch == self.last_gch:
|
||
return
|
||
|
||
self.last_gch = gch
|
||
logger.info(f"查询工程号: {gch}")
|
||
with log_timing_context(f"query_by_gch({gch})", "incoming_inspection"):
|
||
data, err = self.db_manager.query_by_gch(gch)
|
||
if err:
|
||
logger.warning(f"查询失败: {err}")
|
||
self.show_info("查询失败", err)
|
||
return
|
||
|
||
self.inputs["material"].setText(data.get("cd") or "")
|
||
self.inputs["spec"].setText(data.get("size") or "")
|
||
self.inputs["material1"].setText(data.get("heat_number") or "")
|
||
self.inputs["spec1"].setText(data.get("qy_czt") or "")
|
||
self.inputs["info1"].setText(data.get("heat_number2") or "")
|
||
self.inputs["spec2"].setText(data.get("qy_czw") or "")
|
||
def update_ui_with_data(self, data):
|
||
"""
|
||
根据数据库数据更新UI。
|
||
"""
|
||
elements = data.get("elements", {})
|
||
|
||
# 固定元素顺序
|
||
element_order = ["C", "Si", "Mn", "P", "S", "Cr", "Ni", "Mo", "Cu", "Ti", "Nb", "V", "Al", "W", "Co", "N", "Fe", "Se"]
|
||
|
||
# 更新所有元素值,没有的显示为0
|
||
for el in element_order:
|
||
if el in self.inputs:
|
||
value = "0" if (v := float(elements.get(el, 0))) == 0 else f"{v:.6f}"
|
||
self.inputs[el].setText(str(value))
|
||
|
||
# 比对标准值范围
|
||
self.validate_elements(elements)
|
||
|
||
def clear_data(self):
|
||
"""清空所有输入框数据"""
|
||
for inp in self.inputs.values():
|
||
inp.clear()
|
||
self.last_gch = ""
|
||
# 清空后光标重新定位到工程号
|
||
if "batch_no" in self.inputs:
|
||
self.inputs["batch_no"].setFocus()
|
||
|
||
def validate_elements(self, elements):
|
||
"""比对元素值与标准范围,不符合的写入检验信息框"""
|
||
heat_number = self.inputs.get("material1")
|
||
heat_number2 = self.inputs.get("info1")
|
||
|
||
if not heat_number or not heat_number2:
|
||
return
|
||
|
||
hn1 = heat_number.text().strip()
|
||
hn2 = heat_number2.text().strip()
|
||
|
||
if not hn1 and not hn2:
|
||
return
|
||
|
||
with log_timing_context("query_standard_range", "incoming_inspection"):
|
||
heat1_range, heat2_range, err = self.db_manager.query_standard_range(hn1, hn2)
|
||
if err:
|
||
self.show_info("标准值查询失败", err)
|
||
return
|
||
|
||
# 元素名到数据库字段的映射 (18个元素)
|
||
element_map = {
|
||
"C": "c", "Si": "si", "Mn": "mn", "P": "p", "S": "s",
|
||
"Cr": "cr", "Ni": "ni", "Mo": "mo", "Cu": "cu", "Ti": "ti",
|
||
"Nb": "nb", "V": "v", "Al": "al", "W": "w", "Co": "co",
|
||
"N": "n", "Fe": "fe", "Se": "se"
|
||
}
|
||
|
||
def check_range(std_range, elements_data):
|
||
"""检查元素是否在范围内,返回不符合的列表"""
|
||
out_of_range = []
|
||
if not std_range:
|
||
return out_of_range
|
||
|
||
for el_name, db_field in element_map.items():
|
||
if el_name not in elements_data:
|
||
continue
|
||
|
||
try:
|
||
value = float(elements_data[el_name])
|
||
except (ValueError, TypeError):
|
||
continue
|
||
|
||
min_key = f"{db_field}min"
|
||
max_key = f"{db_field}max"
|
||
min_val = std_range.get(min_key)
|
||
max_val = std_range.get(max_key)
|
||
|
||
if min_val is None and max_val is None:
|
||
continue
|
||
try:
|
||
|
||
min_val = float(min_val) if min_val is not None and min_val != '' else None
|
||
max_val = float(max_val) if max_val is not None and max_val != '' else None
|
||
except (ValueError, TypeError) as e:
|
||
logger.debug(f"标准值解析异常: {e}")
|
||
is_out = False
|
||
if min_val is not None and value < min_val:
|
||
is_out = True
|
||
if max_val is not None and value > max_val:
|
||
is_out = True
|
||
|
||
if is_out:
|
||
min_str = str(min_val) if min_val is not None else ""
|
||
max_str = str(max_val) if max_val is not None else ""
|
||
out_of_range.append(f"{el_name}({min_str}-{max_str})")
|
||
|
||
return out_of_range
|
||
|
||
def get_info_text(std_range):
|
||
if not std_range:
|
||
return "无标准值"
|
||
out = check_range(std_range, elements)
|
||
return "/".join(out) + "/" if out else ""
|
||
|
||
self.inputs["insp_info_1"].setText(get_info_text(heat1_range))
|
||
self.inputs["insp_info_2"].setText(get_info_text(heat2_range))
|