rn_for_openharmony_收藏功能的用户体验设计与实现

案例项目开源地址:https://atomgit.com/nutpi/wanandroid_rn_openharmony
收藏是内容类 App 的标配功能。用户看到感兴趣的文章,点一下小心心,下次就能在收藏列表里找到。
这个功能的技术实现不复杂,但要做好用户体验,需要考虑很多细节。
收藏按钮长什么样
我们用 emoji 来做收藏按钮:
<TouchableOpacity onPress={handleCollect}>
<Text style={{fontSize: 18}}>{item.collect ? '❤️' : '🤍'}</Text>
</TouchableOpacity>
为什么用 emoji 而不是图标库
用 emoji 有几个好处。首先是简单,不需要引入额外的图标库,减少包体积。其次是跨平台一致,emoji 在各个平台上都能正常显示,不用担心字体加载问题。最后是直观,红心和白心的含义一目了然,不需要额外解释。
当然 emoji 也有缺点:不同平台的 emoji 样式略有差异,无法自定义颜色和大小(只能通过 fontSize 调整)。如果你的设计对图标有严格要求,还是用图标库更好。
条件渲染的逻辑
item.collect ? '❤️' : '🤍' 是一个三元表达式。item.collect 是布尔值,表示这篇文章是否已被当前用户收藏。已收藏显示红心,未收藏显示白心。
这个状态来自接口返回的文章数据。WanAndroid 的文章列表接口会根据当前登录用户返回每篇文章的收藏状态。
TouchableOpacity 的作用
TouchableOpacity 让这个 Text 组件可以响应点击事件。点击时会有透明度变化的反馈,让用户知道"我点到了"。
onPress={handleCollect} 绑定点击事件处理函数。注意这里传的是函数引用,不是函数调用。如果写成 onPress={handleCollect()},组件渲染时就会立刻执行,而不是等用户点击。
点击收藏的处理逻辑
const handleCollect = async () => {
if (!isLoggedIn) {
Alert.alert('提示', '请先登录');
return;
}
try {
if (item.collect) {
const res = await collectApi.uncollect(item.id);
if (res.errorCode === 0) {
Alert.alert('成功', '已取消收藏');
onCollectChange?.();
}
} else {
const res = await collectApi.collect(item.id);
if (res.errorCode === 0) {
Alert.alert('成功', '收藏成功');
onCollectChange?.();
}
}
} catch (e) {}
};
这个函数做了三件事:检查登录状态、调用收藏/取消收藏接口、通知父组件刷新数据。
async 函数
async 关键字声明这是一个异步函数,里面可以使用 await 等待 Promise。收藏操作需要调用网络接口,是异步的,所以用 async 函数。
登录状态检查
if (!isLoggedIn) {
Alert.alert('提示', '请先登录');
return;
}
收藏功能需要登录才能使用。isLoggedIn 来自 AuthContext,表示当前是否已登录。
如果未登录,弹窗提示用户,然后 return 结束函数执行。这里没有自动打开登录弹窗,而是让用户自己去点击登录。这是一个设计选择:有些 App 会自动弹出登录框,有些只是提示。我们选择后者,侵入性更小。
为什么要在前端检查登录状态
有人可能会问:后端接口不是也会检查登录状态吗?为什么前端还要检查?
前端检查是为了更好的用户体验。如果不检查,用户点击收藏,等待网络请求,然后才看到"请先登录"的错误。前端检查可以立刻给出反馈,不用等网络。
而且,前端检查可以避免不必要的网络请求,节省流量和服务器资源。
收藏和取消收藏的分支
if (item.collect) {
const res = await collectApi.uncollect(item.id);
if (res.errorCode === 0) {
Alert.alert('成功', '已取消收藏');
onCollectChange?.();
}
} else {
const res = await collectApi.collect(item.id);
if (res.errorCode === 0) {
Alert.alert('成功', '收藏成功');
onCollectChange?.();
}
}
根据当前收藏状态决定调用哪个接口。已收藏就取消,未收藏就收藏。这是一个"切换"操作,点一下变成相反的状态。
两个接口的区别
collectApi.collect(item.id) 是收藏接口,把文章添加到收藏列表。
collectApi.uncollect(item.id) 是取消收藏接口,把文章从收藏列表移除。
两个接口都需要传文章 ID,让服务端知道要操作哪篇文章。
errorCode 的判断
res.errorCode === 0 是 WanAndroid 接口的成功标志。errorCode 为 0 表示操作成功,非 0 表示失败。
这里只处理了成功的情况,失败时什么都不做(静默失败)。更好的做法是提示用户:
if (res.errorCode === 0) {
Alert.alert('成功', '收藏成功');
onCollectChange?.();
} else {
Alert.alert('失败', res.errorMsg || '操作失败');
}
收藏 API 的实现
export const collectApi = {
getList: (page: number) => api.get(`/lg/collect/list/${page}/json`),
collect: (id: number) => api.post(`/lg/collect/${id}/json`, {}),
uncollect: (id: number) => api.post(`/lg/uncollect_originId/${id}/json`, {}),
};
三个接口
getList 获取收藏列表,分页加载。
collect 收藏文章,传入文章 ID。
uncollect 取消收藏,传入文章的原始 ID。
为什么取消收藏用 originId
WanAndroid 的收藏列表里,每篇文章有两个 ID:收藏记录的 ID 和文章的原始 ID(originId)。
在文章列表页取消收藏时,我们只有文章的原始 ID,所以用 uncollect_originId 接口。
在收藏列表页取消收藏时,可以用收藏记录的 ID,接口是 /lg/uncollect/{id}/json。
这是 WanAndroid 接口的设计,我们按照它的规则来调用。
POST 请求的空对象
api.post(\/lg/collect/${id}/json`, {})第二个参数是空对象。虽然这个接口不需要请求体,但我们的api.post` 方法要求传一个对象,所以传空对象。
通知父组件刷新数据
onCollectChange?.();
这行代码很短,但很重要。
可选链调用
onCollectChange?.() 是可选链调用。如果 onCollectChange 是 undefined 或 null,不会报错,直接返回 undefined。如果有值,就调用它。
等价于:
if (onCollectChange) {
onCollectChange();
}
为什么需要这个回调
收藏状态变化后,界面上的心形图标应该变化(红心变白心或反过来)。但 item.collect 是 props 传进来的,组件内部不能直接修改。
正确的做法是:通知父组件数据变了,让父组件重新获取数据,然后通过 props 传入新的数据,触发重新渲染。
这就是 React 的单向数据流:数据从父组件流向子组件,子组件想改数据,要通过回调通知父组件。
父组件怎么处理
const loadArticles = async () => {
const res = await homeApi.getArticles(page);
setArticles(res.data.datas);
};
<ArticleCard
item={item}
onCollectChange={loadArticles}
/>
父组件传入 loadArticles 作为回调。收藏状态变化后,重新加载文章列表,新数据里的 collect 字段就是最新的状态。
组件的 Props 定义
interface Props {
item: Article;
showImage?: boolean;
onCollectChange?: () => void;
}
export const ArticleCard = ({item, showImage = false, onCollectChange}: Props) => {
TypeScript 接口
interface Props 定义了组件接受的属性类型。TypeScript 会在编译时检查传入的 props 是否符合类型要求。
必选和可选属性
item: Article 没有问号,是必选属性,调用组件时必须传。
showImage?: boolean 有问号,是可选属性,可以不传。
onCollectChange?: () => void 也是可选的。() => void 表示这是一个无参数、无返回值的函数。
默认值
showImage = false 在解构时设置默认值。如果调用方没传 showImage,就用 false。
onCollectChange 没有设置默认值,所以可能是 undefined,使用时要用可选链。
收藏状态的数据来源
export interface Article {
id: number;
title: string;
author: string;
shareUser: string;
niceDate: string;
link: string;
superChapterName: string;
chapterName: string;
collect: boolean; // 收藏状态
fresh: boolean;
envelopePic: string;
desc: string;
originId?: number;
}
collect 字段是布尔值,表示当前登录用户是否收藏了这篇文章。
这个字段是接口返回的,不是前端计算的。WanAndroid 的接口会根据请求头里的 Cookie 识别用户,然后在返回的文章数据里标记收藏状态。
未登录时的 collect 值
未登录时,所有文章的 collect 都是 false。因为服务端不知道你是谁,无法判断你有没有收藏。
用户体验的细节
即时反馈
点击收藏后立刻弹窗提示,让用户知道操作成功了。虽然界面上的心形图标可能要等数据刷新后才变化,但弹窗是即时的。
防止误操作
收藏和取消收藏都有弹窗确认。有些 App 会在取消收藏时加一个二次确认,防止误触。我们没有加,因为取消收藏不是不可逆的操作,用户可以再次收藏回来。
错误处理
try {
// ...
} catch (e) {}
catch 块是空的,网络错误时静默失败。这不是最佳实践,更好的做法是提示用户:
catch (e) {
Alert.alert('错误', '网络异常,请稍后重试');
}
完整的收藏相关代码
// ArticleCard.tsx 中的收藏逻辑
const {isLoggedIn} = useAuth();
const handleCollect = async () => {
if (!isLoggedIn) {
Alert.alert('提示', '请先登录');
return;
}
try {
if (item.collect) {
const res = await collectApi.uncollect(item.id);
if (res.errorCode === 0) {
Alert.alert('成功', '已取消收藏');
onCollectChange?.();
}
} else {
const res = await collectApi.collect(item.id);
if (res.errorCode === 0) {
Alert.alert('成功', '收藏成功');
onCollectChange?.();
}
}
} catch (e) {}
};
// 收藏按钮
<TouchableOpacity onPress={handleCollect}>
<Text style={{fontSize: 18}}>{item.collect ? '❤️' : '🤍'}</Text>
</TouchableOpacity>
// api.ts 中的收藏接口
export const collectApi = {
getList: (page: number) => api.get(`/lg/collect/list/${page}/json`),
collect: (id: number) => api.post(`/lg/collect/${id}/json`, {}),
uncollect: (id: number) => api.post(`/lg/uncollect_originId/${id}/json`, {}),
};
收藏功能就是这些内容。技术上不复杂,但每个细节都影响用户体验。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)