""" 服装管理模块 - 处理服装款式和材料用量管理 """ 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, QStringListModel, QTimer from PyQt5.QtGui import QPixmap from database import get_db_connection class SearchableComboBox(QComboBox): """支持模糊搜索的下拉框""" def __init__(self, parent=None): super().__init__(parent) self.setEditable(True) self.setInsertPolicy(QComboBox.NoInsert) # 存储所有选项 self.all_items = [] self.all_data = [] self.is_filtering = False # 设置自动完成 self.completer = QCompleter(self) self.completer.setCompletionMode(QCompleter.PopupCompletion) self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.completer.setFilterMode(Qt.MatchContains) self.setCompleter(self.completer) # 连接信号 self.lineEdit().textChanged.connect(self.on_text_changed) def addItem(self, text, userData=None): """添加选项""" # 临时断开信号连接,防止textChanged触发on_text_changed self.lineEdit().textChanged.disconnect() super().addItem(text, userData) if text not in self.all_items: self.all_items.append(text) self.all_data.append(userData) self.update_completer() # 重新连接信号 self.lineEdit().textChanged.connect(self.on_text_changed) def addItems(self, texts): """批量添加选项""" for text in texts: self.addItem(text) def clear(self): """清空所有选项""" if not self.is_filtering: super().clear() self.all_items.clear() self.all_data.clear() self.update_completer() def reset_items(self): """重置所有选项""" # 临时断开信号连接,防止textChanged触发on_text_changed self.lineEdit().textChanged.disconnect() self.is_filtering = True super().clear() for i, item in enumerate(self.all_items): super().addItem(item, self.all_data[i] if i < len(self.all_data) else None) self.is_filtering = False # 重新连接信号 self.lineEdit().textChanged.connect(self.on_text_changed) def update_completer(self): """更新自动完成列表""" model = QStringListModel(self.all_items) self.completer.setModel(model) def on_text_changed(self, text): """文本改变时的处理""" if self.is_filtering: return if not text or text in ["—— 选择型号 ——"]: self.reset_items() # 如果获得焦点且有选项,显示下拉列表 if self.hasFocus() and self.count() > 0: self.showPopup() return # 模糊搜索匹配 filtered_items = [] filtered_data = [] for i, item in enumerate(self.all_items): if text.lower() in item.lower(): filtered_items.append(item) filtered_data.append(self.all_data[i] if i < len(self.all_data) else None) # 更新下拉列表 self.is_filtering = True super().clear() for i, item in enumerate(filtered_items): super().addItem(item, filtered_data[i]) self.is_filtering = False # 如果有匹配项且获得焦点,显示下拉列表 if filtered_items and self.hasFocus(): self.showPopup() 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, usage_per_piece, unit FROM garment_materials WHERE style_number = ? ORDER BY id", (self.style_number,)) for category, fabric_type, usage, unit in cursor.fetchall(): display_category = "" display_type = "" display_model = "" # category字段可能存储型号或类目-类型组合 if category: # 首先检查是否是型号(在fabrics表中查找) fabric_cursor = conn.execute("SELECT category, model FROM fabrics WHERE model = ?", (category,)) fabric_row = fabric_cursor.fetchone() if fabric_row: # 是型号,从fabrics表获取类目信息 fabric_category, model = fabric_row display_model = model if fabric_category and "-" in fabric_category: parts = fabric_category.split("-", 1) display_category = parts[0] display_type = parts[1] else: display_category = fabric_category or "" else: # 不是型号,按类目-类型格式解析 if "-" in category: parts = category.split("-", 1) display_category = parts[0] display_type = parts[1] else: display_category = category # 如果有单独的fabric_type字段,优先使用 if fabric_type: display_type = fabric_type 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=""): """添加材料行""" row = self.material_table.rowCount() self.material_table.insertRow(row) # 列0: 类目下拉框 cat_combo = QComboBox() cat_combo.setEditable(True) # 最后添加自定义选项 cat_combo.addItem("—— 自定义类目 ——") # 先添加所有类目选项 try: with self.get_conn() as conn: # 只获取纯类目(提取"-"前面的部分) cursor = conn.execute(""" SELECT DISTINCT CASE WHEN category LIKE '%-%' THEN SUBSTR(category, 1, INSTR(category, '-') - 1) ELSE category END as major_category FROM fabrics WHERE category IS NOT NULL AND category != '' ORDER BY major_category """) categories = set() for cat_row in cursor.fetchall(): if cat_row[0] and cat_row[0].strip(): categories.add(cat_row[0]) # 添加默认类目 categories.update(["布料", "辅料", "其他"]) 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: # 如果没有指定类目,默认选择第一个实际类目而不是"自定义类目" if cat_combo.count() > 1: cat_combo.setCurrentIndex(0) cat_combo.currentTextChanged.connect(lambda text, r=row: self.on_category_changed(text, r)) self.material_table.setCellWidget(row, 0, cat_combo) # 列1: 类型下拉框 type_combo = QComboBox() type_combo.setEditable(True) # 先添加所有类型选项 try: with self.get_conn() as conn: cursor = conn.execute(""" SELECT DISTINCT CASE WHEN category LIKE '%-%' THEN SUBSTR(category, INSTR(category, '-') + 1) ELSE '默认类型' END as fabric_type FROM fabrics WHERE category IS NOT NULL AND category != '' ORDER BY fabric_type """) types = cursor.fetchall() for type_row in types: if type_row[0] and type_row[0] != '默认类型': type_combo.addItem(type_row[0]) except: pass # 最后添加选择提示 type_combo.addItem("—— 选择类型 ——") if fabric_type: type_combo.setCurrentText(fabric_type) else: # 如果没有指定类型,默认选择第一个实际类型而不是"选择类型" if type_combo.count() > 1: type_combo.setCurrentIndex(0) type_combo.currentTextChanged.connect(lambda text, r=row: self.on_type_changed(text, r)) self.material_table.setCellWidget(row, 1, type_combo) # 列2: 型号下拉框(支持模糊搜索) model_combo = SearchableComboBox() model_combo.addItem("—— 选择型号 ——") # 初始化时加载所有型号 try: with self.get_conn() as conn: cursor = conn.execute(""" SELECT DISTINCT model, color, unit FROM fabrics ORDER BY model """) 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) except Exception as e: pass # 确保默认选中第一项("—— 选择型号 ——") model_combo.setCurrentIndex(0) model_combo.currentTextChanged.connect(lambda text, r=row: self.on_model_selected(text, r)) self.material_table.setCellWidget(row, 2, model_combo) # 列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) # 然后设置型号 - 使用SearchableComboBox的setCurrentText方法 model_combo = self.material_table.cellWidget(row, 2) if isinstance(model_combo, SearchableComboBox): # 确保型号在选项列表中 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 # 如果没找到,直接设置文本(SearchableComboBox支持) if not found: model_combo.setCurrentText(model) def on_category_changed(self, 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("—— 选择类型 ——") # 重新初始化型号下拉框,显示所有型号 model_combo.clear() model_combo.addItem("—— 选择型号 ——") try: with self.get_conn() as conn: # 加载所有类型 cursor = conn.execute(""" SELECT DISTINCT CASE WHEN category LIKE '%-%' THEN SUBSTR(category, INSTR(category, '-') + 1) ELSE '默认类型' END as fabric_type FROM fabrics WHERE category IS NOT NULL AND category != '' ORDER BY fabric_type """) # 如果选择了具体类目,则过滤 if category_text and category_text != "—— 自定义类目 ——": cursor = conn.execute(""" SELECT DISTINCT CASE WHEN category LIKE '%-%' THEN SUBSTR(category, INSTR(category, '-') + 1) ELSE '默认类型' END as fabric_type FROM fabrics WHERE category LIKE ? OR category = ? ORDER BY fabric_type """, (f"{category_text}-%", category_text)) types = cursor.fetchall() for type_row in types: if type_row[0] and type_row[0] != '默认类型': type_combo.addItem(type_row[0]) # 连接类型改变事件 type_combo.currentTextChanged.connect(lambda text, r=row: self.on_type_changed(text, r)) # 加载所有型号到型号下拉框 cursor = conn.execute(""" SELECT DISTINCT model, color, unit FROM fabrics ORDER BY model """) 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) # 确保默认选中第一项("—— 选择型号 ——") model_combo.setCurrentIndex(0) except Exception as e: pass def on_type_changed(self, type_text, row): """当类型改变时,更新型号下拉框""" cat_combo = self.material_table.cellWidget(row, 0) model_combo = self.material_table.cellWidget(row, 2) # 重新初始化型号下拉框,显示所有型号 if hasattr(model_combo, 'clear'): model_combo.clear() model_combo.addItem("—— 选择型号 ——") # 始终显示所有型号,不进行过滤 try: with self.get_conn() as conn: cursor = conn.execute(""" SELECT DISTINCT model, color, unit FROM fabrics ORDER BY model """) 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) # 确保默认选中第一项("—— 选择型号 ——") model_combo.setCurrentIndex(0) except Exception as e: pass def on_model_selected(self, model_text, row): """当型号选择时,自动设置单位并填充类目和类型""" if not model_text or model_text == "—— 选择型号 ——": return 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) # 获取选中项的数据 current_index = model_combo.currentIndex() if current_index > 0: model = model_combo.itemData(current_index) if model: try: with self.get_conn() as conn: cursor = conn.execute("SELECT category, unit FROM fabrics WHERE model = ?", (model,)) row_db = cursor.fetchone() if row_db: category, unit = row_db # 自动填充单位 if unit: unit_combo.setCurrentText(unit) # 自动填充类目和类型 if category: # 解析类目信息,可能是"类目-类型"格式或单独的类目 if '-' in category: parts = category.split('-', 1) cat_text = parts[0] type_text = parts[1] if len(parts) > 1 else "" # 设置类目 cat_combo.setCurrentText(cat_text) # 更新类型下拉框选项 self.on_category_changed(cat_text, row) # 设置类型 if type_text: type_combo.setCurrentText(type_text) else: # 只有类目,没有类型 cat_combo.setCurrentText(category) self.on_category_changed(category, row) 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 "米" # 分别存储类目、类型和型号信息 material_identifier = final_model if final_model else (f"{category}-{fabric_type}" if fabric_type else category) conn.execute("INSERT INTO garment_materials (style_number, category, fabric_type, usage_per_piece, unit) VALUES (?, ?, ?, ?, ?)", (style_number, material_identifier, fabric_type, usage, unit)) conn.commit() QMessageBox.information(self, "成功", "保存完成") self.accept() except Exception as e: QMessageBox.critical(self, "错误", "保存失败: " + str(e))