修复安能实到日期设定 + 站点统计口径重构
- site_anneng.py: set_scan_date 改用 execCommand 全选替换+回车(修复 Ant DatePicker 受控组件未写入导致实到下成当天); wait_scan_form_ready 增加 settle 与 DatePicker 预热,消除首设竞态 - expected_undelivered.py: 应到口径改交接件数、实到改单号去重、未到明细列已到单号(不再编子单号),百世排除 - docs: 补充站点统计逻辑审查报告与韵达/安能计算逻辑梳理
This commit is contained in:
@@ -962,21 +962,62 @@ def anneng_expected_download_impl():
|
||||
# ====================================================================
|
||||
|
||||
|
||||
def set_scan_date(cdp, placeholder, value):
|
||||
"""向扫描时间输入框写入日期时间并回车接受(Ant 可编辑 DatePicker)。"""
|
||||
def set_scan_date(cdp, placeholder, value, target_ymd=None):
|
||||
"""向扫描时间输入框写入日期时间并回车接受(Ant Design DatePicker,rc-picker 受控组件)。
|
||||
|
||||
关键:rc-picker 是 React 受控组件,原生 setter 直接改 .value 会被其重渲染冲掉;且控件里
|
||||
往往已有默认日期,必须先「全选」再整体替换为新值。正确做法对齐人工操作:
|
||||
进入控件(焦点激活) → 全选既有文本 → insertText 整体替换(派发 input 事件, onChange 受理) → 回车提交。
|
||||
|
||||
target_ymd 可显式传入 (y,m,d) 用于写后回读校验;不传则从 value 解析。
|
||||
返回 True 表示写入并校验成功,否则 False。
|
||||
"""
|
||||
import re as _re
|
||||
ph = json.dumps(placeholder)
|
||||
return cdp.eval(
|
||||
"(() => {const inp = [...document.querySelectorAll('input')]"
|
||||
f".find(i => i.placeholder === {ph});"
|
||||
"if (!inp) return false; inp.focus();"
|
||||
"const setter = Object.getOwnPropertyDescriptor("
|
||||
" window.HTMLInputElement.prototype, 'value').set;"
|
||||
f" setter.call(inp, {json.dumps(value)});"
|
||||
" inp.dispatchEvent(new Event('input', {bubbles:true}));"
|
||||
" inp.dispatchEvent(new KeyboardEvent('keydown', {key:'Enter', code:'Enter',"
|
||||
" keyCode:13, which:13, bubbles:true}));"
|
||||
" return inp.value;})()"
|
||||
)
|
||||
val_json = json.dumps(value)
|
||||
if target_ymd is None:
|
||||
m = _re.search(r"(\d{4})[-/](\d{1,2})[-/](\d{1,2})", value)
|
||||
target_ymd = (int(m.group(1)), int(m.group(2)), int(m.group(3))) if m else None
|
||||
|
||||
def _norm(s):
|
||||
if not s:
|
||||
return None
|
||||
mm = _re.search(r"(\d{4})[-/年](\d{1,2})[-/月](\d{1,2})", s)
|
||||
return (int(mm.group(1)), int(mm.group(2)), int(mm.group(3))) if mm else None
|
||||
|
||||
for attempt in range(6):
|
||||
# 1) 进入控件(focus + click 让光标处于激活状态)
|
||||
cdp.eval(
|
||||
"(() => {const inp=[...document.querySelectorAll('input')]"
|
||||
f".find(i=>i.placeholder==={ph}); if(!inp) return false; inp.focus(); inp.click(); return true;}})()"
|
||||
)
|
||||
time.sleep(0.35)
|
||||
# 2) 全选控件里既有的旧日期 → insertText 整体替换为新值(rc-picker 的 onChange 会受理);
|
||||
# execCommand 不可用时回退原生 setter + input 事件。
|
||||
wrote = cdp.eval(
|
||||
"(() => {const inp=[...document.querySelectorAll('input')]"
|
||||
f".find(i=>i.placeholder==={ph}); if(!inp) return false; inp.focus();"
|
||||
"let sel=true; try{ sel=document.execCommand('selectAll'); }catch(e){ try{inp.select();}catch(_){ sel=false; } }"
|
||||
"let done=false; try{ done=document.execCommand('insertText',false," + val_json + "); }catch(e){ done=false; }"
|
||||
"if(!done){ const s=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set;"
|
||||
f" s.call(inp,{val_json}); inp.dispatchEvent(new Event('input',{{bubbles:true}})); }}"
|
||||
"return inp.value;})()"
|
||||
)
|
||||
# 3) 回车提交(文本输入完成按回车即完成设定)
|
||||
cdp.eval(
|
||||
"(() => {const inp=[...document.querySelectorAll('input')]"
|
||||
f".find(i=>i.placeholder==={ph}); if(!inp) return false;"
|
||||
"inp.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',code:'Enter',keyCode:13,which:13,bubbles:true}));"
|
||||
"inp.dispatchEvent(new KeyboardEvent('keyup',{key:'Enter',code:'Enter',keyCode:13,which:13,bubbles:true}));"
|
||||
"return true;})()"
|
||||
)
|
||||
# 4) 写后回读校验(rc-picker 提交后 input.value 会被清空,故只校验「写后瞬间」的值)
|
||||
if target_ymd is not None and _norm(wrote) == target_ymd:
|
||||
return True
|
||||
time.sleep(0.4)
|
||||
# 全部尝试失败:告警,避免静默下成「今天」
|
||||
print(f" ⚠ set_scan_date 未能将 {placeholder} 设为目标日期 {target_ymd}(请检查 DatePicker 是否就绪)")
|
||||
return False
|
||||
|
||||
|
||||
def select_zidan(cdp):
|
||||
@@ -1095,9 +1136,11 @@ def goto_next_page(cdp):
|
||||
|
||||
|
||||
def wait_scan_form_ready(cdp, timeout=20.0):
|
||||
"""等扫描查询表单渲染完(日期输入框 + 子单 选项出现)。
|
||||
"""等扫描查询表单渲染完(日期输入框 + 子单 选项出现),并预热 DatePicker 使其完全挂载。
|
||||
|
||||
新开的 tab 里 URL 一匹配就返回,但 Ant 表单还在渲染,此时设日期/选子单会落空。
|
||||
这里除了等元素出现,再额外 settle 一段时间,并依次 focus 两个 DatePicker 打开面板再
|
||||
Esc 关闭,强制 rc-picker 完成挂载,避免随后 set_scan_date 写入的值被重渲染冲掉。
|
||||
"""
|
||||
wait_until(
|
||||
cdp,
|
||||
@@ -1106,6 +1149,26 @@ def wait_scan_form_ready(cdp, timeout=20.0):
|
||||
"扫描查询表单加载",
|
||||
timeout=timeout,
|
||||
)
|
||||
# 额外 settle:等 SPA 把表单真正挂载稳定(避免出现后又重渲染把值刷掉)
|
||||
time.sleep(1.2)
|
||||
# 预热:依次 focus 两个 DatePicker 打开面板再 Esc 关闭,强制 rc-picker 完全挂载
|
||||
for ph in ("开始日期", "结束日期"):
|
||||
cdp.eval(
|
||||
"(() => {const inp=[...document.querySelectorAll('input')]"
|
||||
f".find(i=>i.placeholder==={json.dumps(ph)}); if(!inp) return false; inp.focus(); return true;}})()"
|
||||
)
|
||||
try:
|
||||
wait_until(cdp, "!!document.querySelector('.ant-picker-panel')",
|
||||
"预热面板打开", timeout=4.0)
|
||||
except Exception:
|
||||
pass
|
||||
cdp.eval(
|
||||
"(() => {const inp=[...document.querySelectorAll('input')]"
|
||||
f".find(i=>i.placeholder==={json.dumps(ph)}); if(!inp) return false;"
|
||||
"inp.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',code:'Escape',keyCode:27,which:27,bubbles:true}));"
|
||||
"return true;})()"
|
||||
)
|
||||
time.sleep(0.3)
|
||||
|
||||
|
||||
def wait_query_settled(cdp, timeout=20.0):
|
||||
|
||||
Reference in New Issue
Block a user