a0660bb7 by 柴进

feat(qml): QML PoC — Apple 高级浅色主题 + Apple Blue

新分支 feat/ui-qml-poc 从 master 起,独立于 feat/ui-redesign-apple-theme。
PySide6 + QtQuick 路线,业务后端不动,只换 UI 层。

文件:
- qml_poc/main_qml.py         入口(QQmlApplicationEngine + AppState QObject)
- qml_poc/qml/Main.qml        ApplicationWindow,按 loggedIn 切换尺寸 + 子页
- qml_poc/qml/Theme.qml       Singleton 设计令牌(24 色 + 尺寸 + 字号 + 跨平台字体栈)
- qml_poc/qml/qmldir          模块声明(singleton Theme)
- qml_poc/qml/LoginScreen.qml 登录页(标题 + 副标题 + 输入 + 复选 + pill 按钮 + 回车提交)
- qml_poc/qml/MainWindow.qml  主窗口(下划线式 TabBar + 任务队列 sidebar + StackLayout)
- qml_poc/qml/ImageGenTab.qml 图片生成 tab UI(参考图卡 + 提示词卡 + 生成设置卡 + 操作 + 预览)
- qml_poc/qml/components/     Card / PrimaryButton / SecondaryButton / ThemedTextField / ThemedComboBox / CaptionLabel

UI 改进:
- caption + combo 用 ColumnLayout 6px 间距;Card 内 12px spacing
- TabBar 改下划线式(不是凸起 tab)
- 主按钮 pill 圆角 980 + Apple Blue + ColorAnimation 120ms hover/pressed
- 输入框焦点 2px 蓝边动效
- 复选框 Apple Blue 实色 + 白色 ✓
- 卡片圆角 12px + 1px 极淡边框

跨平台:QtQuick 跨 Mac/Windows/Linux;字体栈 Qt.platform.os 自动选 SF Pro/Segoe UI。

