This commit is contained in:
2025-11-02 22:06:42 +08:00
parent e71b69db5f
commit c2aa65193d
19 changed files with 427 additions and 274 deletions

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@ logs/
config/ config/
test/ test/
build/ build/
dist/
image_cache/ image_cache/
download/ download/
text_cache/ text_cache/

39
LeonPan.spec Normal file
View File

@@ -0,0 +1,39 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[('D:\\Projects\\Python\\LeonPan PC\\_internal', '_internal'), ('D:\\Projects\\Python\\LeonPan PC\\logo.png', 'logo.png'), ('D:\\Projects\\Python\\LeonPan PC\\welcome_video.py', 'welcome_video.py'), ('D:\\Projects\\Python\\LeonPan PC\\app\\resource', 'app/resource')],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='LeonPan',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=['D:\\Projects\\Python\\LeonPan PC\\logo.png'],
)

39
LeonPan_Console.spec Normal file
View File

@@ -0,0 +1,39 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[('D:\\Projects\\Python\\LeonPan PC\\_internal', '_internal'), ('D:\\Projects\\Python\\LeonPan PC\\logo.png', 'logo.png'), ('D:\\Projects\\Python\\LeonPan PC\\welcome_video.py', 'welcome_video.py'), ('D:\\Projects\\Python\\LeonPan PC\\app\\resource', 'app/resource')],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='LeonPan_Console',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=['D:\\Projects\\Python\\LeonPan PC\\logo.png'],
)

View File

@@ -1,101 +0,0 @@
1. 09-88-4069
2. 72-33-8317
3. 93-54-4878
4. 93-86-1869
5. 14-04-1203
6. 84-00-2929
7. 04-82-6399
8. 79-11-1229
9. 11-06-1474
10. 07-72-4607
11. 56-87-7920
12. 53-25-2426
13. 67-39-6157
14. 63-39-2233
15. 17-96-4158
16. 17-62-6178
17. 15-37-0827
18. 59-52-8112
19. 03-21-4560
20. 55-11-5109
21. 67-99-2556
22. 91-20-2424
23. 22-43-2052
24. 77-64-5252
25. 59-35-8073
26. 55-31-5493
27. 32-76-3175
28. 84-91-2685
29. 87-12-2589
30. 80-19-6183
31. 01-53-7563
32. 48-27-6989
33. 11-13-8688
34. 09-54-6589
35. 07-23-1493
36. 59-42-4357
37. 35-19-0434
38. 14-36-4400
39. 02-87-5750
40. 70-48-9729
41. 72-69-7715
42. 46-99-7577
43. 38-14-9272
44. 40-42-7732
45. 96-91-2081
46. 23-68-6080
47. 91-39-5462
48. 19-92-2732
49. 72-23-5102
50. 27-88-0690
51. 15-45-5394
52. 05-99-9553
53. 99-05-6922
54. 01-89-7062
55. 48-19-1418
56. 55-07-8466
57. 96-22-4957
58. 53-82-8031
59. 18-11-2941
60. 98-93-7633
61. 63-94-6925
62. 12-74-5129
63. 34-17-2602
64. 03-04-7664
65. 90-38-9969
66. 24-89-8276
67. 97-89-9536
68. 80-32-7705
69. 72-32-7795
70. 12-80-4790
71. 73-84-9545
72. 91-03-5053
73. 52-40-1281
74. 74-75-2213
75. 17-50-3710
76. 60-36-2783
77. 98-57-1091
78. 83-59-9231
79. 29-48-8192
80. 12-91-0807
81. 74-25-2849
82. 64-82-0610
83. 10-23-2036
84. 91-63-7704
85. 28-68-1631
86. 47-23-2650
87. 38-04-5634
88. 96-08-5695
89. 03-56-8427
90. 39-51-0578
91. 44-87-6122
92. 64-20-1925
93. 93-14-6077
94. 98-76-4163
95. 94-60-5635
96. 52-43-0973
97. 33-16-2106
98. 12-66-8985
99. 24-57-6007
100. 40-23-4384
<##-##-####>

View File

Before

Width:  |  Height:  |  Size: 9.3 MiB

After

Width:  |  Height:  |  Size: 9.3 MiB

View File

Before

