# 执行计划 — 智能定位功能执行稳定性优化

> **关联 PRD**: `_PRD_智能定位功能执行稳定性优化.md`
> **优先级**: P0
> **执行者**: Claude Code
> **预计工期**: 1 个工作日

---

## 一、执行概览

### 1.1 目标

将 UI 自动化用例执行通过率从 20% 提升到 90%+。

### 1.2 工作分解

| Phase | 任务 | 预计时间 | 优先级 |
|-------|------|----------|--------|
| 1 | 修正功能中心选择器 | 0.5h | P0 |
| 2 | 增加抽屉展开等待逻辑 | 1h | P0 |
| 3 | 优化菜单点击（滚动+重试） | 1h | P0 |
| 4 | 实现二级菜单导航 | 1.5h | P1 |
| 5 | 选择器回退机制 | 1h | P1 |
| 6 | 验证测试 | 1h | P0 |

**总计：约 6 小时**

---

## 二、Phase 1：修正功能中心选择器

### 2.1 任务描述

将功能中心图标的选择器从不准确的 `.home_nav_left` 替换为已验证的 XPath。

### 2.2 实现步骤

#### 步骤 1：更新 smart_locate_service.py

**文件**: `backend/app/services/smart_locate_service.py`

**修改点**: `_navigate_to_menu` 方法中的功能中心点击逻辑

```python
# 修改前
FUNCTION_CENTER_SELECTOR = ".home_nav_left"

# 修改后
FUNCTION_CENTER_SELECTOR = "//*[@id='Home']/div[1]/div[1]/i"
```

#### 步骤 2：更新登录模板

**文件**: `backend/app/services/login_template_service.py`

**修改点**: 默认导航模板中的选择器

```python
# 步骤 9：点击功能中心图标
{
    "order": 9,
    "name": "点击功能中心图标",
    "action": "click",
    "params": {"selector": "//*[@id='Home']/div[1]/div[1]/i"}
}
```

#### 步骤 3：更新用例创建脚本

**文件**: `backend/scripts/create_ui_cases_all_modules.py`

**修改点**: 所有用例中步骤 2 的选择器

```python
{"order": 2, "name": "点击功能中心图标", "action": "click", 
 "params": {"selector": "//*[@id='Home']/div[1]/div[1]/i"}},
```

### 2.3 验证方法

```bash
# 单独测试功能中心点击
python -c "
from backend.app.services.smart_locate_service import SmartLocateService
service = SmartLocateService()
# 测试点击功能中心
"
```

### 2.4 完成标志

- [ ] `smart_locate_service.py` 已更新
- [ ] `login_template_service.py` 已更新
- [ ] `create_ui_cases_all_modules.py` 已更新

---

## 三、Phase 2：增加抽屉展开等待逻辑

### 3.1 任务描述

点击功能中心后，增加等待逻辑确保抽屉完全展开再继续后续步骤。

### 3.2 实现步骤

#### 步骤 1：修改 smart_locate_service.py

**文件**: `backend/app/services/smart_locate_service.py`

**新增方法**: `_wait_for_drawer_open`

```python
def _wait_for_drawer_open(self) -> bool:
    """
    等待功能中心抽屉完全展开
    
    Returns:
        bool: 是否成功等待到抽屉展开
    """
    import time
    
    page = self.executor._page
    
    try:
        # 1. 等待抽屉元素出现
        page.wait_for_selector(".el-drawer", timeout=5000)
        
        # 2. 等待抽屉动画完成（检测是否有 el-drawer__open 类）
        # Element Plus 抽屉动画约 300ms
        time.sleep(0.5)
        
        # 3. 确认抽屉已展开（检查宽度或可见性）
        for _ in range(10):  # 最多等待 1 秒
            drawer = page.query_selector(".el-drawer")
            if drawer:
                # 检查抽屉是否有 open 类或宽度大于 0
                is_open = drawer.evaluate("""
                    el => {
                        const style = window.getComputedStyle(el);
                        return style.width !== '0px' && 
                               el.getAttribute('aria-hidden') !== 'true';
                    }
                """)
                if is_open:
                    break
            time.sleep(0.1)
        
        # 4. 额外稳定等待
        time.sleep(0.3)
        
        return True
        
    except Exception as e:
        self.logger.warning(f"等待抽屉展开失败: {e}")
        return False
```

#### 步骤 2：在 _navigate_to_menu 中调用

**修改 `_navigate_to_menu` 方法**:

```python
async def _navigate_to_menu(self, menu_name: str) -> bool:
    """导航到指定菜单"""
    
    page = self.executor._page
    
    # 1. 点击功能中心
    try:
        page.click(FUNCTION_CENTER_SELECTOR, timeout=10000)
    except Exception as e:
        self.logger.error(f"点击功能中心失败: {e}")
        return False
    
    # 2. 等待抽屉展开（新增）
    if not self._wait_for_drawer_open():
        self.logger.warning("抽屉展开等待失败，但继续尝试")
    
    # 3. 点击菜单
    # ... 后续逻辑
```

### 3.3 验证方法

```python
# 测试脚本
import requests

# 调用智能定位 API，验证抽屉等待逻辑
response = requests.post('http://192.168.5.60/api/element/smart-locate', json={
    'steps': [
        {'order': 1, 'name': '点击功能中心', 'action': 'click', 'params': {}},
        {'order': 2, 'name': '等待抽屉打开', 'action': 'wait', 'params': {'selector': '.el-drawer'}},
    ],
    'auto_login': True,
    'navigate_menu': '信息发布',
})
print(response.json())
```

### 3.4 完成标志

- [ ] `_wait_for_drawer_open` 方法已实现
- [ ] `_navigate_to_menu` 已调用等待逻辑
- [ ] 单元测试通过

---

## 四、Phase 3：优化菜单点击

### 4.1 任务描述

在点击抽屉内菜单前，确保元素可见、可交互，并支持滚动到可见区域。

### 4.2 实现步骤

#### 步骤 1：修改 playwright_executor.py

**文件**: `backend/app/executors/playwright_executor.py`

**修改 `_do_click` 方法**:

```python
def _do_click(self, step: dict) -> dict:
    """执行点击操作"""
    selector = step.get("params", {}).get("selector", "")
    
    try:
        # 1. 等待元素出现
        self._page.wait_for_selector(selector, timeout=10000)
        
        # 2. 滚动到可见区域
        element = self._page.query_selector(selector)
        if element:
            element.scroll_into_view_if_needed()
            self._page.wait_for_timeout(300)  # 滚动后稳定
        
        # 3. 等待元素可点击状态
        self._page.wait_for_selector(selector, state="visible", timeout=5000)
        
        # 4. 执行点击（带重试）
        for retry in range(3):
            try:
                self._page.click(selector, timeout=5000)
                break
            except Exception as e:
                if retry < 2:
                    self._page.wait_for_timeout(1000)
                    # 强制滚动后重试
                    self._page.eval_on_selector(selector, "el => el.scrollIntoView({block: 'center'})")
                else:
                    raise
        
        return {"success": True, "message": f"点击成功: {selector}"}
        
    except Exception as e:
        return {"success": False, "message": f"点击失败: {str(e)}"}
```

#### 步骤 2：修改 smart_locate_service.py 的菜单点击逻辑

**修改点击抽屉内菜单的部分**:

```python
def _click_drawer_menu(self, menu_name: str) -> bool:
    """点击抽屉内菜单"""
    page = self.executor._page
    
    # 构建选择器
    menu_selector = f".el-drawer >> text='{menu_name}'"
    
    try:
        # 1. 等待菜单元素出现
        page.wait_for_selector(menu_selector, timeout=10000)
        
        # 2. 滚动到可见区域
        page.eval_on_selector(menu_selector, 
            "el => el.scrollIntoView({block: 'center', behavior: 'smooth'})")
        page.wait_for_timeout(500)
        
        # 3. 点击（带重试）
        for retry in range(3):
            try:
                page.click(menu_selector, timeout=5000)
                self.logger.info(f"点击菜单成功: {menu_name}")
                return True
            except Exception as e:
                self.logger.warning(f"点击菜单失败(重试 {retry+1}/3): {e}")
                page.wait_for_timeout(1000)
        
        return False
        
    except Exception as e:
        self.logger.error(f"点击抽屉菜单失败: {menu_name}, {e}")
        return False
```

### 4.3 验证方法

执行通知公告、会议管理等菜单的点击测试。

### 4.4 完成标志

- [ ] `playwright_executor.py` 已增加滚动和重试逻辑
- [ ] `smart_locate_service.py` 已优化菜单点击
- [ ] 通知公告、会议管理用例点击成功

---

## 五、Phase 4：实现二级菜单导航

### 5.1 任务描述

支持需要先展开分类再点击子菜单的二级菜单导航。

### 5.2 实现步骤

#### 步骤 1：创建菜单映射配置

**新建文件**: `backend/app/utils/menu_mapping.py`