后续:业务桥层 + 4 个 tab 业务接入 + 打包 + 切主入口(任务清单 #12-#19)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5f2a43f7
1 """按窗口标题截图。
2
3 用法:
4 python capture_window.py <title-substring> [output.png]
5
6 例:
7 python capture_window.py 珠宝壹佰 shot.png
8 python capture_window.py 登录 login.png
9
10 依赖:Pillow(项目已装),ctypes(标准库)。
11 仅 Windows。
12 """
13 import ctypes
14 import sys
15 from ctypes import wintypes
16 from pathlib import Path
17 from PIL import ImageGrab
18
19 user32 = ctypes.windll.user32
20 EnumWindowsProc = ctypes.WINFUNCTYPE(
21 ctypes.c_bool, wintypes.HWND, wintypes.LPARAM
22 )
23
24
25 def _get_window_text(hwnd):
26 length = user32.GetWindowTextLengthW(hwnd)
27 if length == 0:
28 return ""
29 buf = ctypes.create_unicode_buffer(length + 1)
30 user32.GetWindowTextW(hwnd, buf, length + 1)
31 return buf.value
32
33
34 _PROCESS_NAME_CACHE = {}
35
36
37 def _get_process_name(pid: int) -> str:
38 """通过 pid 拿到进程可执行文件名(小写,不含路径)。"""
39 if pid in _PROCESS_NAME_CACHE:
40 return _PROCESS_NAME_CACHE[pid]
41 PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
42 kernel32 = ctypes.windll.kernel32
43 psapi = ctypes.windll.psapi
44 h = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
45 if not h:
46 _PROCESS_NAME_CACHE[pid] = ""
47 return ""
48 try:
49 buf = ctypes.create_unicode_buffer(260)
50 size = wintypes.DWORD(260)
51 if kernel32.QueryFullProcessImageNameW(h, 0, buf, ctypes.byref(size)):
52 name = buf.value.split("\\")[-1].lower()
53 else:
54 name = ""
55 finally:
56 kernel32.CloseHandle(h)
57 _PROCESS_NAME_CACHE[pid] = name
58 return name
59
60
61 def find_window(title_substring: str, exe_filter: tuple = ("pythonw.exe", "python.exe", "zb100imagegenerator.exe")):
62 """返回首个标题包含 title_substring 且属于 exe_filter 进程的可见窗口 hwnd。"""
63 found = []
64
65 def callback(hwnd, lparam):
66 if not user32.IsWindowVisible(hwnd):
67 return True
68 text = _get_window_text(hwnd)
69 if title_substring not in text:
70 return True
71 # 检查进程
72 pid = wintypes.DWORD()
73 user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
74 name = _get_process_name(pid.value)
75 if name in exe_filter:
76 found.append((hwnd, text))
77 return False # 停止枚举
78 return True
79
80 user32.EnumWindows(EnumWindowsProc(callback), 0)
81 return found[0] if found else None
82
83
84 def get_window_bbox(hwnd):
85 """获取窗口外接矩形(含标题栏 / 边框)。"""
86 rect = wintypes.RECT()
87 # DWMWA_EXTENDED_FRAME_BOUNDS = 9,能避开 Win10/11 的不可见 shadow border
88 DWMWA_EXTENDED_FRAME_BOUNDS = 9
89 dwmapi = ctypes.windll.dwmapi
90 res = dwmapi.DwmGetWindowAttribute(
91 wintypes.HWND(hwnd),
92 ctypes.c_uint(DWMWA_EXTENDED_FRAME_BOUNDS),
93 ctypes.byref(rect),
94 ctypes.sizeof(rect),
95 )
96 if res != 0:
97 # 回退到 GetWindowRect
98 user32.GetWindowRect(hwnd, ctypes.byref(rect))
99 return (rect.left, rect.top, rect.right, rect.bottom)
100
101
102 def main():
103 if len(sys.argv) < 2:
104 print(__doc__)
105 sys.exit(1)
106
107 title = sys.argv[1]
108 output = Path(sys.argv[2] if len(sys.argv) > 2 else "shot.png").resolve()
109
110 hit = find_window(title)
111 if not hit:
112 print(f"找不到标题包含 '{title}' 的窗口", file=sys.stderr)
113 sys.exit(2)
114 hwnd, full_title = hit
115
116 # 用 PrintWindow 直接从窗口拿位图,不需要把窗口提前(避免打断用户)
117 bbox = get_window_bbox(hwnd)
118 width = bbox[2] - bbox[0]
119 height = bbox[3] - bbox[1]
120
121 img = _print_window_to_pil(hwnd, width, height)
122 if img is None:
123 # 回退:把窗口提前再截屏
124 import time
125 user32.ShowWindow(hwnd, 9) # SW_RESTORE
126 user32.SetForegroundWindow(hwnd)
127 # 顶置-取消顶置技巧绕开 Windows 前台限制
128 HWND_TOPMOST = -1
129 HWND_NOTOPMOST = -2
130 SWP_NOMOVE = 0x0002
131 SWP_NOSIZE = 0x0001
132 user32.SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE)
133 user32.SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE)
134 time.sleep(0.5)
135 img = ImageGrab.grab(bbox=bbox, all_screens=True)
136
137 img.save(output)
138 print(f"OK hwnd={hwnd} title={full_title!r} size={width}x{height} -> {output}")
139
140
141 def _print_window_to_pil(hwnd, width, height):
142 """用 PrintWindow API 从窗口直接拿位图(不依赖窗口在前台)。"""
143 from PIL import Image
144 gdi32 = ctypes.windll.gdi32
145
146 hdcWindow = user32.GetDC(hwnd)
147 hdcMem = gdi32.CreateCompatibleDC(hdcWindow)
148 hbm = gdi32.CreateCompatibleBitmap(hdcWindow, width, height)
149 gdi32.SelectObject(hdcMem, hbm)
150
151 PW_RENDERFULLCONTENT = 0x00000002
152 ok = user32.PrintWindow(hwnd, hdcMem, PW_RENDERFULLCONTENT)
153
154 if ok:
155 # 提取位图数据
156 class BITMAPINFOHEADER(ctypes.Structure):
157 _fields_ = [
158 ("biSize", wintypes.DWORD),
159 ("biWidth", wintypes.LONG),
160 ("biHeight", wintypes.LONG),
161 ("biPlanes", wintypes.WORD),
162 ("biBitCount", wintypes.WORD),
163 ("biCompression", wintypes.DWORD),
164 ("biSizeImage", wintypes.DWORD),
165 ("biXPelsPerMeter", wintypes.LONG),
166 ("biYPelsPerMeter", wintypes.LONG),
167 ("biClrUsed", wintypes.DWORD),
168 ("biClrImportant", wintypes.DWORD),
169 ]
170
171 class BITMAPINFO(ctypes.Structure):
172 _fields_ = [("bmiHeader", BITMAPINFOHEADER), ("bmiColors", wintypes.DWORD * 3)]
173
174 bmi = BITMAPINFO()
175 bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
176 bmi.bmiHeader.biWidth = width
177 bmi.bmiHeader.biHeight = -height # top-down
178 bmi.bmiHeader.biPlanes = 1
179 bmi.bmiHeader.biBitCount = 32
180 bmi.bmiHeader.biCompression = 0 # BI_RGB
181
182 buf_len = width * height * 4
183 buf = (ctypes.c_ubyte * buf_len)()
184 gdi32.GetDIBits(hdcMem, hbm, 0, height, buf, ctypes.byref(bmi), 0)
185 img = Image.frombuffer("RGBA", (width, height), bytes(buf), "raw", "BGRA", 0, 1).convert("RGB")
186 else:
187 img = None
188
189 gdi32.DeleteObject(hbm)
190 gdi32.DeleteDC(hdcMem)
191 user32.ReleaseDC(hwnd, hdcWindow)
192
193 return img
194
195
196 if __name__ == "__main__":
197 main()
1 """QML PoC 入口 — 验证 QtQuick + Apple 风格自定义视觉。
2
3 只做 UI 演示:
4 - 登录页 (LoginScreen.qml)
5 - 主窗口 (MainWindow.qml) 含 3 tab + 任务队列 sidebar
6 - 图片生成 tab 的核心 UI 元素
7
8 不接业务后端(不真的生成图片 / 调 db),让用户先看视觉。
9 跑:.venv/Scripts/pythonw.exe qml_poc/main_qml.py
10 """
11 import sys
12 from pathlib import Path
13
14 from PySide6.QtCore import QObject, QUrl, Property, Signal, Slot
15 from PySide6.QtGui import QGuiApplication
16 from PySide6.QtQml import QQmlApplicationEngine
17 from PySide6.QtQuickControls2 import QQuickStyle
18
19
20 class AppState(QObject):
21 """暴露给 QML 的全局状态。最小骨架,业务后续再接。"""
22 loggedInChanged = Signal()
23 currentTabChanged = Signal()
24
25 def __init__(self):
26 super().__init__()
27 self._logged_in = False
28 self._current_tab = 0
29
30 @Property(bool, notify=loggedInChanged)
31 def loggedIn(self):
32 return self._logged_in
33
34 @Property(int, notify=currentTabChanged)
35 def currentTab(self):
36 return self._current_tab
37
38 @Slot(str, str, result=bool)
39 def login(self, username: str, password: str) -> bool:
40 # PoC:随便接受,业务不接
41 if not username or not password:
42 return False
43 self._logged_in = True
44 self.loggedInChanged.emit()
45 return True
46
47 @Slot()
48 def logout(self):
49 self._logged_in = False
50 self.loggedInChanged.emit()
51
52 @Slot(int)
53 def setTab(self, idx: int):
54 self._current_tab = idx
55 self.currentTabChanged.emit()
56
57
58 def main():
59 QQuickStyle.setStyle("Basic") # 纯净底,不引入 Material/Win11 的视觉污染
60 app = QGuiApplication(sys.argv)
61 app.setApplicationName("珠宝壹佰图像生成器")
62 app.setOrganizationName("ZB100")
63
64 state = AppState()
65 engine = QQmlApplicationEngine()
66 engine.rootContext().setContextProperty("appState", state)
67
68 qml_dir = Path(__file__).parent / "qml"
69 engine.addImportPath(str(qml_dir))
70 engine.load(QUrl.fromLocalFile(str(qml_dir / "Main.qml")))
71
72 if not engine.rootObjects():
73 print("QML load failed", file=sys.stderr)
74 sys.exit(1)
75
76 sys.exit(app.exec())
77
78
79 if __name__ == "__main__":
80 main()
1 import QtQuick
2 import QtQuick.Controls.Basic
3 import QtQuick.Layouts
4 import "components"
5 import "." as App
6
7 Item {
8 ColumnLayout {
9 anchors.fill: parent
10 anchors.margins: App.Theme.space5
11 spacing: App.Theme.space4
12
13 // ===== 参考图片卡片 =====
14 Card {
15 Layout.fillWidth: true
16 implicitHeight: 240
17
18 ColumnLayout {
19 anchors.fill: parent
20 anchors.margins: App.Theme.space4
21 spacing: App.Theme.space3
22
23 RowLayout {
24 spacing: App.Theme.space3
25 CaptionLabel {
26 text: "参考图片"
27 font.pointSize: App.Theme.fontLg
28 }
29 Item { Layout.fillWidth: true }
30 }
31
32 RowLayout {
33 spacing: App.Theme.space2
34 SecondaryButton { text: "添加图片" }
35 SecondaryButton { text: "📋 粘贴图片" }
36 Label {
37 text: "已选择 0 张"
38 font.family: App.Theme.fontFamily
39 font.pointSize: App.Theme.fontSm
40 color: App.Theme.textSecondary
41 }
42 Label {
43 text: "💡 拖拽或粘贴图片到下方区域"
44 font.family: App.Theme.fontFamily
45 font.pointSize: App.Theme.fontSm
46 color: App.Theme.textTertiary
47 }
48 Item { Layout.fillWidth: true }
49 }
50
51 Rectangle {
52 Layout.fillWidth: true
53 Layout.fillHeight: true
54 color: App.Theme.bgSubtle
55 radius: App.Theme.radiusMd
56 border.width: 2
57 border.color: App.Theme.borderDefault
58
59 Text {
60 anchors.centerIn: parent
61 text: "拖拽图片到这里"
62 color: App.Theme.textTertiary
63 font.family: App.Theme.fontFamily
64 font.pointSize: App.Theme.fontSm
65 }
66 }
67 }
68 }
69
70 // ===== 提示词 + 生成设置(并排)=====
71 RowLayout {
72 spacing: App.Theme.space4
73 Layout.fillWidth: true
74 Layout.preferredHeight: 360
75 Layout.minimumHeight: 340
76
77 // 提示词
78 Card {
79 Layout.fillWidth: true
80 Layout.preferredWidth: 600
81 Layout.fillHeight: true
82
83 ColumnLayout {
84 anchors.fill: parent
85 anchors.margins: App.Theme.space4
86 spacing: App.Theme.space3
87
88 CaptionLabel {
89 text: "提示词"
90 font.pointSize: App.Theme.fontLg
91 }
92
93 RowLayout {
94 spacing: App.Theme.space2
95 SecondaryButton { text: "⭐ 收藏" }
96 Label {
97 text: "快速选择:"
98 font.family: App.Theme.fontFamily
99 font.pointSize: App.Theme.fontSm
100 color: App.Theme.textSecondary
101 }
102 ThemedComboBox {
103 Layout.fillWidth: true
104 model: ["主石换成闪耀的祖母绿", "改成玫瑰金材质", "增加更多碎钻"]
105 }
106 SecondaryButton { text: "删除" }
107 }
108
109 ScrollView {
110 Layout.fillWidth: true
111 Layout.fillHeight: true
112 TextArea {
113 text: "一幅美丽的风景画,有山有湖,日落时分"
114 font.family: App.Theme.fontFamily
115 font.pointSize: App.Theme.fontBase
116 color: App.Theme.textPrimary
117 wrapMode: TextArea.Wrap
118 background: Rectangle {
119 color: App.Theme.bgSubtle
120 radius: App.Theme.radiusMd
121 border.width: 1
122 border.color: App.Theme.borderDefault
123 }
124 }
125 }
126 }
127 }
128
129 // 生成设置
130 Card {
131 Layout.preferredWidth: 280
132 Layout.fillHeight: true
133
134 ColumnLayout {
135 anchors.fill: parent
136 anchors.margins: App.Theme.space4
137 spacing: App.Theme.space3
138
139 CaptionLabel {
140 text: "生成设置"
141 font.pointSize: App.Theme.fontLg
142 }
143
144 Repeater {
145 model: [
146 { caption: "生成模式", options: ["极速模式", "慢速模式"] },
147 { caption: "宽高比", options: ["1:1", "2:3", "3:2", "16:9", "9:16", "4:3", "3:4"] },
148 { caption: "图片尺寸", options: ["1K", "2K", "4K"] }
149 ]
150 delegate: ColumnLayout {
151 spacing: 4
152 Layout.fillWidth: true
153 CaptionLabel { text: modelData.caption; font.pointSize: App.Theme.fontBase }
154 ThemedComboBox {
155 Layout.fillWidth: true
156 model: modelData.options
157 }
158 }
159 }
160
161 Item { Layout.fillHeight: true }
162 }
163 }
164 }
165
166 // ===== 操作按钮行 =====
167 RowLayout {
168 spacing: App.Theme.space3
169 Layout.fillWidth: true
170
171 PrimaryButton {
172 text: "生成图片"
173 }
174 SecondaryButton {
175 text: "下载图片"
176 }
177 Label {
178 text: "● 就绪"
179 font.family: App.Theme.fontFamily
180 font.pointSize: App.Theme.fontSm
181 color: App.Theme.success
182 }
183 Item { Layout.fillWidth: true }
184 }
185
186 // ===== 预览卡片 =====
187 Card {
188 Layout.fillWidth: true
189 Layout.fillHeight: true
190 Layout.minimumHeight: 200
191
192 ColumnLayout {
193 anchors.fill: parent
194 anchors.margins: App.Theme.space4
195 spacing: App.Theme.space3
196
197 CaptionLabel {
198 text: "预览"
199 font.pointSize: App.Theme.fontLg
200 }
201
202 Rectangle {
203 Layout.fillWidth: true
204 Layout.fillHeight: true
205 color: App.Theme.bgSubtle
206 radius: App.Theme.radiusMd
207
208 Column {
209 anchors.centerIn: parent
210 spacing: 4
211
212 Text {
213 anchors.horizontalCenter: parent.horizontalCenter
214 text: "生成的图片将在这里显示"
215 color: App.Theme.textTertiary
216 font.family: App.Theme.fontFamily
217 font.pointSize: App.Theme.fontSm
218 }
219 Text {
220 anchors.horizontalCenter: parent.horizontalCenter
221 text: "双击用系统查看器打开"
222 color: App.Theme.textTertiary
223 font.family: App.Theme.fontFamily
224 font.pointSize: App.Theme.fontXs
225 }
226 }
227 }
228 }
229 }
230 }
231 }
1 import QtQuick
2 import QtQuick.Controls.Basic
3 import QtQuick.Layouts
4 import "components"
5 import "." as App
6
7 Rectangle {
8 id: root
9 color: App.Theme.bgCanvas
10 focus: true
11
12 Keys.onReturnPressed: appState.login(usernameField.text, passwordField.text || "demo")
13 Keys.onEnterPressed: appState.login(usernameField.text, passwordField.text || "demo")
14
15 ColumnLayout {
16 anchors.centerIn: parent
17 width: 360
18 spacing: 0
19
20 // 标题
21 Label {
22 text: "登录"
23 font.family: App.Theme.fontFamily
24 font.pointSize: App.Theme.fontXxl
25 font.weight: Font.Bold
26 color: App.Theme.textPrimary
27 Layout.alignment: Qt.AlignHCenter
28 Layout.bottomMargin: 4
29 }
30
31 Label {
32 text: "珠宝壹佰图像生成器"
33 font.family: App.Theme.fontFamily
34 font.pointSize: App.Theme.fontSm
35 color: App.Theme.textSecondary
36 Layout.alignment: Qt.AlignHCenter
37 Layout.bottomMargin: App.Theme.space5
38 }
39
40 // 用户名
41 CaptionLabel {
42 text: "用户名"
43 Layout.bottomMargin: App.Theme.space2
44 }
45 ThemedTextField {
46 id: usernameField
47 text: "chaijin"
48 Layout.fillWidth: true
49 Layout.bottomMargin: App.Theme.space4
50 }
51
52 // 密码
53 CaptionLabel {
54 text: "密码"
55 Layout.bottomMargin: App.Theme.space2
56 }
57 ThemedTextField {
58 id: passwordField
59 echoMode: TextInput.Password
60 placeholderText: "••••••••"
61 Layout.fillWidth: true
62 Layout.bottomMargin: App.Theme.space2
63 }
64
65 // 复选行
66 RowLayout {
67 spacing: App.Theme.space4
68 Layout.fillWidth: true
69 Layout.bottomMargin: App.Theme.space5
70
71 CheckBox {
72 text: "记住用户名"
73 checked: true
74 font.family: App.Theme.fontFamily
75 font.pointSize: App.Theme.fontSm
76 contentItem: Text {
77 text: parent.text
78 font: parent.font
79 color: App.Theme.textPrimary
80 leftPadding: parent.indicator.width + 6
81 verticalAlignment: Text.AlignVCenter
82 }
83 indicator: Rectangle {
84 implicitWidth: 18
85 implicitHeight: 18
86 radius: 4
87 border.width: 1
88 border.color: parent.checked ? App.Theme.accent : App.Theme.borderStrong
89 color: parent.checked ? App.Theme.accent : App.Theme.bgSurface
90
91 Text {
92 anchors.centerIn: parent
93 text: "✓"
94 color: "white"
95 font.pixelSize: 12
96 font.weight: Font.Bold
97 visible: parent.parent.checked
98 }
99 }
100 }
101
102 CheckBox {
103 text: "记住密码"
104 checked: true
105 font.family: App.Theme.fontFamily
106 font.pointSize: App.Theme.fontSm
107 contentItem: Text {
108 text: parent.text
109 font: parent.font
110 color: App.Theme.textPrimary
111 leftPadding: parent.indicator.width + 6
112 verticalAlignment: Text.AlignVCenter
113 }
114 indicator: Rectangle {
115 implicitWidth: 18
116 implicitHeight: 18
117 radius: 4
118 border.width: 1
119 border.color: parent.checked ? App.Theme.accent : App.Theme.borderStrong
120 color: parent.checked ? App.Theme.accent : App.Theme.bgSurface
121
122 Text {
123 anchors.centerIn: parent
124 text: "✓"
125 color: "white"
126 font.pixelSize: 12
127 font.weight: Font.Bold
128 visible: parent.parent.checked
129 }
130 }
131 }
132
133 Item { Layout.fillWidth: true }
134 }
135
136 // 登录按钮
137 PrimaryButton {
138 text: "登录"
139 Layout.fillWidth: true
140 onClicked: appState.login(usernameField.text, passwordField.text || "demo")
141 }
142
143 Label {
144 id: hint
145 text: ""
146 font.family: App.Theme.fontFamily
147 font.pointSize: App.Theme.fontXs
148 color: App.Theme.textTertiary
149 Layout.alignment: Qt.AlignHCenter
150 Layout.topMargin: App.Theme.space3
151 }
152 }
153 }
1 import QtQuick
2 import QtQuick.Controls.Basic
3 import QtQuick.Layouts
4 import QtQuick.Window
5 import "." as App
6
7 ApplicationWindow {
8 id: window
9 visible: true
10 width: 1280
11 height: 880
12 minimumWidth: 1100
13 minimumHeight: 760
14 title: appState.loggedIn
15 ? "珠宝壹佰图像生成器"
16 : "登录 - 珠宝壹佰图像生成器"
17 color: App.Theme.bgCanvas
18
19 // 登录前 400x460 小窗,登录后 1280x880 主窗口
20 Component.onCompleted: updateGeometry()
21
22 Connections {
23 target: appState
24 function onLoggedInChanged() {
25 updateGeometry()
26 }
27 }
28
29 function updateGeometry() {
30 if (appState.loggedIn) {
31 window.minimumWidth = 1100
32 window.minimumHeight = 760
33 window.width = 1280
34 window.height = 880
35 } else {
36 window.minimumWidth = 360
37 window.minimumHeight = 420
38 window.width = 400
39 window.height = 480
40 }
41 }
42
43 StackLayout {
44 anchors.fill: parent
45 currentIndex: appState.loggedIn ? 1 : 0
46
47 LoginScreen {}
48 MainWindow {}
49 }
50 }
1 import QtQuick
2 import QtQuick.Controls.Basic
3 import QtQuick.Layouts
4 import "components"
5 import "." as App
6
7 Rectangle {
8 id: root
9 color: App.Theme.bgCanvas
10
11 RowLayout {
12 anchors.fill: parent
13 spacing: 0
14
15 // ===== 主内容区 =====
16 ColumnLayout {
17 Layout.fillWidth: true
18 Layout.fillHeight: true
19 spacing: 0
20
21 // Tab Bar
22 Rectangle {
23 Layout.fillWidth: true
24 implicitHeight: 48
25 color: App.Theme.bgCanvas
26
27 Row {
28 anchors.left: parent.left
29 anchors.leftMargin: App.Theme.space5
30 anchors.bottom: parent.bottom
31 spacing: App.Theme.space2
32
33 Repeater {
34 model: ["图片生成", "款式设计", "历史记录"]
35 delegate: Rectangle {
36 width: tabText.implicitWidth + App.Theme.space5 * 2
37 height: 40
38 color: "transparent"
39
40 property bool isActive: index === appState.currentTab
41
42 Text {
43 id: tabText
44 anchors.centerIn: parent
45 text: modelData
46 font.family: App.Theme.fontFamily
47 font.pointSize: App.Theme.fontBase
48 font.weight: parent.isActive ? Font.DemiBold : Font.Normal
49 color: parent.isActive
50 ? App.Theme.accent
51 : (mouseArea.containsMouse
52 ? App.Theme.textPrimary
53 : App.Theme.textSecondary)
54 }
55
56 // 激活态下划线
57 Rectangle {
58 anchors.bottom: parent.bottom
59 anchors.left: parent.left
60 anchors.right: parent.right
61 anchors.leftMargin: App.Theme.space4
62 anchors.rightMargin: App.Theme.space4
63 height: 2
64 radius: 1
65 color: App.Theme.accent
66 visible: parent.isActive
67 }
68
69 MouseArea {
70 id: mouseArea
71 anchors.fill: parent
72 hoverEnabled: true
73 cursorShape: Qt.PointingHandCursor
74 onClicked: appState.setTab(index)
75 }
76 }
77 }
78 }
79
80 // 底部分隔线
81 Rectangle {
82 anchors.left: parent.left
83 anchors.right: parent.right
84 anchors.bottom: parent.bottom
85 height: 1
86 color: App.Theme.divider
87 }
88 }
89
90 // Tab Content
91 StackLayout {
92 Layout.fillWidth: true
93 Layout.fillHeight: true
94 currentIndex: appState.currentTab
95
96 ImageGenTab {}
97
98 // 款式设计 — 占位
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
110 // 历史记录 — 占位
111 Item {
112 Text {
113 anchors.centerIn: parent
114 text: "历史记录 tab 内容\n(QML PoC 暂未实现)"
115 color: App.Theme.textTertiary
116 font.family: App.Theme.fontFamily
117 font.pointSize: App.Theme.fontLg
118 horizontalAlignment: Text.AlignHCenter
119 }
120 }
121 }
122 }
123
124 // ===== 任务队列 sidebar =====
125 Rectangle {
126 Layout.preferredWidth: 200
127 Layout.fillHeight: true
128 color: App.Theme.bgSurface
129
130 // 左侧分隔线
131 Rectangle {
132 anchors.left: parent.left
133 anchors.top: parent.top
134 anchors.bottom: parent.bottom
135 width: 1
136 color: App.Theme.divider
137 }
138
139 ColumnLayout {
140 anchors.fill: parent
141 spacing: 0
142
143 // Sidebar header(与主区 tab bar 等高 48px)
144 Rectangle {
145 Layout.fillWidth: true
146 implicitHeight: 48
147 color: App.Theme.bgSurface
148
149 Label {
150 anchors.left: parent.left
151 anchors.leftMargin: App.Theme.space4
152 anchors.verticalCenter: parent.verticalCenter
153 text: "任务队列"
154 font.family: App.Theme.fontFamily
155 font.pointSize: App.Theme.fontBase
156 font.weight: Font.DemiBold
157 color: App.Theme.textPrimary
158 }
159
160 // 底部分隔线
161 Rectangle {
162 anchors.left: parent.left
163 anchors.right: parent.right
164 anchors.bottom: parent.bottom
165 height: 1
166 color: App.Theme.divider
167 }
168 }
169
170 // 列表
171 ListView {
172 Layout.fillWidth: true
173 Layout.fillHeight: true
174 Layout.margins: App.Theme.space2
175 spacing: 4
176 clip: true
177
178 model: ListModel {
179 ListElement { status: "执行中"; color: "#ff9500" }
180 ListElement { status: "等待中"; color: "#0071e3" }
181 ListElement { status: "已完成"; color: "#34c759" }
182 }
183
184 delegate: Rectangle {
185 width: ListView.view.width
186 height: 36
187 radius: App.Theme.radiusSm
188 color: itemMouse.containsMouse ? App.Theme.bgHover : "transparent"
189
190 Text {
191 anchors.left: parent.left
192 anchors.leftMargin: App.Theme.space3
193 anchors.verticalCenter: parent.verticalCenter
194 text: status
195 color: model.color
196 font.family: App.Theme.fontFamily
197 font.pointSize: App.Theme.fontSm
198 }
199
200 MouseArea {
201 id: itemMouse
202 anchors.fill: parent
203 hoverEnabled: true
204 cursorShape: Qt.PointingHandCursor
205 }
206 }
207 }
208 }
209 }
210 }
211 }
1 pragma Singleton
2 import QtQuick
3
4 QtObject {
5 // 颜色 — Apple 高级浅色
6 readonly property color bgCanvas: "#fbfbfd"
7 readonly property color bgSurface: "#ffffff"
8 readonly property color bgElevated: "#ffffff"
9 readonly property color bgSubtle: "#f4f4f7"
10 readonly property color bgHover: "#f0f0f3"
11
12 readonly property color textPrimary: "#1d1d1f"
13 readonly property color textSecondary: "#6e6e73"
14 readonly property color textTertiary: "#86868b"
15 readonly property color textOnAccent: "#ffffff"
16
17 readonly property color borderDefault: Qt.rgba(0, 0, 0, 0.10)
18 readonly property color borderStrong: Qt.rgba(0, 0, 0, 0.18)
19 readonly property color divider: Qt.rgba(0, 0, 0, 0.06)
20
21 readonly property color accent: "#0071e3"
22 readonly property color accentHover: "#0077ed"
23 readonly property color accentPressed: "#0062c4"
24 readonly property color accentSubtle: Qt.rgba(0, 0.443, 0.89, 0.10)
25
26 readonly property color success: "#34c759"
27 readonly property color warning: "#ff9500"
28 readonly property color danger: "#ff3b30"
29
30 // 尺寸
31 readonly property int radiusSm: 4
32 readonly property int radiusMd: 8
33 readonly property int radiusLg: 12
34 readonly property int radiusPill: 980
35
36 readonly property int space1: 4
37 readonly property int space2: 8
38 readonly property int space3: 12
39 readonly property int space4: 16
40 readonly property int space5: 20
41 readonly property int space6: 24
42
43 readonly property int controlHSm: 28
44 readonly property int controlHMd: 36
45 readonly property int controlHLg: 44
46
47 // 字号(pt)
48 readonly property int fontXs: 10
49 readonly property int fontSm: 11
50 readonly property int fontBase: 12
51 readonly property int fontLg: 14
52 readonly property int fontXl: 17
53 readonly property int fontXxl: 22
54
55 // 字体族
56 readonly property string fontFamily: Qt.platform.os === "osx"
57 ? "SF Pro Text, PingFang SC"
58 : "Segoe UI Variable, Segoe UI, Microsoft YaHei UI"
59 }
1 import QtQuick
2 import QtQuick.Controls.Basic
3 import "../" as App
4
5 Label {
6 font.family: App.Theme.fontFamily
7 font.pointSize: App.Theme.fontBase
8 font.weight: Font.DemiBold
9 color: App.Theme.textPrimary
10 verticalAlignment: Text.AlignVCenter
11 }
1 import QtQuick
2 import "../" as App
3
4 // 卡片容器 - 白底 + 圆角 + 阴影。子元素放进 default property 即可。
5 Rectangle {
6 id: root
7 color: App.Theme.bgSurface
8 radius: App.Theme.radiusLg
9 border.width: 1
10 border.color: App.Theme.borderDefault
11
12 // 阴影由层效果实现(Qt6 native)
13 layer.enabled: true
14 layer.smooth: true
15 }
1 import QtQuick
2 import QtQuick.Controls.Basic
3 import "../" as App
4
5 Button {
6 id: control
7 implicitHeight: App.Theme.controlHLg
8 leftPadding: 24
9 rightPadding: 24
10 font.family: App.Theme.fontFamily
11 font.pointSize: App.Theme.fontBase
12 font.weight: Font.DemiBold
13
14 contentItem: Text {
15 text: control.text
16 font: control.font
17 color: App.Theme.textOnAccent
18 horizontalAlignment: Text.AlignHCenter
19 verticalAlignment: Text.AlignVCenter
20 }
21
22 background: Rectangle {
23 radius: App.Theme.radiusPill
24 color: control.pressed
25 ? App.Theme.accentPressed
26 : (control.hovered ? App.Theme.accentHover : App.Theme.accent)
27
28 Behavior on color {
29 ColorAnimation { duration: 120; easing.type: Easing.OutCubic }
30 }
31 }
32 }
1 import QtQuick
2 import QtQuick.Controls.Basic
3 import "../" as App
4
5 Button {
6 id: control
7 implicitHeight: App.Theme.controlHMd
8 leftPadding: 18
9 rightPadding: 18
10 font.family: App.Theme.fontFamily
11 font.pointSize: App.Theme.fontSm
12
13 contentItem: Text {
14 text: control.text
15 font: control.font
16 color: App.Theme.textPrimary
17 horizontalAlignment: Text.AlignHCenter
18 verticalAlignment: Text.AlignVCenter
19 }
20
21 background: Rectangle {
22 radius: App.Theme.radiusMd
23 color: control.pressed
24 ? App.Theme.bgSubtle
25 : (control.hovered ? App.Theme.bgHover : App.Theme.bgSurface)
26 border.width: 1
27 border.color: control.hovered ? App.Theme.borderStrong : App.Theme.borderDefault
28
29 Behavior on color {
30 ColorAnimation { duration: 120; easing.type: Easing.OutCubic }
31 }
32 }
33 }
1 import QtQuick
2 import QtQuick.Controls.Basic
3 import "../" as App
4
5 ComboBox {
6 id: control
7 implicitHeight: App.Theme.controlHLg
8 font.family: App.Theme.fontFamily
9 font.pointSize: App.Theme.fontBase
10 leftPadding: 14
11 rightPadding: 32
12
13 contentItem: Text {
14 text: control.displayText
15 font: control.font
16 color: App.Theme.textPrimary
17 verticalAlignment: Text.AlignVCenter
18 elide: Text.ElideRight
19 }
20
21 background: Rectangle {
22 radius: App.Theme.radiusMd
23 color: App.Theme.bgSurface
24 border.width: control.activeFocus ? 2 : 1
25 border.color: control.activeFocus
26 ? App.Theme.accent
27 : (control.hovered ? App.Theme.borderStrong : App.Theme.borderDefault)
28
29 Behavior on border.color {
30 ColorAnimation { duration: 100 }
31 }
32 }
33
34 indicator: Canvas {
35 id: chevron
36 width: 12; height: 8
37 x: control.width - width - 14
38 y: control.topPadding + (control.availableHeight - height) / 2
39 contextType: "2d"
40 onPaint: {
41 var ctx = getContext("2d")
42 ctx.reset()
43 ctx.strokeStyle = App.Theme.textSecondary
44 ctx.lineWidth = 1.5
45 ctx.lineCap = "round"
46 ctx.lineJoin = "round"
47 ctx.beginPath()
48 ctx.moveTo(2, 2)
49 ctx.lineTo(width / 2, height - 2)
50 ctx.lineTo(width - 2, 2)
51 ctx.stroke()
52 }
53 }
54
55 popup: Popup {
56 y: control.height + 4
57 width: control.width
58 padding: 4
59
60 background: Rectangle {
61 color: App.Theme.bgElevated
62 radius: App.Theme.radiusMd
63 border.width: 1
64 border.color: App.Theme.borderDefault
65 }
66
67 contentItem: ListView {
68 clip: true
69 implicitHeight: Math.min(contentHeight, 240)
70 model: control.popup.visible ? control.delegateModel : null
71 currentIndex: control.highlightedIndex
72 ScrollBar.vertical: ScrollBar {}
73 }
74 }
75
76 delegate: ItemDelegate {
77 width: control.width
78 height: 32
79 font: control.font
80
81 contentItem: Text {
82 text: modelData
83 font: control.font
84 color: App.Theme.textPrimary
85 verticalAlignment: Text.AlignVCenter
86 leftPadding: 10
87 }
88
89 background: Rectangle {
90 radius: App.Theme.radiusSm
91 color: hovered ? App.Theme.accentSubtle : "transparent"
92 }
93 }
94 }
1 import QtQuick
2 import QtQuick.Controls.Basic
3 import "../" as App
4
5 TextField {
6 id: control
7 implicitHeight: App.Theme.controlHLg
8 leftPadding: 14
9 rightPadding: 14
10 font.family: App.Theme.fontFamily
11 font.pointSize: App.Theme.fontBase
12 color: App.Theme.textPrimary
13 placeholderTextColor: App.Theme.textTertiary
14 selectionColor: App.Theme.accent
15 selectedTextColor: App.Theme.textOnAccent
16
17 background: Rectangle {
18 radius: App.Theme.radiusMd
19 color: App.Theme.bgSurface
20 border.width: control.activeFocus ? 2 : 1
21 border.color: control.activeFocus
22 ? App.Theme.accent
23 : App.Theme.borderDefault
24
25 Behavior on border.color {
26 ColorAnimation { duration: 100 }
27 }
28 }
29 }
1 module Theme
2 singleton Theme 1.0 Theme.qml