From ef8237d8d1e2841dba1e7e5352d5cd0d4e470ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E9=B9=8F?= Date: Fri, 10 Apr 2026 14:37:23 +0800 Subject: [PATCH] =?UTF-8?q?add=20=E9=A1=B9=E7=9B=AE=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E6=88=AA=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project-screenshot-automation-plan.md | 463 +++++ __pycache__/config_loader.cpython-311.pyc | Bin 9919 -> 12654 bytes __pycache__/gui.cpython-311.pyc | Bin 53491 -> 90285 bytes .../project_screenshot.cpython-311.pyc | Bin 0 -> 80985 bytes bat/close_cmd.bat | 3 + bat/run_admin.bat | 8 + bat/run_front.bat | 8 + bat/run_install_admin.bat | 25 + bat/run_install_front.bat | 25 + bat/run_install_server.bat | 47 + bat/run_server.bat | 99 + bat/run_sql.bat | 34 + config_loader.py | 87 + gui.py | 534 +++++- project_screenshot.py | 1611 +++++++++++++++++ 15 files changed, 2943 insertions(+), 1 deletion(-) create mode 100644 .trae/documents/project-screenshot-automation-plan.md create mode 100644 __pycache__/project_screenshot.cpython-311.pyc create mode 100644 bat/close_cmd.bat create mode 100644 bat/run_admin.bat create mode 100644 bat/run_front.bat create mode 100644 bat/run_install_admin.bat create mode 100644 bat/run_install_front.bat create mode 100644 bat/run_install_server.bat create mode 100644 bat/run_server.bat create mode 100644 bat/run_sql.bat create mode 100644 project_screenshot.py diff --git a/.trae/documents/project-screenshot-automation-plan.md b/.trae/documents/project-screenshot-automation-plan.md new file mode 100644 index 0000000..9afa86c --- /dev/null +++ b/.trae/documents/project-screenshot-automation-plan.md @@ -0,0 +1,463 @@ +# 项目截图自动化功能实施计划 + +## 需求分析 + +根据用户提供的八爪鱼 RPA 流程图,需要实现以下功能: + +### 项目结构 + +* **服务后端**: Java SpringBoot 项目 + +* **前台前端**: Vue3 前端项目(部分项目可能没有) + +* **后台前端**: Vue3 管理后台项目 + +### 脚本文件 + +* `run_install_server.bat` - 后端 Maven 构建 + +* `run_install_front.bat` - 前台前端依赖安装 + +* `run_install_admin.bat` - 后台前端依赖安装 + +* `run_server.bat` - 启动服务后端 + +* `run_front.bat` - 启动前台前端 + +* `run_admin.bat` - 启动后台前端 + +* `run_sql.bat` - 导入 SQL 脚本(SQL 脚本在后端项目文件夹下的 db 文件夹中) + +### 执行流程(根据八爪鱼流程图) + +``` +主流程 +├── 初始化变量 +├── 打开网页 +├── 导入 SQL +├── 更换图片 +├── 后台网页截图 +├── 启动后端服务 +├── 启动前端服务 +├── 前端依赖 install +├── 前台网页截图 +├── 收尾工作 +└── 准备工作 +``` + +## 实施步骤 + +### 阶段 1: 创建核心模块 + +#### 1.1 创建 `project_screenshot.py` 模块 + +* 实现 `ProjectScreenshotAutomation` 类 + +* 功能包括: + + * 项目路径配置管理 + + * 执行 bat 脚本(构建、安装依赖、启动服务) + + * SQL 导入功能 + + * 使用 Playwright 进行浏览器控制和截图 + + * 菜单识别和遍历截图 + +#### 1.2 关键功能实现 + +**脚本执行功能:** + +```python +def execute_bat(bat_path: str, cwd: str = None, timeout: int = 300) -> tuple[bool, str]: + """执行 bat 脚本并返回结果""" + +def wait_for_service(url: str, timeout: int = 120) -> bool: + """等待服务启动完成""" +``` + +**SQL 导入功能:** + +```python +def import_sql(sql_file: str, db_config: dict) -> bool: + """导入 SQL 脚本到数据库""" +``` + +**更换前台轮播图片功能:** + +```python +def get_random_images(bg_pic_folder: str, count: int = 3) -> list: + """从指定文件夹随机获取指定数量的图片文件""" + +def copy_images_to_target(source_images: list, target_folder: str) -> bool: + """将源图片复制到目标文件夹并重命名为 swiperPicture1.jpg 等""" + +def replace_swiper_images(project_path: str, desktop_path: str) -> str: + """ + 更换前台轮播图片 + 1. 从桌面 bg_pic 文件夹随机选择 3 张图片 + 2. 复制到项目 server/src/main/resources/static/file 目录 + 3. 重命名为 swiperPicture1.jpg、swiperPicture2.jpg、swiperPicture3.jpg + """ +``` + +**更换后台登录背景图功能:** + +```python +def update_login_vue_background(project_path: str) -> bool: + """ + 修改 login.vue 文件中的背景图片链接 + 1. 从预设图片列表中随机选择一张 + 2. 替换 front/manage_code/src/views/login.vue 中的 background: url() + """ + +# 预设后台登录背景图片列表 +LOGIN_BACKGROUND_IMAGES = [ + "http://img.yidaima.cn/7-dcd4fc1e8d0144aaa064bc282d1af289", + "http://img.yidaima.cn/6-3db67e2e6ff64e018d48282638900a67", + "http://img.yidaima.cn/5-74ee6fe2f0954a8db2574e8f7fbb8ef2", + "http://img.yidaima.cn/4-a926593ba8c444a3984100e99ae322bb", + "http://img.yidaima.cn/3-f91e9a0ed55d45c9aff80a08f554c625", + "http://img.yidaima.cn/2-2bd41cdd57b54b529dc3a139e66233e0", + "http://img.yidaima.cn/1-5a0b7ee2344a46bd987c1cafff19b3ba", + "http://img.yidaima.cn/8-495b9767a2f34197b963a13226d3e1d2", + "http://img.yidaima.cn/10-991483f941914f3d85bf87a4f8748fee", + "http://img.yidaima.cn/11-d274f1d695f7472a8fcd2c8c6238a79e", + "http://img.yidaima.cn/14-0e30653336d849379060ad8c436fe512", + "http://img.yidaima.cn/16-7a58235a2a4040f99e3431c3cac9ea7a", + "http://img.yidaima.cn/17-3406a546b81c412d9e3501cdb0f88b12", + "http://img.yidaima.cn/18-0a2fae1d6634480dabf5471fa86b3623", + "http://img.yidaima.cn/19-107196938e074c92a40b62c60aed18a4" +] +``` + +**截图功能:** + +```python +def capture_screenshots(base_url: str, menu_selectors: list, output_dir: str) -> list: + """根据菜单进行截图""" +``` + +### 阶段 2: 配置管理扩展 + +#### 2.1 更新 `config_loader.py` + +添加项目截图自动化相关配置: + +```yaml +project_screenshot: + # 项目路径配置 + project_path: "" + has_front: true # 是否有前台前端 + + # 脚本路径配置(相对项目路径或绝对路径) + scripts: + install_server: "run_install_server.bat" # 后端 Maven 构建 + install_front: "run_install_front.bat" # 前台前端依赖安装 + install_admin: "run_install_admin.bat" # 后台前端依赖安装 + run_server: "run_server.bat" # 启动服务后端 + run_front: "run_front.bat" # 启动前台前端 + run_admin: "run_admin.bat" # 启动后台前端 + run_sql: "run_sql.bat" # 导入 SQL 脚本 + + # SQL 配置 + sql: + script_path: "server/db/init.sql" # SQL 脚本路径(相对项目路径) + + # 服务配置 + services: + backend: + url: "http://localhost:8080" + health_endpoint: "/actuator/health" + startup_timeout: 120 + front: + url: "http://localhost:8082" # 前台前端地址 + login_url: "http://localhost:8082/#/login" # 前台登录页面 + startup_timeout: 60 + admin: + url: "http://localhost:8081" # 后台前端地址 + login_url: "http://localhost:8081/#/login" # 后台登录页面 + startup_timeout: 60 + + # 登录配置 + login: + admin: # 后台登录配置 + username: "admin" + password: "admin" + username_selector: "input[placeholder='请输入用户名'], input[name='username'], #username" + password_selector: "input[placeholder='请输入密码'], input[name='password'], #password, input[type='password']" + submit_selector: "button[type='submit'], .login-btn, .el-button--primary" + front: # 前台登录配置 + username: "2" + password: "123456" + username_selector: "input[placeholder='请输入用户名'], input[name='username'], #username" + password_selector: "input[placeholder='请输入密码'], input[name='password'], #password, input[type='password']" + submit_selector: "button[type='submit'], .login-btn, .el-button--primary" + + # 截图配置 + screenshot: + output_dir: "./screenshots" + full_page: true + menu_selectors: # 菜单识别选择器 + admin: ".el-menu-item, .el-sub-menu__title" + front: ".nav-item, .menu-item" + delay: 2000 # 截图前等待时间(ms) + + # 轮播图片配置 + swiper_images: + source_folder: "{desktop_path}/bg_pic" # 图片来源文件夹(桌面 bg_pic) + target_subpath: "server/src/main/resources/static/file" # 目标路径(相对于项目路径) + target_names: + - "swiperPicture1.jpg" + - "swiperPicture2.jpg" + - "swiperPicture3.jpg" + count: 3 # 随机选择的图片数量 + + # 后台登录背景图配置 + login_background: + vue_file_subpath: "front/manage_code/src/views/login.vue" # login.vue 相对项目路径 + images: # 预设背景图片列表 + - "http://img.yidaima.cn/7-dcd4fc1e8d0144aaa064bc282d1af289" + - "http://img.yidaima.cn/6-3db67e2e6ff64e018d48282638900a67" + - "http://img.yidaima.cn/5-74ee6fe2f0954a8db2574e8f7fbb8ef2" + - "http://img.yidaima.cn/4-a926593ba8c444a3984100e99ae322bb" + - "http://img.yidaima.cn/3-f91e9a0ed55d45c9aff80a08f554c625" + - "http://img.yidaima.cn/2-2bd41cdd57b54b529dc3a139e66233e0" + - "http://img.yidaima.cn/1-5a0b7ee2344a46bd987c1cafff19b3ba" + - "http://img.yidaima.cn/8-495b9767a2f34197b963a13226d3e1d2" + - "http://img.yidaima.cn/10-991483f941914f3d85bf87a4f8748fee" + - "http://img.yidaima.cn/11-d274f1d695f7472a8fcd2c8c6238a79e" + - "http://img.yidaima.cn/14-0e30653336d849379060ad8c436fe512" + - "http://img.yidaima.cn/16-7a58235a2a4040f99e3431c3cac9ea7a" + - "http://img.yidaima.cn/17-3406a546b81c412d9e3501cdb0f88b12" + - "http://img.yidaima.cn/18-0a2fae1d6634480dabf5471fa86b3623" + - "http://img.yidaima.cn/19-107196938e074c92a40b62c60aed18a4" +``` + +### 阶段 3: GUI 界面开发 + +#### 3.1 在 `gui.py` 中添加新的 Tab + +创建 "项目截图" Tab,包含以下区域: + +**配置区域:** + +* 项目路径选择(文件夹浏览器) + +* 是否有前台前端复选框 + +* 各脚本路径配置(可编辑,有默认值) + +* SQL 脚本路径配置 + +**服务配置区域:** + +* 后端服务 URL 和端口 + +* 前台前端 URL 和端口 + +* 后台前端 URL 和端口 + +**操作按钮区域:** + +1. **构建/安装按钮:** + + * "Maven 构建后端" - 执行 run\_install\_server.bat + + * "安装前台依赖" - 执行 run\_install\_front.bat + + * "安装后台依赖" - 执行 run\_install\_admin.bat + + * "导入 SQL" - 执行 run\_sql.bat + +2. **启动按钮:** + + * "启动后端服务" - 执行 run\_server.bat + + * "启动前台前端" - 执行 run\_front.bat + + * "启动后台前端" - 执行 run\_admin.bat + +3. **截图按钮:** + + * "后台截图" - 遍历后台菜单截图 + + * "前台截图" - 遍历前台菜单截图(如果有) + + * "一键完整流程" - 按顺序执行所有步骤 + +**日志输出区域:** + +* 显示执行日志 + +* 进度条显示当前步骤 + +### 阶段 4: 流程实现 + +#### 4.1 完整流程执行顺序 + +根据八爪鱼流程图,实现以下执行顺序: + +``` +1. 初始化变量 + - 加载项目配置 + - 检查必要文件是否存在 + +2. 准备工作 + - 检查项目路径 + - 验证脚本文件存在 + - 创建截图输出目录 + +3. 导入 SQL + - 执行 run_sql.bat + - 或使用 Python 直接执行 SQL 文件 + +4. 更换图片 + - **更换前台轮播图片**: + - 从桌面 bg_pic 文件夹随机选择 3 张图片 + - 复制到项目 server/src/main/resources/static/file 目录 + - 重命名为 swiperPicture1.jpg、swiperPicture2.jpg、swiperPicture3.jpg + - **更换后台登录背景图**: + - 修改 front/manage_code/src/views/login.vue 文件 + - 从预设图片列表中随机选择一张背景图 + - 替换 CSS 中的 background: url() 属性 + +5. 前端依赖 install + - 执行 run_install_front.bat(如果有前台) + - 执行 run_install_admin.bat + +6. 启动后端服务 + - 执行 run_server.bat + - 等待服务健康检查通过 + +7. 启动前端服务 + - 执行 run_front.bat(如果有前台) + - 执行 run_admin.bat + - 等待服务启动 + +8. 打开后台网页并登录 + - 使用 Playwright 打开后台登录页面 http://localhost:8081/#/login + - 输入用户名 admin,密码 admin + - 点击登录按钮 + - 等待登录成功跳转 + +9. 后台网页截图 + - 登录成功后访问 http://localhost:8081 + - 识别菜单项 + - 遍历每个菜单进行截图 + +10. 前台网页截图(如果有前台) + - 打开前台登录页面 http://localhost:8082/#/login + - 输入用户名 2,密码 123456 + - 点击登录按钮 + - 登录成功后访问 http://localhost:8082 + - 识别菜单项 + - 遍历每个菜单进行截图 + +11. 收尾工作 + - 关闭浏览器 + - 停止服务(可选) + - 生成截图报告 +``` + +### 阶段 5: 截图功能实现 + +#### 5.1 菜单识别和截图 + +```python +class ScreenshotCapture: + def __init__(self, browser, output_dir: str): + self.browser = browser + self.output_dir = output_dir + + async def login(self, login_url: str, username: str, password: str, + username_selector: str, password_selector: str, submit_selector: str): + """ + 执行登录操作 + 1. 打开登录页面 + 2. 输入用户名和密码 + 3. 点击登录按钮 + 4. 等待登录成功 + """ + + async def capture_menu_screenshots(self, base_url: str, menu_selector: str): + """ + 1. 访问基础页面(已登录状态) + 2. 识别所有菜单项 + 3. 点击每个菜单并截图 + 4. 保存到输出目录 + """ + + async def capture_full_page(self, url: str, filename: str): + """截取整页截图""" +``` + +### 阶段 6: 测试和验证 + +#### 6.1 功能测试 + +* [ ] 单个 bat 脚本执行测试 + +* [ ] SQL 导入功能测试 + +* [ ] 服务启动和等待测试 + +* [ ] 菜单识别和截图测试 + +* [ ] 完整流程测试 + +#### 6.2 边界情况处理 + +* 项目没有前台前端的情况 + +* 服务启动超时处理 + +* 菜单识别失败处理 + +* 截图保存失败处理 + +## 文件结构 + +``` +yidaima/ +├── gui.py # 主 GUI(添加项目截图 Tab) +├── config_loader.py # 配置加载器(扩展) +├── project_screenshot.py # 项目截图自动化核心模块(新增) +├── core/ +│ └── browser.py # 浏览器控制(扩展 Playwright) +└── utils/ + └── screenshot.py # 截图工具(扩展) +``` + +## 依赖项 + +需要添加的依赖: + +``` +playwright>=1.40.0 +``` + +安装命令: + +```bash +pip install playwright +playwright install chromium +``` + +## 实施顺序 + +1. **创建** **`project_screenshot.py`** - 核心自动化模块 +2. **扩展** **`config_loader.py`** - 添加项目截图配置 +3. **扩展** **`gui.py`** - 添加项目截图 Tab +4. **测试验证** - 完整流程测试 +5. **打包发布** - PyInstaller 打包 exe + +## 注意事项 + +1. **服务进程管理**: 启动的服务进程需要在适当的时候关闭 +2. **端口占用检查**: 启动服务前检查端口是否被占用 +3. **超时处理**: 每个步骤都需要有合理的超时时间 +4. **错误恢复**: 某一步失败时能够优雅地停止并报告 +5. **并发安全**: 避免同时执行多个自动化流程 + diff --git a/__pycache__/config_loader.cpython-311.pyc b/__pycache__/config_loader.cpython-311.pyc index dbd85d85151a43eca97bdb1a5ab9289b7b52caef..b489816232406e2f8ccf020792bbb3fefd6347d7 100644 GIT binary patch delta 3771 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

