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" ...@@ -5,6 +5,68 @@ import "components"
5 import "." as App 5 import "." as App
6 6
7 Item { 7 Item {
8 id: tab
9
10 // ===== 状态 =====
11 property var refImages: [] // list[str] 参考图本地路径,task #14b 接上传/粘贴/拖拽
12 property string currentTaskId: "" // 当前进行中的任务 id(同一时刻最多一个)
13 property string lastResultPath: "" // 最近一次 taskCompleted 的图片路径
14 property string statusText: "● 就绪"
15 property color statusColor: App.Theme.success
16
17 // ===== 桥层信号 =====
18 Connections {
19 target: imageGen
20
21 function onTaskSubmitted(taskId) {
22 tab.currentTaskId = taskId
23 tab.statusText = "● 已提交"
24 tab.statusColor = App.Theme.accent
25 }
26
27 function onTaskProgress(taskId, progress, msg) {
28 if (taskId !== tab.currentTaskId) return
29 tab.statusText = "● " + (msg || "生成中…")
30 tab.statusColor = App.Theme.accent
31 }
32
33 function onTaskCompleted(taskId, resultPath, prompt, model) {
34 if (taskId !== tab.currentTaskId) return
35 tab.lastResultPath = resultPath
36 tab.currentTaskId = ""
37 tab.statusText = "● 已完成"
38 tab.statusColor = App.Theme.success
39 }
40
41 function onTaskFailed(taskId, error) {
42 if (taskId !== tab.currentTaskId) return
43 tab.currentTaskId = ""
44 tab.statusText = "● " + (error || "失败")
45 tab.statusColor = App.Theme.danger
46 }
47 }
48
49 function submit() {
50 if (tab.currentTaskId !== "") return // busy
51 if (promptArea.text.trim().length === 0) {
52 tab.statusText = "● 请输入提示词"
53 tab.statusColor = App.Theme.danger
54 return
55 }
56 try {
57 imageGen.submitTask(
58 promptArea.text.trim(),
59 tab.refImages,
60 aspectCombo.currentText,
61 sizeCombo.currentText,
62 modeCombo.currentText
63 )
64 } catch (e) {
65 tab.statusText = "● " + e
66 tab.statusColor = App.Theme.danger
67 }
68 }
69
8 ColumnLayout { 70 ColumnLayout {
9 anchors.fill: parent 71 anchors.fill: parent
10 anchors.margins: App.Theme.space5 72 anchors.margins: App.Theme.space5
...@@ -32,10 +94,10 @@ Item { ...@@ -32,10 +94,10 @@ Item {
32 94
33 RowLayout { 95 RowLayout {
34 spacing: App.Theme.space2 96 spacing: App.Theme.space2
35 SecondaryButton { text: "添加图片" } 97 SecondaryButton { text: "添加图片"; enabled: false }
36 SecondaryButton { text: "📋 粘贴图片" } 98 SecondaryButton { text: "📋 粘贴图片"; enabled: false }
37 Label { 99 Label {
38 text: "已选择 0 张" 100 text: "已选择 " + tab.refImages.length + " 张"
39 font.family: App.Theme.fontFamily 101 font.family: App.Theme.fontFamily
40 font.pointSize: App.Theme.fontSm 102 font.pointSize: App.Theme.fontSm
41 color: App.Theme.textSecondary 103 color: App.Theme.textSecondary
...@@ -93,7 +155,7 @@ Item { ...@@ -93,7 +155,7 @@ Item {
93 155
94 RowLayout { 156 RowLayout {
95 spacing: App.Theme.space2 157 spacing: App.Theme.space2
96 SecondaryButton { text: "⭐ 收藏" } 158 SecondaryButton { text: "⭐ 收藏"; enabled: false }
97 Label { 159 Label {
98 text: "快速选择:" 160 text: "快速选择:"
99 font.family: App.Theme.fontFamily 161 font.family: App.Theme.fontFamily
...@@ -101,17 +163,21 @@ Item { ...@@ -101,17 +163,21 @@ Item {
101 color: App.Theme.textSecondary 163 color: App.Theme.textSecondary
102 } 164 }
103 ThemedComboBox { 165 ThemedComboBox {
166 id: quickPromptCombo
104 Layout.fillWidth: true 167 Layout.fillWidth: true
105 model: ["主石换成闪耀的祖母绿", "改成玫瑰金材质", "增加更多碎钻"] 168 model: ["主石换成闪耀的祖母绿", "改成玫瑰金材质", "增加更多碎钻"]
169 onActivated: promptArea.text = currentText
106 } 170 }
107 SecondaryButton { text: "删除" } 171 SecondaryButton { text: "删除"; enabled: false }
108 } 172 }
109 173
110 ScrollView { 174 ScrollView {
111 Layout.fillWidth: true 175 Layout.fillWidth: true
112 Layout.fillHeight: true 176 Layout.fillHeight: true
113 TextArea { 177 TextArea {
114 text: "一幅美丽的风景画,有山有湖,日落时分" 178 id: promptArea
179 text: ""
180 placeholderText: "描述你想生成的图片…"
115 font.family: App.Theme.fontFamily 181 font.family: App.Theme.fontFamily
116 font.pointSize: App.Theme.fontBase 182 font.pointSize: App.Theme.fontBase
117 color: App.Theme.textPrimary 183 color: App.Theme.textPrimary
...@@ -142,21 +208,37 @@ Item { ...@@ -142,21 +208,37 @@ Item {
142 font.pointSize: App.Theme.fontLg 208 font.pointSize: App.Theme.fontLg
143 } 209 }
144 210
145 Repeater { 211 ColumnLayout {
146 model: [ 212 spacing: 4
147 { caption: "生成模式", options: ["极速模式", "慢速模式"] }, 213 Layout.fillWidth: true
148 { caption: "宽高比", options: ["1:1", "2:3", "3:2", "16:9", "9:16", "4:3", "3:4"] }, 214 CaptionLabel { text: "生成模式"; font.pointSize: App.Theme.fontBase }
149 { caption: "图片尺寸", options: ["1K", "2K", "4K"] } 215 ThemedComboBox {
150 ] 216 id: modeCombo
151 delegate: ColumnLayout { 217 Layout.fillWidth: true
218 model: ["极速模式", "慢速模式"]
219 }
220 }
221
222 ColumnLayout {
152 spacing: 4 223 spacing: 4
153 Layout.fillWidth: true 224 Layout.fillWidth: true
154 CaptionLabel { text: modelData.caption; font.pointSize: App.Theme.fontBase } 225 CaptionLabel { text: "宽高比"; font.pointSize: App.Theme.fontBase }
155 ThemedComboBox { 226 ThemedComboBox {
227 id: aspectCombo
156 Layout.fillWidth: true 228 Layout.fillWidth: true
157 model: modelData.options 229 model: ["1:1", "2:3", "3:2", "16:9", "9:16", "4:3", "3:4"]
158 } 230 }
159 } 231 }
232
233 ColumnLayout {
234 spacing: 4
235 Layout.fillWidth: true
236 CaptionLabel { text: "图片尺寸"; font.pointSize: App.Theme.fontBase }
237 ThemedComboBox {
238 id: sizeCombo
239 Layout.fillWidth: true
240 model: ["1K", "2K", "4K"]
241 }
160 } 242 }
161 243
162 Item { Layout.fillHeight: true } 244 Item { Layout.fillHeight: true }
...@@ -170,16 +252,19 @@ Item { ...@@ -170,16 +252,19 @@ Item {
170 Layout.fillWidth: true 252 Layout.fillWidth: true
171 253
172 PrimaryButton { 254 PrimaryButton {
173 text: "生成图片" 255 text: tab.currentTaskId !== "" ? "生成中…" : "生成图片"
256 enabled: tab.currentTaskId === ""
257 onClicked: tab.submit()
174 } 258 }
175 SecondaryButton { 259 SecondaryButton {
176 text: "下载图片" 260 text: "下载图片"
261 enabled: false
177 } 262 }
178 Label { 263 Label {
179 text: "● 就绪" 264 text: tab.statusText
180 font.family: App.Theme.fontFamily 265 font.family: App.Theme.fontFamily
181 font.pointSize: App.Theme.fontSm 266 font.pointSize: App.Theme.fontSm
182 color: App.Theme.success 267 color: tab.statusColor
183 } 268 }
184 Item { Layout.fillWidth: true } 269 Item { Layout.fillWidth: true }
185 } 270 }
...@@ -206,9 +291,22 @@ Item { ...@@ -206,9 +291,22 @@ Item {
206 color: App.Theme.bgSubtle 291 color: App.Theme.bgSubtle
207 radius: App.Theme.radiusMd 292 radius: App.Theme.radiusMd
208 293
294 Image {
295 id: previewImage
296 anchors.fill: parent
297 anchors.margins: App.Theme.space3
298 source: tab.lastResultPath ? "file:///" + tab.lastResultPath : ""
299 fillMode: Image.PreserveAspectFit
300 smooth: true
301 asynchronous: true
302 cache: false // 同路径下次生成会被覆盖,强制重读
303 visible: tab.lastResultPath !== ""
304 }
305
209 Column { 306 Column {
210 anchors.centerIn: parent 307 anchors.centerIn: parent
211 spacing: 4 308 spacing: 4
309 visible: tab.lastResultPath === ""
212 310
213 Text { 311 Text {
214 anchors.horizontalCenter: parent.horizontalCenter 312 anchors.horizontalCenter: parent.horizontalCenter
......