Width:  |  Height:  |  Size: 4.4 MiB

After

Width:  |  Height:  |  Size: 4.4 MiB

View File

Before

Width:  |  Height:  |  Size: 11 MiB

After

Width:  |  Height:  |  Size: 11 MiB

View File

Before

Width:  |  Height:  |  Size: 4.4 MiB

After

Width:  |  Height:  |  Size: 4.4 MiB

View File

Before

Width:  |  Height:  |  Size: 7.9 MiB

After

Width:  |  Height:  |  Size: 7.9 MiB

View File

Before

Width:  |  Height:  |  Size: 5.9 MiB

After

Width:  |  Height:  |  Size: 5.9 MiB

View File

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@@ -68,7 +68,7 @@
let tempFilePath = ''; let tempFilePath = '';
// 初始化Monaco Editor // 初始化Monaco Editor
require(['vs/editor/editor.main'], function () { require(['vs/editor/editor.main'], function() {
// 从URL参数获取内容和语言 // 从URL参数获取内容和语言
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const contentParam = urlParams.get('content'); const contentParam = urlParams.get('content');
@@ -82,6 +82,7 @@
initialContent = Base.decode(contentParam); initialContent = Base.decode(contentParam);
} catch (e) { } catch (e) {
console.error('解码内容失败:', e); console.error('解码内容失败:', e);
initialContent = contentParam; // 如果解码失败,使用原始内容
} }
} }
@@ -89,28 +90,33 @@
editor = monaco.editor.create(document.getElementById('container'), { editor = monaco.editor.create(document.getElementById('container'), {
value: initialContent, value: initialContent,
language: languageParam, language: languageParam,
theme: 'vs-dark',
automaticLayout: true, automaticLayout: true,
minimap: { enabled: true }, minimap: { enabled: true },
scrollBeyondLastLine: false,
fontSize: 14, fontSize: 14,
wordWrap: 'on', fontFamily: 'Consolas, "Courier New", monospace',
scrollBeyondLastLine: false,
lineNumbers: 'on', lineNumbers: 'on',
readOnly: false, roundedSelection: true,
fontFamily: 'Consolas, "Microsoft YaHei", monospace' scrollbar: {
useShadows: false,
verticalScrollbarSize: 10,
horizontalScrollbarSize: 10
}
}); });
// 添加内容变化监听器 // 监听窗口大小变化
editor.onDidChangeModelContent(function() { window.addEventListener('resize', function() {
// 内容变化处理
});
// 窗口大小改变时重新布局
window.onresize = function () {
if (editor) { if (editor) {
editor.layout(); editor.layout();
} }
}; });
// 延迟执行,确保编辑器已初始化
setTimeout(function() {
if (editor) {
editor.focus();
}
}, 100);
}); });
// 暴露获取内容的方法给QWebEngineView调用 // 暴露获取内容的方法给QWebEngineView调用
@@ -151,10 +157,7 @@
language: editor && editor.getModel() ? editor.getModel().getLanguageId() : 'text' language: editor && editor.getModel() ? editor.getModel().getLanguageId() : 'text'
}; };
// 方法1: 保存到localStorage // 直接向本地服务器发送POST请求主要方式
localStorage.setItem('leonpan_editor_content', JSON.stringify(saveData));
// 方法2: 直接向本地服务器发送POST请求主要方式
try { try {
// 获取当前端口号 // 获取当前端口号
const currentPort = window.location.port; const currentPort = window.location.port;
@@ -167,53 +170,38 @@
xhr.onload = function() { xhr.onload = function() {
if (xhr.status === 200) { if (xhr.status === 200) {
console.log('服务器接收保存成功'); console.log('服务器接收保存成功');
alert('内容已成功保存并返回应用程序');
// 尝试使用自定义协议返回应用程序
try {
window.location.href = 'leonpan:save-success';
} catch (e) {
console.error('无法通过协议返回应用程序:', e);
}
// 尝试关闭窗口(某些浏览器可能阻止此操作)
setTimeout(function() {
window.close();
}, 500);
} else {
console.error('服务器返回错误状态:', xhr.status);
alert('保存失败,请重试');
} }
}; };
xhr.onerror = function() { xhr.onerror = function() {
console.error('向服务器发送保存请求失败'); console.error('向服务器发送保存请求失败');
alert('保存请求发送失败,请检查网络连接');
}; };
xhr.send(JSON.stringify(saveData)); xhr.send(JSON.stringify(saveData));
} catch (e) { } catch (e) {
console.error('发送保存请求时出错:', e); console.error('发送保存请求时出错:', e);
alert('保存过程中发生错误: ' + e.message);
} }
// 方法3: 创建一个下载链接作为备用方式 // 备用: 保存到localStorage以便应用程序在POST失败时可以尝试读取
const blob = new Blob([JSON.stringify(saveData)], {type: 'application/json'}); localStorage.setItem('leonpan_editor_content', JSON.stringify(saveData));
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
// 设置文件名和下载路径
a.download = 'editor_content.json';
// 如果提供了临时文件路径,尝试使用该路径
if (tempFilePath) {
// 对于Windows路径需要进行特殊处理
if (navigator.platform.indexOf('Win') !== -1) {
// 在Windows中我们不能直接设置文件系统路径但可以提示用户
console.log('建议保存路径:', tempFilePath);
}
}
a.href = url;
// 显示成功提示
alert('内容已成功保存!应用程序将自动检测到您的更改。');
// 自动触发下载(作为备用机制)
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
// 尝试通过协议处理程序返回应用程序
try {
// 使用自定义协议打开应用程序
window.location.href = 'leonpan:save-success';
} catch (e) {
console.error('无法返回应用程序:', e);
}
} }
// 获取系统临时目录路径(用于显示给用户) // 获取系统临时目录路径(用于显示给用户)
@@ -228,56 +216,28 @@
} }
} }
// 从URL参数获取初始化数据 // 初始化函数根据URL参数
function initFromUrl() { function initFromUrl() {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const contentParam = urlParams.get('content');
const languageParam = urlParams.get('language');
// 获取并设置内容 if (contentParam) {
const encodedContent = urlParams.get('content');
if (encodedContent) {
try { try {
const decodedContent = Base.decode(encodedContent); const decodedContent = Base.decode(contentParam);
setEditorContent(decodedContent); setEditorContent(decodedContent);
} catch (e) { } catch (e) {
console.error('解码内容失败:', e); console.error('从URL初始化内容失败:', e);
} }
} }
// 获取并设置语言 if (languageParam) {
const language = urlParams.get('language') || 'text'; setEditorLanguage(languageParam);
setEditorLanguage(language); }
// 保存文件ID到本地存储
const fileId = urlParams.get('fileId');
if (fileId) {
localStorage.setItem('currentFileId', fileId);
} }
// 获取临时文件路径 // 页面加载完成后执行初始化
tempFilePath = urlParams.get('temp_file') || '';
}
// 初始化时从URL参数加载数据
window.addEventListener('load', function() { window.addEventListener('load', function() {
// 从localStorage中加载可能保存的内容
const savedContent = localStorage.getItem('leonpan_editor_content');
if (savedContent) {
try {
const parsed = JSON.parse(savedContent);
if (parsed.saved && parsed.content) {
// 延迟执行,确保编辑器已初始化
setTimeout(() => {
if (editor) {
const decodedContent = Base.decode(parsed.content);
editor.setValue(decodedContent);
}
}, 1000);
}
} catch (e) {
console.error('解析保存的内容失败:', e);
}
}
// 延迟执行,确保编辑器已初始化 // 延迟执行,确保编辑器已初始化
setTimeout(initFromUrl, 1000); setTimeout(initFromUrl, 1000);
}); });

