257c4a71 by 柴进

feat(qml): task #14a 图片生成 tab 核心闭环(无参考图)

ImageGenTab.qml wiring 改造:
- 加状态 properties: refImages / currentTaskId / lastResultPath / statusText / statusColor
- TextArea 取 placeholder,删硬编码 "风景画" 占位
- 模式/宽高比/尺寸 三个 ComboBox 给 id,submit() 时取 currentText
- 操作行 "生成图片" PrimaryButton onClicked → tab.submit()
  - submit 内部空 prompt 阻断 + 重复点击防护 + 调 imageGen.submitTask
  - 任务进行中按钮文字 "生成中…",结束自动恢复
- Connections 监听 imageGen 4 个信号:
  - taskSubmitted → "● 已提交"
  - taskProgress → "● 正在生成…"
  - taskCompleted → 拿 resultPath 更新 lastResultPath,"● 已完成"
  - taskFailed → "● <error>",红色
- 预览 Rectangle 内嵌 Image (file:/// + lastResultPath, cache:false 强制重读)
  - 无图片时显示占位文字,有图时直接展示

未做(task #14b/c/d 后续 commit):
- 添加图片 / 粘贴图片 按钮(QFileDialog + QClipboard,桥要补 Slot)
- :star: 收藏 + 删除 saved_prompts 持久化
- 下载图片 按钮 / 双击预览打开系统查看器

视觉验证:QML_AUTO_LOGIN=1 启动主窗口,所有控件渲染无回归。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f621d57c
......@@ -5,6 +5,68 @@ import "components"
import "." as App
Item {
id: tab
// ===== 状态 =====
property var refImages: [] // list[str] 参考图本地路径,task #14b 接上传/粘贴/拖拽
property string currentTaskId: "" // 当前进行中的任务 id(同一时刻最多一个)
property string lastResultPath: "" // 最近一次 taskCompleted 的图片路径
property string statusText: "● 就绪"
property color statusColor: App.Theme.success
// ===== 桥层信号 =====
Connections {
target: imageGen
function onTaskSubmitted(taskId) {
tab.currentTaskId = taskId
tab.statusText = "● 已提交"
tab.statusColor = App.Theme.accent
}
function onTaskProgress(taskId, progress, msg) {
if (taskId !== tab.currentTaskId) return
tab.statusText = "● " + (msg || "生成中…")
tab.statusColor = App.Theme.accent
}
function onTaskCompleted(taskId, resultPath, prompt, model) {
if (taskId !== tab.currentTaskId) return
tab.lastResultPath = resultPath
tab.currentTaskId = ""
tab.statusText = "● 已完成"
tab.statusColor = App.Theme.success
}
function onTaskFailed(taskId, error) {
if (taskId !== tab.currentTaskId) return
tab.currentTaskId = ""
tab.statusText = "● " + (error || "失败")
tab.statusColor = App.Theme.danger
}
}
function submit() {
if (tab.currentTaskId !== "") return // busy
if (promptArea.text.trim().length === 0) {
tab.statusText = "● 请输入提示词"
tab.statusColor = App.Theme.danger
return
}
try {
imageGen.submitTask(
promptArea.text.trim(),
tab.refImages,
aspectCombo.currentText,
sizeCombo.currentText,
modeCombo.currentText
)
} catch (e) {
tab.statusText = "● " + e
tab.statusColor = App.Theme.danger
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: App.Theme.space5
......@@ -32,10 +94,10 @@ Item {
RowLayout {
spacing: App.Theme.space2
SecondaryButton { text: "添加图片" }
SecondaryButton { text: "📋 粘贴图片" }
SecondaryButton { text: "添加图片"; enabled: false }
SecondaryButton { text: "📋 粘贴图片"; enabled: false }
Label {
text: "已选择 0 张"
text: "已选择 " + tab.refImages.length + " 张"
font.family: App.Theme.fontFamily
font.pointSize: App.Theme.fontSm
color: App.Theme.textSecondary
......@@ -93,7 +155,7 @@ Item {
RowLayout {
spacing: App.Theme.space2
SecondaryButton { text: "⭐ 收藏" }
SecondaryButton { text: "⭐ 收藏"; enabled: false }
Label {
text: "快速选择:"
font.family: App.Theme.fontFamily
......@@ -101,17 +163,21 @@ Item {
color: App.Theme.textSecondary
}
ThemedComboBox {
id: quickPromptCombo
Layout.fillWidth: true
model: ["主石换成闪耀的祖母绿", "改成玫瑰金材质", "增加更多碎钻"]
onActivated: promptArea.text = currentText
}
SecondaryButton { text: "删除" }
SecondaryButton { text: "删除"; enabled: false }
}
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
TextArea {
text: "一幅美丽的风景画,有山有湖,日落时分"
id: promptArea
text: ""
placeholderText: "描述你想生成的图片…"
font.family: App.Theme.fontFamily
font.pointSize: App.Theme.fontBase
color: App.Theme.textPrimary
......@@ -142,21 +208,37 @@ Item {
font.pointSize: App.Theme.fontLg
}
Repeater {
model: [
{ caption: "生成模式", options: ["极速模式", "慢速模式"] },
{ caption: "宽高比", options: ["1:1", "2:3", "3:2", "16:9", "9:16", "4:3", "3:4"] },
{ caption: "图片尺寸", options: ["1K", "2K", "4K"] }
]
delegate: ColumnLayout {
ColumnLayout {
spacing: 4
Layout.fillWidth: true
CaptionLabel { text: "生成模式"; font.pointSize: App.Theme.fontBase }
ThemedComboBox {
id: modeCombo
Layout.fillWidth: true
model: ["极速模式", "慢速模式"]
}
}
ColumnLayout {
spacing: 4
Layout.fillWidth: true
CaptionLabel { text: modelData.caption; font.pointSize: App.Theme.fontBase }
CaptionLabel { text: "宽高比"; font.pointSize: App.Theme.fontBase }
ThemedComboBox {
id: aspectCombo
Layout.fillWidth: true
model: modelData.options
model: ["1:1", "2:3", "3:2", "16:9", "9:16", "4:3", "3:4"]
}
}
ColumnLayout {
spacing: 4
Layout.fillWidth: true
CaptionLabel { text: "图片尺寸"; font.pointSize: App.Theme.fontBase }
ThemedComboBox {
id: sizeCombo
Layout.fillWidth: true
model: ["1K", "2K", "4K"]
}
}
Item { Layout.fillHeight: true }
......@@ -170,16 +252,19 @@ Item {
Layout.fillWidth: true
PrimaryButton {
text: "生成图片"
text: tab.currentTaskId !== "" ? "生成中…" : "生成图片"
enabled: tab.currentTaskId === ""
onClicked: tab.submit()
}
SecondaryButton {
text: "下载图片"
enabled: false
}
Label {
text: "● 就绪"
text: tab.statusText
font.family: App.Theme.fontFamily
font.pointSize: App.Theme.fontSm
color: App.Theme.success
color: tab.statusColor
}
Item { Layout.fillWidth: true }
}
......@@ -206,9 +291,22 @@ Item {
color: App.Theme.bgSubtle
radius: App.Theme.radiusMd
Image {
id: previewImage
anchors.fill: parent
anchors.margins: App.Theme.space3
source: tab.lastResultPath ? "file:///" + tab.lastResultPath : ""
fillMode: Image.PreserveAspectFit
smooth: true
asynchronous: true
cache: false // 同路径下次生成会被覆盖,强制重读
visible: tab.lastResultPath !== ""
}
Column {
anchors.centerIn: parent
spacing: 4
visible: tab.lastResultPath === ""
Text {
anchors.horizontalCenter: parent.horizontalCenter
......