ByteNoteByteNote
词条收藏站的无限加载与详情页
字

字节笔记本

2026年10月7日 · 约 7 分钟读完

词条收藏站的无限加载与详情页

API中转
¥120

一开始要还原的是一张服务集市首页:横滑卡片、评分、价格、分区标题。做到一半,目标改了。页面不再是卖服务,而是一个单词和短语的收藏站,强调收集和整理,不是打卡背诵。配色希望保留原来的蓝和橙,但中间来回改过两轮:先说渐变不够,又说不够简约明快。功能上随后加上了加载更多、搜索、骨架屏、本地缓存,以及单词详情里默认展开的释义、用法和笔记。

收藏卡片只留词条、译文和例句

列表页先把“学习”从界面里拿掉

收藏站的卡片不放进度条,也不放“今日复习”。一张卡片只留四样东西:类型标签(单词或短语)、词条、译文、例句。类型用颜色分开,单词走蓝色,短语走橙色,这是唯一需要跳出来的信息。右上角一个收藏动作就够了。

视图提供网格和列表两种,搜索框跟在顶栏。过滤同时看分类和关键词:关键词匹配词条、译文和例句,分类不是“全部”时再按 category 收一次。搜索做了 300 毫秒防抖,避免每敲一个字母都过滤整表。

加载更多按页追加,每页 12 条,用 page 和 hasMore 两个状态。滚到底还在请求时,用骨架屏占住高度,不要先把按钮换成一句“加载中”把布局顶下去。

生成物反复语法报错,先少写花括号

加载更多和详情页都在生成环境里炸过,报错很像,都是 Unexpected token,期待的符号有时是分号,有时是逗号,位置落在组件前几行或 JSX 属性中间。有一次明确判断是对象解构写法让解析器吃不消。

能跑起来的那版把卡片参数从解构改成了单个 props:

jsx
function WordCard(props) {
  return (
    <div className="bg-white p-6 rounded-lg hover:shadow-md transition-all">
      <span>{props.type === "word" ? "单词" : "短语"}</span>
      <h3>{props.word}</h3>
      <p>{props.translation}</p>
    </div>
  );
}

这不是 React 不能解构,而是那段生成链路对泛型和花括号很脆。报错位置如果和你肉眼看到的代码对不上,先把解构、嵌套箭头函数和一行里的多个括号拆开,确认能解析,再加回去。后来缓存逻辑太绕,也是整段删掉才重新跑起来的。复杂状态不要和有语法风险的 JSX 绑在同一次生成里。

缓存只承诺 24 小时

能留下来的缓存是一个很小的钩子:localStorage 里存数据和时间戳,读的时候如果 Date.now() - timestamp 小于 24 小时就用,否则当没有缓存。读写都包在 try/catch 里,配额满了或隐私模式写不进去时,只打一条警告,页面退回内存里的列表。

初始化时先读缓存。有数据就把 page 设成 Math.ceil(cachedData.length / 12),避免下一次加载更多从第 0 页把已有条目再拉一遍。没有缓存才请求第一页。

虚拟滚动在需求列表里,和缓存、骨架屏、搜索写在一起,但几次生成都没能和上面的语法错误共存。最后留在页面上的是“先缓存、再分页追加、搜索在已加载数据里过滤”。虚拟列表应该在数据稳定之后再加,不要和解析器还在报错的组件绑在同一轮。

详情页默认展开,不要再点一次

详情不是另一套学习流程。释义、用法、笔记三块直接铺开,不做成手风琴。用户原话就是这三块要直接展开。收成折叠之后,收藏站最有价值的部分要再点一次才能看到,和“整理”这个目的相反。

结构可以很平:标题是词条,下面依次是释义、用法、笔记。释义和例句放在一起,不要拆到两个很远的折叠面板。笔记是用户自己的话,和词典释义同一层级,不要做成附件。

先让列表能滚,再谈虚拟化

这页的顺序是:版式从集市卡片改成词条卡片,交互从“加载更多”长到搜索和缓存,详情保持展开。生成环境如果连解构都解析不了,就先用最笨的 props 把页面跑起来。24 小时的本地缓存和 300 毫秒防抖已经覆盖了重复进入和连续输入。虚拟滚动是下一刀,不是第一刀。

本地缓存以 24 小时为界

集市版式里什么可以留下

第一版静态页是服务集市的骨架,不是词条站。顶部一块渐变区域,下面四列入口,再往下是一排排固定宽度的卡片。卡片上有图、标题、评分、买过的人数、服务商名字和价格。图缺失时用占位图,避免布局塌掉。这些结构里,真正能迁到收藏站的是“卡片加分区标题加横向或网格排列”,价格、评分和人数要拿掉,否则页面还是一个店。

配色改过两轮,方向相反。一轮希望渐变和过渡更多,下一轮又觉得不够简约、不够明快。最后能稳定的做法是大面积留白,只让单词的蓝和短语的橙出现在类型标签上,背景保持浅色,卡片用很轻的阴影。渐变如果要留,只留在页面顶部很短的一条,不要铺满每一张卡片。

加载更多曾经整段生成失败,报错是 Unexpected token,有的位置期待分号,有的期待逗号。这类报错在生成环境里反复出现,行号常常落在文件很靠前的地方,和你刚加的滚动逻辑对不上。处理办法是先让最小卡片渲染出来,再把搜索、缓存、分页一个一个加回去。一次把虚拟滚动、骨架屏、缓存和搜索塞进同一个组件,解析失败时你无法知道是哪一块的括号。

详情页除了展开的释义、用法和笔记,列表上的例句应能在详情里原样看到,不要详情另起一套文案。分类筛选的“全部”是哨兵值,和真实分类比较时要先排除它,否则所有词条都会因为 category 不等于“全部”而被滤掉。这个判断写反一次,搜索框就会像坏了一样:词还在缓存里,屏幕上却是空的。

本地缓存的键要固定,例如词条数据单独一把钥匙。换了数据结构却沿用旧钥匙,旧的 JSON 仍会在 24 小时内被读出来,字段对不上就在过滤时抛异常。读缓存的 try/catch 已经能兜住解析失败,但兜住之后应该把那把钥匙删掉,否则下一次进入还会读到同一份坏数据。 搜索框旁边的视图切换只改变网格或列表,不改变数据。用户切来切去时,如果组件把 words 重置成空数组,会像是搜索把数据弄丢了。视图状态和数据状态要分开。骨架屏只在 loading 为真、而且当前没有可显示卡片时盖住网格。已经有缓存数据时,后台刷新不要用骨架把旧卡片换掉,否则每次进入都闪一下空白。

详情里的三块标题用的是释义、用法、笔记。它们直接作为页面上的小标题,而不是按钮。如果某一块暂时没有正文,仍然留下标题和一句空状态,不要把整个标题去掉,否则三次迭代之后没人知道这块到底还在不在。列表页的例句样式是浅底、小字号、圆角,详情页沿用同一段例句,避免两套文案以后对不上。

相关文章

分享: