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 {
ImageGenTab {}
// 款式设计 — 占位
Item {
Text {
anchors.centerIn: parent
text: "款式设计 tab 内容\n(QML PoC 暂未实现)"
color: App.Theme.textTertiary
font.family: App.Theme.fontFamily
font.pointSize: App.Theme.fontLg
horizontalAlignment: Text.AlignHCenter
}
}
StyleDesignerTab {}
HistoryTab {}
}
......
import QtQuick
import QtQuick.Controls.Basic
import QtQuick.Layouts
import "components"
import "." as App
Item {
id: tab
// ===== 状态 =====
// 8 字段表单值(与 jewelry.categories 顺序一致)
property var formData: ({
"主石形状": "",
"主石材质": "",
"金属": "",
"花头形式": "",
"戒臂结构": "",
"戒臂处理": "",
"辅石镶嵌": "",
"特殊元素": "",
})
property string assembledPrompt: ""
property string currentTaskId: ""
property string lastResultPath: ""
property string statusText: "● 就绪"
property color statusColor: App.Theme.success
function updateField(category, value) {
var copy = Object.assign({}, tab.formData)
copy[category] = value || ""
tab.formData = copy
tab.assembledPrompt = jewelry.previewPrompt(copy)
}
function reassemble() {
tab.assembledPrompt = jewelry.previewPrompt(tab.formData)
}
function submit() {
if (tab.currentTaskId !== "") return
if (tab.assembledPrompt.trim().length === 0) {
tab.statusText = "● 选几个字段先"
tab.statusColor = App.Theme.danger
return
}
try {
// submitTask 同步返回 task_id,自己记下来给信号过滤用
var taskId = imageGen.submitTask(
tab.assembledPrompt,
[], // 款式设计不带参考图
"1:1", // 珠宝出图常用比例
"2K",
"慢速模式" // Pro 模型,质量优先
)
tab.currentTaskId = taskId
tab.statusText = "● 已提交"
tab.statusColor = App.Theme.accent
} catch (e) {
tab.statusText = "● " + e
tab.statusColor = App.Theme.danger
}
}
Component.onCompleted: reassemble()
Connections {
target: jewelry
function onLibraryChanged(category) {
optionsRepeater.reloadAll()
}
}
// 监听生成信号 — 只对自己提交的 task 做反应(taskId 唯一可区分本 tab vs 图片生成 tab)
Connections {
target: imageGen
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
}
}
RowLayout {
anchors.fill: parent
anchors.margins: App.Theme.space5
spacing: App.Theme.space4
// ===== 左侧:8 字段表单 =====
Card {
Layout.preferredWidth: 360
Layout.fillHeight: true
ColumnLayout {
anchors.fill: parent
anchors.margins: App.Theme.space4
spacing: App.Theme.space3
CaptionLabel {
text: "款式字段"
font.pointSize: App.Theme.fontLg
}
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ColumnLayout {
width: parent.width
spacing: App.Theme.space3
Repeater {
id: optionsRepeater
model: jewelry.categories // 8 个类别名
// task #15b 词库变化时重新拉
function reloadAll() {
// QML 自动重新评估 model:,主动 reset
model = []
model = jewelry.categories
}
delegate: ColumnLayout {
Layout.fillWidth: true
spacing: 4
CaptionLabel {
text: modelData
font.pointSize: App.Theme.fontBase
}
ThemedComboBox {
Layout.fillWidth: true
// 头插一个 "(不选)" 让用户能清空字段
model: ["(不选)"].concat(jewelry.getOptions(modelData))
onActivated: {
var v = currentText === "(不选)" ? "" : currentText
tab.updateField(modelData, v)
}
}
}
}
}
}
}
}
// ===== 右侧:Prompt 预览 + 生成 + 结果 =====
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: App.Theme.space4
// Prompt 预览卡
Card {
Layout.fillWidth: true
Layout.preferredHeight: 200
ColumnLayout {
anchors.fill: parent
anchors.margins: App.Theme.space4
spacing: App.Theme.space3
RowLayout {
spacing: App.Theme.space3
CaptionLabel {
text: "Prompt 预览"
font.pointSize: App.Theme.fontLg
}
Item { Layout.fillWidth: true }
Label {
text: tab.assembledPrompt.length + " 字"
font.family: App.Theme.fontFamily
font.pointSize: App.Theme.fontXs
color: App.Theme.textTertiary
}
}
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
Rectangle {
anchors.fill: parent
color: App.Theme.bgSubtle
radius: App.Theme.radiusMd
Text {
anchors.fill: parent
anchors.margins: App.Theme.space3
text: tab.assembledPrompt
color: App.Theme.textPrimary
font.family: App.Theme.fontFamily
font.pointSize: App.Theme.fontSm
wrapMode: Text.Wrap
}
}
}
}
}
// 操作行
RowLayout {
spacing: App.Theme.space3
Layout.fillWidth: true
PrimaryButton {
text: tab.currentTaskId !== "" ? "生成中…" : "生成图片"
enabled: tab.currentTaskId === ""
onClicked: tab.submit()
}
SecondaryButton {
text: "重置字段"
onClicked: {
var fresh = {}
for (var i = 0; i < jewelry.categories.length; i++) {
fresh[jewelry.categories[i]] = ""
}
tab.formData = fresh
tab.reassemble()
}
}
Label {
text: tab.statusText
font.family: App.Theme.fontFamily
font.pointSize: App.Theme.fontSm
color: tab.statusColor
}
Item { Layout.fillWidth: true }
}
// 预览大图
Card {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 240
ColumnLayout {
anchors.fill: parent
anchors.margins: App.Theme.space4
spacing: App.Theme.space3
CaptionLabel {
text: "预览"
font.pointSize: App.Theme.fontLg
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: App.Theme.bgSubtle
radius: App.Theme.radiusMd
Image {
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 !== ""
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onDoubleClicked: {
if (tab.lastResultPath) {
Qt.openUrlExternally("file:///" + tab.lastResultPath)
}
}
}
}
Text {
anchors.centerIn: parent
visible: tab.lastResultPath === ""
text: "选择字段后点 \"生成图片\""
color: App.Theme.textTertiary
font.family: App.Theme.fontFamily
font.pointSize: App.Theme.fontSm
}
}
}
}
}
}
}