news 2026/9/29 16:05:06

React Native鸿蒙开发实战:图片全屏查看器从零实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React Native鸿蒙开发实战:图片全屏查看器从零实现

看到标题你可能第一反应是:React Native 也能开发鸿蒙应用了?还真能。鸿蒙跨平台开发这两年是肉眼可见的火起来,而 React Native(以下简称RN)作为跨平台开发的老牌方案,现在也把触角伸进了鸿蒙生态。今天这篇就从一个最简单的功能切入——图片全屏查看,带着你把环境搭起来、代码跑通、功能落地,顺便把我在真机上踩过的坑一次说清楚。这篇不假设你写过鸿蒙,也不假设你精通RN,只要你会一点JavaScript,就能跟着做完。

如果你是那种已经写了一年React Native、但完全没碰过鸿蒙的前端,或者是一个刚准备入坑鸿蒙的小白,这篇文章都能给到一条能直接走的路径。我自己是从传统RN项目迁移到鸿蒙的,最大的感受是:业务代码几乎不用动,真正要花时间的是环境、原生工程和那些只有真机才能暴露出来的问题。所以我会把“能跑起来”和“跑得顺畅”拆成两大部分,你按顺序来就行。

1. 为什么现在可以用React Native做鸿蒙开发

1.1 纯血鸿蒙带来跨平台需求的拐点

鸿蒙走到今天,已经不是简单套个安卓壳的概念了。HarmonyOS NEXT 从底层上放弃了安卓兼容,这意味着过去“一套安卓APK走天下”的做法在鸿蒙上行不通了。对团队来说,如果产品要同时覆盖 Android、iOS、鸿蒙,摆在前面的选项无非三条:

  1. 三端各写一套原生代码,成本直接乘三;
  2. 用 Flutter 或者 RN 这类跨平台框架,业务层写一套,适配层各自解决;
  3. 用 ArkUI 单独开发鸿蒙版,前端团队再养一套技能树。

第二类方案最吸引人,也是为什么这两年Flutter和RN在鸿蒙社区的热度越来越高。RN 的优势在于:它的核心是把 JavaScript 业务逻辑映射到原生组件上,只要原生适配层够完整,JS侧代码几乎不需要动。鸿蒙能不能接住这套逻辑,关键就看有没有人做那个“原生适配层”。

1.2 React Native的鸿蒙适配:rnoh到底是什么

RN对应的鸿蒙适配方案在社区里一般叫rnoh(React Native OpenHarmony),由 OpenHarmony 社区和华为相关团队在推进。它的做法是在鸿蒙的 ArkUI 层实现一套 RN 原生组件映射,让 RN 的View、Text、Image、ScrollView这些基础组件能翻译成 ArkUI 的对应组件。

这套方案的成熟度是循序渐进的。早期只能跑纯 JS 逻辑项目,后来基础组件的补全率越来越高,再后来 Hermes 引擎、Fabric 架构也开始对鸿蒙做适配。到我现在日常使用的版本,跑一个带网络图片、列表滚动、Modal 弹层的 RN 应用已经没什么问题。

对小白来说,你不需要理解太多底层细节,只需要明白一件事:RN跑在其他平台靠的是各平台的适配层,鸿蒙也不例外。你写 JS 和样式,底层渲染交给鸿蒙原生能力,网络、存储、文件这些模块也都有对应实现。

1.3 这套方案适合谁、不适合谁

说句实在话,rnoh 生态还没有到“任何第三方库都能无脑跑”的程度。我自己的判断标准是:

  • 适合:项目以基础界面、业务表单、列表页、图片展示、简单交互为主,不依赖大量原生模块;
  • 适合:团队已经有 RN 代码库,想快速覆盖鸿蒙,避免重写成本过高;
  • 不适合:重度依赖特殊传感器、复杂多媒体处理、AR/VR 这类强原生能力的项目;
  • 不适合:如果你需要调用的某个原生 SDK 没有鸿蒙版,且社区也没人做适配,那这套方案会很痛苦。

我见过一些团队把地图、支付、音视频通话这些业务搬上鸿蒙,最后都补了大量自定义原生桥接,甚至去找第三方厂商催鸿蒙 SDK。所以在立项之前,先拉一个“第三方依赖”清单,逐项确认鸿蒙兼容性,这个动作一定要做在前面。

