nova-chat 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nova_agent/__init__.py +0 -0
- nova_agent/core/__init__.py +0 -0
- nova_agent/core/chat_worker.py +230 -0
- nova_agent/core/tools.py +2 -0
- nova_agent/main.py +53 -0
- nova_agent/qml/CardDemo.qml +72 -0
- nova_agent/qml/DemoButton.qml +93 -0
- nova_agent/qml/NovaCard.qml +104 -0
- nova_agent/qml/NovaCardDemo.qml +57 -0
- nova_agent/qml/__init__.py +0 -0
- nova_agent/qml/demo_button.py +59 -0
- nova_agent/qml/demo_nova_card.py +38 -0
- nova_agent/qml/demo_nova_card.qml +115 -0
- nova_agent/qml/main.py +30 -0
- nova_agent/qml_demo.py +29 -0
- nova_agent/ui/NovaButton.qml +121 -0
- nova_agent/ui/NovaChatMessage.qml +101 -0
- nova_agent/ui/__init__.py +0 -0
- nova_agent/ui/main_window.py +376 -0
- nova_agent/ui/qml_chat_view.qml +117 -0
- nova_agent/ui/qml_main.py +157 -0
- nova_agent/utils/__init__.py +0 -0
- nova_agent/utils/logger.py +48 -0
- nova_agent/utils/timer.py +33 -0
- nova_agent/utils/window_utils.py +76 -0
- nova_agent/version.py +17 -0
- nova_chat-0.1.0.dist-info/METADATA +26 -0
- nova_chat-0.1.0.dist-info/RECORD +30 -0
- nova_chat-0.1.0.dist-info/WHEEL +5 -0
- nova_chat-0.1.0.dist-info/top_level.txt +1 -0
nova_agent/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# src/ai_chat/core/chat_worker.py
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess # ✅ 新增:用于执行 Ruff 命令行
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import QThread, Signal
|
|
8
|
+
from openai import OpenAI
|
|
9
|
+
|
|
10
|
+
# =====================================================================
|
|
11
|
+
# 📜 智能体内置《架构宪法》(永久记忆)
|
|
12
|
+
# =====================================================================
|
|
13
|
+
SYSTEM_PROMPT = """
|
|
14
|
+
你是 PySide-Quasar 和 PySide-Nova 框架的首席 AI 架构师。
|
|
15
|
+
在协助开发时,必须遵守以下规范:
|
|
16
|
+
1. QPainter 规范(Quasar):纯自绘,禁止 QSS。继承 QBaseWidget,使用 setWidthF。
|
|
17
|
+
2. QML 规范(Nova):纯 QML 声明式 UI,Python 负责逻辑,利用 Property 进行双向绑定。
|
|
18
|
+
3. ⚠️ QML 特殊禁忌:严禁对分组属性(如 border.color)使用 Behavior,必须改用 states + transitions 或独立图层!严禁直接用 parent.width 计算 delegate 宽度,防止死循环!
|
|
19
|
+
4. 动画要求:PySide6 动画必须用 Property(float),严禁使用 @property。
|
|
20
|
+
5. 代码必须包含完整的 Type Hints。
|
|
21
|
+
|
|
22
|
+
工作流程:
|
|
23
|
+
1. 使用 `write_multiple_files` 生成或修改代码。
|
|
24
|
+
2. 生成后,务必调用 `run_ruff` 检查 Python 代码语法(如果是 QML 则跳过)。
|
|
25
|
+
3. 如果 Ruff 报错(除 N802/E501 之外),请修复并重写。
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ChatWorker(QThread):
|
|
30
|
+
chunk_received = Signal(str)
|
|
31
|
+
action_log = Signal(str)
|
|
32
|
+
finished_signal = Signal()
|
|
33
|
+
error_signal = Signal(str)
|
|
34
|
+
|
|
35
|
+
def __init__(self, api_key, base_url, model, messages, project_root):
|
|
36
|
+
super().__init__()
|
|
37
|
+
self.api_key = api_key
|
|
38
|
+
self.base_url = base_url
|
|
39
|
+
self.model = model
|
|
40
|
+
self.messages = messages
|
|
41
|
+
self.project_root = project_root
|
|
42
|
+
self._is_running = True
|
|
43
|
+
|
|
44
|
+
# ✅ 定义 AI 可以使用的工具(新增了 run_ruff)
|
|
45
|
+
self.tools = [
|
|
46
|
+
{
|
|
47
|
+
"type": "function",
|
|
48
|
+
"function": {
|
|
49
|
+
"name": "read_file",
|
|
50
|
+
"description": "读取项目中的文件内容。在修改代码或参考现有组件风格时非常有用。",
|
|
51
|
+
"parameters": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"properties": {
|
|
54
|
+
"file_path": {"type": "string",
|
|
55
|
+
"description": "相对路径,如 src/pyside_quasar/.../button.py"}
|
|
56
|
+
},
|
|
57
|
+
"required": ["file_path"]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"type": "function",
|
|
63
|
+
"function": {
|
|
64
|
+
"name": "write_multiple_files",
|
|
65
|
+
"description": "将生成的代码同时写入到多个指定的项目文件中。当需要创建组件及其演示文件,或者 QML+Python 组合时,使用此工具。",
|
|
66
|
+
"parameters": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"properties": {
|
|
69
|
+
"files": {
|
|
70
|
+
"type": "array",
|
|
71
|
+
"description": "要写入的文件列表",
|
|
72
|
+
"items": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"properties": {
|
|
75
|
+
"file_path": {"type": "string", "description": "相对路径,如 src/xxx.py"},
|
|
76
|
+
"code": {"type": "string", "description": "完整的代码内容"}
|
|
77
|
+
},
|
|
78
|
+
"required": ["file_path", "code"]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"required": ["files"]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"type": "function",
|
|
88
|
+
"function": {
|
|
89
|
+
"name": "run_ruff",
|
|
90
|
+
"description": "运行 ruff check --fix 检查代码语法和格式。如果报错,你需要根据报错修改代码。",
|
|
91
|
+
"parameters": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"properties": {
|
|
94
|
+
"file_path": {"type": "string", "description": "需要检查的文件相对路径"}
|
|
95
|
+
},
|
|
96
|
+
"required": ["file_path"]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
def stop(self):
|
|
103
|
+
self._is_running = False
|
|
104
|
+
|
|
105
|
+
def _read_file(self, file_path):
|
|
106
|
+
full_path = os.path.join(self.project_root, file_path)
|
|
107
|
+
if not os.path.exists(full_path):
|
|
108
|
+
return f"错误: 文件 {file_path} 不存在。"
|
|
109
|
+
try:
|
|
110
|
+
with open(full_path, "r", encoding="utf-8") as f:
|
|
111
|
+
return f.read()
|
|
112
|
+
except Exception as e:
|
|
113
|
+
return f"读取失败: {str(e)}"
|
|
114
|
+
|
|
115
|
+
def _write_files(self, files_list):
|
|
116
|
+
results = []
|
|
117
|
+
for item in files_list:
|
|
118
|
+
file_path = item.get("file_path", "")
|
|
119
|
+
code = item.get("code", "")
|
|
120
|
+
if not file_path or not code:
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
full_path = os.path.join(self.project_root, file_path)
|
|
124
|
+
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
125
|
+
|
|
126
|
+
# 防覆盖机制
|
|
127
|
+
if os.path.exists(full_path):
|
|
128
|
+
bak_path = full_path + ".bak"
|
|
129
|
+
shutil.copy2(full_path, bak_path)
|
|
130
|
+
self.action_log.emit(f"⚠️ 检测到旧文件,已备份至 {file_path}.bak")
|
|
131
|
+
|
|
132
|
+
with open(full_path, "w", encoding="utf-8") as f:
|
|
133
|
+
f.write(code)
|
|
134
|
+
results.append(file_path)
|
|
135
|
+
|
|
136
|
+
return f"成功批量写入 {len(results)} 个文件: {', '.join(results)}"
|
|
137
|
+
|
|
138
|
+
# ✅ 新增:运行 Ruff 检查的核心函数
|
|
139
|
+
def _run_ruff(self, file_path):
|
|
140
|
+
if file_path.endswith(".qml") or file_path.endswith(".qmldir"):
|
|
141
|
+
return "跳过 Ruff 检查:QML 文件无需 Python 的 Ruff 检查,请使用 QML 引擎直接验证。"
|
|
142
|
+
|
|
143
|
+
full_path = os.path.join(self.project_root, file_path)
|
|
144
|
+
if not os.path.exists(full_path):
|
|
145
|
+
return f"错误: 文件 {file_path} 不存在,无法检查。"
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
# 调用系统中的 ruff,加上 --fix 自动修复格式问题
|
|
149
|
+
result = subprocess.run(
|
|
150
|
+
["ruff", "check", "--fix", full_path],
|
|
151
|
+
capture_output=True, text=True, encoding="utf-8"
|
|
152
|
+
)
|
|
153
|
+
output = result.stdout + result.stderr
|
|
154
|
+
if not output.strip():
|
|
155
|
+
return "Ruff 检查通过,没有发现任何语法问题。"
|
|
156
|
+
return f"Ruff 发现以下问题(能自动修复的已修复),请修复剩余的:\n{output}"
|
|
157
|
+
except FileNotFoundError:
|
|
158
|
+
return "错误: 系统中找不到 ruff 命令,请先在终端 pip install ruff。"
|
|
159
|
+
except Exception as e:
|
|
160
|
+
return f"执行 Ruff 时发生异常: {str(e)}"
|
|
161
|
+
|
|
162
|
+
def run(self):
|
|
163
|
+
try:
|
|
164
|
+
client = OpenAI(api_key=self.api_key, base_url=self.base_url)
|
|
165
|
+
|
|
166
|
+
# ✅ 注入系统提示词(宪法)
|
|
167
|
+
final_messages = [{"role": "system", "content": SYSTEM_PROMPT}] + self.messages
|
|
168
|
+
|
|
169
|
+
while self._is_running:
|
|
170
|
+
response = client.chat.completions.create(
|
|
171
|
+
model=self.model,
|
|
172
|
+
messages=final_messages,
|
|
173
|
+
tools=self.tools,
|
|
174
|
+
tool_choice="auto"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
message = response.choices[0].message
|
|
178
|
+
|
|
179
|
+
assistant_msg = {"role": "assistant", "content": message.content or ""}
|
|
180
|
+
if message.tool_calls:
|
|
181
|
+
assistant_msg["tool_calls"] = [
|
|
182
|
+
{"id": tc.id, "type": tc.type,
|
|
183
|
+
"function": {"name": tc.function.name, "arguments": tc.function.arguments}}
|
|
184
|
+
for tc in message.tool_calls
|
|
185
|
+
]
|
|
186
|
+
self.messages.append(assistant_msg)
|
|
187
|
+
final_messages.append(assistant_msg)
|
|
188
|
+
|
|
189
|
+
if message.tool_calls:
|
|
190
|
+
for tool_call in message.tool_calls:
|
|
191
|
+
tool_content = "未执行"
|
|
192
|
+
try:
|
|
193
|
+
args = json.loads(tool_call.function.arguments)
|
|
194
|
+
|
|
195
|
+
if tool_call.function.name == "read_file":
|
|
196
|
+
self.action_log.emit(f"📖 正在读取: {args.get('file_path')}")
|
|
197
|
+
tool_content = self._read_file(args.get("file_path", ""))
|
|
198
|
+
|
|
199
|
+
elif tool_call.function.name == "write_multiple_files":
|
|
200
|
+
files = args.get("files", [])
|
|
201
|
+
file_names = [f.get("file_path") for f in files]
|
|
202
|
+
self.action_log.emit(f"🛠️ 准备批量写入文件: {', '.join(file_names)}")
|
|
203
|
+
tool_content = self._write_files(files)
|
|
204
|
+
|
|
205
|
+
# ✅ 新增:处理 AI 调用 Ruff 的请求
|
|
206
|
+
elif tool_call.function.name == "run_ruff":
|
|
207
|
+
path = args.get("file_path", "")
|
|
208
|
+
self.action_log.emit(f"🔍 正在用 Ruff 检查代码: {path}")
|
|
209
|
+
tool_content = self._run_ruff(path)
|
|
210
|
+
|
|
211
|
+
except json.JSONDecodeError:
|
|
212
|
+
tool_content = "错误: JSON 解析失败,请确保输出格式正确。"
|
|
213
|
+
except Exception as e:
|
|
214
|
+
tool_content = f"工具执行异常: {str(e)}"
|
|
215
|
+
|
|
216
|
+
# 闭环回复
|
|
217
|
+
tool_msg = {"role": "tool", "tool_call_id": tool_call.id, "content": tool_content}
|
|
218
|
+
self.messages.append(tool_msg)
|
|
219
|
+
final_messages.append(tool_msg)
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
if message.content:
|
|
223
|
+
self.chunk_received.emit(message.content)
|
|
224
|
+
|
|
225
|
+
self.finished_signal.emit()
|
|
226
|
+
break
|
|
227
|
+
|
|
228
|
+
except Exception as e:
|
|
229
|
+
if self._is_running:
|
|
230
|
+
self.error_signal.emit(str(e))
|
nova_agent/core/tools.py
ADDED
nova_agent/main.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# src/dev_toolbox_pro/main.py
|
|
2
|
+
import ctypes
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from PySide6.QtCore import QTranslator, QLibraryInfo
|
|
7
|
+
from PySide6.QtGui import QIcon
|
|
8
|
+
from PySide6.QtWidgets import QApplication
|
|
9
|
+
from nova_agent.ui.main_window import MainWindow
|
|
10
|
+
from nova_agent.utils.logger import setup_logger
|
|
11
|
+
from nova_agent.version import APP_USER_MODEL_ID, APP_NAME
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from resources import resources_rc # noqa
|
|
15
|
+
except ImportError:
|
|
16
|
+
print("[WARN] 未找到 resources_rc,图标可能无法显示。")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main():
|
|
20
|
+
if sys.platform == "win32":
|
|
21
|
+
try:
|
|
22
|
+
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(APP_USER_MODEL_ID)
|
|
23
|
+
except Exception as e:
|
|
24
|
+
print(f"设置 AppUserModelID 失败: {e}")
|
|
25
|
+
|
|
26
|
+
logger = setup_logger()
|
|
27
|
+
logger.info(f"{APP_NAME} starting...")
|
|
28
|
+
|
|
29
|
+
app = QApplication(sys.argv)
|
|
30
|
+
|
|
31
|
+
# 汉化逻辑
|
|
32
|
+
translator = QTranslator()
|
|
33
|
+
translations_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
|
|
34
|
+
if translator.load("qtbase_zh_CN", translations_path):
|
|
35
|
+
app.installTranslator(translator)
|
|
36
|
+
|
|
37
|
+
app.setApplicationName(APP_NAME)
|
|
38
|
+
app.setWindowIcon(QIcon(":/icons/set-up.svg"))
|
|
39
|
+
|
|
40
|
+
# 加载样式表 (因为 main.py 和 ui 同级,直接用相对路径)
|
|
41
|
+
qss_path = Path(__file__).parent / "ui" / "styles.qss"
|
|
42
|
+
if qss_path.exists():
|
|
43
|
+
app.setStyleSheet(qss_path.read_text(encoding="utf-8"))
|
|
44
|
+
else:
|
|
45
|
+
logger.warning(f"未找到样式表: {qss_path}")
|
|
46
|
+
|
|
47
|
+
window = MainWindow()
|
|
48
|
+
window.show()
|
|
49
|
+
sys.exit(app.exec())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
main()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// CardDemo.qml
|
|
2
|
+
// NovaCard 演示程序:展示圆角、阴影与 hover 放大动画
|
|
3
|
+
//
|
|
4
|
+
// 注意:卡片宽度由 Column 的 width 统一约束,卡片内部不使用 parent.width
|
|
5
|
+
// 反推自身尺寸,避免与 hover scale 形成布局死循环。
|
|
6
|
+
|
|
7
|
+
import QtQuick 2.15
|
|
8
|
+
import QtQuick.Window 2.15
|
|
9
|
+
import QtQuick.Controls 2.15
|
|
10
|
+
|
|
11
|
+
Window {
|
|
12
|
+
id: window
|
|
13
|
+
width: 900
|
|
14
|
+
height: 620
|
|
15
|
+
visible: true
|
|
16
|
+
title: "NovaCard Demo"
|
|
17
|
+
color: "#F7F8FA"
|
|
18
|
+
|
|
19
|
+
readonly property var cardModel: [
|
|
20
|
+
{ title: "基础卡片", subtitle: "圆角 + 阴影 + hover 放大" },
|
|
21
|
+
{ title: "Elevation", subtitle: "通过 elevation 调整阴影强度" },
|
|
22
|
+
{ title: "可交互", subtitle: "点击卡片会打印日志" },
|
|
23
|
+
{ title: "内容插槽", subtitle: "default property 支持自定义内容" },
|
|
24
|
+
{ title: "长标题会被省略号截断展示效果", subtitle: "ElideRight 模式" },
|
|
25
|
+
{ title: "浅色主题", subtitle: "所有颜色均可通过属性覆盖" }
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
Text {
|
|
29
|
+
id: header
|
|
30
|
+
anchors.left: parent.left
|
|
31
|
+
anchors.top: parent.top
|
|
32
|
+
anchors.margins: 28
|
|
33
|
+
text: "NovaCard"
|
|
34
|
+
font.pixelSize: 26
|
|
35
|
+
font.bold: true
|
|
36
|
+
color: "#1D2129"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
GridView {
|
|
40
|
+
id: grid
|
|
41
|
+
anchors.left: parent.left
|
|
42
|
+
anchors.right: parent.right
|
|
43
|
+
anchors.top: header.bottom
|
|
44
|
+
anchors.bottom: parent.bottom
|
|
45
|
+
anchors.margins: 28
|
|
46
|
+
anchors.topMargin: 16
|
|
47
|
+
cellWidth: (width) / 3
|
|
48
|
+
cellHeight: 200
|
|
49
|
+
clip: true
|
|
50
|
+
model: window.cardModel
|
|
51
|
+
|
|
52
|
+
delegate: Item {
|
|
53
|
+
// 使用 GridView 提供的宽度基准,而非 parent.width,
|
|
54
|
+
// 避免 delegate 与父级宽度互相依赖造成循环绑定。
|
|
55
|
+
width: grid.cellWidth
|
|
56
|
+
height: grid.cellHeight
|
|
57
|
+
|
|
58
|
+
NovaCard {
|
|
59
|
+
anchors.centerIn: parent
|
|
60
|
+
width: Math.min(parent.width - 24, 280)
|
|
61
|
+
height: 168
|
|
62
|
+
title: modelData.title
|
|
63
|
+
subtitle: modelData.subtitle
|
|
64
|
+
elevation: index % 2 === 0 ? 18 : 26
|
|
65
|
+
|
|
66
|
+
onClicked: console.log("Clicked:", modelData.title)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
ScrollBar.vertical: ScrollBar { }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import QtQuick
|
|
2
|
+
import QtQuick.Window
|
|
3
|
+
import QtQuick.Layouts
|
|
4
|
+
|
|
5
|
+
/*!
|
|
6
|
+
DemoButton.qml
|
|
7
|
+
NovaButton 的演示界面:全部 UI 声明式完成,逻辑通过 bridge 交给 Python。
|
|
8
|
+
*/
|
|
9
|
+
Window {
|
|
10
|
+
id: win
|
|
11
|
+
|
|
12
|
+
width: 520
|
|
13
|
+
height: 380
|
|
14
|
+
visible: true
|
|
15
|
+
title: "NovaButton Demo"
|
|
16
|
+
color: "#121212"
|
|
17
|
+
|
|
18
|
+
ColumnLayout {
|
|
19
|
+
anchors.fill: parent
|
|
20
|
+
anchors.margins: 28
|
|
21
|
+
spacing: 22
|
|
22
|
+
|
|
23
|
+
Text {
|
|
24
|
+
text: "NovaButton · hover 动画演示"
|
|
25
|
+
color: "#ECEFF1"
|
|
26
|
+
font.pixelSize: 20
|
|
27
|
+
font.weight: Font.DemiBold
|
|
28
|
+
Layout.fillWidth: true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Text {
|
|
32
|
+
text: "把鼠标移到按钮上,观察背景 / 边框的平滑过渡。"
|
|
33
|
+
color: "#90A4AE"
|
|
34
|
+
font.pixelSize: 13
|
|
35
|
+
Layout.fillWidth: true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
RowLayout {
|
|
39
|
+
spacing: 16
|
|
40
|
+
Layout.topMargin: 8
|
|
41
|
+
|
|
42
|
+
NovaButton {
|
|
43
|
+
text: "主要按钮"
|
|
44
|
+
variant: "primary"
|
|
45
|
+
onTapped: bridge.onPrimaryClicked()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
NovaButton {
|
|
49
|
+
text: "次要按钮"
|
|
50
|
+
variant: "secondary"
|
|
51
|
+
onTapped: bridge.onSecondaryClicked()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
NovaButton {
|
|
55
|
+
text: "危险操作"
|
|
56
|
+
variant: "danger"
|
|
57
|
+
onTapped: bridge.onDangerClicked()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
NovaButton {
|
|
62
|
+
text: "禁用态(无 hover)"
|
|
63
|
+
variant: "secondary"
|
|
64
|
+
enabled: false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Rectangle {
|
|
68
|
+
Layout.fillWidth: true
|
|
69
|
+
Layout.preferredHeight: 1
|
|
70
|
+
color: "#263238"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Text {
|
|
74
|
+
id: counter
|
|
75
|
+
text: "主要按钮点击次数: " + bridge.clickedCount
|
|
76
|
+
color: "#4CAF50"
|
|
77
|
+
font.pixelSize: 14
|
|
78
|
+
Layout.fillWidth: true
|
|
79
|
+
|
|
80
|
+
// Python 修改后自动刷新(bridge 属性变化不会自动通知,靠 Connections 手动同步)
|
|
81
|
+
property int clickedCount: 0
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
Item { Layout.fillHeight: true }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
Connections {
|
|
88
|
+
target: bridge
|
|
89
|
+
function onClickedCountChanged(count: int): void {
|
|
90
|
+
counter.clickedCount = count
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// NovaCard.qml —— Nova 框架卡片组件
|
|
2
|
+
// 纯 QML 声明式 UI:圆角 + 阴影 + hover 放大动画
|
|
3
|
+
import QtQuick
|
|
4
|
+
import QtQuick.Effects
|
|
5
|
+
|
|
6
|
+
Item {
|
|
7
|
+
id: root
|
|
8
|
+
|
|
9
|
+
// ---- 对外属性 ----
|
|
10
|
+
property alias radius: surface.radius
|
|
11
|
+
property alias color: surface.color
|
|
12
|
+
property alias borderColor: surface.border.color
|
|
13
|
+
property alias borderWidth: surface.border.width
|
|
14
|
+
property alias contentItem: contentHost
|
|
15
|
+
|
|
16
|
+
// 阴影配置
|
|
17
|
+
property color shadowColor: "#40000000"
|
|
18
|
+
property real shadowBlur: 24.0
|
|
19
|
+
property real shadowOffsetY: 6.0
|
|
20
|
+
property real shadowOpacity: 1.0
|
|
21
|
+
|
|
22
|
+
// hover 动画配置
|
|
23
|
+
property real hoverScale: 1.03
|
|
24
|
+
property int animationDuration: 180
|
|
25
|
+
property bool hoverEnabled: true
|
|
26
|
+
|
|
27
|
+
// 只读状态
|
|
28
|
+
readonly property bool hovered: hoverArea.containsMouse
|
|
29
|
+
|
|
30
|
+
implicitWidth: 240
|
|
31
|
+
implicitHeight: 140
|
|
32
|
+
|
|
33
|
+
// 放大动画根节点(避免缩放影响外部布局)
|
|
34
|
+
Item {
|
|
35
|
+
id: scaleRoot
|
|
36
|
+
anchors.fill: parent
|
|
37
|
+
scale: root.hoverEnabled && root.hovered ? root.hoverScale : 1.0
|
|
38
|
+
transformOrigin: Item.Center
|
|
39
|
+
|
|
40
|
+
Behavior on scale {
|
|
41
|
+
NumberAnimation {
|
|
42
|
+
duration: root.animationDuration
|
|
43
|
+
easing.type: Easing.OutCubic
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 阴影层
|
|
48
|
+
MultiEffect {
|
|
49
|
+
source: surface
|
|
50
|
+
anchors.fill: surface
|
|
51
|
+
shadowEnabled: true
|
|
52
|
+
shadowColor: root.shadowColor
|
|
53
|
+
shadowBlur: root.shadowBlur / 32.0
|
|
54
|
+
shadowVerticalOffset: root.shadowOffsetY
|
|
55
|
+
shadowOpacity: root.shadowOpacity
|
|
56
|
+
opacity: root.hovered ? 1.0 : 0.85
|
|
57
|
+
|
|
58
|
+
Behavior on opacity {
|
|
59
|
+
NumberAnimation {
|
|
60
|
+
duration: root.animationDuration
|
|
61
|
+
easing.type: Easing.OutCubic
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 卡片本体
|
|
67
|
+
Rectangle {
|
|
68
|
+
id: surface
|
|
69
|
+
anchors.fill: parent
|
|
70
|
+
radius: 12
|
|
71
|
+
color: "#1E2228"
|
|
72
|
+
antialiasing: true
|
|
73
|
+
|
|
74
|
+
border.color: root.hovered ? "#4C8DFF" : "#2E3440"
|
|
75
|
+
border.width: 1
|
|
76
|
+
|
|
77
|
+
Behavior on border.color {
|
|
78
|
+
ColorAnimation {
|
|
79
|
+
duration: root.animationDuration
|
|
80
|
+
easing.type: Easing.OutCubic
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 背景仅负责绘制,内容放在上层容器
|
|
85
|
+
clip: true
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 内容容器(位于 surface 之上,避免被裁剪到阴影层)
|
|
89
|
+
Item {
|
|
90
|
+
id: contentHost
|
|
91
|
+
anchors.fill: surface
|
|
92
|
+
anchors.margins: 16
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// hover 检测区域
|
|
97
|
+
MouseArea {
|
|
98
|
+
id: hoverArea
|
|
99
|
+
anchors.fill: parent
|
|
100
|
+
hoverEnabled: true
|
|
101
|
+
acceptedButtons: Qt.NoButton
|
|
102
|
+
cursorShape: Qt.PointingHandCursor
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import QtQuick
|
|
2
|
+
import QtQuick.Controls
|
|
3
|
+
import QtQuick.Layouts
|
|
4
|
+
|
|
5
|
+
ApplicationWindow {
|
|
6
|
+
id: window
|
|
7
|
+
width: 560
|
|
8
|
+
height: 360
|
|
9
|
+
visible: true
|
|
10
|
+
color: "#f0f2f5"
|
|
11
|
+
title: "NovaCard Demo"
|
|
12
|
+
|
|
13
|
+
GridLayout {
|
|
14
|
+
anchors.centerIn: parent
|
|
15
|
+
columns: 2
|
|
16
|
+
rowSpacing: 32
|
|
17
|
+
columnSpacing: 32
|
|
18
|
+
|
|
19
|
+
Repeater {
|
|
20
|
+
model: [
|
|
21
|
+
{ title: "基础卡片", desc: "默认圆角与时影" },
|
|
22
|
+
{ title: "悬停试试", desc: "鼠标移上去会放大哦" },
|
|
23
|
+
{ title: "自定义半径", desc: "radius: 8" },
|
|
24
|
+
{ title: "无阴影", desc: "shadowBlur: 0" }
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
NovaCard {
|
|
28
|
+
width: 220
|
|
29
|
+
height: 120
|
|
30
|
+
radius: index === 2 ? 8 : 16
|
|
31
|
+
shadowBlur: index === 3 ? 0.0 : 1.0
|
|
32
|
+
|
|
33
|
+
ColumnLayout {
|
|
34
|
+
anchors.fill: parent
|
|
35
|
+
anchors.margins: 16
|
|
36
|
+
spacing: 6
|
|
37
|
+
|
|
38
|
+
Label {
|
|
39
|
+
text: modelData.title
|
|
40
|
+
font.pixelSize: 16
|
|
41
|
+
font.bold: true
|
|
42
|
+
color: "#1f2328"
|
|
43
|
+
}
|
|
44
|
+
Label {
|
|
45
|
+
text: modelData.desc
|
|
46
|
+
font.pixelSize: 12
|
|
47
|
+
color: "#6b7280"
|
|
48
|
+
Layout.fillWidth: true
|
|
49
|
+
wrapMode: Text.WordWrap
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
onClicked: console.log("clicked:", modelData.title)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from PySide6.QtCore import QObject, QUrl, Signal, Slot
|
|
7
|
+
from PySide6.QtGui import QGuiApplication
|
|
8
|
+
from PySide6.QtQml import QQmlApplicationEngine
|
|
9
|
+
from PySide6.QtQuick import QQuickWindow, QSGRendererInterface
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ButtonDemoBridge(QObject):
|
|
13
|
+
"""Python 侧逻辑桥:只负责逻辑,UI 全部在 QML 中声明。"""
|
|
14
|
+
|
|
15
|
+
clickedCountChanged = Signal(int)
|
|
16
|
+
|
|
17
|
+
def __init__(self, parent: QObject | None = None) -> None:
|
|
18
|
+
super().__init__(parent)
|
|
19
|
+
self._clicked_count: int = 0
|
|
20
|
+
|
|
21
|
+
@Slot()
|
|
22
|
+
def onPrimaryClicked(self) -> None:
|
|
23
|
+
self._clicked_count += 1
|
|
24
|
+
print(f"[primary] clicked, total={self._clicked_count}")
|
|
25
|
+
self.clickedCountChanged.emit(self._clicked_count)
|
|
26
|
+
|
|
27
|
+
@Slot()
|
|
28
|
+
def onSecondaryClicked(self) -> None:
|
|
29
|
+
print("[secondary] clicked")
|
|
30
|
+
|
|
31
|
+
@Slot()
|
|
32
|
+
def onDangerClicked(self) -> None:
|
|
33
|
+
print("[danger] clicked")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main() -> None:
|
|
37
|
+
QQuickWindow.setGraphicsApi(QSGRendererInterface.GraphicsApi.Direct3D11)
|
|
38
|
+
|
|
39
|
+
app: QGuiApplication = QGuiApplication(sys.argv)
|
|
40
|
+
|
|
41
|
+
engine: QQmlApplicationEngine = QQmlApplicationEngine()
|
|
42
|
+
|
|
43
|
+
qml_dir: Path = Path(__file__).resolve().parent
|
|
44
|
+
engine.addImportPath(str(qml_dir))
|
|
45
|
+
|
|
46
|
+
bridge: ButtonDemoBridge = ButtonDemoBridge()
|
|
47
|
+
engine.rootContext().setContextProperty("bridge", bridge)
|
|
48
|
+
|
|
49
|
+
demo_qml: Path = qml_dir / "DemoButton.qml"
|
|
50
|
+
engine.load(QUrl.fromLocalFile(str(demo_qml)))
|
|
51
|
+
|
|
52
|
+
if not engine.rootObjects():
|
|
53
|
+
sys.exit(-1)
|
|
54
|
+
|
|
55
|
+
sys.exit(app.exec())
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
main()
|