"""管理端 Page Object — 运费核算 + 运单审核 + 垫资打款 完整流程。 基于 resources/selectors/正常流程-web端.md 中的元素定位。 使用直接 hash URL 导航(比菜单点击更可靠),按钮选择器带 fallback 兜底。 URL 路由: 运费核算: #/tms/Carrier/freight 运单审核: #/tms/Carrier/OrderDetailAudit 垫资-财务打款: #/tms/Carrier/dzFinancialapproval 垫资-货主打款: #/tms/Carrier/dzApplicationForPayment """ from playwright.sync_api import Page from tests.web.admin.pages.base_page import BasePage class FreightAccountingPage(BasePage): """管理端 - 运费核算 / 运单审核 / 垫资打款。""" def __init__(self, page: Page, admin_base_url: str = ""): super().__init__(page) self.admin_base_url = admin_base_url # ════════════════════════════════════════════════════════════ # URL 导航 # ════════════════════════════════════════════════════════════ def _go(self, hash_route: str): """直接 hash URL 跳转,比菜单点击稳定。""" url = f"{self.admin_base_url}/#/{hash_route.lstrip('#').lstrip('/')}" self.page.goto(url, wait_until="domcontentloaded") self.page.wait_for_timeout(2000) # ════════════════════════════════════════════════════════════ # 通用操作 # ════════════════════════════════════════════════════════════ def _fill_order_no(self, order_no: str): """搜索栏输入运输单号(多个同名 input 时取第一个)。""" input_el = self.page.locator('input[placeholder="请输入运输单号"]').first self.wait_visible(input_el) input_el.clear() input_el.fill(order_no) def _click_search(self): """点击查询按钮。""" btn = self.page.locator("button:has-text('查询')").first self.wait_visible(btn) btn.click() self.page.wait_for_timeout(1000) def _table_has_data(self) -> bool: """检查查询结果是否有数据。""" try: self.page.wait_for_selector("table tbody tr", timeout=3_000) return True except Exception: return False def _click_table_action(self, button_index: int): """点击表格固定列操作按钮(纯图标 按钮,selector 文档路径 + fallback)。 vxe-table 操作列按钮结构: td > div > button:nth-child(N) > span > i """ # 精确路径(selector 文档原始定位) sel = ( ".vxe-table--fixed-right-wrapper.scrolling--middle " ".vxe-table--body-wrapper.fixed-right--wrapper " "table > tbody > tr:nth-child(1) > td > div " f"> button:nth-child({button_index}) > span > i" ) # 宽松路径(当 DOM 结构略有变化时兜底) sel_fallback = ( ".vxe-table--fixed-right-wrapper " "table > tbody > tr:nth-child(1) " f"> td:last-child > div > button:nth-child({button_index})" ) for attempt in range(3): try: self.page.wait_for_selector("table tbody tr", timeout=5_000) self.page.wait_for_timeout(300) btn = self.page.locator(sel).first if btn.count() == 0: btn = self.page.locator(sel_fallback).first self.wait_visible(btn, timeout_ms=5_000) btn.click() self.page.wait_for_timeout(800) return except Exception: if attempt == 2: raise self.page.wait_for_timeout(1_000) def _modal(self): """返回当前可见弹窗内容容器(.first 避免 strict mode)。""" return self.page.locator( ".vxe-modal--wrapper.is--visible .vxe-modal--body .vxe-modal--content" ).first def _scroll_modal(self): """滚动弹窗到底部。""" modal = self._modal() self.wait_visible(modal) modal.evaluate("el => el.scrollTop = el.scrollHeight") self.page.wait_for_timeout(500) def _retry_click_modal_btn(self, locator, timeout_ms: int = 5_000, retries: int = 3, wait_after: int = 1500): """弹窗内按钮带重试点击(每次重试重新获取元素,防 DOM 置换)。""" for attempt in range(retries): el = locator() if callable(locator) else locator try: self.wait_visible(el, timeout_ms=timeout_ms) el.click() self.page.wait_for_timeout(wait_after) return except Exception: if attempt == retries - 1: raise self.page.wait_for_timeout(1_000) # ════════════════════════════════════════════════════════════ # 运费核算 # ════════════════════════════════════════════════════════════ def do_freight_accounting(self, order_no: str, retry_sql=None): """运费核算:导航→搜索→(无数据时 SQL 重试)→编辑→核算。 Args: order_no: 运输单号 retry_sql: 可调用对象,首次查询无数据时调用 """ self._go("tms/Carrier/freight") self._fill_order_no(order_no) self._click_search() # 无数据 → 执行 SQL 重试 if not self._table_has_data(): if retry_sql: try: retry_sql() except Exception as e: raise RuntimeError(f"运费核算 SQL 重试失败!单号: {order_no}, 错误: {e}") self._fill_order_no(order_no) self._click_search() if not self._table_has_data(): raise RuntimeError(f"运费核算查询无数据!单号: {order_no}") # 点击编辑 (button_index=4, 来自 selector 文档 Step 3) self._click_table_action(button_index=4) # 核算按钮 (selector 文档 Step 4) btn = self._modal().locator( "form > div:nth-child(4) button:nth-child(4) span" ).first self._retry_click_modal_btn(btn, wait_after=1500) self.screenshot("freight_accounting_done") # ════════════════════════════════════════════════════════════ # 运单审核 # ════════════════════════════════════════════════════════════ def do_waybill_audit(self, order_no: str): """运单审核:导航→搜索→审核→滚动弹窗→审核通过。""" self._go("tms/Carrier/OrderDetailAudit") self._fill_order_no(order_no) self._click_search() # 点击审核 (button_index=2, selector 文档 Step 10) self._click_table_action(button_index=2) # 滚动弹窗 (selector 文档 Step 11) self._scroll_modal() # 审核通过 (selector 文档 Step 12) btn = self._modal().locator( "._btns > button.el-button.el-button--primary.el-button--medium > span" ).first self._retry_click_modal_btn(btn, wait_after=1500) self.screenshot("waybill_audit_done") # ════════════════════════════════════════════════════════════ # 垫资-财务打款 # ════════════════════════════════════════════════════════════ def do_finance_payment(self, order_no: str, verify_code: str = "1234"): """垫资-财务打款:导航→搜索→在线支付→滚动→勾选光大/手机→验证码→确认。""" self._go("tms/Carrier/dzFinancialapproval") self._fill_order_no(order_no) self._click_search() # 在线支付 (button_index=2, selector 文档 Step 18) self._click_table_action(button_index=2) self._scroll_modal() # 勾选光大银行 (selector 文档 Step 20) try: radio = self._modal().locator( "form > div:nth-child(9) > div > div:nth-child(2) " "> label > span.el-radio__input > span" ).first self._retry_click_modal_btn(radio, timeout_ms=3_000, wait_after=300) except Exception: pass # 勾选手机号 (selector 文档 Step 21) try: radio = self.page.locator( "[id^='el-collapse-content-'] > div > div:nth-child(3) " "> label > span.el-radio__input > span" ).first self._retry_click_modal_btn(radio, timeout_ms=3_000, wait_after=300) except Exception: pass # 获取验证码 (selector 文档 Step 22) try: btn = self._modal().locator( "form > div.el-row > div.el-col.el-col-4 button span" ).first self._retry_click_modal_btn(btn, timeout_ms=3_000, wait_after=500) except Exception: pass # 输入验证码 (selector 文档 Step 23) try: inp = self._modal().locator( "form > div.el-row > div.el-col.el-col-20 input" ).first self.wait_visible(inp) inp.clear() inp.fill(verify_code) except Exception: pass # 确认打款 (selector 文档 Step 24) btn = self._modal().locator( "form > div:nth-child(12) > div > button span" ).first self._retry_click_modal_btn(btn, wait_after=1500) self.screenshot("finance_payment_done") # ════════════════════════════════════════════════════════════ # 垫资-货主打款 # ════════════════════════════════════════════════════════════ def do_shipper_payment(self, order_no: str): """垫资-货主打款:导航→搜索→打款→勾选光大→确认打款→确认弹窗。""" self._go("tms/Carrier/dzApplicationForPayment") self._fill_order_no(order_no) self._click_search() # 打款 (button_index=2, selector 文档 Step 30) self._click_table_action(button_index=2) # 勾选光大 (selector 文档 Step 31) try: radio = self._modal().locator( "form > div > div:nth-child(3) > div > div > div:nth-child(3) " "> label > span.el-radio__label" ).first self._retry_click_modal_btn(radio, timeout_ms=3_000, wait_after=300) except Exception: pass # 确认打款 (selector 文档 Step 31-2) btn = self._modal().locator("> div > div > button > span").first self._retry_click_modal_btn(btn, wait_after=1000) # message-box 确认 (selector 文档 Step 32) try: msg = self.page.locator( "body > div.el-message-box__wrapper .el-message-box__btns " "> button.el-button.el-button--default.el-button--small.el-button--primary > span" ) self._retry_click_modal_btn(msg, timeout_ms=5_000, wait_after=1500) except Exception: pass self.screenshot("shipper_payment_done")