产品/客户搜索提前到启动前:输入框防抖搜索+下拉选择+缓存,登录时初始化搜索API,batch结束自动清空
This commit is contained in:
@@ -128,8 +128,26 @@ class App:
|
||||
self.fv_sender_addr = StringVar()
|
||||
self.fv_force_sender_addr = BooleanVar()
|
||||
|
||||
# 搜索基础设施(懒加载)
|
||||
self._search_bridge = None
|
||||
self._search_sess = None
|
||||
self._search_api = None
|
||||
self._loading_config = False
|
||||
self._search_debounce = None
|
||||
|
||||
# 产品/客户预缓存(batch 期间复用)
|
||||
self._product_cached_choice = None
|
||||
self._product_cached_detail = None
|
||||
self._customer_cached_choice = None
|
||||
self._customer_cached_detail = None
|
||||
self._sender_realname_cached = None
|
||||
|
||||
self._build_ui()
|
||||
|
||||
# 监听固定值输入框变化,触发产品/客户搜索
|
||||
self.fv_product.trace_add("write", self._on_product_change)
|
||||
self.fv_customer.trace_add("write", self._on_customer_change)
|
||||
|
||||
# 关闭窗口时清理
|
||||
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||
|
||||
@@ -268,6 +286,7 @@ class App:
|
||||
def _load_config(self):
|
||||
"""加载配置文件。首次运行时从 _MEIPASS 复制到 exe 目录。"""
|
||||
try:
|
||||
self._loading_config = True
|
||||
config_path = _get_config_path()
|
||||
# 打包模式首次运行:从 _MEIPASS 复制 config.json 到可写目录
|
||||
if getattr(sys, 'frozen', False) and not os.path.exists(config_path):
|
||||
@@ -292,6 +311,7 @@ class App:
|
||||
self.fv_force_sender_name.set(fv.get("寄件人", {}).get("force", False))
|
||||
self.fv_sender_addr.set(fv.get("寄件地址", {}).get("value", ""))
|
||||
self.fv_force_sender_addr.set(fv.get("寄件地址", {}).get("force", False))
|
||||
self._loading_config = False
|
||||
|
||||
# 检查已保存的登录会话
|
||||
self._check_session_status()
|
||||
@@ -349,6 +369,189 @@ class App:
|
||||
)
|
||||
self.result_tree.insert("", "end", values=values)
|
||||
|
||||
# ─── 产品/客户搜索 ──────────────────────────────────────
|
||||
|
||||
def _init_search_infra(self):
|
||||
"""在登录后(或重新登录后)初始化搜索基础设施(token bridge + session + api)。"""
|
||||
# 销毁旧的基础设施(可能是过期 cookies 的 session)
|
||||
if self._search_bridge:
|
||||
self._search_bridge.stop()
|
||||
self._search_bridge = None
|
||||
self._search_sess = None
|
||||
self._search_api = None
|
||||
|
||||
try:
|
||||
from pickup_api import TokenBridge, PickupSession, PickupAPI
|
||||
config = load_config(_get_config_path())
|
||||
url = config.get("system", {}).get("url", "https://10.4.188.1/pickup-web/a/postwaybill/main")
|
||||
|
||||
self._search_bridge = TokenBridge()
|
||||
self._search_bridge.start()
|
||||
self._search_sess = PickupSession(url)
|
||||
session_path = _get_session_path()
|
||||
if not self._search_sess.load_session(session_path):
|
||||
self._search_bridge.stop()
|
||||
self._search_bridge = None
|
||||
self._search_sess = None
|
||||
self._log("[error] 搜索初始化失败:未找到登录会话")
|
||||
return
|
||||
resources = self._search_sess.fetch_page_resources()
|
||||
minimal_html = (
|
||||
f"<html><head><script>"
|
||||
f"$_ts=window['$_ts']||{{}};"
|
||||
f"$_ts.nsd={resources['ts_nsd']};"
|
||||
f'$_ts.cd="{resources["ts_cd"]}";'
|
||||
f"</script></head><body></body></html>"
|
||||
)
|
||||
self._search_bridge.initialize(minimal_html, resources["scripts"], resources["base_url"])
|
||||
self._search_api = PickupAPI(self._search_sess, self._search_bridge)
|
||||
try:
|
||||
self._search_api.query_ecommerce("B")
|
||||
self._search_api.query_deliver_date()
|
||||
self._search_api.check_sheet_warning()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
self._log(f"[error] 搜索初始化失败: {e}")
|
||||
if self._search_bridge:
|
||||
self._search_bridge.stop()
|
||||
self._search_bridge = None
|
||||
self._search_sess = None
|
||||
|
||||
def _on_product_change(self, *args):
|
||||
if self._loading_config:
|
||||
return
|
||||
if self._search_debounce:
|
||||
self.root.after_cancel(self._search_debounce)
|
||||
keyword = self.fv_product.get().strip()
|
||||
if not keyword or len(keyword) < 1:
|
||||
return
|
||||
self._search_debounce = self.root.after(300, lambda: self._do_product_search(keyword))
|
||||
|
||||
def _on_customer_change(self, *args):
|
||||
if self._loading_config:
|
||||
return
|
||||
if self._search_debounce:
|
||||
self.root.after_cancel(self._search_debounce)
|
||||
keyword = self.fv_customer.get().strip()
|
||||
if not keyword or len(keyword) < 1:
|
||||
return
|
||||
self._search_debounce = self.root.after(300, lambda: self._do_customer_search(keyword))
|
||||
|
||||
def _do_product_search(self, keyword: str):
|
||||
if self._search_api is None:
|
||||
return
|
||||
try:
|
||||
candidates = self._search_api.query_product(keyword)
|
||||
if not candidates:
|
||||
return
|
||||
names = [c.get("productName", "") for c in candidates]
|
||||
self._show_search_dropdown(
|
||||
"选择产品", names,
|
||||
lambda idx: self._on_product_selected(candidates[idx]),
|
||||
)
|
||||
except Exception as e:
|
||||
self._log(f"[error] 产品搜索失败: {e}")
|
||||
|
||||
def _do_customer_search(self, keyword: str):
|
||||
if self._search_api is None:
|
||||
return
|
||||
try:
|
||||
candidates = self._search_api.query_customer(keyword)
|
||||
if not candidates:
|
||||
return
|
||||
names = [
|
||||
c.get("customerFullName", c.get("customerName", ""))
|
||||
for c in candidates
|
||||
]
|
||||
self._show_search_dropdown(
|
||||
"选择客户", names,
|
||||
lambda idx: self._on_customer_selected(candidates[idx]),
|
||||
)
|
||||
except Exception as e:
|
||||
self._log(f"[error] 客户搜索失败: {e}")
|
||||
|
||||
def _show_search_dropdown(self, title: str, options: list[str], on_select):
|
||||
"""在屏幕中央弹出下拉选择窗口。"""
|
||||
popup = Toplevel(self.root)
|
||||
popup.title(title)
|
||||
popup.geometry("360x260")
|
||||
popup.transient(self.root)
|
||||
popup.grab_set()
|
||||
popup.attributes("-topmost", True)
|
||||
|
||||
Label(popup, text=title, font=("", 10, "bold"), pady=6).pack()
|
||||
|
||||
frame = Frame(popup)
|
||||
frame.pack(fill="both", expand=True, padx=10, pady=5)
|
||||
listbox = Listbox(frame, font=("", 10))
|
||||
scroll = Scrollbar(frame, orient=VERTICAL, command=listbox.yview)
|
||||
listbox.configure(yscrollcommand=scroll.set)
|
||||
scroll.pack(side=RIGHT, fill=Y)
|
||||
listbox.pack(side=LEFT, fill=BOTH, expand=True)
|
||||
for opt in options:
|
||||
listbox.insert(END, opt)
|
||||
|
||||
chosen = [False]
|
||||
|
||||
def select():
|
||||
sel = listbox.curselection()
|
||||
if sel:
|
||||
chosen[0] = True
|
||||
popup.destroy()
|
||||
on_select(sel[0])
|
||||
|
||||
def cancel():
|
||||
popup.destroy()
|
||||
|
||||
listbox.bind("<Double-1>", lambda e: select())
|
||||
listbox.bind("<Return>", lambda e: select())
|
||||
listbox.bind("<Escape>", lambda e: cancel())
|
||||
listbox.focus_set()
|
||||
if listbox.size() > 0:
|
||||
listbox.selection_set(0)
|
||||
|
||||
btn_f = Frame(popup)
|
||||
btn_f.pack(pady=8)
|
||||
Button(btn_f, text="选择", command=select, bg="#4CAF50", fg="white", width=8).pack(side=LEFT, padx=5)
|
||||
Button(btn_f, text="取消", command=cancel, width=8).pack(side=LEFT, padx=5)
|
||||
|
||||
popup.protocol("WM_DELETE_WINDOW", cancel)
|
||||
popup.wait_window()
|
||||
|
||||
def _on_product_selected(self, choice: dict):
|
||||
"""产品选中:缓存选择并获取详情。"""
|
||||
self._product_cached_choice = choice
|
||||
self.fv_product.set(choice.get("productName", ""))
|
||||
# 获取产品详情
|
||||
try:
|
||||
biz_id = choice.get("productId", "")
|
||||
biz_no = choice.get("bizProductNo", "")
|
||||
if biz_id and biz_no and self._search_api:
|
||||
self._product_cached_detail = self._search_api.select_product(biz_id, biz_no)
|
||||
except Exception as e:
|
||||
self._log(f"[error] 获取产品详情失败: {e}")
|
||||
self._product_cached_detail = {}
|
||||
|
||||
def _on_customer_selected(self, choice: dict):
|
||||
"""客户选中:缓存选择并获取详情和寄件人实名。"""
|
||||
self._customer_cached_choice = choice
|
||||
self.fv_customer.set(choice.get("customerFullName", choice.get("customerName", "")))
|
||||
# 获取客户详情
|
||||
try:
|
||||
cust_id = choice.get("customerId", "")
|
||||
if cust_id and self._search_api:
|
||||
detail = self._search_api.select_customer(str(cust_id))
|
||||
if isinstance(detail, dict) and "_text" not in detail:
|
||||
self._customer_cached_detail = detail
|
||||
base = detail.get("baseCustome", {}) if isinstance(detail.get("baseCustome"), dict) else {}
|
||||
sender_no = base.get("businessCode") or base.get("customerSubCode") or ""
|
||||
if sender_no:
|
||||
self._sender_realname_cached = self._search_api.find_sender_history(str(sender_no))
|
||||
except Exception as e:
|
||||
self._log(f"[error] 获取客户详情失败: {e}")
|
||||
self._customer_cached_detail = {}
|
||||
|
||||
# ─── 文件选择 ───────────────────────────────────────────
|
||||
|
||||
def _pick_file(self):
|
||||
@@ -365,6 +568,8 @@ class App:
|
||||
global _engine
|
||||
if _engine:
|
||||
_engine.request_stop()
|
||||
if self._search_bridge:
|
||||
self._search_bridge.stop()
|
||||
self.root.destroy()
|
||||
|
||||
# ─── 控制按钮 ───────────────────────────────────────────
|
||||
@@ -389,10 +594,10 @@ class App:
|
||||
def do_save():
|
||||
try:
|
||||
n = parse_json_and_save(json_text.get("1.0", "end-1c"))
|
||||
self._log(f"已保存 {n} 个 cookies 到 session.json")
|
||||
self.engine_status_var.set("已登录")
|
||||
self.status_label.configure(fg="green")
|
||||
self._check_session_status()
|
||||
self._init_search_infra()
|
||||
win.destroy()
|
||||
except Exception as e:
|
||||
messagebox.showerror("解析失败", f"JSON 解析失败:\n{e}", parent=win)
|
||||
@@ -489,6 +694,20 @@ class App:
|
||||
confirm_callback=self._confirm_callback,
|
||||
)
|
||||
|
||||
# 注入已缓存的搜索基础设施和产品/客户选择
|
||||
if self._search_api is not None:
|
||||
_engine.inject_infrastructure(self._search_sess, self._search_bridge, self._search_api)
|
||||
if self._product_cached_choice is not None:
|
||||
_engine._product_choice = self._product_cached_choice
|
||||
if self._product_cached_detail is not None:
|
||||
_engine._product_detail = self._product_cached_detail
|
||||
if self._customer_cached_choice is not None:
|
||||
_engine._customer_choice = self._customer_cached_choice
|
||||
if self._customer_cached_detail is not None:
|
||||
_engine._customer_detail = self._customer_cached_detail
|
||||
if self._sender_realname_cached is not None:
|
||||
_engine._sender_realname = self._sender_realname_cached
|
||||
|
||||
self._update_status("启动中...")
|
||||
try:
|
||||
await _engine.start()
|
||||
@@ -498,7 +717,7 @@ class App:
|
||||
await _engine.stop()
|
||||
return
|
||||
|
||||
# 验证 engine.start() 中已加载了 session,且包含关键 cookie
|
||||
# 验证已加载了 session,且包含关键 cookie
|
||||
if not _engine._sess or not _engine._sess._http.cookies:
|
||||
self._log_async("[提示] 未找到登录会话,请点击'登录'按钮完成登录后再开始处理")
|
||||
self._update_status("未登录")
|
||||
@@ -564,7 +783,15 @@ class App:
|
||||
_engine = None
|
||||
self._set_controls_state("stopped")
|
||||
self.status_var.set("就绪")
|
||||
self._log("处理结束")
|
||||
# 清空产品/客户输入框(batch 结束后重置)
|
||||
self.fv_product.set("")
|
||||
self.fv_customer.set("")
|
||||
# 清空缓存的选择
|
||||
self._product_cached_choice = None
|
||||
self._product_cached_detail = None
|
||||
self._customer_cached_choice = None
|
||||
self._customer_cached_detail = None
|
||||
self._sender_realname_cached = None
|
||||
|
||||
def _pause(self):
|
||||
global _engine
|
||||
|
||||
Reference in New Issue
Block a user