View File

@@ -12,7 +12,7 @@ _current_language = "zh"
# 翻译词典 # 翻译词典
_translations = {} _translations = {}
# 语言文件目录 # 语言文件目录
_LANG_DIR = Path("app/resource/lang").absolute() _LANG_DIR = Path("_internal/lang").absolute()
# print(Path("app/resource/lang").absolute()) # print(Path("app/resource/lang").absolute())

View File

@@ -268,6 +268,12 @@ class PreviewTextBox(MessageBoxBase):
self._id = _id self._id = _id
self.isChanged = False self.isChanged = False
# 初始化关键变量
self.isContentSaved = False # 明确初始化保存状态
self.httpd = None # 服务器实例引用
self.server_thread = None # 服务器线程引用
self.tempFilePath = None # 临时文件路径
# 设置编辑器HTML文件路径 - 修正为项目根目录的_internal文件夹 # 设置编辑器HTML文件路径 - 修正为项目根目录的_internal文件夹
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
self.editor_index = os.path.join(project_root, "_internal/editor_main/index.html") self.editor_index = os.path.join(project_root, "_internal/editor_main/index.html")
@@ -348,7 +354,10 @@ class PreviewTextBox(MessageBoxBase):
) )
# 隐藏进度条 # 隐藏进度条
self.saveProgressBar.hide() self.saveProgressBar.hide()
QTimer.singleShot(700, self.accept)
# 直接调用accept()关闭窗口,不再使用定时器延迟
# 确保窗口立即关闭并返回成功结果
self.accept()
def _errorSave(self, msg): def _errorSave(self, msg):
logger.error(f"文本文件保存失败文件ID: {self._id}, 错误: {msg}") logger.error(f"文本文件保存失败文件ID: {self._id}, 错误: {msg}")
@@ -432,14 +441,9 @@ class PreviewTextBox(MessageBoxBase):
def setTextContent(self, content): def setTextContent(self, content):
"""设置文本内容并在外部浏览器中打开""" """设置文本内容并在外部浏览器中打开"""
# 检查内容是否已保存,如果已保存则不允许再次编辑 # 每次打开文件时都重置isContentSaved状态为False允许重新编辑
if hasattr(self, 'isContentSaved') and self.isContentSaved: self.isContentSaved = False
logger.warning("内容已保存,不允许再次编辑") logger.info(f"重置isContentSaved为False文件ID: {self._id}")
self.placeholderLabel.setText('''<div style='text-align:center; padding:20px;'>
<h3>已经保存</h3>
<p>该内容已经保存,不再允许编辑</p>
</div>''')
return
logger.info(f"文本文件加载成功,原始内容长度: {len(content)}字符") logger.info(f"文本文件加载成功,原始内容长度: {len(content)}字符")
@@ -563,10 +567,10 @@ class PreviewTextBox(MessageBoxBase):
attempts = 0 attempts = 0
while attempts < max_attempts: while attempts < max_attempts:
try: try:
# 设置处理器的实例引用
EditorHTTPRequestHandler.preview_box_instance = self
# 使用自定义处理器创建服务器 # 使用自定义处理器创建服务器
self.httpd = ReuseTCPServer(("127.0.0.1", port), EditorHTTPRequestHandler) self.httpd = ReuseTCPServer(("127.0.0.1", port), EditorHTTPRequestHandler)
# 保存当前实例的引用,以便处理器可以访问
self.httpd.RequestHandlerClass.preview_box_instance = self
logger.info(f"Web服务器成功在端口 {port} 启动") logger.info(f"Web服务器成功在端口 {port} 启动")
break break
except OSError as e: except OSError as e:
@@ -616,87 +620,44 @@ class PreviewTextBox(MessageBoxBase):
self.pollingTimer.start() self.pollingTimer.start()
def _checkBrowserContent(self): def _checkBrowserContent(self):
"""检查浏览器是否已保存内容""" """检查浏览器是否已保存内容 - 现在仅作为备用方案"""
# 首先尝试从localStorage读取 # 由于我们使用POST请求直接传递内容这里不再需要轮询检查文件
try: # 轮询可以保留作为备用方案但主要保存机制是通过POST请求
import json pass
import os
import base64
# 尝试获取localStorage数据
# 在Windows上localStorage通常存储在用户的AppData目录中
# 由于直接访问localStorage有困难我们使用一个更可靠的方法
# 1. 首先检查临时文件
if self.tempFilePath and os.path.exists(self.tempFilePath):
try:
with open(self.tempFilePath, 'r', encoding='utf-8') as f:
data = json.load(f)
if data.get('saved', False):
logger.info("检测到浏览器已保存内容(通过临时文件)")
self._processSavedContent(data)
return
except Exception as e:
logger.error(f"读取临时文件失败: {e}")
# 2. 作为备用方案,检查是否有特定的保存文件
# 这个文件可以由浏览器通过特定的方法创建
import tempfile
app_data_dir = os.path.join(tempfile.gettempdir(), 'LeonPan')
os.makedirs(app_data_dir, exist_ok=True)
save_file_path = os.path.join(app_data_dir, 'editor_content.json')
if os.path.exists(save_file_path):
try:
with open(save_file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
if data.get('saved', False):
logger.info("检测到浏览器已保存内容(通过备用文件)")
self._processSavedContent(data)
# 删除备用文件
os.unlink(save_file_path)
except Exception as e:
logger.error(f"读取备用保存文件失败: {e}")
except Exception as e:
logger.error(f"检查浏览器内容时出错: {e}")
def _processSavedContent(self, data): def _processSavedContent(self, data):
"""处理已保存的内容""" """处理已保存的内容"""
# 停止轮询 logger.info(f"开始处理保存的内容文件ID: {self._id}")
self.pollingTimer.stop()
# 更新内容 # 更新内容
import base64 import base64
encoded_content = data.get('content', '') encoded_content = data.get('content', '')
try: try:
self.editorContent = base64.b64decode(encoded_content).decode('utf-8') self.editorContent = base64.b64decode(encoded_content).decode('utf-8')
logger.info(f"内容解码成功,长度: {len(self.editorContent)}字符")
except Exception as e: except Exception as e:
logger.error(f"解码内容失败: {e}") logger.error(f"解码内容失败: {e}")
return return
# 标记内容已保存 # 不再标记为已保存让_saveContent方法处理保存逻辑
self.isContentSaved = True # 直接调用_saveContent方法保存内容到服务器不再重置状态
logger.info(f"调用_saveContent方法保存内容到服务器文件ID: {self._id}")
self._saveContent(self.editorContent)
# 更新占位符文本,显示已保存信息 # 这里不再显示成功信息因为_saveContent会通过_successSave方法显示
# 也不再设置定时器重置状态,因为成功保存后窗口会直接关闭
def _resetSaveState(self):
"""重置保存状态,允许再次编辑和保存"""
self.isContentSaved = False
logger.info(f"已重置保存状态允许再次编辑和保存文件ID: {self._id}")
# 恢复默认的占位符文本
self.placeholderLabel.setText('''<div style='text-align:center; padding:20px;'> self.placeholderLabel.setText('''<div style='text-align:center; padding:20px;'>
<h3>已经保存</h3> <h3>文本编辑已在外部浏览器中打开</h3>
<p>内容已从浏览器同步到应用并已保存</p> <p><strong>重要提示:</strong>完成编辑后,请点击浏览器中的"保存并返回"按钮</p>
<p>该内容将不再允许编辑</p> <p>应用正在自动检测...</p>
</div>''') </div>''')
# 禁用保存按钮
self.saveButton.setEnabled(False)
# 清理临时文件
if self.tempFilePath and os.path.exists(self.tempFilePath):
try:
os.unlink(self.tempFilePath)
self.tempFilePath = None
except Exception as e:
logger.error(f"删除临时文件失败: {e}")
def handleError(self, error_msg): def handleError(self, error_msg):
@@ -741,15 +702,10 @@ class PreviewTextBox(MessageBoxBase):
<script> <script>
let editor; let editor;
require.config({{ paths: {{ 'vs': './vs' }} }}); require.config({ paths: { 'vs': './vs' } });
require(['vs/editor/editor.main'], function() {{ require(['vs/editor/editor.main'], function() {{
editor = monaco.editor.create(document.getElementById('container'), {{ editor = monaco.editor.create(document.getElementById('container'), {{
value: `{content.replace('`', '\\`')}`, value: `{content.replace('`', '\\`')}`,
}});
// 重新获取编辑器实例以确保正确初始化
setTimeout(() => {{
editor = monaco.editor.getModels()[0] ? monaco.editor.getModels()[0].getContainerInfo().domNode.monacoEditor : null;
}}, 100);'\\`'}}`,
language: '{language}', language: '{language}',
theme: 'vs-dark', theme: 'vs-dark',
automaticLayout: true, automaticLayout: true,
@@ -761,6 +717,10 @@ class PreviewTextBox(MessageBoxBase):
readOnly: false, readOnly: false,
fontFamily: 'Consolas, "Microsoft YaHei", monospace' fontFamily: 'Consolas, "Microsoft YaHei", monospace'
}}); }});
// 重新获取编辑器实例以确保正确初始化
setTimeout(() => {{
editor = monaco.editor.getModels()[0] ? monaco.editor.getModels()[0].getContainerInfo().domNode.monacoEditor : null;
}}, 100);
// 添加内容变化监听器 // 添加内容变化监听器
editor.onDidChangeModelContent(function() {{ editor.onDidChangeModelContent(function() {{
@@ -833,9 +793,40 @@ class PreviewTextBox(MessageBoxBase):
# JavaScript执行完成的处理已移除 # JavaScript执行完成的处理已移除
def __del__(self):
"""析构函数,确保清理资源"""
# 停止轮询定时器
self.pollingTimer.stop()
# 停止HTTP服务器
if hasattr(self, 'httpd'):
try:
self.httpd.shutdown()
self.httpd.server_close()
logger.info("Web服务器已停止")
except Exception as e:
logger.warning(f"停止Web服务器时出错: {e}")
# 删除临时文件
if hasattr(self, 'tempFilePath') and self.tempFilePath and os.path.exists(self.tempFilePath):
try:
os.unlink(self.tempFilePath)
logger.info(f"临时文件已删除: {self.tempFilePath}")
except Exception as e:
logger.warning(f"删除临时文件失败: {e}")
def _saveContent(self, content): def _saveContent(self, content):
"""保存编辑器内容并提交修改""" """保存编辑器内容并提交修改"""
logger.info(f"保存文本文件修改文件ID: {self._id}") logger.info(f"保存文本文件修改文件ID: {self._id}")
# 确保断开之前可能存在的连接,避免多次连接
if hasattr(self, 'saveTextThread') and self.saveTextThread:
try:
self.saveTextThread.successUpdated.disconnect()
self.saveTextThread.errorUpdated.disconnect()
except:
pass
# 创建新的保存线程
self.saveTextThread = UpdateFileContentThread( self.saveTextThread = UpdateFileContentThread(
self._id, self._id,
content, content,

224
build.py Normal file
View File

@@ -0,0 +1,224 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
应用程序构建脚本
使用PyInstaller将Python应用打包为可执行文件
"""
import os
import sys
import subprocess
import shutil
from pathlib import Path
# 项目根目录
PROJECT_ROOT = Path(__file__).parent.absolute()
# 主程序入口
MAIN_SCRIPT = "main.py"
# 输出目录
OUTPUT_DIR = PROJECT_ROOT / "dist"
BUILD_DIR = PROJECT_ROOT / "build"
# 应用名称
APP_NAME = "LeonPan"
APP_NAME_CONSOLE = "LeonPan_Console"
# 图标文件
ICON_FILE = "logo.png"
# 需要包含的额外文件和目录
EXTRA_DATA = [
("_internal", "_internal"), # 编辑器和相关资源
("logo.png", "logo.png"), # 应用图标
("welcome_video.py", "welcome_video.py"), # 欢迎视频脚本
("app/resource", "app/resource") # 应用资源
]
def run_command(command, cwd=None):
"""执行命令并返回结果"""
print(f"执行命令: {' '.join(command)}")
try:
result = subprocess.run(
command,
cwd=cwd or PROJECT_ROOT,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
print(f"命令执行成功: {result.stdout[:100]}..." if len(result.stdout) > 100 else f"命令执行成功: {result.stdout}")
return True
except subprocess.CalledProcessError as e:
print(f"命令执行失败: {e.stderr}")
return False
def check_dependencies():
"""检查构建依赖是否已安装"""
print("检查构建依赖...")
# 检查pyinstaller
try:
import PyInstaller
print(f"PyInstaller 已安装,版本: {PyInstaller.__version__}")
except ImportError:
print("PyInstaller 未安装,正在安装...")
if not run_command([sys.executable, "-m", "pip", "install", "pyinstaller"]):
print("安装PyInstaller失败请手动安装后重试")
return False
# 安装项目依赖
requirements_file = PROJECT_ROOT / "requirements.txt"
if requirements_file.exists():
print(f"安装项目依赖: {requirements_file}")
if not run_command([sys.executable, "-m", "pip", "install", "-r", str(requirements_file)]):
print("安装项目依赖失败请检查requirements.txt文件")
return False
return True
def clean_output():
"""清理之前的构建输出"""
print("清理之前的构建输出...")
# 删除dist目录
if OUTPUT_DIR.exists():
try:
shutil.rmtree(OUTPUT_DIR)
print(f"已删除目录: {OUTPUT_DIR}")
except Exception as e:
print(f"删除 {OUTPUT_DIR} 失败: {e}")
# 删除build目录
if BUILD_DIR.exists():
try:
shutil.rmtree(BUILD_DIR)
print(f"已删除目录: {BUILD_DIR}")
except Exception as e:
print(f"删除 {BUILD_DIR} 失败: {e}")
# 删除.spec文件
spec_file = PROJECT_ROOT / f"{APP_NAME}.spec"
if spec_file.exists():
try:
os.remove(spec_file)
print(f"已删除文件: {spec_file}")
except Exception as e:
print(f"删除 {spec_file} 失败: {e}")
def build_app():
"""使用PyInstaller构建应用"""
print("开始构建应用...")
# 构建两个版本:无控制台版本和有控制台版本
builds = [
{
"name": APP_NAME, # 无控制台版本
"console": False
},
{
"name": APP_NAME_CONSOLE, # 有控制台版本
"console": True
}
]
# 为每个版本执行构建
for build_config in builds:
build_name = build_config["name"]
use_console = build_config["console"]
print(f"\n构建版本: {build_name} (控制台: {'启用' if use_console else '禁用'})")
# 构建pyinstaller命令
cmd = [
sys.executable,
"-m",
"PyInstaller",
MAIN_SCRIPT,
"--name", build_name,
"--onefile", # 构建单个可执行文件
]
# 设置控制台选项
if use_console:
cmd.append("--console")
else:
cmd.extend(["--windowed", "--noconsole"])
# 添加图标
icon_path = PROJECT_ROOT / ICON_FILE
if icon_path.exists():
cmd.extend(["--icon", str(icon_path)])
print(f"使用图标: {icon_path}")
else:
print(f"警告: 图标文件 {icon_path} 不存在,将使用默认图标")
# 添加额外的数据文件
for src, dst in EXTRA_DATA:
src_path = PROJECT_ROOT / src
if src_path.exists():
cmd.extend(["--add-data", f"{src_path}{os.pathsep}{dst}"])
print(f"添加额外数据: {src} -> {dst}")
else:
print(f"警告: 额外数据 {src_path} 不存在,跳过")
# 执行构建命令
if not run_command(cmd):
print(f"{build_name} 构建失败!")
return False
print("所有版本构建成功!")
return True
def copy_additional_files():
"""复制额外的文件到输出目录"""
print("复制额外文件到输出目录...")
if not OUTPUT_DIR.exists():
print(f"错误: 输出目录 {OUTPUT_DIR} 不存在")
return False
# 复制_internal目录两个可执行文件共用
internal_src = PROJECT_ROOT / "_internal"
internal_dst = OUTPUT_DIR / "_internal"
if internal_src.exists():
try:
if internal_dst.exists():
shutil.rmtree(internal_dst)
shutil.copytree(internal_src, internal_dst)
print(f"已复制 _internal 目录到 {internal_dst}(两个可执行文件共用)")
except Exception as e:
print(f"复制 _internal 目录失败: {e}")
return True
def main():
"""主函数"""
print(f"开始构建 {APP_NAME} 应用...")
print(f"项目根目录: {PROJECT_ROOT}")
# 检查依赖
if not check_dependencies():
print("依赖检查失败,终止构建")
return 1
# 清理输出
clean_output()
# 构建应用
if not build_app():
print("应用构建失败,终止")
return 1
# 复制额外文件
copy_additional_files()
print("\n构建完成!")
print(f"无控制台版本可执行文件: {OUTPUT_DIR / APP_NAME}{'.exe' if sys.platform == 'win32' else ''}")
print(f"有控制台版本可执行文件: {OUTPUT_DIR / APP_NAME_CONSOLE}{'.exe' if sys.platform == 'win32' else ''}")
print(f"共用资源目录: {OUTPUT_DIR / '_internal'}")
return 0
if __name__ == "__main__":
sys.exit(main())

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 KiB

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.