fix(anneng): real-type ZK dateboxes + commit-wait before query, skip out-of-range handovers

This commit is contained in:
Misaka_Company
2026-08-04 14:45:57 +08:00
parent 36b204abe5
commit 2387593eda

View File

@@ -11,8 +11,8 @@
# #
# 应到 = 运单信息导出(每张交接单下应到的运单明细)。整体流程: # 应到 = 运单信息导出(每张交接单下应到的运单明细)。整体流程:
# 主页(重庆鱼洞镇) → 运营管理 → 进站管理 → 进站交接单查询(tab) # 主页(重庆鱼洞镇) → 运营管理 → 进站管理 → 进站交接单查询(tab)
# → 设日期 → 查询 → 等“带小数点的 0”出现数据加载完毕 # → 设日期 → 等 ZK 组件值提交确认 → 查询 → 等“带小数点的 0”出现数据加载完毕
# → 逐条交接单:记下交接单号 → 双击 → 等运单信息加载并核对单号 # → 逐条交接单(交接日期超出范围的不导出):记下交接单号 → 双击 → 等运单信息加载并核对单号
# → 导出 → 双击转移全部待选字段 → 导出数据 → 任务添加成功 → 确认 → 回交接单信息 # → 导出 → 双击转移全部待选字段 → 导出数据 → 任务添加成功 → 确认 → 回交接单信息
# → 关闭进站交接单查询 tab → 打开导出下载(tab) # → 关闭进站交接单查询 tab → 打开导出下载(tab)
# → 轮询直到本批“交接单明细”任务全部导出完成 → 逐个免对话框下载x-auth+TGC 直连 GET # → 轮询直到本批“交接单明细”任务全部导出完成 → 逐个免对话框下载x-auth+TGC 直连 GET
@@ -30,7 +30,7 @@ import sys
import time import time
import urllib.parse import urllib.parse
import urllib.request import urllib.request
from datetime import datetime, timedelta from datetime import date, datetime, timedelta
import pandas as pd import pandas as pd
import websocket import websocket
@@ -375,25 +375,57 @@ def wait_query_page_ready(tab_cdp):
def set_zk_datebox(tab_cdp, index, date_str): def set_zk_datebox(tab_cdp, index, date_str):
"""直接向第 index 个 z-datebox-inp 入日期YYYY-MM-DD并触发 change """向第 index 个 z-datebox-inp 真实键入日期YYYY-MM-DDJS blur 触发提交
安能日期可直接输入无需日历控件。ZK datebox 监听 inputchange 事件 安能日期框是 ZK 组件:直接用原生 setter 改 value + 派发 input/change 事件不会
触发 ZK 的 onChange AU服务端拿不到新值查询仍用旧范围必须用 CDP Input 域
真实键入insertText再 JS blur() 让 ZK 解析、校验并提交 onChange。
不用 Tab 按键触发 blurCDP 按键事件依赖窗口在前台,窗口在后台时会被丢弃。
超出页面允许的查询范围时 ZK 会拒绝(不发 onChange
wait_zk_datebox_committed 等待超时暴露出来。
""" """
js = ( tab_cdp.eval(
"(() => {" "(() => {"
f" const inp = document.querySelectorAll('input.z-datebox-inp')[{index}];" f" const inp = document.querySelectorAll('input.z-datebox-inp')[{index}];"
" if (!inp) return false;" " if (!inp) return false;"
" inp.focus();" " inp.focus();"
" const setter = Object.getOwnPropertyDescriptor(" " inp.select();"
" window.HTMLInputElement.prototype, 'value').set;" " return true;"
f" setter.call(inp, {json.dumps(date_str)});"
" inp.dispatchEvent(new Event('input', {bubbles:true}));"
" inp.dispatchEvent(new Event('change', {bubbles:true}));"
" inp.blur();"
" return inp.value;"
"})()" "})()"
) )
return tab_cdp.eval(js) time.sleep(0.15)
tab_cdp.call("Input.insertText", text=date_str)
time.sleep(0.2)
tab_cdp.eval(
"(() => {"
f" const inp = document.querySelectorAll('input.z-datebox-inp')[{index}];"
" if (inp) inp.blur();"
" return true;"
"})()"
)
def wait_zk_datebox_committed(tab_cdp, index, date_str, timeout=5.0):
"""轮询 ZK 组件值已提交到服务端(读 zk.Widget 的 getText()),确认后再返回。
真实键入 + blur 触发 onChange AU 后服务端接受才会把新值同步回组件setAttr
因此 getText() 等于目标日期即代表服务端已确认。超时说明日期被页面拒绝
(大概率超出该页允许的查询范围),会抛超时报错而不是用旧范围静默查询。
"""
js_date = json.dumps(date_str)
wait_until(
tab_cdp,
"(() => {"
f" const inp = document.querySelectorAll('input.z-datebox-inp')[{index}];"
" if (!inp || typeof zk === 'undefined') return false;"
" const w = zk.Widget.$(inp);"
" if (!w || typeof w.getText !== 'function') return false;"
" const t = String(w.getText() ?? '').trim();"
f" return t === {js_date} || t.split(' ')[0] === {js_date};"
"})()",
f"ZK 日期组件[{index}]已提交 {date_str}",
timeout=timeout,
)
def click_query_button(tab_cdp): def click_query_button(tab_cdp):
@@ -421,12 +453,54 @@ def wait_query_done(tab_cdp):
) )
def collect_jiaojie_dan_ids(tab_cdp): _JAVA_MONTHS = {
"""收集交接单信息表里所有交接单号(从行复选框的 ewbsListNo 解析,按出现顺序)。 m: i
for i, m in enumerate(
[
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
],
start=1,
)
}
交接单号是 19 位纯数字,必须按文本保留,避免精度丢失。
def _parse_java_date(text):
"""解析 Java Date.toString()(如 'Mon Aug 03 05:50:46 CST 2026')为 date。
固定 token 切分,避免 strptime 对 %Z/%a/%b 的 locale 依赖。
""" """
return ( parts = (text or "").split()
if len(parts) != 6:
return None
month = _JAVA_MONTHS.get(parts[1])
if not month:
return None
try:
return date(int(parts[5]), month, int(parts[2]))
except (TypeError, ValueError):
return None
def collect_jiaojie_dan_ids(tab_cdp, start=None, end=None):
"""收集交接单信息表里的交接单号(从行复选框的 ewbsListNo 解析,按出现顺序)。
同时解析每行 value 里的 createdTime交接时间。传 start/enddate
只保留交接日期在 [start, end] 内的交接单;超期或日期解析失败的只跳过、不中断,
数量计入返回的 skipped。交接单号是 19 位纯数字,必须按文本保留,避免精度丢失。
返回 (ids, skipped)。
"""
rows = (
tab_cdp.eval( tab_cdp.eval(
"(() => {" "(() => {"
" const boxes = [...document.querySelectorAll('.z-listbox')];" " const boxes = [...document.querySelectorAll('.z-listbox')];"
@@ -439,14 +513,25 @@ def collect_jiaojie_dan_ids(tab_cdp):
" const rows = [...box.querySelectorAll('tr.z-listitem')]" " const rows = [...box.querySelectorAll('tr.z-listitem')]"
" .filter(tr => tr.querySelector('input[value*=\"ewbsListNo=\"]'));" " .filter(tr => tr.querySelector('input[value*=\"ewbsListNo=\"]'));"
" return rows.map(tr => {" " return rows.map(tr => {"
" const inp = tr.querySelector('input[value*=\"ewbsListNo=\"]');" " const v = (tr.querySelector('input[value*=\"ewbsListNo=\"]').value) || '';"
" const m = /ewbsListNo=(\\d+)/.exec(inp.value || '');" " const m = /ewbsListNo=(\\d+)/.exec(v);"
" return m ? m[1] : null;" " if (!m) return null;"
" const c = /createdTime=([^,\\]]+)/.exec(v);"
" return {id: m[1], created: c ? c[1] : null};"
" }).filter(Boolean);" " }).filter(Boolean);"
"})()" "})()"
) )
or [] or []
) )
ids, skipped = [], 0
for rec in rows:
created = _parse_java_date(rec.get("created")) if rec.get("created") else None
if start is not None and end is not None:
if created is None or not (start <= created <= end):
skipped += 1
continue
ids.append(rec["id"])
return ids, skipped
def dblclick_jiaojie_dan_row(tab_cdp, ewbs_no): def dblclick_jiaojie_dan_row(tab_cdp, ewbs_no):
@@ -947,25 +1032,41 @@ def anneng_expected_download_impl(force=False, date=None):
wait_query_page_ready(tab_cdp) wait_query_page_ready(tab_cdp)
print("✅ 进站交接单查询页已加载") print("✅ 进站交接单查询页已加载")
# 2) 设日期 + 查询 + 等加载 # 2) 设日期 + 提交确认 + 查询 + 等加载
print(">> 正在设置查询时间范围(直接输入日期)...") print(">> 正在设置查询时间范围(直接输入日期)...")
set_zk_datebox(tab_cdp, 0, start_str) set_zk_datebox(tab_cdp, 0, start_str)
# ZK 组件值异步提交:逐个设置并等服务端确认后再设下一个,
# 避免前一个 datebox 的 AU 响应与后一个的键入互相干扰(曾导致偶发提交超时);
# 未确认前点查询会按旧的时间范围查询(安能页面特性)。
wait_zk_datebox_committed(tab_cdp, 0, start_str)
set_zk_datebox(tab_cdp, 1, today_str) set_zk_datebox(tab_cdp, 1, today_str)
time.sleep(0.3) wait_zk_datebox_committed(tab_cdp, 1, today_str)
print(">> 正在执行查询 ...") print(">> 时间范围已提交确认,正在执行查询 ...")
click_query_button(tab_cdp) click_query_button(tab_cdp)
wait_query_done(tab_cdp) wait_query_done(tab_cdp)
print("✅ 查询完成,数据已加载") print("✅ 查询完成,数据已加载")
# 3) 收集交接单号并逐条导出 # 3) 收集交接单号并逐条导出
# 行的渲染可能比统计的小数点晚一拍:轮询等行出现;持续为空才视为无数据。 # 空结果页面会出现「没有任何相关信息」空态文案;若旧行还没被清掉,
# 靠交接日期范围过滤兜底。持续为空才视为无数据。
target_ids = [] target_ids = []
skipped_ids = 0
collect_deadline = time.monotonic() + 15.0 collect_deadline = time.monotonic() + 15.0
while time.monotonic() < collect_deadline: while time.monotonic() < collect_deadline:
target_ids = collect_jiaojie_dan_ids(tab_cdp) if tab_cdp.eval("document.body.innerText.includes('没有任何相关信息')"):
print(">> 查询完成但无交接单数据(页面空态),结束。")
return
target_ids, skipped_ids = collect_jiaojie_dan_ids(
tab_cdp, start=target.date(), end=target.date()
)
if target_ids: if target_ids:
break break
time.sleep(0.5) time.sleep(0.5)
if skipped_ids:
print(
f">> 忽略 {skipped_ids} 条超期/日期解析失败的交接单"
f"(超出 [{start_str} ~ {today_str}] 范围)"
)
print(f">> 共捕获 {len(target_ids)} 条交接单记录") print(f">> 共捕获 {len(target_ids)} 条交接单记录")
if not target_ids: if not target_ids:
print(">> ⚠ 没有交接单数据,结束。") print(">> ⚠ 没有交接单数据,结束。")