2. 给小白的鸿蒙RN开发环境搭建清单

2.1 需要装的软件和版本要求

环境搭建这一步劝退了很多人,主要是版本之间互相有要求,不能乱装。我先给一份我自己验证过的组合,你照着装基本不会翻车:

软件版本建议说明
Node.js18.x 或 20.x LTSRN脚手架和Metro依赖它
DevEco Studio5.0 及以上鸿蒙官方IDE,自带SDK管理
HarmonyOS SDKAPI 12 及以上也就是HarmonyOS NEXT一代的SDK
JDK17DevEco Studio较新版本会要求
React Native0.72.5 或 0.73 系列这是rnoh适配比较完善的版本区间
鸿蒙模拟器或真机建议优先真机模拟器可以跑但图片加载等表现有差异

装完 DevEco Studio 之后,记得进入 SDK Manager 把HarmonyOS SDK 和配套的模拟器镜像装好。Xcode 和 Android Studio 那套如果你已经有了,不影响,因为鸿蒙是独立工程。

2.2 创建React Native鸿蒙项目的两条路径

我实际操作下来,进入鸿蒙RN项目有两条路,各有适用场景。

路径一:从空项目开始,直接生成鸿蒙RN工程

如果你跟我当时一样,没有任何历史代码,建议直接使用 rnoh 提供的脚手架初始化。大致逻辑是:

# 安装脚手架工具 npm install -g @react-native-ohos/react-native-harmony-cli # 初始化项目 npx @react-native-ohos/react-native-harmony-cli init MyHarmonyRNDemo

执行完之后,你会得到一个同时包含React Native 工程和harmony原生工程(或类似命名的鸿蒙目录)的项目结构。这就是标准的“RN + 鸿蒙”模板工程。

路径二:在已有RN项目中增加鸿蒙工程

如果你已经有一个跑在 Android 和 iOS 上的 RN 项目,想新增鸿蒙支持,那就不需要重新初始化项目,而是用脚手架给出的add或者手动拷贝鸿蒙工程模板的方式,把鸿蒙原生工程挂到现有目录下。这一步在 rnoh 官方文档里写得很细,我建议你以当前版本官方文档为准,因为命令细节在不同版本里会有调整。

2.3 环境自检:先把默认项目跑起来再动手

我强烈建议你在写任何功能代码之前,先把生成好的默认项目跑起来。别嫌这一步浪费,很多初学者在默认项目都跑不通的情况下就开写业务代码,最后分不清问题是自己代码的还是环境的。

我自己习惯的自检流程是这样的:

  1. 在项目根目录执行npm install,确认依赖能装干净;
  2. 启动 Metro:npm start;
  3. 在 DevEco Studio 里打开鸿蒙工程,配置好签名后编译;
  4. 把鸿蒙真机通过 USB 连上电脑(如果是模拟器就在 DevEco 里启动虚拟设备);
  5. 编译完成后,看到 RN 的 Log 输出,并且在手机上出现默认首页。

这里要特别提醒:鸿蒙真机调试需要你提前完成开发者实名认证和签名配置,不然 DevEco 在编译阶段就会卡住。模拟器不需要真机签名那么严格,所以急着先看效果的人可以先用模拟器,但真机上的问题模拟器不一定能看到,后面我会详细讲。

3. 图片全屏查看功能的需求拆解与实现思路

3.1 一个图片查看功能到底包含哪几件事

标题里的“图片全屏查看”表面上看就是一个点击放大,但实际拆开,里面至少有四件事:

  1. 图片源:可能是网络图、本地资源、base64 数据;
  2. 展示入口:用户在哪个位置触发全屏,通常是列表里的缩略图;
  3. 全屏弹层:点击之后要覆盖整个页面,并能维持图片比例,不允许拉伸变形;
  4. 收尾交互:查看结束后用户怎么退出,点按钮、点背景、还是滑动关闭。

如果我更严格要求一点,还应该有加载状态(图片没出来时给用户一个转圈提示)、失败状态(网络断了给个重试入口)、手势交互(单指拖动、双指缩放)。