```python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
菜单层级映射配置

用于智能定位时正确导航二级菜单。
"""

# 二级菜单层级关系
MENU_HIERARCHY = {
    # 分类 → 子菜单列表
    "集控控制": [
        "壁纸推送", "脚本命令", "应用管理", "屏幕管理",
        "软件卸载", "设备网络", "门口屏发布", "播放器发布"
    ],
    "资产管理": ["资产信息", "资产设备"],
    "会议运维": ["运维设备", "运维巡检"],
    "会议管理": [],  # 有子菜单但也可直接访问
    "会务管理": [],
    "运维管理": [],
    # 以下为直接菜单（无子菜单）
    "_DIRECT_MENUS_": [
        "信息发布", "数据统计", "信息管理", "预定2.0",
        "数据分析", "管理看板", "通知公告", "会议室管理",
        "系统设置", "维护工单", "其他分类"
    ]
}

def get_menu_parent(menu_name: str) -> str | None:
    """
    获取菜单的父分类
    
    Args:
        menu_name: 菜单名称
        
    Returns:
        父分类名称，如果是直接菜单返回 None
    """
    # 检查是否是直接菜单
    if menu_name in MENU_HIERARCHY.get("_DIRECT_MENUS_", []):
        return None
    
    # 查找父分类
    for parent, children in MENU_HIERARCHY.items():
        if parent == "_DIRECT_MENUS_":
            continue
        if menu_name in children:
            return parent
    
    # 默认认为是直接菜单
    return None

def is_direct_menu(menu_name: str) -> bool:
    """判断是否为直接菜单"""
    return get_menu_parent(menu_name) is None
```

#### 步骤 2：修改 smart_locate_service.py

**修改 `_navigate_to_menu` 方法**:

```python
from app.utils.menu_mapping import get_menu_parent, is_direct_menu

async def _navigate_to_menu(self, menu_name: str) -> bool:
    """导航到指定菜单（支持二级菜单）"""
    
    page = self.executor._page
    
    try:
        # 1. 点击功能中心
        page.click(FUNCTION_CENTER_SELECTOR, timeout=10000)
        
        # 2. 等待抽屉展开
        self._wait_for_drawer_open()
        
        # 3. 检查是否为二级菜单
        parent_category = get_menu_parent(menu_name)
        
        if parent_category:
            # 二级菜单：先点击分类展开
            self.logger.info(f"二级菜单导航: {parent_category} → {menu_name}")
            
            # 点击父分类
            if not self._click_drawer_menu(parent_category):
                self.logger.warning(f"点击分类失败: {parent_category}")
            
            # 等待子菜单展开
            page.wait_for_timeout(500)
            
            # 点击子菜单
            if not self._click_drawer_menu(menu_name):
                self.logger.error(f"点击子菜单失败: {menu_name}")
                return False
        else:
            # 一级菜单：直接点击
            if not self._click_drawer_menu(menu_name):
                self.logger.error(f"点击菜单失败: {menu_name}")
                return False
        
        # 4. 等待抽屉关闭（可选）
        page.wait_for_timeout(500)
        
        return True
        
    except Exception as e:
        self.logger.error(f"导航到菜单失败: {menu_name}, {e}")
        return False
```

### 5.3 验证方法

```bash
# 测试二级菜单导航
python -c "
import requests
response = requests.post('http://192.168.5.60/api/element/smart-locate', json={
    'steps': [{'order': 1, 'name': '点击壁纸推送', 'action': 'click', 'params': {}}],
    'auto_login': True,
    'navigate_menu': '壁纸推送',  # 二级菜单
})
print(response.json())
"
```

### 5.4 完成标志

- [ ] `menu_mapping.py` 已创建
- [ ] `_navigate_to_menu` 支持二级导航
- [ ] 壁纸推送、资产信息等二级菜单用例通过

---

## 六、Phase 5：选择器回退机制

### 6.1 任务描述

主选择器失败时，自动尝试备选选择器。

### 6.2 实现步骤

#### 步骤 1：定义菜单选择器候选列表

**修改**: `backend/app/services/smart_locate_service.py`

```python
# 菜单选择器候选列表（按优先级排序）
MENU_SELECTOR_CANDIDATES = [
    lambda name: f".el-drawer >> text='{name}'",           # 优先：直接文本匹配
    lambda name: f".el-drawer a:has-text('{name}')",       # 备选：链接元素
    lambda name: f".el-drawer button:has-text('{name}')",  # 备选：按钮元素
    lambda name: f".el-drawer [role='menuitem']:has-text('{name}')",  # 备选：菜单项
    lambda name: f"//div[contains(@class,'el-drawer')]//*[contains(text(),'{name}')]",  # XPath
]
```

