5d893c50 by 柴进

feat(qml): task #14d 下载图片 + 双击预览(task #14 全部完成)

ImageGenBridge.saveFile(src, dest) → bool
  shutil.copy2 wrap,源文件不存在 / 写盘失败时记日志返回 False

ImageGenTab.qml:
  - SaveFileDialog (FileDialog SaveFile mode + defaultSuffix png)
  - "下载图片" enabled by lastResultPath !== "",onClicked 弹保存对话框
  - onAccepted 调 imageGen.saveFile,成功状态绿色 "● 已保存到 <path>"
  - 预览 Image 包 MouseArea,onDoubleClicked Qt.openUrlExternally("file:///" + path)

至此 task #14 完整闭环:
  #14a 核心生成(prompt → submit → 进度 → 预览)
  #14b 参考图录入(添加 / 粘贴 / 拖拽 + 缩略图删除)
  #14c 提示词收藏 / 删除(持久化 config.json)
  #14d 下载图片 + 双击预览打开系统查看器

视觉验证:QML_AUTO_LOGIN=1 启动主窗口,UI 完整无回归,下载按钮在无生成图时正确灰。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 80128a44
......@@ -6,6 +6,7 @@ QML 调 imageGen.submitTask(prompt, refs, aspect, size, mode) → 桥层把 mode
然后把 result_path 通过 taskCompleted 信号转出(QML 只拿到文件路径,不传 bytes)。
"""
import logging
import shutil
import tempfile
import time
import uuid
......@@ -153,6 +154,23 @@ class ImageGenBridge(QObject):
self._logger.info(f"剪贴板图片已存: {path}")
return str(path)
@Slot(str, str, result=bool)
def saveFile(self, src: str, dest: str) -> bool:
"""复制 src 到 dest(用户从下载对话框选择目标路径)。失败返回 False。"""
try:
src_path = Path(src)
dest_path = Path(dest)
if not src_path.exists():
self._logger.warning(f"saveFile 源文件不存在: {src}")
return False
dest_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(str(src_path), str(dest_path))
self._logger.info(f"图片已下载: {dest}")
return True
except Exception as e:
self._logger.error(f"saveFile 失败 src={src} dest={dest}: {e}")
return False
@Slot("QVariantList", result="QVariantList")
def normalizeFileUrls(self, urls) -> list:
"""QML DropArea 给的是 file:/// QUrl 列表,转成本地路径字符串列表(过滤非图片)。"""
......
......@@ -39,6 +39,25 @@ Item {
onAccepted: tab.addRefPaths(imageGen.normalizeFileUrls(selectedFiles))
}
FileDialog {
id: saveImageDialog
title: "保存生成的图片"
fileMode: FileDialog.SaveFile
nameFilters: ["PNG 图片 (*.png)"]
defaultSuffix: "png"
onAccepted: {
var dest = selectedFile.toString().replace("file:///", "")
var ok = imageGen.saveFile(tab.lastResultPath, dest)
if (ok) {
tab.statusText = "● 已保存到 " + dest
tab.statusColor = App.Theme.success
} else {
tab.statusText = "● 保存失败"
tab.statusColor = App.Theme.danger
}
}
}
// ===== 桥层信号 =====
Connections {
target: imageGen
......@@ -379,7 +398,8 @@ Item {
}
SecondaryButton {
text: "下载图片"
enabled: false
enabled: tab.lastResultPath !== ""
onClicked: saveImageDialog.open()
}
Label {
text: tab.statusText
......@@ -422,6 +442,16 @@ Item {
asynchronous: true
cache: false // 同路径下次生成会被覆盖,强制重读
visible: tab.lastResultPath !== ""
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onDoubleClicked: {
if (tab.lastResultPath) {
Qt.openUrlExternally("file:///" + tab.lastResultPath)
}
}
}
}
Column {
......