很多新手把“全屏查看”直接理解成“把图变大的页面”,容易忽略退出方式和加载体验,最后做出来的功能会很生硬。

3.2 为什么用Modal而不是跳转新页面

在RN里实现全屏展示,最直接的想法是“我再跳一个页面,页面里把图放大不就行了”。这个方案不是不能用,只是有几个问题:

  • 页面跳转需要维护路由栈,全屏查看本质是临时浮层,不适合占一个路由层级;
  • 页面跳转会有转场动画和页面重建开销,全屏弹层应该更轻量;
  • 如果用户在图片查看器里只是想把图看仔细,并不想改变下游业务状态,那 Modal 模式更贴合语义。

所以正常做法是使用 RN 内置的Modal组件。Modal 在 UI 层级上会直接浮在现有页面之上,并且自带fade、slide等动画模式,非常适合做这种“临时占据全屏”的容器。

3.3 从缩略图到全屏弹层的整体代码骨架

我先给你一个能跑的最小结构,不包含手势,先把“点开-展示-关闭”这个闭环打通。

import React, { useState } from 'react'; import { View, Image, Modal, Text, TouchableOpacity, StyleSheet, } from 'react-native'; export default function App() { const [visible, setVisible] = useState(false); return ( <View style={styles.container}> <TouchableOpacity onPress={() => setVisible(true)}> <Image source={{ uri: 'https://example.com/thumb.jpg' }} style={styles.thumb} /> </TouchableOpacity> <Modal visible={visible} transparent={false} animationType="fade" onRequestClose={() => setVisible(false)} > <View style={styles.fullscreen}> <Image source={{ uri: 'https://example.com/large.jpg' }} style={styles.fullImage} resizeMode="contain" /> <TouchableOpacity style={styles.closeBtn} onPress={() => setVisible(false)} > <Text style={styles.closeText}>关闭</Text> </TouchableOpacity> </View> </Modal> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center' }, thumb: { width: 120, height: 120, borderRadius: 8 }, fullscreen: { flex: 1, backgroundColor: '#000', justifyContent: 'center', alignItems: 'center', }, fullImage: { width: '100%', height: '100%' }, closeBtn: { position: 'absolute', top: 60, right: 20, backgroundColor: 'rgba(255,255,255,0.2)', paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, }, closeText: { color: '#fff', fontSize: 16 }, });

这一段跑起来之后,点击缩略图,黑色全屏层出现,图片居中且保持比例,右上角“关闭”可以退出。到这里你已经完成了一个视觉上完整的全屏查看器。

4. 手把手实现全屏预览:从静态展示到手势操作

4.1 第一步:确保图片在不同场景下渲染正确

全屏查看器的基础是Image组件,但Image在鸿蒙上的表现和你在安卓上写的几乎一模一样,不过有几个细节要留意。

首先是resizeMode。全屏查看时,图片一般要用contain,意思是“保持宽高比的情况下尽量完整展示”,这样横图和竖图都不会被裁剪。如果你用cover,图片会填满屏幕但边缘被切掉,查看原图时体验不好。

其次是网络图片的加载。很多小白在网页上放了一个 http 的图片地址,结果在手机上死活加载不出来。鸿蒙默认是不允许明文 http 请求的,你要么用 https 链接,要么在鸿蒙工程里配置网络安全策略。更好的办法是直接使用 https 图片资源,省掉一堆麻烦。

最后是基础加载优化。如果你点开的是高清原图,最好在缩略图和小图上分开处理:列表里显示窄图,全屏时再加载大图。这个阶段用不到高级的图片缓存库,RN 内置的Image已经能处理基础的缓存,只是不够精细而已。

4.2 第二步:为全屏容器加上手势拖拽关闭

有了基础弹层,下一步就是让用户能更符合直觉地关闭:在全屏状态下,向下拖动图片到一定距离就关闭,拖动小于阈值就回弹。这里用到的核心是PanResponder和Animated。

PanResponder是 RN 提供的手势响应系统,它可以捕获触摸事件并持续追踪手指的坐标变化。我封装了一个简易的拖拽逻辑:监听手指移动的纵向距离,实时改变图片的translateY;手指松开时判断位移是否超过屏幕高度的1/5,超过就关闭,否则用动画回弹到原始位置。

const pan = useRef(new Animated.Value(0)).current; const panResponder = useRef( PanResponder.create({ onMoveShouldSetPanResponder: (_, gestureState) => { return Math.abs(gestureState.dy) > 15 && Math.abs(gestureState.dy) > Math.abs(gestureState.dx); }, onPanResponderMove: (_, gestureState) => { if (gestureState.dy > 0) { pan.setValue(gestureState.dy); } }, onPanResponderRelease: (_, gestureState) => { if (gestureState.dy > screenHeight / 5) { Animated.timing(pan, { toValue: screenHeight, duration: 200, useNativeDriver: true, }).start(() => onClose()); } else { Animated.spring(pan, { toValue: 0, useNativeDriver: true, }).start(); } }, }) ).current;

这段代码的关键在onMoveShouldSetPanResponder里做了方向判断,只有用户手势是向下的才接管,避免和图片内部的双指手势冲突。拖动关闭这种交互在安卓和iOS上都很流行,用户在鸿蒙上一样能快速上手。

4.3 第三步:双指缩放与双击放大,做到能看细节

图片全屏查看如果只能看不能放大,其实还差一口气。尤其用来查看截图、设计稿、照片细节的时候,用户一定会双指捏合放大。

这里我建议小白优先考虑处理方法是:别自己造轮子,先用成熟第三方库验证效果,再决定要不要手写。

在 RN 生态里,react-native-image-zoom-viewer和react-native-image-viewing是两套常见方案。前者内置了缩放、平移、双击放大,甚至支持图片轮播;后者更轻量,专注全屏查看。

但要注意,这些库在鸿蒙上不一定100%可用。我在鸿蒙上试验时发现,一些依赖原生手势模块的第三方库需要额外的鸿蒙原生适配。所以我的建议是:

  • 先检查这个库的依赖里有没有react-native-gesture-handler、react-native-reanimated这类对原生能力要求比较重的模块;
  • 如果没有鸿蒙原生实现,优先使用纯 JS 动画 + RN 内置手势(也就是 PanResponder + Animated)来实现简单的缩放平移;
  • 如果你的需求已经到了图片轮播、贴边回弹、惯性滚动这种复杂度,再考虑引入库,同时提前准备好为鸿蒙做原生适配的预案。

我自己在鸿蒙 Demo 里实现过一版最基础的捏合缩放:通过onTouchStart记录两个手指的初始距离,onTouchMove计算当前距离与初始距离的比值,用这个比值去设置一个Animated.Value,然后给图片施加scale变换。整个过程只用到了 RN 的内置能力,鸿蒙运行良好。

4.4 第四步:加载态、错误态和占位图不能省

很多初学者的图片全屏查看器看起来很简陋,关键就是少了状态管理。我把全屏图片的状态拆成三个:

  • 加载中:显示一个居中的 ActivityIndicator,或者自定义转圈动画;
  • 加载成功:正常渲染图片;
  • 加载失败:显示“图片加载失败”和一个“重试”按钮。

做法上很简单,在 Image 的onLoadStart、onLoadEnd和onError回调中切换状态。代码不算多,但对体验提升非常明显。你想一下:用户在网络波动的时候点开图片,如果整个屏幕一片黑,他根本不知道是自己的网络问题还是应用坏了。

5. 鸿蒙真机上的几个坑:白屏、图片不显示与内存泄漏

5.1 启动白屏的排查链路

热搜词里“react native 启动白屏”热度一直很高,我估计在鸿蒙上也一样会遇到。启动白屏通常分两种情况。

第一种是应用启动到JS Bundle执行完成之间的白屏。RN 应用启动时会先加载 Hermes 引擎,再执行 JavaScript Bundle。这个过程如果比较慢,屏幕上就会一直停留在默认的空白状态。解决办法分几个方向:开启 Metro 的ramBundle分包加载、把启动页背景色设置成有品牌感的颜色、以及延迟首页渲染的时机,让用户先看到闪屏而不是白屏。

第二种是页面加载完但内容空白。这种情况通常是 JS 报错、图片加载失败、或者 Flexbox 布局把内容挤出可视区。我的排查方法是连接 DevEco 的 Log 面板,直接过滤ReactNativeJS关键字,看有没有未捕获的异常。只要 JS 层有报错,Log 里一般都会打出来。

我遇到过最迷惑的一次是:页面 Log 一切正常,没有报错,但页面就是空白。后来发现是根容器的样式用了flex: 1,但父容器的高度没有撑开,导致整个页面高度为0。这种问题在安卓上偶尔也会出现,鸿蒙上同样要留意。

5.2 图片加载不出来,先查权限再查地址

图片全屏查看器最尴尬的瞬间就是:弹层打开了,但图片区域一片空白。鸿蒙对网络权限的管理比早期Android更严格,如果鸿蒙工程里没有声明网络权限,网络图片是拉不下来的。

你需要检查鸿蒙工程里的module.json5或者对应的权限配置文件,确保包含:

{ "name": "ohos.permission.INTERNET" }

这个权限在 DevEco Studio 创建的默认模板里有时不会自动加上,需要手动确认。加了之后还没生效,再检查图片地址是不是 http、有没有走明文流量限制。真机调试时,我还遇到过因为模拟器和真机网络环境不一致导致的偶发加载失败,我的经验是:凡是和网络有关的问题,一律优先真机验证。

5.3 大图全屏的内存与性能问题

全屏查看器打开一张几MB的高清原图,内存占用会明显上升。尤其在图片来源是相册或者远程大文件的时候,连续打开几十张图,应用很容易被系统杀掉或者出现卡顿。

我常用的优化思路有这几点:

  1. 缩略图与高清图分离:列表页只加载低分辨率缩略图,全屏时加载最适合屏幕分辨率的高清图;
  2. 统一走缓存:如果项目里已经有图片缓存机制(比如社区常用的 glide 系缓存),优先发挥缓存作用,避免每次全屏都重新下载原图;
  3. 及时回收:Modal关闭后,把图片状态清空,不要保留大图引用;
  4. 降低图片体积:服务端在上传图片时就压缩到一个合理的最大宽度,比如 1920px,既能满足手机全屏清晰度,又不会把内存撑爆。

我见过一个项目把服务端的原图直接丢给客户端展示,一张图片十几MB,两者在性能和流量上的代价都很大。你在鸿蒙全屏查看这个功能上,最值得花的功夫不是代码,而是把图片资源规格定好。

6. 从能跑到好用:全屏查看器的工程化收尾

6.1 抽成一个可复用的ImageViewer组件

当你把全屏查看器写完之后,肯定不希望每个页面都复制一遍那一堆 Modal、PanResponder、Image 的代码。把它抽成一个独立组件,是工程化第一步。

我会把组件的对外接口设计成这样:

type ImageViewerProps = { visible: boolean; imageUrl: string; onClose: () => void; placeholderColor?: string; allowSwipeDownToDismiss?: boolean; }; export default function ImageViewer({ visible, imageUrl, onClose, placeholderColor = '#000', allowSwipeDownToDismiss = true, }: ImageViewerProps) { // ...全屏展示与手势逻辑 }

这样业务侧只负责传数据和监听关闭,你自己只需要维护一个公共图片预览组件。后续如果要在鸿蒙上针对底层渲染做适配,也只改这一个文件。

6.2 加上缓存、占位图和长图适配

一个面向真实项目的图片查看器,还需要考虑长图预览。长截图在手机上用contain模式会缩得非常小,根本看不清内容。处理长图的常见做法是:在图片加载后获取宽高比,如果图片是明显的高度远大于宽度,就允许图片保持在容器内垂直活动,用户可以通过上下滑动查看完整内容。

鸿蒙上 RN 的ScrollView可以配合图片实现这个效果,思路是把全屏里的 Image 包在一个ScrollView中,并限制图片初始显示高度不超过屏幕高度,超出的部分允许用户滚动。这个方案代码量不大,但能把“查看长截图”这种体验从“不可用”变成“可用”。

占位图的本质是在加载过程中给用户一个明确的视觉反馈。RN 内置的Image有defaultSource属性,可以指定本地占位图资源,这在本地图片加载时很好用。网络图片加载时我更习惯自己在状态机里控制,因为onLoadStart和onLoadEnd的回调时机更容易扩展。

6.3 后续还能扩展的周边能力

一个基础的图片全屏查看器做完之后,我列几个可以继续深入的方向,你按项目需求选择:

  • 图片轮播:把单图查看升级成多图左右切换,类似照片墙的浏览体验;
  • 保存到相册:鸿蒙上需要申请相册写入权限,再调用系统保存能力;
  • 分享:通过鸿蒙的分享能力把图片传给其他应用,注意系统要求的权限与格式约定;
  • 图片信息展示:在全屏时叠加显示图片尺寸、分辨率、拍摄时间等元数据;
  • 批量删除/收藏:在查看器里直接做业务操作,需要你把操作按钮和图片状态做联动。

我个人建议不要一次性把所有能力堆上去。以我的实际经验,把“查看”这条主链路做得流畅干净,比塞满按钮更让用户满意。

最后分享一个小技巧:在鸿蒙上调试 RN 项目时,Metro 的日志和 DevEco 的 Log 常常各自为战。我会把ReactNativeJS和JSBundle两个过滤词搭配使用,前者看业务日志,后者看加载状态,两边一对照,大多数白屏、加载失败和报错问题都能快速定位。等你把这些步骤走完,一个可用的图片全屏查看器就算真正落地了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 16:05:06

暴雪天远程办公全攻略:停电断网下的工作生存指南

暴雪预警又来了。手机上推送一条接一条&#xff0c;最开始还觉得挺浪漫&#xff0c;直到小区群开始有人发水管冻裂的照片&#xff0c;楼下超市的泡面货架被搬空&#xff0c;才意识到这一次不是闹着玩的。我居家办公三年多&#xff0c;经历过两次真正意义上的极端天气&#xff0…

作者头像 李华
网站建设 2026/9/29 16:05:04

GEO优化实战指南:让生成式引擎主动引用你的内容

1. 先弄懂生成式引擎根据什么决定“要不要引用你”1.1 从“十个蓝色链接”到“一段现成答案”&#xff0c;流量逻辑已经变了做GEO优化之前&#xff0c;你得先接受一个事实&#xff1a;用户获取信息的方式&#xff0c;已经发生了根本变化。过去做SEO&#xff0c;大家盯着的是搜索…

作者头像 李华
网站建设 2026/9/29 16:03:51

AI赋能实证论文写作:从拍脑袋到有数有据的完整指南

这几年我帮学生和企业团队改过不少实证研究论文&#xff0c;发现一个很普遍的问题&#xff1a;大家不是不会写&#xff0c;而是选题和论证全靠“拍脑袋”。导师问一句“为什么选这个题目”&#xff0c;回答往往是“感觉这个有意思”或“最近讨论多”&#xff1b;写到假设部分&a…

作者头像 李华
网站建设 2026/9/29 16:03:37

库加载机制深度解析:从静态库到动态库的搜索路径与排错实战

你有没有过这种经历&#xff1a;明明已经把库装到了系统里&#xff0c;程序却依然报错&#xff0c;说找不到某某模块。安装步骤分明是按着网上教程一个不落走的&#xff0c;可到了“加载”这一步&#xff0c;就是差那么临门一脚。这个场景我想所有开发的人都不陌生&#xff0c;…

作者头像 李华
网站建设 2026/9/29 16:03:33

基于深度学习的室内照明智能调节系统:从感知到执行的全链路实战

简介&#xff1a;这份文档面向建筑节能、智能照明与深度学习应用方向的研究者与工程技术人员&#xff0c;围绕室内照明高能耗问题&#xff0c;提出一套基于卷积神经网络的智能调节方案。系统由数据处理模块、光纤通信网络、多路控制开关、状态监测模块与显示单元构成&#xff0…

作者头像 李华
网站建设 2026/9/29 16:01:57

Android蓝牙音频协同机制:A2DP与SCO时序切换原理及实战避坑

1. 从一个真实的音频打架问题说起做Android蓝牙音频开发的朋友&#xff0c;大概率都遇到过这种场景&#xff1a;戴着蓝牙耳机听歌&#xff0c;突然来了一通电话&#xff0c;音乐停了&#xff0c;通话正常&#xff0c;挂断之后音乐又自动恢复。整个过程行云流水&#xff0c;用户…

作者头像 李华