import sys
import os
from PyQt6.QtWidgets import *
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from config.config_manager import ConfigManager
from modules.database_handler import DatabaseHandler
from modules.logger import Logger
import win32gui
import win32con
# 在文件顶部添加导入
import asyncio
from playwright.async_api import async_playwright
import json
import ctypes
import win32gui
from ctypes import wintypes
class MainApp(QMainWindow):
# 在类顶部声明信号
load_started = pyqtSignal(str)
load_finished = pyqtSignal(str)
pids = {} # 存储进程 ID
window_handles = {} # 存储窗口句柄
def __init__(self):
super().__init__()
self.setWindowTitle("多平台发布工具")
self.setWindowTitle("多平台发布工具")
self.setFixedSize(980, 720) # 固定窗口尺寸
self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowMaximizeButtonHint) # 禁用最大化
# 启动自动检查定时器
self.timer = QTimer(self)
self.timer.timeout.connect(self.check_login_status)
self.timer.start(60000) # 每60秒执行一次
# 在 __init__ 方法中添加
self.setSizePolicy(
QSizePolicy.Policy.Fixed,
QSizePolicy.Policy.Expanding
)
self.config = ConfigManager()
# 初始化 Playwright 相关属性
self.playwright = None
self.browser = None
self.pages = {} # 存储页面对象
# 启动 Playwright(异步初始化)
asyncio.get_event_loop().run_until_complete(self.start_playwright())
# 设置主窗口尺寸
# self.setGeometry(100, 100, 1440, 1220)
# self.setFixedSize(980,720)
self.current_browser = None
self.browsers = {}
self.create_project_dirs()
self.logger = Logger()
self.db = DatabaseHandler()
self.init_ui()
self.load_sites_config()
# 在 __init__ 中添加:
self.load_started.connect(self.on_load_started)
self.load_finished.connect(self.on_load_finished)
# 新增槽函数:
def on_load_started(self, url):
self.update_status(url, "正在打开")
def on_load_finished(self, url):
self.update_status(url, "加载完成")
self.update_cell_color(self.get_row_by_url(url), "黄色")
def get_row_by_url(self, url):
for row in range(self.site_table.rowCount()):
site_url = self.config.get_site_url(row)
if site_url == url:
return row
return -1
async def start_playwright(self):
self.playwright = await async_playwright().start()
self.browser = await self.playwright.chromium.launch(headless=False,
args=[
# f"--app={url}", # 应用模式
# "--disable-infobars", # 隐藏自动化提示
# "--no-default-browser-check", # 禁用默认浏览器检查
# "--disable-extensions", # 禁用扩展
# "--disable-notifications", # 禁用通知
# "--disable-session-crashed-bubble", # 隐藏崩溃提示
# "--overscroll-history-navigation=0", # 禁用滑动导航
# "--disable-features=TranslateUI", # 禁用翻译工具栏
# "--hide-scrollbars", # 隐藏滚动条
# "--kiosk" # 全屏无边框(可选)
]
) # 返回 Browser 对象
def create_project_dirs(self):
required_dirs = [
"cookies", # 主目录
"scripts",
"user_data" # 用户数据主目录
]
for d in required_dirs:
dir_path = os.path.join(os.getcwd(), d)
os.makedirs(dir_path, exist_ok=True)
# 为每个站点创建子目录(如user_data/douban)
sites = self.config.load_sites()
for site in sites:
user_data_dir = site.get('user_data_dir')
if user_data_dir:
full_path = os.path.join(os.getcwd(), user_data_dir)
os.makedirs(full_path, exist_ok=True)
def init_ui(self):
print("初始化UI...")
main_widget = QWidget()
main_layout = QHBoxLayout(main_widget)
# main_layout.setContentsMargins(1, 1, 1, 1) # ✅ 清除主布局的边距
# 左侧配置表格
self.site_table = QTableWidget()
self.site_table.setColumnCount(4)
self.site_table.setHorizontalHeaderLabels(['选择', '网站', '状态', '操作'])
self.site_table.setColumnWidth(0, 30)
self.site_table.setColumnWidth(1, 50)
self.site_table.setColumnWidth(2, 80)
self.site_table.setColumnWidth(3, 80)
# 计算左侧表格宽度
left_table_width = 230
self.site_table.setFixedWidth(left_table_width)
# 右侧信息框
self.status_box = QTextEdit()
self.status_box.setStyleSheet("background-color: #000; color: #0f0;")
self.status_box.setReadOnly(True)
# # 计算信息框高度
status_box_height = 720 - 540 - 80 # 主窗口高度720 - 浏览器高度540 - 按钮布局高度30
self.status_box.setFixedHeight(status_box_height)
right_layout = QVBoxLayout()
right_layout.addWidget(self.status_box)
# 添加缩放按钮和保存按钮的布局
button_layout = QHBoxLayout()
# 新增保存按钮
self.save_btn = QPushButton("保存登录信息")
self.save_btn.setStyleSheet("background-color: initial;") # 固定默认背景色
button_layout.addWidget(self.save_btn) # 添加到布局
# 在 init_ui 方法中
self.save_btn.clicked.connect(self.handle_save_button) # 正确连接
right_layout.addLayout(button_layout)
# ✅ 新增:清除右侧布局的边距
# right_layout.setContentsMargins(0, 0, 0, 0)
right_layout.setSpacing(0) # 移除布局间隙
main_layout.addWidget(self.site_table)
main_layout.addLayout(right_layout)
self.setCentralWidget(main_widget)
# 新增状态提示标签(替代浏览器容器)
self.browser_status_label = QLabel("外部浏览器正在加载...")
self.browser_status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
right_layout.addWidget(self.browser_status_label)
def handle_save_button(self):
"""处理保存按钮点击事件"""
selected_rows = []
for row in range(self.site_table.rowCount()):
checkbox = self.site_table.cellWidget(row, 0)
if checkbox.isChecked():
selected_rows.append(row)
if not selected_rows:
QMessageBox.warning(self, "提示", "请选择要保存的网站")
return
# 异步执行保存操作
asyncio.get_event_loop().run_until_complete(
self.async_save_selected(selected_rows)
)
async def async_save_selected(self, rows):
"""异步保存所有选中的行"""
for row in rows:
await self.save_storage_state(row)
async def save_storage_state(self, row):
"""保存指定行的存储状态"""
url = self.config.get_site_url(row)
if url not in self.pages:
QMessageBox.warning(self, "提示", f"页面未加载:{url}")
return
page = self.pages[url]
try:
# 获取存储状态
state = await page.context.storage_state()
# 获取路径并创建目录
path = self.config.get_cookies_path(row)
dir_name = os.path.dirname(path)
os.makedirs(dir_name, exist_ok=True)
# 写入文件
with open(path, 'w') as f:
json.dump(state, f)
# 更新状态提示
self.update_status(url, "登录信息已保存")
self.update_cell_color(row, "绿色")
except Exception as e:
self.update_status(url, f"保存失败:{str(e)}")
print(f"保存失败:{str(e)}")
def load_sites_config(self):
print("加载网站配置...")
sites = self.config.load_sites()
self.site_table.setRowCount(len(sites))
for row, site in enumerate(sites):
checkbox = QCheckBox() # 使用复选框替代单选按钮
checkbox.clicked.connect(lambda checked, current_row=row: self.on_checkbox_toggled(checked, current_row))
self.site_table.setCellWidget(row, 0, checkbox)
site_name = QTableWidgetItem(site['name'])
site_name.setBackground(QColor(255, 0, 0))
status_item = QTableWidgetItem("未登录")
action_btn = QPushButton("刷新")
action_btn.clicked.connect(lambda _, url=site['url']: self.refresh_site(url))
self.site_table.setCellWidget(row, 0, checkbox)
self.site_table.setItem(row, 1, site_name)
self.site_table.setItem(row, 2, status_item)
self.site_table.setCellWidget(row, 3, action_btn)
# 检查登录状态并更新单元格
if self.is_logged_in(row):
status_text = "已登录"
color = "绿色"
else:
status_text = "未登录"
color = "红色"
status_item = QTableWidgetItem(status_text)
self.site_table.setItem(row, 2, status_item)
self.update_cell_color(row, color) # 初始化时设置颜色
def on_checkbox_toggled(self, checked: bool, row: int):
site_url = self.config.get_site_url(row)
print(f"复选框状态:{checked}, 网址:{site_url}")
try:
if not self.browser:
asyncio.get_event_loop().run_until_complete(self.start_playwright())
if checked:
asyncio.get_event_loop().run_until_complete(
self.show_or_create_page(site_url, row)
)
else:
asyncio.get_event_loop().run_until_complete(
self.hide_page(site_url)
)
except Exception as e:
print(f"复选框事件错误:{str(e)}")
async def show_or_create_page(self, url: str, row: int):
try:
if not self.browser:
await self.start_playwright()
if url not in self.pages:
await self.create_playwright_page(url, row)
page = self.pages[url]
await page.bring_to_front()
self.update_cell_color(row, "黄色")
except Exception as e:
print(f"页面操作失败:{str(e)}")
await self.start_playwright() # 异常时重新启动
await self.show_or_create_page(url, row) # 重试
async def hide_page(self, url: str):
page = self.pages.get(url)
if page:
await page.close()
del self.pages[url]
hwnd = self.window_handles.pop(url, None)
if hwnd:
win32gui.ShowWindow(hwnd, win32con.SW_HIDE)
self.update_cell_color(self.get_row_by_url(url), "红色")
# 不要调用 self.browser.close()!
async def create_playwright_page(self, url, row):
if not self.browser: # 检查浏览器是否存在
await self.start_playwright() # 若不存在则重新启动
# 获取路径并确保目录存在
path = self.config.get_cookies_path(row)
dir_name = os.path.dirname(path)
os.makedirs(dir_name, exist_ok=True) # 确保目录存在
# 检查文件是否存在,决定是否使用storage_state
storage_state = path if os.path.exists(path) else None
# 创建页面时使用storage_state(如果存在)
page = await self.browser.new_page(storage_state=storage_state)
self.pages[url] = page
await page.goto(url)
browser_widget = QLabel(f"外部窗口:{url}")
self.browsers[url] = browser_widget
# self.browser_stack.addWidget(browser_widget)
page.on("load", lambda: self.load_finished.emit(url))
page.on("frame navigated", lambda: self.load_started.emit(url))
self.update_cell_color(row, "黄色")
def manage_window(self, url, action='show'):
"""窗口管理函数"""
hwnd = self.window_handles.get(url)
if not hwnd:
return
if action == 'hide':
win32gui.ShowWindow(hwnd, win32con.SW_HIDE)
elif action == 'show':
win32gui.ShowWindow(hwnd, win32con.SW_SHOW)
win32gui.SetForegroundWindow(hwnd)
elif action == 'minimize':
win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE)
# 使用时
async def show_or_create_page(self, url: str, row: int):
if url not in self.pages:
await self.create_playwright_page(url, row)
self.manage_window(url, 'show')
self.update_cell_color(row, "yellow")
def on_site_selected(self, row):
site_url = self.config.get_site_url(row)
cookies_path= self.config.get_cookies_path_url(row)
print(f"选中网站:{site_url}")
# 创建新页面
asyncio.get_event_loop().run_until_complete(self.create_playwright_page(site_url, row))
if site_url in self.pages:
# 若存在,无需操作(Playwright 页面已打开)
pass
else:
# 2. 创建新页面
asyncio.get_event_loop().run_until_complete(
self.create_playwright_page(site_url, row)
)
# 3. 更新当前选中的 URL(用于后续操作)
self.current_url = site_url
def refresh_site(self, url):
if self.current_browser and self.current_browser.url == url:
self.current_browser.reload()
self.current_browser.resize_browser()
# 文件:main.py
def update_status(self, url, message):
"""更新状态栏文本"""
current_text = self.status_box.toPlainText()
new_text = f"{current_text}\n{message}: {url}"
self.status_box.setPlainText(new_text)
def update_cell_color(self, row, color):
item = self.site_table.item(row, 1)
if item:
if color == "黄色":
item.setBackground(QColor(255, 255, 0)) # 黄色
elif color == "绿色":
item.setBackground(QColor(0, 255, 0)) # 绿色
else:
item.setBackground(QColor(255, 0, 0)) # 默认红色
def is_logged_in(self, row):
"""检查指定行的站点是否已登录"""
cookies_path = self.config.get_cookies_path(row)
login_key = self.config.get_login_cookie_key(row)
if not os.path.exists(cookies_path) or not login_key:
return False
try:
with open(cookies_path, 'r') as f:
cookies = json.load(f)
# 检查指定的登录标识 Cookie 是否存在且非空
for cookie in cookies.get('cookies', []):
if cookie['name'] == login_key and cookie['value']:
return True
except Exception as e:
print(f"读取 Cookies 失败:{str(e)}")
return False
def on_load_finished(self, url):
"""页面加载完成时更新登录状态"""
row = self.get_row_by_url(url)
if row != -1:
if self.is_logged_in(row):
self.update_cell_color(row, "绿色")
self.site_table.item(row, 2).setText("已登录")
else:
self.update_cell_color(row, "红色")
self.site_table.item(row, 2).setText("未登录")
def check_login_status(self):
"""自动检查并保存未登录的站点"""
try:
for row in range(self.site_table.rowCount()):
if not self.browser: # 浏览器未启动则跳过
continue
# 获取站点URL
url = self.config.get_site_url(row)
if url not in self.pages:
continue # 页面未加载,无法保存
# 自动触发保存
asyncio.get_event_loop().run_until_complete(
self.save_storage_state(row)
)
except Exception as e:
print(f"定时检查失败:{str(e)}")