From a6d79d9a14e74801df2dd6ad9a25b32e1516ccbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E9=B9=8F?= Date: Wed, 15 Apr 2026 10:24:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E4=B8=80=E9=94=AE=E6=95=B4=E7=90=86=E3=80=81=E6=89=93=E5=8C=85?= =?UTF-8?q?=E5=8F=8A=E8=87=AA=E5=8A=A8=E4=B8=8A=E4=BC=A0=E5=A4=B8=E5=85=8B?= =?UTF-8?q?/=E7=99=BE=E5=BA=A6=E7=BD=91=E7=9B=98=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 8 + baidu_uploader.py | 177 +++++++++++++++++++ config.yaml | 18 ++ config_loader.py | 24 +++ core/actions.py | 1 - db_manager.py | 68 +++++++- gui.py | 397 ++++++++++++++++++++++++++++++++++++++++-- project_screenshot.py | 93 ++++++++++ quark_uploader.py | 166 ++++++++++++++++++ 9 files changed, 933 insertions(+), 19 deletions(-) create mode 100644 baidu_uploader.py create mode 100644 quark_uploader.py diff --git a/.gitignore b/.gitignore index 495b271..6752efd 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,14 @@ yarn-error.log* .env.local .env.*.local +# 网盘登录 Cookie +data/quark_cookies/ +data/baidu_cookies/ + +# Python 缓存 +__pycache__/ +*/__pycache__/ + # 忽略系统生成的临时文件 .DS_Store Thumbs.db \ No newline at end of file diff --git a/baidu_uploader.py b/baidu_uploader.py new file mode 100644 index 0000000..cee1cb3 --- /dev/null +++ b/baidu_uploader.py @@ -0,0 +1,177 @@ +from __future__ import annotations + +import os +import time +import re +from typing import Optional, Callable +from playwright.sync_api import sync_playwright + +class BaiduUploader: + """ + 百度网盘上传工具 - 纯净上传版 (移除分享逻辑) + """ + def __init__(self, chrome_path: str, cookies_dir: str, log_callback: Optional[Callable[[str], None]] = None): + self.chrome_path = chrome_path + self.cookies_dir = cookies_dir + self.log_callback = log_callback + self.url = "https://pan.baidu.com/" + + if not os.path.exists(self.cookies_dir): + os.makedirs(self.cookies_dir, exist_ok=True) + + def _log(self, message: str): + print(message) + if self.log_callback: + self.log_callback(message) + + def upload_file(self, file_path: str, target_folder_name: str, root_path: str = "精品项目整理") -> bool: + """ + 上传文件到百度网盘 + """ + if not os.path.exists(file_path): + self._log(f"错误: 本地文件不存在 {file_path}") + return False + + filename = os.path.basename(file_path) + + # 提取项目编号 + project_id = "" + match = re.search(r'(【[A-Za-z0-9]+】)', target_folder_name) + if match: + project_id = match.group(1) + + try: + with sync_playwright() as p: + self._log(f"正在启动浏览器...") + + launch_args = { + "user_data_dir": self.cookies_dir, + "headless": False, + "executable_path": self.chrome_path if self.chrome_path and os.path.exists(self.chrome_path) else None, + "viewport": {"width": 1280, "height": 800} + } + if not launch_args["executable_path"]: + launch_args.pop("executable_path") + + context = p.chromium.launch_persistent_context(**launch_args) + context.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})") + + page = context.new_page() + page.set_default_timeout(60000) + + # 1. 登录 + self._log("检测登录状态...") + page.goto(self.url, wait_until="domcontentloaded") + + if not self._wait_for_login(page): + self._log("请手动扫码登录...") + page.wait_for_selector("text=全部文件", timeout=180000) + + # 2. 导航 + self._log("登录成功,进入目标路径...") + page.get_by_text("全部文件").first.click(force=True) + time.sleep(4) + + if not self._scan_and_enter(page, root_path): + self._create_folder(page, root_path) + time.sleep(2); self._scan_and_enter(page, root_path) + + search_key = project_id if project_id else target_folder_name + if not self._scan_and_enter(page, search_key, is_fuzzy=True): + self._create_folder(page, target_folder_name) + time.sleep(2); self._scan_and_enter(page, search_key, is_fuzzy=True) + + # 3. 查重 + if self._check_file_exists_robust(page, filename): + self._log(f"云端已存在文件 '{filename}',跳过上传。") + time.sleep(2) + context.close() + return True + + # 4. 执行上传 + self._log(f"开始传输: {filename}") + page.set_input_files("input[type=file]", file_path) + + # 5. 监测列表确认成功 + self._log("上传已启动,监测云端状态中...") + success = self._wait_for_file_in_list(page, filename) + + if success: + self._log("\n" + "="*30) + self._log("百度网盘上传圆满成功!") + self._log("="*30 + "\n") + time.sleep(3) + else: + self._log("警告:监测超时,未能在列表中看到文件,请手动核实。") + + context.close() + return success + + except Exception as e: + self._log(f"程序异常: {str(e)}") + return False + + def _wait_for_login(self, page) -> bool: + for _ in range(10): + if page.get_by_text("全部文件").count() > 0: return True + time.sleep(2) + return False + + def _scan_and_enter(self, page, target_name: str, is_fuzzy: bool = False) -> bool: + try: + page.evaluate("window.scrollTo(0, 300)") + found = page.evaluate(f""" + (args) => {{ + const {{ name, fuzzy }} = args; + const els = [...document.querySelectorAll('span[title], a[title], .wp-s-file-list-drag-copy__item-title-text')]; + const target = els.find(e => {{ + const title = e.getAttribute('title') || e.innerText.trim(); + return fuzzy ? title.includes(name) : title === name; + }}); + if (target) {{ + target.click(); + const ev = new MouseEvent('dblclick', {{ 'view': window, 'bubbles': true, 'cancelable': true }}); + target.dispatchEvent(ev); + return true; + }} + return false; + }} + """, {"name": target_name, "fuzzy": is_fuzzy}) + if found: time.sleep(3); return True + return False + except: return False + + def _create_folder(self, page, folder_name: str): + try: + page.keyboard.press("Escape") + btn = page.get_by_text("新建文件夹").first + if btn.count() > 0: + btn.click(force=True) + time.sleep(1.5) + page.keyboard.type(folder_name, delay=50) + page.keyboard.press("Enter") + time.sleep(3) + except: pass + + def _check_file_exists_robust(self, page, filename: str) -> bool: + return page.evaluate(f""" + (name) => {{ + const els = [...document.querySelectorAll('span[title], a[title], .wp-s-file-list-drag-copy__item-title-text')]; + return els.some(e => (e.getAttribute('title') || e.innerText.trim()) === name); + }} + """, filename) + + def _wait_for_file_in_list(self, page, filename: str) -> bool: + start_time = time.time(); last_refresh = time.time() + while time.time() - start_time < 1800: + if self._check_file_exists_robust(page, filename): + content = page.content() + # 如果文件在列表里,且页面内容没在报错或重试 + if not any(x in content for x in ["正在上传", "传输队列", "0B/s"]): + return True + if time.time() - last_refresh > 25: + try: page.locator(".wp-s-file-list-drag-copy__header-breadcrumb-item").last.click(); time.sleep(3) + except: pass + last_refresh = time.time() + time.sleep(5) + return False diff --git a/config.yaml b/config.yaml index b4f32cb..1139d5f 100644 --- a/config.yaml +++ b/config.yaml @@ -18,3 +18,21 @@ step1: # Step2 配置 step2: url: "http://yidaima.cn:6005/" + +# 数据库配置 +database: + host: "localhost" + port: 3306 + database: "test" + user: "root" + password: "123456" + +# 夸克网盘配置 +quark: + cookies_dir: "C:\\Users\\南音\\Desktop\\yidaima\\data\\quark_cookies" + root_path: "精品项目整理" + +# 百度网盘配置 +baidu: + cookies_dir: "C:\\Users\\南音\\Desktop\\yidaima\\data\\baidu_cookies" + root_path: "精品项目整理" diff --git a/config_loader.py b/config_loader.py index 179c2f1..e4fce05 100644 --- a/config_loader.py +++ b/config_loader.py @@ -52,6 +52,14 @@ class Config: "user": "root", "password": "123456" }, + "quark": { + "cookies_dir": os.path.join(os.getcwd(), "data", "quark_cookies"), + "root_path": "精品项目整理" + }, + "baidu": { + "cookies_dir": os.path.join(os.getcwd(), "data", "baidu_cookies"), + "root_path": "精品项目整理" + }, "project_screenshot": { "project_path": "", "desktop_path": "C:\\Users\\南音\\Desktop", @@ -193,6 +201,18 @@ class Config: if os.environ.get("DB_PASSWORD"): self._config_data["database"]["password"] = os.environ.get("DB_PASSWORD") + # 夸克网盘配置 + if os.environ.get("QUARK_COOKIES_DIR"): + self._config_data["quark"]["cookies_dir"] = os.environ.get("QUARK_COOKIES_DIR") + if os.environ.get("QUARK_ROOT_PATH"): + self._config_data["quark"]["root_path"] = os.environ.get("QUARK_ROOT_PATH") + + # 百度网盘配置 + if os.environ.get("BAIDU_COOKIES_DIR"): + self._config_data["baidu"]["cookies_dir"] = os.environ.get("BAIDU_COOKIES_DIR") + if os.environ.get("BAIDU_ROOT_PATH"): + self._config_data["baidu"]["root_path"] = os.environ.get("BAIDU_ROOT_PATH") + def get(self, key: str, default: Any = None) -> Any: """ 获取配置值,支持点号分隔的路径 @@ -231,6 +251,10 @@ class Config: def database_config(self) -> dict: return self.get("database", {}) + @property + def quark_config(self) -> dict: + return self.get("quark", {}) + @property def project_screenshot_config(self) -> dict: return self.get("project_screenshot", {}) diff --git a/core/actions.py b/core/actions.py index f1cdc32..0d8baff 100644 --- a/core/actions.py +++ b/core/actions.py @@ -84,4 +84,3 @@ class ActionRunner: return m time.sleep(interval) raise TimeoutError(f"等待超时:{timeout}s 内未出现文字:{text}") - diff --git a/db_manager.py b/db_manager.py index 6208013..1f5c1d0 100644 --- a/db_manager.py +++ b/db_manager.py @@ -77,7 +77,8 @@ class DatabaseManager: return True def get_projects(self, page: int = 1, page_size: int = 20, - search_name: Optional[str] = None) -> Tuple[List[ProjectOrder], int]: + search_name: Optional[str] = None, + zlzt: Optional[str] = None) -> Tuple[List[ProjectOrder], int]: """ 获取项目列表(分页) @@ -85,6 +86,7 @@ class DatabaseManager: page: 页码(从1开始) page_size: 每页数量 search_name: 搜索名称(模糊匹配) + zlzt: 整理状态 Returns: (项目列表, 总数量) @@ -96,11 +98,18 @@ class DatabaseManager: cursor = self.connection.cursor(dictionary=True) # 构建查询条件 - where_clause = "" + where_conditions = [] params = [] if search_name: - where_clause = "WHERE name LIKE %s" + where_conditions.append("name LIKE %s") params.append(f"%{search_name}%") + if zlzt: + where_conditions.append("zlzt = %s") + params.append(zlzt) + + where_clause = "" + if where_conditions: + where_clause = "WHERE " + " AND ".join(where_conditions) # 获取总数 count_sql = f"SELECT COUNT(*) as total FROM t_new_project_order_new {where_clause}" @@ -147,7 +156,7 @@ class DatabaseManager: 更新整理状态 Args: - name: 项目名称 + name: 项目名称 (匹配 name 或 name2) zlzt: 新的整理状态值 Returns: @@ -158,8 +167,9 @@ class DatabaseManager: try: cursor = self.connection.cursor() - sql = "UPDATE t_new_project_order_new SET zlzt = %s WHERE name = %s" - cursor.execute(sql, (zlzt, name)) + # 尝试同时匹配 name 和 name2 + sql = "UPDATE t_new_project_order_new SET zlzt = %s WHERE name = %s OR name2 = %s" + cursor.execute(sql, (zlzt, name, name)) self.connection.commit() affected = cursor.rowcount cursor.close() @@ -211,6 +221,52 @@ class DatabaseManager: print(f"[DatabaseManager] 查询失败: {e}") return None + def get_project_by_path(self, path: str) -> Optional[ProjectOrder]: + """ + 根据路径模糊获取项目 + + Args: + path: 项目路径关键字 + + Returns: + 项目对象,如果找不到返回 None + """ + if not self.ensure_connected(): + return None + + try: + cursor = self.connection.cursor(dictionary=True) + # 使用模糊匹配,因为路径可能包含反斜杠或斜杠差异 + sql = """ + SELECT name, paths, zlzt, sfxxm, name1, name2, bianhao, + `desc`, sql_paths, error_message + FROM t_new_project_order_new + WHERE paths LIKE %s OR %s LIKE CONCAT('%', paths, '%') + LIMIT 1 + """ + search_path = f"%{path}%" + cursor.execute(sql, (search_path, path)) + row = cursor.fetchone() + cursor.close() + + if row: + return ProjectOrder( + name=row.get('name'), + paths=row.get('paths'), + zlzt=row.get('zlzt'), + sfxxm=row.get('sfxxm'), + name1=row.get('name1'), + name2=row.get('name2'), + bianhao=row.get('bianhao'), + desc=row.get('desc'), + sql_paths=row.get('sql_paths'), + error_message=row.get('error_message') + ) + return None + except Error as e: + print(f"[DatabaseManager] 查询失败: {e}") + return None + def delete_project(self, name: str) -> bool: """ 删除项目 diff --git a/gui.py b/gui.py index fabe446..dbc4e90 100644 --- a/gui.py +++ b/gui.py @@ -6,6 +6,7 @@ import threading import sys import os import subprocess +import re # 添加当前目录到路径 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) @@ -17,6 +18,8 @@ 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: @@ -50,6 +53,7 @@ class YidaimaGUI: self.page_size = 20 self.total_items = 0 self.search_keyword = "" + self.search_zlzt = "全部" # 创建界面 self.create_widgets() @@ -214,11 +218,22 @@ class YidaimaGUI: 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 = 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.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)) + 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") @@ -370,6 +385,36 @@ class YidaimaGUI: # 测试连接按钮 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 # === 保存按钮 === @@ -541,7 +586,8 @@ class YidaimaGUI: 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 + 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 @@ -573,6 +619,7 @@ class YidaimaGUI: """搜索项目""" 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: @@ -582,6 +629,8 @@ class YidaimaGUI: """刷新项目列表""" self.search_keyword = "" self.search_var.set("") + self.search_zlzt = "全部" + self.search_zlzt_var.set("全部") self.current_page = 1 self._load_projects() @@ -621,7 +670,7 @@ class YidaimaGUI: menu = tk.Menu(self.root, tearoff=0) # 变更已整理状态选项 - new_zlzt = "已整理" if current_zlzt != "已整理" else "未整理" + new_zlzt = "已整理" if current_zlzt != "已整理" else "可整理" menu.add_command( label=f"变更状态: {new_zlzt}", command=lambda: self._update_project_zlzt(name, new_zlzt) @@ -766,6 +815,14 @@ class YidaimaGUI: "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() } } @@ -807,6 +864,10 @@ class YidaimaGUI: 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 方法 ==================== @@ -827,21 +888,31 @@ class YidaimaGUI: # 项目路径 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 = 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)) - ttk.Button(path_frame, text="浏览...", command=self._browse_project_path, width=10).grid(row=0, column=2, 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=1, column=0, sticky=tk.W, 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=1, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(10, 0)) + 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=2, column=0, sticky=tk.W, pady=(10, 0)) + 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=2, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(10, 0)) + 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)) @@ -878,7 +949,7 @@ class YidaimaGUI: # 截图按钮 screenshot_frame = ttk.Frame(button_frame) - screenshot_frame.pack(fill=tk.X) + 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)) @@ -886,6 +957,14 @@ class YidaimaGUI: 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_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)) @@ -1374,6 +1453,300 @@ class YidaimaGUI: 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_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", "精品项目整理") + + # 再次确认项目名称(如果之前是 None,automation 会生成一个) + 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("", lambda e: load_data()) + # 绑定双击选择 + tree.bind("", lambda e: on_select()) + def main(): """启动 GUI""" diff --git a/project_screenshot.py b/project_screenshot.py index 6f3986e..ef41e6f 100644 --- a/project_screenshot.py +++ b/project_screenshot.py @@ -1483,6 +1483,99 @@ class ProjectScreenshotAutomation: except Exception as e: self.log(f"截图收尾工作失败: {str(e)}") + + def organize_and_zip_project(self, override_name: Optional[str] = None) -> Optional[str]: + """ + 整理项目并打包成压缩包 + 将桌面路径下的code文件夹下的第一个子文件夹名称改成项目名称,并打包 + + Args: + override_name: 如果提供,则使用此名称作为项目名称,否则从路径提取 + + Returns: + 压缩包路径 + """ + try: + self.log("=" * 50) + self.log("开始整理项目并打包...") + + # 1. 确定项目名称 + if override_name: + project_name = override_name + self.log(f"使用提供的项目名称: {project_name}") + elif not self.config.project_path: + self.log("未配置项目路径,且未提供覆盖名称,无法提取项目名称") + return None + else: + project_name = os.path.basename(self.config.project_path.rstrip('/\\')) + self.log(f"从路径提取项目名称: {project_name}") + + # 去掉前后的空格 + project_name = project_name.strip() + # 移除非法文件名字符 + project_name = re.sub(r'[\\/:*?"<>|]', '_', project_name) + self.log(f"最终使用的项目名称: {project_name}") + + # 2. 找到 code 文件夹 + code_dir = os.path.join(self.config.desktop_path, "code") + if not os.path.exists(code_dir): + self.log(f"未找到 code 文件夹: {code_dir}") + return None + + # 3. 找到 code 下的第一个子文件夹 + subdirs = [d for d in os.listdir(code_dir) if os.path.isdir(os.path.join(code_dir, d))] + if not subdirs: + self.log(f"code 文件夹下没有子文件夹: {code_dir}") + return None + + # 排除掉一些可能的非项目文件夹 + # subdirs = [d for d in subdirs if d not in ['.git', '__pycache__']] + + first_subdir = subdirs[0] + first_subdir_path = os.path.join(code_dir, first_subdir) + + # 4. 重命名 + new_subdir_path = os.path.join(code_dir, project_name) + + # 如果新路径和旧路径不同,则重命名 + if first_subdir != project_name: + if os.path.exists(new_subdir_path): + self.log(f"目标文件夹已存在,删除旧文件夹: {new_subdir_path}") + shutil.rmtree(new_subdir_path) + + try: + os.rename(first_subdir_path, new_subdir_path) + self.log(f"重命名文件夹: {first_subdir} -> {project_name}") + except Exception as e: + self.log(f"重命名失败: {str(e)}") + return None + else: + self.log(f"文件夹已经是项目名称,无需重命名: {first_subdir}") + + # 5. 打包成压缩包 + zip_target_path = os.path.join(self.config.desktop_path, project_name) + self.log(f"开始打包到桌面: {project_name}.zip") + + # make_archive(base_name, format, root_dir, base_dir) + # base_name 是输出文件的路径(不含后缀) + # root_dir 是要打包的根目录 + # base_dir 是相对于 root_dir 的要打包的目录(包含在压缩包内的顶级目录) + try: + zip_file = shutil.make_archive( + base_name=zip_target_path, + format='zip', + root_dir=code_dir, + base_dir=project_name + ) + self.log(f"打包完成: {zip_file}") + return zip_file + except Exception as e: + self.log(f"打包失败: {str(e)}") + return None + + except Exception as e: + self.log(f"整理打包失败: {str(e)}") + return None def run_full_flow(self) -> bool: """ diff --git a/quark_uploader.py b/quark_uploader.py new file mode 100644 index 0000000..1e5f5c9 --- /dev/null +++ b/quark_uploader.py @@ -0,0 +1,166 @@ +from __future__ import annotations + +import os +import time +from typing import Optional, Callable +from playwright.sync_api import sync_playwright + +class QuarkUploader: + """ + 夸克网盘上传工具 - 终极稳健版 (双重状态锁) + """ + def __init__(self, chrome_path: str, cookies_dir: str, log_callback: Optional[Callable[[str], None]] = None): + self.chrome_path = chrome_path + self.cookies_dir = cookies_dir + self.log_callback = log_callback + self.url = "https://pan.quark.cn/" + + if not os.path.exists(self.cookies_dir): + os.makedirs(self.cookies_dir, exist_ok=True) + + def _log(self, message: str): + print(message) + if self.log_callback: + self.log_callback(message) + + def upload_file(self, file_path: str, target_folder_name: str, root_path: str = "精品项目整理") -> bool: + """ + 上传文件到夸克网盘 + """ + if not os.path.exists(file_path): + self._log(f"错误: 本地文件不存在 {file_path}") + return False + + filename = os.path.basename(file_path) + + try: + with sync_playwright() as p: + self._log(f"正在启动浏览器...") + launch_args = { + "user_data_dir": self.cookies_dir, + "headless": False, + "viewport": {"width": 1280, "height": 800} + } + if self.chrome_path and os.path.exists(self.chrome_path): + launch_args["executable_path"] = self.chrome_path + + context = p.chromium.launch_persistent_context(**launch_args) + page = context.new_page() + + self._log("正在打开夸克网盘...") + page.goto(self.url, wait_until="networkidle") + + # 1. 登录检测 + try: + page.wait_for_selector("text=全部文件", timeout=10000) + except: + self._log("请在浏览器中完成登录...") + page.wait_for_selector("text=全部文件", timeout=180000) + + # 2. 导航到目录 + self._log(f"定位目录: {root_path} > {target_folder_name}") + page.get_by_text("全部文件").first.click() + page.wait_for_timeout(2000) + + # 进入根目录 + if not page.get_by_text(root_path, exact=True).first.is_visible(): + self._create_folder(page, root_path) + page.get_by_text(root_path, exact=True).first.dblclick() + page.wait_for_timeout(1000) + + # 进入项目目录 + if not page.get_by_text(target_folder_name, exact=True).first.is_visible(): + self._create_folder(page, target_folder_name) + page.get_by_text(target_folder_name, exact=True).first.dblclick() + page.wait_for_timeout(2000) + + # 3. 查重 + if page.get_by_text(filename, exact=True).count() > 0: + self._log(f"云端已存在 '{filename}',跳过上传。") + context.close() + return True + + # 4. 执行上传 + self._log(f"开始上传: {filename}") + # 清除可能存在的旧“上传成功”提示 + page.evaluate("() => { const els = document.querySelectorAll('.ant-notification-notice'); els.forEach(e => e.remove()); }") + + page.set_input_files("input[type=file]", file_path) + + # 5. 确认上传已启动 (关键一步) + self._log("等待上传任务初始化...") + try: + # 等待出现任何进度指示元素 + page.wait_for_selector("text=% , .ant-progress-inner, text=正在上传", timeout=20000) + self._log("检测到上传流已建立。") + except: + self._log("警告:未检测到明显的进度条,可能由于文件较小或 UI 延迟,继续监测列表状态。") + + # 6. 深度监测循环 + self._log("进入深度监测模式,请保持浏览器前台运行...") + start_time = time.time() + timeout = 1800 # 30分钟 + + # 连续稳定次数计数 + stable_count = 0 + + while time.time() - start_time < timeout: + # A. 检查全局进度标志 + is_uploading = page.get_by_text("%").is_visible() or \ + page.get_by_text("正在上传").is_visible() or \ + page.locator(".ant-progress-inner").is_visible() + + # B. 检查列表中该行的具体状态 (精准匹配) + # 找到包含文件名的那一行 tr,看里面是否有“正在上传”字样 + row_status_text = "" + try: + row = page.locator(f"tr:has-text('{filename}')").first + if row.is_visible(): + row_status_text = row.inner_text() + except: + pass + + is_item_active = "正在上传" in row_status_text or "%" in row_status_text or "等待上传" in row_status_text + + # C. 确认列表里确实有这个文件 + in_list = page.get_by_text(filename, exact=True).count() > 0 + + if not is_uploading and not is_item_active and in_list: + stable_count += 1 + if stable_count >= 5: # 连续 5 次检测(约 15 秒)都处于稳定态 + self._log("检测到上传任务已从任务列表中消失,且列表文件状态正常。") + self._log("为了绝对安全,最后等待 15 秒进行数据落盘同步...") + page.wait_for_timeout(15000) + self._log("上传确认圆满成功!") + context.close() + return True + else: + self._log(f"上传疑似完成,正在进行稳定性校验 ({stable_count}/5)...") + else: + if is_uploading or is_item_active: + self._log("监测到活跃传输流...") + stable_count = 0 # 只要发现还在传,重置计数 + + time.sleep(3) + + self._log("传输任务监测超时,请确认网速或手动核实。") + context.close() + return False + + except Exception as e: + self._log(f"代码执行异常: {str(e)}") + return False + + def _create_folder(self, page, folder_name: str): + """创建目录""" + try: + page.get_by_text("新建").first.click() + page.wait_for_timeout(500) + page.get_by_text("新建文件夹").first.click() + page.wait_for_timeout(500) + page.keyboard.type(folder_name) + page.keyboard.press("Enter") + self._log(f"已创建目录: {folder_name}") + page.wait_for_timeout(1500) + except: + pass