一层楼 · 六个房间 · 八项功能
这里的每一个房间都对应平台的一项功能。选择学员正在做的事, 他的形象就会走向相应的房间——必要时穿过走廊、推开房门。 除了休息室,这层楼没有一处是装饰。
场景曾经跑偏了。早期版本里的房间叫 focus、
group、coffee——这些是共享办公的词,描述不了你教的任何东西。
这层楼是反过来建的:从功能清单出发。
| 房间 | 服务名 | 这个空间在说什么 |
|---|
考场按你的意思处理得直白——拉开的座次和一只钟,而不是一间被软化的练习室。 它是这里唯一刻意设计成“有感觉”的空间,值得观察学员对它的反应。
第五个场景,同一套约定。服务从四项增到八项;接入方式本身自第一个草图以来没有变过。
campus.setActivity('exam'); // 走向考场并入座
campus.setActivity('diagnostic'); // 穿过走廊去反馈室
campus.on((act, seat) => ...); // 真正坐下后触发
campus.services(); // -> lexicon, models, grammar, class, exam, grading, diagnostic, idle
新增一项功能,就是新增一批带新 kind 的座位,再加一个按钮。
它落在哪个房间,是布局决定,不是代码决定。
window.campus 只存在于本文档的窗口里。嵌入你的站点后,
同源下很难拿到它,跨源则根本拿不到。所以整套 API 也通过
postMessage 提供。
<!-- 声明你的 origin,'ready' 就会主动发过来 -->
<iframe src="writing-campus.html?host=https://your.site"></iframe>
// 宿主页 -> 校园
frame.contentWindow.postMessage(
{ source:'campus', v:1, id:1, method:'setActivity', args:['exam'] }, campusOrigin);
// 校园 -> 宿主页
{ source:'campus-embed', v:1, type:'ready', services:[...] }
{ source:'campus-embed', v:1, type:'result', id, method, value }
{ source:'campus-embed', v:1, type:'error', id, method, message }
{ source:'campus-embed', v:1, type:'arrived', activity, kind } // 跨文档的 campus.on
两条规则,防止任何页面都能驱动别人的形象。方法名在一张固定表里查,
而不是到 API 对象上取——动态取值会把 constructor 及整个原型链一并递出去。
另外绝不向 '*' 回传:这层楼报告的是学员在做什么,所以只锁定一个
origin,也只对它说话。发送方必须是嵌套本页的那个父页面;
通过 parent.frames 摸过来的兄弟框架不是。
embed-demo.html 是一个可用的宿主页——大约 25 行封装,把调用包成 Promise
——它同时就是测试:跨源运行,逐个试探恶意方法名,
并有一个按钮会从兄弟框架里尝试驱动校园。
反馈室里的那块板不是一幅图表的插画。站点给它什么,它就画什么; 一旦真分数进去,它就不再是在描绘产品,而是成为产品的一部分。
campus.setScores([4.5, 5, 5.5, 6, 6.5, 7, 7.5], {
target: 8, // 板上的虚线目标
label: 'IELTS Task 2', // 读数栏的标题
min: 4, max: 9 // 可见分段区间(默认 4-9)
});
campus.getScores(); // 返回副本,调用方改不到原数据
一面墙能装下的有限:历史过长时只保留最近十二次,而不是挤成一片看不清的细缝。
非法输入会被拒绝,仅在控制台警告并返回 false,绝不抛异常 —
这个页面不应该有能力弄坏它所寄居的站点。
第二块板把学员反复出错的地方排序。两种写法都行 — 简单的计数对象,或者当标签里需要空格和标点时用数组。
campus.setErrors({ articles: 12, tense: 9, prepositions: 7 });
campus.setErrors([
{ label: 'Comma splice', n: 21 },
{ label: 'Passive overuse', n: 14 },
], { label: 'Recurring issues' });
计数从高到低排序,墙上只显示前五项,每项一种颜色, 让一个学员的薄弱环节一眼就能认出来。读数栏列出最严重的三项。
格式不对的条目是逐条丢弃,而不是整个调用作废, 所以自动生成的报告里有一个坏字段,不会把整面墙清空。 计数全为零时画成平的,而不是除以零。
又两面实时数据,原理相同。词墙把当前词表做成一张张卡片, 颜色表示每个词的掌握程度;旁边的书架则显示哪些范文已经真的读过了。
campus.setWords([
{ word: 'notwithstanding', state: 'new' },
{ word: 'whereas', state: 'known' },
]);
campus.setWords({ new: 14, learning: 6, known: 22 }); // 只给计数也可以 all you have
campus.setModels([{ title: 'Task 2 — Band 8', read: true }]);
campus.setModels({ read: 11, total: 14 });
十六像素下单个单词根本看不清,所以卡片传达的是分布—— 学员眼前红、黄、绿各占多少——具体数字交给读数栏。 两者都既接受完整列表,也接受纯计数,因为平台通常只有其中一种。
这里每块板都会把读数拉回可见区域,而不是直接隐藏。 板子靠墙而立,它们标签的自然位置往往刚好在上边界之外 — 而一个非得站对位置才能读到的数字,算不上什么报告。