Refactor discrete material cleaner to improve maintainability and testability: - Introduce dependency injection pattern for service components - Add interfaces (IPageNavigator, IMaterialExtractor, IDeletionChecker) - Extract PageNavigator service for page navigation logic - Extract MaterialExtractor service for data extraction - Extract DatabaseDeletionChecker service for deletion logic - Add MaterialInfo data model for structured data - Centralize configuration (CleanerConfig, UIConstants) - Implement separation of concerns across services layer Also includes comprehensive execution mechanism documentation. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
172 lines
6.4 KiB
Python
172 lines
6.4 KiB
Python
"""
|
||
页面导航服务实现
|
||
实现页面导航、iframe 操作等具体逻辑
|
||
"""
|
||
|
||
import re
|
||
import time
|
||
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
|
||
from interfaces.i_page_navigator import IPageNavigator
|
||
from config.ui_constants import UIConstants
|
||
|
||
|
||
class PageNavigator(IPageNavigator):
|
||
"""页面导航实现"""
|
||
|
||
def __init__(self, config: UIConstants, verbose: bool = True):
|
||
"""
|
||
初始化页面导航器
|
||
|
||
Args:
|
||
config: UI 配置
|
||
verbose: 是否打印详细日志
|
||
"""
|
||
self.config = config
|
||
self.verbose = verbose
|
||
|
||
def _print(self, *args, **kwargs):
|
||
"""打印日志(如果 verbose=True)"""
|
||
if self.verbose:
|
||
print(*args, **kwargs)
|
||
|
||
def navigate_to_main_page(self, page) -> tuple:
|
||
"""
|
||
导航到主页面 iframe
|
||
|
||
实际返回 (page, main_frame, inner_frame) 的元组
|
||
"""
|
||
# 点击打开"功能菜单"
|
||
main_frame = page.locator(self.config.selectors.FORWARD_FRAME).content_frame
|
||
main_frame.locator("i").first.click()
|
||
|
||
# 点击打开"离散生产订单维护"
|
||
with page.expect_popup() as page1_info:
|
||
main_frame.get_by_title("离散生产订单维护", exact=True).first.click()
|
||
page1 = page1_info.value
|
||
|
||
# 获取 nested iframe
|
||
main_frame = page1.locator(self.config.selectors.FORWARD_FRAME).content_frame
|
||
inner_frame_locator = main_frame.locator(self.config.selectors.MAIN_IFRAME)
|
||
inner_frame_locator.wait_for(
|
||
state="visible", timeout=self.config.timeouts.IFRAME_VISIBLE
|
||
)
|
||
inner_frame = inner_frame_locator.content_frame
|
||
|
||
return page1, main_frame, inner_frame
|
||
|
||
def navigate_to_order_page(self, main_frame, page):
|
||
"""导航到订单页面"""
|
||
# 这个方法在当前实现中与 navigate_to_main_page 类似
|
||
# 返回新的页面对象和内部 iframe
|
||
return self.navigate_to_main_page(page)
|
||
|
||
def navigate_to_material_plan_page(self, page):
|
||
"""
|
||
导航到备料计划页面
|
||
|
||
Args:
|
||
page: 订单页面对象 (page1)
|
||
|
||
Returns:
|
||
(page2, inner_frame) - 新页面和内部 iframe
|
||
"""
|
||
# 获取主 iframe
|
||
main_frame = page.locator(self.config.selectors.FORWARD_FRAME).content_frame
|
||
inner_frame = main_frame.locator(self.config.selectors.MAIN_IFRAME).content_frame
|
||
|
||
# 点击"更多"
|
||
inner_frame.locator(self.config.selectors.HOT_KEY_HEAD).get_by_text(
|
||
self.config.selectors.MORE_BUTTON
|
||
).click()
|
||
|
||
# 点击"备料计划"并等待新页面
|
||
with page.expect_popup() as page2_info:
|
||
inner_frame.get_by_text(self.config.selectors.MATERIAL_PLAN).click()
|
||
page2 = page2_info.value
|
||
|
||
# 获取 nested iframe
|
||
main_frame = page2.locator(self.config.selectors.FORWARD_FRAME).content_frame
|
||
inner_frame_locator = main_frame.locator(self.config.selectors.MAIN_IFRAME)
|
||
inner_frame_locator.wait_for(
|
||
state="visible", timeout=self.config.timeouts.IFRAME_VISIBLE
|
||
)
|
||
inner_frame = inner_frame_locator.content_frame
|
||
|
||
# 等待备料计划页面加载完成
|
||
self._wait_for_plan_page_loaded(inner_frame)
|
||
|
||
return page2, inner_frame
|
||
|
||
def setup_query_interface(self, frame):
|
||
"""设置查询界面"""
|
||
# 点击图标按钮打开查询界面
|
||
frame.locator(self.config.selectors.SEARCH_ICON).click()
|
||
frame.get_by_text(self.config.selectors.QUERY_BY_ORDER).click()
|
||
frame.get_by_role("tab", name=self.config.selectors.TAB_ALL).click()
|
||
|
||
# 填充并验证显示数量,如果失败则重试
|
||
max_retries = self.config.retry.DISPLAY_COUNT_MAX_RETRIES
|
||
expected_value = self.config.retry.EXPECTED_DISPLAY_COUNT
|
||
|
||
for attempt in range(max_retries):
|
||
frame.locator(self.config.selectors.DISPLAY_COUNT_INPUT).fill(expected_value)
|
||
frame.locator(self.config.selectors.DISPLAY_COUNT_INPUT).press("Enter")
|
||
# 检查填充是否成功
|
||
actual_value = frame.locator(self.config.selectors.DISPLAY_COUNT_INPUT).input_value()
|
||
if actual_value == expected_value:
|
||
self._print(f"文本框填充成功: {expected_value}")
|
||
break
|
||
else:
|
||
self._print(
|
||
f"第 {attempt + 1} 次填充失败,实际值: {actual_value},重试..."
|
||
)
|
||
if attempt == max_retries - 1:
|
||
self._print(
|
||
f"警告: {max_retries} 次尝试后仍未成功填充,继续执行..."
|
||
)
|
||
|
||
def wait_for_page_loaded(self, frame):
|
||
"""等待页面加载完成"""
|
||
loading_locator = frame.locator("div").filter(has_text="加载中").nth(1)
|
||
try:
|
||
loading_locator.wait_for(
|
||
state="visible", timeout=self.config.timeouts.LOADING_VISIBLE_WAIT
|
||
)
|
||
loading_locator.wait_for(
|
||
state="hidden", timeout=self.config.timeouts.LOADING_HIDDEN_WAIT
|
||
)
|
||
except PlaywrightTimeoutError:
|
||
# 加载很快完成,或者没有出现加载提示
|
||
pass
|
||
return True
|
||
|
||
def _wait_for_plan_page_loaded(self, inner_frame):
|
||
"""等待备料计划页面加载完成(内部方法)"""
|
||
self._print("等待备料计划页面加载完成...")
|
||
plan_code_locator = inner_frame.get_by_text(
|
||
re.compile(r"^离散备料计划维护:")
|
||
)
|
||
plan_code_locator.wait_for(
|
||
state="visible", timeout=self.config.timeouts.PLAN_CODE_VISIBLE
|
||
)
|
||
|
||
# 循环检查编码是否已加载
|
||
max_wait = self.config.timeouts.PLAN_CODE_MAX_WAIT
|
||
wait_interval = self.config.timeouts.PLAN_CHECK_INTERVAL
|
||
waited = 0
|
||
plan_code = None
|
||
|
||
while waited < max_wait:
|
||
plan_text = plan_code_locator.inner_text()
|
||
match = re.search(r"离散备料计划维护:(.+)", plan_text)
|
||
if match and match.group(1).strip():
|
||
plan_code = match.group(1).strip()
|
||
break
|
||
time.sleep(wait_interval)
|
||
waited += wait_interval
|
||
|
||
if plan_code:
|
||
self._print(f"备料计划页面加载完成,编码: {plan_code}")
|
||
else:
|
||
self._print(f"警告: 备料计划页面加载超时")
|