from __future__ import annotations import tkinter as tk from tkinter import ttk, messagebox, scrolledtext import threading import sys import os # 添加当前目录到路径 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from config_loader import get_config, Config from step1 import Step1FeastCoding from step2 import Step2Converter from editor_gui import open_editor from markdown_editor import ThemeManager from db_manager import get_db_manager, reset_db_manager, ProjectOrder from project_screenshot import ProjectScreenshotAutomation, ProjectConfig class YidaimaGUI: def __init__(self, root): self.root = root self.root.title("自动化工具") self.root.geometry("1200x900") self.root.resizable(True, True) # 设置样式 self.style = ttk.Style() self.style.configure("TButton", padding=6, relief="flat", background="#007aff") self.style.configure("TLabel", padding=6, font=("Microsoft YaHei", 10)) self.style.configure("TEntry", padding=6) # 设置 Treeview 样式,增加行高 self.style.configure("Treeview", rowheight=30) # 加载配置 self.config = get_config() # 初始化管理器 self.theme_manager = ThemeManager() self._init_db_manager() # 运行状态 self.is_running = False # 文章管理分页状态 self.current_page = 1 self.page_size = 20 self.total_items = 0 self.search_keyword = "" # 创建界面 self.create_widgets() def _init_db_manager(self): """初始化数据库管理器""" try: db_config = self.config.get("database", {}) self.db_manager = get_db_manager( host=db_config.get("host", "localhost"), database=db_config.get("database", "test"), user=db_config.get("user", "root"), password=db_config.get("password", "123456"), port=db_config.get("port", 3306) ) except Exception as e: print(f"[GUI] 初始化数据库管理器失败: {e}") self.db_manager = None def create_widgets(self): """创建主界面""" # 主框架 main_frame = ttk.Frame(self.root, padding="10") main_frame.pack(fill=tk.BOTH, expand=True) # 创建 Notebook (Tab 控件) self.notebook = ttk.Notebook(main_frame) self.notebook.pack(fill=tk.BOTH, expand=True) # === Tab 1: 发布微信公众号 === self.tab_publish = ttk.Frame(self.notebook, padding="10") self.notebook.add(self.tab_publish, text="发布微信公众号") self._create_publish_tab() # === Tab 2: 文章发布管理 === self.tab_manage = ttk.Frame(self.notebook, padding="10") self.notebook.add(self.tab_manage, text="文章发布管理") self._create_manage_tab() # === Tab 3: 项目运行截图 === self.tab_screenshot = ttk.Frame(self.notebook, padding="10") self.notebook.add(self.tab_screenshot, text="项目运行截图") self._create_screenshot_tab() # === Tab 4: 参数设置 === self.tab_settings = ttk.Frame(self.notebook, padding="10") self.notebook.add(self.tab_settings, text="参数设置") self._create_settings_tab() def _create_publish_tab(self): """创建发布微信公众号 Tab""" # 配置网格 self.tab_publish.columnconfigure(0, weight=1) self.tab_publish.rowconfigure(2, weight=1) # === 配置区域 === config_frame = ttk.LabelFrame(self.tab_publish, text="微信公众号配置", padding="10") config_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10)) config_frame.columnconfigure(1, weight=1) # 项目名称 ttk.Label(config_frame, text="项目名称:").grid(row=0, column=0, sticky=tk.W) self.project_name_var = tk.StringVar( value=self.config.get("step1.project_name", "【A173】基于Springboot + vue3实现的学生交流互助平台") ) self.project_name_entry = ttk.Entry(config_frame, textvariable=self.project_name_var, width=50) self.project_name_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(10, 0)) # CSS 样式方案 ttk.Label(config_frame, text="CSS 样式方案:").grid(row=1, column=0, sticky=tk.W, pady=(10, 0)) self.css_scheme_var = tk.StringVar() scheme_frame = ttk.Frame(config_frame) scheme_frame.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(10, 0)) scheme_frame.columnconfigure(0, weight=1) self.css_scheme_combo = ttk.Combobox( scheme_frame, values=self._get_css_scheme_names(), textvariable=self.css_scheme_var, state="readonly", width=50 ) self.css_scheme_combo.grid(row=0, column=0, sticky=(tk.W, tk.E)) self.css_scheme_combo.set("默认") # 刷新按钮 ttk.Button(scheme_frame, text="刷新", command=self.refresh_css_schemes, width=8).grid(row=0, column=1, padx=(5, 0)) # === 操作按钮区域 === button_frame = ttk.Frame(self.tab_publish) button_frame.grid(row=1, column=0, pady=(10, 10)) # 发布微信公众号按钮 self.publish_btn = ttk.Button( button_frame, text="发布微信公众号", command=self.run_full_flow, width=25 ) self.publish_btn.pack(side=tk.LEFT) # Markdown 编辑器按钮 ttk.Button( button_frame, text="Markdown 编辑器", command=self._open_markdown_editor, width=15 ).pack(side=tk.LEFT, padx=(10, 0)) # === 日志输出区域 === log_frame = ttk.LabelFrame(self.tab_publish, text="运行日志", padding="10") log_frame.grid(row=2, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(10, 0)) log_frame.columnconfigure(0, weight=1) log_frame.rowconfigure(0, weight=1) self.log_text = scrolledtext.ScrolledText( log_frame, wrap=tk.WORD, width=80, height=15, font=("Consolas", 9) ) self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) # 进度条 self.progress = ttk.Progressbar( self.tab_publish, mode='indeterminate', length=400 ) self.progress.grid(row=3, column=0, pady=(10, 0), sticky=(tk.W, tk.E)) # 状态标签 self.status_var = tk.StringVar(value="就绪") self.status_label = ttk.Label( self.tab_publish, textvariable=self.status_var, foreground="#666" ) self.status_label.grid(row=4, column=0, pady=(10, 0)) def _create_manage_tab(self): """创建文章发布管理 Tab""" # 配置网格 self.tab_manage.columnconfigure(0, weight=1) self.tab_manage.rowconfigure(2, weight=1) # === 搜索区域 === search_frame = ttk.Frame(self.tab_manage) search_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10)) search_frame.columnconfigure(1, weight=1) ttk.Label(search_frame, text="搜索名称:").grid(row=0, column=0, sticky=tk.W) self.search_var = tk.StringVar() self.search_entry = ttk.Entry(search_frame, textvariable=self.search_var, width=40) self.search_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(10, 0)) ttk.Button(search_frame, text="搜索", command=self._search_projects, width=10).grid(row=0, column=2, padx=(10, 0)) ttk.Button(search_frame, text="刷新", command=self._refresh_projects, width=10).grid(row=0, column=3, padx=(10, 0)) # === 数据表格区域 === table_frame = ttk.LabelFrame(self.tab_manage, text="项目列表", padding="5") table_frame.grid(row=1, column=0, rowspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10)) table_frame.columnconfigure(0, weight=1) table_frame.rowconfigure(0, weight=1) # 创建 Treeview columns = ("name", "name2", "paths", "zlzt", "sfxxm", "bianhao", "actions") self.tree = ttk.Treeview( table_frame, columns=columns, show="headings", height=35 ) # 设置列标题 self.tree.heading("name", text="项目名称") self.tree.heading("name2", text="名称2") self.tree.heading("paths", text="路径") self.tree.heading("zlzt", text="整理状态") self.tree.heading("sfxxm", text="是否新项目") self.tree.heading("bianhao", text="编号") self.tree.heading("actions", text="操作") # 设置列宽 self.tree.column("name", width=150, anchor=tk.W) self.tree.column("name2", width=100, anchor=tk.W) self.tree.column("paths", width=120, anchor=tk.W) self.tree.column("zlzt", width=80, anchor=tk.CENTER) self.tree.column("sfxxm", width=80, anchor=tk.CENTER) self.tree.column("bianhao", width=80, anchor=tk.CENTER) self.tree.column("actions", width=200, anchor=tk.CENTER) # 添加滚动条 scrollbar_y = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.tree.yview) scrollbar_x = ttk.Scrollbar(table_frame, orient=tk.HORIZONTAL, command=self.tree.xview) self.tree.configure(yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set) self.tree.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) scrollbar_y.grid(row=0, column=1, sticky=(tk.N, tk.S)) scrollbar_x.grid(row=1, column=0, sticky=(tk.W, tk.E)) # 绑定右键点击事件 self.tree.bind("", self._on_tree_right_click) # === 分页区域 === page_frame = ttk.Frame(self.tab_manage) page_frame.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=(5, 0)) self.page_info_var = tk.StringVar(value="第 1 页,共 0 页,共 0 条") ttk.Label(page_frame, textvariable=self.page_info_var).pack(side=tk.LEFT) ttk.Button(page_frame, text="上一页", command=self._prev_page).pack(side=tk.LEFT, padx=(20, 5)) ttk.Button(page_frame, text="下一页", command=self._next_page).pack(side=tk.LEFT, padx=(5, 0)) # 加载数据(延迟加载,避免初始化时出错) # self._load_projects() def _create_settings_tab(self): """创建参数设置 Tab""" # 创建 Canvas 和 Scrollbar 实现滚动 canvas = tk.Canvas(self.tab_settings) scrollbar = ttk.Scrollbar(self.tab_settings, orient="vertical", command=canvas.yview) self.settings_frame = ttk.Frame(canvas, padding="10") self.settings_frame.bind( "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) ) canvas.create_window((0, 0), window=self.settings_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") # 配置网格 self.settings_frame.columnconfigure(1, weight=1) row = 0 # === 网站账号配置 === website_frame = ttk.LabelFrame(self.settings_frame, text="网站账号配置", padding="10") website_frame.grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 15)) website_frame.columnconfigure(1, weight=1) ttk.Label(website_frame, text="网站用户名:").grid(row=0, column=0, sticky=tk.W) self.setting_username_var = tk.StringVar(value=self.config.get("step1.username", "wangpeng")) ttk.Entry(website_frame, textvariable=self.setting_username_var, width=40).grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(10, 0)) ttk.Label(website_frame, text="网站密码:").grid(row=1, column=0, sticky=tk.W, pady=(10, 0)) self.setting_password_var = tk.StringVar(value=self.config.get("step1.password", "")) ttk.Entry(website_frame, textvariable=self.setting_password_var, show="*", width=40).grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(10, 0)) row += 1 # === Chrome 配置 === chrome_frame = ttk.LabelFrame(self.settings_frame, text="Chrome 配置", padding="10") chrome_frame.grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 15)) chrome_frame.columnconfigure(1, weight=1) ttk.Label(chrome_frame, text="Chrome 路径:").grid(row=0, column=0, sticky=tk.W) self.setting_chrome_var = tk.StringVar(value=self.config.get("chrome.path", r"C:\Program Files\Google\Chrome\Application\chrome.exe")) ttk.Entry(chrome_frame, textvariable=self.setting_chrome_var, width=50).grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(10, 0)) row += 1 # === 微信公众号配置 === wechat_frame = ttk.LabelFrame(self.settings_frame, text="微信公众号配置", padding="10") wechat_frame.grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 15)) wechat_frame.columnconfigure(1, weight=1) ttk.Label(wechat_frame, text="AppID:").grid(row=0, column=0, sticky=tk.W) self.setting_appid_var = tk.StringVar(value=self.config.get("wechat.appid", "")) ttk.Entry(wechat_frame, textvariable=self.setting_appid_var, width=40).grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(10, 0)) ttk.Label(wechat_frame, text="AppSecret:").grid(row=1, column=0, sticky=tk.W, pady=(10, 0)) self.setting_appsecret_var = tk.StringVar(value=self.config.get("wechat.appsecret", "")) ttk.Entry(wechat_frame, textvariable=self.setting_appsecret_var, show="*", width=40).grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(10, 0)) row += 1 # === 数据库配置 === db_frame = ttk.LabelFrame(self.settings_frame, text="数据库配置", padding="10") db_frame.grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 15)) db_frame.columnconfigure(1, weight=1) ttk.Label(db_frame, text="主机地址:").grid(row=0, column=0, sticky=tk.W) self.setting_db_host_var = tk.StringVar(value=self.config.get("database.host", "localhost")) ttk.Entry(db_frame, textvariable=self.setting_db_host_var, width=30).grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(10, 0)) ttk.Label(db_frame, text="端口:").grid(row=1, column=0, sticky=tk.W, pady=(10, 0)) self.setting_db_port_var = tk.StringVar(value=str(self.config.get("database.port", "3306"))) ttk.Entry(db_frame, textvariable=self.setting_db_port_var, width=30).grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(10, 0)) ttk.Label(db_frame, text="数据库名:").grid(row=2, column=0, sticky=tk.W, pady=(10, 0)) self.setting_db_name_var = tk.StringVar(value=self.config.get("database.database", "test")) ttk.Entry(db_frame, textvariable=self.setting_db_name_var, width=30).grid(row=2, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(10, 0)) ttk.Label(db_frame, text="用户名:").grid(row=3, column=0, sticky=tk.W, pady=(10, 0)) self.setting_db_user_var = tk.StringVar(value=self.config.get("database.user", "root")) ttk.Entry(db_frame, textvariable=self.setting_db_user_var, width=30).grid(row=3, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(10, 0)) ttk.Label(db_frame, text="密码:").grid(row=4, column=0, sticky=tk.W, pady=(10, 0)) self.setting_db_pass_var = tk.StringVar(value=self.config.get("database.password", "123456")) ttk.Entry(db_frame, textvariable=self.setting_db_pass_var, show="*", width=30).grid(row=4, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(10, 0)) # 测试连接按钮 ttk.Button(db_frame, text="🧪 测试连接", command=self._test_db_connection).grid(row=5, column=1, sticky=tk.W, pady=(10, 0)) row += 1 # === 保存按钮 === btn_frame = ttk.Frame(self.settings_frame) btn_frame.grid(row=row, column=0, columnspan=2, pady=(20, 0)) ttk.Button(btn_frame, text="💾 保存配置", command=self._save_settings, width=20).pack(side=tk.LEFT, padx=(0, 10)) ttk.Button(btn_frame, text="🔄 重置", command=self._reset_settings, width=15).pack(side=tk.LEFT) # ==================== 发布公众号 Tab 方法 ==================== def _open_markdown_editor(self): """打开 Markdown 编辑器""" try: open_editor(parent=self.root) except Exception as e: messagebox.showerror("错误", f"打开 Markdown 编辑器失败: {e}") def log(self, message: str): """添加日志""" self.log_text.insert(tk.END, f"{message}\n") self.log_text.see(tk.END) self.root.update_idletasks() def set_running(self, running: bool): """设置运行状态""" self.is_running = running state = tk.DISABLED if running else tk.NORMAL self.publish_btn.config(state=state) if running: self.progress.start() self.status_var.set("运行中...") else: self.progress.stop() self.status_var.set("就绪") def run_full_flow(self): """运行完整流程""" if self.is_running: return thread = threading.Thread(target=self._run_full_flow_thread) thread.daemon = True thread.start() def _run_full_flow_thread(self): """在后台线程中运行完整流程""" try: self.set_running(True) self.root.after(0, lambda: self.log("=" * 50)) self.root.after(0, lambda: self.log("开始运行完整流程...")) self.root.after(0, lambda: self.log("=" * 50)) # 获取配置(从配置文件读取) chrome_path = self.config.get("chrome.path", "") username = self.config.get("step1.username", "") password = self.config.get("step1.password", "") project_name = self.project_name_var.get() # Step 1 self.root.after(0, lambda: self.log("\n[Step 1] 开始执行...")) # 定义日志回调函数,将 step1 的日志输出到 GUI def step1_log_callback(message: str): self.root.after(0, lambda msg=message: self.log(msg)) step1 = Step1FeastCoding(chrome_path, username, password, log_callback=step1_log_callback) step1.project_name = project_name # 设置项目名称 step1.run() self.root.after(0, lambda: self.log("[Step 1] 执行完成!")) # Step 2 self.root.after(0, lambda: self.log("\n[Step 2] 开始执行...")) # 定义日志回调函数,将 step2 的日志输出到 GUI def log_callback(message: str): self.root.after(0, lambda msg=message: self.log(msg)) # 获取 CSS 方案 ID(根据名称查找) scheme_name = self.css_scheme_var.get() css_scheme_id = self._get_scheme_id_by_name(scheme_name) step2 = Step2Converter(log_callback=log_callback, css_scheme_id=css_scheme_id) step2.run() self.root.after(0, lambda: self.log("[Step 2] 执行完成!")) self.root.after(0, lambda: self.log("\n" + "=" * 50)) self.root.after(0, lambda: self.log("完整流程执行成功!")) self.root.after(0, lambda: self.log("=" * 50)) self.root.after(0, lambda: messagebox.showinfo("成功", "完整流程执行成功!")) except Exception as e: error_msg = str(e) self.root.after(0, lambda: self.log(f"\n[ERROR] {error_msg}")) self.root.after(0, lambda: messagebox.showerror("错误", f"执行失败: {error_msg}")) finally: self.set_running(False) def _get_css_scheme_names(self): """获取 CSS 方案名称列表(只显示名称)""" schemes = self.theme_manager.get_all_css_schemes() return ["默认"] + [s.name for s in schemes] def _get_scheme_id_by_name(self, name: str): """根据名称获取方案 ID""" if name == "默认": return None schemes = self.theme_manager.get_all_css_schemes() for scheme in schemes: if scheme.name == name: return scheme.id return None def refresh_css_schemes(self): """刷新 CSS 方案列表""" # 重新加载配置 self.theme_manager = ThemeManager() # 更新下拉框 self.css_scheme_combo['values'] = self._get_css_scheme_names() messagebox.showinfo("提示", "CSS 方案列表已刷新!") # ==================== 文章管理 Tab 方法 ==================== def _load_projects(self): """加载项目列表""" if not self.db_manager: messagebox.showwarning("警告", "数据库未配置或连接失败") return try: projects, total = self.db_manager.get_projects( page=self.current_page, page_size=self.page_size, search_name=self.search_keyword if self.search_keyword else None ) self.total_items = total # 清空表格 for item in self.tree.get_children(): self.tree.delete(item) # 插入数据 for project in projects: self.tree.insert("", tk.END, values=( project.name or "", project.name2 or "", project.paths or "", project.zlzt or "", project.sfxxm or "", project.bianhao or "", "右键操作" )) # 更新分页信息 total_pages = (total + self.page_size - 1) // self.page_size if total > 0 else 1 self.page_info_var.set(f"第 {self.current_page} 页,共 {total_pages} 页,共 {total} 条") except Exception as e: messagebox.showerror("错误", f"加载数据失败: {e}") def _search_projects(self): """搜索项目""" try: self.search_keyword = self.search_var.get().strip() self.current_page = 1 self._load_projects() except Exception as e: messagebox.showerror("错误", f"搜索失败: {e}") def _refresh_projects(self): """刷新项目列表""" self.search_keyword = "" self.search_var.set("") self.current_page = 1 self._load_projects() def _prev_page(self): """上一页""" if self.current_page > 1: self.current_page -= 1 self._load_projects() def _next_page(self): """下一页""" total_pages = (self.total_items + self.page_size - 1) // self.page_size if self.total_items > 0 else 1 if self.current_page < total_pages: self.current_page += 1 self._load_projects() def _on_tree_right_click(self, event): """处理表格右键点击事件""" # 获取点击位置对应的项 item = self.tree.identify_row(event.y) if not item: return # 选中当前项 self.tree.selection_set(item) # 获取选中项的值 values = self.tree.item(item, "values") if not values: return name = values[0] name2 = values[1] current_zlzt = values[3] # 整理状态现在在第4列(索引3) # 创建操作菜单 menu = tk.Menu(self.root, tearoff=0) # 变更已整理状态选项 new_zlzt = "已整理" if current_zlzt != "已整理" else "未整理" menu.add_command( label=f"变更状态: {new_zlzt}", command=lambda: self._update_project_zlzt(name, new_zlzt) ) menu.add_separator() # 删除选项 menu.add_command( label="删除", command=lambda: self._delete_project(name) ) menu.add_separator() # 复制路径选项 menu.add_command( label="复制路径", command=lambda: self._copy_path(name) ) menu.add_separator() # 复制name2名称选项 menu.add_command( label="复制名称2", command=lambda: self._copy_name2(name2) ) # 显示菜单 menu.post(event.x_root, event.y_root) def _update_project_zlzt(self, name: str, zlzt: str): """更新项目整理状态""" if not self.db_manager: return if messagebox.askyesno("确认", f"确定将 '{name}' 标记为 {zlzt} 吗?"): if self.db_manager.update_zlzt(name, zlzt): messagebox.showinfo("成功", f"已更新为 {zlzt}") self._load_projects() else: messagebox.showerror("错误", "更新失败") def _delete_project(self, name: str): """删除项目""" if not self.db_manager: return if messagebox.askyesno("确认", f"确定删除 '{name}' 吗?\n此操作不可恢复!"): if self.db_manager.delete_project(name): messagebox.showinfo("成功", "删除成功") self._load_projects() else: messagebox.showerror("错误", "删除失败") def _copy_name2(self, name2: str): """复制name2名称到剪贴板""" if not name2: messagebox.showwarning("提示", "名称2为空,无法复制") return self.root.clipboard_clear() self.root.clipboard_append(name2) messagebox.showinfo("成功", f"已复制: {name2}") def _copy_path(self, name: str): """复制项目路径到剪贴板""" if not self.db_manager: messagebox.showwarning("提示", "数据库未连接,无法获取路径") return try: # 获取项目路径 project = self.db_manager.get_project_by_name(name) if project and project.paths: self.root.clipboard_clear() self.root.clipboard_append(project.paths) messagebox.showinfo("成功", f"已复制路径: {project.paths}") else: messagebox.showwarning("提示", "项目路径为空,无法复制") except Exception as e: messagebox.showerror("错误", f"复制路径失败: {e}") # ==================== 参数设置 Tab 方法 ==================== def _test_db_connection(self): """测试数据库连接""" def test_in_thread(): try: from db_manager import DatabaseManager host = self.setting_db_host_var.get() port_str = self.setting_db_port_var.get() port = int(port_str) if port_str else 3306 database = self.setting_db_name_var.get() user = self.setting_db_user_var.get() password = self.setting_db_pass_var.get() print(f"[TestDB] 测试连接: {host}:{port}/{database} as {user}") db = DatabaseManager(host, database, user, password, port) success, error = db.test_connection() if success: self.root.after(0, lambda: messagebox.showinfo("成功", "数据库连接成功!")) else: self.root.after(0, lambda: messagebox.showerror("失败", f"连接失败: {error}")) except Exception as e: print(f"[TestDB] 测试连接异常: {e}") self.root.after(0, lambda msg=str(e): messagebox.showerror("错误", f"测试连接出错: {msg}")) # 在新线程中运行,避免阻塞 GUI thread = threading.Thread(target=test_in_thread) thread.daemon = True thread.start() def _save_settings(self): """保存配置""" try: # 构建新配置 new_config = { "step1": { "url": "https://feast.yidaima.cn/login", "username": self.setting_username_var.get(), "password": self.setting_password_var.get(), "project_name": self.project_name_var.get(), "feast_button": "FeastCoding" }, "step2": { "url": "http://yidaima.cn:6005/" }, "chrome": { "path": self.setting_chrome_var.get() }, "wechat": { "appid": self.setting_appid_var.get(), "appsecret": self.setting_appsecret_var.get() }, "database": { "host": self.setting_db_host_var.get(), "port": int(self.setting_db_port_var.get() or 3306), "database": self.setting_db_name_var.get(), "user": self.setting_db_user_var.get(), "password": self.setting_db_pass_var.get() } } # 保存到文件 import yaml config_path = os.path.join(os.path.dirname(__file__), "config.yaml") with open(config_path, 'w', encoding='utf-8') as f: yaml.dump(new_config, f, allow_unicode=True, sort_keys=False) # 重新加载配置 global _config_instance _config_instance = None self.config = get_config() # 重置数据库管理器 reset_db_manager() self._init_db_manager() # 更新发布页面的值 self.username_var.set(self.config.get("step1.username", "")) self.password_var.set(self.config.get("step1.password", "")) self.chrome_path_var.set(self.config.get("chrome.path", "")) messagebox.showinfo("成功", "配置已保存!") except Exception as e: messagebox.showerror("错误", f"保存失败: {e}") def _reset_settings(self): """重置设置""" if messagebox.askyesno("确认", "确定要重置所有设置吗?"): self.setting_username_var.set(self.config.get("step1.username", "wangpeng")) self.setting_password_var.set(self.config.get("step1.password", "")) self.setting_chrome_var.set(self.config.get("chrome.path", r"C:\Program Files\Google\Chrome\Application\chrome.exe")) self.setting_appid_var.set(self.config.get("wechat.appid", "")) self.setting_appsecret_var.set(self.config.get("wechat.appsecret", "")) self.setting_db_host_var.set(self.config.get("database.host", "localhost")) self.setting_db_port_var.set(str(self.config.get("database.port", "3306"))) self.setting_db_name_var.set(self.config.get("database.database", "test")) self.setting_db_user_var.set(self.config.get("database.user", "root")) self.setting_db_pass_var.set(self.config.get("database.password", "123456")) # ==================== 项目运行截图 Tab 方法 ==================== def _create_screenshot_tab(self): """创建项目运行截图 Tab""" # 配置网格 self.tab_screenshot.columnconfigure(0, weight=1) self.tab_screenshot.rowconfigure(3, weight=1) # 获取项目截图配置 ps_config = self.config.get("project_screenshot", {}) # === 项目路径配置区域 === path_frame = ttk.LabelFrame(self.tab_screenshot, text="项目路径配置", padding="10") path_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10)) path_frame.columnconfigure(1, weight=1) # 项目路径 ttk.Label(path_frame, text="项目路径:").grid(row=0, column=0, sticky=tk.W) self.ps_project_path_var = tk.StringVar(value=ps_config.get("project_path", "")) ps_project_path_entry = ttk.Entry(path_frame, textvariable=self.ps_project_path_var, width=60) ps_project_path_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(10, 0)) ttk.Button(path_frame, text="浏览...", command=self._browse_project_path, width=10).grid(row=0, column=2, padx=(10, 0)) # 桌面路径 ttk.Label(path_frame, text="桌面路径:").grid(row=1, column=0, sticky=tk.W, pady=(10, 0)) self.ps_desktop_path_var = tk.StringVar(value=ps_config.get("desktop_path", r"C:\Users\南音\Desktop")) ps_desktop_path_entry = ttk.Entry(path_frame, textvariable=self.ps_desktop_path_var, width=60) ps_desktop_path_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(10, 0)) # bat 文件夹名称 ttk.Label(path_frame, text="bat文件夹:").grid(row=2, column=0, sticky=tk.W, pady=(10, 0)) self.ps_bat_folder_var = tk.StringVar(value=ps_config.get("bat_folder", r"C:\Users\南音\Desktop\yidaima\bat")) ps_bat_folder_entry = ttk.Entry(path_frame, textvariable=self.ps_bat_folder_var, width=60) ps_bat_folder_entry.grid(row=2, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(10, 0)) # 代码目标路径 ttk.Label(path_frame, text="代码目标路径:").grid(row=3, column=0, sticky=tk.W, pady=(10, 0)) self.ps_code_target_var = tk.StringVar(value=ps_config.get("code_target_path", r"D:\code")) ps_code_target_entry = ttk.Entry(path_frame, textvariable=self.ps_code_target_var, width=60) ps_code_target_entry.grid(row=3, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(10, 0)) # 是否显示 CMD 窗口 self.ps_show_cmd_var = tk.BooleanVar(value=ps_config.get("show_cmd_window", True)) ttk.Checkbutton(path_frame, text="显示CMD窗口", variable=self.ps_show_cmd_var).grid(row=4, column=1, sticky=tk.W, padx=(10, 0), pady=(10, 0)) # === 操作按钮区域 === button_frame = ttk.LabelFrame(self.tab_screenshot, text="操作按钮", padding="10") button_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(0, 10)) # 构建/安装按钮 install_frame = ttk.Frame(button_frame) install_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Button(install_frame, text="整理代码", command=self._ps_organize_code, width=15).pack(side=tk.LEFT, padx=(0, 5)) ttk.Button(install_frame, text="Maven构建后端", command=self._ps_install_server, width=15).pack(side=tk.LEFT, padx=(5, 5)) ttk.Button(install_frame, text="安装前台依赖", command=self._ps_install_front, width=15).pack(side=tk.LEFT, padx=(5, 5)) ttk.Button(install_frame, text="安装后台依赖", command=self._ps_install_admin, width=15).pack(side=tk.LEFT, padx=(5, 5)) ttk.Button(install_frame, text="导入SQL", command=self._ps_run_sql, width=15).pack(side=tk.LEFT, padx=(5, 0)) # 启动按钮 start_frame = ttk.Frame(button_frame) start_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Button(start_frame, text="启动后端服务", command=self._ps_start_server, width=15).pack(side=tk.LEFT, padx=(0, 5)) ttk.Button(start_frame, text="启动前台前端", command=self._ps_start_front, width=15).pack(side=tk.LEFT, padx=(5, 5)) ttk.Button(start_frame, text="启动后台前端", command=self._ps_start_admin, width=15).pack(side=tk.LEFT, padx=(5, 5)) ttk.Button(start_frame, text="更换图片", command=self._ps_replace_images, width=15).pack(side=tk.LEFT, padx=(5, 0)) # 截图按钮 screenshot_frame = ttk.Frame(button_frame) screenshot_frame.pack(fill=tk.X) ttk.Button(screenshot_frame, text="后台截图", command=self._ps_capture_admin, width=15).pack(side=tk.LEFT, padx=(0, 5)) ttk.Button(screenshot_frame, text="前台截图", command=self._ps_capture_front, width=15).pack(side=tk.LEFT, padx=(5, 5)) ttk.Button(screenshot_frame, text="收尾工作", command=self._ps_finalize_screenshots, width=15).pack(side=tk.LEFT, padx=(5, 5)) ttk.Button(screenshot_frame, text="一键完整流程", command=self._ps_run_full_flow, width=20).pack(side=tk.LEFT, padx=(5, 5)) ttk.Button(screenshot_frame, text="关闭所有CMD", command=self._ps_close_all_cmd, width=15).pack(side=tk.LEFT, padx=(5, 0)) # === 日志输出区域 === log_frame = ttk.LabelFrame(self.tab_screenshot, text="运行日志", padding="10") log_frame.grid(row=2, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(10, 0)) log_frame.columnconfigure(0, weight=1) log_frame.rowconfigure(0, weight=1) self.ps_log_text = scrolledtext.ScrolledText( log_frame, wrap=tk.WORD, width=80, height=15, font=("Consolas", 9) ) self.ps_log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) # 进度条 self.ps_progress = ttk.Progressbar( self.tab_screenshot, mode='indeterminate', length=400 ) self.ps_progress.grid(row=3, column=0, pady=(10, 0), sticky=(tk.W, tk.E)) # 状态标签 self.ps_status_var = tk.StringVar(value="就绪") self.ps_status_label = ttk.Label( self.tab_screenshot, textvariable=self.ps_status_var, foreground="#666" ) self.ps_status_label.grid(row=4, column=0, pady=(10, 0)) def _browse_project_path(self): """浏览项目路径""" from tkinter import filedialog path = filedialog.askdirectory(title="选择项目文件夹") if path: self.ps_project_path_var.set(path) def _ps_log(self, message: str): """添加项目截图日志""" self.ps_log_text.insert(tk.END, f"{message}\n") self.ps_log_text.see(tk.END) self.root.update_idletasks() def _get_ps_config(self) -> ProjectConfig: """获取项目截图配置""" ps_config = self.config.get("project_screenshot", {}) # 自动检测是否有前台前端 has_front = self._check_has_front(self.ps_project_path_var.get()) self._ps_log(f"自动检测前台前端: {'有' if has_front else '无'}") return ProjectConfig( project_path=self.ps_project_path_var.get(), desktop_path=self.ps_desktop_path_var.get(), has_front=has_front, install_server=ps_config.get("scripts", {}).get("install_server", "run_install_server.bat"), install_front=ps_config.get("scripts", {}).get("install_front", "run_install_front.bat"), install_admin=ps_config.get("scripts", {}).get("install_admin", "run_install_admin.bat"), run_server=ps_config.get("scripts", {}).get("run_server", "run_server.bat"), run_front=ps_config.get("scripts", {}).get("run_front", "run_front.bat"), run_admin=ps_config.get("scripts", {}).get("run_admin", "run_admin.bat"), run_sql=ps_config.get("scripts", {}).get("run_sql", "run_sql.bat"), bat_folder=self.ps_bat_folder_var.get(), sql_script_path=ps_config.get("sql", {}).get("script_path", "server/db/init.sql"), backend_url=ps_config.get("services", {}).get("backend", {}).get("url", "http://localhost:8080"), backend_startup_timeout=ps_config.get("services", {}).get("backend", {}).get("startup_timeout", 120), front_url=ps_config.get("services", {}).get("front", {}).get("url", "http://localhost:8082"), front_login_url=ps_config.get("services", {}).get("front", {}).get("login_url", "http://localhost:8082/#/login"), front_startup_timeout=ps_config.get("services", {}).get("front", {}).get("startup_timeout", 60), admin_url=ps_config.get("services", {}).get("admin", {}).get("url", "http://localhost:8081"), admin_login_url=ps_config.get("services", {}).get("admin", {}).get("login_url", "http://localhost:8081/#/login"), admin_startup_timeout=ps_config.get("services", {}).get("admin", {}).get("startup_timeout", 60), admin_username=ps_config.get("login", {}).get("admin", {}).get("username", "admin"), admin_password=ps_config.get("login", {}).get("admin", {}).get("password", "admin"), front_username=ps_config.get("login", {}).get("front", {}).get("username", "2"), front_password=ps_config.get("login", {}).get("front", {}).get("password", "123456"), screenshot_output_dir=ps_config.get("screenshot", {}).get("output_dir", "./screenshots"), screenshot_delay=ps_config.get("screenshot", {}).get("delay", 2000), admin_menu_selector=ps_config.get("screenshot", {}).get("menu_selectors", {}).get("admin", ".el-menu-item, .el-sub-menu__title"), front_menu_selector=ps_config.get("screenshot", {}).get("menu_selectors", {}).get("front", ".nav-item, .menu-item"), swiper_source_folder=ps_config.get("swiper_images", {}).get("source_folder", "bg_pic"), swiper_target_subpath=ps_config.get("swiper_images", {}).get("target_subpath", "server/src/main/resources/static/file"), swiper_target_names=ps_config.get("swiper_images", {}).get("target_names", ["swiperPicture1.jpg", "swiperPicture2.jpg", "swiperPicture3.jpg"]), swiper_count=ps_config.get("swiper_images", {}).get("count", 3), login_vue_subpath=ps_config.get("login_background", {}).get("vue_file_subpath", "front/manage_code/src/views/login.vue"), login_background_images=ps_config.get("login_background", {}).get("images", []), code_source_folder=ps_config.get("code_source_folder", "code"), code_target_path=self.ps_code_target_var.get(), show_cmd_window=self.ps_show_cmd_var.get() ) def _check_has_front(self, project_path: str) -> bool: """ 自动检测是否有前台前端 检查项目路径下的源码文件夹中是否包含 client_code 目录 Args: project_path: 项目路径 Returns: 是否有前台前端 """ try: code_dir = os.path.join(project_path, "源码") if not os.path.exists(code_dir): return False # 获取 code 文件夹下的第一个子目录 first_subdir = None for item in os.listdir(code_dir): item_path = os.path.join(code_dir, item) if os.path.isdir(item_path): first_subdir = item_path break if not first_subdir: return False # 遍历查找 client_code 目录 for root, dirs, files in os.walk(first_subdir): if 'client_code' in dirs: return True return False except Exception as e: print(f"检测前台前端失败: {str(e)}") return False def _ps_set_running(self, running: bool): """设置项目截图运行状态""" if running: self.ps_progress.start() self.ps_status_var.set("运行中...") else: self.ps_progress.stop() self.ps_status_var.set("就绪") def _ps_install_server(self): """Maven 构建后端""" def run(): try: self._ps_set_running(True) self._ps_log("=" * 50) self._ps_log("开始 Maven 构建后端...") config = self._get_ps_config() automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) success, msg = automation.install_server() if success: self._ps_log("Maven 构建完成!") else: self._ps_log(f"构建失败: {msg}") except Exception as e: self._ps_log(f"错误: {str(e)}") finally: self._ps_set_running(False) threading.Thread(target=run, daemon=True).start() def _ps_install_front(self): """安装前台前端依赖""" def run(): try: self._ps_set_running(True) self._ps_log("=" * 50) self._ps_log("开始安装前台前端依赖...") config = self._get_ps_config() automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) success, msg = automation.install_front() if success: self._ps_log("前台依赖安装完成!") else: self._ps_log(f"安装失败: {msg}") except Exception as e: self._ps_log(f"错误: {str(e)}") finally: self._ps_set_running(False) threading.Thread(target=run, daemon=True).start() def _ps_install_admin(self): """安装后台前端依赖""" def run(): try: self._ps_set_running(True) self._ps_log("=" * 50) self._ps_log("开始安装后台前端依赖...") config = self._get_ps_config() automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) success, msg = automation.install_admin() if success: self._ps_log("后台依赖安装完成!") else: self._ps_log(f"安装失败: {msg}") except Exception as e: self._ps_log(f"错误: {str(e)}") finally: self._ps_set_running(False) threading.Thread(target=run, daemon=True).start() def _ps_run_sql(self): """导入 SQL 脚本""" def run(): try: self._ps_set_running(True) self._ps_log("=" * 50) self._ps_log("开始导入 SQL...") config = self._get_ps_config() automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) success, msg = automation.run_sql_script() if success: self._ps_log("SQL 导入完成!") else: self._ps_log(f"导入失败: {msg}") except Exception as e: self._ps_log(f"错误: {str(e)}") finally: self._ps_set_running(False) threading.Thread(target=run, daemon=True).start() def _ps_start_server(self): """启动后端服务""" def run(): try: self._ps_set_running(True) self._ps_log("=" * 50) self._ps_log("启动后端服务...") config = self._get_ps_config() automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) automation.start_backend() if automation.wait_for_service(config.backend_url, config.backend_startup_timeout): self._ps_log("后端服务启动成功!") else: self._ps_log("后端服务启动超时,请手动检查") except Exception as e: self._ps_log(f"错误: {str(e)}") finally: self._ps_set_running(False) threading.Thread(target=run, daemon=True).start() def _ps_start_front(self): """启动前台前端""" def run(): try: self._ps_set_running(True) self._ps_log("=" * 50) self._ps_log("启动前台前端...") config = self._get_ps_config() automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) automation.start_front() if automation.wait_for_service(config.front_url, config.front_startup_timeout): self._ps_log("前台前端启动成功!") else: self._ps_log("前台前端启动超时,请手动检查") except Exception as e: self._ps_log(f"错误: {str(e)}") finally: self._ps_set_running(False) threading.Thread(target=run, daemon=True).start() def _ps_organize_code(self): """整理项目代码""" def run(): try: self._ps_set_running(True) self._ps_log("=" * 50) self._ps_log("开始整理项目代码...") config = self._get_ps_config() automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) sql_filename = automation.organize_project_code() if sql_filename: self._ps_log(f"代码整理完成!SQL文件名: {sql_filename}") else: self._ps_log("代码整理完成,但未获取到SQL文件名") except Exception as e: self._ps_log(f"错误: {str(e)}") finally: self._ps_set_running(False) threading.Thread(target=run, daemon=True).start() def _ps_start_admin(self): """启动后台前端""" def run(): try: self._ps_set_running(True) self._ps_log("=" * 50) self._ps_log("启动后台前端...") config = self._get_ps_config() automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) automation.start_admin() if automation.wait_for_service(config.admin_url, config.admin_startup_timeout): self._ps_log("后台前端启动成功!") else: self._ps_log("后台前端启动超时,请手动检查") except Exception as e: self._ps_log(f"错误: {str(e)}") finally: self._ps_set_running(False) threading.Thread(target=run, daemon=True).start() def _ps_replace_images(self): """更换图片""" def run(): try: self._ps_set_running(True) self._ps_log("=" * 50) self._ps_log("更换图片...") config = self._get_ps_config() automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) # 更换前台轮播图片 self._ps_log("更换前台轮播图片...") result = automation.replace_swiper_images() self._ps_log(result) # 更换后台登录背景图 self._ps_log("更换后台登录背景图...") if automation.update_login_vue_background(): self._ps_log("后台登录背景图更换成功!") else: self._ps_log("后台登录背景图更换失败") except Exception as e: self._ps_log(f"错误: {str(e)}") finally: self._ps_set_running(False) threading.Thread(target=run, daemon=True).start() def _ps_capture_admin(self): """后台截图""" def run(): try: self._ps_set_running(True) self._ps_log("=" * 50) self._ps_log("开始后台截图...") config = self._get_ps_config() automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) import asyncio # 截图保存到桌面路径下的"截图"文件夹 output_dir = os.path.join(config.desktop_path, "截图") os.makedirs(output_dir, exist_ok=True) screenshots = asyncio.run(automation.capture_admin_screenshots_only(output_dir)) self._ps_log(f"后台截图完成!共生成 {len(screenshots)} 张截图,保存到: {output_dir}") except Exception as e: self._ps_log(f"错误: {str(e)}") finally: self._ps_set_running(False) threading.Thread(target=run, daemon=True).start() def _ps_capture_front(self): """前台截图""" def run(): try: self._ps_set_running(True) self._ps_log("=" * 50) self._ps_log("开始前台截图...") config = self._get_ps_config() automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) import asyncio # 截图保存到桌面路径下的"截图"文件夹 output_dir = os.path.join(config.desktop_path, "截图") os.makedirs(output_dir, exist_ok=True) screenshots = asyncio.run(automation.capture_front_screenshots_only(output_dir)) self._ps_log(f"前台截图完成!共生成 {len(screenshots)} 张截图,保存到: {output_dir}") except Exception as e: self._ps_log(f"错误: {str(e)}") finally: self._ps_set_running(False) threading.Thread(target=run, daemon=True).start() def _ps_finalize_screenshots(self): """截图收尾工作""" def run(): try: self._ps_set_running(True) self._ps_log("=" * 50) self._ps_log("开始执行截图收尾工作...") config = self._get_ps_config() automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) # 截图保存到桌面路径下的"截图"文件夹 output_dir = os.path.join(config.desktop_path, "截图") automation.finalize_screenshots(output_dir) self._ps_log(f"截图收尾工作完成!") except Exception as e: self._ps_log(f"错误: {str(e)}") finally: self._ps_set_running(False) threading.Thread(target=run, daemon=True).start() def _ps_run_full_flow(self): """一键完整流程""" def run(): try: self._ps_set_running(True) config = self._get_ps_config() automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) success = automation.run_full_flow() if success: self.root.after(0, lambda: messagebox.showinfo("成功", "完整流程执行成功!")) else: self.root.after(0, lambda: messagebox.showerror("错误", "流程执行失败,请查看日志")) except Exception as e: self._ps_log(f"错误: {str(e)}") self.root.after(0, lambda: messagebox.showerror("错误", f"执行失败: {str(e)}")) finally: self._ps_set_running(False) threading.Thread(target=run, daemon=True).start() def _ps_close_all_cmd(self): """关闭所有CMD窗口""" def run(): try: self._ps_set_running(True) self._ps_log("=" * 50) self._ps_log("开始关闭所有CMD窗口...") config = self._get_ps_config() # 构建 close_cmd.bat 路径 close_cmd_bat = os.path.join(config.desktop_path, config.bat_folder, "close_cmd.bat") if not os.path.exists(close_cmd_bat): self._ps_log(f"错误:close_cmd.bat 不存在: {close_cmd_bat}") return self._ps_log(f"执行: {close_cmd_bat}") # 执行 bat 文件(弹出CMD窗口) import subprocess if config.show_cmd_window: # 显示CMD窗口 startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.wShowWindow = 1 # SW_SHOWNORMAL subprocess.Popen( ['cmd', '/c', close_cmd_bat], creationflags=subprocess.CREATE_NEW_CONSOLE, startupinfo=startupinfo, cwd=config.desktop_path ) self._ps_log("CMD窗口已弹出执行关闭命令") else: # 不显示CMD窗口 result = subprocess.run( ['cmd', '/c', close_cmd_bat], capture_output=True, text=True, encoding='utf-8', errors='ignore' ) if result.returncode == 0: self._ps_log("CMD窗口关闭成功!") if result.stdout: self._ps_log(result.stdout) else: self._ps_log(f"关闭失败: {result.stderr}") except Exception as e: self._ps_log(f"错误: {str(e)}") finally: self._ps_set_running(False) threading.Thread(target=run, daemon=True).start() def main(): """启动 GUI""" root = tk.Tk() app = YidaimaGUI(root) root.mainloop() if __name__ == "__main__": main()