This commit is contained in:
liqiang
2025-09-19 10:56:00 +08:00
parent ab1e0268d1
commit 2800d1ee14
39 changed files with 3903 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
# dvadmin3-build
## 介绍
一款适用于**django-vue3-admin** 编译打包exe、macOS的dmg文件等打包工具。支持加密代码、一键启动项目无需考虑环境。
**dvadmin3-build** 是一个方便的工具,用于将**django-vue3-admin**项目编译打包为可执行文件如exe或macOS的dmg文件等。它提供了以下好处
- 方便的部署:使用**dvadmin3-build**,您无需担心环境依赖和配置问题。您可以将整个项目打包为一个可执行文件或者安装包,方便快速部署在任何支持的操作系统上。
- 代码加密:**dvadmin3-build** 支持代码加密,可以将您的项目源代码加密为二进制格式,增加代码的安全性和保护知识产权。
- 一键启动项目:打包后的可执行文件或安装包可以通过简单的双击来启动项目,无需手动设置和配置环境,减少了部署和启动的复杂性。
- 跨平台支持:**dvadmin3-build** 可以将**django-vue3-admin**项目打包为适用于多个操作系统的可执行文件或安装包包括Windows、macOS、Ubuntu、中标麒麟等操作系统中。
- 易于使用:**dvadmin3-build** 提供了简单易懂的命令行界面,使得打包过程更加简便和高效。只需一个简单的命令,即可完成项目的打包。
## 功能支持项
- [ ] 支持平台
- [x] Windows
- [x] MacOS
- [ ] Ubuntu
- [ ] 中标麒麟
- [ ] 支持功能
- [x] 一键启动 dvadmin3
- [x] 托盘最小化
- [ ] dvadmin 初始化
- [ ] 数据库配置
- [ ] 端口配置
- [ ] 支持celery异步模块
- [ ] 进程守护
## 功能及使用方法
### 安装依赖
pip install dvadmin3_build-1.0.0-py3-none-any.whl
### 前端编译
yarn run build:local
### 后端
#### settings.py 中添加模块
~~~
INSTALLED_APPS = [
...
"dvadmin3_build"
]
HIDDEN_IMPORTS = [
'xxxx' # 添加 app 中自己的模块,用于编译
]
~~~
#### 迁移与初始化(项目已迁移过的不再需要)
~~~
python manage.py makemigrations
python manage.py migrate
python manage.py init
python manage.py init_data
~~~
#### 编译
~~~
# 编译后位于 dist 目录
python manage.py build
# windows 打包需要安装 InstallForgeSetup.exe使用 dvadmin3_InstallForge.ifp 模板dvadmin3_build/windows_build_tools 目录下)
# InstallForge 打包教程https://www.pythonguis.com/tutorials/packaging-pyqt6-applications-windows-pyinstaller/#setup
~~~

View File

View File

@@ -0,0 +1,21 @@
#!/bin/sh
# 获取传入的全局参数
dist_folder=$1
icon_path=$2
# 清空dmg文件夹。
rm -rf "$dist_folder/DVAServer"
rm -rf "$dist_folder/main"
# 如果DMG已经存在则删除它。
test -f "$dist_folder/DVAServer.dmg" && rm "$dist_folder/DVAServer.dmg"
create-dmg \
--volname "DVAServer" \
--volicon $icon_path \
--window-pos 200 120 \
--window-size 600 300 \
--icon-size 100 \
--icon "DVAServer.app" 175 120 \
--hide-extension "DVAServer.app" \
--app-drop-link 425 120 \
"$dist_folder/DVAServer.dmg" \
"$dist_folder/"

View File

