555 lines
28 KiB
Python
555 lines
28 KiB
Python
from __future__ import annotations
|
||
|
||
import queue
|
||
import threading
|
||
import tkinter as tk
|
||
from pathlib import Path
|
||
from tkinter import filedialog, messagebox, ttk
|
||
|
||
from .deployer import DeploymentConfig, build_deployment_plan, run_steps
|
||
from .environment import EnvironmentCheck, check_environment, load_tool_paths, save_tool_paths
|
||
from .installer import build_install_command, recommend_jdk_version, run_install_command
|
||
from .path_manager import apply_user_environment, build_environment_update
|
||
from .scanner import ScanResult, scan_project
|
||
|
||
|
||
TEXT = {
|
||
"title": "\u0053\u0070\u0072\u0069\u006e\u0067\u0020\u0042\u006f\u006f\u0074\u0020\u002b\u0020\u0056\u0075\u0065\u0020\u81ea\u52a8\u5316\u90e8\u7f72\u5de5\u5177",
|
||
"workbench": "\u90e8\u7f72\u5de5\u4f5c\u53f0",
|
||
"select_dir": "\u9009\u62e9\u76ee\u5f55",
|
||
"scan": "\u626b\u63cf\u9879\u76ee",
|
||
"check_env": "\u68c0\u6d4b\u73af\u5883",
|
||
"deploy": "\u6267\u884c\u90e8\u7f72",
|
||
"results": "\u9879\u76ee\u4e0e\u73af\u5883\u7ed3\u679c",
|
||
"config": "\u90e8\u7f72\u914d\u7f6e",
|
||
"tool_paths": "\u5de5\u5177\u8def\u5f84\uff08\u53ef\u9009\uff09",
|
||
"logs": "\u8fd0\u884c\u65e5\u5fd7",
|
||
}
|
||
|
||
SIDEBAR_ITEMS = ("项目识别", "环境检测", "工具路径", "数据库", "部署任务", "运行日志")
|
||
|
||
|
||
def database_name_from_sql_files(sql_files: list[Path]) -> str:
|
||
return sql_files[0].stem if sql_files else ""
|
||
|
||
|
||
class DeployWorkbench(tk.Tk):
|
||
def __init__(self) -> None:
|
||
super().__init__()
|
||
self.title(TEXT["title"])
|
||
self.geometry("1260x820")
|
||
self.minsize(1080, 700)
|
||
|
||
self.project_dir = tk.StringVar()
|
||
self.backend_port = tk.StringVar(value="8080")
|
||
self.user_frontend_port = tk.StringVar(value="8082")
|
||
self.admin_frontend_port = tk.StringVar(value="8081")
|
||
self.mysql_user = tk.StringVar(value="root")
|
||
self.mysql_password = tk.StringVar()
|
||
self.mysql_database = tk.StringVar()
|
||
self.output_dir = tk.StringVar()
|
||
self.jdk_version = tk.StringVar(value="8")
|
||
self.jdk_reason = tk.StringVar(value="默认推荐 JDK 8,扫描项目后会自动调整。")
|
||
|
||
self.tool_paths = load_tool_paths()
|
||
self.tool_path_vars: dict[str, tk.StringVar] = {
|
||
name: tk.StringVar(value=str(self.tool_paths.get(name, "")))
|
||
for name in ("MySQL", "JDK", "Maven", "Gradle", "Node.js", "npm", "Git")
|
||
}
|
||
|
||
self.scan_result: ScanResult | None = None
|
||
self.environment_results: list[EnvironmentCheck] = []
|
||
self.ui_queue: queue.Queue[tuple[str, object]] = queue.Queue()
|
||
self.sidebar_buttons: dict[str, tk.Button] = {}
|
||
self.tool_path_entries: dict[str, ttk.Entry] = {}
|
||
self.deployment_running = False
|
||
self.deploy_buttons: list[ttk.Button] = []
|
||
|
||
self._configure_style()
|
||
self._build_layout()
|
||
self.after(120, self._drain_queue)
|
||
|
||
def _configure_style(self) -> None:
|
||
self.configure(bg="#f5f7fb")
|
||
self.style = ttk.Style(self)
|
||
self.style.theme_use("clam")
|
||
self.style.configure("TFrame", background="#f5f7fb")
|
||
self.style.configure("Sidebar.TFrame", background="#172033")
|
||
self.style.configure("Panel.TFrame", background="#ffffff", relief="flat")
|
||
self.style.configure("TLabel", background="#f5f7fb", foreground="#243043", font=("Microsoft YaHei UI", 10))
|
||
self.style.configure("Sidebar.TLabel", background="#172033", foreground="#dce6f7", font=("Microsoft YaHei UI", 10))
|
||
self.style.configure("Title.TLabel", background="#f5f7fb", foreground="#172033", font=("Microsoft YaHei UI", 18, "bold"))
|
||
self.style.configure("CardTitle.TLabel", background="#ffffff", foreground="#172033", font=("Microsoft YaHei UI", 11, "bold"))
|
||
self.style.configure("CardValue.TLabel", background="#ffffff", foreground="#2563eb", font=("Microsoft YaHei UI", 12, "bold"))
|
||
self.style.configure("Muted.TLabel", background="#ffffff", foreground="#667085", font=("Microsoft YaHei UI", 9))
|
||
self.style.configure("Primary.TButton", font=("Microsoft YaHei UI", 10, "bold"), padding=(14, 8))
|
||
self.style.configure("TButton", font=("Microsoft YaHei UI", 10), padding=(10, 6))
|
||
self.style.configure("TEntry", padding=6)
|
||
|
||
def _build_layout(self) -> None:
|
||
shell = ttk.Frame(self)
|
||
shell.pack(fill=tk.BOTH, expand=True)
|
||
|
||
sidebar = ttk.Frame(shell, style="Sidebar.TFrame", width=190)
|
||
sidebar.pack(side=tk.LEFT, fill=tk.Y)
|
||
sidebar.pack_propagate(False)
|
||
ttk.Label(sidebar, text=TEXT["workbench"], style="Sidebar.TLabel", font=("Microsoft YaHei UI", 16, "bold")).pack(
|
||
anchor=tk.W, padx=22, pady=(26, 22)
|
||
)
|
||
for item in SIDEBAR_ITEMS:
|
||
button = tk.Button(
|
||
sidebar,
|
||
text=item,
|
||
command=lambda name=item: self.activate_sidebar_item(name),
|
||
anchor=tk.W,
|
||
relief=tk.FLAT,
|
||
bd=0,
|
||
cursor="hand2",
|
||
bg="#172033",
|
||
activebackground="#22314d",
|
||
fg="#dce6f7",
|
||
activeforeground="#ffffff",
|
||
font=("Microsoft YaHei UI", 10),
|
||
padx=20,
|
||
pady=8,
|
||
)
|
||
button.pack(fill=tk.X, padx=6, pady=1)
|
||
self.sidebar_buttons[item] = button
|
||
|
||
content = ttk.Frame(shell)
|
||
content.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=24, pady=22)
|
||
self._build_header(content)
|
||
self._build_dashboard(content)
|
||
self._build_middle(content)
|
||
self._build_logs(content)
|
||
|
||
def _build_header(self, parent: ttk.Frame) -> None:
|
||
ttk.Label(parent, text="Spring Boot + Vue 自动化部署", style="Title.TLabel").pack(anchor=tk.W)
|
||
action_bar = ttk.Frame(parent)
|
||
action_bar.pack(fill=tk.X, pady=(14, 16))
|
||
self.project_dir_entry = ttk.Entry(action_bar, textvariable=self.project_dir)
|
||
self.project_dir_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
|
||
ttk.Button(action_bar, text=TEXT["select_dir"], command=self.choose_folder).pack(side=tk.LEFT, padx=4)
|
||
ttk.Button(action_bar, text=TEXT["scan"], style="Primary.TButton", command=self.scan_folder).pack(side=tk.LEFT, padx=4)
|
||
ttk.Button(action_bar, text=TEXT["check_env"], command=self.check_environment_async).pack(side=tk.LEFT, padx=4)
|
||
ttk.Button(action_bar, text="\u5b89\u88c5\u73af\u5883", style="Primary.TButton", command=self.show_environment_installer).pack(side=tk.LEFT, padx=4)
|
||
self.deploy_button = ttk.Button(
|
||
action_bar, text=TEXT["deploy"], style="Primary.TButton", command=self.run_deploy_plan_async
|
||
)
|
||
self.deploy_button.pack(side=tk.LEFT, padx=4)
|
||
self.deploy_buttons.append(self.deploy_button)
|
||
|
||
def _build_dashboard(self, parent: ttk.Frame) -> None:
|
||
cards = ttk.Frame(parent)
|
||
cards.pack(fill=tk.X, pady=(0, 16))
|
||
self.card_labels: dict[str, ttk.Label] = {}
|
||
for key, title in (("backend", "后端"), ("frontend", "前端"), ("sql", "SQL 文件"), ("env", "环境")):
|
||
card = ttk.Frame(cards, style="Panel.TFrame", padding=16)
|
||
card.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 12))
|
||
ttk.Label(card, text=title, style="CardTitle.TLabel").pack(anchor=tk.W)
|
||
value = ttk.Label(card, text="未扫描", style="CardValue.TLabel")
|
||
value.pack(anchor=tk.W, pady=(9, 3))
|
||
ttk.Label(card, text="等待操作", style="Muted.TLabel").pack(anchor=tk.W)
|
||
self.card_labels[key] = value
|
||
|
||
def _build_middle(self, parent: ttk.Frame) -> None:
|
||
middle = ttk.Frame(parent)
|
||
middle.pack(fill=tk.BOTH, expand=True)
|
||
|
||
left = ttk.Frame(middle, style="Panel.TFrame", padding=16)
|
||
left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 14))
|
||
ttk.Label(left, text=TEXT["results"], style="CardTitle.TLabel").pack(anchor=tk.W)
|
||
self.result_text = tk.Text(left, height=16, wrap=tk.WORD, relief=tk.FLAT, bg="#ffffff", fg="#243043", font=("Consolas", 10))
|
||
self.result_text.pack(fill=tk.BOTH, expand=True, pady=(10, 0))
|
||
|
||
right = ttk.Frame(middle, style="Panel.TFrame", padding=16, width=420)
|
||
right.pack(side=tk.LEFT, fill=tk.Y)
|
||
right.pack_propagate(False)
|
||
|
||
ttk.Label(right, text=TEXT["config"], style="CardTitle.TLabel").pack(anchor=tk.W)
|
||
self.mysql_user_widget = self._add_field(right, "MySQL 用户", self.mysql_user)
|
||
self.mysql_password_widget = self._add_field(right, "MySQL 密码", self.mysql_password, show="*")
|
||
self.database_widget = self._add_field(right, "数据库名", self.mysql_database)
|
||
ttk.Label(
|
||
right,
|
||
text="服务端口使用默认值:8080 / 8082 / 8081",
|
||
style="Muted.TLabel",
|
||
).pack(anchor=tk.W, pady=(12, 0))
|
||
|
||
def _build_jdk_install_section(self, parent: ttk.Frame) -> None:
|
||
ttk.Label(parent, text="JDK 推荐与安装", style="CardTitle.TLabel").pack(anchor=tk.W)
|
||
row = ttk.Frame(parent, style="Panel.TFrame")
|
||
row.pack(fill=tk.X, pady=(8, 4))
|
||
ttk.Label(row, text="版本", style="Muted.TLabel").pack(side=tk.LEFT, padx=(0, 8))
|
||
ttk.Combobox(row, textvariable=self.jdk_version, values=("8", "11", "17", "21"), state="readonly", width=8).pack(side=tk.LEFT)
|
||
ttk.Button(row, text="安装 JDK", command=self.install_jdk_async).pack(side=tk.LEFT, padx=(10, 0))
|
||
ttk.Button(row, text="查看命令", command=self.preview_jdk_install_command).pack(side=tk.LEFT, padx=(6, 0))
|
||
ttk.Button(row, text="加入 PATH", command=lambda: self.add_tool_to_path("JDK")).pack(side=tk.LEFT, padx=(6, 0))
|
||
ttk.Label(parent, textvariable=self.jdk_reason, style="Muted.TLabel", wraplength=360).pack(anchor=tk.W, pady=(2, 0))
|
||
|
||
def _build_logs(self, parent: ttk.Frame) -> None:
|
||
log_panel = ttk.Frame(parent, style="Panel.TFrame", padding=14)
|
||
log_panel.pack(fill=tk.BOTH, pady=(16, 0))
|
||
ttk.Label(log_panel, text=TEXT["logs"], style="CardTitle.TLabel").pack(anchor=tk.W)
|
||
self.log_text = tk.Text(log_panel, height=9, wrap=tk.WORD, relief=tk.FLAT, bg="#111827", fg="#d1fadf", insertbackground="#d1fadf", font=("Consolas", 10))
|
||
self.log_text.pack(fill=tk.BOTH, expand=True, pady=(10, 0))
|
||
|
||
def _add_field(self, parent: ttk.Frame, label: str, variable: tk.StringVar, show: str | None = None) -> ttk.Entry:
|
||
ttk.Label(parent, text=label, style="Muted.TLabel").pack(anchor=tk.W, pady=(10, 4))
|
||
entry = ttk.Entry(parent, textvariable=variable, show=show)
|
||
entry.pack(fill=tk.X)
|
||
return entry
|
||
|
||
def _add_tool_path_row(self, parent: ttk.Frame, name: str) -> None:
|
||
ttk.Label(parent, text=name, style="Muted.TLabel").pack(anchor=tk.W, pady=(8, 3))
|
||
row = ttk.Frame(parent, style="Panel.TFrame")
|
||
row.pack(fill=tk.X)
|
||
entry = ttk.Entry(row, textvariable=self.tool_path_vars[name])
|
||
entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 6))
|
||
self.tool_path_entries[name] = entry
|
||
ttk.Button(row, text="选择", command=lambda tool=name: self.choose_tool_path(tool)).pack(side=tk.LEFT)
|
||
if name not in {"JDK", "npm"}:
|
||
ttk.Button(row, text="安装", command=lambda tool=name: self.install_tool_async(tool)).pack(side=tk.LEFT, padx=(6, 0))
|
||
ttk.Button(row, text="PATH", command=lambda tool=name: self.add_tool_to_path(tool)).pack(side=tk.LEFT, padx=(6, 0))
|
||
|
||
def show_environment_installer(self) -> None:
|
||
dialog = tk.Toplevel(self)
|
||
dialog.title("\u5b89\u88c5\u73af\u5883")
|
||
dialog.geometry("680x420")
|
||
dialog.minsize(620, 360)
|
||
dialog.configure(bg="#f5f7fb")
|
||
dialog.transient(self)
|
||
|
||
panel = ttk.Frame(dialog, style="Panel.TFrame", padding=18)
|
||
panel.pack(fill=tk.BOTH, expand=True, padx=16, pady=16)
|
||
ttk.Label(panel, text="\u5b89\u88c5\u73af\u5883", style="CardTitle.TLabel").pack(anchor=tk.W)
|
||
ttk.Label(
|
||
panel,
|
||
text="\u5b89\u88c5\u524d\u4f1a\u5f39\u7a97\u786e\u8ba4 winget \u547d\u4ee4\uff0c\u5b89\u88c5\u5b8c\u6210\u540e\u4f1a\u81ea\u52a8\u91cd\u65b0\u68c0\u6d4b\u73af\u5883\u3002",
|
||
style="Muted.TLabel",
|
||
).pack(anchor=tk.W, pady=(4, 12))
|
||
|
||
jdk_row = ttk.Frame(panel, style="Panel.TFrame")
|
||
jdk_row.pack(fill=tk.X, pady=5)
|
||
ttk.Label(jdk_row, text="JDK", width=12, style="CardTitle.TLabel").pack(side=tk.LEFT)
|
||
ttk.Combobox(jdk_row, textvariable=self.jdk_version, values=("8", "11", "17", "21"), state="readonly", width=8).pack(side=tk.LEFT)
|
||
ttk.Button(jdk_row, text="\u5b89\u88c5", command=self.install_jdk_async).pack(side=tk.LEFT, padx=(8, 0))
|
||
ttk.Button(jdk_row, text="\u52a0\u5165 PATH", command=lambda: self.add_tool_to_path("JDK")).pack(side=tk.LEFT, padx=(8, 0))
|
||
ttk.Label(jdk_row, textvariable=self.jdk_reason, style="Muted.TLabel").pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(10, 0))
|
||
|
||
for name in ("Maven", "Gradle", "Node.js", "Git", "MySQL"):
|
||
row = ttk.Frame(panel, style="Panel.TFrame")
|
||
row.pack(fill=tk.X, pady=5)
|
||
ttk.Label(row, text=name, width=12, style="CardTitle.TLabel").pack(side=tk.LEFT)
|
||
ttk.Button(row, text="\u5b89\u88c5", command=lambda tool=name: self.install_tool_async(tool)).pack(side=tk.LEFT)
|
||
ttk.Button(row, text="\u9009\u62e9\u8def\u5f84", command=lambda tool=name: self.choose_tool_path(tool)).pack(side=tk.LEFT, padx=(8, 0))
|
||
ttk.Button(row, text="\u52a0\u5165 PATH", command=lambda tool=name: self.add_tool_to_path(tool)).pack(side=tk.LEFT, padx=(8, 0))
|
||
|
||
ttk.Separator(panel).pack(fill=tk.X, pady=14)
|
||
ttk.Button(panel, text="\u68c0\u6d4b\u73af\u5883", command=self.check_environment_async).pack(side=tk.LEFT)
|
||
ttk.Button(panel, text="\u5173\u95ed", command=dialog.destroy).pack(side=tk.RIGHT)
|
||
|
||
def activate_sidebar_item(self, item: str) -> None:
|
||
for name, button in self.__dict__.get("sidebar_buttons", {}).items():
|
||
button.configure(bg="#22314d" if name == item else "#172033", fg="#ffffff" if name == item else "#dce6f7")
|
||
|
||
if item == "项目识别":
|
||
self._focus_widget(self.__dict__.get("project_dir_entry"))
|
||
elif item == "环境检测":
|
||
self.check_environment_async()
|
||
elif item == "工具路径":
|
||
entries = self.__dict__.get("tool_path_entries", {})
|
||
first_entry = next(iter(entries.values()), None)
|
||
self._focus_widget(first_entry)
|
||
elif item == "数据库":
|
||
self._focus_widget(self.__dict__.get("database_widget"))
|
||
elif item == "部署任务":
|
||
self._focus_widget(self.__dict__.get("deploy_button"))
|
||
elif item == "运行日志":
|
||
log_text = self.__dict__.get("log_text")
|
||
self._focus_widget(log_text)
|
||
if log_text is not None:
|
||
log_text.see(tk.END)
|
||
|
||
def _focus_widget(self, widget: object | None) -> None:
|
||
if widget is not None:
|
||
widget.focus_set()
|
||
|
||
def choose_folder(self) -> None:
|
||
folder = filedialog.askdirectory(title="选择前后端项目根目录")
|
||
if folder:
|
||
self.project_dir.set(folder)
|
||
if not self.output_dir.get():
|
||
self.output_dir.set(str(Path(folder) / "deploy"))
|
||
|
||
def choose_output_dir(self) -> None:
|
||
folder = filedialog.askdirectory(title="选择部署输出目录")
|
||
if folder:
|
||
self.output_dir.set(folder)
|
||
|
||
def choose_tool_path(self, name: str) -> None:
|
||
file_path = filedialog.askopenfilename(title=f"选择 {name} 可执行文件", filetypes=(("可执行文件", "*.exe *.cmd *.bat"), ("所有文件", "*.*")))
|
||
if file_path:
|
||
self.tool_path_vars[name].set(file_path)
|
||
self.save_tool_path_settings(silent=True)
|
||
|
||
def save_tool_path_settings(self, silent: bool = False) -> None:
|
||
paths = {name: value.get().strip() for name, value in self.tool_path_vars.items() if value.get().strip()}
|
||
save_tool_paths(paths)
|
||
self.tool_paths = load_tool_paths()
|
||
if not silent:
|
||
self._append_log("工具路径已保存。")
|
||
|
||
def scan_folder(self) -> None:
|
||
if not self.project_dir.get().strip():
|
||
messagebox.showwarning("需要目录", "请先选择或输入项目文件夹。")
|
||
return
|
||
try:
|
||
self.scan_result = scan_project(self.project_dir.get().strip())
|
||
except Exception as exc:
|
||
messagebox.showerror("扫描失败", str(exc))
|
||
return
|
||
database_name = database_name_from_sql_files(self.scan_result.sql_files)
|
||
if database_name:
|
||
self.mysql_database.set(database_name)
|
||
self._append_log(f"已根据 SQL 文件自动填写数据库名: {database_name}")
|
||
self._apply_jdk_recommendation()
|
||
self._render_scan_result()
|
||
self._append_log("项目扫描完成。")
|
||
|
||
def _apply_jdk_recommendation(self) -> None:
|
||
recommendation = recommend_jdk_version(self.scan_result)
|
||
self.jdk_version.set(recommendation.version)
|
||
self.jdk_reason.set(recommendation.reason)
|
||
self._append_log(f"JDK 推荐: {recommendation.reason}")
|
||
|
||
def check_environment_async(self) -> None:
|
||
self.save_tool_path_settings(silent=True)
|
||
manual_paths = load_tool_paths()
|
||
self._append_log("开始检测本机环境,会同时检查 PATH、手动路径和常见安装目录...")
|
||
threading.Thread(target=lambda: self.ui_queue.put(("env", check_environment(manual_paths=manual_paths))), daemon=True).start()
|
||
|
||
def preview_jdk_install_command(self) -> None:
|
||
command = build_install_command("JDK", version=self.jdk_version.get())
|
||
self._append_log(f"JDK {self.jdk_version.get()} 安装命令: {' '.join(command)}")
|
||
|
||
def install_jdk_async(self) -> None:
|
||
command = build_install_command("JDK", version=self.jdk_version.get())
|
||
if not messagebox.askyesno(
|
||
"确认安装 JDK",
|
||
"即将通过 winget 安装 JDK。\n\n"
|
||
f"版本: JDK {self.jdk_version.get()}\n"
|
||
f"命令: {' '.join(command)}\n\n"
|
||
"安装可能需要管理员权限,并可能修改 PATH。是否继续?",
|
||
):
|
||
return
|
||
self._append_log(f"开始安装 JDK {self.jdk_version.get()}...")
|
||
threading.Thread(target=lambda: self.ui_queue.put(("install_done", ("JDK", run_install_command(command, self._thread_log)))), daemon=True).start()
|
||
|
||
def install_tool_async(self, tool_name: str) -> None:
|
||
try:
|
||
command = build_install_command(tool_name)
|
||
except ValueError as exc:
|
||
messagebox.showwarning("暂不支持自动安装", str(exc))
|
||
return
|
||
if not messagebox.askyesno(
|
||
f"确认安装 {tool_name}",
|
||
f"即将通过 winget 安装 {tool_name}。\n\n"
|
||
f"命令: {' '.join(command)}\n\n"
|
||
"安装可能需要管理员权限,并可能修改 PATH。是否继续?",
|
||
):
|
||
return
|
||
self._append_log(f"开始安装 {tool_name}...")
|
||
threading.Thread(
|
||
target=lambda: self.ui_queue.put(("install_done", (tool_name, run_install_command(command, self._thread_log)))),
|
||
daemon=True,
|
||
).start()
|
||
|
||
def add_tool_to_path(self, tool_name: str) -> None:
|
||
result = self._find_environment_result(tool_name)
|
||
executable = Path(result.command[0]) if result and Path(result.command[0]).exists() else None
|
||
configured = self.tool_path_vars.get(tool_name)
|
||
if executable is None and configured and configured.get().strip() and Path(configured.get().strip()).exists():
|
||
executable = Path(configured.get().strip())
|
||
if executable is None:
|
||
messagebox.showwarning("未找到可执行文件", f"请先检测环境,或手动选择 {tool_name} 的可执行文件。")
|
||
return
|
||
|
||
update = build_environment_update(tool_name, executable)
|
||
env_text = "\n".join(f"{name}={value}" for name, value in update.env_vars.items()) or "无额外环境变量"
|
||
if not messagebox.askyesno(
|
||
"确认加入 PATH",
|
||
f"将写入当前用户环境变量,不修改系统级 PATH。\n\n"
|
||
f"工具: {tool_name}\n"
|
||
f"PATH 目录: {update.path_entries[0]}\n"
|
||
f"{env_text}\n\n"
|
||
"修改后新打开的终端会生效。是否继续?",
|
||
):
|
||
return
|
||
try:
|
||
apply_user_environment(update)
|
||
except Exception as exc:
|
||
messagebox.showerror("写入失败", str(exc))
|
||
return
|
||
self._append_log(f"{tool_name} 已加入当前用户 PATH。")
|
||
self.check_environment_async()
|
||
|
||
def add_all_available_tools_to_path(self) -> None:
|
||
available = [
|
||
item.name
|
||
for item in self.environment_results
|
||
if item.installed and Path(item.command[0]).exists() and item.source in {"手动指定", "自动发现"}
|
||
]
|
||
if not available:
|
||
messagebox.showinfo("没有可加入项", "请先检测环境,或手动指定工具路径。")
|
||
return
|
||
if not messagebox.askyesno("确认加入 PATH", f"将这些工具加入当前用户 PATH:\n{', '.join(available)}\n\n是否继续?"):
|
||
return
|
||
for name in available:
|
||
result = self._find_environment_result(name)
|
||
if result:
|
||
apply_user_environment(build_environment_update(name, Path(result.command[0])))
|
||
self._append_log(f"{name} 已加入当前用户 PATH。")
|
||
self.check_environment_async()
|
||
|
||
def _find_environment_result(self, tool_name: str) -> EnvironmentCheck | None:
|
||
for item in self.environment_results:
|
||
if item.name == tool_name:
|
||
return item
|
||
return None
|
||
|
||
def _set_deployment_running(self, running: bool) -> None:
|
||
self.deployment_running = running
|
||
state = tk.DISABLED if running else tk.NORMAL
|
||
for button in self.deploy_buttons:
|
||
button.configure(state=state)
|
||
|
||
def _run_deployment_worker(self, plan) -> None:
|
||
exit_code = 1
|
||
try:
|
||
exit_code = run_steps(plan, self._thread_log)
|
||
except Exception as exc:
|
||
self.ui_queue.put(("log", f"部署线程异常: {exc}"))
|
||
finally:
|
||
self.ui_queue.put(("deploy_done", exit_code))
|
||
|
||
def run_deploy_plan_async(self) -> None:
|
||
if self.deployment_running:
|
||
self._append_log("部署任务正在执行,请等待当前任务结束。")
|
||
return
|
||
plan = self._current_plan()
|
||
if not plan:
|
||
return
|
||
if not messagebox.askyesno("确认执行", "将开始自动执行本地部署:构建前后端、复制前端产物、导入 SQL,并启动后端、用户端和管理端服务。请确认继续。"):
|
||
return
|
||
self._set_deployment_running(True)
|
||
worker = threading.Thread(target=self._run_deployment_worker, args=(plan,), daemon=True)
|
||
try:
|
||
worker.start()
|
||
except Exception as exc:
|
||
self._set_deployment_running(False)
|
||
self._append_log(f"无法启动部署线程: {exc}")
|
||
|
||
def _current_plan(self):
|
||
if self.scan_result is None:
|
||
messagebox.showwarning("需要扫描", "请先扫描项目。")
|
||
return []
|
||
config = DeploymentConfig(
|
||
mysql_user=self.mysql_user.get().strip() or "root",
|
||
mysql_password=self.mysql_password.get(),
|
||
mysql_database=self.mysql_database.get().strip(),
|
||
backend_port="8080",
|
||
user_frontend_port="8082",
|
||
admin_frontend_port="8081",
|
||
output_dir=None,
|
||
tool_paths=load_tool_paths(),
|
||
)
|
||
return build_deployment_plan(self.scan_result, config)
|
||
|
||
def _render_scan_result(self) -> None:
|
||
assert self.scan_result is not None
|
||
scan = self.scan_result
|
||
self.card_labels["backend"].configure(text="已识别" if scan.backend else "未找到")
|
||
self.card_labels["frontend"].configure(text=f"{len(scan.detected_frontends)} 个" if scan.detected_frontends else "未找到")
|
||
self.card_labels["sql"].configure(text=f"{len(scan.sql_files)} 个")
|
||
|
||
lines = [f"根目录: {scan.root}", ""]
|
||
lines.append(f"后端: {scan.backend.path if scan.backend else '未识别'}")
|
||
if scan.backend:
|
||
lines.append(f" 依据: {scan.backend.detail}")
|
||
if scan.backend.metadata:
|
||
lines.append(" 版本线索:")
|
||
for key, value in scan.backend.metadata.items():
|
||
lines.append(f" {key}: {value}")
|
||
lines.append(f"前端: {len(scan.detected_frontends)} 个")
|
||
for frontend in scan.detected_frontends:
|
||
display_name = frontend.metadata.get("display_name", frontend.path.name)
|
||
port = frontend.metadata.get("port", "未配置")
|
||
lines.append(f" {display_name}: {frontend.path}")
|
||
lines.append(f" 端口: {port},依据: {frontend.detail}")
|
||
lines.append(f"JDK 推荐: JDK {self.jdk_version.get()} - {self.jdk_reason.get()}")
|
||
lines.append("SQL:")
|
||
lines.extend(f" - {path}" for path in scan.sql_files[:20])
|
||
if len(scan.sql_files) > 20:
|
||
lines.append(f" ... 还有 {len(scan.sql_files) - 20} 个")
|
||
self._set_result_text("\n".join(lines))
|
||
|
||
def _render_environment(self, results: list[EnvironmentCheck]) -> None:
|
||
self.environment_results = results
|
||
installed = sum(1 for item in results if item.installed)
|
||
self.card_labels["env"].configure(text=f"{installed}/{len(results)} 可用")
|
||
lines = ["环境检测结果:", ""]
|
||
for item in results:
|
||
status = "OK" if item.installed else "MISS"
|
||
lines.append(f"[{status}] {item.name} ({item.source}): {item.message}")
|
||
lines.append(f" 命令: {' '.join(item.command)}")
|
||
if item.version:
|
||
lines.append(f" 版本: {item.version}")
|
||
self._set_result_text("\n".join(lines))
|
||
self._append_log("环境检测完成。")
|
||
|
||
def _set_result_text(self, value: str) -> None:
|
||
self.result_text.configure(state=tk.NORMAL)
|
||
self.result_text.delete("1.0", tk.END)
|
||
self.result_text.insert(tk.END, value)
|
||
|
||
def _append_log(self, value: str) -> None:
|
||
self.log_text.configure(state=tk.NORMAL)
|
||
self.log_text.insert(tk.END, value + "\n")
|
||
self.log_text.see(tk.END)
|
||
|
||
def _thread_log(self, value: str) -> None:
|
||
self.ui_queue.put(("log", value))
|
||
|
||
def _drain_queue(self) -> None:
|
||
while True:
|
||
try:
|
||
event, payload = self.ui_queue.get_nowait()
|
||
except queue.Empty:
|
||
break
|
||
if event == "env":
|
||
self._render_environment(payload) # type: ignore[arg-type]
|
||
elif event == "log":
|
||
self._append_log(str(payload))
|
||
elif event == "deploy_done":
|
||
self._set_deployment_running(False)
|
||
self._append_log(f"部署任务结束,退出码: {payload}")
|
||
elif event == "install_done":
|
||
tool_name, exit_code = payload # type: ignore[misc]
|
||
self._append_log(f"{tool_name} 安装命令结束,退出码: {exit_code}")
|
||
self.check_environment_async()
|
||
self.after(1800, lambda name=tool_name: self._prompt_add_tool_after_install(name))
|
||
self.after(120, self._drain_queue)
|
||
|
||
def _prompt_add_tool_after_install(self, tool_name: str) -> None:
|
||
result = self._find_environment_result(tool_name)
|
||
if result and result.installed and Path(result.command[0]).exists():
|
||
extra = " 并设置 JAVA_HOME" if tool_name == "JDK" else ""
|
||
if messagebox.askyesno(f"{tool_name} 已安装", f"检测到 {tool_name} 可执行文件,是否加入当前用户 PATH{extra}?"):
|
||
self.add_tool_to_path(tool_name)
|
||
|
||
|
||
def run_app() -> None:
|
||
app = DeployWorkbench()
|
||
app.mainloop()
|