Files
yidaima_tools/gui.py
2026-05-12 17:49:51 +08:00

1825 lines
81 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 tkinter as tk
from tkinter import ttk, messagebox, scrolledtext
import threading
import sys
import os
import subprocess
import re
# 添加当前目录到路径
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
from quark_uploader import QuarkUploader
from baidu_uploader import BaiduUploader
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.search_zlzt = "全部"
# 创建界面
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))
# 调用API方式选项
self.use_api_var = tk.BooleanVar(value=False)
ttk.Checkbutton(
config_frame,
text="调用API方式 (跳过浏览器自动化直接调用API获取文章内容)",
variable=self.use_api_var
).grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=(10, 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=30)
self.search_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(10, 0))
ttk.Label(search_frame, text="整理状态:").grid(row=0, column=2, sticky=tk.W, padx=(10, 0))
self.search_zlzt_var = tk.StringVar(value="全部")
self.zlzt_combo = ttk.Combobox(
search_frame,
textvariable=self.search_zlzt_var,
values=("全部", "已整理", "可整理"),
state="readonly",
width=10
)
self.zlzt_combo.grid(row=0, column=3, sticky=tk.W, padx=(5, 0))
ttk.Button(search_frame, text="搜索", command=self._search_projects, width=10).grid(row=0, column=4, padx=(10, 0))
ttk.Button(search_frame, text="刷新", command=self._refresh_projects, width=10).grid(row=0, column=5, 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("<Button-3>", 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(
"<Configure>",
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
# === 夸克网盘配置 ===
quark_frame = ttk.LabelFrame(self.settings_frame, text="夸克网盘配置", padding="10")
quark_frame.grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 15))
quark_frame.columnconfigure(1, weight=1)
ttk.Label(quark_frame, text="Cookie 目录:").grid(row=0, column=0, sticky=tk.W)
self.setting_quark_cookies_var = tk.StringVar(value=self.config.get("quark.cookies_dir", os.path.join(os.getcwd(), "data", "quark_cookies")))
ttk.Entry(quark_frame, textvariable=self.setting_quark_cookies_var, width=50).grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(10, 0))
ttk.Label(quark_frame, text="根目录名称:").grid(row=1, column=0, sticky=tk.W, pady=(10, 0))
self.setting_quark_root_var = tk.StringVar(value=self.config.get("quark.root_path", "精品项目整理"))
ttk.Entry(quark_frame, textvariable=self.setting_quark_root_var, width=40).grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(10, 0))
row += 1
# === 百度网盘配置 ===
baidu_frame = ttk.LabelFrame(self.settings_frame, text="百度网盘配置", padding="10")
baidu_frame.grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 15))
baidu_frame.columnconfigure(1, weight=1)
ttk.Label(baidu_frame, text="Cookie 目录:").grid(row=0, column=0, sticky=tk.W)
self.setting_baidu_cookies_var = tk.StringVar(value=self.config.get("baidu.cookies_dir", os.path.join(os.getcwd(), "data", "baidu_cookies")))
ttk.Entry(baidu_frame, textvariable=self.setting_baidu_cookies_var, width=50).grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(10, 0))
ttk.Label(baidu_frame, text="根目录名称:").grid(row=1, column=0, sticky=tk.W, pady=(10, 0))
self.setting_baidu_root_var = tk.StringVar(value=self.config.get("baidu.root_path", "精品项目整理"))
ttk.Entry(baidu_frame, textvariable=self.setting_baidu_root_var, width=40).grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(10, 0), 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()
use_api = self.use_api_var.get()
# Step 1
self.root.after(0, lambda: self.log("\n[Step 1] 开始执行..."))
if use_api:
# API 方式获取内容
self.root.after(0, lambda: self.log("[Step 1] 使用 API 方式获取文章内容..."))
import requests
import re
# 从项目名称中提取编号,如"【A167】" -> "A167"
match = re.search(r'【(.+?)】', project_name)
code_name = match.group(1) if match else project_name
api_url = "https://feast.yidaima.cn/prod-api/office/copyTemplate/open/generate"
params = {
"templateName": "FeastCoding",
"codeName": code_name
}
try:
response = requests.get(api_url, params=params, timeout=30)
response.raise_for_status()
json_data = response.json()
# 从 JSON 中提取 msg 字段内容
content = json_data.get("msg", "")
# 将 \\n 替换为空行
content = content.replace("\\n", "\n")
# 设置剪贴板内容
subprocess.run(
["powershell", "-command", f"Set-Clipboard -Value '{content.replace(chr(39), chr(39)+chr(39))}'"],
capture_output=True
)
self.root.after(0, lambda: self.log(f"[Step 1] API 调用成功,已复制到剪贴板,内容长度: {len(content)} 字符"))
except Exception as e:
self.root.after(0, lambda: self.log(f"[Step 1] API 调用失败: {e}"))
raise
else:
# 浏览器方式
self.root.after(0, lambda: self.log("[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] 浏览器方式执行完成!"))
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, project_name=project_name)
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,
zlzt=self.search_zlzt if self.search_zlzt and self.search_zlzt != "全部" 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.search_zlzt = self.search_zlzt_var.get()
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.search_zlzt = "全部"
self.search_zlzt_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()
},
"quark": {
"cookies_dir": self.setting_quark_cookies_var.get(),
"root_path": self.setting_quark_root_var.get()
},
"baidu": {
"cookies_dir": self.setting_baidu_cookies_var.get(),
"root_path": self.setting_baidu_root_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"))
self.setting_quark_cookies_var.set(self.config.get("quark.cookies_dir", os.path.join(os.getcwd(), "data", "quark_cookies")))
self.setting_quark_root_var.set(self.config.get("quark.root_path", "精品项目整理"))
self.setting_baidu_cookies_var.set(self.config.get("baidu.cookies_dir", os.path.join(os.getcwd(), "data", "baidu_cookies")))
self.setting_baidu_root_var.set(self.config.get("baidu.root_path", "精品项目整理"))
# ==================== 项目运行截图 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=50)
ps_project_path_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(10, 0))
btn_path_frame = ttk.Frame(path_frame)
btn_path_frame.grid(row=0, column=2, padx=(10, 0))
ttk.Button(btn_path_frame, text="选择项目", command=self._show_project_selector, width=10).pack(side=tk.LEFT)
ttk.Button(btn_path_frame, text="浏览...", command=self._browse_project_path, width=10).pack(side=tk.LEFT, padx=(5, 0))
# 项目名称 (name2)
ttk.Label(path_frame, text="项目名称:").grid(row=1, column=0, sticky=tk.W, pady=(10, 0))
self.ps_project_name_var = tk.StringVar(value=ps_config.get("project_name2", ""))
ps_project_name_entry = ttk.Entry(path_frame, textvariable=self.ps_project_name_var, width=60)
ps_project_name_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(10, 0))
# 桌面路径
ttk.Label(path_frame, text="桌面路径:").grid(row=2, 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=2, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(10, 0))
# bat 文件夹名称
ttk.Label(path_frame, text="bat文件夹:").grid(row=3, 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=3, 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, pady=(0, 10))
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))
# 上传按钮
upload_frame = ttk.Frame(button_frame)
upload_frame.pack(fill=tk.X)
ttk.Button(upload_frame, text="整理项目", command=self._ps_organize_project, width=15).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(upload_frame, text="整理并上传夸克", command=self._ps_organize_upload_cloud, width=18).pack(side=tk.LEFT, padx=(5, 5))
ttk.Button(upload_frame, text="整理并上传百度", command=self._ps_organize_upload_baidu, width=18).pack(side=tk.LEFT, padx=(5, 5))
ttk.Button(upload_frame, text="上传夸克网盘", command=self._ps_upload_quark, width=15).pack(side=tk.LEFT, padx=(5, 5))
ttk.Button(upload_frame, text="上传百度网盘", command=self._ps_upload_baidu, 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 _ps_organize_project(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)
project_name = self.ps_project_name_var.get().strip()
if not project_name:
project_name = None
else:
self._ps_log(f"使用项目名称: {project_name}")
zip_file = automation.organize_and_zip_project(override_name=project_name)
if zip_file:
self._ps_log("=" * 50)
self._ps_log(f"项目整理完成!压缩包已生成:{zip_file}")
self.root.after(0, lambda: messagebox.showinfo("成功", f"项目整理并打包成功!\n压缩包已生成在桌面。"))
else:
self._ps_log("=" * 50)
self._ps_log("项目整理失败,请检查日志。")
self.root.after(0, lambda: messagebox.showerror("错误", "项目整理并打包失败。"))
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_upload_quark(self):
"""仅上传到夸克网盘"""
def run():
try:
self._ps_set_running(True)
self._ps_log("=" * 50)
self._ps_log("准备上传到夸克网盘...")
# 检查压缩包是否存在
project_name = self.ps_project_name_var.get().strip()
desktop_path = self.ps_desktop_path_var.get()
if not project_name:
# 如果没填,尝试从路径拿
project_name = os.path.basename(self.ps_project_path_var.get().rstrip('/\\')).strip()
zip_file = os.path.join(desktop_path, f"{project_name}.zip")
if not os.path.exists(zip_file):
self._ps_log(f"错误:未找到压缩包,请先执行‘整理项目’\n路径:{zip_file}")
self.root.after(0, lambda: messagebox.showwarning("提示", "未找到对应的项目压缩包,请先执行‘整理项目’。"))
return
chrome_path = self.config.get("chrome.path", "")
cookies_dir = self.config.get("quark.cookies_dir", os.path.join(os.getcwd(), "data", "quark_cookies"))
root_path = self.config.get("quark.root_path", "精品项目整理")
uploader = QuarkUploader(chrome_path, cookies_dir, log_callback=self._ps_log)
success = uploader.upload_file(zip_file, project_name, root_path)
if success:
self._ps_log("=" * 50)
self._ps_log("夸克网盘上传成功!")
self.root.after(0, lambda: messagebox.showinfo("成功", "上传夸克网盘成功!"))
else:
self._ps_log("=" * 50)
self._ps_log("夸克网盘上传失败,请查看日志。")
self.root.after(0, lambda: messagebox.showerror("错误", "上传夸克网盘失败。"))
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_upload_baidu(self):
"""上传到百度网盘"""
def run():
try:
self._ps_set_running(True)
self._ps_log("=" * 50)
self._ps_log("准备上传到百度网盘...")
project_name = self.ps_project_name_var.get().strip()
desktop_path = self.ps_desktop_path_var.get()
if not project_name:
project_name = os.path.basename(self.ps_project_path_var.get().rstrip('/\\')).strip()
zip_file = os.path.join(desktop_path, f"{project_name}.zip")
if not os.path.exists(zip_file):
self._ps_log(f"错误:未找到压缩包,请先执行‘整理项目’\n路径:{zip_file}")
self.root.after(0, lambda: messagebox.showwarning("提示", "未找到对应的项目压缩包,请先执行‘整理项目’。"))
return
chrome_path = self.config.get("chrome.path", "")
cookies_dir = self.config.get("baidu.cookies_dir", os.path.join(os.getcwd(), "data", "baidu_cookies"))
root_path = self.config.get("baidu.root_path", "精品项目整理")
uploader = BaiduUploader(chrome_path, cookies_dir, log_callback=self._ps_log)
success = uploader.upload_file(zip_file, project_name, root_path)
if success:
self._ps_log("=" * 50)
self._ps_log("百度网盘上传成功!")
self.root.after(0, lambda: messagebox.showinfo("成功", "上传百度网盘成功!"))
else:
self._ps_log("=" * 50)
self._ps_log("百度网盘上传失败,请查看日志。")
self.root.after(0, lambda: messagebox.showerror("错误", "上传百度网盘失败。"))
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_upload_baidu(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)
project_name = self.ps_project_name_var.get().strip()
if not project_name:
project_name = None
else:
self._ps_log(f"使用项目名称: {project_name}")
zip_file = automation.organize_and_zip_project(override_name=project_name)
if zip_file:
self._ps_log("=" * 50)
self._ps_log(f"项目整理完成!压缩包已生成:{zip_file}")
self._ps_log("\n开始准备上传到百度网盘...")
chrome_path = self.config.get("chrome.path", "")
baidu_cookies_dir = self.config.get("baidu.cookies_dir", os.path.join(os.getcwd(), "data", "baidu_cookies"))
baidu_root_path = self.config.get("baidu.root_path", "精品项目整理")
if not project_name:
project_name = os.path.basename(zip_file).replace(".zip", "")
uploader = BaiduUploader(
chrome_path=chrome_path,
cookies_dir=baidu_cookies_dir,
log_callback=self._ps_log
)
success = uploader.upload_file(
file_path=zip_file,
target_folder_name=project_name,
root_path=baidu_root_path
)
if success:
self._ps_log("=" * 50)
self._ps_log("项目已成功整理并上传到百度网盘!")
self.root.after(0, lambda: messagebox.showinfo("成功", "项目整理并上传百度网盘成功!"))
else:
self._ps_log("=" * 50)
self._ps_log("上传百度网盘失败,请检查日志。")
self.root.after(0, lambda: messagebox.showerror("错误", "上传百度网盘失败,请检查日志。"))
else:
self._ps_log("=" * 50)
self._ps_log("项目整理失败,请检查日志。")
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_organize_upload_cloud(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)
# 直接从输入框获取项目名称,不需要再查数据库
project_name = self.ps_project_name_var.get().strip()
original_project_name = project_name # 保持原始名称用于后续更新数据库
if not project_name:
# 如果为空,则让 automation 尝试从路径提取
project_name = None
else:
self._ps_log(f"使用项目名称: {project_name}")
# 1. 执行整理打包
zip_file = automation.organize_and_zip_project(override_name=project_name)
if zip_file:
self._ps_log("=" * 50)
self._ps_log(f"项目整理完成!压缩包已生成:{zip_file}")
# 2. 上传到夸克网盘
self._ps_log("\n开始准备上传到夸克网盘...")
chrome_path = self.config.get("chrome.path", "")
quark_cookies_dir = self.config.get("quark.cookies_dir", os.path.join(os.getcwd(), "data", "quark_cookies"))
quark_root_path = self.config.get("quark.root_path", "精品项目整理")
# 再次确认项目名称(如果之前是 Noneautomation 会生成一个)
if not project_name:
project_name = os.path.basename(zip_file).replace(".zip", "")
uploader = QuarkUploader(
chrome_path=chrome_path,
cookies_dir=quark_cookies_dir,
log_callback=self._ps_log
)
success = uploader.upload_file(
file_path=zip_file,
target_folder_name=project_name,
root_path=quark_root_path
)
if success:
self._ps_log("=" * 50)
self._ps_log("项目已成功整理并上传到夸克网盘!")
self.root.after(0, lambda: messagebox.showinfo("成功", "项目整理并上传网盘成功!"))
else:
self._ps_log("=" * 50)
self._ps_log("上传网盘失败,请检查日志。")
self.root.after(0, lambda: messagebox.showerror("错误", "上传网盘失败,请检查日志。"))
else:
self._ps_log("=" * 50)
self._ps_log("项目整理失败,请检查日志。")
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 _show_project_selector(self):
"""显示项目选择对话框"""
if not self.db_manager:
messagebox.showwarning("警告", "数据库未连接")
return
selector = tk.Toplevel(self.root)
selector.title("选择项目")
selector.geometry("800x600")
selector.transient(self.root)
selector.grab_set()
# 搜索框
search_frame = ttk.Frame(selector, padding="10")
search_frame.pack(fill=tk.X)
ttk.Label(search_frame, text="项目名称:").pack(side=tk.LEFT)
search_var = tk.StringVar()
search_entry = ttk.Entry(search_frame, textvariable=search_var, width=40)
search_entry.pack(side=tk.LEFT, padx=10)
# 结果列表
table_frame = ttk.Frame(selector, padding="10")
table_frame.pack(fill=tk.BOTH, expand=True)
columns = ("name", "name2", "paths", "zlzt")
tree = ttk.Treeview(table_frame, columns=columns, show="headings")
tree.heading("name", text="项目名称")
tree.heading("name2", text="名称2")
tree.heading("paths", text="项目路径")
tree.heading("zlzt", text="状态")
tree.column("name", width=200)
tree.column("name2", width=150)
tree.column("paths", width=300)
tree.column("zlzt", width=80)
scrollbar = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=tree.yview)
tree.configure(yscrollcommand=scrollbar.set)
tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
def load_data():
keyword = search_var.get().strip()
# 清空
for item in tree.get_children():
tree.delete(item)
# 简单查询前100条
projects, _ = self.db_manager.get_projects(page=1, page_size=100, search_name=keyword if keyword else None)
for p in projects:
tree.insert("", tk.END, values=(p.name, p.name2, p.paths, p.zlzt))
# 搜索按钮
ttk.Button(search_frame, text="搜索", command=load_data).pack(side=tk.LEFT)
# 确认按钮
def on_select():
selected = tree.selection()
if not selected:
messagebox.showwarning("提示", "请先选择一个项目")
return
values = tree.item(selected[0], "values")
project_name = values[0]
project_name2 = values[1]
project_path = values[2]
# 更新主界面变量
self.ps_project_path_var.set(project_path)
# 使用名称2填入新增的项目名称输入框
self.ps_project_name_var.set(project_name2 if project_name2 else project_name)
# 同时更新发布页面的项目名称,方便后续对应
if project_name2 and '' in project_name2:
self.project_name_var.set(project_name2)
else:
self.project_name_var.set(project_name)
self._ps_log(f"已选择项目: {project_name}")
self._ps_log(f"更新项目路径: {project_path}")
self._ps_log(f"更新项目名称: {self.ps_project_name_var.get()}")
selector.destroy()
footer_frame = ttk.Frame(selector, padding="10")
footer_frame.pack(fill=tk.X)
ttk.Button(footer_frame, text="确定选择", command=on_select, width=15).pack(side=tk.RIGHT)
ttk.Button(footer_frame, text="取消", command=selector.destroy, width=15).pack(side=tk.RIGHT, padx=10)
# 初始加载
load_data()
# 绑定回车搜索
search_entry.bind("<Return>", lambda e: load_data())
# 绑定双击选择
tree.bind("<Double-1>", lambda e: on_select())
def main():
"""启动 GUI"""
root = tk.Tk()
app = YidaimaGUI(root)
root.mainloop()
if __name__ == "__main__":
main()