@@ -0,0 +1,49 @@
# Form implementation generated from reading ui file 'dvadmin_main.ui'
#
# Created by: PyQt6 UI code generator 6.4.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_DvadminManager(object):
def setupUi(self, DvadminManager):
DvadminManager.setObjectName("DvadminManager")
DvadminManager.resize(400, 300)
DvadminManager.setMinimumSize(QtCore.QSize(400, 300))
DvadminManager.setMaximumSize(QtCore.QSize(400, 300))
self.label = QtWidgets.QLabel(parent=DvadminManager)
self.label.setGeometry(QtCore.QRect(10, 13, 60, 21))
self.label.setObjectName("label")
self.status_label = QtWidgets.QLabel(parent=DvadminManager)
self.status_label.setGeometry(QtCore.QRect(80, 12, 60, 21))
self.status_label.setObjectName("status_label")
self.start_button = QtWidgets.QPushButton(parent=DvadminManager)
self.start_button.setGeometry(QtCore.QRect(210, 10, 81, 26))
self.start_button.setObjectName("start_button")
self.stop_button = QtWidgets.QPushButton(parent=DvadminManager)
self.stop_button.setGeometry(QtCore.QRect(310, 9, 81, 26))
self.stop_button.setObjectName("stop_button")
self.line = QtWidgets.QFrame(parent=DvadminManager)
self.line.setGeometry(QtCore.QRect(0, 40, 401, 16))
self.line.setFrameShape(QtWidgets.QFrame.Shape.HLine)
self.line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
self.line.setObjectName("line")
self.log_label = QtWidgets.QTextEdit(parent=DvadminManager)
self.log_label.setGeometry(QtCore.QRect(-1, 47, 411, 261))
self.log_label.setObjectName("log_label")
self.retranslateUi(DvadminManager)
QtCore.QMetaObject.connectSlotsByName(DvadminManager)
def retranslateUi(self, DvadminManager):
_translate = QtCore.QCoreApplication.translate
DvadminManager.setWindowTitle(_translate("DvadminManager", "服务管理器"))
self.label.setText(_translate("DvadminManager", "运行状态:"))
self.status_label.setText(_translate("DvadminManager", "未启动"))
self.start_button.setText(_translate("DvadminManager", "启动服务"))
self.stop_button.setText(_translate("DvadminManager", "结束服务"))
self.log_label.setHtml(_translate("DvadminManager", ""))

View File

@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DvadminManager</class>
<widget class="QDialog" name="DvadminManager">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>300</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>400</width>
<height>300</height>
</size>
</property>
<property name="windowTitle">
<string>服务管理器</string>
</property>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>10</x>
<y>13</y>
<width>60</width>
<height>21</height>
</rect>
</property>
<property name="text">
<string>运行状态:</string>
</property>
</widget>
<widget class="QLabel" name="status_label">
<property name="geometry">
<rect>
<x>80</x>
<y>12</y>
<width>60</width>
<height>21</height>
</rect>
</property>
<property name="text">
<string>未启动</string>
</property>
</widget>
<widget class="QPushButton" name="start_button">
<property name="geometry">
<rect>
<x>210</x>
<y>10</y>
<width>81</width>
<height>26</height>
</rect>
</property>
<property name="text">
<string>启动服务</string>
</property>
</widget>
<widget class="QPushButton" name="stop_button">
<property name="geometry">
<rect>
<x>310</x>
<y>9</y>
<width>81</width>
<height>26</height>
</rect>
</property>
<property name="text">
<string>结束服务</string>
</property>
</widget>
<widget class="Line" name="line">
<property name="geometry">
<rect>
<x>0</x>
<y>40</y>
<width>401</width>
<height>16</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
<widget class="QTextEdit" name="log_label">
<property name="geometry">
<rect>
<x>-1</x>
<y>47</y>
<width>411</width>
<height>261</height>
</rect>
</property>
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'.AppleSystemUIFont'; font-size:13pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,8 @@
# extra-hooks/hooks-uvicorn.py
from PyInstaller.utils.hooks import get_package_paths, collect_submodules
datas = [
(get_package_paths('uvicorn')[1], 'uvicorn'),
]
hiddenimports = collect_submodules('whitenoise')

View File

@@ -0,0 +1,41 @@
# UTF-8
#
# For more details about fixed file info 'ffi' see:
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
VSVersionInfo(
ffi=FixedFileInfo(
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
# Set not needed items to zero 0.
filevers=(1, 0, 0, 0),
prodvers=(1, 0, 0, 0),
# Contains a bitmask that specifies the valid bits 'flags'r
mask=0x3f,
# Contains a bitmask that specifies the Boolean attributes of the file.
flags=0x0,
# The operating system for which this file was designed.
# 0x4 - NT and there is no need to change it.
OS=0x40004,
# The general type of file.
# 0x1 - the file is an application.
fileType=0x1,
# The function of the file.
# 0x0 - the function is not defined for this fileType
subtype=0x0,
# Creation date and time stamp.
date=(0, 0)
),
kids=[
StringFileInfo(
[
StringTable(
'080404b0',
[StringStruct('CompanyName', '北京信码新创科技有限公司'),
StringStruct('FileDescription', '边缘Agent'),
StringStruct('FileVersion', '1.0.0.0'),
StringStruct('LegalCopyright', 'Copyright (C) 2021-2025 北京信码新创科技有限公司 All Rights Reserved'),
StringStruct('ProductName', '边缘Agent'),
StringStruct('ProductVersion', '1.0.0.0')])
]),
VarFileInfo([VarStruct('Translation', [2052, 1200])])
]
)

