Files
AutoDeploy/deploy_tool/gui.py
2026-07-14 16:31:23 +08:00

555 lines
28 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()