feat: 增加项目一键整理、打包及自动上传夸克/百度网盘功能
This commit is contained in:
8
.gitignore
vendored
8
.gitignore
vendored
@@ -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
|
||||
177
baidu_uploader.py
Normal file
177
baidu_uploader.py
Normal file
@@ -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
|
||||
18
config.yaml
18
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: "精品项目整理"
|
||||
|
||||
@@ -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", {})
|
||||
|
||||
@@ -84,4 +84,3 @@ class ActionRunner:
|
||||
return m
|
||||
time.sleep(interval)
|
||||
raise TimeoutError(f"等待超时:{timeout}s 内未出现文字:{text}")
|
||||
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
删除项目
|
||||
|
||||
397
gui.py
397
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")
|
||||
@@ -372,6 +387,36 @@ class YidaimaGUI:
|
||||
|
||||
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))
|
||||
@@ -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("<Return>", lambda e: load_data())
|
||||
# 绑定双击选择
|
||||
tree.bind("<Double-1>", lambda e: on_select())
|
||||
|
||||
|
||||
def main():
|
||||
"""启动 GUI"""
|
||||
|
||||
@@ -1484,6 +1484,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:
|
||||
"""
|
||||
执行完整流程(按照按钮排序顺序)
|
||||
|
||||
166
quark_uploader.py
Normal file
166
quark_uploader.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user