View File

@@ -0,0 +1,57 @@
import json
import os
from pathlib import Path
from django.core.management.base import BaseCommand
from application.settings import BASE_DIR
from application import settings
class Command(BaseCommand):
"""
生产初始化菜单: python manage.py build
"""
def add_arguments(self, parser):
pass
def handle(self, *args, **options):
print(args, options)
base_path = Path(__file__).resolve().parent.parent
# main.spec 路径
main_spec_path = os.path.join(base_path.parent, 'main.spec')
# 执行编译
import subprocess
# 执行命令
HIDDEN_IMPORTS = ','.join(getattr(settings, 'HIDDEN_IMPORTS', []))
command = f'export BASE_DIR="{BASE_DIR}" && export HIDDEN_IMPORTS="{HIDDEN_IMPORTS}" && rm -rf {os.path.join(BASE_DIR, "dist")} && pyinstaller -y --clean {main_spec_path}'
if os.sys.platform.startswith('win'):
# Windows操作系统
# command = f'setx BASE_DIR "{BASE_DIR}" && set HIDDEN_IMPORTS "{HIDDEN_IMPORTS}" && del {os.path.join(BASE_DIR, "dist")} && pyinstaller -y --clean {main_spec_path}'
command = f'setx BASE_DIR "{BASE_DIR}" && setx HIDDEN_IMPORTS "{HIDDEN_IMPORTS}" && pyinstaller -y --clean {main_spec_path}'
print(command)
print("当前环境是 Windows")
elif os.sys.platform.startswith('linux'):
# Linux操作系统
print("当前环境是 Linux")
command += f' && rm -rf {os.path.join(BASE_DIR, "build")}'
elif os.sys.platform.startswith('darwin'):
# macOS操作系统
print("当前环境是 macOS")
build_dmg_path = os.path.join(base_path.parent, 'builddmg.sh')
# 判断logo 是否存在
logo_path = os.path.join(BASE_DIR, 'static', 'logo.icns')
if not os.path.exists(logo_path):
# 文件不存在的处理逻辑
logo_path = os.path.join(base_path.parent, 'static', 'logo.icns')
command += f' && chmod +x {build_dmg_path} && {build_dmg_path} {os.path.join(BASE_DIR, "dist")} {logo_path}'
command += f' && rm -rf {os.path.join(BASE_DIR, "build")}'
print(command)
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
for line in process.stdout:
print(line.replace('\n', ''))
# # 等待进程结束
process.wait()

View File

