我做的这个页面,左边读诗,右边看月夜。第一张运行截图里,我选中了《水调歌头》的“转朱阁,低绮户,照无眠”,旁边就有对应的短注和窗景;换到王建《十五夜望月》,诗句、短注、画面也一起换了。比起只摆一张月亮图,我更想让读者朋友们点一句诗,再看看窗外发生了什么。
“月下读诗”是我用 AtomGit 上的华为云码道 CodeArts 代码智能体做的中秋诗词阅读网页。我们可以选句、看短注、切换诗篇,也能展开《水调歌头》全文,或暂停窗景动效。我用 Vue 3、TypeScript 和 Vite 把它做成了打开就能读的小应用:不用注册账号,阅读过程也不依赖在线大模型。
先把范围收住:诗能读,画面再跟上
最初给 Agent 提需求时,我先把内容压到三首:苏轼《水调歌头》、王建《十五夜望月》、白居易《八月十五日夜湓亭望月》,每首挑三处句子供读者点选。苏轼这首默认只露出下阕,标明“节选”;想看小序和上阕,再点“展开全文”。那么画面怎么配合阅读?我的要求是:点中诗句,短注马上更新,窗景可以慢慢变;即使暂停动效,也照样能读诗、选句。
我还给 Agent 一张概念效果图,让它参考左右布局,但文字、目录、按钮和窗景都得在网页里真正做出来。那张图只是我想要的样子;下面两张运行截图,才是页面做出来后的效果。我特别提了一个小要求:打开页面就能读第一首诗,别让读者等“写字动画”播完才可以点句子。
诗文不能只顾着排版,我也得看看文字从哪儿来。开发记录显示,Agent 查资料时有些页面能打开,维基文库当时却超时了;这部分没比对完的情况,它写进了docs/SOURCES.md。白居易那首采用“临风一叹无人会”,文档也记下了另一网络页面的不同写法。写这篇文章时,我又去看了《全唐诗》卷 440 的在线录文,确实能找到这句。不过,这只是在线文本的核对,我没有查古籍原刻,更谈不上完成版本考证。页面里的九条短注也是辅助阅读的小解释,不是权威译注。
页面能用了,我又改了三处观感
Agent 把诗文数据、选句逻辑和页面组件搭起来后,我先看了一遍运行画面。结果有三处让我想再改改:目录一打开就占着位置,读诗时有点碍眼;右边的图片看着不完整;整幅画面也太静了。我把这三点发回会话,请 Agent 收起目录、调整图片比例,再加一点缓慢的呼吸感。截图里保留了我当时的反馈和它的分析过程。
接下来我对照了源码:目录初始是open = false;窗景视口定为 16:9,和项目里图片的比例接近;图片用 22 秒完成一次缓慢缩放,也照顾到了暂停和系统的“减少动态效果”设置。我还发现旧 README 没跟上这次改动,它写的仍是“内联 SVG、25 秒薄云漂移”。当前MoonWindow.vue实际用的是三张本地 WebP 图片,外加 CSS 光色叠层。所以这里我按现有代码和运行画面来写,不沿用旧说明。
第二张运行截图里,我切到了王建《十五夜望月》。目录收起来了,右边换成庭院与桂花的月夜图;左边选中“中庭地白树栖鸦”,下方的短注和窗景说明也跟着换。两张图能让我们看清页面前后是什么样;至于选句怎样带动这些变化,我又回头看了组件和状态代码。
一句诗怎样找到自己的窗景
读者点一句诗,窗景为什么能找到对应的变化?我在代码里看到,诗行、注释和场景参数是分开放的。每行诗有固定 ID,通过annotationId找到短注;另外还有九条“诗行 → 场景目标”的配置。接下来,程序先拿这首诗的基础场景,再叠上选中句子的设置。src/data/scenePresets.ts里负责这一步的代码不长:
exportfunctionsceneTargetForSelection(poem:Poem,lineId:string|null,):{state:SceneState;durationMs:number;note:string}{constbase:SceneState={...DEFAULT_SCENE,...(BASE_SCENE_PRESETS[poem.id]??{})}if(!lineId){return{state:base,durationMs:600,note:''}}constlink=findLineScene(poem.id,lineId)if(!link){return{state:base,durationMs:600,note:''}}conststate:SceneState={...base,...link.preset.target}return{state,durationMs:link.preset.durationMs,note:link.note}}这样一来,切到另一首诗,上一首的短注就不会留在下面;接连选句时,状态会按最后一次选中的 ID 更新。我也在意文字能不能被正常阅读:诗句是页面上的文字,带注释的句子做成可用键盘操作的按钮,没有把字直接烘焙进窗景图片。
不过我翻到MoonWindow.vue时,也发现一个没做完的地方。组件目前会读取月亮亮度、冷暖色调、地面和水面的光层设置,但还没用到预设里的cloudOffsetX、floorLightOffsetX和birdOpacity。所以窗景说明虽然写了“云掠过月面”“窗格投影移动”,我不能说这些都已经做成逐部件动画。现版代码里能确认的,是三张图片随诗切换,以及滤镜和整幅叠层的透明度随选句调整。
动效开关我也核对了一遍。useMotionPreference.ts分别看用户有没有暂停、系统是否要求“减少动态效果”、页面当前是否可见;三项都允许,动效才播放。暂停以后,我们仍能选诗、读短注。要是这时再切换诗图,现版还留着约 200 毫秒的淡入过渡,并不是“所有动画瞬间消失”。
我核对了什么,还没核对什么
写到测试这里,我得把两次记录分开说。开发会话的截图里,Agent 报告首版 Playwright 点击流 36 项、键盘流 23 项通过;那时还没换成现在的 WebP 场景图。我没有把这份旧记录当成现版视觉效果的浏览器回归测试。
然后我在当前本地项目重跑了npm test、npm run typecheck和npm run build:三个测试文件共 27 项通过,类型检查也过了,Vite 构建成功。单测检查了三首诗和九条短注的数据、切诗选句状态,以及暂停、系统偏好和页面可见性如何配合。这能说明相关逻辑在当前工作区跑通了,但我还没在真实手机上检查字的大小和触摸操作,也没逐句核对动画。当前工作区的依赖版本与首次提交时不同,所以两次结果得分开看。
我把源码推到了 AtomGit:cannonjinx/moonlight-poetry。下面这张 Agent 截图里能看到账号cannonjinx和推送记录;我也核对过远端main,它与本地当前提交2f38a61一致。同学们如果想自己打开看看,拉取项目后执行npm install、npm run dev即可启动;npm test、npm run typecheck、npm run build则可以分别复核逻辑、类型和构建。
做完这一版,我最在意的还是读诗时会不会被画面打断。目录默认收起、图片比例调对以后,页面顺眼多了;回头看源码,我又发现有些场景参数还没接进窗景,README 也停在旧实现。接下来我会补测真实手机、Safari 和大字号场景,再把这些遗漏逐个补上。