de306ffe by 柴进

feat(ui): 接入主入口 + 迁移 LoginDialog/ImageGeneratorWindow 样式

- main() 创建 QApplication 后立即 apply_theme,所有后续 widget(含 preflight QMessageBox)都吃全局主题
- LoginDialog: 删 80 行 inline QSS 块,objectName=loginDialog/loginTitle,主按钮 variant=primary,error_label status property
- ImageGeneratorWindow: 删 50 行 apply_styles QSS 块,删 30 处 inline setStyleSheet
- "生成图片" 主按钮 variant=primary(pill 圆角 + accent 实色背景)
- status_label 全部用 _set_status(success|warning|danger|info|muted) helper
- DragDropScrollArea 拖拽态走 drag_state property(idle/active)
- 缩略图 role=thumb/thumb_index,删除按钮 #thumbDeleteBtn variant=danger
- generated_image_label has_image property,"已复制" success-flash variant
- preview_label/prompt_display 走 objectName + 全局 QSS

剩余:StyleDesignerTab 和 task_queue.py。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3bc29f7b
...@@ -1241,26 +1241,33 @@ class LoginDialog(QDialog): ...@@ -1241,26 +1241,33 @@ class LoginDialog(QDialog):
1241 1241
1242 def setup_ui(self): 1242 def setup_ui(self):
1243 """Build login dialog UI""" 1243 """Build login dialog UI"""
1244 self.setObjectName("loginDialog")
1244 self.setWindowTitle("登录 - 珠宝壹佰图像生成器") 1245 self.setWindowTitle("登录 - 珠宝壹佰图像生成器")
1245 self.setFixedSize(400, 400) 1246 self.setFixedSize(400, 420)
1246 1247
1247 # Main layout 1248 # Main layout
1248 main_layout = QVBoxLayout() 1249 main_layout = QVBoxLayout()
1249 main_layout.setContentsMargins(40, 40, 40, 40) 1250 main_layout.setContentsMargins(40, 40, 40, 40)
1250 main_layout.setSpacing(20) 1251 main_layout.setSpacing(16)
1251 1252
1252 # Title 1253 # Title
1253 title_label = QLabel("登录") 1254 title_label = QLabel("登录")
1254 title_label.setObjectName("title") 1255 title_label.setObjectName("loginTitle")
1256 title_label.setProperty("role", "title")
1255 title_label.setAlignment(Qt.AlignCenter) 1257 title_label.setAlignment(Qt.AlignCenter)
1256 main_layout.addWidget(title_label) 1258 main_layout.addWidget(title_label)
1257 1259
1258 main_layout.addSpacing(10) 1260 subtitle_label = QLabel("珠宝壹佰图像生成器")
1261 subtitle_label.setObjectName("loginSubtitle")
1262 subtitle_label.setProperty("role", "secondary")
1263 subtitle_label.setAlignment(Qt.AlignCenter)
1264 main_layout.addWidget(subtitle_label)
1265
1266 main_layout.addSpacing(8)
1259 1267
1260 # Username field 1268 # Username field
1261 username_label = QLabel("用户名") 1269 username_label = QLabel("用户名")
1262 username_label.setObjectName("field_label") 1270 username_label.setProperty("role", "caption")
1263
1264 main_layout.addWidget(username_label) 1271 main_layout.addWidget(username_label)
1265 1272
1266 self.username_entry = QLineEdit() 1273 self.username_entry = QLineEdit()
...@@ -1268,11 +1275,9 @@ class LoginDialog(QDialog): ...@@ -1268,11 +1275,9 @@ class LoginDialog(QDialog):
1268 self.username_entry.setFixedHeight(40) 1275 self.username_entry.setFixedHeight(40)
1269 main_layout.addWidget(self.username_entry) 1276 main_layout.addWidget(self.username_entry)
1270 1277
1271 main_layout.addSpacing(10)
1272
1273 # Password field 1278 # Password field
1274 password_label = QLabel("密码") 1279 password_label = QLabel("密码")
1275 password_label.setObjectName("field_label") 1280 password_label.setProperty("role", "caption")
1276 main_layout.addWidget(password_label) 1281 main_layout.addWidget(password_label)
1277 1282
1278 self.password_entry = QLineEdit() 1283 self.password_entry = QLineEdit()
...@@ -1280,6 +1285,8 @@ class LoginDialog(QDialog): ...@@ -1280,6 +1285,8 @@ class LoginDialog(QDialog):
1280 self.password_entry.setFixedHeight(40) 1285 self.password_entry.setFixedHeight(40)
1281 1286
1282 # Handle saved password placeholder 1287 # Handle saved password placeholder
1288 # 占位符态用 inline 语义化 hint:已保存密码时整体灰一点,
1289 # 用户开始输入会触发 on_password_change 清除。
1283 if self.saved_password_hash: 1290 if self.saved_password_hash:
1284 self.password_entry.setPlaceholderText("••••••••") 1291 self.password_entry.setPlaceholderText("••••••••")
1285 self.password_entry.setStyleSheet("QLineEdit { color: #999999; }") 1292 self.password_entry.setStyleSheet("QLineEdit { color: #999999; }")
...@@ -1302,16 +1309,20 @@ class LoginDialog(QDialog): ...@@ -1302,16 +1309,20 @@ class LoginDialog(QDialog):
1302 checkbox_layout.addStretch() 1309 checkbox_layout.addStretch()
1303 main_layout.addLayout(checkbox_layout) 1310 main_layout.addLayout(checkbox_layout)
1304 1311
1305 # Login button 1312 main_layout.addSpacing(4)
1313
1314 # Login button - 主按钮
1306 self.login_button = QPushButton("登录") 1315 self.login_button = QPushButton("登录")
1307 self.login_button.setObjectName("login_button") 1316 self.login_button.setObjectName("login_button")
1317 self.login_button.setProperty("variant", "primary")
1308 self.login_button.setFixedHeight(40) 1318 self.login_button.setFixedHeight(40)
1309 self.login_button.clicked.connect(self.on_login) 1319 self.login_button.clicked.connect(self.on_login)
1310 main_layout.addWidget(self.login_button) 1320 main_layout.addWidget(self.login_button)
1311 1321
1312 # Error label 1322 # Error / info label
1313 self.error_label = QLabel("") 1323 self.error_label = QLabel("")
1314 self.error_label.setObjectName("error_label") 1324 self.error_label.setObjectName("error_label")
1325 self.error_label.setProperty("status", "muted")
1315 self.error_label.setAlignment(Qt.AlignCenter) 1326 self.error_label.setAlignment(Qt.AlignCenter)
1316 self.error_label.setWordWrap(True) 1327 self.error_label.setWordWrap(True)
1317 main_layout.addWidget(self.error_label) 1328 main_layout.addWidget(self.error_label)
...@@ -1327,58 +1338,16 @@ class LoginDialog(QDialog): ...@@ -1327,58 +1338,16 @@ class LoginDialog(QDialog):
1327 self.username_entry.setFocus() 1338 self.username_entry.setFocus()
1328 1339
1329 def apply_styles(self): 1340 def apply_styles(self):
1330 """Apply QSS stylesheet""" 1341 """登录页样式由全局 theme.py 提供,这里只设置 objectName / property。"""
1331 self.setStyleSheet(""" 1342 # objectName / property 已在 setup_ui 里设置完毕,全局 QSS 自动命中。
1332 QDialog { 1343 pass
1333 background-color: #ffffff; 1344
1334 } 1345 def _set_error(self, status: str, text: str = "") -> None:
1335 QLabel#title { 1346 """统一设置 error_label。status: muted|danger|success|info"""
1336 font-size: 20pt; 1347 self.error_label.setText(text)
1337 font-weight: bold; 1348 self.error_label.setProperty("status", status)
1338 color: #1d1d1f; 1349 self.error_label.style().unpolish(self.error_label)
1339 } 1350 self.error_label.style().polish(self.error_label)
1340 QLabel#field_label {
1341 font-size: 10pt;
1342 color: #666666;
1343 }
1344 QLineEdit {
1345 padding: 8px;
1346 border: 1px solid #e5e5e5;
1347 border-radius: 4px;
1348 background-color: #fafafa;
1349 font-size: 11pt;
1350 color: #000000;
1351 }
1352 QLineEdit:focus {
1353 border: 1px solid #007AFF;
1354 }
1355 QPushButton#login_button {
1356 background-color: #007AFF;
1357 color: white;
1358 font-size: 10pt;
1359 font-weight: bold;
1360 padding: 10px 20px;
1361 border: none;
1362 border-radius: 6px;
1363 }
1364 QPushButton#login_button:hover {
1365 background-color: #0051D5;
1366 }
1367 QPushButton#login_button:pressed {
1368 background-color: #003D99;
1369 }
1370 QPushButton#login_button:disabled {
1371 background-color: #cccccc;
1372 }
1373 QCheckBox {
1374 font-size: 9pt;
1375 color: #1d1d1f;
1376 }
1377 QLabel#error_label {
1378 color: #ff3b30;
1379 font-size: 9pt;
1380 }
1381 """)
1382 1351
1383 def on_password_change(self): 1352 def on_password_change(self):
1384 """Handle password field changes""" 1353 """Handle password field changes"""
...@@ -1404,8 +1373,7 @@ class LoginDialog(QDialog): ...@@ -1404,8 +1373,7 @@ class LoginDialog(QDialog):
1404 1373
1405 # Disable button during authentication 1374 # Disable button during authentication
1406 self.login_button.setEnabled(False) 1375 self.login_button.setEnabled(False)
1407 self.error_label.setText("正在验证...") 1376 self._set_error("muted", "正在验证...")
1408 self.error_label.setStyleSheet("QLabel { color: #666666; }")
1409 1377
1410 # Determine which password to use 1378 # Determine which password to use
1411 if not self.password_changed and self.saved_password_hash: 1379 if not self.password_changed and self.saved_password_hash:
...@@ -1541,8 +1509,7 @@ class LoginDialog(QDialog): ...@@ -1541,8 +1509,7 @@ class LoginDialog(QDialog):
1541 QMessageBox.critical(self, "登录错误", message) 1509 QMessageBox.critical(self, "登录错误", message)
1542 1510
1543 # 同时保留标签显示 1511 # 同时保留标签显示
1544 self.error_label.setText(message) 1512 self._set_error("danger", message)
1545 self.error_label.setStyleSheet("QLabel { color: #ff3b30; }")
1546 1513
1547 def get_remember_user(self): 1514 def get_remember_user(self):
1548 """Get remember username checkbox state""" 1515 """Get remember username checkbox state"""
...@@ -1599,17 +1566,14 @@ class DragDropScrollArea(QScrollArea): ...@@ -1599,17 +1566,14 @@ class DragDropScrollArea(QScrollArea):
1599 super().__init__(parent) 1566 super().__init__(parent)
1600 self.setAcceptDrops(True) 1567 self.setAcceptDrops(True)
1601 self.parent_window = parent 1568 self.parent_window = parent
1602 self.setStyleSheet(""" 1569 self.setObjectName("referenceImageDrop")
1603 QScrollArea { 1570 self._set_drag_state("idle")
1604 border: 2px dashed #e5e5e5; 1571
1605 border-radius: 8px; 1572 def _set_drag_state(self, state: str) -> None:
1606 background-color: #fafafa; 1573 """状态切换:'idle' | 'active'。视觉由全局 theme.py 控制。"""
1607 } 1574 self.setProperty("drag_state", state)
1608 QScrollArea:hover { 1575 self.style().unpolish(self)
1609 border-color: #007AFF; 1576 self.style().polish(self)
1610 background-color: #f0f8ff;
1611 }
1612 """)
1613 1577
1614 def dragEnterEvent(self, event: QDragEnterEvent): 1578 def dragEnterEvent(self, event: QDragEnterEvent):
1615 """拖拽进入事件处理""" 1579 """拖拽进入事件处理"""
...@@ -1628,26 +1592,14 @@ class DragDropScrollArea(QScrollArea): ...@@ -1628,26 +1592,14 @@ class DragDropScrollArea(QScrollArea):
1628 file_path = url.toLocalFile() 1592 file_path = url.toLocalFile()
1629 if self.is_valid_image_file(file_path): 1593 if self.is_valid_image_file(file_path):
1630 event.acceptProposedAction() 1594 event.acceptProposedAction()
1631 self.setStyleSheet(""" 1595 self._set_drag_state("active")
1632 QScrollArea {
1633 border: 2px dashed #007AFF;
1634 border-radius: 8px;
1635 background-color: #e6f3ff;
1636 }
1637 """)
1638 return 1596 return
1639 1597
1640 # 检查剪贴板图像 1598 # 检查剪贴板图像
1641 try: 1599 try:
1642 if mime_data.hasImage(): 1600 if mime_data.hasImage():
1643 event.acceptProposedAction() 1601 event.acceptProposedAction()
1644 self.setStyleSheet(""" 1602 self._set_drag_state("active")
1645 QScrollArea {
1646 border: 2px dashed #007AFF;
1647 border-radius: 8px;
1648 background-color: #e6f3ff;
1649 }
1650 """)
1651 return 1603 return
1652 except Exception: 1604 except Exception:
1653 pass 1605 pass
...@@ -1656,34 +1608,14 @@ class DragDropScrollArea(QScrollArea): ...@@ -1656,34 +1608,14 @@ class DragDropScrollArea(QScrollArea):
1656 1608
1657 def dragLeaveEvent(self, event): 1609 def dragLeaveEvent(self, event):
1658 """拖拽离开事件处理""" 1610 """拖拽离开事件处理"""
1659 self.setStyleSheet(""" 1611 self._set_drag_state("idle")
1660 QScrollArea {
1661 border: 2px dashed #e5e5e5;
1662 border-radius: 8px;
1663 background-color: #fafafa;
1664 }
1665 QScrollArea:hover {
1666 border-color: #007AFF;
1667 background-color: #f0f8ff;
1668 }
1669 """)
1670 1612
1671 def dropEvent(self, event: QDropEvent): 1613 def dropEvent(self, event: QDropEvent):
1672 """拖拽放置事件处理""" 1614 """拖拽放置事件处理"""
1673 mime_data = event.mimeData() 1615 mime_data = event.mimeData()
1674 1616
1675 # 重置样式 1617 # 重置样式
1676 self.setStyleSheet(""" 1618 self._set_drag_state("idle")
1677 QScrollArea {
1678 border: 2px dashed #e5e5e5;
1679 border-radius: 8px;
1680 background-color: #fafafa;
1681 }
1682 QScrollArea:hover {
1683 border-color: #007AFF;
1684 background-color: #f0f8ff;
1685 }
1686 """)
1687 1619
1688 # 内部缩略图重排 1620 # 内部缩略图重排
1689 if mime_data.hasFormat(THUMB_REORDER_MIME): 1621 if mime_data.hasFormat(THUMB_REORDER_MIME):
...@@ -2094,31 +2026,15 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2094,31 +2026,15 @@ class ImageGeneratorWindow(QMainWindow):
2094 # Paste button 2026 # Paste button
2095 paste_btn = QPushButton("📋 粘贴图片") 2027 paste_btn = QPushButton("📋 粘贴图片")
2096 paste_btn.clicked.connect(self.paste_from_clipboard) 2028 paste_btn.clicked.connect(self.paste_from_clipboard)
2097 paste_btn.setStyleSheet("""
2098 QPushButton {
2099 background-color: #f0f0f0;
2100 border: 1px solid #d0d0d0;
2101 padding: 8px 16px;
2102 border-radius: 4px;
2103 font-size: 12px;
2104 min-width: 80px;
2105 }
2106 QPushButton:hover {
2107 background-color: #e8e8e8;
2108 border-color: #007AFF;
2109 }
2110 QPushButton:pressed {
2111 background-color: #d0d0d0;
2112 }
2113 """)
2114 upload_header.addWidget(paste_btn) 2029 upload_header.addWidget(paste_btn)
2115 2030
2116 self.image_count_label = QLabel("已选择 0 张") 2031 self.image_count_label = QLabel("已选择 0 张")
2032 self.image_count_label.setProperty("role", "secondary")
2117 upload_header.addWidget(self.image_count_label) 2033 upload_header.addWidget(self.image_count_label)
2118 2034
2119 # Drag and drop hint - 紧凑显示在右侧 2035 # Drag and drop hint - 紧凑显示在右侧
2120 hint_label = QLabel("💡 拖拽或粘贴图片到下方区域") 2036 hint_label = QLabel("💡 拖拽或粘贴图片到下方区域")
2121 hint_label.setStyleSheet("QLabel { color: #666666; font-size: 11px; margin-left: 10px; }") 2037 hint_label.setProperty("role", "muted")
2122 upload_header.addWidget(hint_label) 2038 upload_header.addWidget(hint_label)
2123 2039
2124 upload_header.addStretch() 2040 upload_header.addStretch()
...@@ -2167,7 +2083,6 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2167,7 +2083,6 @@ class ImageGeneratorWindow(QMainWindow):
2167 2083
2168 # Prompt text area 2084 # Prompt text area
2169 self.prompt_text = QTextEdit() 2085 self.prompt_text = QTextEdit()
2170 self.prompt_text.setStyleSheet("font-size: 16px;")
2171 self.prompt_text.setPlainText("一幅美丽的风景画,有山有湖,日落时分") 2086 self.prompt_text.setPlainText("一幅美丽的风景画,有山有湖,日落时分")
2172 self.prompt_text.textChanged.connect(self.check_favorite_status) 2087 self.prompt_text.textChanged.connect(self.check_favorite_status)
2173 prompt_layout.addWidget(self.prompt_text) 2088 prompt_layout.addWidget(self.prompt_text)
...@@ -2226,6 +2141,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2226,6 +2141,7 @@ class ImageGeneratorWindow(QMainWindow):
2226 # Action buttons 2141 # Action buttons
2227 action_layout = QHBoxLayout() 2142 action_layout = QHBoxLayout()
2228 self.generate_btn = QPushButton("生成图片") 2143 self.generate_btn = QPushButton("生成图片")
2144 self.generate_btn.setProperty("variant", "primary")
2229 self.generate_btn.clicked.connect(self.generate_image_async) 2145 self.generate_btn.clicked.connect(self.generate_image_async)
2230 action_layout.addWidget(self.generate_btn) 2146 action_layout.addWidget(self.generate_btn)
2231 2147
...@@ -2235,6 +2151,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2235,6 +2151,7 @@ class ImageGeneratorWindow(QMainWindow):
2235 action_layout.addWidget(self.download_btn) 2151 action_layout.addWidget(self.download_btn)
2236 2152
2237 self.status_label = QLabel("● 就绪") 2153 self.status_label = QLabel("● 就绪")
2154 self.status_label.setProperty("status", "muted")
2238 action_layout.addWidget(self.status_label) 2155 action_layout.addWidget(self.status_label)
2239 action_layout.addStretch() 2156 action_layout.addStretch()
2240 2157
...@@ -2245,9 +2162,9 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2245,9 +2162,9 @@ class ImageGeneratorWindow(QMainWindow):
2245 preview_layout = QVBoxLayout() 2162 preview_layout = QVBoxLayout()
2246 2163
2247 self.preview_label = QLabel("生成的图片将在这里显示\n双击用系统查看器打开") 2164 self.preview_label = QLabel("生成的图片将在这里显示\n双击用系统查看器打开")
2165 self.preview_label.setObjectName("previewPlaceholder")
2248 self.preview_label.setAlignment(Qt.AlignCenter) 2166 self.preview_label.setAlignment(Qt.AlignCenter)
2249 self.preview_label.setMinimumHeight(300) 2167 self.preview_label.setMinimumHeight(300)
2250 self.preview_label.setStyleSheet("QLabel { color: #999999; font-size: 10pt; }")
2251 self.preview_label.mouseDoubleClickEvent = self.open_fullsize_view 2168 self.preview_label.mouseDoubleClickEvent = self.open_fullsize_view
2252 2169
2253 preview_layout.addWidget(self.preview_label) 2170 preview_layout.addWidget(self.preview_label)
...@@ -2348,9 +2265,8 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2348,9 +2265,8 @@ class ImageGeneratorWindow(QMainWindow):
2348 prompt_layout.addLayout(prompt_header) 2265 prompt_layout.addLayout(prompt_header)
2349 2266
2350 self.prompt_display = QLabel("请选择一个历史记录查看详情") 2267 self.prompt_display = QLabel("请选择一个历史记录查看详情")
2268 self.prompt_display.setObjectName("promptDisplay")
2351 self.prompt_display.setWordWrap(True) 2269 self.prompt_display.setWordWrap(True)
2352 self.prompt_display.setStyleSheet(
2353 "QLabel { padding: 8px; background-color: #f9f9f9; border: 1px solid #ddd; border-radius: 4px; }")
2354 prompt_layout.addWidget(self.prompt_display) 2270 prompt_layout.addWidget(self.prompt_display)
2355 2271
2356 prompt_group.setLayout(prompt_layout) 2272 prompt_group.setLayout(prompt_layout)
...@@ -2403,11 +2319,10 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2403,11 +2319,10 @@ class ImageGeneratorWindow(QMainWindow):
2403 gen_layout.setAlignment(Qt.AlignCenter) 2319 gen_layout.setAlignment(Qt.AlignCenter)
2404 2320
2405 self.generated_image_label = QLabel("请选择一个历史记录查看生成图片") 2321 self.generated_image_label = QLabel("请选择一个历史记录查看生成图片")
2322 self.generated_image_label.setObjectName("previewImage")
2406 self.generated_image_label.setAlignment(Qt.AlignCenter) 2323 self.generated_image_label.setAlignment(Qt.AlignCenter)
2407 self.generated_image_label.setMinimumSize(200, 200) # Larger size for generated image 2324 self.generated_image_label.setMinimumSize(200, 200) # Larger size for generated image
2408 self.generated_image_label.setMaximumSize(300, 300) 2325 self.generated_image_label.setMaximumSize(300, 300)
2409 self.generated_image_label.setStyleSheet(
2410 "QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }")
2411 self.generated_image_label.mouseDoubleClickEvent = self.open_generated_image_from_history 2326 self.generated_image_label.mouseDoubleClickEvent = self.open_generated_image_from_history
2412 2327
2413 gen_layout.addWidget(self.generated_image_label) 2328 gen_layout.addWidget(self.generated_image_label)
...@@ -2422,60 +2337,25 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2422,60 +2337,25 @@ class ImageGeneratorWindow(QMainWindow):
2422 return panel 2337 return panel
2423 2338
2424 def apply_styles(self): 2339 def apply_styles(self):
2425 """Apply QSS stylesheet""" 2340 """主窗口样式由全局 theme.py 提供,这里只接最后的信号。"""
2426 """Apply QSS stylesheet - 最小化自定义样式""" 2341 self.setObjectName("mainWindow")
2427 self.setStyleSheet("""
2428 QMainWindow {
2429 background-color: #ffffff;
2430 }
2431 QGroupBox {
2432 font-weight: bold;
2433 font-size: 10pt;
2434 border: 1px solid #e5e5e5;
2435 border-radius: 6px;
2436 margin-top: 2px;
2437 padding-top: 2px;
2438 }
2439 QGroupBox::title {
2440 subcontrol-origin: margin;
2441 left: 10px;
2442 padding: 0 5px;
2443 }
2444 QPushButton {
2445 padding: 6px 12px;
2446 font-size: 9pt;
2447 border: 1px solid #cccccc;
2448 border-radius: 4px;
2449 }
2450 QPushButton:disabled {
2451 color: #999999;
2452 }
2453 QComboBox {
2454 padding: 5px;
2455 min-width: 100px;
2456 }
2457 QTextEdit {
2458 border: 1px solid #e5e5e5;
2459 border-radius: 4px;
2460 font-size: 10pt;
2461 }
2462 QScrollArea {
2463 border: none;
2464 background-color: #f6f6f6;
2465 }
2466 /* 只保留必要的边框和背景,不设置颜色 */
2467 QLineEdit, QComboBox, QTextEdit {
2468 border: 1px solid #cccccc;
2469 border-radius: 4px;
2470 }
2471 QLineEdit:focus, QComboBox:focus, QTextEdit:focus {
2472 border: 1px solid #007AFF; /* 保持焦点状态 */
2473 }
2474 """)
2475
2476 # Connect signals after all widgets are created 2342 # Connect signals after all widgets are created
2477 self.saved_prompts_combo.currentIndexChanged.connect(self.load_saved_prompt) 2343 self.saved_prompts_combo.currentIndexChanged.connect(self.load_saved_prompt)
2478 2344
2345 def _set_status(self, status: str, text: str | None = None) -> None:
2346 """统一状态标签视觉。status: success|warning|danger|info|muted"""
2347 if text is not None:
2348 self.status_label.setText(text)
2349 self.status_label.setProperty("status", status)
2350 self.status_label.style().unpolish(self.status_label)
2351 self.status_label.style().polish(self.status_label)
2352
2353 def _set_image_state(self, label, has_image: bool) -> None:
2354 """切换 #previewImage 的 has_image 属性,让 QSS 命中对应外观。"""
2355 label.setProperty("has_image", "true" if has_image else "false")
2356 label.style().unpolish(label)
2357 label.style().polish(label)
2358
2479 def upload_images(self): 2359 def upload_images(self):
2480 """Upload reference images""" 2360 """Upload reference images"""
2481 self.logger.info("开始上传参考图片") 2361 self.logger.info("开始上传参考图片")
...@@ -2504,7 +2384,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2504,7 +2384,7 @@ class ImageGeneratorWindow(QMainWindow):
2504 self.update_image_preview() 2384 self.update_image_preview()
2505 self.image_count_label.setText(f"已选择 {len(self.uploaded_images)} 张") 2385 self.image_count_label.setText(f"已选择 {len(self.uploaded_images)} 张")
2506 self.status_label.setText(f"● 已添加 {valid_count} 张参考图片") 2386 self.status_label.setText(f"● 已添加 {valid_count} 张参考图片")
2507 self.status_label.setStyleSheet("QLabel { color: #34C759; }") 2387 self._set_status("success")
2508 self.logger.info(f"图片上传完成,有效图片: {valid_count} 张") 2388 self.logger.info(f"图片上传完成,有效图片: {valid_count} 张")
2509 2389
2510 # 检查极速模式下的多图限制 2390 # 检查极速模式下的多图限制
...@@ -2537,7 +2417,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2537,7 +2417,7 @@ class ImageGeneratorWindow(QMainWindow):
2537 self.update_image_preview() 2417 self.update_image_preview()
2538 self.image_count_label.setText(f"已选择 {len(self.uploaded_images)} 张") 2418 self.image_count_label.setText(f"已选择 {len(self.uploaded_images)} 张")
2539 self.status_label.setText(f"● 已通过拖拽添加 {added_count} 张参考图片") 2419 self.status_label.setText(f"● 已通过拖拽添加 {added_count} 张参考图片")
2540 self.status_label.setStyleSheet("QLabel { color: #34C759; }") 2420 self._set_status("success")
2541 2421
2542 # 检查极速模式下的多图限制 2422 # 检查极速模式下的多图限制
2543 # self.check_multi_image_mode_conflict() 2423 # self.check_multi_image_mode_conflict()
...@@ -2572,7 +2452,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2572,7 +2452,7 @@ class ImageGeneratorWindow(QMainWindow):
2572 self.update_image_preview() 2452 self.update_image_preview()
2573 self.image_count_label.setText(f"已选择 {len(self.uploaded_images)} 张") 2453 self.image_count_label.setText(f"已选择 {len(self.uploaded_images)} 张")
2574 self.status_label.setText("● 已添加剪贴板图片") 2454 self.status_label.setText("● 已添加剪贴板图片")
2575 self.status_label.setStyleSheet("QLabel { color: #34C759; }") 2455 self._set_status("success")
2576 else: 2456 else:
2577 QMessageBox.critical(self, "错误", "无法保存剪贴板图片") 2457 QMessageBox.critical(self, "错误", "无法保存剪贴板图片")
2578 2458
...@@ -2694,7 +2574,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2694,7 +2574,7 @@ class ImageGeneratorWindow(QMainWindow):
2694 self.update_image_preview() 2574 self.update_image_preview()
2695 self.image_count_label.setText(f"已选择 {len(self.uploaded_images)} 张") 2575 self.image_count_label.setText(f"已选择 {len(self.uploaded_images)} 张")
2696 self.status_label.setText(f"● 已粘贴 {added} 张图片") 2576 self.status_label.setText(f"● 已粘贴 {added} 张图片")
2697 self.status_label.setStyleSheet("QLabel { color: #34C759; }") 2577 self._set_status("success")
2698 return 2578 return
2699 2579
2700 # 没有文件URL,尝试获取纯图像数据(截图、从应用复制的图片等) 2580 # 没有文件URL,尝试获取纯图像数据(截图、从应用复制的图片等)
...@@ -2728,7 +2608,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2728,7 +2608,7 @@ class ImageGeneratorWindow(QMainWindow):
2728 self.update_image_preview() 2608 self.update_image_preview()
2729 self.image_count_label.setText(f"已选择 {len(self.uploaded_images)} 张") 2609 self.image_count_label.setText(f"已选择 {len(self.uploaded_images)} 张")
2730 self.status_label.setText("● 已添加剪贴板图片") 2610 self.status_label.setText("● 已添加剪贴板图片")
2731 self.status_label.setStyleSheet("QLabel { color: #34C759; }") 2611 self._set_status("success")
2732 2612
2733 except Exception as e: 2613 except Exception as e:
2734 self.logger.error(f"粘贴失败: {e}", exc_info=True) 2614 self.logger.error(f"粘贴失败: {e}", exc_info=True)
...@@ -2812,32 +2692,21 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2812,32 +2692,21 @@ class ImageGeneratorWindow(QMainWindow):
2812 2692
2813 # Image label 2693 # Image label
2814 img_label = QLabel() 2694 img_label = QLabel()
2695 img_label.setProperty("role", "thumb")
2815 img_label.setPixmap(pixmap) 2696 img_label.setPixmap(pixmap)
2816 img_label.setFixedSize(100, 100) 2697 img_label.setFixedSize(100, 100)
2817 img_label.setStyleSheet("QLabel { border: 1px solid #e5e5e5; }")
2818 container_layout.addWidget(img_label) 2698 container_layout.addWidget(img_label)
2819 2699
2820 # Info row 2700 # Info row
2821 info_layout = QHBoxLayout() 2701 info_layout = QHBoxLayout()
2822 index_label = QLabel(f"图 {idx + 1}") 2702 index_label = QLabel(f"图 {idx + 1}")
2823 index_label.setStyleSheet("QLabel { font-size: 12pt; color: #666666; font-weight: bold; }") 2703 index_label.setProperty("role", "thumb_index")
2824 info_layout.addWidget(index_label) 2704 info_layout.addWidget(index_label)
2825 2705
2826 del_btn = QPushButton("✕") 2706 del_btn = QPushButton("✕")
2707 del_btn.setObjectName("thumbDeleteBtn")
2708 del_btn.setProperty("variant", "danger")
2827 del_btn.setFixedSize(20, 20) 2709 del_btn.setFixedSize(20, 20)
2828 del_btn.setStyleSheet("""
2829 QPushButton {
2830 background-color: #ff4444;
2831 color: white;
2832 font-weight: bold;
2833 border: none;
2834 border-radius: 3px;
2835 padding: 0px;
2836 }
2837 QPushButton:hover {
2838 background-color: #FF3B30;
2839 }
2840 """)
2841 del_btn.clicked.connect(lambda checked, i=idx: self.delete_image(i)) 2710 del_btn.clicked.connect(lambda checked, i=idx: self.delete_image(i))
2842 info_layout.addWidget(del_btn) 2711 info_layout.addWidget(del_btn)
2843 2712
...@@ -2862,7 +2731,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2862,7 +2731,7 @@ class ImageGeneratorWindow(QMainWindow):
2862 self.update_image_preview() 2731 self.update_image_preview()
2863 self.image_count_label.setText(f"已选择 {len(self.uploaded_images)} 张") 2732 self.image_count_label.setText(f"已选择 {len(self.uploaded_images)} 张")
2864 self.status_label.setText("● 已删除图片") 2733 self.status_label.setText("● 已删除图片")
2865 self.status_label.setStyleSheet("QLabel { color: #FF9500; }") 2734 self._set_status("warning")
2866 else: 2735 else:
2867 self.logger.warning(f"无效的图片索引: {index}") 2736 self.logger.warning(f"无效的图片索引: {index}")
2868 2737
...@@ -2888,7 +2757,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2888,7 +2757,7 @@ class ImageGeneratorWindow(QMainWindow):
2888 self.logger.info(f"缩略图重排: {src_index} -> {target_index}") 2757 self.logger.info(f"缩略图重排: {src_index} -> {target_index}")
2889 self.update_image_preview() 2758 self.update_image_preview()
2890 self.status_label.setText(f"● 已调整图片顺序 (图 {src_index + 1} → 图 {target_index + 1})") 2759 self.status_label.setText(f"● 已调整图片顺序 (图 {src_index + 1} → 图 {target_index + 1})")
2891 self.status_label.setStyleSheet("QLabel { color: #007AFF; }") 2760 self._set_status("info")
2892 2761
2893 def update_saved_prompts_list(self): 2762 def update_saved_prompts_list(self):
2894 """Update the saved prompts dropdown""" 2763 """Update the saved prompts dropdown"""
...@@ -2913,7 +2782,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2913,7 +2782,7 @@ class ImageGeneratorWindow(QMainWindow):
2913 prompt = self.prompt_text.toPlainText().strip() 2782 prompt = self.prompt_text.toPlainText().strip()
2914 if not prompt: 2783 if not prompt:
2915 self.status_label.setText("● 提示词不能为空") 2784 self.status_label.setText("● 提示词不能为空")
2916 self.status_label.setStyleSheet("QLabel { color: #FF3B30; }") 2785 self._set_status("danger")
2917 return 2786 return
2918 2787
2919 if prompt in self.saved_prompts: 2788 if prompt in self.saved_prompts:
...@@ -2927,7 +2796,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2927,7 +2796,7 @@ class ImageGeneratorWindow(QMainWindow):
2927 self.update_saved_prompts_list() 2796 self.update_saved_prompts_list()
2928 self.status_label.setText("● 该提示词已收藏") 2797 self.status_label.setText("● 该提示词已收藏")
2929 2798
2930 self.status_label.setStyleSheet("QLabel { color: #34C759; }") 2799 self._set_status("success")
2931 self.check_favorite_status() 2800 self.check_favorite_status()
2932 2801
2933 def load_saved_prompt(self): 2802 def load_saved_prompt(self):
...@@ -2936,7 +2805,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2936,7 +2805,7 @@ class ImageGeneratorWindow(QMainWindow):
2936 if 0 <= index < len(self.saved_prompts): 2805 if 0 <= index < len(self.saved_prompts):
2937 self.prompt_text.setPlainText(self.saved_prompts[index]) 2806 self.prompt_text.setPlainText(self.saved_prompts[index])
2938 self.status_label.setText("● 已加载提示词") 2807 self.status_label.setText("● 已加载提示词")
2939 self.status_label.setStyleSheet("QLabel { color: #007AFF; }") 2808 self._set_status("info")
2940 2809
2941 def delete_saved_prompt(self): 2810 def delete_saved_prompt(self):
2942 """Delete the currently selected saved prompt""" 2811 """Delete the currently selected saved prompt"""
...@@ -2944,14 +2813,14 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2944,14 +2813,14 @@ class ImageGeneratorWindow(QMainWindow):
2944 2813
2945 if index < 0 or index >= len(self.saved_prompts): 2814 if index < 0 or index >= len(self.saved_prompts):
2946 self.status_label.setText("● 请先选择要删除的提示词") 2815 self.status_label.setText("● 请先选择要删除的提示词")
2947 self.status_label.setStyleSheet("QLabel { color: #FF9500; }") 2816 self._set_status("warning")
2948 return 2817 return
2949 2818
2950 self.saved_prompts.pop(index) 2819 self.saved_prompts.pop(index)
2951 self.save_config() 2820 self.save_config()
2952 self.update_saved_prompts_list() 2821 self.update_saved_prompts_list()
2953 self.status_label.setText("● 已删除提示词") 2822 self.status_label.setText("● 已删除提示词")
2954 self.status_label.setStyleSheet("QLabel { color: #34C759; }") 2823 self._set_status("success")
2955 2824
2956 def get_selected_model(self): 2825 def get_selected_model(self):
2957 """根据生成模式返回对应的模型名称""" 2826 """根据生成模式返回对应的模型名称"""
...@@ -3049,7 +2918,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -3049,7 +2918,7 @@ class ImageGeneratorWindow(QMainWindow):
3049 # self.update_image_preview() 2918 # self.update_image_preview()
3050 # self.image_count_label.setText(f"已选择 {len(self.uploaded_images)} 张") 2919 # self.image_count_label.setText(f"已选择 {len(self.uploaded_images)} 张")
3051 # self.status_label.setText("● 已保留第一张参考图片") 2920 # self.status_label.setText("● 已保留第一张参考图片")
3052 # self.status_label.setStyleSheet("QLabel { color: #FF9500; }") 2921 # self._set_status("warning")
3053 2922
3054 def generate_image_async(self): 2923 def generate_image_async(self):
3055 """Submit image generation task to queue""" 2924 """Submit image generation task to queue"""
...@@ -3099,7 +2968,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -3099,7 +2968,7 @@ class ImageGeneratorWindow(QMainWindow):
3099 2968
3100 # Update UI 2969 # Update UI
3101 self.status_label.setText(f"● 任务已提交 (ID: {task_id[:8]})") 2970 self.status_label.setText(f"● 任务已提交 (ID: {task_id[:8]})")
3102 self.status_label.setStyleSheet("QLabel { color: #007AFF; }") 2971 self._set_status("info")
3103 2972
3104 except RuntimeError as e: 2973 except RuntimeError as e:
3105 QMessageBox.warning(self, "队列已满", str(e)) 2974 QMessageBox.warning(self, "队列已满", str(e))
...@@ -3115,7 +2984,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -3115,7 +2984,7 @@ class ImageGeneratorWindow(QMainWindow):
3115 self.display_image() 2984 self.display_image()
3116 self.download_btn.setEnabled(True) 2985 self.download_btn.setEnabled(True)
3117 self.status_label.setText("● 图片生成成功") 2986 self.status_label.setText("● 图片生成成功")
3118 self.status_label.setStyleSheet("QLabel { color: #34C759; }") 2987 self._set_status("success")
3119 2988
3120 # Save to history 2989 # Save to history
3121 try: 2990 try:
...@@ -3139,7 +3008,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -3139,7 +3008,7 @@ class ImageGeneratorWindow(QMainWindow):
3139 self.download_btn.setEnabled(True) 3008 self.download_btn.setEnabled(True)
3140 self.generate_btn.setEnabled(True) 3009 self.generate_btn.setEnabled(True)
3141 self.status_label.setText("● 图片生成成功") 3010 self.status_label.setText("● 图片生成成功")
3142 self.status_label.setStyleSheet("QLabel { color: #34C759; }") 3011 self._set_status("success")
3143 3012
3144 # 自动保存到历史记录 3013 # 自动保存到历史记录
3145 try: 3014 try:
...@@ -3162,7 +3031,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -3162,7 +3031,7 @@ class ImageGeneratorWindow(QMainWindow):
3162 QMessageBox.critical(self, "错误", f"生成失败: {error_msg}") 3031 QMessageBox.critical(self, "错误", f"生成失败: {error_msg}")
3163 self.generate_btn.setEnabled(True) 3032 self.generate_btn.setEnabled(True)
3164 self.status_label.setText("● 生成失败") 3033 self.status_label.setText("● 生成失败")
3165 self.status_label.setStyleSheet("QLabel { color: #FF3B30; }") 3034 self._set_status("danger")
3166 3035
3167 def update_status(self, message): 3036 def update_status(self, message):
3168 """Update status label""" 3037 """Update status label"""
...@@ -3196,7 +3065,6 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -3196,7 +3065,6 @@ class ImageGeneratorWindow(QMainWindow):
3196 ) 3065 )
3197 3066
3198 self.preview_label.setPixmap(scaled_pixmap) 3067 self.preview_label.setPixmap(scaled_pixmap)
3199 self.preview_label.setStyleSheet("")
3200 self.logger.info("[display_image] 图片显示完成") 3068 self.logger.info("[display_image] 图片显示完成")
3201 except Exception as e: 3069 except Exception as e:
3202 self.logger.error(f"[display_image] 图片显示失败: {e}", exc_info=True) 3070 self.logger.error(f"[display_image] 图片显示失败: {e}", exc_info=True)
...@@ -3222,7 +3090,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -3222,7 +3090,7 @@ class ImageGeneratorWindow(QMainWindow):
3222 3090
3223 processing_method = "格式转换后" if processed_bytes != self.generated_image_bytes else "原始数据" 3091 processing_method = "格式转换后" if processed_bytes != self.generated_image_bytes else "原始数据"
3224 self.status_label.setText(f"● 已用系统查看器打开 ({processing_method})") 3092 self.status_label.setText(f"● 已用系统查看器打开 ({processing_method})")
3225 self.status_label.setStyleSheet("QLabel { color: #007AFF; }") 3093 self._set_status("info")
3226 except Exception as e: 3094 except Exception as e:
3227 QMessageBox.critical(self, "错误", f"无法打开系统图片查看器: {str(e)}") 3095 QMessageBox.critical(self, "错误", f"无法打开系统图片查看器: {str(e)}")
3228 3096
...@@ -3300,7 +3168,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -3300,7 +3168,7 @@ class ImageGeneratorWindow(QMainWindow):
3300 3168
3301 # 只使用状态栏提示,不显示弹窗 3169 # 只使用状态栏提示,不显示弹窗
3302 self.status_label.setText("● 图片已保存") 3170 self.status_label.setText("● 图片已保存")
3303 self.status_label.setStyleSheet("QLabel { color: #34C759; }") 3171 self._set_status("success")
3304 except Exception as e: 3172 except Exception as e:
3305 self.logger.error(f"图片保存失败: {str(e)}") 3173 self.logger.error(f"图片保存失败: {str(e)}")
3306 QMessageBox.critical(self, "错误", f"保存失败: {str(e)}") 3174 QMessageBox.critical(self, "错误", f"保存失败: {str(e)}")
...@@ -3443,7 +3311,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -3443,7 +3311,7 @@ class ImageGeneratorWindow(QMainWindow):
3443 if total == 0: 3311 if total == 0:
3444 self.clear_details_panel() 3312 self.clear_details_panel()
3445 self.status_label.setText("● 历史记录已删除") 3313 self.status_label.setText("● 历史记录已删除")
3446 self.status_label.setStyleSheet("QLabel { color: #34C759; }") 3314 self._set_status("success")
3447 else: 3315 else:
3448 QMessageBox.critical(self, "错误", "删除历史记录失败") 3316 QMessageBox.critical(self, "错误", "删除历史记录失败")
3449 3317
...@@ -3489,7 +3357,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -3489,7 +3357,7 @@ class ImageGeneratorWindow(QMainWindow):
3489 self.history_count_label.setText("共 0 条历史记录") 3357 self.history_count_label.setText("共 0 条历史记录")
3490 self.clear_details_panel() 3358 self.clear_details_panel()
3491 self.status_label.setText("● 历史记录已清空") 3359 self.status_label.setText("● 历史记录已清空")
3492 self.status_label.setStyleSheet("QLabel { color: #34C759; }") 3360 self._set_status("success")
3493 3361
3494 except Exception as e: 3362 except Exception as e:
3495 QMessageBox.critical(self, "错误", f"清空历史记录失败: {str(e)}") 3363 QMessageBox.critical(self, "错误", f"清空历史记录失败: {str(e)}")
...@@ -3526,7 +3394,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -3526,7 +3394,7 @@ class ImageGeneratorWindow(QMainWindow):
3526 3394
3527 if not reference_paths: 3395 if not reference_paths:
3528 no_images_label = QLabel("无参考图片") 3396 no_images_label = QLabel("无参考图片")
3529 no_images_label.setStyleSheet("color: #999;") 3397 no_images_label.setProperty("role", "muted")
3530 self.ref_images_layout.addWidget(no_images_label) 3398 self.ref_images_layout.addWidget(no_images_label)
3531 return 3399 return
3532 3400
...@@ -3550,7 +3418,7 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -3550,7 +3418,7 @@ class ImageGeneratorWindow(QMainWindow):
3550 image_label.setPixmap(thumbnail) 3418 image_label.setPixmap(thumbnail)
3551 image_label.setFixedSize(size, size) 3419 image_label.setFixedSize(size, size)
3552 image_label.setAlignment(Qt.AlignCenter) 3420 image_label.setAlignment(Qt.AlignCenter)
3553 image_label.setStyleSheet("border: 1px solid #ddd; margin: 5px;") 3421 image_label.setProperty("role", "thumb")
3554 image_label.mouseDoubleClickEvent = lambda e, path=ref_path: self.open_reference_image(path) 3422 image_label.mouseDoubleClickEvent = lambda e, path=ref_path: self.open_reference_image(path)
3555 self.ref_images_layout.addWidget(image_label) 3423 self.ref_images_layout.addWidget(image_label)
3556 except Exception as e: 3424 except Exception as e:
...@@ -3571,22 +3439,18 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -3571,22 +3439,18 @@ class ImageGeneratorWindow(QMainWindow):
3571 Qt.SmoothTransformation 3439 Qt.SmoothTransformation
3572 ) 3440 )
3573 self.generated_image_label.setPixmap(scaled_pixmap) 3441 self.generated_image_label.setPixmap(scaled_pixmap)
3574 self.generated_image_label.setStyleSheet( 3442 self._set_image_state(self.generated_image_label, has_image=True)
3575 "QLabel { border: 1px solid #ddd; background-color: white; }")
3576 self.current_generated_image_path = image_path 3443 self.current_generated_image_path = image_path
3577 else: 3444 else:
3578 self.generated_image_label.setText("图片加载失败") 3445 self.generated_image_label.setText("图片加载失败")
3579 self.generated_image_label.setStyleSheet( 3446 self._set_image_state(self.generated_image_label, has_image=False)
3580 "QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }")
3581 except Exception as e: 3447 except Exception as e:
3582 print(f"Failed to load generated image {image_path}: {e}") 3448 print(f"Failed to load generated image {image_path}: {e}")
3583 self.generated_image_label.setText("图片加载失败") 3449 self.generated_image_label.setText("图片加载失败")
3584 self.generated_image_label.setStyleSheet( 3450 self._set_image_state(self.generated_image_label, has_image=False)
3585 "QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }")
3586 else: 3451 else:
3587 self.generated_image_label.setText("图片文件不存在") 3452 self.generated_image_label.setText("图片文件不存在")
3588 self.generated_image_label.setStyleSheet( 3453 self._set_image_state(self.generated_image_label, has_image=False)
3589 "QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }")
3590 3454
3591 def clear_details_panel(self): 3455 def clear_details_panel(self):
3592 """Clear the details panel""" 3456 """Clear the details panel"""
...@@ -3602,13 +3466,12 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -3602,13 +3466,12 @@ class ImageGeneratorWindow(QMainWindow):
3602 if child: 3466 if child:
3603 child.setParent(None) 3467 child.setParent(None)
3604 no_images_label = QLabel("无参考图片") 3468 no_images_label = QLabel("无参考图片")
3605 no_images_label.setStyleSheet("color: #999;") 3469 no_images_label.setProperty("role", "muted")
3606 self.ref_images_layout.addWidget(no_images_label) 3470 self.ref_images_layout.addWidget(no_images_label)
3607 3471
3608 self.generated_image_label.setText("请选择一个历史记录查看生成图片") 3472 self.generated_image_label.setText("请选择一个历史记录查看生成图片")
3609 self.generated_image_label.setPixmap(QPixmap()) 3473 self.generated_image_label.setPixmap(QPixmap())
3610 self.generated_image_label.setStyleSheet( 3474 self._set_image_state(self.generated_image_label, has_image=False)
3611 "QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }")
3612 3475
3613 def copy_prompt_text(self): 3476 def copy_prompt_text(self):
3614 """Copy the prompt text to clipboard""" 3477 """Copy the prompt text to clipboard"""
...@@ -3618,9 +3481,10 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -3618,9 +3481,10 @@ class ImageGeneratorWindow(QMainWindow):
3618 clipboard.setText(self.current_history_prompt) 3481 clipboard.setText(self.current_history_prompt)
3619 3482
3620 # Show success message briefly 3483 # Show success message briefly
3621 original_text = self.copy_prompt_btn.text()
3622 self.copy_prompt_btn.setText("✅ 已复制") 3484 self.copy_prompt_btn.setText("✅ 已复制")
3623 self.copy_prompt_btn.setStyleSheet("QPushButton { background-color: #34C759; color: white; }") 3485 self.copy_prompt_btn.setProperty("variant", "success-flash")
3486 self.copy_prompt_btn.style().unpolish(self.copy_prompt_btn)
3487 self.copy_prompt_btn.style().polish(self.copy_prompt_btn)
3624 3488
3625 # Reset after 2 seconds 3489 # Reset after 2 seconds
3626 QTimer.singleShot(2000, lambda: self.reset_copy_button()) 3490 QTimer.singleShot(2000, lambda: self.reset_copy_button())
...@@ -3628,7 +3492,9 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -3628,7 +3492,9 @@ class ImageGeneratorWindow(QMainWindow):
3628 def reset_copy_button(self): 3492 def reset_copy_button(self):
3629 """Reset the copy button appearance""" 3493 """Reset the copy button appearance"""
3630 self.copy_prompt_btn.setText("📋 复制") 3494 self.copy_prompt_btn.setText("📋 复制")
3631 self.copy_prompt_btn.setStyleSheet("") 3495 self.copy_prompt_btn.setProperty("variant", "")
3496 self.copy_prompt_btn.style().unpolish(self.copy_prompt_btn)
3497 self.copy_prompt_btn.style().polish(self.copy_prompt_btn)
3632 3498
3633 def open_generated_image_from_history(self, event): 3499 def open_generated_image_from_history(self, event):
3634 """Open the generated image from history in system viewer""" 3500 """Open the generated image from history in system viewer"""
...@@ -4779,7 +4645,7 @@ class StyleDesignerTab(QWidget): ...@@ -4779,7 +4645,7 @@ class StyleDesignerTab(QWidget):
4779 4645
4780 # 更新状态提示 4646 # 更新状态提示
4781 self.status_label.setText("● 图片生成成功") 4647 self.status_label.setText("● 图片生成成功")
4782 self.status_label.setStyleSheet("QLabel { color: #34C759; }") 4648 self._set_status("success")
4783 4649
4784 def on_generation_error(self, error_msg: str): 4650 def on_generation_error(self, error_msg: str):
4785 """生成失败回调""" 4651 """生成失败回调"""
...@@ -4835,7 +4701,7 @@ class StyleDesignerTab(QWidget): ...@@ -4835,7 +4701,7 @@ class StyleDesignerTab(QWidget):
4835 f.write(self.generated_image_bytes) 4701 f.write(self.generated_image_bytes)
4836 # 使用状态栏提示而不是弹窗 4702 # 使用状态栏提示而不是弹窗
4837 self.status_label.setText("● 图片已保存") 4703 self.status_label.setText("● 图片已保存")
4838 self.status_label.setStyleSheet("QLabel { color: #34C759; }") 4704 self._set_status("success")
4839 except Exception as e: 4705 except Exception as e:
4840 QMessageBox.critical(self, "错误", f"保存失败: {e}") 4706 QMessageBox.critical(self, "错误", f"保存失败: {e}")
4841 4707
...@@ -4938,7 +4804,11 @@ def main(): ...@@ -4938,7 +4804,11 @@ def main():
4938 # 第4步:创建 QApplication(preflight 对话框需要) 4804 # 第4步:创建 QApplication(preflight 对话框需要)
4939 logger.info("[BOOT] Phase 4: 创建 QApplication...") 4805 logger.info("[BOOT] Phase 4: 创建 QApplication...")
4940 app = QApplication(sys.argv) 4806 app = QApplication(sys.argv)
4941 logger.info("[BOOT] Phase 4 完成: QApplication 已创建") 4807 # 立即应用全局主题:所有后续创建的 widget(含 preflight 的 QMessageBox)
4808 # 都会自动跟随。ThemeManager 必须有引用持有,否则 GC 会断开信号。
4809 from theme import apply_theme
4810 app.theme_manager = apply_theme(app)
4811 logger.info(f"[BOOT] Phase 4 完成: QApplication 已创建,主题: {app.theme_manager.current_mode()}")
4942 4812
4943 # 第 4.5 步:启动门禁 preflight 4813 # 第 4.5 步:启动门禁 preflight
4944 # 任一检查失败 → 弹"应用启动失败,请联系 @柴进" → sys.exit(1) 4814 # 任一检查失败 → 弹"应用启动失败,请联系 @柴进" → sys.exit(1)
......
1 ## Design Document: Image History Management
2
3 ### Context
4 当前应用缺乏图片历史记录功能,用户生成图片后需要手动下载和管理。需要实现自动保存、管理和查看历史生成记录的功能,提升用户体验和工作效率。
5
6 ### Goals / Non-Goals
7 **Goals:**
8 - 自动保存所有生成的图片和相关元数据
9 - 提供直观的历史记录浏览界面
10 - 支持从历史记录快速重新加载图片
11 - 保持与现有功能的完全兼容
12
13 **Non-Goals:**
14 - 不实现云同步功能(纯本地存储)
15 - 不修改现有图片生成核心逻辑
16 - 不依赖外部数据库或复杂依赖
17
18 ### Decisions
19
20 #### 1. 存储架构
21 **Decision**: 使用文件系统 + JSON索引的轻量级存储方案
22 - **Why**: 避免引入重型数据库依赖,保持应用轻量化
23 - **Alternatives considered**: SQLite数据库, Redis缓存, 文件系统-only方案
24 - **Chosen approach**: `/images/YYYYMMDDHHMMSS/` 目录结构 + 全局 `history_index.json`
25
26 #### 2. 数据结构设计
27 **Decision**: 采用分层存储结构
28 ```
29 /images/
30 ├── history_index.json # 全局索引文件
31 └── 20241201123456/ # 按时间戳的目录
32 ├── metadata.json # 该会话的元数据
33 ├── generated.png # 生成的图片
34 ├── reference1.jpg # 参考图片1
35 └── reference2.jpg # 参考图片2
36 ```
37
38 #### 3. UI集成方案
39 **Decision**: 在现有主界面添加"历史记录"标签页
40 - **Why**: 保持界面一致性,最小化学习成本
41 - **Alternatives considered**: 独立历史记录窗口, 侧边栏集成, 右键菜单
42
43 #### 4. 性能优化策略
44 **Decision**: 延迟加载和缩略图缓存
45 - 历史记录列表只加载基本信息和缩略图
46 - 点击时才加载完整图片
47 - 实现缩略图缓存机制
48
49 ### Risks / Trade-offs
50 - **[磁盘空间使用]** → 添加用户配置选项:最大历史记录数量限制
51 - **[大量历史记录性能]** → 实现分页加载和虚拟滚动
52 - **[跨平台路径处理]** → 使用 `pathlib.Path` 确保跨平台兼容性
53 - **[UI响应性]** → 使用异步加载避免界面冻结
54
55 ### Migration Plan
56 1. **Phase 1**: 实现 `HistoryManager` 类和基础存储逻辑
57 2. **Phase 2**: 修改现有图片生成流程,添加自动保存
58 3. **Phase 3**: 开发历史记录界面和交互功能
59 4. **Phase 4**: 添加配置选项和性能优化
60
61 **Rollback plan**: 所有功能都是增量添加,移除新功能不会影响现有系统稳定性。
62
63 ### Open Questions
64 - 历史记录的默认数量上限应该是多少?
65 - 是否需要历史记录的搜索功能?
66 - 是否需要支持历史记录的导入/导出?
67 - 如何处理参考图片的版权和隐私问题?
68
69 ### Technical Architecture Details
70
71 #### HistoryManager Class Structure
72 ```python
73 class HistoryManager:
74 def __init__(self, base_path: Path)
75 def save_generation(self, image_bytes, prompt, references, params)
76 def load_history_index(self) -> List[HistoryItem]
77 def get_history_item(self, timestamp: str) -> HistoryItem
78 def delete_history_item(self, timestamp: str) -> bool
79 def cleanup_old_records(self, max_count: int)
80 ```
81
82 #### Data Models
83 ```python
84 @dataclass
85 class HistoryItem:
86 timestamp: str
87 prompt: str
88 generated_image_path: Path
89 reference_image_paths: List[Path]
90 aspect_ratio: str
91 image_size: str
92 model: str
93 created_at: datetime
94 ```
95
96 #### Integration Points
97 - **ImageGenerationWorker**: 修改 `on_image_generated` 回调
98 - **ImageGeneratorWindow**: 添加历史记录标签页和相关UI组件
99 - **Configuration**: 扩展 `config.json` 添加历史记录相关配置
...\ No newline at end of file ...\ No newline at end of file
1 # Change: Add Image History Management
2
3 ## Why
4 用户需要一个图片历史记录功能来管理和查看之前生成的图片,目前每次生成图片后都需要手动下载,缺乏历史记录管理和本地图片库功能。
5
6 ## What Changes
7 - 添加图片自动下载功能:在图片生成成功后自动保存到本地 `/images/` 目录
8 - 创建历史记录管理系统:按时间戳存储图片和相关元数据(prompt、参考图等)
9 - 新增历史记录界面:在主界面添加历史记录标签页,展示所有历史生成记录
10 - 实现本地图片加载:允许用户点击历史记录中的缩略图来加载和查看完整图片
11
12 ## Impact
13 - **Affected specs**:
14 - `image-generation` (MODIFIED: 添加自动下载和历史记录保存)
15 - `user-interface` (MODIFIED: 添加历史记录标签页)
16 - **Affected code**:
17 - `image_generator.py:937-944` (修改 `on_image_generated` 方法添加自动保存)
18 - `image_generator.py:1002-1020` (修改 `download_image` 方法)
19 - 新增 `HistoryManager` 类和相关历史记录管理逻辑
20 - 主界面UI修改:添加历史记录标签页和展示组件
...\ No newline at end of file ...\ No newline at end of file
1 ## ADDED Requirements
2
3 ### Requirement: Automatic Image History Storage
4 The system SHALL automatically save all generated images and their metadata to local storage when image generation is successful.
5
6 #### Scenario: Image generation auto-save
7 - **WHEN** image generation is completed successfully
8 - **THEN** the system SHALL automatically save the generated image to a timestamped directory
9 - **AND** save the prompt text and generation parameters
10 - **AND** copy any reference images used
11 - **AND** update the global history index
12
13 #### Scenario: Directory structure creation
14 - **WHEN** creating a new history record
15 - **THEN** the system SHALL create a directory named with YYYYMMDDHHMMSS timestamp format
16 - **AND** ensure the directory structure is: `/images/{timestamp}/generated.png, reference1.jpg, reference2.jpg, metadata.json`
17
18 ### Requirement: History Record Management
19 The system SHALL provide functionality to manage and browse image generation history.
20
21 #### Scenario: History index loading
22 - **WHEN** the application starts or the history tab is opened
23 - **THEN** the system SHALL load the history index from `history_index.json`
24 - **AND** display a list of all historical generation records sorted by timestamp
25
26 #### Scenario: History record deletion
27 - **WHEN** the user selects a history record and chooses to delete it
28 - **THEN** the system SHALL remove the record from the index
29 - **AND** delete the corresponding directory and all its files
30 - **AND** confirm the deletion action with the user
31
32 ### Requirement: History Browser Interface
33 The system SHALL provide a user interface to browse and interact with image generation history.
34
35 #### Scenario: History list display
36 - **WHEN** the history tab is opened
37 - **THEN** the system SHALL display a list of historical records
38 - **AND** show thumbnail previews of generated images
39 - **AND** display prompt text, generation date, and parameters for each record
40
41 #### Scenario: History item selection
42 - **WHEN** the user clicks on a history item
43 - **THEN** the system SHALL load the corresponding image into the main preview area
44 - **AND** display the original prompt text in the prompt input field
45 - **AND** restore the generation parameters (aspect ratio, size, model)
46
47 ### Requirement: Local Image Loading
48 The system SHALL enable loading previously generated images from local history.
49
50 #### Scenario: Load image from history
51 - **WHEN** a history item is selected
52 - **THEN** the system SHALL load the full-resolution image from local storage
53 - **AND** display it in the preview area with the same functionality as newly generated images
54 - **AND** enable download and full-screen viewing options
55
56 #### Scenario: Reference image restoration
57 - **WHEN** loading a history item that had reference images
58 - **THEN** the system SHALL display the reference images in the reference area
59 - **AND** allow the user to modify or remove them before generating new images
60
61 ### Requirement: Configuration and Settings
62 The system SHALL provide configuration options for history management.
63
64 #### Scenario: History storage location
65 - **WHEN** the application starts
66 - **THEN** the system SHALL create the images directory in the application's data directory
67 - **AND** allow users to configure a custom storage location through settings
68
69 #### Scenario: History limits configuration
70 - **WHEN** the number of history records exceeds the configured limit
71 - **THEN** the system SHALL automatically remove the oldest records
72 - **AND** provide user settings to configure the maximum number of history records
73
74 ### Requirement: History Data Persistence
75 The system SHALL ensure history data persists across application sessions.
76
77 #### Scenario: Data persistence
78 - **WHEN** the application is closed and reopened
79 - **THEN** the system SHALL preserve all saved history records
80 - **AND** maintain the same directory structure and file organization
81 - **AND** recover gracefully if the history index file is corrupted
82
83 ## MODIFIED Requirements
84
85 ### Requirement: Image Generation Workflow
86 The system SHALL integrate history saving into the existing image generation workflow.
87
88 #### Scenario: Enhanced image generation completion
89 - **WHEN** image generation is completed successfully
90 - **THEN** the system SHALL perform all existing success behaviors (display image, enable download)
91 - **AND** additionally save the image and metadata to local history
92 - **AND** update the history index with the new record
93
94 #### Scenario: Error handling in history saving
95 - **WHEN** history saving encounters an error
96 - **THEN** the system SHALL not affect the image generation success state
97 - **AND** log the error without interrupting user experience
98 - **AND** continue with normal image display and download functionality
...\ No newline at end of file ...\ No newline at end of file
1 ## 1. 核心数据结构和存储
2 - [x] 1.1 创建 `HistoryManager` 类,负责历史记录的管理
3 - [x] 1.2 设计历史记录数据结构:时间戳、prompt、参考图路径、生成图路径
4 - [x] 1.3 实现本地文件存储逻辑:`/images/YYYYMMDDHHMMSS/` 目录结构
5 - [x] 1.4 创建历史记录索引文件 `history_index.json`
6
7 ## 2. 图片自动下载和保存
8 - [x] 2.1 修改 `on_image_generated()` 方法,在生成成功后自动保存图片
9 - [x] 2.2 实现参考图片的本地复制和保存
10 - [x] 2.3 创建元数据文件 `metadata.json` 保存prompt和参数信息
11 - [x] 2.4 更新历史记录索引文件
12
13 ## 3. 历史记录管理功能
14 - [x] 3.1 实现 `HistoryManager.load_history()` 方法加载历史记录
15 - [x] 3.2 实现 `HistoryManager.get_history_item()` 方法获取单个历史记录
16 - [x] 3.3 实现 `HistoryManager.delete_history_item()` 方法删除历史记录
17 - [x] 3.4 实现历史记录的搜索和过滤功能
18
19 ## 4. 历史记录界面开发
20 - [x] 4.1 在主界面添加"历史记录"标签页
21 - [x] 4.2 创建历史记录列表组件,显示缩略图和基本信息
22 - [x] 4.3 实现历史记录项的点击加载功能
23 - [x] 4.4 添加历史记录的右键菜单(删除、导出等)
24
25 ## 5. 图片展示和交互
26 - [x] 5.1 实现从历史记录加载图片到预览区域
27 - [x] 5.2 创建历史记录详情查看功能(显示完整prompt、参数等)
28 - [x] 5.3 实现历史记录图片的双击全屏查看
29 - [x] 5.4 添加历史记录图片的批量导出功能
30
31 ## 6. 配置和设置
32 - [x] 6.1 添加历史记录存储路径配置选项
33 - [x] 6.2 实现历史记录数量限制和自动清理设置
34 - [x] 6.3 添加历史记录功能的启用/禁用开关
35
36 ## 7. 测试和验证
37 - [x] 7.1 测试图片生成后自动保存到历史记录
38 - [x] 7.2 测试历史记录界面的加载和显示
39 - [x] 7.3 测试从历史记录加载图片功能
40 - [x] 7.4 测试历史记录的删除和清理功能
41 - [x] 7.5 验证大量历史记录下的性能表现
42
43 ## 8. 文档和部署
44 - [ ] 8.1 更新README.md,添加历史记录功能说明
45 - [ ] 8.2 创建历史记录功能的使用指南
46 - [ ] 8.3 测试在不同操作系统下的兼容性
...\ No newline at end of file ...\ No newline at end of file
1 ## Design Document: Simple Logging System
2
3 ### Context
4 当前应用缺乏基本的日志记录功能,当出现问题时难以诊断。需要建立简单实用的日志系统来记录关键事件和错误信息。
5
6 ### Goals / Non-Goals
7 **Goals:**
8 - 提供基本的日志记录功能
9 - 记录关键操作和错误信息
10 - 实现简单的日志文件管理
11 - 使用标准库,保持简单
12 - 帮助快速定位和解决问题
13
14 **Non-Goals:**
15 - 不实现复杂的日志分析功能
16 - 不集成第三方日志服务
17 - 不实现实时日志监控
18 - 不设计复杂的日志分类系统
19
20 ### Decisions
21
22 #### 1. 日志框架选择
23 **Decision**: 使用 Python 标准库 `logging` 模块
24 - **Why**: 标准库提供足够的基础功能,无额外依赖
25 - **Alternatives considered**: print语句、简单的文件写入
26 - **Chosen approach**: Python logging + 基础配置
27
28 #### 2. 日志文件结构
29 **Decision**: 单一日志文件
30 ```
31 logs/
32 └── app.log # 统一的应用日志文件
33 ```
34
35 #### 3. 日志轮转策略
36 **Decision**: 基于文件大小的简单轮转
37 - 最大文件大小:10MB
38 - 保留最近 5 个历史文件
39 - 不使用压缩,保持简单
40
41 #### 4. 日志级别策略
42 - **INFO**: 记录正常操作流程
43 - **WARNING**: 记录潜在问题
44 - **ERROR**: 记录操作失败和错误
45 - **DEBUG**: 开发时可启用详细信息
46
47 ### Technical Architecture Details
48
49 #### 简化的日志系统
50 ```
51 SimpleLogger
52 ├── 初始化基础配置
53 ├── 创建单一logger
54 ├── 设置简单格式化器
55 └── 实现基础轮转
56
57 格式化器
58 ├── 时间戳
59 ├── 日志级别
60 ├── 模块名称
61 └── 消息内容
62 ```
63
64 #### 关键日志记录点
65 1. **应用启动**: 应用启动、配置加载
66 2. **用户认证**: 登录成功/失败、认证错误
67 3. **配置操作**: 配置文件读取错误
68 4. **数据库连接**: 连接成功/失败、查询错误
69 5. **图片生成**: API调用成功/失败、生成错误
70 6. **文件操作**: 图片保存成功/失败、历史记录错误
71
72 #### 性能考虑
73 - 使用标准库的优化性能
74 - 简单格式减少格式化开销
75 - 合理的轮转避免大文件问题
76 - 关键操作才记录,避免过度日志
77
78 ### Integration Points
79
80 #### 模块集成
81 - **DatabaseManager**: 添加基础数据库日志
82 - **ImageGeneratorWindow**: 添加关键操作日志
83 - **ImageGenerationWorker**: 添加生成过程日志
84 - **HistoryManager**: 添加文件操作日志
85
86 #### 配置集成
87 ```json
88 {
89 "logging": {
90 "enabled": true,
91 "level": "INFO",
92 "max_file_size_mb": 10
93 }
94 }
95 ```
96
97 ### Implementation Plan
98 1. **Step 1**: 创建简单的日志初始化函数
99 2. **Step 2**: 集成到 DatabaseManager
100 3. **Step 3**: 集成到图片生成模块
101 4. **Step 4**: 集成到错误处理
102 5. **Step 5**: 测试和验证
103
104 ### Design Principles
105 - 简单实用:只记录必要信息
106 - 不影响性能:关键操作才记录
107 - 易于查看:日志文件直接可读
108 - 维护简单:最小化代码复杂度
...\ No newline at end of file ...\ No newline at end of file
1 # Change: Add Simple Logging System
2
3 ## Why
4 当前应用缺乏基本的日志记录功能,当用户遇到API错误、配置问题、图片生成失败等情况时,开发者无法获得基本的诊断信息。添加简单的日志系统将帮助快速定位和解决常见问题。
5
6 ## What Changes
7 - 添加简单的日志记录功能,统一输出到 `logs/app.log`
8 - 记录关键事件:应用启动、用户登录、图片生成、错误信息
9 - 使用标准Python logging模块,保持简单实用
10 - 实现基本的日志文件轮转(大小限制)
11 - 记录基本的错误信息和操作状态
12
13 ## Impact
14 - **Affected specs**:
15 - `logging-system` (ADDED: 基础日志记录功能)
16 - `error-handling` (MODIFIED: 添加基本错误日志)
17 - `user-operations` (MODIFIED: 记录关键操作日志)
18 - **Affected code**:
19 - 全局添加简单日志初始化
20 - DatabaseManager 类添加基础数据库日志
21 - ImageGeneratorWindow 类添加关键操作日志
22 - ImageGenerationWorker 类添加生成过程日志
23 - 主要错误处理函数添加日志记录
...\ No newline at end of file ...\ No newline at end of file
1 ## ADDED Requirements
2
3 ### Requirement: Simple Logging System
4 The system SHALL provide a simple logging system that records key application events and errors for basic troubleshooting.
5
6 #### Scenario: Application initialization logging
7 - **WHEN** the application starts
8 - **THEN** the system SHALL log startup information and configuration loading
9 - **AND** SHALL record logging system initialization
10 - **AND** SHALL note the application version and platform
11
12 #### Scenario: Basic logging classification
13 - **WHEN** logging events occur
14 - **THEN** the system SHALL categorize logs into appropriate levels (INFO, WARNING, ERROR, DEBUG)
15 - **AND** SHALL write all logs to a single app.log file
16 - **AND** SHALL maintain consistent formatting across log entries
17
18 ### Requirement: Basic Log File Management
19 The system SHALL implement simple log file management with size-based rotation.
20
21 #### Scenario: Log file creation and rotation
22 - **WHEN** the application starts
23 - **THEN** the system SHALL create a logs/ directory in the application folder
24 - **AND** SHALL create a single app.log file for all logs
25 - **AND** SHALL implement file rotation when file reaches 10MB size limit
26 - **AND** SHALL keep the 5 most recent log files
27
28 ### Requirement: Database Operation Logging
29 The system SHALL record basic database-related operations for simple debugging.
30
31 #### Scenario: Database connection logging
32 - **WHEN** attempting to connect to the database
33 - **THEN** the system SHALL log connection attempts and results
34 - **AND** SHALL record basic database information
35 - **AND** SHALL log connection failures with error messages
36
37 #### Scenario: Database error logging
38 - **WHEN** database operations fail
39 - **THEN** the system SHALL log the error type and basic information
40 - **AND** SHALL record the operation that failed
41 - **AND** SHALL avoid logging sensitive database data
42
43 ### Requirement: User Operation Logging
44 The system SHALL log key user actions for troubleshooting user experience issues.
45
46 #### Scenario: User authentication logging
47 - **WHEN** users attempt to log in
48 - **THEN** the system SHALL log login attempts and results
49 - **AND** SHALL record authentication failures with basic error info
50 - **AND** SHALL not log sensitive user credentials
51
52 #### Scenario: Image generation logging
53 - **WHEN** users generate images
54 - **THEN** the system SHALL log generation requests and basic parameters
55 - **AND** SHALL record API call results
56 - **AND** SHALL log generation failures with error information
57
58 ### Requirement: Error Handling Logging
59 The system SHALL provide basic error logging for troubleshooting common issues.
60
61 #### Scenario: Basic exception logging
62 - **WHEN** key exceptions occur
63 - **THEN** the system SHALL log the exception type and message
64 - **AND** SHALL record the operation context
65 - **AND** SHALL avoid overly detailed stack traces
66
67 #### Scenario: Configuration error logging
68 - **WHEN** configuration loading fails
69 - **THEN** the system SHALL log the configuration error
70 - **AND** SHALL record the file path and error details
71 - **AND** SHALL provide fallback behavior information
72
73 ### Requirement: Simple Configuration
74 The system SHALL provide basic logging configuration options.
75
76 #### Scenario: Logging configuration management
77 - **WHEN** the application starts
78 - **THEN** the system SHALL load logging settings from config.json
79 - **AND** SHALL support enabling/disabling the logging system
80 - **AND** SHALL support basic log level configuration
81
82 #### Scenario: Log level control
83 - **WHEN** different logging needs arise
84 - **THEN** the system SHALL support switching between INFO, WARNING, ERROR levels
85 - **AND** SHALL allow enabling DEBUG level for development
86 - **AND** SHALL apply configuration changes immediately
87
88 ### Requirement: Cross-Platform Compatibility
89 The logging system SHALL work consistently across Windows, macOS, and Linux platforms.
90
91 #### Scenario: Simple cross-platform behavior
92 - **WHEN** running on different operating systems
93 - **THEN** the system SHALL use standard file permissions
94 - **AND** SHALL handle basic path and encoding issues
95 - **THEN** SHALL create logs in application directory
96
97 #### Scenario: Log file readability
98 - **WHEN** users need to view log files
99 - **THEN** the system SHALL ensure log files are readable by text editors
100 - **AND** SHALL use UTF-8 encoding for character support
101 - **AND** SHALL keep log format simple and clear
...\ No newline at end of file ...\ No newline at end of file
1 ## 1. 简单日志系统实现
2 - [ ] 1.1 创建简单的日志初始化函数
3 - [ ] 1.2 实现单一日志文件(logs/app.log)
4 - [ ] 1.3 设置基础日志格式化器
5 - [ ] 1.4 实现简单的文件轮转(10MB大小限制)
6 - [ ] 1.5 创建 logs/ 目录
7
8 ## 2. 基础日志级别
9 - [ ] 2.1 实现 INFO、WARNING、ERROR 三个主要级别
10 - [ ] 2.2 可选 DEBUG 级别用于开发调试
11 - [ ] 2.3 实现简单的日志级别控制
12 - [ ] 2.4 添加控制台输出选项
13 - [ ] 2.5 实现日志启用/禁用开关
14
15 ## 3. 集成到关键模块
16 - [ ] 3.1 DatabaseManager 集成:记录连接状态和错误
17 - [ ] 3.2 ImageGeneratorWindow 集成:记录用户关键操作
18 - [ ] 3.3 ImageGenerationWorker 集成:记录API调用结果
19 - [ ] 3.4 HistoryManager 集成:记录文件操作状态
20 - [ ] 3.5 主函数集成:记录应用启动和退出
21
22 ## 4. 错误处理日志
23 - [ ] 4.1 为主要异常添加基本日志记录
24 - [ ] 4.2 记录关键错误信息(无需完整堆栈)
25 - [ ] 4.3 添加配置加载错误日志
26 - [ ] 4.4 记录网络和API调用错误
27 - [ ] 4.5 记录文件操作失败信息
28
29 ## 5. 配置集成
30 - [ ] 5.1 在 config.json 中添加基础日志配置
31 - [ ] 5.2 实现日志启用/禁用开关
32 - [ ] 5.3 添加日志级别配置选项
33 - [ ] 5.4 实现日志文件大小限制配置
34 - [ ] 5.5 集成日志配置到启动流程
35
36 ## 6. 基础性能优化
37 - [ ] 6.1 确保日志不阻塞主线程
38 - [ ] 6.2 只在关键操作时记录日志
39 - [ ] 6.3 使用简单的日志格式
40 - [ ] 6.4 避免过度详细的日志记录
41 - [ ] 6.5 测试日志对性能的影响
42
43 ## 7. 测试和验证
44 - [ ] 7.1 测试日志文件创建和基本写入
45 - [ ] 7.2 验证日志文件轮转功能
46 - [ ] 7.3 测试不同级别日志输出
47 - [ ] 7.4 验证错误日志记录功能
48 - [ ] 7.5 确保日志不影响正常功能
49
50 ## 8. 简单文档
51 - [ ] 8.1 创建日志系统简要说明
52 - [ ] 8.2 编写常见问题排查指南
53 - [ ] 8.3 创建日志配置说明
54 - [ ] 8.4 编写基本调试方法
55 - [ ] 8.5 更新README中的故障排查部分
...\ No newline at end of file ...\ No newline at end of file
1 ## Design Document: Standalone History Browsing Interface
2
3 ### Context
4 当前历史记录实现允许用户双击历史项目将其加载到图片生成界面,但这会干扰用户的当前工作状态。用户希望历史记录作为纯粹的浏览功能,直接在历史记录界面中查看完整信息。
5
6 ### Goals / Non-Goals
7 **Goals:**
8 - 提供独立的历史记录浏览功能
9 - 在历史记录界面直接显示prompt、参考图和生成图
10 - 支持prompt文本复制功能
11 - 保持与图片生成功能的完全隔离
12 - 提供直观的图片预览和详情展示
13
14 **Non-Goals:**
15 - 不再支持从历史记录加载到生成界面
16 - 不修改现有的图片生成工作流程
17 - 不增加复杂的编辑功能
18
19 ### Decisions
20
21 #### 1. 界面重构方案
22 **Decision**: 重新设计历史记录标签页为详情浏览模式
23 - **Why**: 避免界面间的操作干扰,提供更清晰的用户体验
24 - **Alternatives considered**: 弹窗详情、侧边栏详情页
25 - **Chosen approach**: 在历史记录标签页内实现完整的详情显示
26
27 #### 2. 信息展示布局
28 **Decision**: 采用上下布局:历史列表在上(显示生成图预览),详情展示在下
29 - **Why**: 类似文件管理器的熟悉交互模式,预览图让用户快速识别内容
30 - **Alternatives considered**: 左右分栏、全屏切换、纯列表模式
31 - **Chosen approach**: 固定比例的上下分割布局,列表项显示生成图缩略图
32
33 #### 3. 交互方式
34 **Decision**: 单击选择历史项显示详情,双击或右键提供额外操作
35 - **Why**: 提供清晰的选择反馈和操作入口
36 - **Alternatives considered**: 仅单击、仅右键菜单
37 - **Chosen approach**: 简单的单击选择 + 右键菜单操作
38
39 ### Technical Architecture Details
40
41 #### 新的UI组件结构
42 ```
43 历史记录标签页
44 ├── 工具栏 (刷新、清空、删除)
45 ├── 分割器 (QSplitter)
46 │ ├── 上部: 历史记录列表 (QListWidget)
47 │ │ ├── 每个列表项显示:
48 │ │ │ ├── 生成图片缩略图 (120x120)
49 │ │ │ ├── 时间戳
50 │ │ │ └── Prompt前20个字符
51 │ │ └── 悬停显示完整信息
52 │ └── 下部: 详情展示区域 (QWidget)
53 │ ├── Prompt区域 (QTextEdit + 复制按钮)
54 │ ├── 参数信息 (宽高比、尺寸)
55 │ ├── 参考图片区域 (QScrollArea)
56 │ └── 生成图片大预览 (QLabel)
57 ```
58
59 #### 数据流设计
60 1. **选择历史项**: 用户点击列表项 → 加载详细数据 → 更新详情区域
61 2. **复制Prompt**: 点击复制按钮 → 复制到剪贴板 → 显示成功提示
62 3. **删除操作**: 右键菜单删除 → 确认对话框 → 更新列表和详情
63
64 ### Risks / Trade-offs
65 - **[界面复杂度]** → 通过清晰的布局和分组来管理
66 - **[图片加载性能]** → 实现延迟加载和缓存机制
67 - **[用户体验]** → 提供清晰的视觉反馈和操作提示
68
69 ### Implementation Plan
70 1. **Phase 1**: 移除现有的加载到界面功能
71 2. **Phase 2**: 重新设计历史记录标签页UI
72 3. **Phase 3**: 实现详情展示功能
73 4. **Phase 4**: 添加复制和操作功能
74 5. **Phase 5**: 测试和优化
75
76 ### Open Questions
77 - 历史记录列表和详情区域的比例应该是多少?
78 - 是否需要添加历史记录的搜索功能?
79 - 如何处理大量历史记录时的性能问题?
...\ No newline at end of file ...\ No newline at end of file
1 # Change: Refactor History Browsing to Standalone Interface
2
3 ## Why
4 当前的历史记录实现会干扰原有的图片生成界面,用户希望历史记录作为一个独立的浏览界面,直接显示prompt、参考图和生成图,而不需要加载到原有界面中,避免操作冲突和界面混乱。
5
6 ## What Changes
7 - 重新设计历史记录界面为独立的详情浏览模式
8 - 移除历史记录加载到原有界面的功能
9 - 在历史记录标签页中直接显示完整信息:
10 - Prompt文本(支持复制)
11 - 参考图片缩略图
12 - 生成图片大图预览
13 - 保持两个功能完全独立,互不干扰
14
15 ## Impact
16 - **Affected specs**:
17 - `history-interface` (MODIFIED: 改为独立浏览模式)
18 - `user-interface` (MODIFIED: 移除界面间加载功能)
19 - **Affected code**:
20 - `image_generator.py:1447-1484` (移除 load_history_item 方法中加载到界面的逻辑)
21 - `image_generator.py:1033` (重新设计历史记录标签页UI)
22 - 新增独立的历史记录详情显示组件
...\ No newline at end of file ...\ No newline at end of file
1 ## MODIFIED Requirements
2
3 ### Requirement: Standalone History Browsing Interface
4 The system SHALL provide a standalone history browsing interface that displays complete information without interfering with the image generation interface.
5
6 #### Scenario: History item selection for detail viewing
7 - **WHEN** the user clicks on a history item
8 - **THEN** the system SHALL display detailed information in the details panel
9 - **AND** SHALL not modify any controls in the image generation interface
10 - **AND** SHALL show the complete prompt text, reference images, and generated image
11
12 #### Scenario: Independent interface operation
13 - **WHEN** the user switches between generation tab and history tab
14 - **THEN** the system SHALL preserve the state of each tab independently
15 - **AND** SHALL not load history data into generation controls
16 - **AND** SHALL maintain separate user contexts for each tab
17
18 ## REMOVED Requirements
19
20 ### Requirement: History Item Loading to Generation Interface
21 **Reason**: This feature interferes with the user's current work state and creates confusing interactions. Users want history as a pure browsing feature.
22
23 **Migration**: Replace with standalone detail viewing within the history tab.
24
25 ## ADDED Requirements
26
27 ### Requirement: History List with Image Previews
28 The system SHALL display generated image thumbnails directly in the history list for quick visual identification.
29
30 #### Scenario: History item thumbnail display
31 - **WHEN** the history list is loaded
32 - **THEN** the system SHALL display a 120x120 pixel thumbnail of each generated image
33 - **AND** SHALL include the timestamp and prompt preview (first 20 characters)
34 - **AND** SHALL maintain consistent thumbnail sizing and aspect ratio
35
36 #### Scenario: Image thumbnail loading
37 - **WHEN** loading history items
38 - **THEN** the system SHALL generate thumbnails efficiently
39 - **AND** SHALL show loading indicators for slow-loading images
40 - **AND** SHALL handle missing or corrupted image files gracefully
41
42 ### Requirement: Prompt Text Display and Copy
43 The system SHALL display the full prompt text for each history item with copy functionality.
44
45 #### Scenario: Prompt text display
46 - **WHEN** a history item is selected
47 - **THEN** the system SHALL display the complete prompt text in a dedicated area
48 - **AND** SHALL format the text for easy reading
49 - **AND** SHALL show prompt length and creation time
50
51 #### Scenario: Prompt text copying
52 - **WHEN** the user clicks the copy button
53 - **THEN** the system SHALL copy the full prompt text to clipboard
54 - **AND** SHALL show a success confirmation message
55 - **AND** SHALL maintain the original prompt formatting
56
57 ### Requirement: Reference Images Gallery
58 The system SHALL display reference images used in the generation process.
59
60 #### Scenario: Reference images display
61 - **WHEN** a history item with reference images is selected
62 - **THEN** the system SHALL display all reference images as thumbnails
63 - **AND** SHALL allow viewing full-size images on double-click
64 - **AND** SHALL show "No reference images" when none exist
65
66 #### Scenario: Reference image interaction
67 - **WHEN** the user double-clicks a reference image thumbnail
68 - **THEN** the system SHALL open the image in system default viewer
69 - **AND** SHALL handle missing image files gracefully
70
71 ### Requirement: Generated Image Preview
72 The system SHALL provide a large preview of the generated image within the history interface.
73
74 #### Scenario: Generated image display
75 - **WHEN** a history item is selected
76 - **THEN** the system SHALL display the generated image in a large preview area
77 - **AND** SHALL scale the image appropriately for the available space
78 - **AND** SHALL maintain image aspect ratio
79
80 #### Scenario: Generated image interaction
81 - **WHEN** the user double-clicks the generated image preview
82 - **THEN** the system SHALL open the image in system default viewer
83 - **AND** SHALL handle missing image files with appropriate error messages
84
85 ### Requirement: History Item Details Panel
86 The system SHALL provide a comprehensive details panel for selected history items.
87
88 #### Scenario: Details panel layout
89 - **WHEN** the history tab is opened
90 - **THEN** the system SHALL display a split-view interface
91 - **AND** SHALL show history list with image previews in the upper portion
92 - **AND** SHALL show details panel in the lower portion
93 - **AND** SHALL maintain responsive proportions
94
95 #### Scenario: Enhanced hover information
96 - **WHEN** the user hovers over a history list item
97 - **THEN** the system SHALL display comprehensive tooltip information
98 - **AND** SHALL include full prompt text, timestamp, aspect ratio, and image size
99 - **AND** SHALL provide quick visual preview without requiring selection
100
101 #### Scenario: Empty state handling
102 - **WHEN** no history item is selected
103 - **THEN** the system SHALL display helpful placeholder text
104 - **AND** SHALL guide user to select an item for details
105 - **AND** SHALL maintain a clean, uncluttered interface
106
107 ### Requirement: Independent Tab Management
108 The system SHALL ensure complete independence between generation and history tabs.
109
110 #### Scenario: Tab switching preservation
111 - **WHEN** the user switches between tabs
112 - **THEN** the system SHALL preserve all input fields and selections
113 - **AND** SHALL not transfer data between interfaces
114 - **AND** SHALL maintain separate operational contexts
115
116 #### Scenario: Concurrent operation support
117 - **WHEN** a history item is being viewed
118 - **THEN** the user SHALL still be able to work in the generation tab
119 - **AND** SHALL not have their work interrupted by history browsing
120 - **AND** SHALL experience smooth tab transitions
...\ No newline at end of file ...\ No newline at end of file
1 ## 1. 移除干扰性功能
2 - [ ] 1.1 移除历史记录双击加载到原有界面的功能
3 - [ ] 1.2 移除 load_history_item 方法中的界面设置逻辑
4 - [ ] 1.3 更新历史记录项的交互提示和工具提示
5 - [ ] 1.4 保留右键菜单中的删除和文件管理器功能
6
7 ## 2. 重新设计历史记录UI
8 - [ ] 2.1 重新设计 setup_history_tab 方法,添加分割器布局
9 - [ ] 2.2 创建历史记录详情展示区域
10 - [ ] 2.3 修改历史列表项显示:添加生成图缩略图+时间戳+Prompt预览
11 - [ ] 2.4 添加Prompt文本显示区域和复制按钮
12 - [ ] 2.5 创建参考图片和生成图片的预览区域
13 - [ ] 2.6 设置合适的布局比例和样式
14 - [ ] 2.7 优化悬停提示信息,包含完整详情
15
16 ## 3. 实现详情展示功能
17 - [ ] 3.1 实现单击历史项显示详情的逻辑
18 - [ ] 3.2 实现Prompt文本的显示和格式化
19 - [ ] 3.3 实现参考图片缩略图的加载和显示
20 - [ ] 3.4 实现生成图片的大图预览功能
21 - [ ] 3.5 添加生成参数信息显示
22
23 ## 4. 添加交互功能
24 - [ ] 4.1 实现Prompt文本复制功能
25 - [ ] 4.2 添加复制成功的视觉反馈
26 - [ ] 4.3 实现参考图片和生成图片的双击放大查看
27 - [ ] 4.4 更新右键菜单,移除加载选项
28 - [ ] 4.5 添加刷新详情的功能
29
30 ## 5. 优化用户体验
31 - [ ] 5.1 实现无选中状态时的提示信息
32 - [ ] 5.2 添加加载状态的指示器
33 - [ ] 5.3 优化图片加载性能和缓存
34 - [ ] 5.4 调整界面布局的响应式设计
35 - [ ] 5.5 添加键盘快捷键支持
36
37 ## 6. 测试和验证
38 - [ ] 6.1 测试历史记录浏览的独立性
39 - [ ] 6.2 验证不会干扰图片生成界面
40 - [ ] 6.3 测试复制功能的准确性
41 - [ ] 6.4 验证图片显示的正确性
42 - [ ] 6.5 测试大量历史记录的性能表现
43
44 ## 7. 文档和清理
45 - [ ] 7.1 更新历史记录功能的使用说明
46 - [ ] 7.2 清理不再使用的代码和方法
47 - [ ] 7.3 验证所有功能正常工作
48 - [ ] 7.4 确保UI一致性和用户体验
...\ No newline at end of file ...\ No newline at end of file
1 #!/usr/bin/env python3
2 """
3 测试配置文件加载逻辑
4 """
5 import json
6 import sys
7 import tempfile
8 from pathlib import Path
9
10 # 模拟PyInstaller环境
11 sys.frozen = True
12 sys._MEIPASS = "C:\\Users\\Shady\\PycharmProjects\\GoogleNanoBananaApp" # 模拟路径
13
14 # 导入配置加载逻辑
15 def test_config_loading():
16 """测试配置文件加载的多种路径"""
17
18 # 模拟exe所在目录的config.json
19 exe_dir_config = Path(sys.executable).parent / 'config.json' if hasattr(sys, 'executable') else None
20
21 # 模拟_MEIPASS目录的config.json
22 meipass_config = Path(sys._MEIPASS) / 'config.json'
23
24 print(f"模拟PyInstaller环境:")
25 print(f" sys.frozen: {getattr(sys, 'frozen', False)}")
26 print(f" sys._MEIPASS: {sys._MEIPASS}")
27
28 # 测试各个路径
29 test_paths = []
30 if hasattr(sys, 'executable'):
31 test_paths.append(("exe目录", Path(sys.executable).parent / 'config.json'))
32
33 test_paths.append(("_MEIPASS目录", Path(sys._MEIPASS) / 'config.json'))
34 test_paths.append(("当前目录", Path('.') / 'config.json'))
35
36 for name, path in test_paths:
37 if path.exists():
38 print(f"[OK] {name}: {path} - 存在")
39 try:
40 with open(path, 'r', encoding='utf-8') as f:
41 config = json.load(f)
42 db_config = config.get("db_config")
43 if db_config:
44 print(f" [OK] 包含数据库配置: {db_config.get('host', 'N/A')}")
45 else:
46 print(f" [FAIL] 未包含数据库配置")
47 except Exception as e:
48 print(f" [FAIL] 读取失败: {e}")
49 else:
50 print(f"[FAIL] {name}: {path} - 不存在")
51
52 if __name__ == "__main__":
53 test_config_loading()
...\ No newline at end of file ...\ No newline at end of file
...@@ -285,6 +285,13 @@ QPushButton[variant="ghost"]:hover {{ ...@@ -285,6 +285,13 @@ QPushButton[variant="ghost"]:hover {{
285 color: {c['accent']}; 285 color: {c['accent']};
286 }} 286 }}
287 287
288 /* 短暂成功反馈("已复制"等,2秒后回退)*/
289 QPushButton[variant="success-flash"] {{
290 background-color: {c['success']};
291 color: white;
292 border: 1px solid {c['success']};
293 }}
294
288 /* 链接按钮 — 像超链接 */ 295 /* 链接按钮 — 像超链接 */
289 QPushButton[variant="link"] {{ 296 QPushButton[variant="link"] {{
290 background-color: transparent; 297 background-color: transparent;
...@@ -601,6 +608,70 @@ QSplitter::handle:vertical {{ ...@@ -601,6 +608,70 @@ QSplitter::handle:vertical {{
601 background: {c['bg_subtle']}; 608 background: {c['bg_subtle']};
602 border-radius: {s['radius_md']}; 609 border-radius: {s['radius_md']};
603 }} 610 }}
611
612 /* ========== 参考图拖拽区(drag_state property)========== */
613 #referenceImageDrop[drag_state="idle"] {{
614 border: 2px dashed {c['border_default']};
615 border-radius: {s['radius_md']};
616 background-color: {c['bg_subtle']};
617 }}
618 #referenceImageDrop[drag_state="idle"]:hover {{
619 border: 2px dashed {c['accent']};
620 background-color: {c['accent_subtle']};
621 }}
622 #referenceImageDrop[drag_state="active"] {{
623 border: 2px dashed {c['accent']};
624 border-radius: {s['radius_md']};
625 background-color: {c['accent_subtle']};
626 }}
627
628 /* ========== 已生成图片缩略图卡 ========== */
629 QLabel[role="thumb"] {{
630 border: 1px solid {c['border_default']};
631 border-radius: {s['radius_sm']};
632 background-color: {c['bg_surface']};
633 }}
634 QLabel[role="thumb_index"] {{
635 color: {c['text_secondary']};
636 font-size: {s['font_base']};
637 font-weight: 600;
638 }}
639
640 /* ========== 预览区生成图(result_label / generated_image_label)========== */
641 QLabel#previewImage {{
642 background-color: {c['bg_subtle']};
643 border: 1px solid {c['border_default']};
644 border-radius: {s['radius_md']};
645 color: {c['text_tertiary']};
646 }}
647 QLabel#previewImage[has_image="true"] {{
648 background-color: {c['bg_canvas']};
649 border-color: {c['border_default']};
650 }}
651
652 /* ========== 历史详情提示词显示框 ========== */
653 QLabel#promptDisplay {{
654 background-color: {c['bg_subtle']};
655 border: 1px solid {c['border_default']};
656 border-radius: {s['radius_md']};
657 padding: 8px 10px;
658 color: {c['text_primary']};
659 }}
660
661 /* ========== 缩略图删除按钮(20x20 紧凑)========== */
662 QPushButton#thumbDeleteBtn {{
663 background-color: {c['danger']};
664 color: white;
665 border: none;
666 border-radius: 3px;
667 padding: 0;
668 min-height: 0;
669 font-size: {s['font_xs']};
670 font-weight: 700;
671 }}
672 QPushButton#thumbDeleteBtn:hover {{
673 background-color: {c['danger_hover']};
674 }}
604 """ 675 """
605 676
606 677
......