@@ -0,0 +1,304 @@
import json
import os
import re
import signal
import subprocess
import webbrowser
import time
import threading
from pathlib import Path
from PyQt6.QtCore import QObject, QRunnable, QThreadPool, QTimer, pyqtSignal, Qt
from PyQt6.QtNetwork import QLocalServer
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel, QSystemTrayIcon, QMenu, QMessageBox
from PyQt6.QtGui import QIcon, QTextCharFormat, QColor, QTextCursor
from PyQt6 import QtCore, QtGui, QtWidgets
import sys
# # 编译ui
# pyuic6 dvadmin_main.ui -o dvadmin_main.py
# 由于编译问题把dvadmin_main.py代码复制到本脚本中
class Ui_DvadminManager(object):
def setupUi(self, DvadminManager):
DvadminManager.setObjectName("DvadminManager")
DvadminManager.resize(400, 300)
DvadminManager.setMinimumSize(QtCore.QSize(400, 300))
DvadminManager.setMaximumSize(QtCore.QSize(400, 300))
self.label = QtWidgets.QLabel(parent=DvadminManager)
self.label.setGeometry(QtCore.QRect(10, 13, 60, 21))
self.label.setObjectName("label")
self.status_label = QtWidgets.QLabel(parent=DvadminManager)
self.status_label.setGeometry(QtCore.QRect(80, 12, 60, 21))
self.status_label.setObjectName("status_label")
self.start_button = QtWidgets.QPushButton(parent=DvadminManager)
self.start_button.setGeometry(QtCore.QRect(210, 10, 81, 26))
self.start_button.setObjectName("start_button")
self.stop_button = QtWidgets.QPushButton(parent=DvadminManager)
self.stop_button.setGeometry(QtCore.QRect(310, 9, 81, 26))
self.stop_button.setObjectName("stop_button")
self.line = QtWidgets.QFrame(parent=DvadminManager)
self.line.setGeometry(QtCore.QRect(0, 40, 401, 16))
self.line.setFrameShape(QtWidgets.QFrame.Shape.HLine)
self.line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
self.line.setObjectName("line")
self.log_label = QtWidgets.QTextEdit(parent=DvadminManager)
self.log_label.setGeometry(QtCore.QRect(-1, 47, 411, 261))
self.log_label.setObjectName("log_label")
self.retranslateUi(DvadminManager)
QtCore.QMetaObject.connectSlotsByName(DvadminManager)
def retranslateUi(self, DvadminManager):
_translate = QtCore.QCoreApplication.translate
DvadminManager.setWindowTitle(_translate("DvadminManager", "服务管理器"))
self.label.setText(_translate("DvadminManager", "运行状态:"))
self.status_label.setText(_translate("DvadminManager", "未启动"))
self.start_button.setText(_translate("DvadminManager", "启动服务"))
self.stop_button.setText(_translate("DvadminManager", "结束服务"))
self.log_label.setHtml(_translate("DvadminManager", ""))
class SelectWorkerSignals(QObject):
result = pyqtSignal(str)
stop = pyqtSignal(bool)
list_process = []
class ServerWorkerSignals(QObject):
result = pyqtSignal(str)
class SelectWorker(QRunnable):
def __init__(self):
super().__init__()
self.signals = SelectWorkerSignals()
self.is_run = True
def run(self):
# 模拟异步操作
import time
import psutil
while self.is_run:
# 遍历进程列表,结束即关闭服务
# 获取所有正在运行的进程列表
processes = psutil.process_iter()
new_list_process = []
for process in processes:
# 如果进程名称包含"uvicorn",则进行监控
if process.pid in list_process:
new_list_process.append(process.pid)
if list_process and not new_list_process:
self.signals.result.emit("异步,进程不存在!")
# 等待1秒
time.sleep(1)
def handle_stop_result(self, result):
"""
异步获取进程结果后执行
"""
self.is_run = result
class ServerWorker(QRunnable):
def __init__(self):
super().__init__()
self.signals = ServerWorkerSignals()
def run(self):
# 启动dvadmin服务
import os
print("启动dvadmin服务")
# import main
# main.run()
# 定义要执行的命令
command = f"./main"
if os.sys.platform.startswith('win'):
# Windows操作系统
print("当前环境是Windows", Path(__file__).resolve().parent)
print("当前环境是Windows", Path(__file__).resolve())
command = f"{Path(__file__).resolve().parent.parent}/main.exe"
elif os.sys.platform.startswith('linux'):
# Linux操作系统
print("当前环境是Linux")
elif os.sys.platform.startswith('darwin'):
# macOS操作系统
print("当前环境是macOS")
command = f"{Path(__file__).resolve().parent.parent}/MacOS/main"
else:
# 其他操作系统
print("当前环境是其他操作系统")
self.signals.result.emit(json.dumps({"code": 2001, "msg": command}))
global list_process
# 使用subprocess.Popen执行命令
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
self.signals.result.emit(json.dumps({"code": 2000, "msg": "服务启动成功..."}))
# 持续获取输出结果
pid = process.pid
list_process.append(pid)
for line in process.stdout:
match = re.search(r'Started server process \[(\d+)\]', line)
# 判断是否匹配成功
if match:
# 获取匹配到的数字
number = match.group(1)
list_process.append(number)
list_process = list(set(list_process))
self.signals.result.emit(json.dumps({"code": 2001, "msg": line.replace('\n', '')}))
# 等待进程结束
process.wait()
class MainWindow(QMainWindow, Ui_DvadminManager):
def __init__(self):
super().__init__()
self.setupUi(self)
# 开始、结束按钮
self.start_button.clicked.connect(self.start_service)
self.stop_button.clicked.connect(self.stop_service)
# 托盘按钮及事件
self.tray_icon = QSystemTrayIcon(QIcon(os.path.join(Path(__file__).resolve().parent, 'static','logo.icns')), self)
self.tray_icon.activated.connect(self.tray_icon_activated)
self.tray_menu = QMenu(self)
self.tray_menu.addAction(self.start_button.text(), self.start_service)
self.tray_menu.addAction(self.stop_button.text(), self.stop_service)
self.tray_menu.addSeparator()
self.tray_menu.addAction("退出", QApplication.quit)
self.tray_icon.setContextMenu(self.tray_menu)
self.tray_icon.show()
self.log_label.setReadOnly(True)
# 信号
self.select_worker = SelectWorker()
self.select_worker.signals.result.connect(self.handle_select_result)
self.select_worker.signals.stop.connect(self.select_worker.handle_stop_result)
self.server_worker = ServerWorker()
self.server_worker.signals.result.connect(self.handle_server_servers)
# 异步
self.select_threadpool = QThreadPool()
self.server_threadpool = QThreadPool()
def handle_select_result(self, result):
"""
异步获取进程结果后执行
"""
self.append_to_log('服务进程异常,服务已停止...', color='red')
global list_process
list_process = []
self.status_label.setText("已停止")
self.status_label.setStyleSheet("color: red;")
def handle_server_servers(self, result):
"""
启动服务
"""
json_result = json.loads(result)
if json_result.get('code') == 2000:
self.append_to_log(json_result.get('msg'), color='green')
# 启动成功打开浏览器
url = "http://127.0.0.1:8000/web/"
def open_browser_after_delay(url, delay):
time.sleep(delay)
webbrowser.open(url)
threading.Thread(target=open_browser_after_delay, args=(url, 3)).start()
self.status_label.setText("运行中")
self.status_label.setStyleSheet("color: green;")
elif json_result.get('code') == 2001:
self.append_to_log(json_result.get('msg'))
else:
self.append_to_log(json_result.get('msg'), color='red')
self.status_label.setText("已停止")
self.status_label.setStyleSheet("color: red;")
def append_to_log(self, message, color=None):
"""
添加日志颜色
"""
cursor = self.log_label.textCursor()
format = QTextCharFormat()
if color:
if color == "green":
format.setForeground(QColor("green"))
elif color == "red":
format.setForeground(QColor("red"))
cursor.movePosition(QTextCursor.MoveOperation.End)
cursor.insertText(message, format)
cursor.insertBlock()
self.log_label.setTextCursor(cursor)
self.log_label.ensureCursorVisible()
self.log_label.verticalScrollBar().setValue(self.log_label.verticalScrollBar().maximum())
def start_service(self):
global list_process
if not list_process:
# 启动服务,执行启动脚本
self.server_threadpool.clear()
self.select_threadpool.clear()
self.select_worker.signals.stop.emit(True)
self.select_threadpool.startOnReservedThread(self.select_worker.run)
self.server_threadpool.startOnReservedThread(self.server_worker.run)
self.status_label.setText("正在启动中")
self.status_label.setStyleSheet("color: green;")
else:
self.append_to_log("服务已启动...")
def stop_service(self):
"""
停止服务
"""
global list_process
if list_process:
for pid in list_process:
try:
os.kill(int(pid), signal.SIGTERM)
except Exception as e:
print('Exception', e)
pass
list_process = []
self.select_worker.signals.stop.emit(False)
self.select_threadpool.clear()
self.server_threadpool.clear()
self.status_label.setText("已停止")
self.status_label.setStyleSheet("color: red;")
self.service_running = False
self.append_to_log("服务已停止...", color='red')
def tray_icon_activated(self, reason):
"""
托盘激活
"""
if reason == QSystemTrayIcon.ActivationReason.Trigger:
self.showNormal()
self.activateWindow()
def showEvent(self, event):
# 自定义的显示事件处理
if self.status_label.text() != '已停止':
self.select_worker.signals.stop.emit(True)
self.select_threadpool.startOnReservedThread(self.select_worker.run)
return super().showEvent(event)
def closeEvent(self, event):
event.ignore()
self.hide()
self.select_worker.signals.stop.emit(False)
if __name__ == '__main__':
app = QApplication(sys.argv)
server = QLocalServer()
if server.listen('DVAServer'):
app.setQuitOnLastWindowClosed(False)
app.setWindowIcon(QIcon(os.path.join(Path(__file__).resolve().parent, 'static','logo.icns')))
window = MainWindow()
window.show()
sys.exit(app.exec())
else:
message_box = QMessageBox(QMessageBox.Icon.Information, 'Information', '应用程序已在运行')
message_box.exec()
sys.exit(0) # 如果已有实例在运行,则退出应用程序

View File

@@ -0,0 +1,28 @@
import setuptools
with open("README.md", "r") as fh:
long_description = fh.read()
setuptools.setup(
name="dvadmin3-build",
version="1.0.0",
author="DVAdmin",
author_email="liqiang@django-vue-admin.com",
description="一款适用于django-vue3-admin 编译打包exe、macOS的dmg文件等打包工具。支持加密代码、一键启动项目无需考虑环境。",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://gitee.com/huge-dream/dvadmin-build",
packages=setuptools.find_packages(),
python_requires='>=3.7, <4',
install_requires=[
"pyinstaller>=6.8.0",
"PyQt6>=6.4.2",
"psutil==6.0.0",
],
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
include_package_data=True
)

Binary file not shown.