lPlIM 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 delta 7859 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 diff --git a/__pycache__/project_screenshot.cpython-311.pyc b/__pycache__/project_screenshot.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..18150037f12fb92008716b36962821bc7fb0e0bc GIT binary patch literal 80985 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 NT)fVh3Jl3?{lA}s?Qj49 literal 0 HcmV?d00001 diff --git a/bat/close_cmd.bat b/bat/close_cmd.bat new file mode 100644 index 0000000..ae0871b --- /dev/null +++ b/bat/close_cmd.bat @@ -0,0 +1,3 @@ +@echo off + +taskkill /f /im cmd.exe /t \ No newline at end of file diff --git a/bat/run_admin.bat b/bat/run_admin.bat new file mode 100644 index 0000000..850bbbd --- /dev/null +++ b/bat/run_admin.bat @@ -0,0 +1,8 @@ +@echo off +chcp 65001 >nul + +cd /d D:\code\front\manage_code + +npm run serve + +pause \ No newline at end of file diff --git a/bat/run_front.bat b/bat/run_front.bat new file mode 100644 index 0000000..35ac674 --- /dev/null +++ b/bat/run_front.bat @@ -0,0 +1,8 @@ +@echo off +chcp 65001 >nul + +cd /d D:\code\front\client_code + +npm run serve + +pause \ No newline at end of file diff --git a/bat/run_install_admin.bat b/bat/run_install_admin.bat new file mode 100644 index 0000000..7ba3aca --- /dev/null +++ b/bat/run_install_admin.bat @@ -0,0 +1,25 @@ +@echo off +:: 设置编码为 UTF-8 +chcp 65001 >nul + +:: 1. 增加 /d 参数切换盘符,并检查路径是否存在 +cd /d D:\code\front\manage_code +if %errorlevel% neq 0 ( + echo [错误] 无法找到目录: D:\code\front\manage_code + pause + exit /b +) + +echo 当前目录: %cd% + +:: 2. 判断 node_modules 是否存在 +if not exist "node_modules\" ( + echo 未检测到 node_modules 目录,开始安装依赖... + :: 使用 call 确保 npm 执行完后脚本继续运行 + call npm install +) else ( + echo 检测到 node_modules 目录,跳过依赖安装。 +) + +echo. +echo 执行完毕! \ No newline at end of file diff --git a/bat/run_install_front.bat b/bat/run_install_front.bat new file mode 100644 index 0000000..2eb4ff0 --- /dev/null +++ b/bat/run_install_front.bat @@ -0,0 +1,25 @@ +@echo off +:: 设置编码为 UTF-8 +chcp 65001 >nul + +:: 1. 增加 /d 参数切换盘符,并检查路径是否存在 +cd /d D:\code\front\client_code +if %errorlevel% neq 0 ( + echo [错误] 无法找到目录: D:\code\front\client_code + pause + exit /b +) + +echo 当前目录: %cd% + +:: 2. 判断 node_modules 是否存在 +if not exist "node_modules\" ( + echo 未检测到 node_modules 目录,开始安装依赖... + :: 使用 call 确保 npm 执行完后脚本继续运行 + call npm install +) else ( + echo 检测到 node_modules 目录,跳过依赖安装。 +) + +echo. +echo 执行完毕! \ No newline at end of file diff --git a/bat/run_install_server.bat b/bat/run_install_server.bat new file mode 100644 index 0000000..a357606 --- /dev/null +++ b/bat/run_install_server.bat @@ -0,0 +1,47 @@ +@echo off +chcp 65001 >nul +setlocal enabledelayedexpansion + +:: 强制切换到脚本所在目录(解决管理员模式启动默认在System32的问题) +cd /d "%~dp0" + +echo 正确盘符: %~d0 +echo 正确路径: %cd% + +:: 使用/d参数强制切换驱动器+目录 +cd /d "D:\code\server" 2>nul +if %errorlevel% neq 0 ( + echo [ERROR] Can not enter D:\code\server + echo 请检查: + echo 1. D盘是否存在 + echo 2. code目录是否存在 + echo 3. 是否有权限访问 + exit /b +) + +echo 扫描子目录中... +dir /ad /b + +set "first_dir=" +for /f "delims=" %%d in ('dir /ad /b 2^>nul') do ( + set "first_dir=%%d" + goto :dir_found +) + +:dir_found +if not defined first_dir ( + echo [ERROR] No subdirectory found + exit /b +) + +echo 进入子目录: !first_dir! +cd /d "!first_dir!" +echo 当前路径: %cd% + +if not exist "target\" ( + echo 执行Maven构建... + call mvn clean package +) else ( + pause + echo 检测到 target 目录,跳过Maven构建。 +) \ No newline at end of file diff --git a/bat/run_server.bat b/bat/run_server.bat new file mode 100644 index 0000000..e3e73cd --- /dev/null +++ b/bat/run_server.bat @@ -0,0 +1,99 @@ +@echo off +chcp 65001 >nul +setlocal enabledelayedexpansion + +:: 强制切换到脚本所在目录(解决管理员模式启动默认在System32的问题) +cd /d "%~dp0" + +echo 正确盘符: %~d0 +echo 正确路径: %cd% + +:: 使用/d参数强制切换驱动器+目录 +cd /d "D:\code\server" 2>nul +if %errorlevel% neq 0 ( + echo [ERROR] Can not enter D:\code\server + echo 请检查: + echo 1. D盘是否存在 + echo 2. code目录是否存在 + echo 3. 是否有权限访问 + exit /b +) + +echo 扫描子目录中... +dir /ad /b + +set "first_dir=" +for /f "delims=" %%d in ('dir /ad /b 2^>nul') do ( + set "first_dir=%%d" + goto :dir_found +) + +:dir_found +if not defined first_dir ( + echo [ERROR] No subdirectory found + exit /b +) + +echo 进入子目录: !first_dir! +cd /d "!first_dir!" +echo 当前路径: %cd% + +if not exist "target\" ( + echo 执行Maven构建... + call mvn clean package +) else ( + echo 检测到 target 目录,跳过Maven构建。 +) + +echo 进入target目录... +cd target +echo 目录内容: +dir /b + +:: ========== 新增端口检查功能 ========== +echo 正在检查8080端口占用... +set "port_pid=" +for /f "tokens=5" %%p in ('netstat -ano -p tcp ^| findstr ":8080" ^| findstr "LISTENING"') do ( + set "port_pid=%%p" + echo 检测到端口占用PID: !port_pid! + taskkill /F /PID !port_pid! >nul 2>&1 && ( + echo 成功终止进程: !port_pid! + ) || ( + echo [警告] 无法终止进程: !port_pid! + echo 可能需要管理员权限,请右键用管理员身份运行 + ) +) + +if not defined port_pid ( + echo 8080端口未被占用 +) + +:: ========== 端口检查结束 ========== + +set "jar_file=" +for /f "delims=" %%j in ('dir /b /a-d *.jar ^| findstr /v "original.jar"') do ( + set "jar_file=%%j" + goto :jar_found +) + +:jar_found +if not defined jar_file ( + echo [ERROR] No valid jar found + echo 建议检查: + echo 1. Maven构建是否成功 + echo 2. target目录内容 + dir /b *.jar + exit /b +) + +echo 启动Jar包: !jar_file! +java -jar "!jar_file!" +if %errorlevel% neq 0 ( + echo [ERROR] Java启动失败 (code: %errorlevel%) + echo 可能原因: + echo 1. 缺少依赖 + echo 2. 端口占用 + echo 3. 配置错误 + pause +) + diff --git a/bat/run_sql.bat b/bat/run_sql.bat new file mode 100644 index 0000000..5af8a89 --- /dev/null +++ b/bat/run_sql.bat @@ -0,0 +1,34 @@ +@echo off +chcp 65001 >nul + +cd /d D:\code\db + +set "MYSQL_PATH=C:\Program Files\MySQL\MySQL Server 8.0\bin\mysql.exe" + +for %%F in (*.sql) do ( + echo 正在处理文件: %%F + "%MYSQL_PATH%" -u root -p123456 -e "CREATE DATABASE IF NOT EXISTS %%~nF;" + + :: 第一次导入尝试 + "%MYSQL_PATH%" -u root -p123456 %%~nF < %%F + + :: 检查数据库是否存在 + "%MYSQL_PATH%" -u root -p123456 -e "USE %%~nF;" 2>nul + if errorlevel 1 ( + echo 数据库 %%~nF 第一次导入失败,尝试重新导入... + + :: 第二次导入尝试 + "%MYSQL_PATH%" -u root -p123456 -e "DROP DATABASE IF EXISTS %%~nF; CREATE DATABASE %%~nF;" + "%MYSQL_PATH%" -u root -p123456 %%~nF < %%F + + :: 再次检查 + "%MYSQL_PATH%" -u root -p123456 -e "USE %%~nF;" 2>nul + if errorlevel 1 ( + echo 错误: 数据库 %%~nF 导入失败! + ) else ( + echo 数据库 %%~nF 重新导入成功 + ) + ) else ( + echo 数据库 %%~nF 导入完成 + ) +) \ No newline at end of file diff --git a/config_loader.py b/config_loader.py index 3c24211..179c2f1 100644 --- a/config_loader.py +++ b/config_loader.py @@ -51,6 +51,89 @@ class Config: "database": "test", "user": "root", "password": "123456" + }, + "project_screenshot": { + "project_path": "", + "desktop_path": "C:\\Users\\南音\\Desktop", + "has_front": True, + "bat_folder": "C:\\Users\\南音\\Desktop\\yidaima\\bat", + "show_cmd_window": True, + "code_source_folder": "code", + "code_target_path": "D:\\code", + "scripts": { + "install_server": "run_install_server.bat", + "install_front": "run_install_front.bat", + "install_admin": "run_install_admin.bat", + "run_server": "run_server.bat", + "run_front": "run_front.bat", + "run_admin": "run_admin.bat", + "run_sql": "run_sql.bat" + }, + "sql": { + "script_path": "server/db/init.sql" + }, + "services": { + "backend": { + "url": "http://localhost:8080", + "health_endpoint": "/actuator/health", + "startup_timeout": 120 + }, + "front": { + "url": "http://localhost:8082", + "login_url": "http://localhost:8082/#/login", + "startup_timeout": 60 + }, + "admin": { + "url": "http://localhost:8081", + "login_url": "http://localhost:8081/#/login", + "startup_timeout": 60 + } + }, + "login": { + "admin": { + "username": "admin", + "password": "admin" + }, + "front": { + "username": "2", + "password": "123456" + } + }, + "screenshot": { + "output_dir": "./screenshots", + "full_page": True, + "menu_selectors": { + "admin": ".el-menu-item, .el-sub-menu__title", + "front": ".nav-item, .menu-item" + }, + "delay": 2000 + }, + "swiper_images": { + "source_folder": "bg_pic", + "target_subpath": "server/src/main/resources/static/file", + "target_names": ["swiperPicture1.jpg", "swiperPicture2.jpg", "swiperPicture3.jpg"], + "count": 3 + }, + "login_background": { + "vue_file_subpath": "front/manage_code/src/views/login.vue", + "images": [ + "http://img.yidaima.cn/7-dcd4fc1e8d0144aaa064bc282d1af289", + "http://img.yidaima.cn/6-3db67e2e6ff64e018d48282638900a67", + "http://img.yidaima.cn/5-74ee6fe2f0954a8db2574e8f7fbb8ef2", + "http://img.yidaima.cn/4-a926593ba8c444a3984100e99ae322bb", + "http://img.yidaima.cn/3-f91e9a0ed55d45c9aff80a08f554c625", + "http://img.yidaima.cn/2-2bd41cdd57b54b529dc3a139e66233e0", + "http://img.yidaima.cn/1-5a0b7ee2344a46bd987c1cafff19b3ba", + "http://img.yidaima.cn/8-495b9767a2f34197b963a13226d3e1d2", + "http://img.yidaima.cn/10-991483f941914f3d85bf87a4f8748fee", + "http://img.yidaima.cn/11-d274f1d695f7472a8fcd2c8c6238a79e", + "http://img.yidaima.cn/14-0e30653336d849379060ad8c436fe512", + "http://img.yidaima.cn/16-7a58235a2a4040f99e3431c3cac9ea7a", + "http://img.yidaima.cn/17-3406a546b81c412d9e3501cdb0f88b12", + "http://img.yidaima.cn/18-0a2fae1d6634480dabf5471fa86b3623", + "http://img.yidaima.cn/19-107196938e074c92a40b62c60aed18a4" + ] + } } } @@ -148,6 +231,10 @@ class Config: def database_config(self) -> dict: return self.get("database", {}) + @property + def project_screenshot_config(self) -> dict: + return self.get("project_screenshot", {}) + # 全局配置实例 _config_instance = None diff --git a/gui.py b/gui.py index 8b01de5..2cfe5df 100644 --- a/gui.py +++ b/gui.py @@ -15,6 +15,7 @@ from step2 import Step2Converter from editor_gui import open_editor from markdown_editor import ThemeManager from db_manager import get_db_manager, reset_db_manager, ProjectOrder +from project_screenshot import ProjectScreenshotAutomation, ProjectConfig class YidaimaGUI: @@ -86,7 +87,12 @@ class YidaimaGUI: self.tab_manage = ttk.Frame(self.notebook, padding="10") self.notebook.add(self.tab_manage, text="文章发布管理") self._create_manage_tab() - + + # === Tab 3: 项目运行截图 === + self.tab_screenshot = ttk.Frame(self.notebook, padding="10") + self.notebook.add(self.tab_screenshot, text="项目运行截图") + self._create_screenshot_tab() + # === Tab 4: 参数设置 === self.tab_settings = ttk.Frame(self.notebook, padding="10") self.notebook.add(self.tab_settings, text="参数设置") @@ -756,6 +762,532 @@ class YidaimaGUI: self.setting_db_user_var.set(self.config.get("database.user", "root")) self.setting_db_pass_var.set(self.config.get("database.password", "123456")) + # ==================== 项目运行截图 Tab 方法 ==================== + + def _create_screenshot_tab(self): + """创建项目运行截图 Tab""" + # 配置网格 + self.tab_screenshot.columnconfigure(0, weight=1) + self.tab_screenshot.rowconfigure(3, weight=1) + + # 获取项目截图配置 + ps_config = self.config.get("project_screenshot", {}) + + # === 项目路径配置区域 === + path_frame = ttk.LabelFrame(self.tab_screenshot, text="项目路径配置", padding="10") + path_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10)) + path_frame.columnconfigure(1, weight=1) + + # 项目路径 + ttk.Label(path_frame, text="项目路径:").grid(row=0, column=0, sticky=tk.W) + self.ps_project_path_var = tk.StringVar(value=ps_config.get("project_path", "")) + ps_project_path_entry = ttk.Entry(path_frame, textvariable=self.ps_project_path_var, width=60) + ps_project_path_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(10, 0)) + ttk.Button(path_frame, text="浏览...", command=self._browse_project_path, width=10).grid(row=0, column=2, padx=(10, 0)) + + # 桌面路径 + ttk.Label(path_frame, text="桌面路径:").grid(row=1, column=0, sticky=tk.W, pady=(10, 0)) + self.ps_desktop_path_var = tk.StringVar(value=ps_config.get("desktop_path", r"C:\Users\南音\Desktop")) + ps_desktop_path_entry = ttk.Entry(path_frame, textvariable=self.ps_desktop_path_var, width=60) + ps_desktop_path_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(10, 0)) + + # bat 文件夹名称 + ttk.Label(path_frame, text="bat文件夹:").grid(row=2, column=0, sticky=tk.W, pady=(10, 0)) + self.ps_bat_folder_var = tk.StringVar(value=ps_config.get("bat_folder", r"C:\Users\南音\Desktop\yidaima\bat")) + ps_bat_folder_entry = ttk.Entry(path_frame, textvariable=self.ps_bat_folder_var, width=60) + ps_bat_folder_entry.grid(row=2, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(10, 0)) + + # 代码目标路径 + ttk.Label(path_frame, text="代码目标路径:").grid(row=3, column=0, sticky=tk.W, pady=(10, 0)) + self.ps_code_target_var = tk.StringVar(value=ps_config.get("code_target_path", r"D:\code")) + ps_code_target_entry = ttk.Entry(path_frame, textvariable=self.ps_code_target_var, width=60) + ps_code_target_entry.grid(row=3, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(10, 0)) + + # 是否有前台前端 + self.ps_has_front_var = tk.BooleanVar(value=ps_config.get("has_front", True)) + ttk.Checkbutton(path_frame, text="有前台前端", variable=self.ps_has_front_var).grid(row=4, column=1, sticky=tk.W, padx=(10, 0), pady=(10, 0)) + + # 是否显示 CMD 窗口 + self.ps_show_cmd_var = tk.BooleanVar(value=ps_config.get("show_cmd_window", True)) + ttk.Checkbutton(path_frame, text="显示CMD窗口", variable=self.ps_show_cmd_var).grid(row=5, column=1, sticky=tk.W, padx=(10, 0), pady=(10, 0)) + + # === 操作按钮区域 === + button_frame = ttk.LabelFrame(self.tab_screenshot, text="操作按钮", padding="10") + button_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(0, 10)) + + # 构建/安装按钮 + install_frame = ttk.Frame(button_frame) + install_frame.pack(fill=tk.X, pady=(0, 10)) + + ttk.Button(install_frame, text="整理代码", command=self._ps_organize_code, width=15).pack(side=tk.LEFT, padx=(0, 5)) + ttk.Button(install_frame, text="Maven构建后端", command=self._ps_install_server, width=15).pack(side=tk.LEFT, padx=(5, 5)) + ttk.Button(install_frame, text="安装前台依赖", command=self._ps_install_front, width=15).pack(side=tk.LEFT, padx=(5, 5)) + ttk.Button(install_frame, text="安装后台依赖", command=self._ps_install_admin, width=15).pack(side=tk.LEFT, padx=(5, 5)) + ttk.Button(install_frame, text="导入SQL", command=self._ps_run_sql, width=15).pack(side=tk.LEFT, padx=(5, 0)) + + # 启动按钮 + start_frame = ttk.Frame(button_frame) + start_frame.pack(fill=tk.X, pady=(0, 10)) + + ttk.Button(start_frame, text="启动后端服务", command=self._ps_start_server, width=15).pack(side=tk.LEFT, padx=(0, 5)) + ttk.Button(start_frame, text="启动前台前端", command=self._ps_start_front, width=15).pack(side=tk.LEFT, padx=(5, 5)) + ttk.Button(start_frame, text="启动后台前端", command=self._ps_start_admin, width=15).pack(side=tk.LEFT, padx=(5, 5)) + ttk.Button(start_frame, text="更换图片", command=self._ps_replace_images, width=15).pack(side=tk.LEFT, padx=(5, 0)) + + # 截图按钮 + screenshot_frame = ttk.Frame(button_frame) + screenshot_frame.pack(fill=tk.X) + + ttk.Button(screenshot_frame, text="后台截图", command=self._ps_capture_admin, width=15).pack(side=tk.LEFT, padx=(0, 5)) + ttk.Button(screenshot_frame, text="前台截图", command=self._ps_capture_front, width=15).pack(side=tk.LEFT, padx=(5, 5)) + ttk.Button(screenshot_frame, text="收尾工作", command=self._ps_finalize_screenshots, width=15).pack(side=tk.LEFT, padx=(5, 5)) + ttk.Button(screenshot_frame, text="一键完整流程", command=self._ps_run_full_flow, width=20).pack(side=tk.LEFT, padx=(5, 5)) + ttk.Button(screenshot_frame, text="关闭所有CMD", command=self._ps_close_all_cmd, width=15).pack(side=tk.LEFT, padx=(5, 0)) + + # === 日志输出区域 === + log_frame = ttk.LabelFrame(self.tab_screenshot, text="运行日志", padding="10") + log_frame.grid(row=2, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(10, 0)) + log_frame.columnconfigure(0, weight=1) + log_frame.rowconfigure(0, weight=1) + + self.ps_log_text = scrolledtext.ScrolledText( + log_frame, + wrap=tk.WORD, + width=80, + height=15, + font=("Consolas", 9) + ) + self.ps_log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # 进度条 + self.ps_progress = ttk.Progressbar( + self.tab_screenshot, + mode='indeterminate', + length=400 + ) + self.ps_progress.grid(row=3, column=0, pady=(10, 0), sticky=(tk.W, tk.E)) + + # 状态标签 + self.ps_status_var = tk.StringVar(value="就绪") + self.ps_status_label = ttk.Label( + self.tab_screenshot, + textvariable=self.ps_status_var, + foreground="#666" + ) + self.ps_status_label.grid(row=4, column=0, pady=(10, 0)) + + def _browse_project_path(self): + """浏览项目路径""" + from tkinter import filedialog + path = filedialog.askdirectory(title="选择项目文件夹") + if path: + self.ps_project_path_var.set(path) + + def _ps_log(self, message: str): + """添加项目截图日志""" + self.ps_log_text.insert(tk.END, f"{message}\n") + self.ps_log_text.see(tk.END) + self.root.update_idletasks() + + def _get_ps_config(self) -> ProjectConfig: + """获取项目截图配置""" + ps_config = self.config.get("project_screenshot", {}) + return ProjectConfig( + project_path=self.ps_project_path_var.get(), + desktop_path=self.ps_desktop_path_var.get(), + has_front=self.ps_has_front_var.get(), + install_server=ps_config.get("scripts", {}).get("install_server", "run_install_server.bat"), + install_front=ps_config.get("scripts", {}).get("install_front", "run_install_front.bat"), + install_admin=ps_config.get("scripts", {}).get("install_admin", "run_install_admin.bat"), + run_server=ps_config.get("scripts", {}).get("run_server", "run_server.bat"), + run_front=ps_config.get("scripts", {}).get("run_front", "run_front.bat"), + run_admin=ps_config.get("scripts", {}).get("run_admin", "run_admin.bat"), + run_sql=ps_config.get("scripts", {}).get("run_sql", "run_sql.bat"), + bat_folder=self.ps_bat_folder_var.get(), + sql_script_path=ps_config.get("sql", {}).get("script_path", "server/db/init.sql"), + backend_url=ps_config.get("services", {}).get("backend", {}).get("url", "http://localhost:8080"), + backend_startup_timeout=ps_config.get("services", {}).get("backend", {}).get("startup_timeout", 120), + front_url=ps_config.get("services", {}).get("front", {}).get("url", "http://localhost:8082"), + front_login_url=ps_config.get("services", {}).get("front", {}).get("login_url", "http://localhost:8082/#/login"), + front_startup_timeout=ps_config.get("services", {}).get("front", {}).get("startup_timeout", 60), + admin_url=ps_config.get("services", {}).get("admin", {}).get("url", "http://localhost:8081"), + admin_login_url=ps_config.get("services", {}).get("admin", {}).get("login_url", "http://localhost:8081/#/login"), + admin_startup_timeout=ps_config.get("services", {}).get("admin", {}).get("startup_timeout", 60), + admin_username=ps_config.get("login", {}).get("admin", {}).get("username", "admin"), + admin_password=ps_config.get("login", {}).get("admin", {}).get("password", "admin"), + front_username=ps_config.get("login", {}).get("front", {}).get("username", "2"), + front_password=ps_config.get("login", {}).get("front", {}).get("password", "123456"), + screenshot_output_dir=ps_config.get("screenshot", {}).get("output_dir", "./screenshots"), + screenshot_delay=ps_config.get("screenshot", {}).get("delay", 2000), + admin_menu_selector=ps_config.get("screenshot", {}).get("menu_selectors", {}).get("admin", ".el-menu-item, .el-sub-menu__title"), + front_menu_selector=ps_config.get("screenshot", {}).get("menu_selectors", {}).get("front", ".nav-item, .menu-item"), + swiper_source_folder=ps_config.get("swiper_images", {}).get("source_folder", "bg_pic"), + swiper_target_subpath=ps_config.get("swiper_images", {}).get("target_subpath", "server/src/main/resources/static/file"), + swiper_target_names=ps_config.get("swiper_images", {}).get("target_names", ["swiperPicture1.jpg", "swiperPicture2.jpg", "swiperPicture3.jpg"]), + swiper_count=ps_config.get("swiper_images", {}).get("count", 3), + login_vue_subpath=ps_config.get("login_background", {}).get("vue_file_subpath", "front/manage_code/src/views/login.vue"), + login_background_images=ps_config.get("login_background", {}).get("images", []), + code_source_folder=ps_config.get("code_source_folder", "code"), + code_target_path=self.ps_code_target_var.get(), + show_cmd_window=self.ps_show_cmd_var.get() + ) + + def _ps_set_running(self, running: bool): + """设置项目截图运行状态""" + if running: + self.ps_progress.start() + self.ps_status_var.set("运行中...") + else: + self.ps_progress.stop() + self.ps_status_var.set("就绪") + + def _ps_install_server(self): + """Maven 构建后端""" + def run(): + try: + self._ps_set_running(True) + self._ps_log("=" * 50) + self._ps_log("开始 Maven 构建后端...") + config = self._get_ps_config() + automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) + success, msg = automation.install_server() + if success: + self._ps_log("Maven 构建完成!") + else: + self._ps_log(f"构建失败: {msg}") + except Exception as e: + self._ps_log(f"错误: {str(e)}") + finally: + self._ps_set_running(False) + + threading.Thread(target=run, daemon=True).start() + + def _ps_install_front(self): + """安装前台前端依赖""" + def run(): + try: + self._ps_set_running(True) + self._ps_log("=" * 50) + self._ps_log("开始安装前台前端依赖...") + config = self._get_ps_config() + automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) + success, msg = automation.install_front() + if success: + self._ps_log("前台依赖安装完成!") + else: + self._ps_log(f"安装失败: {msg}") + except Exception as e: + self._ps_log(f"错误: {str(e)}") + finally: + self._ps_set_running(False) + + threading.Thread(target=run, daemon=True).start() + + def _ps_install_admin(self): + """安装后台前端依赖""" + def run(): + try: + self._ps_set_running(True) + self._ps_log("=" * 50) + self._ps_log("开始安装后台前端依赖...") + config = self._get_ps_config() + automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) + success, msg = automation.install_admin() + if success: + self._ps_log("后台依赖安装完成!") + else: + self._ps_log(f"安装失败: {msg}") + except Exception as e: + self._ps_log(f"错误: {str(e)}") + finally: + self._ps_set_running(False) + + threading.Thread(target=run, daemon=True).start() + + def _ps_run_sql(self): + """导入 SQL 脚本""" + def run(): + try: + self._ps_set_running(True) + self._ps_log("=" * 50) + self._ps_log("开始导入 SQL...") + config = self._get_ps_config() + automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) + success, msg = automation.run_sql_script() + if success: + self._ps_log("SQL 导入完成!") + else: + self._ps_log(f"导入失败: {msg}") + except Exception as e: + self._ps_log(f"错误: {str(e)}") + finally: + self._ps_set_running(False) + + threading.Thread(target=run, daemon=True).start() + + def _ps_start_server(self): + """启动后端服务""" + def run(): + try: + self._ps_set_running(True) + self._ps_log("=" * 50) + self._ps_log("启动后端服务...") + config = self._get_ps_config() + automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) + automation.start_backend() + if automation.wait_for_service(config.backend_url, config.backend_startup_timeout): + self._ps_log("后端服务启动成功!") + else: + self._ps_log("后端服务启动超时,请手动检查") + except Exception as e: + self._ps_log(f"错误: {str(e)}") + finally: + self._ps_set_running(False) + + threading.Thread(target=run, daemon=True).start() + + def _ps_start_front(self): + """启动前台前端""" + def run(): + try: + self._ps_set_running(True) + self._ps_log("=" * 50) + self._ps_log("启动前台前端...") + config = self._get_ps_config() + automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) + automation.start_front() + if automation.wait_for_service(config.front_url, config.front_startup_timeout): + self._ps_log("前台前端启动成功!") + else: + self._ps_log("前台前端启动超时,请手动检查") + except Exception as e: + self._ps_log(f"错误: {str(e)}") + finally: + self._ps_set_running(False) + + threading.Thread(target=run, daemon=True).start() + + def _ps_organize_code(self): + """整理项目代码""" + def run(): + try: + self._ps_set_running(True) + self._ps_log("=" * 50) + self._ps_log("开始整理项目代码...") + config = self._get_ps_config() + automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) + sql_filename = automation.organize_project_code() + if sql_filename: + self._ps_log(f"代码整理完成!SQL文件名: {sql_filename}") + else: + self._ps_log("代码整理完成,但未获取到SQL文件名") + except Exception as e: + self._ps_log(f"错误: {str(e)}") + finally: + self._ps_set_running(False) + + threading.Thread(target=run, daemon=True).start() + + def _ps_start_admin(self): + """启动后台前端""" + def run(): + try: + self._ps_set_running(True) + self._ps_log("=" * 50) + self._ps_log("启动后台前端...") + config = self._get_ps_config() + automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) + automation.start_admin() + if automation.wait_for_service(config.admin_url, config.admin_startup_timeout): + self._ps_log("后台前端启动成功!") + else: + self._ps_log("后台前端启动超时,请手动检查") + except Exception as e: + self._ps_log(f"错误: {str(e)}") + finally: + self._ps_set_running(False) + + threading.Thread(target=run, daemon=True).start() + + def _ps_replace_images(self): + """更换图片""" + def run(): + try: + self._ps_set_running(True) + self._ps_log("=" * 50) + self._ps_log("更换图片...") + config = self._get_ps_config() + automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) + + # 更换前台轮播图片 + self._ps_log("更换前台轮播图片...") + result = automation.replace_swiper_images() + self._ps_log(result) + + # 更换后台登录背景图 + self._ps_log("更换后台登录背景图...") + if automation.update_login_vue_background(): + self._ps_log("后台登录背景图更换成功!") + else: + self._ps_log("后台登录背景图更换失败") + + except Exception as e: + self._ps_log(f"错误: {str(e)}") + finally: + self._ps_set_running(False) + + threading.Thread(target=run, daemon=True).start() + + def _ps_capture_admin(self): + """后台截图""" + def run(): + try: + self._ps_set_running(True) + self._ps_log("=" * 50) + self._ps_log("开始后台截图...") + config = self._get_ps_config() + automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) + + import asyncio + # 截图保存到桌面路径下的"截图"文件夹 + output_dir = os.path.join(config.desktop_path, "截图") + os.makedirs(output_dir, exist_ok=True) + + screenshots = asyncio.run(automation.capture_admin_screenshots_only(output_dir)) + self._ps_log(f"后台截图完成!共生成 {len(screenshots)} 张截图,保存到: {output_dir}") + + except Exception as e: + self._ps_log(f"错误: {str(e)}") + finally: + self._ps_set_running(False) + + threading.Thread(target=run, daemon=True).start() + + def _ps_capture_front(self): + """前台截图""" + def run(): + try: + self._ps_set_running(True) + self._ps_log("=" * 50) + self._ps_log("开始前台截图...") + config = self._get_ps_config() + automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) + + import asyncio + # 截图保存到桌面路径下的"截图"文件夹 + output_dir = os.path.join(config.desktop_path, "截图") + os.makedirs(output_dir, exist_ok=True) + + screenshots = asyncio.run(automation.capture_front_screenshots_only(output_dir)) + self._ps_log(f"前台截图完成!共生成 {len(screenshots)} 张截图,保存到: {output_dir}") + + except Exception as e: + self._ps_log(f"错误: {str(e)}") + finally: + self._ps_set_running(False) + + threading.Thread(target=run, daemon=True).start() + + def _ps_finalize_screenshots(self): + """截图收尾工作""" + def run(): + try: + self._ps_set_running(True) + self._ps_log("=" * 50) + self._ps_log("开始执行截图收尾工作...") + config = self._get_ps_config() + automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) + + # 截图保存到桌面路径下的"截图"文件夹 + output_dir = os.path.join(config.desktop_path, "截图") + + automation.finalize_screenshots(output_dir) + self._ps_log(f"截图收尾工作完成!") + + except Exception as e: + self._ps_log(f"错误: {str(e)}") + finally: + self._ps_set_running(False) + + threading.Thread(target=run, daemon=True).start() + + def _ps_run_full_flow(self): + """一键完整流程""" + def run(): + try: + self._ps_set_running(True) + config = self._get_ps_config() + automation = ProjectScreenshotAutomation(config, log_callback=self._ps_log) + success = automation.run_full_flow() + if success: + self.root.after(0, lambda: messagebox.showinfo("成功", "完整流程执行成功!")) + else: + self.root.after(0, lambda: messagebox.showerror("错误", "流程执行失败,请查看日志")) + except Exception as e: + self._ps_log(f"错误: {str(e)}") + self.root.after(0, lambda: messagebox.showerror("错误", f"执行失败: {str(e)}")) + finally: + self._ps_set_running(False) + + threading.Thread(target=run, daemon=True).start() + + def _ps_close_all_cmd(self): + """关闭所有CMD窗口""" + def run(): + try: + self._ps_set_running(True) + self._ps_log("=" * 50) + self._ps_log("开始关闭所有CMD窗口...") + config = self._get_ps_config() + + # 构建 close_cmd.bat 路径 + close_cmd_bat = os.path.join(config.desktop_path, config.bat_folder, "close_cmd.bat") + + if not os.path.exists(close_cmd_bat): + self._ps_log(f"错误:close_cmd.bat 不存在: {close_cmd_bat}") + return + + self._ps_log(f"执行: {close_cmd_bat}") + + # 执行 bat 文件(弹出CMD窗口) + import subprocess + + if config.show_cmd_window: + # 显示CMD窗口 + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = 1 # SW_SHOWNORMAL + + subprocess.Popen( + ['cmd', '/c', close_cmd_bat], + creationflags=subprocess.CREATE_NEW_CONSOLE, + startupinfo=startupinfo, + cwd=config.desktop_path + ) + self._ps_log("CMD窗口已弹出执行关闭命令") + else: + # 不显示CMD窗口 + result = subprocess.run( + ['cmd', '/c', close_cmd_bat], + capture_output=True, + text=True, + encoding='utf-8', + errors='ignore' + ) + if result.returncode == 0: + self._ps_log("CMD窗口关闭成功!") + if result.stdout: + self._ps_log(result.stdout) + else: + self._ps_log(f"关闭失败: {result.stderr}") + + except Exception as e: + self._ps_log(f"错误: {str(e)}") + finally: + self._ps_set_running(False) + + threading.Thread(target=run, daemon=True).start() + def main(): """启动 GUI""" diff --git a/project_screenshot.py b/project_screenshot.py new file mode 100644 index 0000000..6f3986e --- /dev/null +++ b/project_screenshot.py @@ -0,0 +1,1611 @@ +from __future__ import annotations + +import os +import re +import glob +import random +import shutil +import subprocess +import time +import asyncio +from pathlib import Path +from typing import Optional, Callable, List, Tuple +from dataclasses import dataclass + +# 预设后台登录背景图片列表 +LOGIN_BACKGROUND_IMAGES = [ + "http://img.yidaima.cn/7-dcd4fc1e8d0144aaa064bc282d1af289", + "http://img.yidaima.cn/6-3db67e2e6ff64e018d48282638900a67", + "http://img.yidaima.cn/5-74ee6fe2f0954a8db2574e8f7fbb8ef2", + "http://img.yidaima.cn/4-a926593ba8c444a3984100e99ae322bb", + "http://img.yidaima.cn/3-f91e9a0ed55d45c9aff80a08f554c625", + "http://img.yidaima.cn/2-2bd41cdd57b54b529dc3a139e66233e0", + "http://img.yidaima.cn/1-5a0b7ee2344a46bd987c1cafff19b3ba", + "http://img.yidaima.cn/8-495b9767a2f34197b963a13226d3e1d2", + "http://img.yidaima.cn/10-991483f941914f3d85bf87a4f8748fee", + "http://img.yidaima.cn/11-d274f1d695f7472a8fcd2c8c6238a79e", + "http://img.yidaima.cn/14-0e30653336d849379060ad8c436fe512", + "http://img.yidaima.cn/16-7a58235a2a4040f99e3431c3cac9ea7a", + "http://img.yidaima.cn/17-3406a546b81c412d9e3501cdb0f88b12", + "http://img.yidaima.cn/18-0a2fae1d6634480dabf5471fa86b3623", + "http://img.yidaima.cn/19-107196938e074c92a40b62c60aed18a4" +] + + +@dataclass +class ProjectConfig: + """项目配置数据类""" + project_path: str + has_front: bool = True + desktop_path: str = "" + + # 脚本路径(相对于 bat 文件夹或绝对路径) + bat_folder: str = "new" # bat 文件所在文件夹(相对于桌面路径) + install_server: str = "run_install_server.bat" + install_front: str = "run_install_front.bat" + install_admin: str = "run_install_admin.bat" + run_server: str = "run_server.bat" + run_front: str = "run_front.bat" + run_admin: str = "run_admin.bat" + run_sql: str = "run_sql.bat" + + # SQL 配置 + sql_script_path: str = "server/db/init.sql" + + # 服务配置 + backend_url: str = "http://localhost:8080" + backend_startup_timeout: int = 30 + + front_url: str = "http://localhost:8082" + front_login_url: str = "http://localhost:8082/#/login" + front_startup_timeout: int = 60 + + admin_url: str = "http://localhost:8081" + admin_login_url: str = "http://localhost:8081/#/login" + admin_startup_timeout: int = 60 + + # 登录配置 + admin_username: str = "admin" + admin_password: str = "admin" + front_username: str = "2" + front_password: str = "123456" + + # 截图配置 + screenshot_output_dir: str = "./screenshots" + screenshot_delay: int = 2000 + admin_menu_selector: str = ".el-menu-item, .el-sub-menu__title" + front_menu_selector: str = ".nav-item, .menu-item" + + # 轮播图片配置 + swiper_source_folder: str = "bg_pic" + swiper_target_subpath: str = "src/main/resources/static/file" + swiper_target_names: List[str] = None + swiper_count: int = 3 + + # 登录背景图配置 + login_vue_subpath: str = "src/views/login.vue" + login_background_images: List[str] = None + + # 代码整理配置 + code_source_folder: str = "code" # 桌面上的 code 文件夹名称 + code_target_path: str = r"D:\code" # 整理代码的目标目录(默认 D:\code) + + # 执行选项 + show_cmd_window: bool = True # 是否显示 CMD 窗口 + + def __post_init__(self): + if self.swiper_target_names is None: + self.swiper_target_names = ["swiperPicture1.jpg", "swiperPicture2.jpg", "swiperPicture3.jpg"] + if self.login_background_images is None: + self.login_background_images = LOGIN_BACKGROUND_IMAGES.copy() + + +class ProjectScreenshotAutomation: + """项目截图自动化类""" + + def __init__(self, config: ProjectConfig, log_callback: Optional[Callable[[str], None]] = None): + self.config = config + self.log_callback = log_callback or print + self.processes: List[subprocess.Popen] = [] + + def log(self, message: str): + """输出日志""" + self.log_callback(message) + + def execute_bat(self, bat_path: str, cwd: str = None, timeout: int = 300) -> Tuple[bool, str]: + """ + 执行 bat 脚本并返回结果 + + Args: + bat_path: bat 文件路径 + cwd: 工作目录 + timeout: 超时时间(秒) + + Returns: + (success, message) + """ + try: + if not os.path.exists(bat_path): + return False, f"脚本文件不存在: {bat_path}" + + self.log(f"执行脚本: {bat_path}") + self.log(f"项目路径参数: {self.config.project_path}") + + # 根据配置决定是否显示 CMD 窗口 + startupinfo = subprocess.STARTUPINFO() + creationflags = 0 + + if self.config.show_cmd_window: + # 显示 CMD 窗口 + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = 1 # SW_SHOWNORMAL + creationflags = 0x00000010 # CREATE_NEW_CONSOLE + self.log("CMD 窗口: 显示") + else: + # 隐藏 CMD 窗口 + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = 0 # SW_HIDE + self.log("CMD 窗口: 隐藏") + + # 构建命令,传入项目路径作为参数 + cmd = f'"{bat_path}" "{self.config.project_path}"' + + process = subprocess.Popen( + cmd, + cwd=cwd, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + startupinfo=startupinfo, + creationflags=creationflags + ) + + self.processes.append(process) + + try: + stdout_bytes, stderr_bytes = process.communicate(timeout=timeout) + # 尝试多种编码解码输出 + stdout = self._decode_output(stdout_bytes) + stderr = self._decode_output(stderr_bytes) + + if process.returncode == 0: + return True, stdout + else: + return False, f"脚本执行失败: {stderr}" + except subprocess.TimeoutExpired: + process.kill() + return False, f"脚本执行超时({timeout}秒)" + + except Exception as e: + return False, f"执行脚本出错: {str(e)}" + + def _decode_output(self, data: bytes) -> str: + """尝试多种编码解码字节数据""" + if not data: + return "" + + # 尝试的编码列表(GBK/GB2312/CP936 优先,因为 bat 脚本使用 chcp 936) + encodings = ['gbk', 'gb2312', 'cp936', 'utf-8', 'latin-1'] + + for encoding in encodings: + try: + return data.decode(encoding, errors='replace') + except Exception: + continue + + # 如果都失败,使用 latin-1 解码(不会丢失数据) + return data.decode('latin-1', errors='replace') + + def clear_directory(self, path: str): + """删除目录下的所有内容""" + if os.path.exists(path): + for item in os.listdir(path): + item_path = os.path.join(path, item) + if os.path.isfile(item_path) or os.path.islink(item_path): + os.unlink(item_path) + elif os.path.isdir(item_path): + shutil.rmtree(item_path) + self.log(f"已清空目录: {path}") + else: + self.log(f"目录不存在: {path}") + + def get_first_subdirectory(self, path: str) -> Optional[str]: + """获取指定目录下的第一个子文件夹""" + if os.path.exists(path): + for item in os.listdir(path): + item_path = os.path.join(path, item) + if os.path.isdir(item_path): + return item_path + return None + + def copy_directory(self, src: str, dst: str): + """复制目录及其内容""" + if os.path.exists(src): + shutil.copytree(src, dst, dirs_exist_ok=True) + self.log(f"已复制 {src} 到 {dst}") + else: + self.log(f"源目录不存在: {src}") + + def get_filename_without_extension(self, file_path: str) -> str: + """获取文件名(不带后缀)""" + return os.path.splitext(os.path.basename(file_path))[0] + + def organize_project_code(self) -> Optional[str]: + """ + 整理项目代码 + + 步骤: + 1. 清空桌面 code 文件夹 + 2. 把项目路径的代码复制到桌面 code 文件夹 + 3. 在桌面 code 文件夹中找前后端代码和 db + 4. 放到代码目标路径下的 \front、\server、\db 文件夹 + 5. 返回 SQL 目录下的第一个文件名(不带后缀) + + Returns: + 第一个 SQL 文件名(不带后缀),失败返回 None + """ + try: + self.log("开始整理项目代码...") + + # 步骤 1: 清空桌面 code 文件夹和截图文件夹 + desktop_code_path = os.path.join(self.config.desktop_path, self.config.code_source_folder) + screenshot_path = os.path.join(self.config.desktop_path, "截图") + + self.log(f"步骤1: 清空桌面 code 文件夹 {desktop_code_path}...") + self.clear_directory(desktop_code_path) + os.makedirs(desktop_code_path, exist_ok=True) + + self.log(f"步骤1: 清空桌面截图文件夹 {screenshot_path}...") + self.clear_directory(screenshot_path) + os.makedirs(screenshot_path, exist_ok=True) + + # 步骤 2: 把项目路径的代码复制到桌面 code 文件夹 + self.log("\n步骤2: 复制项目代码到桌面 code 文件夹...") + project_source_path = os.path.join(self.config.project_path, "源码") + + if os.path.exists(project_source_path): + self.log(f"从 {project_source_path} 复制代码到 {desktop_code_path}") + self.copy_directory(project_source_path, desktop_code_path) + else: + self.log(f"警告:项目源码文件夹不存在: {project_source_path}") + self.log("将直接使用桌面 code 文件夹中的现有代码") + + # 步骤 3: 在桌面 code 文件夹中找第一个子文件夹 + self.log("\n步骤3: 查找项目子目录...") + first_subdir = self.get_first_subdirectory(desktop_code_path) + + if not first_subdir: + self.log("未找到桌面 code 文件夹下的子目录") + return None + + self.log(f"找到的子目录: {first_subdir}") + + # 步骤 4: 清空并创建代码目标路径下的目录 + self.log(f"\n步骤4: 准备代码目标路径 {self.config.code_target_path}...") + target_dirs = [ + os.path.join(self.config.code_target_path, "front"), + os.path.join(self.config.code_target_path, "server"), + os.path.join(self.config.code_target_path, "db"), + ] + for dir_path in target_dirs: + self.clear_directory(dir_path) + os.makedirs(dir_path, exist_ok=True) + + # 步骤 5: 复制 client_code 和 manage_code 到代码目标路径的 front + self.log("\n步骤5: 复制前端代码...") + front_target = os.path.join(self.config.code_target_path, "front") + for folder in ["client_code", "manage_code"]: + src = os.path.join(first_subdir, folder) + dst = os.path.join(front_target, folder) + self.copy_directory(src, dst) + + # 步骤 6: 复制 server_code 到代码目标路径的 server + self.log("\n步骤6: 复制后端代码...") + server_src = os.path.join(first_subdir, "server_code") + server_dst = os.path.join(self.config.code_target_path, "server", "server_code") + self.copy_directory(server_src, server_dst) + + # 步骤 7: 复制 sql 文件到代码目标路径的 db + self.log("\n步骤7: 复制 SQL 文件...") + sql_src = os.path.join(first_subdir, "server_code", "sql") + db_target = os.path.join(self.config.code_target_path, "db") + first_sql_filename = None + + if os.path.exists(sql_src): + for item in os.listdir(sql_src): + item_src = os.path.join(sql_src, item) + item_dst = os.path.join(db_target, item) + if os.path.isfile(item_src): + shutil.copy2(item_src, item_dst) + self.log(f"已复制文件 {item} 到 {db_target}") + if first_sql_filename is None: + first_sql_filename = self.get_filename_without_extension(item) + elif os.path.isdir(item_src): + shutil.copytree(item_src, item_dst, dirs_exist_ok=True) + self.log(f"已复制目录 {item} 到 {db_target}") + else: + self.log(f"SQL 目录不存在: {sql_src}") + return None + + # 步骤 8: 返回 SQL 目录下的第一个文件名(不带后缀) + self.log("\n步骤8: 代码整理完成!") + if first_sql_filename: + self.log(f"SQL 文件名(不带后缀): {first_sql_filename}") + return first_sql_filename + else: + self.log("SQL 目录中没有文件,无法获取文件名") + return None + + except Exception as e: + self.log(f"整理项目代码出错: {str(e)}") + return None + + def execute_bat_async(self, bat_path: str, cwd: str = None) -> subprocess.Popen: + """ + 异步执行 bat 脚本(用于启动服务) + + Args: + bat_path: bat 文件路径 + cwd: 工作目录 + + Returns: + subprocess.Popen 对象 + """ + if not os.path.exists(bat_path): + raise FileNotFoundError(f"脚本文件不存在: {bat_path}") + + self.log(f"异步执行脚本: {bat_path}") + self.log(f"项目路径参数: {self.config.project_path}") + + # 根据配置决定是否显示 CMD 窗口 + startupinfo = subprocess.STARTUPINFO() + creationflags = 0 + + if self.config.show_cmd_window: + # 显示 CMD 窗口 + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = 1 # SW_SHOWNORMAL + creationflags = 0x00000010 # CREATE_NEW_CONSOLE + self.log("CMD 窗口: 显示") + else: + # 隐藏 CMD 窗口 + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = 0 # SW_HIDE + self.log("CMD 窗口: 隐藏") + + # 构建命令,传入项目路径作为参数 + cmd = f'"{bat_path}" "{self.config.project_path}"' + + # 对于启动服务的场景,不捕获输出,让用户可以直接在 CMD 窗口中看到输出并交互 + if self.config.show_cmd_window: + # 显示 CMD 窗口,不捕获输出 + # 使用 cmd /c 启动 bat 文件,确保 CMD 窗口正确显示 + full_cmd = f'cmd /c "{cmd}"' + process = subprocess.Popen( + full_cmd, + cwd=cwd, + shell=False, + startupinfo=startupinfo, + creationflags=creationflags + ) + else: + # 隐藏 CMD 窗口,捕获输出 + process = subprocess.Popen( + cmd, + cwd=cwd, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + startupinfo=startupinfo, + creationflags=creationflags + ) + + self.processes.append(process) + return process + + def wait_for_service(self, url: str, timeout: int = 120) -> bool: + """ + 等待服务启动完成 + + Args: + url: 服务健康检查 URL + timeout: 超时时间(秒) + + Returns: + 是否启动成功 + """ + import urllib.request + import urllib.error + + self.log(f"等待服务启动: {url} (超时时间: {timeout}秒)") + start_time = time.time() + check_count = 0 + + while time.time() - start_time < timeout: + check_count += 1 + elapsed = int(time.time() - start_time) + + # 每10秒输出一次日志 + if check_count % 5 == 0: + self.log(f" 已等待 {elapsed} 秒,继续检查...") + + try: + req = urllib.request.Request(url, method='GET') + req.add_header('User-Agent', 'Mozilla/5.0') + with urllib.request.urlopen(req, timeout=5) as response: + if response.status == 200: + self.log(f"服务已启动: {url} (共等待 {elapsed} 秒)") + return True + except urllib.error.HTTPError as e: + # 某些服务返回 401/403 也表示服务已启动 + if e.code in [401, 403]: + self.log(f"服务已启动(需要认证): {url} (共等待 {elapsed} 秒)") + return True + except Exception as e: + # 每20秒输出一次错误信息 + if check_count % 10 == 0: + self.log(f" 检查失败: {str(e)[:50]}") + + time.sleep(2) + + self.log(f"服务启动超时: {url} (超时时间: {timeout}秒)") + return False + + def import_sql(self, sql_file: str, db_config: dict = None) -> Tuple[bool, str]: + """ + 导入 SQL 脚本到数据库 + + Args: + sql_file: SQL 文件路径 + db_config: 数据库配置 + + Returns: + (success, message) + """ + try: + if not os.path.exists(sql_file): + return False, f"SQL 文件不存在: {sql_file}" + + self.log(f"导入 SQL 文件: {sql_file}") + + # 使用 mysql 命令行导入 + if db_config: + host = db_config.get('host', 'localhost') + port = db_config.get('port', 3306) + user = db_config.get('user', 'root') + password = db_config.get('password', '') + database = db_config.get('database', '') + + cmd = f'mysql -h {host} -P {port} -u {user}' + if password: + cmd += f' -p{password}' + if database: + cmd += f' {database}' + cmd += f' < "{sql_file}"' + + result = subprocess.run(cmd, shell=True, capture_output=True, text=True) + if result.returncode != 0: + return False, f"SQL 导入失败: {result.stderr}" + + self.log("SQL 导入成功") + + # 执行额外的 SQL 语句 + self.log("执行额外的 SQL 更新操作...") + self._execute_post_import_sql(host, port, user, password, database) + + return True, "SQL 导入成功" + else: + # 如果没有数据库配置,尝试执行 run_sql.bat + bat_path = os.path.join(self.config.project_path, self.config.run_sql) + return self.execute_bat(bat_path, cwd=self.config.project_path) + + except Exception as e: + return False, f"导入 SQL 出错: {str(e)}" + + def _execute_post_import_sql(self, host: str, port: int, user: str, password: str, database: str): + """ + 导入 SQL 后执行的额外操作 + + Args: + host: 数据库主机 + port: 数据库端口 + user: 用户名 + password: 密码 + database: 数据库名 + """ + try: + import pymysql + + # 连接数据库 + conn = pymysql.connect( + host=host, + port=port, + user=user, + password=password, + database=database, + charset='utf8mb4' + ) + + with conn.cursor() as cursor: + # 查询包含 ming 或 hao 的列 + query = """ + SELECT TABLE_NAME, COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE (COLUMN_NAME LIKE '%ming%' OR COLUMN_NAME LIKE '%hao%') + AND TABLE_SCHEMA = %s + """ + cursor.execute(query, (database,)) + results = cursor.fetchall() + + self.log(f"找到 {len(results)} 个匹配的列") + + # 执行更新 + for table_name, column_name in results: + update_sql = f"UPDATE `{table_name}` SET `{column_name}` = '2' WHERE `{column_name}` LIKE '%2%'" + try: + cursor.execute(update_sql) + affected_rows = cursor.rowcount + conn.commit() + self.log(f"更新 {table_name}.{column_name}: 影响 {affected_rows} 行") + except Exception as e: + self.log(f"更新 {table_name}.{column_name} 失败: {str(e)}") + conn.rollback() + + conn.close() + self.log("额外 SQL 操作执行完成") + + except ImportError: + self.log("警告:未安装 pymysql,跳过额外 SQL 操作") + except Exception as e: + self.log(f"执行额外 SQL 操作失败: {str(e)}") + + def get_random_images(self, bg_pic_folder: str, count: int = 3) -> List[str]: + """ + 从指定文件夹随机获取指定数量的图片文件 + + Args: + bg_pic_folder: 图片文件夹路径 + count: 需要获取的图片数量 + + Returns: + 随机选择的图片文件路径列表 + """ + image_extensions = ['*.jpg', '*.jpeg', '*.png', '*.bmp', '*.gif', + '*.JPG', '*.JPEG', '*.PNG', '*.BMP', '*.GIF'] + + all_images = [] + for extension in image_extensions: + pattern = os.path.join(bg_pic_folder, extension) + all_images.extend(glob.glob(pattern)) + + if len(all_images) < count: + self.log(f"警告:文件夹中只有 {len(all_images)} 张图片,少于需要的 {count} 张") + return all_images + + selected_images = random.sample(all_images, count) + self.log(f"从 {len(all_images)} 张图片中随机选择了 {len(selected_images)} 张:") + for i, img in enumerate(selected_images, 1): + self.log(f" {i}. {os.path.basename(img)}") + + return selected_images + + def copy_images_to_target(self, source_images: List[str], target_folder: str) -> bool: + """ + 将源图片复制到目标文件夹并重命名 + + Args: + source_images: 源图片文件路径列表 + target_folder: 目标文件夹路径 + + Returns: + 是否复制成功 + """ + os.makedirs(target_folder, exist_ok=True) + + for i, source_image in enumerate(source_images): + if i >= len(self.config.swiper_target_names): + break + + target_path = os.path.join(target_folder, self.config.swiper_target_names[i]) + + try: + shutil.copy2(source_image, target_path) + self.log(f"成功复制: {os.path.basename(source_image)} -> {self.config.swiper_target_names[i]}") + except Exception as e: + self.log(f"复制失败: {os.path.basename(source_image)} -> {self.config.swiper_target_names[i]}, 错误: {str(e)}") + return False + + return True + + def replace_swiper_images(self) -> str: + """ + 更换前台轮播图片 + + Returns: + 执行结果信息 + """ + try: + bg_pic_folder = os.path.join(self.config.desktop_path, self.config.swiper_source_folder) + # 使用代码目标路径下的 server/server_code 目录 + target_folder = os.path.join(self.config.code_target_path, "server", "server_code", "src", "main", "resources", "static", "file") + + self.log(f"源文件夹: {bg_pic_folder}") + self.log(f"目标文件夹: {target_folder}") + + if not os.path.exists(bg_pic_folder): + error_msg = f"错误:源文件夹不存在: {bg_pic_folder}" + self.log(error_msg) + return error_msg + + selected_images = self.get_random_images(bg_pic_folder, self.config.swiper_count) + + if not selected_images: + error_msg = "错误:源文件夹中没有找到图片文件" + self.log(error_msg) + return error_msg + + success = self.copy_images_to_target(selected_images, target_folder) + + if success: + result_msg = f"成功!已随机选择 {len(selected_images)} 张图片并覆盖到目标位置" + self.log(result_msg) + return result_msg + else: + error_msg = "复制过程中出现错误" + self.log(error_msg) + return error_msg + + except Exception as e: + error_msg = f"程序执行出错: {str(e)}" + self.log(error_msg) + return error_msg + + def update_login_vue_background(self) -> bool: + """ + 修改 login.vue 文件中的背景图片链接 + + Returns: + 是否更新成功 + """ + # 使用代码目标路径下的 front/manage_code 目录 + file_path = os.path.join(self.config.code_target_path, "front", "manage_code", "src", "views", "login.vue") + + try: + if not os.path.exists(file_path): + self.log(f"错误:文件 {file_path} 不存在") + return False + + with open(file_path, 'r', encoding='utf-8') as file: + content = file.read() + + self.log(f"成功读取文件:{file_path}") + + selected_image = random.choice(self.config.login_background_images) + self.log(f"随机选择的图片:{selected_image}") + + # 使用正则表达式匹配并替换背景图片URL + pattern = r'background-image:\s*url\([^)]+\)' + + if not re.search(pattern, content): + self.log("警告:未找到匹配的背景图片URL模式") + return False + + new_content = re.sub(pattern, f'background-image: url({selected_image})', content) + + with open(file_path, 'w', encoding='utf-8') as file: + file.write(new_content) + + self.log(f"成功更新背景图片为:{selected_image}") + return True + + except Exception as e: + self.log(f"处理文件时出错:{str(e)}") + return False + + def _get_bat_path(self, bat_name: str) -> str: + """获取 bat 文件的完整路径(优先从桌面 new 文件夹查找)""" + # 首先检查桌面 new 文件夹 + desktop_bat_path = os.path.join(self.config.desktop_path, self.config.bat_folder, bat_name) + if os.path.exists(desktop_bat_path): + return desktop_bat_path + + # 其次检查项目路径 + project_bat_path = os.path.join(self.config.project_path, bat_name) + if os.path.exists(project_bat_path): + return project_bat_path + + # 默认返回桌面 new 文件夹路径(即使不存在,让调用方处理错误) + return desktop_bat_path + + def install_server(self) -> Tuple[bool, str]: + """Maven 构建后端""" + bat_path = self._get_bat_path(self.config.install_server) + return self.execute_bat(bat_path, cwd=self.config.project_path, timeout=600) + + def install_front(self) -> Tuple[bool, str]: + """安装前台前端依赖""" + bat_path = self._get_bat_path(self.config.install_front) + return self.execute_bat(bat_path, cwd=self.config.project_path, timeout=300) + + def install_admin(self) -> Tuple[bool, str]: + """安装后台前端依赖""" + bat_path = self._get_bat_path(self.config.install_admin) + return self.execute_bat(bat_path, cwd=self.config.project_path, timeout=300) + + def run_sql_script(self) -> Tuple[bool, str]: + """导入 SQL 脚本""" + # SQL 脚本优先从桌面 new 文件夹查找 + desktop_sql_path = os.path.join(self.config.desktop_path, self.config.bat_folder, self.config.run_sql) + if os.path.exists(desktop_sql_path): + # 执行 bat 文件导入 SQL + success, msg = self.execute_bat(desktop_sql_path, cwd=self.config.project_path, timeout=300) + return success, msg + + # 其次检查项目路径下的 SQL 脚本 + sql_file = os.path.join(self.config.project_path, self.config.sql_script_path) + # 尝试从 SQL 文件中提取数据库名 + database_name = self._extract_database_from_sql(sql_file) + if database_name: + db_config = { + 'host': 'localhost', + 'port': 3306, + 'user': 'root', + 'password': '', + 'database': database_name + } + return self.import_sql(sql_file, db_config) + else: + return self.import_sql(sql_file) + + def _extract_database_from_sql(self, sql_file: str) -> str: + """ + 从 SQL 文件中提取数据库名 + + Args: + sql_file: SQL 文件路径 + + Returns: + 数据库名,如果无法提取则返回空字符串 + """ + try: + if not os.path.exists(sql_file): + return "" + + with open(sql_file, 'r', encoding='utf-8') as f: + content = f.read() + + # 尝试匹配 USE database; 或 CREATE DATABASE database; 等语句 + import re + patterns = [ + r"USE\s+`?(\w+)`?\s*;", + r"CREATE\s+DATABASE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?(\w+)`?", + ] + + for pattern in patterns: + match = re.search(pattern, content, re.IGNORECASE) + if match: + database_name = match.group(1) + self.log(f"从 SQL 文件中提取到数据库名: {database_name}") + return database_name + + return "" + + except Exception as e: + self.log(f"提取数据库名失败: {str(e)}") + return "" + + def start_backend(self) -> subprocess.Popen: + """启动后端服务""" + bat_path = self._get_bat_path(self.config.run_server) + return self.execute_bat_async(bat_path, cwd=self.config.project_path) + + def start_front(self) -> subprocess.Popen: + """启动前台前端""" + bat_path = self._get_bat_path(self.config.run_front) + return self.execute_bat_async(bat_path, cwd=self.config.project_path) + + def start_admin(self) -> subprocess.Popen: + """启动后台前端""" + bat_path = self._get_bat_path(self.config.run_admin) + return self.execute_bat_async(bat_path, cwd=self.config.project_path) + + def stop_all_processes(self): + """停止所有启动的进程""" + self.log("停止所有服务进程...") + for process in self.processes: + try: + process.terminate() + process.wait(timeout=5) + except Exception: + try: + process.kill() + except Exception: + pass + self.processes.clear() + + async def capture_screenshots_with_playwright(self, output_dir: str) -> List[str]: + """ + 使用 Playwright 进行截图(同时截取前后台) + + Args: + output_dir: 截图输出目录 + + Returns: + 截图文件路径列表 + """ + from playwright.async_api import async_playwright + + screenshots = [] + os.makedirs(output_dir, exist_ok=True) + + async with async_playwright() as p: + browser = await p.chromium.launch(headless=False) + context = await browser.new_context(viewport={'width': 1920, 'height': 1080}) + + try: + # 后台截图 + self.log("开始后台截图...") + admin_screenshots = await self._capture_admin_screenshots(context, output_dir) + screenshots.extend(admin_screenshots) + + # 前台截图(如果有) + if self.config.has_front: + self.log("开始前台截图...") + front_screenshots = await self._capture_front_screenshots(context, output_dir) + screenshots.extend(front_screenshots) + + finally: + await context.close() + await browser.close() + + return screenshots + + async def capture_admin_screenshots_only(self, output_dir: str) -> List[str]: + """ + 仅截取后台页面 + + Args: + output_dir: 截图输出目录 + + Returns: + 截图文件路径列表 + """ + from playwright.async_api import async_playwright + + screenshots = [] + os.makedirs(output_dir, exist_ok=True) + + async with async_playwright() as p: + browser = await p.chromium.launch(headless=False) + context = await browser.new_context(viewport={'width': 1920, 'height': 1080}) + + try: + self.log("开始后台截图...") + admin_screenshots = await self._capture_admin_screenshots(context, output_dir) + screenshots.extend(admin_screenshots) + finally: + await context.close() + await browser.close() + + return screenshots + + async def capture_front_screenshots_only(self, output_dir: str) -> List[str]: + """ + 仅截取前台页面 + + Args: + output_dir: 截图输出目录 + + Returns: + 截图文件路径列表 + """ + from playwright.async_api import async_playwright + + screenshots = [] + os.makedirs(output_dir, exist_ok=True) + + async with async_playwright() as p: + browser = await p.chromium.launch(headless=False) + context = await browser.new_context(viewport={'width': 1920, 'height': 1080}) + + try: + self.log("开始前台截图...") + front_screenshots = await self._capture_front_screenshots(context, output_dir) + screenshots.extend(front_screenshots) + finally: + await context.close() + await browser.close() + + return screenshots + + async def _capture_menu_recursive(self, page, output_dir: str, screenshots: List[str], captured_menus: set, depth: int, max_depth: int = 3, processed_items: set = None): + """ + 递归截图菜单 + + Args: + page: Playwright page 对象 + output_dir: 截图输出目录 + screenshots: 截图路径列表 + captured_menus: 已截图的菜单名称集合 + depth: 当前递归深度 + max_depth: 最大递归深度 + processed_items: 已处理的菜单项文本集合(避免重复处理) + """ + if depth > max_depth: + return + + if processed_items is None: + processed_items = set() + + # 查找所有可见的菜单项(包括子菜单和普通菜单项) + # 使用更通用的选择器获取所有 el-menu 下的 li 元素 + menu_selectors = [ + "xpath=//ul[contains(@class, 'el-menu')]//li", + self.config.admin_menu_selector + ] + + menu_items = [] + for selector in menu_selectors: + try: + items = await page.query_selector_all(selector) + if items: + menu_items = items + self.log(f"使用选择器定位菜单成功: {selector}, 找到 {len(items)} 个") + break + except Exception as e: + self.log(f"选择器 {selector} 失败: {str(e)}") + continue + + self.log(f"第 {depth} 层发现 {len(menu_items)} 个菜单项") + + for i, item in enumerate(menu_items): + try: + # 获取菜单文本 + text = await item.text_content() + if not text: + continue + + text = text.strip().replace('/', '_').replace('\\', '_') + if not text or len(text) > 50: + continue + + # 检查是否已经处理过这个菜单项 + if text in processed_items: + self.log(f"菜单 '{text}' 已处理过,跳过") + continue + + # 检查元素是否可见 + is_visible = await item.is_visible() + if not is_visible: + continue + + # 标记为已处理 + processed_items.add(text) + + # 检查是否是子菜单(有 el-sub-menu 类) + try: + is_submenu = await item.evaluate('el => el.classList.contains("el-sub-menu")') + except Exception: + is_submenu = False + + # 检查是否有子菜单项(ul 元素) + has_children = False + try: + children = await item.query_selector_all(':scope > ul.el-menu--inline, :scope > ul.el-menu') + has_children = len(children) > 0 + except Exception: + pass + + self.log(f"处理菜单: {text}, is_submenu={is_submenu}, has_children={has_children}") + + if is_submenu or has_children: + # 点击展开子菜单 + self.log(f"点击展开子菜单: {text}") + try: + await item.click(force=True, timeout=3000) + except Exception: + try: + await item.evaluate('el => el.click()') + except Exception: + continue + + await asyncio.sleep(1.5) + + # 递归处理子菜单(只处理当前子菜单下的子项) + await self._capture_submenu_items(page, item, output_dir, screenshots, captured_menus, depth + 1, max_depth, processed_items) + else: + # 普通菜单项,检查是否已截图 + if text in captured_menus: + self.log(f"菜单 '{text}' 已截图,跳过") + continue + + # 点击菜单 + self.log(f"点击菜单: {text}") + try: + await item.click(force=True, timeout=3000) + except Exception: + try: + await item.evaluate('el => el.click()') + except Exception: + continue + + await asyncio.sleep(self.config.screenshot_delay / 1000) + + # 截图 + screenshot_path = os.path.join(output_dir, f'{text}管理.png') + await page.screenshot(path=screenshot_path, full_page=True) + screenshots.append(screenshot_path) + captured_menus.add(text) + self.log(f"截图已保存: {screenshot_path}") + + except Exception as e: + self.log(f"菜单处理失败: {str(e)}") + continue + + async def _capture_submenu_items(self, page, parent_item, output_dir: str, screenshots: List[str], captured_menus: set, depth: int, max_depth: int, processed_items: set): + """ + 处理子菜单项(只在父元素下查找) + + Args: + page: Playwright page 对象 + parent_item: 父菜单元素 + output_dir: 截图输出目录 + screenshots: 截图路径列表 + captured_menus: 已截图的菜单名称集合 + depth: 当前递归深度 + max_depth: 最大递归深度 + processed_items: 已处理的菜单项文本集合 + """ + if depth > max_depth: + return + + # 在父元素下查找子菜单项 + submenu_selectors = [ + ':scope > ul.el-menu--inline > li', + ':scope > ul > li', + ] + + submenu_items = [] + for selector in submenu_selectors: + try: + items = await parent_item.query_selector_all(selector) + if items: + submenu_items = items + self.log(f"在子菜单下找到 {len(items)} 个子项") + break + except Exception: + continue + + for item in submenu_items: + try: + # 获取菜单文本 + text = await item.text_content() + if not text: + continue + + text = text.strip().replace('/', '_').replace('\\', '_') + if not text or len(text) > 50: + continue + + # 检查是否已经处理过 + if text in processed_items: + self.log(f"子菜单 '{text}' 已处理过,跳过") + continue + + # 检查元素是否可见 + is_visible = await item.is_visible() + if not is_visible: + continue + + # 标记为已处理 + processed_items.add(text) + + # 检查是否有子菜单 + try: + is_submenu = await item.evaluate('el => el.classList.contains("el-sub-menu")') + except Exception: + is_submenu = False + + has_children = False + try: + children = await item.query_selector_all(':scope > ul.el-menu--inline, :scope > ul.el-menu') + has_children = len(children) > 0 + except Exception: + pass + + self.log(f"处理子菜单: {text}, is_submenu={is_submenu}, has_children={has_children}") + + if is_submenu or has_children: + # 点击展开子菜单 + self.log(f"点击展开子菜单: {text}") + try: + await item.click(force=True, timeout=3000) + except Exception: + try: + await item.evaluate('el => el.click()') + except Exception: + continue + + await asyncio.sleep(1.5) + + # 递归处理子菜单 + await self._capture_submenu_items(page, item, output_dir, screenshots, captured_menus, depth + 1, max_depth, processed_items) + else: + # 普通菜单项,检查是否已截图 + if text in captured_menus: + self.log(f"菜单 '{text}' 已截图,跳过") + continue + + # 点击菜单 + self.log(f"点击菜单: {text}") + try: + await item.click(force=True, timeout=3000) + except Exception: + try: + await item.evaluate('el => el.click()') + except Exception: + continue + + await asyncio.sleep(self.config.screenshot_delay / 1000) + + # 截图 + screenshot_path = os.path.join(output_dir, f'{text}管理.png') + await page.screenshot(path=screenshot_path, full_page=True) + screenshots.append(screenshot_path) + captured_menus.add(text) + self.log(f"截图已保存: {screenshot_path}") + + except Exception as e: + self.log(f"子菜单处理失败: {str(e)}") + continue + + async def _capture_admin_screenshots(self, context, output_dir: str) -> List[str]: + """截取后台页面""" + screenshots = [] + page = await context.new_page() + + try: + # 访问登录页面 + self.log(f"访问后台登录页面: {self.config.admin_login_url}") + await page.goto(self.config.admin_login_url, wait_until='networkidle') + await asyncio.sleep(2) + + # 截取登录页面 + login_screenshot_path = os.path.join(output_dir, '后台登录.png') + await page.screenshot(path=login_screenshot_path, full_page=True) + screenshots.append(login_screenshot_path) + self.log(f"登录页面截图已保存: {login_screenshot_path}") + + # 输入用户名密码 + self.log(f"登录后台: {self.config.admin_username}") + + # 用户名输入框选择器(按优先级顺序) + username_selectors = [ + "xpath=//div[contains(text(), '用户名:')]/following-sibling::input", + "xpath=//input[@placeholder='请输入账号']", + "input[placeholder='请输入用户名']", + "input[name='username']", + "#username", + "input[type='text']" + ] + + # 密码输入框选择器(按优先级顺序) + password_selectors = [ + "xpath=//div[contains(text(), '密码:')]/following-sibling::input", + "xpath=//input[@placeholder='请输入密码']", + "input[placeholder='请输入密码']", + "input[name='password']", + "#password", + "input[type='password']" + ] + + # 登录按钮选择器 + submit_selectors = [ + "xpath=//button[.//span[text()='登录']]", + "button[type='submit']", + ".login-btn", + ".el-button--primary", + "button:has-text('登录')" + ] + + # 输入用户名 + username_filled = False + for selector in username_selectors: + try: + await page.fill(selector, self.config.admin_username, timeout=2000) + self.log(f"用户名输入框定位成功: {selector}") + username_filled = True + break + except Exception: + continue + if not username_filled: + self.log("警告:未能找到用户名输入框") + + # 输入密码 + password_filled = False + for selector in password_selectors: + try: + await page.fill(selector, self.config.admin_password, timeout=2000) + self.log(f"密码输入框定位成功: {selector}") + password_filled = True + break + except Exception: + continue + if not password_filled: + self.log("警告:未能找到密码输入框") + + # 点击登录 + login_clicked = False + for selector in submit_selectors: + try: + await page.click(selector, timeout=2000) + self.log(f"登录按钮定位成功: {selector}") + login_clicked = True + break + except Exception: + continue + if not login_clicked: + self.log("警告:未能找到登录按钮") + + # 等待登录成功 + await asyncio.sleep(3) + + # 访问后台首页 + self.log(f"访问后台首页: {self.config.admin_url}") + await page.goto(self.config.admin_url, wait_until='networkidle') + await asyncio.sleep(self.config.screenshot_delay / 1000) + + # 截取首页 + screenshot_path = os.path.join(output_dir, '后台首页.png') + await page.screenshot(path=screenshot_path, full_page=True) + screenshots.append(screenshot_path) + self.log(f"截图已保存: {screenshot_path}") + + # 识别菜单并截图 + try: + # 用于记录已截图的菜单名称,避免重复 + captured_menus = set() + + # 递归截图菜单 + await self._capture_menu_recursive(page, output_dir, screenshots, captured_menus, 0) + + except Exception as e: + self.log(f"菜单识别失败: {str(e)}") + + finally: + await page.close() + + return screenshots + + async def _capture_front_screenshots(self, context, output_dir: str) -> List[str]: + """截取前台页面""" + screenshots = [] + page = await context.new_page() + + try: + # 访问登录页面 + self.log(f"访问前台登录页面: {self.config.front_login_url}") + await page.goto(self.config.front_login_url, wait_until='networkidle') + await asyncio.sleep(2) + + # 输入用户名密码 + self.log(f"登录前台: {self.config.front_username}") + + # 用户名输入框选择器(按优先级顺序) + username_selectors = [ + "xpath=//div[contains(text(), '账号:')]/following-sibling::input", + "xpath=//input[@placeholder='请输入账号']", + "input[placeholder='请输入用户名']", + "input[name='username']", + "#username", + "input[type='text']" + ] + + # 密码输入框选择器(按优先级顺序) + password_selectors = [ + "xpath=//div[contains(text(), '密码:')]/following-sibling::input", + "xpath=//input[@placeholder='请输入密码']", + "input[placeholder='请输入密码']", + "input[name='password']", + "#password", + "input[type='password']" + ] + + # 登录按钮选择器 + submit_selectors = [ + "xpath=//button[.//span[text()='登录']]", + "button[type='submit']", + ".login-btn", + ".el-button--primary", + "button:has-text('登录')" + ] + + # 输入用户名 + username_filled = False + for selector in username_selectors: + try: + await page.fill(selector, self.config.front_username, timeout=2000) + self.log(f"用户名输入框定位成功: {selector}") + username_filled = True + break + except Exception: + continue + if not username_filled: + self.log("警告:未能找到用户名输入框") + + # 输入密码 + password_filled = False + for selector in password_selectors: + try: + await page.fill(selector, self.config.front_password, timeout=2000) + self.log(f"密码输入框定位成功: {selector}") + password_filled = True + break + except Exception: + continue + if not password_filled: + self.log("警告:未能找到密码输入框") + + # 点击登录 + login_clicked = False + for selector in submit_selectors: + try: + await page.click(selector, timeout=2000) + self.log(f"登录按钮定位成功: {selector}") + login_clicked = True + break + except Exception: + continue + if not login_clicked: + self.log("警告:未能找到登录按钮") + + # 等待登录成功 + await asyncio.sleep(3) + + # 访问前台首页 http://localhost:8082 + front_home_url = "http://localhost:8082" + self.log(f"访问前台首页: {front_home_url}") + await page.goto(front_home_url, wait_until='networkidle') + await asyncio.sleep(self.config.screenshot_delay / 1000) + + # 识别菜单并截图 + try: + # 使用 xpath 查找 el-menu 下的菜单项 + menu_selector = "xpath=//ul[contains(@class, 'el-menu')]//li" + menu_items = await page.query_selector_all(menu_selector) + self.log(f"发现 {len(menu_items)} 个菜单项") + + # 用于记录已截图的菜单名称,避免重复 + captured_front_menus = set() + + for i, item in enumerate(menu_items[:15]): # 最多截图15个菜单 + try: + # 获取菜单文本 + text = await item.text_content() + if not text: + continue + + text = text.strip().replace('/', '_').replace('\\', '_') + if not text or len(text) > 50: + continue + + # 检查是否已截图 + if text in captured_front_menus: + self.log(f"菜单 '{text}' 已截图,跳过") + continue + + # 检查元素是否可见 + is_visible = await item.is_visible() + if not is_visible: + continue + + # 点击菜单 + self.log(f"点击菜单: {text}") + try: + await item.click(force=True, timeout=3000) + except Exception: + try: + await item.evaluate('el => el.click()') + except Exception: + continue + + await asyncio.sleep(self.config.screenshot_delay / 1000) + + # 截图,文件名就是菜单名称 + screenshot_path = os.path.join(output_dir, f'{text}.png') + await page.screenshot(path=screenshot_path, full_page=True) + screenshots.append(screenshot_path) + captured_front_menus.add(text) + self.log(f"截图已保存: {screenshot_path}") + + except Exception as e: + self.log(f"菜单截图失败: {str(e)}") + continue + + except Exception as e: + self.log(f"菜单识别失败: {str(e)}") + + finally: + await page.close() + + return screenshots + + def finalize_screenshots(self, folder_path: str): + """ + 截图收尾工作:按创建时间排序并重命名截图文件 + + Args: + folder_path: 截图文件夹路径 + """ + try: + self.log("=" * 50) + self.log("开始执行截图收尾工作...") + + # 获取文件夹中所有文件 + files = os.listdir(folder_path) + + # 筛选出图片文件 + image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'] + image_files = [f for f in files if os.path.splitext(f)[1].lower() in image_extensions] + + if not image_files: + self.log("未找到图片文件") + return + + self.log(f"找到 {len(image_files)} 个图片文件") + + # 根据创建时间排序图片文件 + image_files.sort(key=lambda f: os.path.getctime(os.path.join(folder_path, f))) + + # 开始重命名 + renamed_count = 0 + for index, filename in enumerate(image_files, start=1): + # 获取文件扩展名 + ext = os.path.splitext(filename)[1] + file_new_name = os.path.splitext(filename)[0] + + # 构建新文件名 + new_name = f"{index}_{file_new_name}{ext}" + + # 构建完整旧路径和新路径 + old_path = os.path.join(folder_path, filename) + new_path = os.path.join(folder_path, new_name) + + # 重命名文件 + try: + os.rename(old_path, new_path) + self.log(f"重命名: {filename} -> {new_name}") + renamed_count += 1 + except Exception as e: + self.log(f"无法重命名 {filename}: {e}") + + self.log(f"截图收尾工作完成,共重命名 {renamed_count} 个文件") + + except Exception as e: + self.log(f"截图收尾工作失败: {str(e)}") + + def run_full_flow(self) -> bool: + """ + 执行完整流程(按照按钮排序顺序) + + Returns: + 是否执行成功 + """ + try: + self.log("=" * 50) + self.log("开始执行项目截图完整流程") + self.log("=" * 50) + + # 1. 初始化变量和准备工作 + self.log("\n[1/11] 初始化变量和准备工作...") + if not os.path.exists(self.config.project_path): + self.log(f"错误:项目路径不存在: {self.config.project_path}") + return False + + # 截图保存到桌面路径下的"截图"文件夹 + output_dir = os.path.join(self.config.desktop_path, "截图") + os.makedirs(output_dir, exist_ok=True) + self.log(f"截图输出目录: {output_dir}") + + # 2. 整理项目代码 + self.log("\n[2/11] 整理项目代码...") + sql_filename = self.organize_project_code() + if sql_filename: + self.log(f"代码整理完成,SQL文件名: {sql_filename}") + else: + self.log("警告:代码整理可能未完全成功,继续执行...") + + # 3. 安装后端依赖 (Maven构建) + self.log("\n[3/11] Maven 构建后端...") + success, msg = self.install_server() + if not success: + self.log(f"警告:Maven 构建失败: {msg}") + else: + self.log("Maven 构建完成") + + # 4. 安装前端依赖 + self.log("\n[4/11] 安装前端依赖...") + + self.log("安装后台前端依赖...") + success, msg = self.install_admin() + if not success: + self.log(f"警告:后台依赖安装失败: {msg}") + + if self.config.has_front: + self.log("安装前台前端依赖...") + success, msg = self.install_front() + if not success: + self.log(f"警告:前台依赖安装失败: {msg}") + + # 5. 启动后端服务 + self.log("\n[5/11] 启动后端服务...") + backend_process = self.start_backend() + if not self.wait_for_service( + self.config.backend_url, + self.config.backend_startup_timeout + ): + self.log("警告:后端服务启动超时,继续执行...") + + # 6. 启动后台前端 + self.log("\n[6/11] 启动后台前端...") + admin_process = self.start_admin() + if not self.wait_for_service(self.config.admin_url, self.config.admin_startup_timeout): + self.log("警告:后台前端启动超时,继续执行...") + + # 7. 启动前台前端 + if self.config.has_front: + self.log("\n[7/11] 启动前台前端...") + front_process = self.start_front() + if not self.wait_for_service(self.config.front_url, self.config.front_startup_timeout): + self.log("警告:前台前端启动超时,继续执行...") + + # 8. 更换图片 + self.log("\n[8/11] 更换图片...") + + # 更换前台轮播图片 + self.log("更换前台轮播图片...") + self.replace_swiper_images() + + # 更换后台登录背景图 + self.log("更换后台登录背景图...") + self.update_login_vue_background() + + # 9. 导入 SQL + self.log("\n[9/11] 导入 SQL...") + success, msg = self.run_sql_script() + if not success: + self.log(f"警告:SQL 导入失败: {msg}") + else: + self.log("SQL 导入成功") + + # 10. 截图 + self.log("\n[10/11] 开始截图...") + try: + screenshots = asyncio.run(self.capture_screenshots_with_playwright(output_dir)) + self.log(f"共生成 {len(screenshots)} 张截图") + except Exception as e: + self.log(f"截图失败: {str(e)}") + + # 11. 收尾工作 + self.log("\n[11/11] 执行收尾工作...") + self.finalize_screenshots(output_dir) + self.log("完整流程执行完成!") + self.log("=" * 50) + + return True + + except Exception as e: + self.log(f"\n[ERROR] 流程执行失败: {str(e)}") + return False + + +if __name__ == "__main__": + # 测试代码 + config = ProjectConfig( + project_path=r"D:\new_project", + desktop_path=r"C:\Users\Administrator\Desktop", + has_front=True + ) + + automation = ProjectScreenshotAutomation(config) + automation.run_full_flow()