fix(zto): 修复双月日期控件 real-today 定位失败(月初1号隐藏 ghost cell)

中通 jQuery Date Range Picker 始终显示双月面板。月初时(如8月1日),
当天日期同时出现在 month1(隐藏 ghost cell)和 month2(可见 cell),
两者的 CSS class 都含 real-today,.first 取到隐藏元素导致超时。

改为直接从 Python datetime 计算目标时间戳,新增 _zto_find_visible_day
查找可见日期格(跳过 display:none 的 ghost cell),同步修复
_zto_flip_to_target_month 的可见性判断。

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-08-01 17:33:20 +08:00
parent eaf56c1c9e
commit b916d82058

View File

@@ -4,7 +4,7 @@ import os
import re
import time
import yaml
from datetime import datetime
from datetime import datetime, timedelta
import pandas as pd
from inbound_verify.paths import DOWNLOAD_DIR, CONFIG_PATH
@@ -93,20 +93,50 @@ def _dom_click(locator):
)
def _zto_compute_target_time(offset):
"""直接从 Python datetime 计算目标日期的毫秒级时间戳(本地时区零点)。
不再从 DOM 的 real-today 元素读取 time 属性,避免双月视图下 real-today
同时出现在 month1隐藏 ghost cell和 month2可见导致 .first 取到隐藏元素。
"""
target_date = datetime.now().date() - timedelta(days=offset)
target_dt = datetime(target_date.year, target_date.month, target_date.day)
return int(target_dt.timestamp() * 1000)
def _zto_find_visible_day(frame_locator, target_time):
"""在双月日期控件中查找可见的日期格子。
jQuery Date Range Picker 双月视图下,同一天可能出现在两个面板中:
- month1左面板的溢出 ghost celldisplay:none不可见
- month2右面板的正常 cell可见
同一日期在 DOM 中可能有毫秒级差异(零点 vs 23:59:59遍历匹配并返回
第一个 visible 的;无可见匹配返回 None。
"""
# 尝试两个时间变体:零点 和 23:59:59部分 checked/selected 格用后者)
for time_variant in (target_time, target_time + 86399000):
sel = f"td div.day[time='{time_variant}']"
cells = frame_locator.locator(sel)
count = cells.count()
for i in range(count):
if cells.nth(i).is_visible():
return cells.nth(i)
return None
def _zto_flip_to_target_month(frame_locator, page, target_time, max_flips=12):
"""中通日历(jQuery-Date-Range-Picker 双月视图)跨月导航:目标日期不在当前视窗时,
循环点 .prev 把目标月翻进 month1 视窗。offset 恒指向过去,故只往前翻;步长 1 月/次
用 JS 派发点击(.evaluate("el=>el.click()"))避开 .date-range-length-tip 等 hover 遮挡。
循环点 .prev 把目标月翻进视窗。用 _zto_find_visible_day 判可见(跳过隐藏 ghost cell
返回 True 若目标格子最终可见。"""
sel = f"td div.day[time='{target_time}']"
for _ in range(max_flips):
if frame_locator.locator(sel).first.is_visible():
if _zto_find_visible_day(frame_locator, target_time) is not None:
return True
frame_locator.locator(".date-picker-wrapper .prev").first.evaluate(
"el => el.click()"
)
page.wait_for_timeout(450)
return frame_locator.locator(sel).first.is_visible()
return _zto_find_visible_day(frame_locator, target_time) is not None
def zto_smart_menu_click(page, menu_path):
@@ -170,32 +200,29 @@ def zto_expected_download_impl(page, force=False, date=None):
ewb_frame.locator("#beginDate").click()
page.wait_for_timeout(500)
today_cell = ewb_frame.locator("td div.day.real-today").first
today_cell.wait_for(state="visible")
# 直接从 Python datetime 计算目标时间戳,不再依赖 DOM real-today双月视图
# 下 real-today 可能同时出现在 month1 隐藏 ghost cell 和 month2 可见 cell
# .first 会取到隐藏的那个导致 wait_for(visible) 超时)。
target_time = _zto_compute_target_time(offset)
target_cell = _zto_find_visible_day(ewb_frame, target_time)
today_time_str = today_cell.get_attribute("time")
if today_time_str:
today_time = int(today_time_str)
target_time = today_time - offset * 86400000
target_cell = ewb_frame.locator(f"td div.day[time='{target_time}']").first
if target_cell is None:
print(" 目标日期不在当前视窗,正在翻月导航 ...")
if not _zto_flip_to_target_month(ewb_frame, page, target_time):
raise RuntimeError(
f"翻月后仍无法定位目标日期格子(time={target_time})"
)
target_cell = _zto_find_visible_day(ewb_frame, target_time)
if target_cell is None:
raise RuntimeError(
f"翻月后仍无法定位目标日期格子(time={target_time})"
)
# 偏移日期跨月时目标格子不在当前双月视窗 → 向前翻月把它带进视窗,不再降级为当天
if not target_cell.is_visible():
print(" 偏移日期跨月,正在向前翻月导航到目标 ...")
if not _zto_flip_to_target_month(ewb_frame, page, target_time):
raise RuntimeError(
f"翻月后仍无法定位目标日期格子(time={target_time})"
)
# 日期格子用 _dom_click 直接派发事件(同实到):.click() 会先 hover 格子,触发
# “范围长度”提示气泡(.date-range-length-tip)盖住格子导致点击被遮挡超时,
# 跨月选中非今日格子时尤为明显。
_dom_click(target_cell)
page.wait_for_timeout(300)
_dom_click(target_cell)
else:
today_cell.click()
page.wait_for_timeout(300)
today_cell.click()
# 日期格子用 _dom_click 直接派发事件:.click() 会先 hover 格子,触发
# "范围长度"提示气泡(.date-range-length-tip)盖住格子导致点击被遮挡超时。
_dom_click(target_cell)
page.wait_for_timeout(300)
_dom_click(target_cell)
page.wait_for_timeout(500)
@@ -422,31 +449,29 @@ def zto_actual_download_impl(page, date=None):
arr_frame.locator("#daterange").click()
page.wait_for_timeout(500)
today_cell = arr_frame.locator("td div.day.real-today").first
today_cell.wait_for(state="visible")
# 直接从 Python datetime 计算目标时间戳,不再依赖 DOM real-today双月视图
# 下 real-today 可能同时出现在 month1 隐藏 ghost cell 和 month2 可见 cell
# .first 会取到隐藏的那个导致 wait_for(visible) 超时)。
target_time = _zto_compute_target_time(offset)
target_cell = _zto_find_visible_day(arr_frame, target_time)
today_time_str = today_cell.get_attribute("time")
if today_time_str:
today_time = int(today_time_str)
target_time = today_time - offset * 86400000
target_cell = arr_frame.locator(f"td div.day[time='{target_time}']").first
if target_cell is None:
print(" 目标日期不在当前视窗,正在翻月导航 ...")
if not _zto_flip_to_target_month(arr_frame, page, target_time):
raise RuntimeError(
f"翻月后仍无法定位目标日期格子(time={target_time})"
)
target_cell = _zto_find_visible_day(arr_frame, target_time)
if target_cell is None:
raise RuntimeError(
f"翻月后仍无法定位目标日期格子(time={target_time})"
)
# 日期格子用 _dom_click 直接派发事件Playwright 的 .click() 会先 hover 格子,
# 触发范围长度提示气泡(.date-range-length-tip)盖住格子,导致点击被判遮挡而超时。
# 偏移日期跨月时目标格子不在当前双月视窗 → 向前翻月把它带进视窗,不再降级为当天。
if not target_cell.is_visible():
print(" 偏移日期跨月,正在向前翻月导航到目标 ...")
if not _zto_flip_to_target_month(arr_frame, page, target_time):
raise RuntimeError(
f"翻月后仍无法定位目标日期格子(time={target_time})"
)
_dom_click(target_cell)
page.wait_for_timeout(300)
_dom_click(target_cell)
else:
_dom_click(today_cell)
page.wait_for_timeout(300)
_dom_click(today_cell)
# 日期格子用 _dom_click 直接派发事件Playwright 的 .click() 会先 hover 格子,
# 触发"范围长度"提示气泡(.date-range-length-tip)盖住格子,导致点击被判遮挡而超时。
_dom_click(target_cell)
page.wait_for_timeout(300)
_dom_click(target_cell)
page.wait_for_timeout(500)
@@ -695,7 +720,7 @@ def _zto_poll_and_download_tasks(page, export_times, download_dir, final_filenam
)
# ====================================================================
# 所有目标文件下载完成后,关闭导出任务管理标签页
# 所有目标文件下载完成后,关闭"导出任务管理"标签页
# ====================================================================
print(">> 【导出任务管理】下载完成,正在关闭标签页...")
try: