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