This commit is contained in:
2025-12-23 00:30:36 +08:00
parent 192c05707a
commit 033a1acef3
16 changed files with 2870 additions and 1342 deletions

777
garment_dialogs.py Normal file
View File

@@ -0,0 +1,777 @@
"""
服装管理模块 - 处理服装款式和材料用量管理
"""
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))