rn_for_openharmony狗狗之家app实战-领养完成实现

案例开源地址:https://atomgit.com/nutpi/rn_openharmony_dogimg

流程的终点也是体验的一部分

用户填完领养申请表,点击提交,然后呢?如果直接跳回首页,用户会困惑:我的申请提交成功了吗?接下来会发生什么?

完成页就是为了解决这个问题。它给用户一个明确的反馈,告诉他们"你做的事情成功了",同时引导他们进行下一步操作。

很多开发者觉得完成页不重要,随便写个"成功"就完事了。但实际上,这个页面是用户情绪的高点,处理好了能大大提升用户满意度。

页面的导入

typescript 复制代码
import React from 'react';
import {View, Text, TouchableOpacity, StyleSheet} from 'react-native';

导入很精简,只用到了最基础的组件。

没有 ScrollView,因为内容很少,一屏就能显示完。没有 Image,因为用 emoji 代替了图片。

这种极简的依赖是完成页的特点,它不需要复杂的功能,只需要清晰地传达信息。

typescript 复制代码
import {useNavigation, useRoute} from '../../hooks';

两个自定义 Hook:useNavigation 处理页面跳转,useRoute 获取路由参数。

这两个 Hook 在项目里用得很多,几乎每个需要跳转或接收参数的页面都会用到。

获取导航方法

