""" 服装管理模块 - 处理服装款式和材料用量管理 """ import os from datetime import datetime from PIL import Image from PyQt5.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QLineEdit, QPushButton, QComboBox, QTableWidget, QTableWidgetItem, QHeaderView, QMessageBox, QFileDialog, QDoubleSpinBox, QWidget, QCompleter ) from PyQt5.QtCore import Qt, QTimer, QSortFilterProxyModel from PyQt5.QtGui import QPixmap from database import get_db_connection class SearchableComboBox(QComboBox): """支持模糊搜索的下拉框 - 基于 QSortFilterProxyModel 和 QCompleter""" def __init__(self, parent=None): super(SearchableComboBox, self).__init__(parent) self.setFocusPolicy(Qt.StrongFocus) self.setEditable(True) self.setInsertPolicy(QComboBox.NoInsert) # 添加过滤模型来过滤匹配项 self.pFilterModel = QSortFilterProxyModel(self) self.pFilterModel.setFilterCaseSensitivity(Qt.CaseInsensitive) self.pFilterModel.setSourceModel(self.model()) # 添加自动完成器,使用过滤模型 self.completer = QCompleter(self.pFilterModel, self) # 始终显示所有(过滤后的)完成项 self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion) self.setCompleter(self.completer) # 连接信号 self.lineEdit().textEdited.connect(self.pFilterModel.setFilterFixedString) self.completer.activated.connect(self.on_completer_activated) # 当从自动完成器中选择项时,从组合框中选择对应的项 def on_completer_activated(self, text): if text: index = self.findText(text) self.setCurrentIndex(index) self.activated[str].emit(self.itemText(index)) # 当模型改变时,同时更新过滤器和自动完成器的模型 def setModel(self, model): super(SearchableComboBox, self).setModel(model) self.pFilterModel.setSourceModel(model) self.completer.setModel(self.pFilterModel) # 当模型列改变时,同时更新过滤器和自动完成器的模型列 def setModelColumn(self, column): self.completer.setCompletionColumn(column) self.pFilterModel.setFilterKeyColumn(column) super(SearchableComboBox, self).setModelColumn(column) def setDefaultText(self, text): """设置默认文本 - 兼容方法""" line_edit = self.lineEdit() if line_edit: line_edit.blockSignals(True) line_edit.setText(text) line_edit.blockSignals(False) # 尝试找到匹配的项 index = self.findText(text) if index >= 0: self.setCurrentIndex(index) else: self.setCurrentIndex(-1) class GarmentLibraryDialog(QDialog): """服装库管理对话框""" def __init__(self, db_path): super().__init__() self.db_path = db_path self.setWindowTitle("衣服款号管理") self.resize(1300, 750) self.setup_ui() self.load_garments() def setup_ui(self): """设置用户界面""" layout = QVBoxLayout(self) # 操作按钮区域 op_layout = QHBoxLayout() op_layout.addWidget(QLabel("搜索款号:")) self.search_input = QLineEdit() self.search_input.textChanged.connect(self.load_garments) op_layout.addWidget(self.search_input) add_btn = QPushButton("新增/编辑款号") add_btn.clicked.connect(self.edit_garment) op_layout.addWidget(add_btn) del_btn = QPushButton("删除选中款号") del_btn.clicked.connect(self.delete_garment) op_layout.addWidget(del_btn) refresh_btn = QPushButton("刷新") refresh_btn.clicked.connect(self.load_garments) op_layout.addWidget(refresh_btn) layout.addLayout(op_layout) # 服装表格 self.garment_table = QTableWidget() self.garment_table.setColumnCount(3) self.garment_table.setHorizontalHeaderLabels(["款号", "类目数量", "款式图预览"]) self.garment_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.garment_table.itemDoubleClicked.connect(self.edit_garment_from_table) layout.addWidget(self.garment_table) def get_conn(self): """获取数据库连接""" return get_db_connection(self.db_path) def load_garments(self): """加载服装列表""" keyword = self.search_input.text().strip() try: with self.get_conn() as conn: query = "SELECT style_number, image_path FROM garments" params = [] if keyword: query += " WHERE style_number LIKE ?" params = ["%" + keyword + "%"] query += " ORDER BY style_number" cursor = conn.execute(query, params) rows = cursor.fetchall() self.garment_table.setRowCount(len(rows)) for i in range(len(rows)): self.garment_table.setRowHeight(i, 140) for row_idx, (style_number, image_path) in enumerate(rows): self.garment_table.setItem(row_idx, 0, QTableWidgetItem(style_number)) # 查询材料数量 with self.get_conn() as conn: cursor2 = conn.execute("SELECT COUNT(*) FROM garment_materials WHERE style_number = ?", (style_number,)) count = cursor2.fetchone()[0] self.garment_table.setItem(row_idx, 1, QTableWidgetItem(str(count))) # 显示图片预览 image_item = QTableWidgetItem() image_item.setTextAlignment(Qt.AlignCenter) if image_path and os.path.exists(image_path): try: pixmap = QPixmap(image_path).scaled(130, 130, Qt.KeepAspectRatio, Qt.SmoothTransformation) image_item.setData(Qt.DecorationRole, pixmap) except: image_item.setText("加载失败") else: image_item.setText("无图片") self.garment_table.setItem(row_idx, 2, image_item) except Exception as e: QMessageBox.critical(self, "加载失败", "错误: " + str(e)) def edit_garment_from_table(self): """从表格编辑服装""" row = self.garment_table.currentRow() if row >= 0: style_number = self.garment_table.item(row, 0).text() self.edit_garment(style_number) def edit_garment(self, style_number=None): """编辑服装""" dialog = GarmentEditDialog(self.db_path, style_number) if dialog.exec_(): self.load_garments() def delete_garment(self): """删除服装""" row = self.garment_table.currentRow() if row < 0: QMessageBox.warning(self, "提示", "请先选中一款号") return style_number = self.garment_table.item(row, 0).text() reply = QMessageBox.question(self, "确认", f"删除款号 '{style_number}' 及其所有信息?") if reply == QMessageBox.Yes: try: with self.get_conn() as conn: conn.execute("DELETE FROM garment_materials WHERE style_number = ?", (style_number,)) conn.execute("DELETE FROM garments WHERE style_number = ?", (style_number,)) conn.commit() self.load_garments() QMessageBox.information(self, "成功", "删除完成") except Exception as e: QMessageBox.critical(self, "错误", "删除失败: " + str(e)) class GarmentEditDialog(QDialog): """服装编辑对话框""" def __init__(self, db_path, style_number=None): super().__init__() self.db_path = db_path self.style_number = style_number self.current_image_path = None self.setWindowTitle("编辑款号" if style_number else "新增款号") self.resize(1300, 850) self.setup_ui() if style_number: self.load_garment_data() def setup_ui(self): """设置用户界面""" layout = QVBoxLayout(self) # 基本信息区域 basic_layout = QGridLayout() basic_layout.addWidget(QLabel("款号:"), 0, 0, Qt.AlignRight) self.style_input = QLineEdit() if self.style_number: self.style_input.setText(self.style_number) self.style_input.setEnabled(not self.style_number) basic_layout.addWidget(self.style_input, 0, 1) basic_layout.addWidget(QLabel("款式图:"), 1, 0, Qt.AlignRight) self.image_label = QLabel("无图片") self.image_label.setFixedSize(300, 300) self.image_label.setStyleSheet("border: 1px solid gray;") self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setScaledContents(True) basic_layout.addWidget(self.image_label, 1, 1, 5, 1) upload_btn = QPushButton("上传/更换图片") upload_btn.clicked.connect(self.upload_image) basic_layout.addWidget(upload_btn, 1, 2) layout.addLayout(basic_layout) # 材料用量区域 layout.addWidget(QLabel("材料用量(单件):")) self.material_table = QTableWidget() self.material_table.setColumnCount(6) self.material_table.setHorizontalHeaderLabels(["类目", "类型", "型号", "单件用量", "单位", "删除"]) self.material_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) layout.addWidget(self.material_table) # 按钮区域 btn_layout = QHBoxLayout() add_default_btn = QPushButton("快速添加标准类目") add_default_btn.clicked.connect(self.add_default_categories) btn_layout.addWidget(add_default_btn) add_custom_btn = QPushButton("添加自定义类目") add_custom_btn.clicked.connect(lambda: self.add_material_row()) btn_layout.addWidget(add_custom_btn) layout.addLayout(btn_layout) # 保存/取消按钮 buttons = QHBoxLayout() save_btn = QPushButton("保存") save_btn.clicked.connect(self.save_garment) buttons.addWidget(save_btn) cancel_btn = QPushButton("取消") cancel_btn.clicked.connect(self.reject) buttons.addWidget(cancel_btn) layout.addLayout(buttons) def get_conn(self): """获取数据库连接""" return get_db_connection(self.db_path) def upload_image(self): """上传图片""" file_path, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "Images (*.png *.jpg *.jpeg *.bmp)") if file_path: try: img = Image.open(file_path).convert("RGB") img.thumbnail((800, 800)) os.makedirs("images", exist_ok=True) filename = os.path.basename(file_path) save_path = os.path.join("images", filename) img.save(save_path, "JPEG", quality=85) self.current_image_path = save_path pixmap = QPixmap(save_path).scaled(300, 300, Qt.KeepAspectRatio, Qt.SmoothTransformation) self.image_label.setPixmap(pixmap) except Exception as e: QMessageBox.critical(self, "错误", "上传图片失败: " + str(e)) def load_garment_data(self): """加载服装数据""" try: with self.get_conn() as conn: cursor = conn.execute("SELECT image_path FROM garments WHERE style_number = ?", (self.style_number,)) row = cursor.fetchone() if row and row[0] and os.path.exists(row[0]): self.current_image_path = row[0] pixmap = QPixmap(row[0]).scaled(300, 300, Qt.KeepAspectRatio, Qt.SmoothTransformation) self.image_label.setPixmap(pixmap) self.load_materials() except Exception as e: QMessageBox.critical(self, "错误", "加载失败: " + str(e)) def load_materials(self): """加载材料列表""" try: with self.get_conn() as conn: cursor = conn.execute("SELECT category, fabric_type, model, usage_per_piece, unit FROM garment_materials WHERE style_number = ? ORDER BY id", (self.style_number,)) for category, fabric_type, model, usage, unit in cursor.fetchall(): # 直接使用数据库中的三个字段 display_category = category or "" display_type = fabric_type or "" display_model = model or "" self.add_material_row(display_category, display_type, usage or 0, unit or "米", display_model) except Exception as e: QMessageBox.critical(self, "错误", "加载材料失败: " + str(e)) def add_default_categories(self): """添加默认类目""" defaults = [("A料", "", "米"), ("B料", "", "米"), ("C料", "", "米"), ("D料", "", "米"), ("花边", "", "码"), ("胸杯", "", "一对"), ("拉链", "", "个"), ("辅料", "", "个")] for cat, fabric_type, unit in defaults: self.add_material_row(cat, fabric_type, 0, unit) def add_material_row(self, category="", fabric_type="", usage=0.0, unit="米", model=""): """添加材料行""" print("add_material_row", category, fabric_type, usage, unit, model) row = self.material_table.rowCount() self.material_table.insertRow(row) # 列0: 类目下拉框 cat_combo = QComboBox() cat_combo.setEditable(False) # 最后添加自定义选项 cat_combo.addItem("—— 自定义类目 ——") # 先添加所有类目选项 try: with self.get_conn() as conn: cursor = conn.execute(""" SELECT DISTINCT category FROM fabrics WHERE category IS NOT NULL AND category != '' ORDER BY category """) categories = set() for cat_row in cursor.fetchall(): if cat_row[0] and cat_row[0].strip(): categories.add(cat_row[0]) for cat in sorted(categories): cat_combo.addItem(cat) except: # 如果查询失败,使用默认类目 cat_combo.addItem("布料") cat_combo.addItem("辅料") cat_combo.addItem("其他") if category: cat_combo.setCurrentText(category) else: # 如果没有指定类目,默认选择"—— 自定义类目 ——"(索引0) print("没有指定类目,默认选择'—— 自定义类目 ——'(索引0)") print(cat_combo.count()) print(cat_combo.currentIndex()) # 打印所有选项 for i in range(cat_combo.count()): print(cat_combo.itemText(i)) cat_combo.setCurrentIndex(0) self.material_table.setCellWidget(row, 0, cat_combo) # 列1: 类型下拉框 type_combo = QComboBox() type_combo.setEditable(False) # 最后添加选择提示 type_combo.addItem("—— 选择类型 ——") # 先添加所有类型选项 try: with self.get_conn() as conn: cursor = conn.execute(""" SELECT DISTINCT fabric_type FROM fabrics WHERE fabric_type IS NOT NULL AND fabric_type != '' ORDER BY fabric_type """) types = cursor.fetchall() for type_row in types: if type_row[0] and type_row[0].strip(): type_combo.addItem(type_row[0]) except: pass if fabric_type: type_combo.setCurrentText(fabric_type) else: # 如果没有指定类型,默认选择"—— 选择类型 ——"(索引0) print("没有指定类型,默认选择'—— 选择类型 ——'(索引0)") print(type_combo.count()) print(type_combo.currentIndex()) # 打印所有选项 for i in range(type_combo.count()): print(type_combo.itemText(i)) type_combo.setCurrentIndex(0) self.material_table.setCellWidget(row, 1, type_combo) # 列2: 型号下拉框(支持模糊搜索) model_combo = SearchableComboBox() model_combo.addItem("—— 选择型号 ——") # 先将下拉框添加到表格中,这样update_model_combo_range才能获取到类目和类型 self.material_table.setCellWidget(row, 2, model_combo) # 初始化时根据类目和类型加载对应的型号 self.update_model_combo_range(model_combo, row) # 列3: 单件用量 usage_spin = QDoubleSpinBox() usage_spin.setRange(0, 1000) usage_spin.setValue(usage) usage_spin.setDecimals(3) self.material_table.setCellWidget(row, 3, usage_spin) # 列4: 单位 unit_combo = QComboBox() unit_combo.setEditable(True) unit_combo.addItems(["米", "码", "公斤", "一对", "个", "条"]) unit_combo.setCurrentText(unit) self.material_table.setCellWidget(row, 4, unit_combo) # 列5: 删除按钮 del_btn = QPushButton("删除") del_btn.clicked.connect(lambda _, r=row: self.material_table.removeRow(r)) self.material_table.setCellWidget(row, 5, del_btn) # 初始化类型和型号选项 # self.on_category_changed(cat_combo.currentText(), row) # 如果没有选择具体类目,初始化时显示全部型号 # if cat_combo.currentText() == "—— 自定义类目 ——": # self.on_type_changed("—— 选择类型 ——", row) # 如果有指定的型号,需要在初始化完成后设置 # if model: # # 先设置类型(如果有的话) # if fabric_type: # type_combo.setCurrentText(fabric_type) # self.on_type_changed(fabric_type, row) # # 然后设置型号 # model_combo = self.material_table.cellWidget(row, 2) # if model_combo: # # 确保型号在选项列表中 # found = False # for i in range(model_combo.count()): # item_data = model_combo.itemData(i) # item_text = model_combo.itemText(i) # if item_data == model or item_text == model: # model_combo.setCurrentIndex(i) # found = True # break cat_combo.currentTextChanged.connect(lambda text, r=row: self.on_category_changed(text, r)) type_combo.currentTextChanged.connect(lambda text, r=row: self.on_type_changed(text, r)) # 当用户真正选择了某个型号(包括通过模糊搜索补全选择),再联动类目和类型 model_combo.activated[str].connect(lambda text, r=row: self.on_model_selected(text, r)) def update_model_combo_range(self, model_combo, row): print("update_model_combo_range", model_combo, row) """更新型号下拉框的搜索范围""" cat_combo = self.material_table.cellWidget(row, 0) type_combo = self.material_table.cellWidget(row, 1) category_text = cat_combo.currentText() if cat_combo else "" type_text = type_combo.currentText() if type_combo else "" # 阻止信号防止触发on_model_selected model_combo.blockSignals(True) line_edit = model_combo.lineEdit() if line_edit: line_edit.blockSignals(True) try: with self.get_conn() as conn: # 根据类目和类型构建查询条件 if category_text and category_text != "—— 自定义类目 ——" and type_text and type_text != "—— 选择类型 ——": # 同时根据类目和类型过滤 cursor = conn.execute(""" SELECT DISTINCT model, color, unit FROM fabrics WHERE category = ? AND fabric_type = ? ORDER BY model """, (category_text, type_text)) elif category_text and category_text != "—— 自定义类目 ——": # 只根据类目过滤 cursor = conn.execute(""" SELECT DISTINCT model, color, unit FROM fabrics WHERE category = ? ORDER BY model """, (category_text,)) elif type_text and type_text != "—— 选择类型 ——": # 只根据类型过滤 cursor = conn.execute(""" SELECT DISTINCT model, color, unit FROM fabrics WHERE fabric_type = ? ORDER BY model """, (type_text,)) else: # 显示所有型号 cursor = conn.execute(""" SELECT DISTINCT model, color, unit FROM fabrics ORDER BY model """) # 保存当前用户输入的文本(如果有) current_text = line_edit.text() if line_edit else "" cursor_position = line_edit.cursorPosition() if line_edit else 0 print("current_text", current_text) print("cursor_position", cursor_position) # 清空并重新填充型号下拉框 model_combo.clear() models = cursor.fetchall() for model_row in models: model, color, unit = model_row # 显示格式:型号-颜色(如果有颜色的话) display_text = model if color and color.strip(): display_text = f"{model}-{color}" model_combo.addItem(display_text, model) # 恢复用户输入的文本,如果没有输入则设置默认文本 if line_edit: if current_text and current_text != "—— 选择型号 ——": line_edit.setText(current_text) line_edit.setCursorPosition(cursor_position) else: if isinstance(model_combo, SearchableComboBox): model_combo.setDefaultText("—— 选择型号 ——") else: model_combo.setCurrentIndex(0) except Exception as e: pass finally: if line_edit: line_edit.blockSignals(False) model_combo.blockSignals(False) def on_category_changed(self, category_text, row): """当类目改变时,更新类型下拉框""" print("on_category_changed", category_text, row) type_combo = self.material_table.cellWidget(row, 1) model_combo = self.material_table.cellWidget(row, 2) # 清空类型下拉框 type_combo.clear() type_combo.addItem("—— 选择类型 ——") try: with self.get_conn() as conn: # 加载所有类型 if category_text and category_text != "—— 自定义类目 ——": # 如果选择了具体类目,则过滤 cursor = conn.execute(""" SELECT DISTINCT fabric_type FROM fabrics WHERE category = ? AND fabric_type IS NOT NULL AND fabric_type != '' ORDER BY fabric_type """, (category_text,)) else: # 加载所有类型 cursor = conn.execute(""" SELECT DISTINCT fabric_type FROM fabrics WHERE fabric_type IS NOT NULL AND fabric_type != '' ORDER BY fabric_type """) types = cursor.fetchall() for type_row in types: if type_row[0] and type_row[0].strip(): type_combo.addItem(type_row[0]) # 连接类型改变事件 type_combo.currentTextChanged.connect(lambda text, r=row: self.on_type_changed(text, r)) # 更新型号下拉框的搜索范围 self.update_model_combo_range(model_combo, row) except Exception as e: pass def on_type_changed(self, type_text, row): """当类型改变时,更新类目和型号下拉框""" print("on_type_changed", type_text, row) cat_combo = self.material_table.cellWidget(row, 0) model_combo = self.material_table.cellWidget(row, 2) if not model_combo: return # 如果选择了具体类型,自动选中该类型对应的类目 if cat_combo and type_text and type_text != "—— 选择类型 ——": try: with self.get_conn() as conn: # 查询该类型对应的类目 cursor = conn.execute(""" SELECT DISTINCT category FROM fabrics WHERE fabric_type = ? AND category IS NOT NULL AND category != '' ORDER BY category LIMIT 1 """, (type_text,)) row_db = cursor.fetchone() if row_db and row_db[0]: category = row_db[0].strip() # 在类目下拉框中查找并选中该类目 index = cat_combo.findText(category) if index >= 0: cat_combo.blockSignals(True) cat_combo.setCurrentIndex(index) cat_combo.blockSignals(False) except Exception as e: pass # 更新型号下拉框的搜索范围 self.update_model_combo_range(model_combo, row) def on_model_selected(self, model_text, row): """当型号选择时,自动设置单位并填充类目和类型""" print("on_model_selected", model_text, row) # 先获取所有需要的控件 cat_combo = self.material_table.cellWidget(row, 0) type_combo = self.material_table.cellWidget(row, 1) model_combo = self.material_table.cellWidget(row, 2) unit_combo = self.material_table.cellWidget(row, 4) if not model_text or model_text == "—— 选择型号 ——": # 空选择,不做联动 return # 从显示文本中解析真实型号(例如 "M001-红" -> "M001") display_text = model_text.strip() base_model = display_text.split("-", 1)[0] if "-" in display_text else display_text try: with self.get_conn() as conn: cursor = conn.execute( "SELECT category, fabric_type, unit FROM fabrics WHERE model = ? AND (is_deleted IS NULL OR is_deleted = 0)", (base_model,) ) row_db = cursor.fetchone() if row_db: category, fabric_type, unit = row_db # 自动填充单位 if unit: unit_combo.setCurrentText(unit) # 自动填充类目和类型,阻止信号以避免触发默认更新逻辑 if category: # 阻止类目、类型和型号的信号,避免触发默认更新逻辑 cat_combo.blockSignals(True) type_combo.blockSignals(True) model_combo.blockSignals(True) # 设置类目 cat_combo.setCurrentText(category) # 更新类型下拉框选项(直接调用,不会触发信号因为已经阻止了) self.on_category_changed(category, row) # 重新阻止信号(因为on_category_changed内部可能恢复了信号) model_combo.blockSignals(True) # 设置类型 if fabric_type: type_combo.setCurrentText(fabric_type) # 恢复信号 cat_combo.blockSignals(False) type_combo.blockSignals(False) model_combo.blockSignals(False) except: pass def save_garment(self): """保存服装""" style_number = self.style_input.text().strip() if not style_number: QMessageBox.warning(self, "错误", "请输入款号") return try: with self.get_conn() as conn: conn.execute('INSERT OR REPLACE INTO garments (style_number, image_path) VALUES (?, ?)', (style_number, self.current_image_path)) conn.execute("DELETE FROM garment_materials WHERE style_number = ?", (style_number,)) for row in range(self.material_table.rowCount()): # 获取各列的值 category_widget = self.material_table.cellWidget(row, 0) # 类目 type_widget = self.material_table.cellWidget(row, 1) # 类型 model_widget = self.material_table.cellWidget(row, 2) # 型号 usage_widget = self.material_table.cellWidget(row, 3) # 单件用量 unit_widget = self.material_table.cellWidget(row, 4) # 单位 category = category_widget.currentText().strip() fabric_type = type_widget.currentText().strip() model = model_widget.currentText().strip() # 处理类目和类型 if category == "—— 自定义类目 ——": category = "" if fabric_type == "—— 选择类型 ——": fabric_type = "" # 如果选择了具体型号,获取型号的实际值 final_model = "" if model and model != "—— 选择型号 ——": model_data = model_widget.itemData(model_widget.currentIndex()) final_model = model_data if model_data else model # 至少需要有类目或型号 if not category and not final_model: continue usage = usage_widget.value() unit = unit_widget.currentText().strip() or "米" # 分别存储类目、类型和型号到对应字段 conn.execute("INSERT INTO garment_materials (style_number, category, fabric_type, model, usage_per_piece, unit) VALUES (?, ?, ?, ?, ?, ?)", (style_number, category, fabric_type, final_model, usage, unit)) conn.commit() QMessageBox.information(self, "成功", "保存完成") self.accept() except Exception as e: QMessageBox.critical(self, "错误", "保存失败: " + str(e))