fix(anneng): real-type ZK dateboxes + commit-wait before query, skip out-of-range handovers
This commit is contained in:
@@ -11,8 +11,8 @@
|
||||
#
|
||||
# 应到 = 运单信息导出(每张交接单下应到的运单明细)。整体流程:
|
||||
# 主页(重庆鱼洞镇) → 运营管理 → 进站管理 → 进站交接单查询(tab)
|
||||
# → 设日期 → 查询 → 等“带小数点的 0”出现(数据加载完毕)
|
||||
# → 逐条交接单:记下交接单号 → 双击 → 等运单信息加载并核对单号
|
||||
# → 设日期 → 等 ZK 组件值提交确认 → 查询 → 等“带小数点的 0”出现(数据加载完毕)
|
||||
# → 逐条交接单(交接日期超出范围的不导出):记下交接单号 → 双击 → 等运单信息加载并核对单号
|
||||
# → 导出 → 双击转移全部待选字段 → 导出数据 → 任务添加成功 → 确认 → 回交接单信息
|
||||
# → 关闭进站交接单查询 tab → 打开导出下载(tab)
|
||||
# → 轮询直到本批“交接单明细”任务全部导出完成 → 逐个免对话框下载(x-auth+TGC 直连 GET)
|
||||
@@ -30,7 +30,7 @@ import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
import pandas as pd
|
||||
import websocket
|
||||
@@ -375,25 +375,57 @@ def wait_query_page_ready(tab_cdp):
|
||||
|
||||
|
||||
def set_zk_datebox(tab_cdp, index, date_str):
|
||||
"""直接向第 index 个 z-datebox-inp 写入日期(YYYY-MM-DD),并触发 change。
|
||||
"""向第 index 个 z-datebox-inp 真实键入日期(YYYY-MM-DD),JS blur 触发提交。
|
||||
|
||||
安能的日期可直接输入(无需日历控件)。ZK datebox 监听 input 的 change 事件。
|
||||
安能日期框是 ZK 组件:直接用原生 setter 改 value + 派发 input/change 事件不会
|
||||
触发 ZK 的 onChange AU(服务端拿不到新值,查询仍用旧范围);必须用 CDP Input 域
|
||||
真实键入(insertText),再 JS blur() 让 ZK 解析、校验并提交 onChange。
|
||||
不用 Tab 按键触发 blur:CDP 按键事件依赖窗口在前台,窗口在后台时会被丢弃。
|
||||
超出页面允许的查询范围时 ZK 会拒绝(不发 onChange),由
|
||||
wait_zk_datebox_committed 等待超时暴露出来。
|
||||
"""
|
||||
js = (
|
||||
tab_cdp.eval(
|
||||
"(() => {"
|
||||
f" const inp = document.querySelectorAll('input.z-datebox-inp')[{index}];"
|
||||
" if (!inp) return false;"
|
||||
" inp.focus();"
|
||||
" const setter = Object.getOwnPropertyDescriptor("
|
||||
" window.HTMLInputElement.prototype, 'value').set;"
|
||||
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;"
|
||||
" inp.select();"
|
||||
" return true;"
|
||||
"})()"
|
||||
)
|
||||
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):
|
||||
@@ -421,12 +453,54 @@ def wait_query_done(tab_cdp):
|
||||
)
|
||||
|
||||
|
||||
def collect_jiaojie_dan_ids(tab_cdp):
|
||||
"""收集交接单信息表里所有交接单号(从行复选框的 ewbsListNo 解析,按出现顺序)。
|
||||
_JAVA_MONTHS = {
|
||||
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/end(date)时,
|
||||
只保留交接日期在 [start, end] 内的交接单;超期或日期解析失败的只跳过、不中断,
|
||||
数量计入返回的 skipped。交接单号是 19 位纯数字,必须按文本保留,避免精度丢失。
|
||||
返回 (ids, skipped)。
|
||||
"""
|
||||
rows = (
|
||||
tab_cdp.eval(
|
||||
"(() => {"
|
||||
" const boxes = [...document.querySelectorAll('.z-listbox')];"
|
||||
@@ -439,14 +513,25 @@ def collect_jiaojie_dan_ids(tab_cdp):
|
||||
" const rows = [...box.querySelectorAll('tr.z-listitem')]"
|
||||
" .filter(tr => tr.querySelector('input[value*=\"ewbsListNo=\"]'));"
|
||||
" return rows.map(tr => {"
|
||||
" const inp = tr.querySelector('input[value*=\"ewbsListNo=\"]');"
|
||||
" const m = /ewbsListNo=(\\d+)/.exec(inp.value || '');"
|
||||
" return m ? m[1] : null;"
|
||||
" const v = (tr.querySelector('input[value*=\"ewbsListNo=\"]').value) || '';"
|
||||
" const m = /ewbsListNo=(\\d+)/.exec(v);"
|
||||
" if (!m) return null;"
|
||||
" const c = /createdTime=([^,\\]]+)/.exec(v);"
|
||||
" return {id: m[1], created: c ? c[1] : null};"
|
||||
" }).filter(Boolean);"
|
||||
"})()"
|
||||
)
|
||||
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):
|
||||
@@ -947,25 +1032,41 @@ def anneng_expected_download_impl(force=False, date=None):
|
||||
wait_query_page_ready(tab_cdp)
|
||||
print("✅ 进站交接单查询页已加载")
|
||||
|
||||
# 2) 设日期 + 查询 + 等加载
|
||||
# 2) 设日期 + 提交确认 + 查询 + 等加载
|
||||
print(">> 正在设置查询时间范围(直接输入日期)...")
|
||||
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)
|
||||
time.sleep(0.3)
|
||||
print(">> 正在执行查询 ...")
|
||||
wait_zk_datebox_committed(tab_cdp, 1, today_str)
|
||||
print(">> 时间范围已提交确认,正在执行查询 ...")
|
||||
click_query_button(tab_cdp)
|
||||
wait_query_done(tab_cdp)
|
||||
print("✅ 查询完成,数据已加载")
|
||||
|
||||
# 3) 收集交接单号并逐条导出
|
||||
# 行的渲染可能比统计的小数点晚一拍:轮询等行出现;持续为空才视为无数据。
|
||||
# 空结果页面会出现「没有任何相关信息」空态文案;若旧行还没被清掉,
|
||||
# 靠交接日期范围过滤兜底。持续为空才视为无数据。
|
||||
target_ids = []
|
||||
skipped_ids = 0
|
||||
collect_deadline = time.monotonic() + 15.0
|
||||
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:
|
||||
break
|
||||
time.sleep(0.5)
|
||||
if skipped_ids:
|
||||
print(
|
||||
f">> 忽略 {skipped_ids} 条超期/日期解析失败的交接单"
|
||||
f"(超出 [{start_str} ~ {today_str}] 范围)"
|
||||
)
|
||||
print(f">> 共捕获 {len(target_ids)} 条交接单记录")
|
||||
if not target_ids:
|
||||
print(">> ⚠ 没有交接单数据,结束。")
|
||||
|
||||
Reference in New Issue
Block a user