Angular 页面自动化进阶:Select2 组件与表单提交

第4节批改作业时用了 angular.element().scope() 注入,但只讲了流程没讲原理。这一节深入 Angular 1.x 的数据绑定机制,解决自动化中最头疼的问题:DOM 操作无效,必须走 scope


为什么直接操作 DOM 没用

普通页面,你 document.querySelector('input').value = '张三' 就完事了。但 Angular 1.x 页面不行:

lua 复制代码
你的操作:  input.value = "张三"
              ↓
DOM 确实变了:  input 显示 "张三"
              ↓
但 Angular 的 scope 里还是旧值
              ↓
用户点"提交"时,Angular 读 scope → 提交旧值
              ↓
你的修改被丢弃

Angular 1.x 的双向绑定是 scope → DOM 方向,不是 DOM → scope。你改了 DOM,Angular 不知道。

解决方案:三条路

方案 做法 适用场景
scope 注入 angular.element(el).scope() 直接改 scope Angular 1.x 页面(本文重点)
事件触发 改完 DOM 后 dispatchEvent(new Event('input')) React / Vue 等现代框架
Playwright 原生 page.fill() / page.click() 普通页面、非框架组件

OUCHN 平台是 Angular 1.x,所以全文围绕 scope 注入展开。


Scope 注入通用模式

基本三步

javascript 复制代码
// 1. 找到带 ng-controller 的元素
var el = document.querySelector("[ng-controller='CourseListController']");

// 2. 取出 scope 对象
var scope = angular.element(el).scope();

// 3. 修改数据 + 触发更新
scope.conditions.role = ['assistant_instructor'];
scope.$apply();  // 关键:通知 Angular "数据变了,重新渲染"

为什么需要 $apply()