typescript 复制代码
export function AdoptDonePage() {
  const {reset} = useNavigation();

注意这里解构的是 reset 而不是常见的 navigate

这是个很重要的区别,后面会详细讲。简单说,reset 会清空导航栈,而 navigate 只是往栈里加一个页面。

获取路由参数

typescript 复制代码
  const {params} = useRoute<{name: string}>();

泛型 <{name: string}> 告诉 TypeScript 参数的结构。

从申请页跳转过来时,只传了狗狗的名字:

typescript 复制代码
navigate('AdoptDone', {name: params.dog.name})

为什么只传名字而不是整个 dog 对象?因为完成页只需要显示名字,传递最小必要的数据是个好习惯。数据越少,出错的可能性越小,内存占用也越少。

页面容器

typescript 复制代码
  return (
    <View style={s.container}>

整个页面就是一个 View 容器,里面放着所有内容。

typescript 复制代码
container: {
  flex: 1, 
  backgroundColor: '#fff', 
  alignItems: 'center', 
  justifyContent: 'center', 
  padding: 40
},

flex: 1 让容器撑满整个屏幕。

backgroundColor: '#fff' 白色背景,干净清爽,和之前页面的灰色背景形成对比,暗示"这是个特殊的页面"。

alignItems: 'center' 让所有子元素水平居中。

justifyContent: 'center' 让所有子元素垂直居中。

这两个属性组合起来,内容就会显示在屏幕正中央。这种居中布局在完成页、错误页、空状态页很常见。

padding: 40 比较大的内边距,让内容不会太靠近屏幕边缘,视觉上更舒适。

庆祝图标

typescript 复制代码
      <Text style={s.icon}>🎉</Text>

用 emoji 作为图标,简单直接。🎉 是"庆祝"的意思,传达"恭喜你完成了"的情绪。

typescript 复制代码
icon: {fontSize: 70, marginBottom: 20},

70 号字非常大,是整个页面的视觉焦点。用户一进来第一眼就会看到它。

marginBottom: 20 和下面的标题拉开距离,形成呼吸感。

为什么用 emoji 而不是图片?

  • 不需要额外的图片资源,减少包体积
  • emoji 在各个平台都能显示,不用担心兼容性
  • 表达力强,一个符号就能传达情绪

当然,如果设计师提供了专门的插画,用图片会更精致。但在快速开发阶段,emoji 是个很好的选择。

标题文字

typescript 复制代码
      <Text style={s.title}>申请已提交</Text>

标题要明确告知结果。"申请已提交"比"操作成功"更具体,用户一眼就知道发生了什么。

typescript 复制代码
title: {fontSize: 24, fontWeight: '600', color: '#333', marginBottom: 12},

24 号字是页面里最大的文字(除了 emoji)。

fontWeight: '600' 半粗体,比普通文字更醒目,但不像 '700' 或 'bold' 那么重。

color: '#333' 深灰色,比纯黑柔和一些。

marginBottom: 12 和描述文字拉开适当距离。

描述文字

typescript 复制代码
      <Text style={s.desc}>感谢您申请领养 {params.name}!</Text>
      <Text style={s.desc}>我们会尽快审核您的申请并与您联系。</Text>

两行描述,各有作用:

第一行表达感谢 ,并且提到狗狗的名字,让用户感到被重视。{params.name} 是动态插入的,每个用户看到的都是自己申请的那只狗。

第二行说明后续,告诉用户接下来会发生什么。这很重要,能消除用户的不确定感。"我提交了,然后呢?"------这行文字回答了这个问题。

typescript 复制代码
desc: {fontSize: 15, color: '#666', textAlign: 'center', marginBottom: 6},

15 号字比标题小,color: '#666' 灰色比标题浅,形成视觉层次。

textAlign: 'center' 文字居中对齐,和整体的居中布局呼应。

marginBottom: 6 两行描述之间有小间距,但不会太大,因为它们是一组内容。

返回按钮

typescript 复制代码
      <TouchableOpacity style={s.btn} onPress={() => reset('Home')}>
        <Text style={s.btnText}>返回首页</Text>
      </TouchableOpacity>

这是页面上唯一的操作入口。

注意 onPress 里调用的是 reset('Home') 而不是 navigate('Home')。这个区别非常重要,是这篇文章的核心知识点。

typescript 复制代码
btn: {
  backgroundColor: '#D2691E', 
  paddingVertical: 14, 
  paddingHorizontal: 40, 
  borderRadius: 8, 
  marginTop: 30
},

主题色背景,和整个 App 的风格统一。

paddingHorizontal: 40 让按钮比较宽,更容易点击,也更有存在感。

marginTop: 30 和上面的描述拉开较大距离,视觉上把页面分成"信息区"和"操作区"两部分。

typescript 复制代码
btnText: {fontSize: 16, fontWeight: '600', color: '#fff'},

白色文字在深色背景上清晰可见。16 号半粗体,和其他页面的按钮保持一致。

这是理解导航系统的关键。

先看 navigate 的实现:

typescript 复制代码
navigate(route: string, params?: Params) {
  this.route = route;
  this.params = params || {};
  this.stack.push({route, params});
  this.notify();
}

this.stack.push 把新页面压入栈顶。栈是一种后进先出的数据结构,就像一摞盘子,新盘子放在最上面。

用户从首页一路点到完成页,栈里的内容是:

Home → AdoptList → AdoptDetail → AdoptForm → AdoptDone

如果在完成页用 navigate('Home'),栈变成:

Home → AdoptList → AdoptDetail → AdoptForm → AdoptDone → Home

这时候用户按返回键,会回到 AdoptDone,再按回到 AdoptForm......这显然不对,用户已经完成申请了,不应该再回到申请流程。

再看 reset 的实现:

typescript 复制代码
reset(route: string) {
  this.route = route;
  this.params = {};
  this.stack = [{route}];
  this.notify();
}

this.stack = [{route}] 直接重置整个栈,只保留目标页面。

reset('Home') 后,栈变成:

Home

只有一个页面,用户按返回键没有反应(或者退出 App),这才是正确的行为。

什么时候用 reset

流程结束时:领养申请完成、订单支付成功、注册完成等。这些场景下,用户不应该回到中间步骤。

登录/登出时:登录成功后 reset 到首页,登出后 reset 到登录页。

切换账号时:清空之前账号的页面栈,避免数据混乱。

goBack 的实现

顺便看看返回是怎么工作的:

typescript 复制代码
goBack(): boolean {
  if (this.stack.length > 1) {
    this.stack.pop();
    const prev = this.stack[this.stack.length - 1];
    this.route = prev.route;
    this.params = prev.params || {};
    this.notify();
    return true;
  }
  return false;
}

this.stack.pop() 弹出栈顶元素,就是当前页面。

然后取新的栈顶作为当前页面,更新 route 和 params。

如果栈里只有一个页面,返回 false 表示无法返回。Header 组件会根据这个判断是否显示返回按钮。

canGoBack 的作用

typescript 复制代码
canGoBack() { return this.stack.length > 1; }

这个方法很简单,但很有用。

Header 组件用它来决定是否显示返回按钮:

typescript 复制代码
{showBack && canBack && (
  <TouchableOpacity onPress={goBack} style={s.btn}>
    <Text style={s.back}>←</Text>
  </TouchableOpacity>
)}

首页的栈里只有一个页面,canGoBack() 返回 false,所以首页不显示返回按钮。

完成页用 reset 后,栈里也只有一个页面,所以如果完成页有 Header,也不会显示返回按钮。

订阅机制

Navigator 用发布-订阅模式通知页面变化:

typescript 复制代码
private listeners = new Set<Listener>();

subscribe(fn: Listener): () => void {
  this.listeners.add(fn);
  return () => {
    this.listeners.delete(fn);
  };
}

listeners 是个 Set,存储所有订阅者。

subscribe 方法把订阅函数加入 Set,返回一个取消订阅的函数。

这个返回值的设计很巧妙,可以直接用在 useEffect 的清理函数里:

typescript 复制代码
useEffect(() => {
  const unsubscribe = navigator.subscribe(handleChange);
  return unsubscribe;  // 组件卸载时自动取消订阅
}, []);
typescript 复制代码
private notify() {
  this.listeners.forEach(fn => fn(this.route, this.params));
}

每次路由变化时,遍历所有订阅者,调用它们的回调函数,传入新的路由和参数。

React Navigation 是 React Native 最流行的导航库,功能强大,社区活跃。但我们选择自己实现,有几个原因:

鸿蒙兼容性:第三方库可能没有适配鸿蒙系统,自己实现的代码可控性更强。

学习价值:理解导航的原理比会用库更重要。知道栈是怎么工作的,以后用任何导航库都能快速上手。

简单够用:我们的需求不复杂,100 行代码就能满足。引入一个大库反而增加了复杂度。

当然,如果项目需求复杂,比如需要 Tab 导航、Drawer 导航、深度链接等,还是建议用成熟的库。

完成页的情感设计

好的完成页不只是"告诉用户成功了",还要让用户感觉好

视觉上:大图标、居中布局、留白充足,给人轻松愉悦的感觉。

文案上:感谢语、后续说明,让用户感到被重视和安心。

操作上:只有一个按钮,不让用户纠结该点哪个。

这些细节加起来,就是用户体验的差距。

扩展:添加动画

完成页可以加点入场动画,增强庆祝感:

typescript 复制代码
const scale = useRef(new Animated.Value(0)).current;

useEffect(() => {
  Animated.spring(scale, {
    toValue: 1,
    friction: 3,
    useNativeDriver: true,
  }).start();
}, []);

创建一个动画值,初始是 0。

组件挂载时,用弹簧动画把它变成 1。friction: 3 让动画有弹性,会稍微过冲再回弹。

typescript 复制代码
<Animated.Text style={[s.icon, {transform: [{scale}]}]}>
  🎉
</Animated.Text>

把动画值应用到 transform 的 scale 属性,图标就会从 0 放大到 1,有个"蹦出来"的效果。

扩展:多个操作选项

有时候用户完成一个流程后,可能有多种后续需求:

typescript 复制代码
<TouchableOpacity style={s.btn} onPress={() => reset('Home')}>
  <Text style={s.btnText}>返回首页</Text>
</TouchableOpacity>
<TouchableOpacity style={s.linkBtn} onPress={() => reset('Adopt')}>
  <Text style={s.linkText}>继续浏览其他狗狗</Text>
</TouchableOpacity>

主按钮是"返回首页",次要操作用文字链接的形式。

这样既有明确的主路径,又给用户提供了选择。

小结

领养完成页的实现要点:

  • 居中布局:alignItems + justifyContent 让内容在屏幕中央
  • 情感设计:emoji、感谢语、后续说明,给用户温暖的感受
  • reset 导航:清空栈,防止用户回到已完成的流程
  • 栈的原理:理解 push、pop、reset 对栈的影响

完成页代码虽少,但它是用户旅程的终点,值得认真对待。一个好的结束,能让整个体验加分。

下一篇讲领养地图页面,展示待领养狗狗的地理分布。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
奔跑的露西ly5 小时前
【HarmonyOS NEXT】Stage模型
华为·harmonyos
威哥爱编程6 小时前
鸿蒙 APP 还是卡顿?API 21 性能优化这 3 招,立竿见影!
harmonyos·arkts·arkui
威哥爱编程6 小时前
List 组件渲染慢?鸿蒙API 21 复用机制深度剖析,一行代码提速 200%!
harmonyos·arkts·arkui
2501_944521007 小时前
rn_for_openharmony商城项目app实战-语言设置实现
javascript·数据库·react native·react.js·harmonyos
程序猿追8 小时前
【鸿蒙PC桌面端开发】使用ArkTS做出RGB 色环选择器
华为·harmonyos
zhujian826379 小时前
二十五、【鸿蒙 NEXT】@ObservedV2/@Trace实现组件动态刷新
华为·harmonyos·trace·lazyforeach·observedv2
wszy180910 小时前
rn_for_openharmony_空状态与加载状态:别让用户对着白屏发呆
android·javascript·react native·react.js·harmonyos
SameX10 小时前
鸿蒙应用的“任意门”:Deep Linking 与 App Linking 的相爱相杀
harmonyos
AlbertZein10 小时前
HarmonyOS一杯冰美式的时间 -- @Watch 到 @Monitor
harmonyos