流程的终点也是体验的一部分
用户填完领养申请表,点击提交,然后呢?如果直接跳回首页,用户会困惑:我的申请提交成功了吗?接下来会发生什么?
完成页就是为了解决这个问题。它给用户一个明确的反馈,告诉他们"你做的事情成功了",同时引导他们进行下一步操作。
很多开发者觉得完成页不重要,随便写个"成功"就完事了。但实际上,这个页面是用户情绪的高点,处理好了能大大提升用户满意度。
页面的导入
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 号半粗体,和其他页面的按钮保持一致。
reset 和 navigate 的本质区别
这是理解导航系统的关键。
先看 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 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