Angular 的脏检查机制:只在"Angular 知道的事件"(ng-click、 http回调等)触发时自动检查。你从外部改scope,Angular不知道,必须手动调'http 回调等)触发时自动检查。你从外部改 scope,Angular 不知道,必须手动调 ` http回调等)触发时自动检查。你从外部改scope,Angular不知道,必须手动调'apply()`。

javascript 复制代码
// ❌ 不调 $apply(),Angular 不知道数据变了
scope.conditions.keyword = '数学';
// 页面不会更新,后续操作还是旧数据

// ✅ 调了 $apply(),Angular 重新渲染
scope.conditions.keyword = '数学';
scope.$apply();
// 页面立即更新

踩坑:$apply() 在 diggest 循环进行中调用会报错。如果不确定,用 $applyAsync() 替代,它会在下一个 tick 执行。

在 Playwright 中执行

python 复制代码
result = await page.evaluate("""
    async () => {
        var el = document.querySelector(
            "[ng-controller='CourseListController']"
        );
        var scope = angular.element(el).scope();

        scope.conditions.role = ['assistant_instructor'];
        scope.search();  // 触发后端筛选请求
        scope.$apply();

        // 等待 Angular 处理完
        await new Promise(r => setTimeout(r, 2000));

        // 读取筛选后的课程列表
        return JSON.stringify(scope.courses || []);
    }
""")
courses = json.loads(result)

Select2 / MultiSelect 组件处理

这是第4节"坑1"的完整版。OUCHN 课程列表的角色选择器是 jQuery UI MultiSelect,直接设 DOM value 无效。

错误尝试

python 复制代码
# ❌ Playwright 原生 select_option 无效
await page.select_option(".course-role-select", "assistant_instructor")
# MultiSelect 不是原生 <select>,这个方法不生效

# ❌ 直接改 DOM value 也无效
await page.evaluate("""
    document.querySelector('.course-role').value = 'assistant_instructor';
""")
# DOM 变了但 Angular scope 没变

正确做法:走 scope

python 复制代码
result = await page.evaluate("""
    async () => {
        // 1. 找到课程列表控制器
        var el = document.querySelector(
            '[ng-controller="CourseListController"]'
        );
        var scope = angular.element(el).scope();

        // 2. 设角色条件(注意是数组,MultiSelect 支持多选)
        scope.conditions.role = ['assistant_instructor'];

        // 3. 触发搜索(后端会根据条件筛选)
        scope.search();

        // 4. 通知 Angular 更新视图
        scope.$apply();

        // 5. 等待后端返回
        await new Promise(r => setTimeout(r, 3000));

        // 6. 读取结果
        return JSON.stringify({
            total: scope.courses.length,
            sample: scope.courses.slice(0, 3).map(c => c.name)
        });
    }
""")
data = json.loads(result)
print(f"筛选后课程数: {data['total']}")

效果对比

方式 课程数 原因
不筛选(默认全部角色) 83 包含学生、教师、管理员的所有课程
DOM value 设置 83 Angular 没收到,条件没变
scope 注入 + search() 20 只剩辅导老师负责的课程

从 83 门到 20 门,后续每门课的处理时间 × 4 倍速度提升。


setViewingData 与异步弹窗

第4节批改作业的核心操作:调用 scope.setViewingData(idx) 触发评分弹窗。

为什么不能直接点按钮

评分弹窗不是页面上的固定元素,而是 Angular 动态生成的。它需要 setViewingData() 先设置当前查看的学生数据,Angular 才会渲染弹窗。

javascript 复制代码
async (params) => {
    // 1. 取学生列表 scope
    var psc = document.querySelector(
        "[ng-controller='PagedSubmissionListController']"
    );
    var s = angular.element(psc).scope();

    // 2. setViewingData 是异步的!必须 await
    await s.setViewingData(params.idx);

    // 3. 弹窗可能默认 display:none,暴力显示
    var modal = document.getElementById('give-score');
    modal.style.display = 'block';
    modal.style.visibility = 'visible';
    modal.style.opacity = '1';

    // 4. 等弹窗内 Angular 控制器初始化
    await new Promise(r => setTimeout(r, 1500));

    // 5. 取评分 scope,填入分数和评语
    var g = angular.element(
        document.querySelector("[ng-controller='GiveScoreController']")
    ).scope();
    g.dataBindTo.score = params.score;
    g.dataBindTo.reviewer_comment = params.comment;
    g.$apply();

    // 6. 点击保存
    var btns = document.querySelectorAll('#give-score button');
    for (var i = 0; i < btns.length; i++) {
        if (btns[i].textContent.indexOf('保存') >= 0) {
            btns[i].click();
            break;
        }
    }

    // 7. 等待保存完成,隐藏弹窗
    await new Promise(r => setTimeout(r, 3000));
    modal.style.display = 'none';
    return 'ok';
}

关键细节

步骤 为什么这么做
await s.setViewingData() 它是异步的,不 await 会等弹窗渲染完
强制 display:block 某些情况下 Angular 设了 display:none
等 1500ms GiveScoreController 需要时间初始化
g.$apply() 改了 score 和 comment,必须通知 Angular
模糊匹配"保存" 按钮文本可能是"保存""提交""确认"

表单提交与按钮匹配

问题:按钮文本不固定

python 复制代码
# ❌ 硬编码文本,容易失败
await page.click('button:has-text("保存")')
# 如果按钮文本是"提交评分"或"确认",就点不到

# ✅ 模糊匹配多个关键词
await page.evaluate("""
    () => {
        var btns = document.querySelectorAll('button');
        var keywords = ['保存', '提交', '确认', '确定'];
        for (var btn of btns) {
            var text = btn.textContent.trim();
            for (var kw of keywords) {
                if (text.indexOf(kw) >= 0 && btn.offsetParent !== null) {
                    btn.click();
                    return text;
                }
            }
        }
        return null;
    }
""")

只点可见按钮

javascript 复制代码
// 有时页面上有多个匹配按钮(比如弹窗和背景页各一个)
// 只点可见的那个
var btns = document.querySelectorAll('button');
for (var btn of btns) {
    if (btn.offsetParent === null) continue;  // 不可见,跳过
    if (btn.textContent.indexOf('保存') >= 0) {
        btn.click();
        break;
    }
}

offsetParent === null 是检测元素是否可见的可靠方法。display:nonevisibility:hiddenopacity:0 的元素 offsetParent 都是 null(opacity:0 除外,需单独检测)。


分页数据提取

Angular 分页通常不会改 URL,而是通过 scope 变量控制。直接操作 scope 翻页:

python 复制代码
all_items = []
page_num = 1

while True:
    result = await page.evaluate("""
        async (pageNum) => {
            var scope = angular.element(
                document.querySelector('[ng-controller]')
            ).scope();

            // 切换到指定页
            scope.currentPage = pageNum;
            scope.$apply();

            // 等待数据加载
            await new Promise(r => setTimeout(r, 2000));

            var items = scope.list || [];
            var totalPages = scope.totalPages || 1;
            return JSON.stringify({
                items: items,
                totalPages: totalPages,
                currentPage: scope.currentPage
            });
        }
    """, page_num)

    data = json.loads(result)
    all_items.extend(data['items'])

    if page_num >= data['totalPages']:
        break
    page_num += 1

print(f"共提取 {len(all_items)} 条数据,{page_num} 页")

五个真实踩坑

坑 1 --- scope 是 undefined

现象angular.element(el).scope() 返回 undefined。

原因 :元素没有 ng-controller 属性,或者 Angular 还没初始化。

修复:等待 Angular 加载完成再取 scope:

python 复制代码
# 等待 Angular 初始化
await page.wait_for_function(
    "() => window.angular && angular.element(document).scope()",
    timeout=30000
)
# OUCHN 的 Angular 初始化特别慢,需要 10-15 秒
await asyncio.sleep(10)

坑 2 --- $apply() 报错 "digest already in progress"

现象 :调用 $apply() 抛异常。

原因 :Angular 正在进行脏检查循环,不能嵌套 $apply()

修复 :用 $applyAsync()setTimeout 延迟执行:

javascript 复制代码
// 安全写法
if (scope.$$phase) {
    // 正在 digest 中,用 setTimeout 延迟
    setTimeout(function() {
        scope.conditions.role = ['assistant_instructor'];
        scope.$apply();
    }, 0);
} else {
    scope.conditions.role = ['assistant_instructor'];
    scope.$apply();
}

坑 3 --- setViewingData 不返回 Promise

现象await s.setViewingData(idx) 报错 "not a function" 或 "cannot await"。

原因 :不同版本的 Angular 控制器,setViewingData 可能不是异步的。

修复:兼容写法:

javascript 复制代码
try {
    await s.setViewingData(params.idx);
} catch(e) {
    s.setViewingData(params.idx);  // 同步调用
}
await new Promise(r => setTimeout(r, 2000));  // 手动等待

坑 4 --- 弹窗里的 scope 取不到

现象GiveScoreController 的 scope 取出来是 null。

原因:弹窗还没渲染完就取 scope。

修复:等弹窗内 Angular 控制器初始化:

javascript 复制代码
// 等 modal 出现
await new Promise(r => setTimeout(r, 1500));

// 循环等待 scope 可用
for (var i = 0; i < 10; i++) {
    var g = angular.element(
        document.querySelector("[ng-controller='GiveScoreController']")
    ).scope();
    if (g && g.dataBindTo) break;
    await new Promise(r => setTimeout(r, 500));
}

坑 5 --- 保存按钮点了但没提交

现象:按钮 click 了,但分数没保存成功。

原因$apply() 没调或调得太晚,Angular 还没把新数据同步到 DOM。

修复 :确保 $apply() 在 click 之前执行,且留足时间:

javascript 复制代码
g.dataBindTo.score = params.score;
g.dataBindTo.reviewer_comment = params.comment;
g.$apply();
await new Promise(r => setTimeout(r, 500));  // 等 Angular 渲染
// 然后才点保存
btn.click();

小结

学会的 要点
Angular 1.x 绑定 scope → DOM 单向,改 DOM 不影响 scope
scope 注入三步 angular.element(el).scope() → 改数据 → $apply()
Select2 处理 走 scope 设 conditions + 调 search() + $apply()
异步弹窗 setViewingData() 是异步的,需 await + 等渲染
按钮匹配 模糊匹配关键词 + 只点可见按钮
分页提取 操作 scope.currentPage + $apply() + 等数据加载
踩坑预防 等 Angular 初始化、$apply 嵌套检测、弹窗 scope 轮询

下一篇预告

第 7 节:定时任务与自动化调度------RRULE 调度规则、recurring/once 两种模式、有效期控制、多任务编排。让脚本每天自己跑起来。


📺 系列文章:

本文首发于稀土掘金。欢迎关注后续连载。

相关推荐
exarunner1 小时前
KV Cache 驱逐要不要上 Radix Tree?以 Mooncake模拟一下
人工智能
橘子星1 小时前
🤖 从零造一个AI程序员:30行核心代码拆解Cursor背后的Agent原理
人工智能
大鱼>1 小时前
AI+货物追踪:智能快递柜追踪系统
人工智能·深度学习·算法·机器学习
程序喵大人1 小时前
【AI专栏】图解Transformer - 第04章:LLM生成
人工智能·深度学习·llm·transformer
1234567890@world1 小时前
知识管理 | 数字化 | APQC
大数据·数据库·人工智能
千维百策6661 小时前
AI 编码智能体平台如何提升研发效能:从 CI 修复到工程工作流自动化
人工智能·ci/cd·自动化
以和为贵1 小时前
🔥前端也能搞懂流式输出:从 SSE 到打字机效果
前端·人工智能·架构
sunywz2 小时前
【AI RAG知识库】06.【导入】【Web服务集成】
人工智能
zeng_jun_yv2 小时前
测试 Agent 评测体系详细方案
人工智能