#### 步骤 2：修改点击逻辑

```python
def _click_drawer_menu(self, menu_name: str) -> bool:
    """点击抽屉内菜单（带回退机制）"""
    page = self.executor._page
    
    # 尝试每个候选选择器
    for i, selector_fn in enumerate(MENU_SELECTOR_CANDIDATES):
        selector = selector_fn(menu_name)
        
        try:
            # 检查元素是否存在
            element = page.query_selector(selector)
            if not element:
                continue
            
            # 滚动到可见区域
            element.scroll_into_view_if_needed()
            page.wait_for_timeout(300)
            
            # 尝试点击
            element.click(timeout=5000)
            
            self.logger.info(f"点击菜单成功(选择器{i+1}): {menu_name}")
            return True
            
        except Exception as e:
            self.logger.warning(f"选择器{i+1}点击失败: {selector}, {e}")
            continue
    
    # 所有选择器都失败
    self.logger.error(f"所有选择器都无法点击: {menu_name}")
    return False
```

### 6.3 验证方法

测试各种菜单的点击，观察是否使用了备选选择器。

### 6.4 完成标志

- [ ] 选择器候选列表已定义
- [ ] 点击逻辑支持回退
- [ ] 日志记录使用了哪个选择器

---

## 七、Phase 6：验证测试

### 7.1 测试范围

执行以下 5 个验证用例：

| 用例 | 预期结果 | 验证点 |
|------|----------|--------|
| 集控控制-壁纸推送功能验证 | ✅ 通过 | 二级菜单导航 |
| 信息发布-页面访问验证 | ✅ 通过 | 基准对照 |
| 资产管理-资产信息页面验证 | ✅ 通过 | 二级菜单导航 |
| 通知公告-页面访问验证 | ✅ 通过 | 一级菜单点击 |
| 会议管理-会议列表查看 | ✅ 通过 | 一级菜单点击 |

### 7.2 测试步骤

```bash
# 1. 部署代码到服务器
cd backend/scripts
python deploy_smart_locate.py

# 2. 重启服务
ssh ubains@192.168.5.60 "docker restart plat-auto-test-app"

# 3. 执行验证用例
python -c "
import requests, time

# 创建执行
response = requests.post('http://192.168.5.60/api/executions', json={
    'name': '智能定位优化验证',
    'case_ids': [
        'case_xxx',  # 替换为实际用例ID
    ]
})
exec_id = response.json()['id']

# 触发执行
requests.post(f'http://192.168.5.60/api/executions/{exec_id}/run')

# 等待完成
time.sleep(60)

# 查询结果
response = requests.get(f'http://192.168.5.60/api/executions/{exec_id}/results')
print(response.json())
"
```

### 7.3 验收标准

| 指标 | 目标 | 实际 | 状态 |
|------|------|------|------|
| 执行通过率 | ≥ 90% | - | ⬜ |
| 抽屉等待成功率 | 100% | - | ⬜ |
| 菜单点击成功率 | 100% | - | ⬜ |

---

## 八、风险与应对

### 8.1 风险列表

| 风险 | 影响 | 应对措施 |
|------|------|----------|
| 二级菜单映射不完整 | 部分菜单无法导航 | 补充完整的菜单映射表 |
| 抽屉动画时间变化 | 等待时间不够 | 使用动态检测代替固定等待 |
| 选择器失效 | 用例执行失败 | 维护选择器候选列表 |

### 8.2 回滚方案

如果优化后出现更严重问题：

```bash
# 回滚到优化前版本
git revert <commit_hash>
docker restart plat-auto-test-app
```

---

## 九、文档更新

### 9.1 需要更新的文档

| 文档 | 更新内容 |
|------|----------|
| `HANDOFF_UI自动化.md` | 记录优化内容和验证结果 |
| `_经验总结_脚本录制与元素定位.md` | 补充选择器最佳实践 |

### 9.2 提交信息模板

```
feat(smart-locate): 智能定位执行稳定性优化

- 修正功能中心选择器为已验证的 XPath
- 增加抽屉展开等待逻辑
- 优化菜单点击（滚动+重试）
- 实现二级菜单导航支持
- 增加选择器回退机制

验证结果：5/5 用例通过 (100%)

Co-Authored-By: Claude <noreply@anthropic.com>
```

---

*本文档由 Claude Code 于 2026-08-05 创建。*