eb469aab by 柴进

feat(qml): task #15a 款式设计 tab 主体(8 字段 + Prompt 预览 + 生成)

qml_poc/qml/StyleDesignerTab.qml (新):
  - 左侧 360px 卡片:8 字段 ComboBox 表单(jewelry.categories Repeater)
    每个字段头插 "(不选)" 让用户能清空
    onActivated → updateField(category, value) → 触发 reassemble
  - 右侧上半 Prompt 预览:实时调 jewelry.previewPrompt(formData)
    Text 只读展示 + 字数统计
  - 右侧操作行:生成图片 PrimaryButton (调 imageGen.submitTask
    默认 1:1 / 2K / 慢速模式 Pro 模型) + 重置字段 + 状态指示器
  - 右侧下半预览:lastResultPath Image,双击系统查看器打开
  - 监听 imageGen 4 个信号但只对 currentTaskId 匹配的做反应(taskId
    唯一可区分图片生成 tab vs 款式设计 tab,互不冲突)
  - 监听 jewelry.libraryChanged → optionsRepeater.reloadAll
    (task #15b 词库管理对话框触发时刷新 ComboBox model)

MainWindow.qml: 款式设计占位 Item → StyleDesignerTab {}

未做(task #15b):
  - 词库管理子对话框(每个类别能增删词条)
  - jewelry.addItem / removeItem / resetAll Slot 都已就位,缺 UI

视觉验证:QML_DEBUG_TAB=1 直接进款式设计 tab,8 字段 ComboBox + Prompt
预览(PromptAssembler 在空表单时给 fallback "一款高端精品珠宝戒指设计…
高端珠宝渲染" 52 字)+ 生成按钮全就位,UI 无回归。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cabdf6a1
...@@ -95,17 +95,7 @@ Rectangle { ...@@ -95,17 +95,7 @@ Rectangle {
95 95
96 ImageGenTab {} 96 ImageGenTab {}
97 97
98 // 款式设计 — 占位 98 StyleDesignerTab {}
99 Item {
100 Text {
101 anchors.centerIn: parent
102 text: "款式设计 tab 内容\n(QML PoC 暂未实现)"
103 color: App.Theme.textTertiary
104 font.family: App.Theme.fontFamily
105 font.pointSize: App.Theme.fontLg
106 horizontalAlignment: Text.AlignHCenter
107 }
108 }
109 99
110 HistoryTab {} 100 HistoryTab {}
111 } 101 }
......
1 import QtQuick
2 import QtQuick.Controls.Basic
3 import QtQuick.Layouts
4 import "components"
5 import "." as App
6
7 Item {
8 id: tab
9
10 // ===== 状态 =====
11 // 8 字段表单值(与 jewelry.categories 顺序一致)
12 property var formData: ({
13 "主石形状": "",
14 "主石材质": "",
15 "金属": "",
16 "花头形式": "",
17 "戒臂结构": "",
18 "戒臂处理": "",
19 "辅石镶嵌": "",
20 "特殊元素": "",
21 })
22 property string assembledPrompt: ""
23 property string currentTaskId: ""
24 property string lastResultPath: ""
25 property string statusText: "● 就绪"
26 property color statusColor: App.Theme.success
27
28 function updateField(category, value) {
29 var copy = Object.assign({}, tab.formData)
30 copy[category] = value || ""
31 tab.formData = copy
32 tab.assembledPrompt = jewelry.previewPrompt(copy)
33 }
34
35 function reassemble() {
36 tab.assembledPrompt = jewelry.previewPrompt(tab.formData)
37 }
38
39 function submit() {
40 if (tab.currentTaskId !== "") return
41 if (tab.assembledPrompt.trim().length === 0) {
42 tab.statusText = "● 选几个字段先"
43 tab.statusColor = App.Theme.danger
44 return
45 }
46 try {
47 // submitTask 同步返回 task_id,自己记下来给信号过滤用
48 var taskId = imageGen.submitTask(
49 tab.assembledPrompt,
50 [], // 款式设计不带参考图
51 "1:1", // 珠宝出图常用比例
52 "2K",
53 "慢速模式" // Pro 模型,质量优先
54 )
55 tab.currentTaskId = taskId
56 tab.statusText = "● 已提交"
57 tab.statusColor = App.Theme.accent
58 } catch (e) {
59 tab.statusText = "● " + e
60 tab.statusColor = App.Theme.danger
61 }
62 }
63
64 Component.onCompleted: reassemble()
65
66 Connections {
67 target: jewelry
68 function onLibraryChanged(category) {
69 optionsRepeater.reloadAll()
70 }
71 }
72
73 // 监听生成信号 — 只对自己提交的 task 做反应(taskId 唯一可区分本 tab vs 图片生成 tab)
74 Connections {
75 target: imageGen
76
77 function onTaskProgress(taskId, progress, msg) {
78 if (taskId !== tab.currentTaskId) return
79 tab.statusText = "● " + (msg || "生成中…")
80 tab.statusColor = App.Theme.accent
81 }
82 function onTaskCompleted(taskId, resultPath, prompt, model) {
83 if (taskId !== tab.currentTaskId) return
84 tab.lastResultPath = resultPath
85 tab.currentTaskId = ""
86 tab.statusText = "● 已完成"
87 tab.statusColor = App.Theme.success
88 }
89 function onTaskFailed(taskId, error) {
90 if (taskId !== tab.currentTaskId) return
91 tab.currentTaskId = ""
92 tab.statusText = "● " + (error || "失败")
93 tab.statusColor = App.Theme.danger
94 }
95 }
96
97 RowLayout {
98 anchors.fill: parent
99 anchors.margins: App.Theme.space5
100 spacing: App.Theme.space4
101
102 // ===== 左侧:8 字段表单 =====
103 Card {
104 Layout.preferredWidth: 360
105 Layout.fillHeight: true
106
107 ColumnLayout {
108 anchors.fill: parent
109 anchors.margins: App.Theme.space4
110 spacing: App.Theme.space3
111
112 CaptionLabel {
113 text: "款式字段"
114 font.pointSize: App.Theme.fontLg
115 }
116
117 ScrollView {
118 Layout.fillWidth: true
119 Layout.fillHeight: true
120 clip: true
121
122 ColumnLayout {
123 width: parent.width
124 spacing: App.Theme.space3
125
126 Repeater {
127 id: optionsRepeater
128 model: jewelry.categories // 8 个类别名
129
130 // task #15b 词库变化时重新拉
131 function reloadAll() {
132 // QML 自动重新评估 model:,主动 reset
133 model = []
134 model = jewelry.categories
135 }
136
137 delegate: ColumnLayout {
138 Layout.fillWidth: true
139 spacing: 4
140
141 CaptionLabel {
142 text: modelData
143 font.pointSize: App.Theme.fontBase
144 }
145 ThemedComboBox {
146 Layout.fillWidth: true
147 // 头插一个 "(不选)" 让用户能清空字段
148 model: ["(不选)"].concat(jewelry.getOptions(modelData))
149 onActivated: {
150 var v = currentText === "(不选)" ? "" : currentText
151 tab.updateField(modelData, v)
152 }
153 }
154 }
155 }
156 }
157 }
158 }
159 }
160
161 // ===== 右侧:Prompt 预览 + 生成 + 结果 =====
162 ColumnLayout {
163 Layout.fillWidth: true
164 Layout.fillHeight: true
165 spacing: App.Theme.space4
166
167 // Prompt 预览卡
168 Card {
169 Layout.fillWidth: true
170 Layout.preferredHeight: 200
171
172 ColumnLayout {
173 anchors.fill: parent
174 anchors.margins: App.Theme.space4
175 spacing: App.Theme.space3
176
177 RowLayout {
178 spacing: App.Theme.space3
179 CaptionLabel {
180 text: "Prompt 预览"
181 font.pointSize: App.Theme.fontLg
182 }
183 Item { Layout.fillWidth: true }
184 Label {
185 text: tab.assembledPrompt.length + " 字"
186 font.family: App.Theme.fontFamily
187 font.pointSize: App.Theme.fontXs
188 color: App.Theme.textTertiary
189 }
190 }
191
192 ScrollView {
193 Layout.fillWidth: true
194 Layout.fillHeight: true
195
196 Rectangle {
197 anchors.fill: parent
198 color: App.Theme.bgSubtle
199 radius: App.Theme.radiusMd
200
201 Text {
202 anchors.fill: parent
203 anchors.margins: App.Theme.space3
204 text: tab.assembledPrompt
205 color: App.Theme.textPrimary
206 font.family: App.Theme.fontFamily
207 font.pointSize: App.Theme.fontSm
208 wrapMode: Text.Wrap
209 }
210 }
211 }
212 }
213 }
214
215 // 操作行
216 RowLayout {
217 spacing: App.Theme.space3
218 Layout.fillWidth: true
219
220 PrimaryButton {
221 text: tab.currentTaskId !== "" ? "生成中…" : "生成图片"
222 enabled: tab.currentTaskId === ""
223 onClicked: tab.submit()
224 }
225 SecondaryButton {
226 text: "重置字段"
227 onClicked: {
228 var fresh = {}
229 for (var i = 0; i < jewelry.categories.length; i++) {
230 fresh[jewelry.categories[i]] = ""
231 }
232 tab.formData = fresh
233 tab.reassemble()
234 }
235 }
236 Label {
237 text: tab.statusText
238 font.family: App.Theme.fontFamily
239 font.pointSize: App.Theme.fontSm
240 color: tab.statusColor
241 }
242 Item { Layout.fillWidth: true }
243 }
244
245 // 预览大图
246 Card {
247 Layout.fillWidth: true
248 Layout.fillHeight: true
249 Layout.minimumHeight: 240
250
251 ColumnLayout {
252 anchors.fill: parent
253 anchors.margins: App.Theme.space4
254 spacing: App.Theme.space3
255
256 CaptionLabel {
257 text: "预览"
258 font.pointSize: App.Theme.fontLg
259 }
260
261 Rectangle {
262 Layout.fillWidth: true
263 Layout.fillHeight: true
264 color: App.Theme.bgSubtle
265 radius: App.Theme.radiusMd
266
267 Image {
268 anchors.fill: parent
269 anchors.margins: App.Theme.space3
270 source: tab.lastResultPath ? "file:///" + tab.lastResultPath : ""
271 fillMode: Image.PreserveAspectFit
272 smooth: true
273 asynchronous: true
274 cache: false
275 visible: tab.lastResultPath !== ""
276
277 MouseArea {
278 anchors.fill: parent
279 cursorShape: Qt.PointingHandCursor
280 onDoubleClicked: {
281 if (tab.lastResultPath) {
282 Qt.openUrlExternally("file:///" + tab.lastResultPath)
283 }
284 }
285 }
286 }
287
288 Text {
289 anchors.centerIn: parent
290 visible: tab.lastResultPath === ""
291 text: "选择字段后点 \"生成图片\""
292 color: App.Theme.textTertiary
293 font.family: App.Theme.fontFamily
294 font.pointSize: App.Theme.fontSm
295 }
296 }
297 }
298 }
299 }
300 }
301 }