From e8b789e906bf4d4168db2b0e460e981ad4f612d5 Mon Sep 17 00:00:00 2001 From: Misaka Date: Sat, 20 Jun 2026 19:22:01 +0800 Subject: [PATCH] Add anti-collapse menu nav and fix multi-tab Strict Mode leakage - Rewrite yunda_smart_menu_click to check each parent submenu's is-opened state before clicking (skip if already open), using XPath parent-child isolation to avoid collapsing accordion menus - Scope the expected/actual-arrival loading mask, #sum panel, and empty-record hint to #tab-1 and add .first to fix Playwright Strict Mode (5 elements found) leaks across multiple open tabs - Move _yunda_poll_and_download_tasks above yunda_actual_download for definition order Co-Authored-By: Claude --- site_yunda.py | 416 +++++++++++++++++++++++++++----------------------- 1 file changed, 221 insertions(+), 195 deletions(-) diff --git a/site_yunda.py b/site_yunda.py index cad3672..26c4fdd 100644 --- a/site_yunda.py +++ b/site_yunda.py @@ -52,12 +52,40 @@ def _wait_and_get_frame_by_selector(page, selector, timeout_ms=20000): def yunda_smart_menu_click(page, menu_path): - """韵达排他性二级树形菜单智能展开器""" - print(f">> 正在导航韵达菜单: {' -> '.join(menu_path)}") + """ + 韵达排他性多级树形菜单智能导航器(带防折叠状态断言机制) + 采用 XPath 亲子轴绝对隔离技术,彻底斩断嵌套手风琴菜单的向下漏包干扰 + """ + print(f">> 正在智能路由韵达菜单: {' -> '.join(menu_path)}") + for item in menu_path: - locator = page.locator(".el-submenu__title, .el-menu-item", has_text=item).first - locator.click() - page.wait_for_timeout(600) + title_locator = page.locator( + f"xpath=//div[contains(@class, 'el-submenu__title') and .//span[normalize-space(.)='{item}']]" + ).first + parent_li = page.locator( + f"xpath=//div[contains(@class, 'el-submenu__title') and .//span[normalize-space(.)='{item}']]/.." + ).first + leaf_locator = page.locator( + f"xpath=//li[contains(@class, 'el-menu-item') and .//span[normalize-space(.)='{item}']]" + ).first + + if title_locator.is_visible(): + current_class = parent_li.get_attribute("class") or "" + is_opened = "is-opened" in current_class + + if not is_opened: + print(f" -> 探测到父菜单 [{item}] 当前处于【收起】状态,执行点击展开") + title_locator.click() + page.wait_for_timeout(500) + else: + print( + f" -> 探测到父菜单 [{item}] 当前已经处于【展开】状态,安全跳过点击(防折叠保护激活)" + ) + + elif leaf_locator.is_visible(): + print(f" -> 成功对焦目标叶子节点 [{item}],直接执行跳转点击") + leaf_locator.click() + page.wait_for_timeout(1000) def yunda_expected_download(page): @@ -125,27 +153,26 @@ def yunda_expected_download(page): calendar2.locator(".laydate-btns-confirm").click() page.wait_for_timeout(500) - # ==================================================================== - # 🛡️ 深度优化:三段式安全缓冲,彻底规避 DOM 旧状态穿透陷阱 - # ==================================================================== + # 3. 多维状态机静默加载校验 print(">> 正在触发现场查询数据流...") ws_frame.locator("a.btn-success", has_text="查询").click() - # 阶段1:物理安全挂起,赋予浏览器触发渲染周期和 Ajax 的生命力 page.wait_for_timeout(800) - # 阶段2:精准抓取 Bootstrap Table 专属的加载遮罩层 + # ==================================================================== + # 🛡️ 核心修复:将 Loading 蒙层和数据断言限制在当前的 #tab-1 结界内部 + # 彻底解决多标签页导致 Strict Mode (5 elements found) 的污染问题 + # ==================================================================== loading_mask = ws_frame.locator( - ".fixed-table-loading", has_text="正在努力地加载数据中" - ) + "#tab-1 .fixed-table-loading", has_text="正在努力地加载数据中" + ).first if loading_mask.is_visible(): print(" ⏳ 检测到专属数据加载罩,正在等待后端重载返回...") loading_mask.wait_for(state="hidden", timeout=30000) - page.wait_for_timeout(500) # DOM 重排完毕后再缓冲半秒 + page.wait_for_timeout(500) - # 阶段3:进入绝对真实的终态判定世界 - sum_panel = ws_frame.locator("#sum") + sum_panel = ws_frame.locator("#sum").first has_data = False if sum_panel.is_visible(): @@ -158,9 +185,10 @@ def yunda_expected_download(page): ) if not has_data: + # 同样将空数据提示隔离在当前 tab 中 if ws_frame.locator( - ".no-records-found", has_text="没有找到匹配的记录" - ).is_visible(): + "#tab-1 .no-records-found", has_text="没有找到匹配的记录" + ).first.is_visible(): print( " ⚠️ 确认为空数据环境:系统提示【没有找到匹配的记录】,正在执行复位净化..." ) @@ -184,7 +212,7 @@ def yunda_expected_download(page): current_row = ws_frame.locator("#exampleTable1 tbody tr[data-index]").nth(i) raw_no = current_row.locator("td").nth(1).inner_text().strip() - print(f" -> 锁定提取单号: {raw_no}") + print(f" -> 锁定交接单号: {raw_no}") current_row.dblclick() @@ -201,7 +229,6 @@ def yunda_expected_download(page): export_frame.locator(".allRight").click() page.wait_for_timeout(400) - # 智能重试容错状态机 print(" >> 正在建立后台离线任务...") task_success = False for attempt in range(5): @@ -263,180 +290,6 @@ def yunda_expected_download(page): print(f"\n❌ 任务执行过程中发生异常: {e}") -def yunda_actual_download(page): - """韵达:实到货物数据下载""" - print("\n▶ 开始执行【韵达 - 实到货物数据下载】任务...") - target_task_title = "扫描记录数据" - - download_dir = os.path.join(os.getcwd(), "downloads") - if not os.path.exists(download_dir): - os.makedirs(download_dir) - - export_times = [] - - try: - page.locator(".el-menu-item", has_text="首页").wait_for( - state="visible", timeout=15000 - ) - print("✅ 韵达工作台首页成功加载") - - yunda_smart_menu_click(page, ["报表管理", "扫描记录查询"]) - - print(">> [雷达扫描] 正在跨域动态追踪【扫描记录查询】业务窗体...") - ws_frame = _wait_and_get_frame_by_selector(page, "#startDate") - - ws_frame.locator(".no-records-found", has_text="没有找到匹配的记录").wait_for( - state="visible", timeout=15000 - ) - print("✅ 扫描记录查询工作区初始化完毕") - - query_days = 1 - try: - if os.path.exists("config.yaml"): - with open("config.yaml", "r", encoding="utf-8") as f: - config = yaml.safe_load(f) or {} - query_days = int(config.get("yunda", {}).get("query_days", 1)) - except Exception: - pass - - today = datetime.now() - start_date = today - timedelta(days=(query_days - 1)) - - print(f">> 正在精准对焦实到时间区间: [近 {query_days} 天]") - - print(" >> 正在设定起始时间...") - ws_frame.locator("#startDate").click() - page.wait_for_timeout(400) - box1 = ws_frame.locator("#laydate_box:visible").first - box1.locator( - f"td[y='{start_date.year}'][m='{start_date.month}'][d='{start_date.day}']" - ).click() - page.wait_for_timeout(400) - - print(" >> 正在设定截止时间...") - ws_frame.locator("#endDate").click() - page.wait_for_timeout(400) - box2 = ws_frame.locator("#laydate_box:visible").first - box2.locator( - f"td[y='{today.year}'][m='{today.month}'][d='{today.day}']" - ).click() - page.wait_for_timeout(500) - - print(" >> 正在变更扫描类型为【到件】...") - ws_frame.locator("#scanRecordTyp").select_option(value="03") - page.wait_for_timeout(500) - - # ==================================================================== - # 🛡️ 深度优化:实到数据的三段式安全缓冲判定 - # ==================================================================== - print(">> 正在触发现场查询数据流...") - ws_frame.locator('input[type="button"][value="查询"]').click() - - page.wait_for_timeout(800) - - loading_mask = ws_frame.locator( - ".fixed-table-loading", has_text="正在努力地加载数据中" - ) - if loading_mask.is_visible(): - print(" ⏳ 检测到专属数据加载罩,正在等待后端重载返回...") - loading_mask.wait_for(state="hidden", timeout=30000) - - page.wait_for_timeout(500) - - pg_info = ws_frame.locator(".pagination-info") - has_records = False - - if pg_info.is_visible(): - info_text = pg_info.inner_text() - match_total = re.search(r"总共\s*(\d+)\s*条记录", info_text) - if match_total and int(match_total.group(1)) > 0: - has_records = True - print( - f" ✅ 深度断言:实到数据渲染完毕,总记录数: [{match_total.group(1)}] 条。" - ) - - if not has_records: - if ws_frame.locator( - ".no-records-found", has_text="没有找到匹配的记录" - ).is_visible(): - print( - " ⚠️ 当前查询范围内确认为空数据:系统提示【没有找到匹配的记录】,终止并关闭环境。" - ) - page.locator(".tags-view-item", has_text="扫描记录查询").locator( - ".el-icon-close" - ).click() - return - else: - print(" ⚠️ 发生渲染异常,未找到数据也未找到空记录提示。安全收尾...") - page.locator(".tags-view-item", has_text="扫描记录查询").locator( - ".el-icon-close" - ).click() - return - - # 5. 执行导出流 - print(">> 正在发起【导出】申请指令...") - ws_frame.locator('input[type="button"][id="export"]').click() - - ws_frame.locator(".layui-layer-title", has_text="数据导出").wait_for( - state="visible", timeout=15000 - ) - export_frame = ws_frame.frame_locator('iframe[name="myFrame"]') - - export_frame.locator(".allRight").click() - page.wait_for_timeout(400) - - print(" >> 正在建立后台离线任务...") - task_success = False - for attempt in range(5): - export_frame.locator("#submitbutton", has_text="导出数据").click() - confirm_link = export_frame.get_by_role("link", name="确定") - try: - confirm_link.wait_for(state="visible", timeout=6000) - if export_frame.get_by_text("导出任务建立成功").is_visible(): - print(" ✅ 判定通过:成功捕获到【导出任务建立成功】特征!") - confirm_link.click() - task_success = True - break - elif export_frame.get_by_text("请选择格式相应的导出字段").is_visible(): - print(" ⚠️ 警告:检测到字段未全选,执行强补点击...") - confirm_link.click() - page.wait_for_timeout(500) - export_frame.locator(".allRight").click() - page.wait_for_timeout(500) - else: - confirm_link.click() - page.wait_for_timeout(1000) - except Exception: - page.wait_for_timeout(1000) - - if not task_success: - raise RuntimeError( - "致命异常:连续 5 次尝试均无法成功建立实到数据离线任务。" - ) - - ws_frame.locator(".layui-layer-close1").click() - page.wait_for_timeout(500) - export_times.append(datetime.now()) - - print(">> 📤 任务提交流闭环,正在执行【扫描记录查询】工作台销毁...") - page.locator(".tags-view-item", has_text="扫描记录查询").locator( - ".el-icon-close" - ).click() - page.wait_for_timeout(500) - - # 6. 收割下载 - _yunda_poll_and_download_tasks( - page, - export_times, - target_task_title, - download_dir, - "韵达-实到货物数据.xlsx", - ) - - except Exception as e: - print(f"\n❌ 任务执行过程中发生异常: {e}") - - def _yunda_poll_and_download_tasks( page, export_times, target_task_title, download_dir, final_filename ): @@ -445,6 +298,7 @@ def _yunda_poll_and_download_tasks( yunda_smart_menu_click(page, ["基础数据", "导出服务"]) export_ws_frame = page.frame_locator("section iframe") + export_ws_frame.get_by_role("cell", name="模块名称", exact=True).wait_for( state="visible", timeout=15000 ) @@ -531,7 +385,7 @@ def _yunda_poll_and_download_tasks( except Exception as e: print(f" ❌ 文件流接收失败: {e}") - # 环境清理 + # 环境清理:彻底关闭“导出服务”Tab print(">> 📥 【导出服务】数据提取链闭环,正在执行当前 Tab 窗口销毁...") try: page.locator(".tags-view-item", has_text="导出服务").locator( @@ -541,7 +395,7 @@ def _yunda_poll_and_download_tasks( except: pass - # 合并扁平数据集 + # 9. 合并扁平数据集 if downloaded_files: print("\n>> 🧪 正在启动离线数据清洗与高能扁平合并流...") all_dfs = [] @@ -565,3 +419,175 @@ def _yunda_poll_and_download_tasks( for file_path in downloaded_files: os.remove(file_path) print(" ✅ 临时缓存阵列已无缝净化。") + + +def yunda_actual_download(page): + """韵达:实到货物数据下载""" + print("\n▶ 开始执行【韵达 - 实到货物数据下载】任务...") + target_task_title = "扫描记录数据" + + download_dir = os.path.join(os.getcwd(), "downloads") + if not os.path.exists(download_dir): + os.makedirs(download_dir) + + export_times = [] + + try: + page.locator(".el-menu-item", has_text="首页").wait_for( + state="visible", timeout=15000 + ) + print("✅ 韵达工作台首页成功加载") + + yunda_smart_menu_click(page, ["报表管理", "扫描记录查询"]) + + print(">> [雷达扫描] 正在跨域动态追踪【扫描记录查询】业务窗体...") + ws_frame = _wait_and_get_frame_by_selector(page, "#startDate") + + ws_frame.locator( + ".no-records-found", has_text="没有找到匹配的记录" + ).first.wait_for(state="visible", timeout=15000) + print("✅ 扫描记录查询工作区初始化完毕") + + query_days = 1 + try: + if os.path.exists("config.yaml"): + with open("config.yaml", "r", encoding="utf-8") as f: + config = yaml.safe_load(f) or {} + query_days = int(config.get("yunda", {}).get("query_days", 1)) + except Exception: + pass + + today = datetime.now() + start_date = today - timedelta(days=(query_days - 1)) + + print(f">> 正在精准对焦实到时间区间: [近 {query_days} 天]") + + print(" >> 正在设定起始时间...") + ws_frame.locator("#startDate").click() + page.wait_for_timeout(400) + box1 = ws_frame.locator("#laydate_box:visible").first + box1.locator( + f"td[y='{start_date.year}'][m='{start_date.month}'][d='{start_date.day}']" + ).click() + page.wait_for_timeout(400) + + print(" >> 正在设定截止时间...") + ws_frame.locator("#endDate").click() + page.wait_for_timeout(400) + box2 = ws_frame.locator("#laydate_box:visible").first + box2.locator( + f"td[y='{today.year}'][m='{today.month}'][d='{today.day}']" + ).click() + page.wait_for_timeout(500) + + print(" >> 正在变更扫描类型为【到件】...") + ws_frame.locator("#scanRecordTyp").select_option(value="03") + page.wait_for_timeout(500) + + print(">> 正在触发现场查询数据流...") + ws_frame.locator('input[type="button"][value="查询"]').click() + + page.wait_for_timeout(800) + + # 同样在这里加上防 strict mode 穿透的 first 隔离 + loading_mask = ws_frame.locator( + ".fixed-table-loading", has_text="正在努力地加载数据中" + ).first + if loading_mask.is_visible(): + print(" ⏳ 检测到专属数据加载罩,正在等待后端重载返回...") + loading_mask.wait_for(state="hidden", timeout=30000) + + page.wait_for_timeout(500) + + pg_info = ws_frame.locator(".pagination-info").first + has_records = False + + if pg_info.is_visible(): + info_text = pg_info.inner_text() + match_total = re.search(r"总共\s*(\d+)\s*条记录", info_text) + if match_total and int(match_total.group(1)) > 0: + has_records = True + print( + f" ✅ 深度断言:实到数据渲染完毕,总记录数: [{match_total.group(1)}] 条。" + ) + + if not has_records: + if ws_frame.locator( + ".no-records-found", has_text="没有找到匹配的记录" + ).first.is_visible(): + print( + " ⚠️ 当前查询范围内确认为空数据:系统提示【没有找到匹配的记录】,终止并关闭环境。" + ) + page.locator(".tags-view-item", has_text="扫描记录查询").locator( + ".el-icon-close" + ).click() + return + else: + print(" ⚠️ 发生渲染异常,未找到数据也未找到空记录提示。安全收尾...") + page.locator(".tags-view-item", has_text="扫描记录查询").locator( + ".el-icon-close" + ).click() + return + + # 5. 执行导出流 + print(">> 正在发起【导出】申请指令...") + ws_frame.locator('input[type="button"][id="export"]').click() + + ws_frame.locator(".layui-layer-title", has_text="数据导出").wait_for( + state="visible", timeout=15000 + ) + export_frame = ws_frame.frame_locator('iframe[name="myFrame"]') + + export_frame.locator(".allRight").click() + page.wait_for_timeout(400) + + print(" >> 正在建立后台离线任务...") + task_success = False + for attempt in range(5): + export_frame.locator("#submitbutton", has_text="导出数据").click() + confirm_link = export_frame.get_by_role("link", name="确定") + try: + confirm_link.wait_for(state="visible", timeout=6000) + if export_frame.get_by_text("导出任务建立成功").is_visible(): + print(" ✅ 判定通过:成功捕获到【导出任务建立成功】特征!") + confirm_link.click() + task_success = True + break + elif export_frame.get_by_text("请选择格式相应的导出字段").is_visible(): + print(" ⚠️ 警告:检测到字段未全选,执行强补点击...") + confirm_link.click() + page.wait_for_timeout(500) + export_frame.locator(".allRight").click() + page.wait_for_timeout(500) + else: + confirm_link.click() + page.wait_for_timeout(1000) + except Exception: + page.wait_for_timeout(1000) + + if not task_success: + raise RuntimeError( + "致命异常:连续 5 次尝试均无法成功建立实到数据离线任务。" + ) + + ws_frame.locator(".layui-layer-close1").click() + page.wait_for_timeout(500) + export_times.append(datetime.now()) + + print(">> 📤 任务提交流闭环,正在执行【扫描记录查询】工作台销毁...") + page.locator(".tags-view-item", has_text="扫描记录查询").locator( + ".el-icon-close" + ).click() + page.wait_for_timeout(500) + + # 6. 收割下载 + _yunda_poll_and_download_tasks( + page, + export_times, + target_task_title, + download_dir, + "韵达-实到货物数据.xlsx", + ) + + except Exception as e: + print(f"\n❌ 任务执行过程中发生异常: {e}")