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>
Showing
14 changed files
with
1197 additions
and
0 deletions
capture_window.py
0 → 100644
| 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() |
qml_poc/main_qml.py
0 → 100644
| 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() |
qml_poc/qml/ImageGenTab.qml
0 → 100644
| 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 | } |
qml_poc/qml/LoginScreen.qml
0 → 100644
| 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 | } |
qml_poc/qml/Main.qml
0 → 100644
| 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 | } |
qml_poc/qml/MainWindow.qml
0 → 100644
| 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 | } |
qml_poc/qml/Theme.qml
0 → 100644
| 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 | } |
qml_poc/qml/components/CaptionLabel.qml
0 → 100644
qml_poc/qml/components/Card.qml
0 → 100644
| 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 | } |
qml_poc/qml/components/PrimaryButton.qml
0 → 100644
| 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 | } |
qml_poc/qml/components/SecondaryButton.qml
0 → 100644
| 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 | } |
qml_poc/qml/components/ThemedComboBox.qml
0 → 100644
| 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 | } |
qml_poc/qml/components/ThemedTextField.qml
0 → 100644
| 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 | } |
qml_poc/qml/qmldir
0 → 100644
-
Please register or sign in to post a comment