HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

H5页面中的图片、视频加载缓慢

h5

问题现象
浏览应用内带有图片、视频的内容时,出现加载慢的现象。

背景知识
ArkWeb提供了Web组件,页面加载是Web组件的基本功能。可以在Web组件创建时指定默认加载的网络页面,后续需要变更页面,可以通过loadUrl()接口加载指定的网页。
图片资源加载优化:在应用开发中,当图片资源数量或大小超过特定阈值时,由于图片资源的格式需要通过CPU解压缩为纹理格式才能直接被GPU读取,将增加CPU的处理时间,导致图片加载延迟,此时应采用图片资源加载优化方案优化图片资源的处理。
ArkUI Inspector:DevEco Studio提供的布局分析工具,开发者可以借助它预览真机或模拟器中的UI效果,快速定位布局层级问题,也可以观察组件属性、不同组件之间的关系等。
点击放大

ArkWeb分析:DevEco Profiler提供的ArkWeb加载和丢帧等问题分析模板,可以结合ArkWeb执行流程的关键Trace点来定位问题发生的阶段。如果问题发生在渲染阶段,可以结合H:RosenWeb数据,线程运行状态以及帧渲染流程打点数据,进一步分析丢帧问题。
Web页面加载流程Trace关键点:

Web网页加载流程拆解

Trace关键字

应用收到点击事件

DispatchTouchEvent

Web组件初始化

NWebImpl|CreateNWeb

导航

H:LocalFrame::Navigate| url=https://xxx

创建URLLoader加载图片资源

H:URLLoader::URLLoader | url=https://xxx

启动请求

H:HttpCacheTransaction::Start | url=https://xxx

创建HttpStream

H:HttpNetworkTransaction::DoCreateStream |url=https://xxx

发送请求

H:HttpNetworkTransaction::DoSendRequestComplete | url=https://xxx

接收到响应头

H:HttpNetworkTransaction::DoReadHeadersComplete | url=https://xxx

开始接收响应体

H:NavigationBodyLoader::StartLoadingBody |url=https://xxx

数据接收完成

H:URLLoader::NotifyCompleted | responseCode | url=https://xxx

绘制

H:SingleThreadProxy::ScheduledActionSendBeginMainFrame

渲染

NotifyReadyToCommitOnImpl

完成时延结束

SkiaOutputSurfaceImplOnGpu::SwapBuffers

Web页资源加载全流程:
不同于HarmonyOS ArkUI页面,Web页面的所有信息均是通过URL链接获取,在本地解析成DOM结构后渲染为适配当前设备屏幕比例、可操控的页面。全流程分为三大部分:浏览器控制流程,资源下载流程,渲染送显流程。

点击放大
浏览器控制流程主要负责处理用户输入、创建Web组件、管控资源下载流程和渲染送显流程:
接收并处理用户输入:
接收用户点击跳转Web页的指令,根据相应的触摸事件逻辑或指定跳转命令,获得目的URL地址、渲染坐标等信息,初始化Web组件。

创建资源下载流程和渲染送显流程:
调用相关接口,将所需数据打包,用于初始化资源下载流程和渲染送显流程。

传递资源:
在网络进程将页面资源下载后,打包传递给渲染进程。

关闭本次加载并释放相应资源:
在渲染进程完成后,检查各进程状态并释放相应资源,关闭本次加载。

资源下载流程:
点击放大
启动请求,创建URLRequest:
网页加载管理器URLLoader将初始化一个含有当前网络参数的URLRequest,它将驱动一次完整的网络请求任务。

检查缓存项:
URLRequest将从网络请求中拿到cookies并检查是否存在缓存项,如果不存在则会新建。

通过HttpStream建立网络连接:
URLRequest请求一个已匹配缓存项的数据传输流进程HttpStream,并生成一个包含主机名、端口号等信息的socket。

根据DNS域名服务器返回的重定向路径,发送socket,完成与接收方的TCP连接。

发送请求,读取响应头head:
URLRequest通过传输接口格式化请求头和请求体后,发送到接收方。接收方会等待并接收响应头,向上传递给接收方的URLRequest并保存cookies。

读取响应体:
发送方URLRequest会创建一个数据管道,并将一端传递给接收方。发送方每次会先从管道获取一个新的共享内存,再通过URLRequest写入数据,直到数据全部发送完毕。

服务器端会接收到共享内存的读取请求并依次读取,直到读取完毕全部数据。

断开连接并销毁URLRequest:
读取完成后,将关闭TCP连接及与这个请求相关的进程,并保存可复用的socket,最后销毁URLRequest。

渲染流程:
将html转换为树形结构DOM树:
通过html解析器,分析并拆解html页面信息,将其重构为元素相互独立的DOM树结构。再根据元素在html页面中的分布位置和形态,分析出对应ArkUI组件及其具体属性值、坐标信息。

绘制准备:
通过图层树,实现层叠上下文,保证DOM重叠时可以用正确的顺序合成页面。再将每个图层的绘制拆分成很多小的绘制指令,组成待绘制列表。提交给合成器后会将图层划分为图块(大图层和小图层以相同大小的图块进行管理、缓存和渲染)。

渲染送显:
将图块信息转换为带有颜色透明度信息的位图(光栅化)。之后合成器会通过viz线程向GPU_task插入送显任务,将像素点输出到屏幕上。

问题定位
使用Ark Inspector查看应用布局,该页面布局为Web页面。在右侧搜索框输入content确认该网页地址。
点击放大

在浏览器中输入该网页地址,然后打开网络(Ctrl+U),可以看到图片/视频资源地址为"https://xxx"。

点击放大

使用DevEco Studio Profiler的ArkWeb分析抓取该图片/视频资源加载过程的Trace,再根据加载流程中的Trace关键字表拆分该图片/视频资源加载过程,可找到其加载过程及耗时。
以图片举例,具体步骤如下所示(对应图中红点标识):

首先将Trace关键字表中的对应关键字放入搜索框,例如“发送请求”:H:HttpNetworkTransaction::DoSendRequestComplete| url=https://xxx (改为对应的图片URL)。

然后在搜索结果中筛选目标,找出任意一次成功复现的节点。

最后放大该时间轴比例,直至可以清晰显示该线程的运行时长及其具体运行时间点。

点击放大

重复以上步骤,依次搜索“应用收到点击事件”、“发起请求”、“开始接收资源下载数据”、“资源下载完成”、“绘制”、“渲染完成”的Trace关键字,获知以下加载流程全景图:

点击放大

点击放大

选中A点时间坐标,拖向B点的时间坐标,即可标记该时间段,并显示其时长。依次标记所需时间段,可知:总时长2s,应用收到点击事件到开始接收响应体耗时137ms,下载全程耗时1.5s,渲染流程单独耗时337ms。由此可知,总时长过长的主要原因是过长的下载耗时。

点击放大

复制URL到浏览器,可打开下载该图片资源,发现该图片大小为3.2M,明显超出该场景表现所需的图片资源大小,在未经充分加载优化的情况下会导致加载耗时过长。

点击放大

分析结论
由于该图片/视频资源较大、未经过充分加载优化,导致出现加载缓慢的现象。
https://pastebin.com/QNRn2bjK
https://pastebin.com/hwbnfATi
https://pastebin.com/gAzAXLff
https://pastebin.com/VfQdx54p
https://pastebin.com/m1kt60bz
https://pastebin.com/Y9zewtFb
https://pastebin.com/NZ2QMAgn
https://pastebin.com/r1zJPik4
https://pastebin.com/RuXS6qcT
https://pastebin.com/MGtrMJwL
https://pastebin.com/XpN65jfS
https://pastebin.com/kxWz7LwQ
https://pastebin.com/DgAzrw6P
https://pastebin.com/bD6uV8t5
https://pastebin.com/EaSLm137
https://pastebin.com/8WvnTSR4
https://pastebin.com/NHTUPBV5
https://pastebin.com/zwr6rKV6
https://pastebin.com/VbQkGn6E
https://pastebin.com/418ZtBAb
https://pastebin.com/H3Nk02Bz
https://pastebin.com/vdi2RUcw
https://pastebin.com/e6WB7EyF
https://pastebin.com/kcAeSAKN
https://pastebin.com/T3t1yXbH
https://pastebin.com/2kzjNjPh
https://pastebin.com/BZEHW3GE
https://pastebin.com/riFvG5sv
https://pastebin.com/rq1Rt35d
https://pastebin.com/81zfaGBF
https://pastebin.com/HWj6grJT
https://pastebin.com/vwQtFAVv
https://pastebin.com/5ZE1KmsZ
https://pastebin.com/LzstGNxy
https://pastebin.com/ZqSsy7w0
https://pastebin.com/KJb7c7K4
https://pastebin.com/HCn7vnUt
https://pastebin.com/wAFYUfqT
https://pastebin.com/5gn7rZ5i
https://pastebin.com/fgErKAft
https://pastebin.com/bsgyzqGY
https://pastebin.com/wnJ7V3nb
https://pastebin.com/Ue4ci9LT
https://pastebin.com/qFTp5130
https://pastebin.com/DSxEQKav
https://pastebin.com/HrYaKKZS
https://pastebin.com/LDNChUMj
https://pastebin.com/jwBzr0Nq
https://pastebin.com/Y8CankGX
https://pastebin.com/jMuDwuFH
https://pastebin.com/B3aHx3P4
https://pastebin.com/cpUmKPEN
https://pastebin.com/ADZzerpL
https://pastebin.com/fdU1T7HH
https://pastebin.com/zVykfg7b
https://pastebin.com/7e4fAuMN
https://pastebin.com/u92VZShy
https://pastebin.com/BnB6cKvJ
https://pastebin.com/w6Yr9VB8
https://pastebin.com/kKE1AiEx
https://pastebin.com/v32qznLW
https://pastebin.com/tBQzSBqT
https://pastebin.com/eVuDgJzE
https://pastebin.com/2NPkE4kD
https://pastebin.com/Lnge5Pks
https://pastebin.com/4vuq5CFH
https://pastebin.com/qPzfcYwV
https://pastebin.com/MpK1mx7q
https://pastebin.com/2NFh64Ff
https://pastebin.com/A1Au9Phs
https://pastebin.com/aR3XMgi8
https://pastebin.com/9NDy94AJ
https://pastebin.com/nF6y4CM6
https://pastebin.com/snUJbCD4
https://pastebin.com/4221ZXt8
https://pastebin.com/96vLQn1S
https://pastebin.com/b4J1iVU5
https://pastebin.com/YRqJQZHn
https://pastebin.com/EHz8VFhH
https://pastebin.com/NCVFXb2E
https://pastebin.com/NhUJhfsJ
https://pastebin.com/4RbA0t9X
https://pastebin.com/tntHazTQ
https://pastebin.com/GeLTXqsa
https://pastebin.com/GEc14SUH
https://pastebin.com/98UxqNxj
https://pastebin.com/bXgwyxDS
https://pastebin.com/4a4zBHH9
https://pastebin.com/eVAEe1Bq
https://pastebin.com/NAVsusn4
https://pastebin.com/SSmEJFWc
https://pastebin.com/tgbeKpmw
https://pastebin.com/Sp7H2HF7
https://pastebin.com/sfE6aaHr
https://pastebin.com/2DZ4qnLe
https://pastebin.com/FZpnVJJM
https://pastebin.com/5Uncff3D
https://pastebin.com/Fiv90dVc
https://pastebin.com/nETky3vG
https://pastebin.com/FCYRxim3
https://pastebin.com/XZ0VcS3K
https://pastebin.com/RBuxUwxW
https://pastebin.com/Bba7JVAJ
https://pastebin.com/eJnQrngW
https://pastebin.com/BNpBq32h
https://pastebin.com/6AxaMU1T
https://pastebin.com/fsw2qeC0
https://pastebin.com/vDN7GuP6
https://pastebin.com/e72g4X5w
https://pastebin.com/ziybYwjQ
https://pastebin.com/6bUmWAWF
https://pastebin.com/CtBGzzJT
https://pastebin.com/LzYyEXGk
https://pastebin.com/aQpDbtqP
https://pastebin.com/AD40Arih
https://pastebin.com/t2cZzG8u
https://pastebin.com/w63u8Xq3
https://pastebin.com/fmaZHkGd
https://pastebin.com/2PMGAfKB
https://pastebin.com/7b8q6q48
https://pastebin.com/VfhdfPiN
https://pastebin.com/crmU54jF
https://pastebin.com/2SkQYm5Q
https://pastebin.com/CL8uckZw
https://pastebin.com/GBBP3d39
https://pastebin.com/W9UPpcg7
https://pastebin.com/FZ1WaffX
https://pastebin.com/GRPN2xFr
https://pastebin.com/fwtQ2RX0
https://pastebin.com/Jc9XRYm2
https://pastebin.com/maABnEXG
https://pastebin.com/bjPuX9pQ
https://pastebin.com/G9wCG67H
https://pastebin.com/f1yWEYvA
https://pastebin.com/dDYDV16c
https://pastebin.com/hW75kfej
https://pastebin.com/Pqgf8TBW
https://pastebin.com/e1xYhRPV
https://pastebin.com/yeGBY9BX
https://pastebin.com/3qyBsxEu
https://pastebin.com/UTNZXBmX
https://pastebin.com/SJZ4Ui0t
https://pastebin.com/VuHG278E
https://pastebin.com/CUuJ7N0B
https://pastebin.com/kusPdf8X
https://pastebin.com/N3YRkwXs
https://pastebin.com/15AS2ghw
https://pastebin.com/MTLW2jVp
https://pastebin.com/rzpuv3P7
https://pastebin.com/Z8CRch9q
https://pastebin.com/NqFarRyP
https://pastebin.com/i9k48G0n
https://pastebin.com/TWvwMyXC
https://pastebin.com/rvm7dsDL
https://pastebin.com/ED4TcSrY
https://pastebin.com/e9PLWh3t
https://pastebin.com/s2PuH6uv
https://pastebin.com/GrTLVWMe
https://pastebin.com/GrD1gY9i
https://pastebin.com/AGRV1q3A
https://pastebin.com/pKkNt9Ey
https://pastebin.com/pmGnKiCt
https://pastebin.com/mmStCkUc
https://pastebin.com/0Vjus1Tn
https://pastebin.com/dBWsMvS8
https://pastebin.com/srsJ0MtM
https://pastebin.com/kaLXpsUc
https://pastebin.com/FDgPZKBG
https://pastebin.com/qWTvEJSx
https://pastebin.com/hvG11U3X

继续阅读 »

问题现象
浏览应用内带有图片、视频的内容时,出现加载慢的现象。

背景知识
ArkWeb提供了Web组件,页面加载是Web组件的基本功能。可以在Web组件创建时指定默认加载的网络页面,后续需要变更页面,可以通过loadUrl()接口加载指定的网页。
图片资源加载优化:在应用开发中,当图片资源数量或大小超过特定阈值时,由于图片资源的格式需要通过CPU解压缩为纹理格式才能直接被GPU读取,将增加CPU的处理时间,导致图片加载延迟,此时应采用图片资源加载优化方案优化图片资源的处理。
ArkUI Inspector:DevEco Studio提供的布局分析工具,开发者可以借助它预览真机或模拟器中的UI效果,快速定位布局层级问题,也可以观察组件属性、不同组件之间的关系等。
点击放大

ArkWeb分析:DevEco Profiler提供的ArkWeb加载和丢帧等问题分析模板,可以结合ArkWeb执行流程的关键Trace点来定位问题发生的阶段。如果问题发生在渲染阶段,可以结合H:RosenWeb数据,线程运行状态以及帧渲染流程打点数据,进一步分析丢帧问题。
Web页面加载流程Trace关键点:

Web网页加载流程拆解

Trace关键字

应用收到点击事件

DispatchTouchEvent

Web组件初始化

NWebImpl|CreateNWeb

导航

H:LocalFrame::Navigate| url=https://xxx

创建URLLoader加载图片资源

H:URLLoader::URLLoader | url=https://xxx

启动请求

H:HttpCacheTransaction::Start | url=https://xxx

创建HttpStream

H:HttpNetworkTransaction::DoCreateStream |url=https://xxx

发送请求

H:HttpNetworkTransaction::DoSendRequestComplete | url=https://xxx

接收到响应头

H:HttpNetworkTransaction::DoReadHeadersComplete | url=https://xxx

开始接收响应体

H:NavigationBodyLoader::StartLoadingBody |url=https://xxx

数据接收完成

H:URLLoader::NotifyCompleted | responseCode | url=https://xxx

绘制

H:SingleThreadProxy::ScheduledActionSendBeginMainFrame

渲染

NotifyReadyToCommitOnImpl

完成时延结束

SkiaOutputSurfaceImplOnGpu::SwapBuffers

Web页资源加载全流程:
不同于HarmonyOS ArkUI页面,Web页面的所有信息均是通过URL链接获取,在本地解析成DOM结构后渲染为适配当前设备屏幕比例、可操控的页面。全流程分为三大部分:浏览器控制流程,资源下载流程,渲染送显流程。

点击放大
浏览器控制流程主要负责处理用户输入、创建Web组件、管控资源下载流程和渲染送显流程:
接收并处理用户输入:
接收用户点击跳转Web页的指令,根据相应的触摸事件逻辑或指定跳转命令,获得目的URL地址、渲染坐标等信息,初始化Web组件。

创建资源下载流程和渲染送显流程:
调用相关接口,将所需数据打包,用于初始化资源下载流程和渲染送显流程。

传递资源:
在网络进程将页面资源下载后,打包传递给渲染进程。

关闭本次加载并释放相应资源:
在渲染进程完成后,检查各进程状态并释放相应资源,关闭本次加载。

资源下载流程:
点击放大
启动请求,创建URLRequest:
网页加载管理器URLLoader将初始化一个含有当前网络参数的URLRequest,它将驱动一次完整的网络请求任务。

检查缓存项:
URLRequest将从网络请求中拿到cookies并检查是否存在缓存项,如果不存在则会新建。

通过HttpStream建立网络连接:
URLRequest请求一个已匹配缓存项的数据传输流进程HttpStream,并生成一个包含主机名、端口号等信息的socket。

根据DNS域名服务器返回的重定向路径,发送socket,完成与接收方的TCP连接。

发送请求,读取响应头head:
URLRequest通过传输接口格式化请求头和请求体后,发送到接收方。接收方会等待并接收响应头,向上传递给接收方的URLRequest并保存cookies。

读取响应体:
发送方URLRequest会创建一个数据管道,并将一端传递给接收方。发送方每次会先从管道获取一个新的共享内存,再通过URLRequest写入数据,直到数据全部发送完毕。

服务器端会接收到共享内存的读取请求并依次读取,直到读取完毕全部数据。

断开连接并销毁URLRequest:
读取完成后,将关闭TCP连接及与这个请求相关的进程,并保存可复用的socket,最后销毁URLRequest。

渲染流程:
将html转换为树形结构DOM树:
通过html解析器,分析并拆解html页面信息,将其重构为元素相互独立的DOM树结构。再根据元素在html页面中的分布位置和形态,分析出对应ArkUI组件及其具体属性值、坐标信息。

绘制准备:
通过图层树,实现层叠上下文,保证DOM重叠时可以用正确的顺序合成页面。再将每个图层的绘制拆分成很多小的绘制指令,组成待绘制列表。提交给合成器后会将图层划分为图块(大图层和小图层以相同大小的图块进行管理、缓存和渲染)。

渲染送显:
将图块信息转换为带有颜色透明度信息的位图(光栅化)。之后合成器会通过viz线程向GPU_task插入送显任务,将像素点输出到屏幕上。

问题定位
使用Ark Inspector查看应用布局,该页面布局为Web页面。在右侧搜索框输入content确认该网页地址。
点击放大

在浏览器中输入该网页地址,然后打开网络(Ctrl+U),可以看到图片/视频资源地址为"https://xxx"。

点击放大

使用DevEco Studio Profiler的ArkWeb分析抓取该图片/视频资源加载过程的Trace,再根据加载流程中的Trace关键字表拆分该图片/视频资源加载过程,可找到其加载过程及耗时。
以图片举例,具体步骤如下所示(对应图中红点标识):

首先将Trace关键字表中的对应关键字放入搜索框,例如“发送请求”:H:HttpNetworkTransaction::DoSendRequestComplete| url=https://xxx (改为对应的图片URL)。

然后在搜索结果中筛选目标,找出任意一次成功复现的节点。

最后放大该时间轴比例,直至可以清晰显示该线程的运行时长及其具体运行时间点。

点击放大

重复以上步骤,依次搜索“应用收到点击事件”、“发起请求”、“开始接收资源下载数据”、“资源下载完成”、“绘制”、“渲染完成”的Trace关键字,获知以下加载流程全景图:

点击放大

点击放大

选中A点时间坐标,拖向B点的时间坐标,即可标记该时间段,并显示其时长。依次标记所需时间段,可知:总时长2s,应用收到点击事件到开始接收响应体耗时137ms,下载全程耗时1.5s,渲染流程单独耗时337ms。由此可知,总时长过长的主要原因是过长的下载耗时。

点击放大

复制URL到浏览器,可打开下载该图片资源,发现该图片大小为3.2M,明显超出该场景表现所需的图片资源大小,在未经充分加载优化的情况下会导致加载耗时过长。

点击放大

分析结论
由于该图片/视频资源较大、未经过充分加载优化,导致出现加载缓慢的现象。
https://pastebin.com/QNRn2bjK
https://pastebin.com/hwbnfATi
https://pastebin.com/gAzAXLff
https://pastebin.com/VfQdx54p
https://pastebin.com/m1kt60bz
https://pastebin.com/Y9zewtFb
https://pastebin.com/NZ2QMAgn
https://pastebin.com/r1zJPik4
https://pastebin.com/RuXS6qcT
https://pastebin.com/MGtrMJwL
https://pastebin.com/XpN65jfS
https://pastebin.com/kxWz7LwQ
https://pastebin.com/DgAzrw6P
https://pastebin.com/bD6uV8t5
https://pastebin.com/EaSLm137
https://pastebin.com/8WvnTSR4
https://pastebin.com/NHTUPBV5
https://pastebin.com/zwr6rKV6
https://pastebin.com/VbQkGn6E
https://pastebin.com/418ZtBAb
https://pastebin.com/H3Nk02Bz
https://pastebin.com/vdi2RUcw
https://pastebin.com/e6WB7EyF
https://pastebin.com/kcAeSAKN
https://pastebin.com/T3t1yXbH
https://pastebin.com/2kzjNjPh
https://pastebin.com/BZEHW3GE
https://pastebin.com/riFvG5sv
https://pastebin.com/rq1Rt35d
https://pastebin.com/81zfaGBF
https://pastebin.com/HWj6grJT
https://pastebin.com/vwQtFAVv
https://pastebin.com/5ZE1KmsZ
https://pastebin.com/LzstGNxy
https://pastebin.com/ZqSsy7w0
https://pastebin.com/KJb7c7K4
https://pastebin.com/HCn7vnUt
https://pastebin.com/wAFYUfqT
https://pastebin.com/5gn7rZ5i
https://pastebin.com/fgErKAft
https://pastebin.com/bsgyzqGY
https://pastebin.com/wnJ7V3nb
https://pastebin.com/Ue4ci9LT
https://pastebin.com/qFTp5130
https://pastebin.com/DSxEQKav
https://pastebin.com/HrYaKKZS
https://pastebin.com/LDNChUMj
https://pastebin.com/jwBzr0Nq
https://pastebin.com/Y8CankGX
https://pastebin.com/jMuDwuFH
https://pastebin.com/B3aHx3P4
https://pastebin.com/cpUmKPEN
https://pastebin.com/ADZzerpL
https://pastebin.com/fdU1T7HH
https://pastebin.com/zVykfg7b
https://pastebin.com/7e4fAuMN
https://pastebin.com/u92VZShy
https://pastebin.com/BnB6cKvJ
https://pastebin.com/w6Yr9VB8
https://pastebin.com/kKE1AiEx
https://pastebin.com/v32qznLW
https://pastebin.com/tBQzSBqT
https://pastebin.com/eVuDgJzE
https://pastebin.com/2NPkE4kD
https://pastebin.com/Lnge5Pks
https://pastebin.com/4vuq5CFH
https://pastebin.com/qPzfcYwV
https://pastebin.com/MpK1mx7q
https://pastebin.com/2NFh64Ff
https://pastebin.com/A1Au9Phs
https://pastebin.com/aR3XMgi8
https://pastebin.com/9NDy94AJ
https://pastebin.com/nF6y4CM6
https://pastebin.com/snUJbCD4
https://pastebin.com/4221ZXt8
https://pastebin.com/96vLQn1S
https://pastebin.com/b4J1iVU5
https://pastebin.com/YRqJQZHn
https://pastebin.com/EHz8VFhH
https://pastebin.com/NCVFXb2E
https://pastebin.com/NhUJhfsJ
https://pastebin.com/4RbA0t9X
https://pastebin.com/tntHazTQ
https://pastebin.com/GeLTXqsa
https://pastebin.com/GEc14SUH
https://pastebin.com/98UxqNxj
https://pastebin.com/bXgwyxDS
https://pastebin.com/4a4zBHH9
https://pastebin.com/eVAEe1Bq
https://pastebin.com/NAVsusn4
https://pastebin.com/SSmEJFWc
https://pastebin.com/tgbeKpmw
https://pastebin.com/Sp7H2HF7
https://pastebin.com/sfE6aaHr
https://pastebin.com/2DZ4qnLe
https://pastebin.com/FZpnVJJM
https://pastebin.com/5Uncff3D
https://pastebin.com/Fiv90dVc
https://pastebin.com/nETky3vG
https://pastebin.com/FCYRxim3
https://pastebin.com/XZ0VcS3K
https://pastebin.com/RBuxUwxW
https://pastebin.com/Bba7JVAJ
https://pastebin.com/eJnQrngW
https://pastebin.com/BNpBq32h
https://pastebin.com/6AxaMU1T
https://pastebin.com/fsw2qeC0
https://pastebin.com/vDN7GuP6
https://pastebin.com/e72g4X5w
https://pastebin.com/ziybYwjQ
https://pastebin.com/6bUmWAWF
https://pastebin.com/CtBGzzJT
https://pastebin.com/LzYyEXGk
https://pastebin.com/aQpDbtqP
https://pastebin.com/AD40Arih
https://pastebin.com/t2cZzG8u
https://pastebin.com/w63u8Xq3
https://pastebin.com/fmaZHkGd
https://pastebin.com/2PMGAfKB
https://pastebin.com/7b8q6q48
https://pastebin.com/VfhdfPiN
https://pastebin.com/crmU54jF
https://pastebin.com/2SkQYm5Q
https://pastebin.com/CL8uckZw
https://pastebin.com/GBBP3d39
https://pastebin.com/W9UPpcg7
https://pastebin.com/FZ1WaffX
https://pastebin.com/GRPN2xFr
https://pastebin.com/fwtQ2RX0
https://pastebin.com/Jc9XRYm2
https://pastebin.com/maABnEXG
https://pastebin.com/bjPuX9pQ
https://pastebin.com/G9wCG67H
https://pastebin.com/f1yWEYvA
https://pastebin.com/dDYDV16c
https://pastebin.com/hW75kfej
https://pastebin.com/Pqgf8TBW
https://pastebin.com/e1xYhRPV
https://pastebin.com/yeGBY9BX
https://pastebin.com/3qyBsxEu
https://pastebin.com/UTNZXBmX
https://pastebin.com/SJZ4Ui0t
https://pastebin.com/VuHG278E
https://pastebin.com/CUuJ7N0B
https://pastebin.com/kusPdf8X
https://pastebin.com/N3YRkwXs
https://pastebin.com/15AS2ghw
https://pastebin.com/MTLW2jVp
https://pastebin.com/rzpuv3P7
https://pastebin.com/Z8CRch9q
https://pastebin.com/NqFarRyP
https://pastebin.com/i9k48G0n
https://pastebin.com/TWvwMyXC
https://pastebin.com/rvm7dsDL
https://pastebin.com/ED4TcSrY
https://pastebin.com/e9PLWh3t
https://pastebin.com/s2PuH6uv
https://pastebin.com/GrTLVWMe
https://pastebin.com/GrD1gY9i
https://pastebin.com/AGRV1q3A
https://pastebin.com/pKkNt9Ey
https://pastebin.com/pmGnKiCt
https://pastebin.com/mmStCkUc
https://pastebin.com/0Vjus1Tn
https://pastebin.com/dBWsMvS8
https://pastebin.com/srsJ0MtM
https://pastebin.com/kaLXpsUc
https://pastebin.com/FDgPZKBG
https://pastebin.com/qWTvEJSx
https://pastebin.com/hvG11U3X

收起阅读 »

用AI写了个uni-app插件,结果自己改了三版才跑通——聊聊AI编程的“靠谱度”边界

uni-app

背景:一个看似简单,实则处处是坑的需求

上个月接到一个任务:在 uni-app 项目中实现一个图片裁剪插件,支持缩放、旋转和拖拽。我第一反应是“这不简单吗?网上模板一大堆,让 AI 写一个不就完了?”于是,我打开常用的 AI 代码助手,把需求描述得清清楚楚:用 uni-app 写一个图片裁剪组件,支持手势缩放、旋转、拖拽,兼容 H5 和微信小程序。

AI 秒回一个完整的 vue 组件,代码结构清晰,注释到位。我心里一阵窃喜:这效率,以后谁还自己写?结果一跑,直接翻车。H5 端页面卡死,小程序端裁剪框歪了,旋转后图片飞出边界。更离谱的是,AI 生成的代码里,touch 事件直接用了 @touchmove.prevent,在微信小程序里压根不生效。

于是,我开始了“AI 写一版,我改一版”的循环。前后改了三个大版本,踩了无数坑,才算勉强跑通。这篇文章就来聊聊——AI 写 uni-app 插件到底靠不靠谱,以及我们该怎么用好它。

问题分析:AI 编程的“三大盲区”

在改代码的过程中,我逐渐摸清了 AI 的弱点。它擅长的是“拼凑”,而不是“适配”。

  1. 平台差异被忽略
    uni-app 最大的特点就是跨平台。AI 生成的代码往往默认“一套逻辑走天下”。比如,在 H5 端,touch 事件处理非常自由,可以用 e.touches[0] 直接取坐标;但在微信小程序里,touches 对象的 clientX 和 pageX 在不同组件层级下表现不同。AI 生成的裁剪组件,在小程序里拖拽时,坐标计算完全错位,因为没考虑到 uni.createSelectorQuery() 的异步特性。

  2. 边界情况全靠猜
    AI 会写“理想状态”的代码,但不会考虑用户的实际操作。比如,它生成的旋转逻辑是:每次旋转 90°,然后更新 transform。但用户如果连续快速旋转,或者先旋转再缩放,图片的 transform-origin 就会乱。AI 没有处理这种“状态叠加”的场景,导致图片在多次操作后直接飞出去。

  3. 性能优化完全缺失
    uni-app 的 H5 端和原生小程序端,性能模型完全不同。AI 生成的代码在 H5 端用 v-bind:style 动态绑定 transform,每次手指滑动都触发一次重渲染。这在 H5 上勉强能跑,但在小程序里,由于 setData 的异步机制和渲染层与逻辑层的通信开销,直接导致页面卡死。AI 不会主动考虑用 requestAnimationFrame 节流,也不会用 computed 缓存计算结果。

第一版:推倒重来,从“事件模型”开始改

第一版 AI 代码的核心逻辑是:在 touchstart 时记录初始位置,touchmove 时更新 transform: translate(X, Y) scale(S) rotate(R)。看起来没问题,但小程序里一跑,拖拽时有延迟,而且缩放后图片边界不对。

问题定位:AI 用了 @touchmove="onTouchMove",但在 onTouchMove 里直接计算并赋值给 this.style。在小程序里,touchmove 事件触发频率极高(每秒几十次),每次都要执行 setData 更新样式,性能瓶颈就在这里。

解决方案:改用 uni.createSelectorQuery() 获取组件边界,然后用 requestAnimationFrame 节流。具体来说:

javascript
// 节流函数,控制更新频率
let ticking = false;
function onTouchMove(e) {
if (!ticking) {
requestAnimationFrame(() => {
updateTransform(e.touches[0]);
ticking = false;
});
ticking = true;
}
}

同时,把 transform 的计算结果缓存到 computed 属性里,只在手势结束时才一次性写入 data。这样,setData 的调用次数从每秒几十次降到每秒一次。

踩坑记录:requestAnimationFrame 在微信小程序里是支持的,但要注意,它返回的是一个 number 类型的 ID,不能用 cancelAnimationFrame 直接取消(因为小程序的实现有差异)。我的做法是:用一个 flag 变量控制,而不是依赖 cancelAnimationFrame。

第二版:旋转与缩放的“叠加噩梦”

第二版改完,拖拽和缩放基本 ok,但旋转加入后,问题又来了。用户先旋转 90°,再缩放 2x,图片位置会跳变。原因是 AI 的旋转逻辑是:每次旋转后,重新计算 transform: translate(x, y) scale(s) rotate(r),但 x 和 y 是基于初始坐标的,旋转后图片的视觉中心变了,translate 的偏移量却没更新。

问题定位:旋转改变了图片的坐标系,但 translate 的偏移量还是基于原始坐标系计算的。比如,图片中心在 (100, 100),旋转 90° 后,中心点还在 (100, 100),但图片的视觉边界变了。AI 没有考虑“旋转后中心点偏移”这个细节。

解决方案:改用“矩阵变换”的思路,而不是直接操作 translate、scale、rotate。因为 transform: matrix() 可以统一处理所有变换,而且能保持坐标系的连续性。

javascript
// 使用矩阵变换,把所有操作叠加到一个 matrix 上
let currentMatrix = [1, 0, 0, 1, 0, 0]; // [a, b, c, d, tx, ty]

function applyRotation(angle) {
const rad = angle Math.PI / 180;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
// 矩阵乘法:旋转矩阵
当前矩阵
currentMatrix = [
cos currentMatrix[0] + sin currentMatrix[2],
cos currentMatrix[1] + sin currentMatrix[3],
-sin currentMatrix[0] + cos currentMatrix[2],
-sin currentMatrix[1] + cos currentMatrix[3],
currentMatrix[4],
currentMatrix[5]
];
updateStyle();
}

踩坑记录:矩阵变换虽然精准,但在 uni-app 里,transform: matrix() 的兼容性要特别注意。微信小程序支持 matrix,但 H5 端的某些旧版浏览器可能不支持。我的做法是:在 onReady 里用 uni.getSystemInfoSync() 判断平台,如果是 H5 且版本低,就回退到 translate + scale + rotate 的分步写法。

第三版:裁剪框的“边界锁定”与“用户体验”

前两版跑通后,第三版主要解决裁剪框的边界锁定问题。AI 生成的代码里,用户可以把图片拖到裁剪框外面,然后裁剪出空白区域。这显然不行。

问题定位:AI 只计算了图片的 transform 值,但没有计算图片在裁剪框内的有效显示区域。它不会自动判断“图片边缘是否超出裁剪框边界”,更不会做“边界回弹”。

解决方案:在每次手势结束时,计算图片的四个角在裁剪框坐标系中的坐标,然后做边界约束。

javascript
function clampBounds() {
// 获取裁剪框的尺寸(假设已知 width, height)
const cropRect = { x: 0, y: 0, w: cropWidth, h: cropHeight };
// 获取图片当前变换后的四个角坐标(通过矩阵计算)
const corners = getImageCorners(currentMatrix, imageWidth, imageHeight);
// 计算需要偏移的量,保证所有角都在裁剪框内
let dx = 0, dy = 0;
for (const corner of corners) {
if (corner.x cropRect.x + cropRect.w) dx = Math.min(dx, cropRect.x + cropRect.w - corner.x);
if (corner.y cropRect.y + cropRect.h) dy = Math.min(dy, cropRect.y + cropRect.h - corner.y);
}
// 更新矩阵的平移部分
currentMatrix[4] += dx;
currentMatrix[5] += dy;
updateStyle();
}

用户体验优化:边界锁定后,用户如果拖动到边缘,会感觉“卡死了”。我加了弹性效果:当图片边缘超出边界时,用 transition: all 0.2s ease-out 让图片“弹回”边界内。这样用户操作时,手感更自然。

踩坑记录:弹性效果在 H5 端用 CSS transition 就行,但在小程序里,transition 对 transform 的支持有限(特别是微信小程序的旧版基础库)。我的做法是:用 uni.createAnimation() 创建动画实例,手动控制回弹的缓动效果。

注意事项:AI 编程的“靠谱度”边界总结

通过这次三版迭代,我总结了几条 AI 编程的“靠谱度”边界,供大家参考:

  1. AI 擅长的:快速生成“骨架代码”,比如组件的基本结构、事件绑定、样式模板。这些工作重复性高,AI 效率极高。
  2. AI 不擅长的:处理平台差异、边界情况、性能优化、用户体验细节。这些需要开发者对 uni-app 的底层机制有深入理解。
  3. 建议做法:让 AI 写 70% 的代码(基础功能 + 模板),自己写 30% 的“硬核”部分(事件处理、状态管理、性能优化)。不要期望 AI 能输出一个生产级的插件。
  4. 测试策略:AI 生成的代码,一定要在“最小环境”里测试,比如单独创建一个空白 uni-app 项目,只引入这个组件,测试 H5 和微信小程序两个端。等基础功能 ok,再集成到实际项目中。

总结:AI 是“实习生”,不是“主力”

这次经历让我从一个“AI 万能论者”变成了“AI 实用主义者”。AI 能帮我们省下写模板的时间,但真正决定代码质量的,还是我们自己对 uni-app 的理解深度。比如,你是否知道 uni-app 的 touch 事件在 H5 和小程序里的坐标系差异?你是否了解 requestAnimationFrame 在跨平台下的兼容性?这些知识,AI 不会主动告诉你,只能靠你自己去踩坑、去总结。

如果你也想用 AI 写 uni-app 插件,我的建议是:先让 AI 写一个“玩具版”,然后基于这个“玩具版”去迭代。不要指望一次成型。每次迭代,你都会发现自己对问题的理解更深一层。这,可能才是 AI 编程真正有价值的地方——它逼着我们思考那些原本会被忽略的细节。

继续阅读 »

背景:一个看似简单,实则处处是坑的需求

上个月接到一个任务:在 uni-app 项目中实现一个图片裁剪插件,支持缩放、旋转和拖拽。我第一反应是“这不简单吗?网上模板一大堆,让 AI 写一个不就完了?”于是,我打开常用的 AI 代码助手,把需求描述得清清楚楚:用 uni-app 写一个图片裁剪组件,支持手势缩放、旋转、拖拽,兼容 H5 和微信小程序。

AI 秒回一个完整的 vue 组件,代码结构清晰,注释到位。我心里一阵窃喜:这效率,以后谁还自己写?结果一跑,直接翻车。H5 端页面卡死,小程序端裁剪框歪了,旋转后图片飞出边界。更离谱的是,AI 生成的代码里,touch 事件直接用了 @touchmove.prevent,在微信小程序里压根不生效。

于是,我开始了“AI 写一版,我改一版”的循环。前后改了三个大版本,踩了无数坑,才算勉强跑通。这篇文章就来聊聊——AI 写 uni-app 插件到底靠不靠谱,以及我们该怎么用好它。

问题分析:AI 编程的“三大盲区”

在改代码的过程中,我逐渐摸清了 AI 的弱点。它擅长的是“拼凑”,而不是“适配”。

  1. 平台差异被忽略
    uni-app 最大的特点就是跨平台。AI 生成的代码往往默认“一套逻辑走天下”。比如,在 H5 端,touch 事件处理非常自由,可以用 e.touches[0] 直接取坐标;但在微信小程序里,touches 对象的 clientX 和 pageX 在不同组件层级下表现不同。AI 生成的裁剪组件,在小程序里拖拽时,坐标计算完全错位,因为没考虑到 uni.createSelectorQuery() 的异步特性。

  2. 边界情况全靠猜
    AI 会写“理想状态”的代码,但不会考虑用户的实际操作。比如,它生成的旋转逻辑是:每次旋转 90°,然后更新 transform。但用户如果连续快速旋转,或者先旋转再缩放,图片的 transform-origin 就会乱。AI 没有处理这种“状态叠加”的场景,导致图片在多次操作后直接飞出去。

  3. 性能优化完全缺失
    uni-app 的 H5 端和原生小程序端,性能模型完全不同。AI 生成的代码在 H5 端用 v-bind:style 动态绑定 transform,每次手指滑动都触发一次重渲染。这在 H5 上勉强能跑,但在小程序里,由于 setData 的异步机制和渲染层与逻辑层的通信开销,直接导致页面卡死。AI 不会主动考虑用 requestAnimationFrame 节流,也不会用 computed 缓存计算结果。

第一版:推倒重来,从“事件模型”开始改

第一版 AI 代码的核心逻辑是:在 touchstart 时记录初始位置,touchmove 时更新 transform: translate(X, Y) scale(S) rotate(R)。看起来没问题,但小程序里一跑,拖拽时有延迟,而且缩放后图片边界不对。

问题定位:AI 用了 @touchmove="onTouchMove",但在 onTouchMove 里直接计算并赋值给 this.style。在小程序里,touchmove 事件触发频率极高(每秒几十次),每次都要执行 setData 更新样式,性能瓶颈就在这里。

解决方案:改用 uni.createSelectorQuery() 获取组件边界,然后用 requestAnimationFrame 节流。具体来说:

javascript
// 节流函数,控制更新频率
let ticking = false;
function onTouchMove(e) {
if (!ticking) {
requestAnimationFrame(() => {
updateTransform(e.touches[0]);
ticking = false;
});
ticking = true;
}
}

同时,把 transform 的计算结果缓存到 computed 属性里,只在手势结束时才一次性写入 data。这样,setData 的调用次数从每秒几十次降到每秒一次。

踩坑记录:requestAnimationFrame 在微信小程序里是支持的,但要注意,它返回的是一个 number 类型的 ID,不能用 cancelAnimationFrame 直接取消(因为小程序的实现有差异)。我的做法是:用一个 flag 变量控制,而不是依赖 cancelAnimationFrame。

第二版:旋转与缩放的“叠加噩梦”

第二版改完,拖拽和缩放基本 ok,但旋转加入后,问题又来了。用户先旋转 90°,再缩放 2x,图片位置会跳变。原因是 AI 的旋转逻辑是:每次旋转后,重新计算 transform: translate(x, y) scale(s) rotate(r),但 x 和 y 是基于初始坐标的,旋转后图片的视觉中心变了,translate 的偏移量却没更新。

问题定位:旋转改变了图片的坐标系,但 translate 的偏移量还是基于原始坐标系计算的。比如,图片中心在 (100, 100),旋转 90° 后,中心点还在 (100, 100),但图片的视觉边界变了。AI 没有考虑“旋转后中心点偏移”这个细节。

解决方案:改用“矩阵变换”的思路,而不是直接操作 translate、scale、rotate。因为 transform: matrix() 可以统一处理所有变换,而且能保持坐标系的连续性。

javascript
// 使用矩阵变换,把所有操作叠加到一个 matrix 上
let currentMatrix = [1, 0, 0, 1, 0, 0]; // [a, b, c, d, tx, ty]

function applyRotation(angle) {
const rad = angle Math.PI / 180;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
// 矩阵乘法:旋转矩阵
当前矩阵
currentMatrix = [
cos currentMatrix[0] + sin currentMatrix[2],
cos currentMatrix[1] + sin currentMatrix[3],
-sin currentMatrix[0] + cos currentMatrix[2],
-sin currentMatrix[1] + cos currentMatrix[3],
currentMatrix[4],
currentMatrix[5]
];
updateStyle();
}

踩坑记录:矩阵变换虽然精准,但在 uni-app 里,transform: matrix() 的兼容性要特别注意。微信小程序支持 matrix,但 H5 端的某些旧版浏览器可能不支持。我的做法是:在 onReady 里用 uni.getSystemInfoSync() 判断平台,如果是 H5 且版本低,就回退到 translate + scale + rotate 的分步写法。

第三版:裁剪框的“边界锁定”与“用户体验”

前两版跑通后,第三版主要解决裁剪框的边界锁定问题。AI 生成的代码里,用户可以把图片拖到裁剪框外面,然后裁剪出空白区域。这显然不行。

问题定位:AI 只计算了图片的 transform 值,但没有计算图片在裁剪框内的有效显示区域。它不会自动判断“图片边缘是否超出裁剪框边界”,更不会做“边界回弹”。

解决方案:在每次手势结束时,计算图片的四个角在裁剪框坐标系中的坐标,然后做边界约束。

javascript
function clampBounds() {
// 获取裁剪框的尺寸(假设已知 width, height)
const cropRect = { x: 0, y: 0, w: cropWidth, h: cropHeight };
// 获取图片当前变换后的四个角坐标(通过矩阵计算)
const corners = getImageCorners(currentMatrix, imageWidth, imageHeight);
// 计算需要偏移的量,保证所有角都在裁剪框内
let dx = 0, dy = 0;
for (const corner of corners) {
if (corner.x cropRect.x + cropRect.w) dx = Math.min(dx, cropRect.x + cropRect.w - corner.x);
if (corner.y cropRect.y + cropRect.h) dy = Math.min(dy, cropRect.y + cropRect.h - corner.y);
}
// 更新矩阵的平移部分
currentMatrix[4] += dx;
currentMatrix[5] += dy;
updateStyle();
}

用户体验优化:边界锁定后,用户如果拖动到边缘,会感觉“卡死了”。我加了弹性效果:当图片边缘超出边界时,用 transition: all 0.2s ease-out 让图片“弹回”边界内。这样用户操作时,手感更自然。

踩坑记录:弹性效果在 H5 端用 CSS transition 就行,但在小程序里,transition 对 transform 的支持有限(特别是微信小程序的旧版基础库)。我的做法是:用 uni.createAnimation() 创建动画实例,手动控制回弹的缓动效果。

注意事项:AI 编程的“靠谱度”边界总结

通过这次三版迭代,我总结了几条 AI 编程的“靠谱度”边界,供大家参考:

  1. AI 擅长的:快速生成“骨架代码”,比如组件的基本结构、事件绑定、样式模板。这些工作重复性高,AI 效率极高。
  2. AI 不擅长的:处理平台差异、边界情况、性能优化、用户体验细节。这些需要开发者对 uni-app 的底层机制有深入理解。
  3. 建议做法:让 AI 写 70% 的代码(基础功能 + 模板),自己写 30% 的“硬核”部分(事件处理、状态管理、性能优化)。不要期望 AI 能输出一个生产级的插件。
  4. 测试策略:AI 生成的代码,一定要在“最小环境”里测试,比如单独创建一个空白 uni-app 项目,只引入这个组件,测试 H5 和微信小程序两个端。等基础功能 ok,再集成到实际项目中。

总结:AI 是“实习生”,不是“主力”

这次经历让我从一个“AI 万能论者”变成了“AI 实用主义者”。AI 能帮我们省下写模板的时间,但真正决定代码质量的,还是我们自己对 uni-app 的理解深度。比如,你是否知道 uni-app 的 touch 事件在 H5 和小程序里的坐标系差异?你是否了解 requestAnimationFrame 在跨平台下的兼容性?这些知识,AI 不会主动告诉你,只能靠你自己去踩坑、去总结。

如果你也想用 AI 写 uni-app 插件,我的建议是:先让 AI 写一个“玩具版”,然后基于这个“玩具版”去迭代。不要指望一次成型。每次迭代,你都会发现自己对问题的理解更深一层。这,可能才是 AI 编程真正有价值的地方——它逼着我们思考那些原本会被忽略的细节。

收起阅读 »

如何解决XComponent无法触发触摸事件问题

问题现象
在XComponent组件中绑定onTouch方法,但发现该方法中的日志未被输出,表明onTouch事件未能正常触发。

问题参考如下:

Column() {
XComponent({
id: 'xcomponentId-00',
type: "12314",
libraryname: 'nativerender'
})
.onTouch((event: TouchEvent) => {
console.info(onTouch ${event.type})
})
}
.height(CommonConstants.XCOMPONENT_HEIGHT)
.width('100%')
背景知识
XComponent接口提供用于图形绘制和媒体数据写入的Surface,XComponent负责将其嵌入到视图中,支持应用自定义Surface位置和大小。当XComponentType参数为SURFACE或TEXTURE时,支持onTouch等通用事件。

解决方案
当问题代码中使用XComponent(value: {id: string, type: XComponentType, libraryname?: string, controller?: XComponentController})接口时,若配置了libraryname参数,点击事件、触摸事件、按键事件等通用事件将仅响应C-API侧的事件接口,因此问题代码中的onTouch事件无法被触发。
为使onTouch事件正常响应,建议改用不含libraryname参数的XComponent(options: XComponentOptions)接口进行配置。
@Entry
@Component
struct TouchEventDemo {
xComponentController: XComponentController = new XComponentController();
private aiController: ImageAnalyzerController = new ImageAnalyzerController();
private options: ImageAIOptions = {
types: [ImageAnalyzerType.SUBJECT, ImageAnalyzerType.TEXT],
aiController: this.aiController
};

build() {
RelativeContainer() {
Column() {
XComponent({
type: XComponentType.SURFACE,
controller: this.xComponentController,
imageAIOptions: this.options
})
.onTouch((event: TouchEvent) => {
console.info(onTouch ${event.type});
})
}
.height('100%')
.width('100%')
}
}
}
https://pastebin.com/hvG11U3X
https://pastebin.com/5vptr8Vj
https://pastebin.com/spr7PWcm
https://pastebin.com/tUag4haX
https://pastebin.com/jzrFTCja
https://pastebin.com/tfu0SQsU
https://pastebin.com/2JeU38Mk
https://pastebin.com/eKgfgpUT
https://pastebin.com/73u8nze7
https://pastebin.com/4R6vjQ2e
https://pastebin.com/Ft9TAMiA
https://pastebin.com/skk2MENv
https://pastebin.com/C8J9MCsS
https://pastebin.com/D8eS2eVs
https://pastebin.com/RaU1cpC8
https://pastebin.com/wMA4np3d
https://pastebin.com/ZU1mhEvu
https://pastebin.com/5aUaBFzL
https://pastebin.com/KCTNuVQP
https://pastebin.com/PFhGqsLd
https://pastebin.com/CqqNRshL
https://pastebin.com/xyiRj81f
https://pastebin.com/156bZ7kc
https://pastebin.com/pKpShzm2
https://pastebin.com/h7PuBnh2
https://pastebin.com/8UcYTqbk
https://pastebin.com/xkMGBUL8
https://pastebin.com/BivPc7y2
https://pastebin.com/JGtxHhde
https://pastebin.com/BjDgBxnN
https://pastebin.com/ap8KeUuq
https://pastebin.com/xHKEb7rn
https://pastebin.com/DM1qdNRY
https://pastebin.com/vtxzFrXD
https://pastebin.com/aQdD2bLf
https://pastebin.com/d0ugdU4E
https://pastebin.com/Kygm5PDF
https://pastebin.com/mvAFf2cH
https://pastebin.com/wyUkyjiR
https://pastebin.com/riLYiEup
https://pastebin.com/gyyG9xim
https://pastebin.com/92KWyBzX
https://pastebin.com/SYxiX3vk
https://pastebin.com/ccQnUpjp
https://pastebin.com/9itPbhvZ
https://pastebin.com/kK5SsjRL
https://pastebin.com/fKJVLMK4
https://pastebin.com/2bRA2tTD
https://pastebin.com/bK56A1Ji
https://pastebin.com/qZdZ0ZJc
https://pastebin.com/QkWkCjEe
https://pastebin.com/uuKEVPS5
https://pastebin.com/0SqY91RV
https://pastebin.com/64YFs9BU
https://pastebin.com/KXZMGD1y
https://pastebin.com/cmDJHdy8
https://pastebin.com/XhikpFRA
https://pastebin.com/a967PHpL
https://pastebin.com/82SJ8KES
https://pastebin.com/JGUMK4Nx
https://pastebin.com/k65Apwb6
https://pastebin.com/9MKp2Bps
https://pastebin.com/dZ3k4mTn
https://pastebin.com/PgdZ425U
https://pastebin.com/ecG58Lav
https://pastebin.com/N0BBP0u7
https://pastebin.com/yHRL54ZE
https://pastebin.com/U0xa6Y0G
https://pastebin.com/UTMgGYNa
https://pastebin.com/R0zbC3rT
https://pastebin.com/97HSLFJD
https://pastebin.com/9khsxUzA
https://pastebin.com/9fNLJNN7
https://pastebin.com/4WNM1pZx
https://pastebin.com/Tw5GufjZ
https://pastebin.com/Wt26ty6F
https://pastebin.com/inqkXD0y
https://pastebin.com/ni9a0CDf
https://pastebin.com/p7MJC9KZ
https://pastebin.com/vAY8apDR
https://pastebin.com/Arn9MVZD
https://pastebin.com/QjR2Tbc9
https://pastebin.com/pZJsQkbP
https://pastebin.com/34a3s54B
https://pastebin.com/Qd8jWSJy
https://pastebin.com/5ULyrRin
https://pastebin.com/Mysz4mvi
https://pastebin.com/NSCN6QCt
https://pastebin.com/73SimwCg
https://pastebin.com/dkxTW70g
https://pastebin.com/YiqBc7eL
https://pastebin.com/T0Mi3Rtw
https://pastebin.com/5gxVnwcS
https://pastebin.com/4fcAGJqe
https://pastebin.com/DpHm4jGV
https://pastebin.com/3pgkL5s1
https://pastebin.com/xxXpzDjG
https://pastebin.com/a7GwePbx
https://pastebin.com/TiwMauwm
https://pastebin.com/Bdv3mYgA
https://pastebin.com/16hgsDGD
https://pastebin.com/VG4LgHSx
https://pastebin.com/M66rtgJR
https://pastebin.com/HtKqFQsB
https://pastebin.com/bbXWPnfj
https://pastebin.com/rWUN3huF
https://pastebin.com/UcTg7hJ1
https://pastebin.com/dn8Xx3GV
https://pastebin.com/1FP6Ntcf
https://pastebin.com/51v1RYdK
https://pastebin.com/P7UWVTwF
https://pastebin.com/Ft0AhqBE
https://pastebin.com/ZzVNTgPn
https://pastebin.com/sNjruc5F
https://pastebin.com/jMpMTVV8
https://pastebin.com/ci0DJWN1
https://pastebin.com/xxGwHDYx
https://pastebin.com/pW2P4G7e
https://pastebin.com/Zt4KZFQJ
https://pastebin.com/1zS2gHeb
https://pastebin.com/JHuHqTqf
https://pastebin.com/SwWKxw4j
https://pastebin.com/5PL1YwjX
https://pastebin.com/RrDicvTi
https://pastebin.com/UazzqSb2
https://pastebin.com/S1gyeG3P
https://pastebin.com/GKQbJJv2
https://pastebin.com/CJjdP4B3
https://pastebin.com/GB663WX1
https://pastebin.com/RQ19buAq
https://pastebin.com/148WQqS3
https://pastebin.com/U2qz1gVU
https://pastebin.com/ZeTTswjH
https://pastebin.com/vJfCU6ua
https://pastebin.com/sJgFKYdu
https://pastebin.com/F1Sj7ehQ
https://pastebin.com/CAYPv3Wv
https://pastebin.com/QC2VFSwf
https://pastebin.com/WRgpuuNh
https://pastebin.com/12Kn9u92
https://pastebin.com/yE8AK7QP
https://pastebin.com/XbqQm6PP
https://pastebin.com/WJmTxCVM
https://pastebin.com/NPNT49x6
https://pastebin.com/U1xYBpaB
https://pastebin.com/CAJ57UsS
https://pastebin.com/vqnVBxTA
https://pastebin.com/pHTHBxzP
https://pastebin.com/bqL71TeY
https://pastebin.com/wu4su4Fw
https://pastebin.com/sn6Dt1qx
https://pastebin.com/TQSHKCdX
https://pastebin.com/QbqNaMzW
https://pastebin.com/ntxUbuAm
https://pastebin.com/mTKE6ebR
https://pastebin.com/BWfdfKAF
https://pastebin.com/5i405zXJ
https://pastebin.com/tMKCxf2E
https://pastebin.com/SxfitKct

继续阅读 »

问题现象
在XComponent组件中绑定onTouch方法,但发现该方法中的日志未被输出,表明onTouch事件未能正常触发。

问题参考如下:

Column() {
XComponent({
id: 'xcomponentId-00',
type: "12314",
libraryname: 'nativerender'
})
.onTouch((event: TouchEvent) => {
console.info(onTouch ${event.type})
})
}
.height(CommonConstants.XCOMPONENT_HEIGHT)
.width('100%')
背景知识
XComponent接口提供用于图形绘制和媒体数据写入的Surface,XComponent负责将其嵌入到视图中,支持应用自定义Surface位置和大小。当XComponentType参数为SURFACE或TEXTURE时,支持onTouch等通用事件。

解决方案
当问题代码中使用XComponent(value: {id: string, type: XComponentType, libraryname?: string, controller?: XComponentController})接口时,若配置了libraryname参数,点击事件、触摸事件、按键事件等通用事件将仅响应C-API侧的事件接口,因此问题代码中的onTouch事件无法被触发。
为使onTouch事件正常响应,建议改用不含libraryname参数的XComponent(options: XComponentOptions)接口进行配置。
@Entry
@Component
struct TouchEventDemo {
xComponentController: XComponentController = new XComponentController();
private aiController: ImageAnalyzerController = new ImageAnalyzerController();
private options: ImageAIOptions = {
types: [ImageAnalyzerType.SUBJECT, ImageAnalyzerType.TEXT],
aiController: this.aiController
};

build() {
RelativeContainer() {
Column() {
XComponent({
type: XComponentType.SURFACE,
controller: this.xComponentController,
imageAIOptions: this.options
})
.onTouch((event: TouchEvent) => {
console.info(onTouch ${event.type});
})
}
.height('100%')
.width('100%')
}
}
}
https://pastebin.com/hvG11U3X
https://pastebin.com/5vptr8Vj
https://pastebin.com/spr7PWcm
https://pastebin.com/tUag4haX
https://pastebin.com/jzrFTCja
https://pastebin.com/tfu0SQsU
https://pastebin.com/2JeU38Mk
https://pastebin.com/eKgfgpUT
https://pastebin.com/73u8nze7
https://pastebin.com/4R6vjQ2e
https://pastebin.com/Ft9TAMiA
https://pastebin.com/skk2MENv
https://pastebin.com/C8J9MCsS
https://pastebin.com/D8eS2eVs
https://pastebin.com/RaU1cpC8
https://pastebin.com/wMA4np3d
https://pastebin.com/ZU1mhEvu
https://pastebin.com/5aUaBFzL
https://pastebin.com/KCTNuVQP
https://pastebin.com/PFhGqsLd
https://pastebin.com/CqqNRshL
https://pastebin.com/xyiRj81f
https://pastebin.com/156bZ7kc
https://pastebin.com/pKpShzm2
https://pastebin.com/h7PuBnh2
https://pastebin.com/8UcYTqbk
https://pastebin.com/xkMGBUL8
https://pastebin.com/BivPc7y2
https://pastebin.com/JGtxHhde
https://pastebin.com/BjDgBxnN
https://pastebin.com/ap8KeUuq
https://pastebin.com/xHKEb7rn
https://pastebin.com/DM1qdNRY
https://pastebin.com/vtxzFrXD
https://pastebin.com/aQdD2bLf
https://pastebin.com/d0ugdU4E
https://pastebin.com/Kygm5PDF
https://pastebin.com/mvAFf2cH
https://pastebin.com/wyUkyjiR
https://pastebin.com/riLYiEup
https://pastebin.com/gyyG9xim
https://pastebin.com/92KWyBzX
https://pastebin.com/SYxiX3vk
https://pastebin.com/ccQnUpjp
https://pastebin.com/9itPbhvZ
https://pastebin.com/kK5SsjRL
https://pastebin.com/fKJVLMK4
https://pastebin.com/2bRA2tTD
https://pastebin.com/bK56A1Ji
https://pastebin.com/qZdZ0ZJc
https://pastebin.com/QkWkCjEe
https://pastebin.com/uuKEVPS5
https://pastebin.com/0SqY91RV
https://pastebin.com/64YFs9BU
https://pastebin.com/KXZMGD1y
https://pastebin.com/cmDJHdy8
https://pastebin.com/XhikpFRA
https://pastebin.com/a967PHpL
https://pastebin.com/82SJ8KES
https://pastebin.com/JGUMK4Nx
https://pastebin.com/k65Apwb6
https://pastebin.com/9MKp2Bps
https://pastebin.com/dZ3k4mTn
https://pastebin.com/PgdZ425U
https://pastebin.com/ecG58Lav
https://pastebin.com/N0BBP0u7
https://pastebin.com/yHRL54ZE
https://pastebin.com/U0xa6Y0G
https://pastebin.com/UTMgGYNa
https://pastebin.com/R0zbC3rT
https://pastebin.com/97HSLFJD
https://pastebin.com/9khsxUzA
https://pastebin.com/9fNLJNN7
https://pastebin.com/4WNM1pZx
https://pastebin.com/Tw5GufjZ
https://pastebin.com/Wt26ty6F
https://pastebin.com/inqkXD0y
https://pastebin.com/ni9a0CDf
https://pastebin.com/p7MJC9KZ
https://pastebin.com/vAY8apDR
https://pastebin.com/Arn9MVZD
https://pastebin.com/QjR2Tbc9
https://pastebin.com/pZJsQkbP
https://pastebin.com/34a3s54B
https://pastebin.com/Qd8jWSJy
https://pastebin.com/5ULyrRin
https://pastebin.com/Mysz4mvi
https://pastebin.com/NSCN6QCt
https://pastebin.com/73SimwCg
https://pastebin.com/dkxTW70g
https://pastebin.com/YiqBc7eL
https://pastebin.com/T0Mi3Rtw
https://pastebin.com/5gxVnwcS
https://pastebin.com/4fcAGJqe
https://pastebin.com/DpHm4jGV
https://pastebin.com/3pgkL5s1
https://pastebin.com/xxXpzDjG
https://pastebin.com/a7GwePbx
https://pastebin.com/TiwMauwm
https://pastebin.com/Bdv3mYgA
https://pastebin.com/16hgsDGD
https://pastebin.com/VG4LgHSx
https://pastebin.com/M66rtgJR
https://pastebin.com/HtKqFQsB
https://pastebin.com/bbXWPnfj
https://pastebin.com/rWUN3huF
https://pastebin.com/UcTg7hJ1
https://pastebin.com/dn8Xx3GV
https://pastebin.com/1FP6Ntcf
https://pastebin.com/51v1RYdK
https://pastebin.com/P7UWVTwF
https://pastebin.com/Ft0AhqBE
https://pastebin.com/ZzVNTgPn
https://pastebin.com/sNjruc5F
https://pastebin.com/jMpMTVV8
https://pastebin.com/ci0DJWN1
https://pastebin.com/xxGwHDYx
https://pastebin.com/pW2P4G7e
https://pastebin.com/Zt4KZFQJ
https://pastebin.com/1zS2gHeb
https://pastebin.com/JHuHqTqf
https://pastebin.com/SwWKxw4j
https://pastebin.com/5PL1YwjX
https://pastebin.com/RrDicvTi
https://pastebin.com/UazzqSb2
https://pastebin.com/S1gyeG3P
https://pastebin.com/GKQbJJv2
https://pastebin.com/CJjdP4B3
https://pastebin.com/GB663WX1
https://pastebin.com/RQ19buAq
https://pastebin.com/148WQqS3
https://pastebin.com/U2qz1gVU
https://pastebin.com/ZeTTswjH
https://pastebin.com/vJfCU6ua
https://pastebin.com/sJgFKYdu
https://pastebin.com/F1Sj7ehQ
https://pastebin.com/CAYPv3Wv
https://pastebin.com/QC2VFSwf
https://pastebin.com/WRgpuuNh
https://pastebin.com/12Kn9u92
https://pastebin.com/yE8AK7QP
https://pastebin.com/XbqQm6PP
https://pastebin.com/WJmTxCVM
https://pastebin.com/NPNT49x6
https://pastebin.com/U1xYBpaB
https://pastebin.com/CAJ57UsS
https://pastebin.com/vqnVBxTA
https://pastebin.com/pHTHBxzP
https://pastebin.com/bqL71TeY
https://pastebin.com/wu4su4Fw
https://pastebin.com/sn6Dt1qx
https://pastebin.com/TQSHKCdX
https://pastebin.com/QbqNaMzW
https://pastebin.com/ntxUbuAm
https://pastebin.com/mTKE6ebR
https://pastebin.com/BWfdfKAF
https://pastebin.com/5i405zXJ
https://pastebin.com/tMKCxf2E
https://pastebin.com/SxfitKct

收起阅读 »

uni-app 国际化问题配置示例

国际化 i18n 多语言

如有更好的解决方法,欢迎大家评论和指正。

  • 目前存在的问题

    1. IOS平台 通用启动界面的应用名称,会使用回退语言,没有实现国际化配置,可参考iOS平台自定义storyboard启动界面
    2. 还有目前没有涉及到的国际化,如:uniCloud和Android、IOS的strings 等。

  • 项目根目录下 locale 文件夹

    ├── locale  
    │   ├── en.json  
    │   ├── index.js  
    │   ├── // 如果有其他语言,如日语,则添加 ja.json  
    │   ├── uni-app.en.json  
    │   ├── // 如果有其他语言,如日语,则添加 uni-app.ja.json  
    │   ├── uni-app.zh-Hans.json  
    │   ├── uni-app.zh-Hant.json  
    │   ├── zh-Hans.json  
    │   └── zh-Hant.json  

    这里做下简单说明:

    1. uni-app.*.json 这些文件,决定了手机系统桌面应用名称、默认启动图显示名称和系统默认 tabbar 定义的国际化;
    2. uni-app.*.json 使用跟官方文档一致 如:%app.name%%privacyDescription.NSBluetoothAlwaysUsageDescription%

    1. index.js

      import en from './en.json'  
      import zhHans from './zh-Hans.json'  
      import zhHant from './zh-Hant.json'  
      // import ja from './ja.json'  
      export default {  
          en,  
          'zh-Hans': zhHans,  
          'zh-Hant': zhHant,  
          // 如果有其他语言,如日语,则添加 ja  
      }  
    2. en.json

      {  
          "tabbar.index": "Home",  
          "tabbar.list": "List",  
          "tabbar.me": "Me",  
      
          // ... 其他字段  
      }  
    3. zh-Hans.json

      {  
          "tabbar.index": "首页",  
          "tabbar.list": "列表",  
          "tabbar.me": "我的",  
      
          // ... 其他字段  
      }  
    4. zh-Hant.json

      {  
          "tabbar.index": "首頁",  
          "tabbar.list": "清單",  
          "tabbar.me": "我的",  
      
          // ... 其他字段  
      }  

    配置应用名称和IOS平台配置隐私权限描述国际化,还有一种方式官方提供的 manifest.json国际化配置【本文章末尾】

    注意: 我试过在uni-app.en.json中把隐私权限描述写到下面ios内打包,没有效果

      // 这里是错误说明,不要这样写,正确写法是下面的 uni-app.en.json 示例  
      {  
          "common": {  
              "app.name": "app name",  
              "app.description": "app description",  
          },  
          "ios": {  
              "privacyDescription":{  
                  "NSBluetoothAlwaysUsageDescription": "Requires access to your Bluetooth to function properly",  
                  "NSBluetoothPeripheralUsageDescription": "Requires access to your Bluetooth to function properly"  
              }  
          },  
          "android": {}  
      }  

    1. uni-app.en.json

      {  
          "common": {  
              "app.name": "app name",  
              "app.description": "app description",  
      
              "privacyDescription.NSBluetoothAlwaysUsageDescription": "Requires access to your Bluetooth to function properly",  
              "privacyDescription.NSBluetoothPeripheralUsageDescription": "Requires access to your Bluetooth to function properly"  
          },  
          "ios": {},  
          "android": {}  
      }  
    2. uni-app.zh-Hans.json

      {  
          "common": {  
              "app.name": "应用名称",  
              "app.description": "应用描述",  
      
              "privacyDescription.NSBluetoothAlwaysUsageDescription": "需要访问您的蓝牙以正常工作",  
              "privacyDescription.NSBluetoothPeripheralUsageDescription": "需要访问您的蓝牙以正常工作"  
          },  
          "ios": {},  
          "android": {}  
      }  
    3. uni-app.zh-Hant.json

      {  
          "common": {  
              "app.name": "應用名稱",  
              "app.description": "應用描述",  
      
              "privacyDescription.NSBluetoothAlwaysUsageDescription": "需要訪問您的藍牙以正常工作",  
              "privacyDescription.NSBluetoothPeripheralUsageDescription": "需要訪問您的藍牙以正常工作"  
          },  
          "ios": {},  
          "android": {}  
      }  
  • manifest.json

  1. 配置 manifest.json 的默认语言 在 manifest.json 中设置 locale 为 "auto",这是跟随系统语言的关键。

    {  
        "name": "%app.name%",  
        "appid": "__your_appid__",  
        "locale": "auto" // 设置为 auto,跟随系统 os 的语言  
        "fallbackLocale" : "en" // 默认回退语言 这个根据自己需求设置  
        // ... 其他配置  
    }  
  2. 配置 android 和 ios 原生平台支持的语言列表 (关键步骤) 这一步在标准基座下由 dcloud 预先完成,但你的应用必须自行配置,否则打包后系统无法识别你的应用支持哪些语言,通常会回退到默认语言 (如英文)。【这里不提供详细示例,可以直接看官方文档中的示例】

    {  
        "app-plus" : {  
            "locales": {  
                "en": {}, // 英文  
                "zh": {}, // 中文  (特别注意)  
                "zh-Hans": {}, // 简体中文  
                "zh-Hant": {}, // 繁体中文  
                // 如果有其他语言,如日语,则添加 "ja": {}  
            }  
        }  
    }  

    关于"zh": {} 这个配置说明:

    1. 如果这个"zh": {}不配置,当系统语言是【简体中文】,应用名称和隐私权限描述会回退到【默认语言】,而不是使用【简体中文】-搞不懂为什么
    2. 虽然官方文档语言代码中提到locales下的键名(key)可以取值示例,没有zh-Hanszh-Hant两个键名,但是实际测试中,其可以正常工作,其他键名会回退到【默认语言】,而不是当前【系统语言】
继续阅读 »

如有更好的解决方法,欢迎大家评论和指正。

  • 目前存在的问题

    1. IOS平台 通用启动界面的应用名称,会使用回退语言,没有实现国际化配置,可参考iOS平台自定义storyboard启动界面
    2. 还有目前没有涉及到的国际化,如:uniCloud和Android、IOS的strings 等。

  • 项目根目录下 locale 文件夹

    ├── locale  
    │   ├── en.json  
    │   ├── index.js  
    │   ├── // 如果有其他语言,如日语,则添加 ja.json  
    │   ├── uni-app.en.json  
    │   ├── // 如果有其他语言,如日语,则添加 uni-app.ja.json  
    │   ├── uni-app.zh-Hans.json  
    │   ├── uni-app.zh-Hant.json  
    │   ├── zh-Hans.json  
    │   └── zh-Hant.json  

    这里做下简单说明:

    1. uni-app.*.json 这些文件,决定了手机系统桌面应用名称、默认启动图显示名称和系统默认 tabbar 定义的国际化;
    2. uni-app.*.json 使用跟官方文档一致 如:%app.name%%privacyDescription.NSBluetoothAlwaysUsageDescription%

    1. index.js

      import en from './en.json'  
      import zhHans from './zh-Hans.json'  
      import zhHant from './zh-Hant.json'  
      // import ja from './ja.json'  
      export default {  
          en,  
          'zh-Hans': zhHans,  
          'zh-Hant': zhHant,  
          // 如果有其他语言,如日语,则添加 ja  
      }  
    2. en.json

      {  
          "tabbar.index": "Home",  
          "tabbar.list": "List",  
          "tabbar.me": "Me",  
      
          // ... 其他字段  
      }  
    3. zh-Hans.json

      {  
          "tabbar.index": "首页",  
          "tabbar.list": "列表",  
          "tabbar.me": "我的",  
      
          // ... 其他字段  
      }  
    4. zh-Hant.json

      {  
          "tabbar.index": "首頁",  
          "tabbar.list": "清單",  
          "tabbar.me": "我的",  
      
          // ... 其他字段  
      }  

    配置应用名称和IOS平台配置隐私权限描述国际化,还有一种方式官方提供的 manifest.json国际化配置【本文章末尾】

    注意: 我试过在uni-app.en.json中把隐私权限描述写到下面ios内打包,没有效果

      // 这里是错误说明,不要这样写,正确写法是下面的 uni-app.en.json 示例  
      {  
          "common": {  
              "app.name": "app name",  
              "app.description": "app description",  
          },  
          "ios": {  
              "privacyDescription":{  
                  "NSBluetoothAlwaysUsageDescription": "Requires access to your Bluetooth to function properly",  
                  "NSBluetoothPeripheralUsageDescription": "Requires access to your Bluetooth to function properly"  
              }  
          },  
          "android": {}  
      }  

    1. uni-app.en.json

      {  
          "common": {  
              "app.name": "app name",  
              "app.description": "app description",  
      
              "privacyDescription.NSBluetoothAlwaysUsageDescription": "Requires access to your Bluetooth to function properly",  
              "privacyDescription.NSBluetoothPeripheralUsageDescription": "Requires access to your Bluetooth to function properly"  
          },  
          "ios": {},  
          "android": {}  
      }  
    2. uni-app.zh-Hans.json

      {  
          "common": {  
              "app.name": "应用名称",  
              "app.description": "应用描述",  
      
              "privacyDescription.NSBluetoothAlwaysUsageDescription": "需要访问您的蓝牙以正常工作",  
              "privacyDescription.NSBluetoothPeripheralUsageDescription": "需要访问您的蓝牙以正常工作"  
          },  
          "ios": {},  
          "android": {}  
      }  
    3. uni-app.zh-Hant.json

      {  
          "common": {  
              "app.name": "應用名稱",  
              "app.description": "應用描述",  
      
              "privacyDescription.NSBluetoothAlwaysUsageDescription": "需要訪問您的藍牙以正常工作",  
              "privacyDescription.NSBluetoothPeripheralUsageDescription": "需要訪問您的藍牙以正常工作"  
          },  
          "ios": {},  
          "android": {}  
      }  
  • manifest.json

  1. 配置 manifest.json 的默认语言 在 manifest.json 中设置 locale 为 "auto",这是跟随系统语言的关键。

    {  
        "name": "%app.name%",  
        "appid": "__your_appid__",  
        "locale": "auto" // 设置为 auto,跟随系统 os 的语言  
        "fallbackLocale" : "en" // 默认回退语言 这个根据自己需求设置  
        // ... 其他配置  
    }  
  2. 配置 android 和 ios 原生平台支持的语言列表 (关键步骤) 这一步在标准基座下由 dcloud 预先完成,但你的应用必须自行配置,否则打包后系统无法识别你的应用支持哪些语言,通常会回退到默认语言 (如英文)。【这里不提供详细示例,可以直接看官方文档中的示例】

    {  
        "app-plus" : {  
            "locales": {  
                "en": {}, // 英文  
                "zh": {}, // 中文  (特别注意)  
                "zh-Hans": {}, // 简体中文  
                "zh-Hant": {}, // 繁体中文  
                // 如果有其他语言,如日语,则添加 "ja": {}  
            }  
        }  
    }  

    关于"zh": {} 这个配置说明:

    1. 如果这个"zh": {}不配置,当系统语言是【简体中文】,应用名称和隐私权限描述会回退到【默认语言】,而不是使用【简体中文】-搞不懂为什么
    2. 虽然官方文档语言代码中提到locales下的键名(key)可以取值示例,没有zh-Hanszh-Hant两个键名,但是实际测试中,其可以正常工作,其他键名会回退到【默认语言】,而不是当前【系统语言】
收起阅读 »

安卓sdk插件封装

插件开发 插件需求

现在有3个安卓原生sdk,分别是
1-称重的,
2-ai识别物品的,
3-人脸识别的,

要封装成插件,让我们在uniapp开发的app中调用。
能接的v:xxxx1024xxxx1024

现在有3个安卓原生sdk,分别是
1-称重的,
2-ai识别物品的,
3-人脸识别的,

要封装成插件,让我们在uniapp开发的app中调用。
能接的v:xxxx1024xxxx1024

使用openCustomDialog实现菜单弹窗并返回选中项回调

问题现象
如何使用openCustomDialog实现一个从外部传参的菜单弹窗,并且可以在外部监听菜单项的选中状态。

效果预览
点击放大

背景知识
不依赖UI组件的全局自定义弹出框 (openCustomDialog)存在两种入参方式创建自定义弹出框,ComponentContent和Builder:

ComponentContent封装内容可以与UI界面解耦,调用更加灵活。
使用Builder的方式,必须要与上下文做绑定,与UI存在一定耦合,具体请参考:不依赖UI组件的全局自定义弹出框 (openCustomDialog)的说明部分和@Builder装饰器使用说明。
解决方案
参考完整示例实现弹窗工具类,其核心是以下属性的具体实现:
ctx:获取UI上下文用于实现弹窗服务。
contentNode:构建弹窗的内容节点,其中弹窗中菜单项的个数,内容和传给外部监听的回调通过Params类型的数组传递。
options:弹窗的配置选项,如位置、样式、动画等。
export class CustomerPromptAction {
static ctx: UIContext;
static contentNode: ComponentContent<Object>;
static options: promptAction.BaseDialogOptions;

// 定义方法,从外部传入上下文。
static setContext(context: UIContext) {
CustomerPromptAction.ctx = context;
}

// 配置内容节点。
static setContentNode(context: UIContext, para: Params[]) {
let contentNode: ComponentContent<Object> = new ComponentContent(context, wrapBuilder(buildText), para);
CustomerPromptAction.contentNode = contentNode;
}

// 弹窗配置项。
static setOptions(options: promptAction.BaseDialogOptions) {
CustomerPromptAction.options = options;
}

// 定义弹窗打开的方法。
static openDialog(options: promptAction.BaseDialogOptions, context: UIContext, para: Params[]) {
CustomerPromptAction.setContext(context);
CustomerPromptAction.setContentNode(context, para);
CustomerPromptAction.setOptions({ alignment: DialogAlignment.Bottom, offset: { dx: 0, dy: -20 } });
if (CustomerPromptAction.contentNode !== null) {
CustomerPromptAction.ctx.getPromptAction()
.openCustomDialog(CustomerPromptAction.contentNode, options)
.then(() => {
})
.catch(() => {
});
}
}

// 关闭弹窗。
static closeDialog() {
if (CustomerPromptAction.contentNode !== null) {
CustomerPromptAction.ctx.getPromptAction()
.closeCustomDialog(CustomerPromptAction.contentNode)
.then(() => {
})
.catch(() => {
});
}
}
}
在页面中调用弹窗。
自定义菜单项实体类,声明属性方法,并通过构造器实例化。
export class Params {
title: string = "";
iconName: Resource;
click: (index: number) => void;

constructor(title: string, iconName: Resource, click: (index: number) => void) {
this.title = title;
this.iconName = iconName;
this.click = click;
}
}
定义接收菜单项选中回调的函数,根据返回的index处理逻辑。
// 点击弹窗中按钮的回调,并根据返回的index判断逻辑。
callBack = (index: number) => {
switch (index) {
case 0:
this.getUIContext().getPromptAction().showToast({
message: '点击了关闭',
duration: 2000
});
break;
case 1:
this.getUIContext().getPromptAction().showToast({
message: '点击了刷新',
duration: 2000
});
break;
case 2:
this.getUIContext().getPromptAction().showToast({
message: '点击了帮助',
duration: 2000
});
break;
}
};
初始化菜单数据源。
@State arr: Params[] = [
new Params('关闭', $r('app.media.startIcon'), this.callBack),
new Params('刷新', $r('app.media.startIcon'), this.callBack),
new Params('帮助', $r('app.media.startIcon'), this.callBack)];
完整示例参考如下:

CustomerPromptAction.ets。
import { ComponentContent, promptAction } from '@kit.ArkUI';
import { UIContext } from '@ohos.arkui.UIContext';
import { Params } from './Params';

@Builder
function buildText(params: Params[]) {
Column() {
Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceAround }) {
ForEach(params, (item: Params, index: number) => {
Column() {
Image(item.iconName)
.width(30)
.height(30)
.margin({ top: 20 })
.onClick(() => {
item.click(index);
});
Text(item.title)
.fontSize(15)
.margin({ top: 20 });
};
});
}
.width('100%');

Text('取消')  
  .margin({ top: 20 })  
  .height(50)  
  .width('100%')  
  .textAlign(TextAlign.Center)  
  .fontColor(Color.Blue)  
  .onClick(() => {  
    CustomerPromptAction.closeDialog();  
  });  

}
.backgroundColor('#FFF0F0F0')
.width('100%')
.borderRadius(10);
}
export class CustomerPromptAction {
static ctx: UIContext;
static contentNode: ComponentContent<Object>;
static options: promptAction.BaseDialogOptions;

// 定义方法,从外部传入上下文。
static setContext(context: UIContext) {
CustomerPromptAction.ctx = context;
}

// 配置内容节点。
static setContentNode(context: UIContext, para: Params[]) {
let contentNode: ComponentContent<Object> = new ComponentContent(context, wrapBuilder(buildText), para);
CustomerPromptAction.contentNode = contentNode;
}

// 弹窗配置项。
static setOptions(options: promptAction.BaseDialogOptions) {
CustomerPromptAction.options = options;
}

// 定义弹窗打开的方法。
static openDialog(options: promptAction.BaseDialogOptions, context: UIContext, para: Params[]) {
CustomerPromptAction.setContext(context);
CustomerPromptAction.setContentNode(context, para);
CustomerPromptAction.setOptions({ alignment: DialogAlignment.Bottom, offset: { dx: 0, dy: -20 } });
if (CustomerPromptAction.contentNode !== null) {
CustomerPromptAction.ctx.getPromptAction()
.openCustomDialog(CustomerPromptAction.contentNode, options)
.then(() => {
})
.catch(() => {
});
}
}

// 关闭弹窗。
static closeDialog() {
if (CustomerPromptAction.contentNode !== null) {
CustomerPromptAction.ctx.getPromptAction()
.closeCustomDialog(CustomerPromptAction.contentNode)
.then(() => {
})
.catch(() => {
});
}
}
}
Index.ets。
import { Params } from './Params';
import { CustomerPromptAction } from './CustomerPromptAction';

@Entry
@Component
struct Index {
// 点击弹窗中按钮的回调,并根据返回的index判断逻辑。
callBack = (index: number) => {
switch (index) {
case 0:
this.getUIContext().getPromptAction().showToast({
message: '点击了关闭',
duration: 2000
});
break;
case 1:
this.getUIContext().getPromptAction().showToast({
message: '点击了刷新',
duration: 2000
});
break;
case 2:
this.getUIContext().getPromptAction().showToast({
message: '点击了帮助',
duration: 2000
});
break;
}
};
// 初始化弹窗菜单的数据源。
@State arr: Params[] = [
new Params('关闭', $r('app.media.startIcon'), this.callBack),
new Params('刷新', $r('app.media.startIcon'), this.callBack),
new Params('帮助', $r('app.media.startIcon'), this.callBack)];

build() {
Row() {
Column() {
Button('横向菜单')
.margin({ top: 50 })
.onClick(() => {
CustomerPromptAction.openDialog({
alignment: DialogAlignment.Bottom,
offset: { dx: 0, dy: -20 }
}, this.getUIContext(), this.arr);
});
}
.width('100%')
.height('100%');
}
.height('100%');
}
}
Params.ets。
export class Params {
title: string = "";
iconName: Resource;
click: (index: number) => void;

constructor(title: string, iconName: Resource, click: (index: number) => void) {
this.title = title;
this.iconName = iconName;
this.click = click;
}
}
https://pastebin.com/epc9DuYu
https://pastebin.com/CErz3YL5
https://pastebin.com/6YfF0p9z
https://pastebin.com/kvpAvpdi
https://pastebin.com/X9fq64a2
https://pastebin.com/R0mRDZm4
https://pastebin.com/urWyLjrt
https://pastebin.com/hcKewAUE
https://pastebin.com/MjLnmvNu
https://pastebin.com/DYMeHxN4
https://pastebin.com/L5C77sX1
https://pastebin.com/rM22qRKb
https://pastebin.com/s8kwwq5k
https://pastebin.com/A6qfadrP
https://pastebin.com/PmbE57qe
https://pastebin.com/FiuhbGfN
https://pastebin.com/B2XYkbnq
https://pastebin.com/e0q36QBZ
https://pastebin.com/VwfcTkgK
https://pastebin.com/STYB1p4a
https://pastebin.com/dL8avv7N
https://pastebin.com/WgRdkJJY
https://pastebin.com/MHmf76dA
https://pastebin.com/2b2X9tbY
https://pastebin.com/uVqiYmpP
https://pastebin.com/iWFWeXfu
https://pastebin.com/tcz08Ga5
https://pastebin.com/rFqwzRhK
https://pastebin.com/bCm1D1Wj
https://pastebin.com/EWbGJZcy
https://pastebin.com/R30cJtHA
https://pastebin.com/aV9QTvJL
https://pastebin.com/EF75B0Xs
https://pastebin.com/UFQ2LwDh
https://pastebin.com/2CK4bM77
https://pastebin.com/ksCNHaF7
https://pastebin.com/k61KcBd4
https://pastebin.com/5GUXdSsP
https://pastebin.com/byn4kM9X
https://pastebin.com/nLG9ztxg
https://pastebin.com/PGrz7isp
https://pastebin.com/Jdv6Vcs1
https://pastebin.com/wc3q2WXq
https://pastebin.com/7JM3Dt9d
https://pastebin.com/PKueg3Q9
https://pastebin.com/C8hvWEnm
https://pastebin.com/nXYRTagp
https://pastebin.com/rzgJVLh2
https://pastebin.com/GXM3nwFL
https://pastebin.com/t0GkNTPB
https://pastebin.com/SGvqUZUw
https://pastebin.com/8SfWYWuA
https://pastebin.com/Cy2FKdCM
https://pastebin.com/Cwi3azHM
https://pastebin.com/WkBmAyzu
https://pastebin.com/NtcjEH8D
https://pastebin.com/vPFjfiTV
https://pastebin.com/VWQEns27
https://pastebin.com/W6sGgxN4
https://pastebin.com/BRTkHWLB
https://pastebin.com/b7kSTqmz
https://pastebin.com/TKxKXExY
https://pastebin.com/WbG5WLAC
https://pastebin.com/kNf5AwB7
https://pastebin.com/cgRrpGud
https://pastebin.com/TqFJkVxd
https://pastebin.com/Vwrjm3w7
https://pastebin.com/4ZMkzUBr
https://pastebin.com/GBW5cMYt
https://pastebin.com/RmGU8Ame
https://pastebin.com/RJ11SAm3
https://pastebin.com/kQrLCYA9
https://pastebin.com/9EH4eaY8
https://pastebin.com/YacTrPKQ
https://pastebin.com/DBYCLtGD
https://pastebin.com/y9eemXwr
https://pastebin.com/jTRFengj
https://pastebin.com/5vE7dpsX
https://pastebin.com/Kk4bZ1L8
https://pastebin.com/Kaxbk4tA
https://pastebin.com/LGR3sxN9
https://pastebin.com/2jbBiu33
https://pastebin.com/p0i2Fj9p
https://pastebin.com/uThxW0MS
https://pastebin.com/hr1MtNuu
https://pastebin.com/nt6fFaYk
https://pastebin.com/9RMs3tAN
https://pastebin.com/3pp9A2SP
https://pastebin.com/fbQQkXb2
https://pastebin.com/RNXkhP0Y
https://pastebin.com/62GMDSK1
https://pastebin.com/A47VdXsL
https://pastebin.com/TMXrdhxC
https://pastebin.com/pZV96ZWh
https://pastebin.com/BnZ4zhH2
https://pastebin.com/hqGmeuVV
https://pastebin.com/aqnxjmby
https://pastebin.com/uNxvnAFc
https://pastebin.com/tkDrfMPT
https://pastebin.com/6ufWP0fd
https://pastebin.com/H9PDFDiL
https://pastebin.com/qM9xgF7K
https://pastebin.com/iyJTH6aB
https://pastebin.com/ZiJyZLfp
https://pastebin.com/yZ0LA5dB
https://pastebin.com/8YX63tah
https://pastebin.com/v6DvRcJ2
https://pastebin.com/dQSyGA1C
https://pastebin.com/VLfUFJ3F
https://pastebin.com/1dHdGtmp
https://pastebin.com/ufJFhNjk
https://pastebin.com/t1WuAJPZ
https://pastebin.com/pZUndcCA
https://pastebin.com/ZPDUXz25
https://pastebin.com/ULjPJmEH
https://pastebin.com/EhwMqtVW
https://pastebin.com/EMCGkgwT
https://pastebin.com/60tHCxEM
https://pastebin.com/c4nYDLJs
https://pastebin.com/gQSdXcnY
https://pastebin.com/jc1GvWkA
https://pastebin.com/EVjzT0Bn
https://pastebin.com/hE2m4nfV
https://pastebin.com/yf3kYzc4
https://pastebin.com/Cj6MtRgk
https://pastebin.com/e7Hmp2wM
https://pastebin.com/0HzG8VJj
https://pastebin.com/XSTJdK3z
https://pastebin.com/mnfsGkiu
https://pastebin.com/REx1hsyn
https://pastebin.com/50VxQZdS
https://pastebin.com/0mxMQAfy
https://pastebin.com/AtGFtGFZ
https://pastebin.com/eWZmH6hX
https://pastebin.com/68JVfXcf
https://pastebin.com/Q47SWvBD
https://pastebin.com/pRR9gzGf
https://pastebin.com/KiMStGV8
https://pastebin.com/k8vp0yy7
https://pastebin.com/tN4ChEUp
https://pastebin.com/7pAZ3CdD
https://pastebin.com/rpSjghL8
https://pastebin.com/dYFDgUM2
https://pastebin.com/NQyE3NAS
https://pastebin.com/LYbXc4ma
https://pastebin.com/naCsLg7e
https://pastebin.com/YKnWCm7f
https://pastebin.com/zpSHGbnT
https://pastebin.com/qQg87Qa5
https://pastebin.com/wkijxdgV
https://pastebin.com/ASWCH7DB
https://pastebin.com/NAQqmtQW
https://pastebin.com/6QHpyMV1
https://pastebin.com/WT2xgp5n
https://pastebin.com/qnTJDwPy
https://pastebin.com/fZvEStv0
https://pastebin.com/VfxtAWJG
https://pastebin.com/vyQJKxFQ
https://pastebin.com/ExGA44gn
https://pastebin.com/qa32zS0w
https://pastebin.com/GfBS4n1X
https://pastebin.com/JhJXWrUD
https://pastebin.com/WNPAYC7r
https://pastebin.com/i77xpTm7
https://pastebin.com/jvyz7bDq
https://pastebin.com/h9iLgLDD
https://pastebin.com/0dtPxmtT
https://pastebin.com/4w2UaMSn
https://pastebin.com/Xy4S2CRs
https://pastebin.com/EHQtV6b7
https://pastebin.com/ucP8B1iB
https://pastebin.com/2Wcf2GNC
https://pastebin.com/L9SiHQ9Z
https://pastebin.com/wXCZSJuj
https://pastebin.com/vbPTR3SF
https://pastebin.com/7yxGcVHj
https://pastebin.com/MNAcjFjQ
https://pastebin.com/0A4KPjNU
https://pastebin.com/8nBAWbca
https://pastebin.com/Gh9dhxLt
https://pastebin.com/08qFJBye
https://pastebin.com/mfweHrVr
https://pastebin.com/RWwcnrYC
https://pastebin.com/anp3JpJ4
https://pastebin.com/hvBGQjK0
https://pastebin.com/EGww5k1k
https://pastebin.com/wdwGG4Mx
https://pastebin.com/qKr2rRmv
https://pastebin.com/VpBYTpBH
https://pastebin.com/Bsix0p2q
https://pastebin.com/d0rRmyuc
https://pastebin.com/1YVNhKx3
https://pastebin.com/PGscNx9F
https://pastebin.com/sE0Mv39Y
https://pastebin.com/20dhC8pa
https://pastebin.com/wGmcz5e6
https://pastebin.com/cApeLHfX
https://pastebin.com/mBT8cEji
https://pastebin.com/4hXLXigr
https://pastebin.com/fgt151cu

继续阅读 »

问题现象
如何使用openCustomDialog实现一个从外部传参的菜单弹窗,并且可以在外部监听菜单项的选中状态。

效果预览
点击放大

背景知识
不依赖UI组件的全局自定义弹出框 (openCustomDialog)存在两种入参方式创建自定义弹出框,ComponentContent和Builder:

ComponentContent封装内容可以与UI界面解耦,调用更加灵活。
使用Builder的方式,必须要与上下文做绑定,与UI存在一定耦合,具体请参考:不依赖UI组件的全局自定义弹出框 (openCustomDialog)的说明部分和@Builder装饰器使用说明。
解决方案
参考完整示例实现弹窗工具类,其核心是以下属性的具体实现:
ctx:获取UI上下文用于实现弹窗服务。
contentNode:构建弹窗的内容节点,其中弹窗中菜单项的个数,内容和传给外部监听的回调通过Params类型的数组传递。
options:弹窗的配置选项,如位置、样式、动画等。
export class CustomerPromptAction {
static ctx: UIContext;
static contentNode: ComponentContent<Object>;
static options: promptAction.BaseDialogOptions;

// 定义方法,从外部传入上下文。
static setContext(context: UIContext) {
CustomerPromptAction.ctx = context;
}

// 配置内容节点。
static setContentNode(context: UIContext, para: Params[]) {
let contentNode: ComponentContent<Object> = new ComponentContent(context, wrapBuilder(buildText), para);
CustomerPromptAction.contentNode = contentNode;
}

// 弹窗配置项。
static setOptions(options: promptAction.BaseDialogOptions) {
CustomerPromptAction.options = options;
}

// 定义弹窗打开的方法。
static openDialog(options: promptAction.BaseDialogOptions, context: UIContext, para: Params[]) {
CustomerPromptAction.setContext(context);
CustomerPromptAction.setContentNode(context, para);
CustomerPromptAction.setOptions({ alignment: DialogAlignment.Bottom, offset: { dx: 0, dy: -20 } });
if (CustomerPromptAction.contentNode !== null) {
CustomerPromptAction.ctx.getPromptAction()
.openCustomDialog(CustomerPromptAction.contentNode, options)
.then(() => {
})
.catch(() => {
});
}
}

// 关闭弹窗。
static closeDialog() {
if (CustomerPromptAction.contentNode !== null) {
CustomerPromptAction.ctx.getPromptAction()
.closeCustomDialog(CustomerPromptAction.contentNode)
.then(() => {
})
.catch(() => {
});
}
}
}
在页面中调用弹窗。
自定义菜单项实体类,声明属性方法,并通过构造器实例化。
export class Params {
title: string = "";
iconName: Resource;
click: (index: number) => void;

constructor(title: string, iconName: Resource, click: (index: number) => void) {
this.title = title;
this.iconName = iconName;
this.click = click;
}
}
定义接收菜单项选中回调的函数,根据返回的index处理逻辑。
// 点击弹窗中按钮的回调,并根据返回的index判断逻辑。
callBack = (index: number) => {
switch (index) {
case 0:
this.getUIContext().getPromptAction().showToast({
message: '点击了关闭',
duration: 2000
});
break;
case 1:
this.getUIContext().getPromptAction().showToast({
message: '点击了刷新',
duration: 2000
});
break;
case 2:
this.getUIContext().getPromptAction().showToast({
message: '点击了帮助',
duration: 2000
});
break;
}
};
初始化菜单数据源。
@State arr: Params[] = [
new Params('关闭', $r('app.media.startIcon'), this.callBack),
new Params('刷新', $r('app.media.startIcon'), this.callBack),
new Params('帮助', $r('app.media.startIcon'), this.callBack)];
完整示例参考如下:

CustomerPromptAction.ets。
import { ComponentContent, promptAction } from '@kit.ArkUI';
import { UIContext } from '@ohos.arkui.UIContext';
import { Params } from './Params';

@Builder
function buildText(params: Params[]) {
Column() {
Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceAround }) {
ForEach(params, (item: Params, index: number) => {
Column() {
Image(item.iconName)
.width(30)
.height(30)
.margin({ top: 20 })
.onClick(() => {
item.click(index);
});
Text(item.title)
.fontSize(15)
.margin({ top: 20 });
};
});
}
.width('100%');

Text('取消')  
  .margin({ top: 20 })  
  .height(50)  
  .width('100%')  
  .textAlign(TextAlign.Center)  
  .fontColor(Color.Blue)  
  .onClick(() => {  
    CustomerPromptAction.closeDialog();  
  });  

}
.backgroundColor('#FFF0F0F0')
.width('100%')
.borderRadius(10);
}
export class CustomerPromptAction {
static ctx: UIContext;
static contentNode: ComponentContent<Object>;
static options: promptAction.BaseDialogOptions;

// 定义方法,从外部传入上下文。
static setContext(context: UIContext) {
CustomerPromptAction.ctx = context;
}

// 配置内容节点。
static setContentNode(context: UIContext, para: Params[]) {
let contentNode: ComponentContent<Object> = new ComponentContent(context, wrapBuilder(buildText), para);
CustomerPromptAction.contentNode = contentNode;
}

// 弹窗配置项。
static setOptions(options: promptAction.BaseDialogOptions) {
CustomerPromptAction.options = options;
}

// 定义弹窗打开的方法。
static openDialog(options: promptAction.BaseDialogOptions, context: UIContext, para: Params[]) {
CustomerPromptAction.setContext(context);
CustomerPromptAction.setContentNode(context, para);
CustomerPromptAction.setOptions({ alignment: DialogAlignment.Bottom, offset: { dx: 0, dy: -20 } });
if (CustomerPromptAction.contentNode !== null) {
CustomerPromptAction.ctx.getPromptAction()
.openCustomDialog(CustomerPromptAction.contentNode, options)
.then(() => {
})
.catch(() => {
});
}
}

// 关闭弹窗。
static closeDialog() {
if (CustomerPromptAction.contentNode !== null) {
CustomerPromptAction.ctx.getPromptAction()
.closeCustomDialog(CustomerPromptAction.contentNode)
.then(() => {
})
.catch(() => {
});
}
}
}
Index.ets。
import { Params } from './Params';
import { CustomerPromptAction } from './CustomerPromptAction';

@Entry
@Component
struct Index {
// 点击弹窗中按钮的回调,并根据返回的index判断逻辑。
callBack = (index: number) => {
switch (index) {
case 0:
this.getUIContext().getPromptAction().showToast({
message: '点击了关闭',
duration: 2000
});
break;
case 1:
this.getUIContext().getPromptAction().showToast({
message: '点击了刷新',
duration: 2000
});
break;
case 2:
this.getUIContext().getPromptAction().showToast({
message: '点击了帮助',
duration: 2000
});
break;
}
};
// 初始化弹窗菜单的数据源。
@State arr: Params[] = [
new Params('关闭', $r('app.media.startIcon'), this.callBack),
new Params('刷新', $r('app.media.startIcon'), this.callBack),
new Params('帮助', $r('app.media.startIcon'), this.callBack)];

build() {
Row() {
Column() {
Button('横向菜单')
.margin({ top: 50 })
.onClick(() => {
CustomerPromptAction.openDialog({
alignment: DialogAlignment.Bottom,
offset: { dx: 0, dy: -20 }
}, this.getUIContext(), this.arr);
});
}
.width('100%')
.height('100%');
}
.height('100%');
}
}
Params.ets。
export class Params {
title: string = "";
iconName: Resource;
click: (index: number) => void;

constructor(title: string, iconName: Resource, click: (index: number) => void) {
this.title = title;
this.iconName = iconName;
this.click = click;
}
}
https://pastebin.com/epc9DuYu
https://pastebin.com/CErz3YL5
https://pastebin.com/6YfF0p9z
https://pastebin.com/kvpAvpdi
https://pastebin.com/X9fq64a2
https://pastebin.com/R0mRDZm4
https://pastebin.com/urWyLjrt
https://pastebin.com/hcKewAUE
https://pastebin.com/MjLnmvNu
https://pastebin.com/DYMeHxN4
https://pastebin.com/L5C77sX1
https://pastebin.com/rM22qRKb
https://pastebin.com/s8kwwq5k
https://pastebin.com/A6qfadrP
https://pastebin.com/PmbE57qe
https://pastebin.com/FiuhbGfN
https://pastebin.com/B2XYkbnq
https://pastebin.com/e0q36QBZ
https://pastebin.com/VwfcTkgK
https://pastebin.com/STYB1p4a
https://pastebin.com/dL8avv7N
https://pastebin.com/WgRdkJJY
https://pastebin.com/MHmf76dA
https://pastebin.com/2b2X9tbY
https://pastebin.com/uVqiYmpP
https://pastebin.com/iWFWeXfu
https://pastebin.com/tcz08Ga5
https://pastebin.com/rFqwzRhK
https://pastebin.com/bCm1D1Wj
https://pastebin.com/EWbGJZcy
https://pastebin.com/R30cJtHA
https://pastebin.com/aV9QTvJL
https://pastebin.com/EF75B0Xs
https://pastebin.com/UFQ2LwDh
https://pastebin.com/2CK4bM77
https://pastebin.com/ksCNHaF7
https://pastebin.com/k61KcBd4
https://pastebin.com/5GUXdSsP
https://pastebin.com/byn4kM9X
https://pastebin.com/nLG9ztxg
https://pastebin.com/PGrz7isp
https://pastebin.com/Jdv6Vcs1
https://pastebin.com/wc3q2WXq
https://pastebin.com/7JM3Dt9d
https://pastebin.com/PKueg3Q9
https://pastebin.com/C8hvWEnm
https://pastebin.com/nXYRTagp
https://pastebin.com/rzgJVLh2
https://pastebin.com/GXM3nwFL
https://pastebin.com/t0GkNTPB
https://pastebin.com/SGvqUZUw
https://pastebin.com/8SfWYWuA
https://pastebin.com/Cy2FKdCM
https://pastebin.com/Cwi3azHM
https://pastebin.com/WkBmAyzu
https://pastebin.com/NtcjEH8D
https://pastebin.com/vPFjfiTV
https://pastebin.com/VWQEns27
https://pastebin.com/W6sGgxN4
https://pastebin.com/BRTkHWLB
https://pastebin.com/b7kSTqmz
https://pastebin.com/TKxKXExY
https://pastebin.com/WbG5WLAC
https://pastebin.com/kNf5AwB7
https://pastebin.com/cgRrpGud
https://pastebin.com/TqFJkVxd
https://pastebin.com/Vwrjm3w7
https://pastebin.com/4ZMkzUBr
https://pastebin.com/GBW5cMYt
https://pastebin.com/RmGU8Ame
https://pastebin.com/RJ11SAm3
https://pastebin.com/kQrLCYA9
https://pastebin.com/9EH4eaY8
https://pastebin.com/YacTrPKQ
https://pastebin.com/DBYCLtGD
https://pastebin.com/y9eemXwr
https://pastebin.com/jTRFengj
https://pastebin.com/5vE7dpsX
https://pastebin.com/Kk4bZ1L8
https://pastebin.com/Kaxbk4tA
https://pastebin.com/LGR3sxN9
https://pastebin.com/2jbBiu33
https://pastebin.com/p0i2Fj9p
https://pastebin.com/uThxW0MS
https://pastebin.com/hr1MtNuu
https://pastebin.com/nt6fFaYk
https://pastebin.com/9RMs3tAN
https://pastebin.com/3pp9A2SP
https://pastebin.com/fbQQkXb2
https://pastebin.com/RNXkhP0Y
https://pastebin.com/62GMDSK1
https://pastebin.com/A47VdXsL
https://pastebin.com/TMXrdhxC
https://pastebin.com/pZV96ZWh
https://pastebin.com/BnZ4zhH2
https://pastebin.com/hqGmeuVV
https://pastebin.com/aqnxjmby
https://pastebin.com/uNxvnAFc
https://pastebin.com/tkDrfMPT
https://pastebin.com/6ufWP0fd
https://pastebin.com/H9PDFDiL
https://pastebin.com/qM9xgF7K
https://pastebin.com/iyJTH6aB
https://pastebin.com/ZiJyZLfp
https://pastebin.com/yZ0LA5dB
https://pastebin.com/8YX63tah
https://pastebin.com/v6DvRcJ2
https://pastebin.com/dQSyGA1C
https://pastebin.com/VLfUFJ3F
https://pastebin.com/1dHdGtmp
https://pastebin.com/ufJFhNjk
https://pastebin.com/t1WuAJPZ
https://pastebin.com/pZUndcCA
https://pastebin.com/ZPDUXz25
https://pastebin.com/ULjPJmEH
https://pastebin.com/EhwMqtVW
https://pastebin.com/EMCGkgwT
https://pastebin.com/60tHCxEM
https://pastebin.com/c4nYDLJs
https://pastebin.com/gQSdXcnY
https://pastebin.com/jc1GvWkA
https://pastebin.com/EVjzT0Bn
https://pastebin.com/hE2m4nfV
https://pastebin.com/yf3kYzc4
https://pastebin.com/Cj6MtRgk
https://pastebin.com/e7Hmp2wM
https://pastebin.com/0HzG8VJj
https://pastebin.com/XSTJdK3z
https://pastebin.com/mnfsGkiu
https://pastebin.com/REx1hsyn
https://pastebin.com/50VxQZdS
https://pastebin.com/0mxMQAfy
https://pastebin.com/AtGFtGFZ
https://pastebin.com/eWZmH6hX
https://pastebin.com/68JVfXcf
https://pastebin.com/Q47SWvBD
https://pastebin.com/pRR9gzGf
https://pastebin.com/KiMStGV8
https://pastebin.com/k8vp0yy7
https://pastebin.com/tN4ChEUp
https://pastebin.com/7pAZ3CdD
https://pastebin.com/rpSjghL8
https://pastebin.com/dYFDgUM2
https://pastebin.com/NQyE3NAS
https://pastebin.com/LYbXc4ma
https://pastebin.com/naCsLg7e
https://pastebin.com/YKnWCm7f
https://pastebin.com/zpSHGbnT
https://pastebin.com/qQg87Qa5
https://pastebin.com/wkijxdgV
https://pastebin.com/ASWCH7DB
https://pastebin.com/NAQqmtQW
https://pastebin.com/6QHpyMV1
https://pastebin.com/WT2xgp5n
https://pastebin.com/qnTJDwPy
https://pastebin.com/fZvEStv0
https://pastebin.com/VfxtAWJG
https://pastebin.com/vyQJKxFQ
https://pastebin.com/ExGA44gn
https://pastebin.com/qa32zS0w
https://pastebin.com/GfBS4n1X
https://pastebin.com/JhJXWrUD
https://pastebin.com/WNPAYC7r
https://pastebin.com/i77xpTm7
https://pastebin.com/jvyz7bDq
https://pastebin.com/h9iLgLDD
https://pastebin.com/0dtPxmtT
https://pastebin.com/4w2UaMSn
https://pastebin.com/Xy4S2CRs
https://pastebin.com/EHQtV6b7
https://pastebin.com/ucP8B1iB
https://pastebin.com/2Wcf2GNC
https://pastebin.com/L9SiHQ9Z
https://pastebin.com/wXCZSJuj
https://pastebin.com/vbPTR3SF
https://pastebin.com/7yxGcVHj
https://pastebin.com/MNAcjFjQ
https://pastebin.com/0A4KPjNU
https://pastebin.com/8nBAWbca
https://pastebin.com/Gh9dhxLt
https://pastebin.com/08qFJBye
https://pastebin.com/mfweHrVr
https://pastebin.com/RWwcnrYC
https://pastebin.com/anp3JpJ4
https://pastebin.com/hvBGQjK0
https://pastebin.com/EGww5k1k
https://pastebin.com/wdwGG4Mx
https://pastebin.com/qKr2rRmv
https://pastebin.com/VpBYTpBH
https://pastebin.com/Bsix0p2q
https://pastebin.com/d0rRmyuc
https://pastebin.com/1YVNhKx3
https://pastebin.com/PGscNx9F
https://pastebin.com/sE0Mv39Y
https://pastebin.com/20dhC8pa
https://pastebin.com/wGmcz5e6
https://pastebin.com/cApeLHfX
https://pastebin.com/mBT8cEji
https://pastebin.com/4hXLXigr
https://pastebin.com/fgt151cu

收起阅读 »

额度

我花了149 元人民币 ,买了你们AI 一个月的 订阅,每天就一个问题 ,额度就没有了,我用codex 和claude code 都能解决很多问题,你们这个额度是不是在骗人呀,也没有客服, 你们是一帮不懂经营的人再搞这个吗。

我花了149 元人民币 ,买了你们AI 一个月的 订阅,每天就一个问题 ,额度就没有了,我用codex 和claude code 都能解决很多问题,你们这个额度是不是在骗人呀,也没有客服, 你们是一帮不懂经营的人再搞这个吗。

写了个VSCode插件,用Claude把uni-app页面截图直接转成可运行的Vue代码

uni-app

用VSCode插件+Claude实现uni-app截图转代码的实战记录

上周设计师又扔过来一沓截图,我对着“个人中心”页面的设计稿,手指在键盘上悬了三秒——又要从头写一遍套的布局吗?作为跨端项目里的“截图搬运工”,我太清楚这种重复劳动有多磨人了:打开截图,对照着像素调间距,一行一行敲template,一个普通列表页半小时起步。更扎心的是,这种工作做完就忘,下次换个颜色改个文案,又得重来。

某天下午,我盯着桌面上几十张截图发呆,突然想到:Claude的多模态能力已经能识别图片内容,为什么不能让它直接吐出可运行的uni-app代码?但每次手动拖图进Claude、复制代码、粘贴到项目里,中间至少有七八步操作。于是,一个“截图→右键→生成Vue文件”的念头冒了出来,我花了两个晚上搓了个VSCode插件,把整个流程塞进了一个右键菜单里。

为什么截图转代码是个“看起来简单,做起来翻车”的事

动手前,我列了几个绕不过去的坎。不是所有截图AI都能看懂,也不是所有生成的代码都能直接跑起来。

第一个坑:uni-app的语法不是标准Vue。 AI如果没见过、这些组件,大概率会甩出一堆,然后你手动改成,再调整移动端适配。更烦人的是,uni.xxx API 和 Vue Router 完全是两套东西,AI 生成的页面跳转逻辑基本得重写。

第二个坑:截图里的样式信息是“伪3D”。 AI只能靠视觉猜测间距和颜色,但实际项目里我们用flex布局、rpx单位、scoped样式。它生成的代码经常是margin: 20px硬撑,换个屏幕就崩了。最离谱的一次,Claude给我生成了font-size: 4rpx,我盯着那个小到肉眼看不见的字,哭笑不得。

第三个坑:代码只有壳,没有魂。 登录页截图上显示着输入框和按钮,但AI不会帮你写表单校验、不会处理提交后的跳转、不会绑定v-model。它只能画个皮,里面的逻辑还得你自己填。

我的目标很明确:不是要一个“一键生成完整App”的噱头,而是让开发者从“对着截图敲布局”变成“对着截图改逻辑”。把最无聊的部分——写template、调间距——交给AI,剩下的业务逻辑自己掌控。

插件怎么串起来的:右键→AI→生成文件

核心思路超级简单:在VSCode里选中一张截图,右键点击,Claude API 生成代码,然后自动创建.vue文件并打开。全程不用切窗口,不用复制粘贴。

技术选型上的取舍

  • 插件框架:VSCode Extension API + TypeScript,注册一个右键菜单命令,只在图片文件上显示。
  • AI模型:Claude 3.5 Sonnet。我试过GPT-4o和Gemini,Claude在视觉理解和代码格式规范上最平衡,而且system prompt能很好地约束输出结构。
  • 图片处理:直接传base64给API,省去文件上传的麻烦。

实现流程里的三个关键节点

  1. 注册命令
    在package.json里定义extension.uniImageToCode命令,绑定右键菜单,通过vscode.window.activeTextEditor获取选中文件路径。

  2. Prompt设计——这是最烧脑的部分
    我写了一个详细的system prompt,核心要求包括:

    • 必须用uni-app组件(view、scroll-view、uni-list等),禁止
    • 样式用rpx单位,字体至少28rpx,布局优先flex
    • 输出完整的++三段式
    • 输入框用uni-easyinput并给出v-model绑定
    • 列表用uni-list和uni-list-item,并定义基础的data数组

特别加了一句反例提示:“不要使用,应该用”,因为Claude经常把中划线写成驼峰。

  1. 处理AI返回的不完整代码
    第一次测试时,Claude只返回了template,没有script。后来我在prompt里加了“请确保输出完整的单文件组件代码,不要省略任何部分”,并在后端做了校验:如果返回内容不包含export default,就重新请求一次。另外,API的max_tokens设成4000,因为uni-app组件代码通常较长,设小了会被截断。

核心代码片段(简化版)

typescript
// 读取图片转base64
const imageBuffer = fs.readFileSync(imagePath);
const base64Image = imageBuffer.toString('base64');

// 调用Claude API
const response = await axios.post('https://api.anthropic.com/v1/messages', {
model: 'claude-3-5-sonnet-20241022',
max_tokens: 4000,
messages: [{
role: 'user',
content: [
{ type: 'text', text: systemPrompt },
{ type: 'image', source: { type: 'base64', media_type: mimeType, data: base64Image } }
]
}]
});

// 提取代码块并创建文件
const code = response.data.content[0].text;
const template = code.match(/[\s\S]*?/)[0];
const script = code.match(//)[0];
const style = code.match(//)[0];
const fullCode = ${template}\n${script}\n${style};

// 写入新文件(如 login.png → login.vue)
vscode.workspace.fs.writeFile(newUri, Buffer.from(fullCode));
vscode.window.showTextDocument(newUri);

API key没有硬编码,通过VSCode配置项让用户自行设置,也支持从环境变量读取。

实战体验:哪些场景真香,哪些翻了车

插件写完后,我拿真实项目里的截图测了一遍,结果有惊喜也有槽点。

真香场景:结构清晰的静态页面,比如设置页、个人中心、商品列表。生成的代码基本可以直接跑,样式还原度大概70%——间距、颜色、字体大小都挺准,布局结构从几十行flex缩减到微调几处。最省时间的是表单页,输入框、按钮、校验框架都给你搭好了,改改字段名就能用。

翻车现场:

  • 复杂交互页面:带tab切换、下拉刷新、上拉加载的,Claude生成的代码只是样子像,逻辑基本不可用。比如tab切换它用了v-if硬切换,没有缓存也没有动画,需要自己重写。
  • 第三方组件库:截图里用了uView或ThorUI的组件,Claude不认识,直接生成普通的view替代,失去了组件化的优势。这点目前无解,除非我在prompt里把组件库的API全写进去。
  • 特殊字体和图标:截图里的图标,Claude会用硬写Unicode字符,而不是用字体图标组件,导致样式和交互都丢失。

我的使用建议:插件最适合生成静态页面或简单的表单/列表页。对于复杂交互的页面,先让它生成骨架,然后对照截图手动补充事件和API调用。另外,生成后一定要检查样式里的rpx值,Claude偶尔会抽风给出不合理的数值(比如字体4rpx,明显是错的)。

效果对比与下一步计划

从手动截图+写代码的30分钟,缩短到生成+微调的5分钟,这是最直观的收益。虽然不完美,但方向对了——把开发者从“像素级还原”中解放出来,专注于业务逻辑和交互设计。

接下来几个想加的功能:

  • 支持选择生成风格:比如生成uView组件版本还是原生uni-app版本,通过prompt里的组件库约束实现。
  • 增量更新:如果已经有一个.vue文件,只更新template部分,保留原有的script逻辑。这需要解析AST,比较复杂,但很实用。
  • 本地模型支持:考虑接入Ollama运行小模型,避免每次都调用云端API,既省钱又保护隐私。不过本地模型的视觉能力目前还差一些,可能只适合简单的页面。

代码已经放在GitHub上了(链接就不放了,搜“uni-app截图转代码”应该能找到)。如果你也在做uni-app开发,并且经常被截图折磨,可以试试这个思路。记住,工具只是辅助,真正的价值在你的业务逻辑和交互设计上。

继续阅读 »

用VSCode插件+Claude实现uni-app截图转代码的实战记录

上周设计师又扔过来一沓截图,我对着“个人中心”页面的设计稿,手指在键盘上悬了三秒——又要从头写一遍套的布局吗?作为跨端项目里的“截图搬运工”,我太清楚这种重复劳动有多磨人了:打开截图,对照着像素调间距,一行一行敲template,一个普通列表页半小时起步。更扎心的是,这种工作做完就忘,下次换个颜色改个文案,又得重来。

某天下午,我盯着桌面上几十张截图发呆,突然想到:Claude的多模态能力已经能识别图片内容,为什么不能让它直接吐出可运行的uni-app代码?但每次手动拖图进Claude、复制代码、粘贴到项目里,中间至少有七八步操作。于是,一个“截图→右键→生成Vue文件”的念头冒了出来,我花了两个晚上搓了个VSCode插件,把整个流程塞进了一个右键菜单里。

为什么截图转代码是个“看起来简单,做起来翻车”的事

动手前,我列了几个绕不过去的坎。不是所有截图AI都能看懂,也不是所有生成的代码都能直接跑起来。

第一个坑:uni-app的语法不是标准Vue。 AI如果没见过、这些组件,大概率会甩出一堆,然后你手动改成,再调整移动端适配。更烦人的是,uni.xxx API 和 Vue Router 完全是两套东西,AI 生成的页面跳转逻辑基本得重写。

第二个坑:截图里的样式信息是“伪3D”。 AI只能靠视觉猜测间距和颜色,但实际项目里我们用flex布局、rpx单位、scoped样式。它生成的代码经常是margin: 20px硬撑,换个屏幕就崩了。最离谱的一次,Claude给我生成了font-size: 4rpx,我盯着那个小到肉眼看不见的字,哭笑不得。

第三个坑:代码只有壳,没有魂。 登录页截图上显示着输入框和按钮,但AI不会帮你写表单校验、不会处理提交后的跳转、不会绑定v-model。它只能画个皮,里面的逻辑还得你自己填。

我的目标很明确:不是要一个“一键生成完整App”的噱头,而是让开发者从“对着截图敲布局”变成“对着截图改逻辑”。把最无聊的部分——写template、调间距——交给AI,剩下的业务逻辑自己掌控。

插件怎么串起来的:右键→AI→生成文件

核心思路超级简单:在VSCode里选中一张截图,右键点击,Claude API 生成代码,然后自动创建.vue文件并打开。全程不用切窗口,不用复制粘贴。

技术选型上的取舍

  • 插件框架:VSCode Extension API + TypeScript,注册一个右键菜单命令,只在图片文件上显示。
  • AI模型:Claude 3.5 Sonnet。我试过GPT-4o和Gemini,Claude在视觉理解和代码格式规范上最平衡,而且system prompt能很好地约束输出结构。
  • 图片处理:直接传base64给API,省去文件上传的麻烦。

实现流程里的三个关键节点

  1. 注册命令
    在package.json里定义extension.uniImageToCode命令,绑定右键菜单,通过vscode.window.activeTextEditor获取选中文件路径。

  2. Prompt设计——这是最烧脑的部分
    我写了一个详细的system prompt,核心要求包括:

    • 必须用uni-app组件(view、scroll-view、uni-list等),禁止
    • 样式用rpx单位,字体至少28rpx,布局优先flex
    • 输出完整的++三段式
    • 输入框用uni-easyinput并给出v-model绑定
    • 列表用uni-list和uni-list-item,并定义基础的data数组

特别加了一句反例提示:“不要使用,应该用”,因为Claude经常把中划线写成驼峰。

  1. 处理AI返回的不完整代码
    第一次测试时,Claude只返回了template,没有script。后来我在prompt里加了“请确保输出完整的单文件组件代码,不要省略任何部分”,并在后端做了校验:如果返回内容不包含export default,就重新请求一次。另外,API的max_tokens设成4000,因为uni-app组件代码通常较长,设小了会被截断。

核心代码片段(简化版)

typescript
// 读取图片转base64
const imageBuffer = fs.readFileSync(imagePath);
const base64Image = imageBuffer.toString('base64');

// 调用Claude API
const response = await axios.post('https://api.anthropic.com/v1/messages', {
model: 'claude-3-5-sonnet-20241022',
max_tokens: 4000,
messages: [{
role: 'user',
content: [
{ type: 'text', text: systemPrompt },
{ type: 'image', source: { type: 'base64', media_type: mimeType, data: base64Image } }
]
}]
});

// 提取代码块并创建文件
const code = response.data.content[0].text;
const template = code.match(/[\s\S]*?/)[0];
const script = code.match(//)[0];
const style = code.match(//)[0];
const fullCode = ${template}\n${script}\n${style};

// 写入新文件(如 login.png → login.vue)
vscode.workspace.fs.writeFile(newUri, Buffer.from(fullCode));
vscode.window.showTextDocument(newUri);

API key没有硬编码,通过VSCode配置项让用户自行设置,也支持从环境变量读取。

实战体验:哪些场景真香,哪些翻了车

插件写完后,我拿真实项目里的截图测了一遍,结果有惊喜也有槽点。

真香场景:结构清晰的静态页面,比如设置页、个人中心、商品列表。生成的代码基本可以直接跑,样式还原度大概70%——间距、颜色、字体大小都挺准,布局结构从几十行flex缩减到微调几处。最省时间的是表单页,输入框、按钮、校验框架都给你搭好了,改改字段名就能用。

翻车现场:

  • 复杂交互页面:带tab切换、下拉刷新、上拉加载的,Claude生成的代码只是样子像,逻辑基本不可用。比如tab切换它用了v-if硬切换,没有缓存也没有动画,需要自己重写。
  • 第三方组件库:截图里用了uView或ThorUI的组件,Claude不认识,直接生成普通的view替代,失去了组件化的优势。这点目前无解,除非我在prompt里把组件库的API全写进去。
  • 特殊字体和图标:截图里的图标,Claude会用硬写Unicode字符,而不是用字体图标组件,导致样式和交互都丢失。

我的使用建议:插件最适合生成静态页面或简单的表单/列表页。对于复杂交互的页面,先让它生成骨架,然后对照截图手动补充事件和API调用。另外,生成后一定要检查样式里的rpx值,Claude偶尔会抽风给出不合理的数值(比如字体4rpx,明显是错的)。

效果对比与下一步计划

从手动截图+写代码的30分钟,缩短到生成+微调的5分钟,这是最直观的收益。虽然不完美,但方向对了——把开发者从“像素级还原”中解放出来,专注于业务逻辑和交互设计。

接下来几个想加的功能:

  • 支持选择生成风格:比如生成uView组件版本还是原生uni-app版本,通过prompt里的组件库约束实现。
  • 增量更新:如果已经有一个.vue文件,只更新template部分,保留原有的script逻辑。这需要解析AST,比较复杂,但很实用。
  • 本地模型支持:考虑接入Ollama运行小模型,避免每次都调用云端API,既省钱又保护隐私。不过本地模型的视觉能力目前还差一些,可能只适合简单的页面。

代码已经放在GitHub上了(链接就不放了,搜“uni-app截图转代码”应该能找到)。如果你也在做uni-app开发,并且经常被截图折磨,可以试试这个思路。记住,工具只是辅助,真正的价值在你的业务逻辑和交互设计上。

收起阅读 »

试了一圈AI编程助手,发现写uni-app时还是得自己动手调这几个坑

去年接了个uni-app外包,想着用AI编程助手冲一下效率,结果差点翻车翻到沟里。

Copilot、Cursor、通义灵码挨个试了一遍,它们写页面模板确实快——一个列表页、一个表单,几秒就给你整出来。但一碰到uni-app的跨端细节,AI就像个刚毕业的实习生:代码看起来能跑,一部署到小程序就崩给你看。下面这5个坑,是我亲手填过的,AI永远填不上。

  1. 条件编译:AI把它当普通if-else用

uni-app的#ifdef和#ifndef是跨端生存的关键,但AI写代码时,经常把平台逻辑混在一起,甚至直接写个if (process.env.PLATFORM === 'h5')——这玩意儿在微信小程序里根本跑不动。

真实翻车现场:我需要在小程序端调wx.getUserProfile,H5端用uni.getUserInfo。AI甩给我一段代码:

javascript
uni.getUserProfile({
desc: '用于完善资料',
success: (res) => console.log(res.userInfo)
});

在H5端直接报错——getUserProfile不存在。我手动拆成条件编译:

javascript
// #ifdef MP-WEIXIN
uni.getUserProfile({
desc: '用于完善资料',
success: (res) => this.userInfo = res.userInfo
});
// #endif

// #ifdef H5
uni.getUserInfo({
provider: 'weixin',
success: (infoRes) => this.userInfo = infoRes.userInfo
});
// #endif

血泪教训:AI经常把#ifndef H5写成#ifndef MP-WEIXIN,逻辑反了。每次写完条件编译,我都手动跑一遍所有目标端的模拟器,别信AI的“智能判断”。

  1. 自定义组件传值:小程序对props类型有洁癖

uni-app支持Vue的props,但在小程序端,props不能传函数类型。AI不懂这个,生成了这样的代码:

vue
// 子组件
点击
props: {
onSuccess: Function
}

微信开发者工具直接报警告:“props不能接受Function类型”。我改成$emit事件机制才搞定:

vue
// 父组件

// 子组件
点击
export default {
emits: ['success'],
methods: {
emitSuccess() {
this.$emit('success', { data: '...' });
}
}
}

坑点:AI生成的组件代码里,emits选项经常漏掉,事件传递直接失效。手动补上emits: ['success'],否则在微信开发者工具里会一直报“未注册事件”。

  1. uni.request封装:AI给的拦截器有毒

AI推荐用axios封装请求,但uni-app在小程序端不支持axios(没有XMLHttpRequest)。AI给的代码是:

javascript
import axios from 'axios';
const instance = axios.create({ baseURL: '...' });

H5端能跑,小程序端直接报错。我换成uni.request手动封装:

javascript
// request.js
const baseURL = 'https://api.example.com';

function request(options) {
const token = uni.getStorageSync('token');
const header = {
'Content-Type': 'application/json',
...(token ? { 'Authorization': Bearer ${token} } : {})
};

return new Promise((resolve, reject) => {
uni.request({
url: baseURL options.url,
method: options.method || 'GET',
data: options.data,
header: { ...header, ...options.header },
success: (res) => {
if (res.statusCode === 200) resolve(res.data);
else if (res.statusCode === 401) {
uni.navigateTo({ url: '/pages/login/login' });
reject(new Error('Unauthorized'));
} else reject(new Error('Request failed'));
},
fail: (err) => {
uni.showToast({ title: '网络异常', icon: 'none' });
reject(err);
}
});
});
}

export default request;

隐藏坑:AI生成的代码经常忘记处理401,或者把uni.navigateTo写成uni.redirectTo,导致登录后无法返回。另外,uni.request的header在小程序端有长度限制(大概4KB),AI不会告诉你——我遇到过token太长直接请求失败的情况。

  1. 页面生命周期混用:onLoad和mounted分不清

uni-app的onLoad和Vue的mounted执行顺序不同,AI经常把需要页面参数的处理逻辑塞进mounted里。

真实翻车:从列表页跳转详情页,需要获取options.id。AI写了:

javascript
mounted() {
const id = this.$route.query.id; // H5端能用,小程序端this.$route不存在
}

小程序端直接报错。我统一改为onLoad:

javascript
export default {
onLoad(options) {
this.id = options.id;
this.fetchDetail();
},
mounted() {
// 只做DOM初始化,比如图表渲染
// 不依赖this.id
}
}

坑点:AI经常在onLoad和mounted里都调用fetchDetail,导致接口请求两次。我习惯只保留onLoad里的请求,mounted只做DOM操作。

  1. 样式兼容:rpx和px混用成灾难

uni-app推荐用rpx做响应式单位,但AI生成的样式里px、rpx、vw混用,在不同屏幕尺寸下布局直接崩。

我的手动规则:

  • 字体大小:font-size: 28rpx(用rpx)
  • 固定宽高(按钮、图标):width: 100rpx(用rpx)
  • 边框、阴影:border: 1px solid #ccc(用px)
  • 百分比布局:width: 100%或flex

AI翻车案例:一个卡片组件,AI写了padding: 20px,在iPhone 12上正常,iPhone SE上右边距过大。改成padding: 30rpx后适配了所有屏幕。

坑点:AI有时生成calc(100vw - 40rpx)这种混合表达式,小程序端编译报错。统一用rpx,需要百分比宽度时用width: 100%配合padding。


最后说点实在的

别把AI当救世主。它写模板、列表页、基础逻辑确实快,但uni-app的跨端细节、条件编译、原生API调用、生命周期管理,这些坑它一个都填不上。

我的工作流:

  1. 只把AI当草稿生成器:用它写页面结构和基础逻辑,然后手动检查跨端兼容性。
  2. 建自己的代码片段库:把条件编译、请求封装、组件传值这些高频模式整理成模板,比让AI每次重新生成靠谱得多。
  3. 多在小程序端测试:AI生成的代码在H5端往往能跑,小程序端才是真正的“照妖镜”——我至少有一半的bug是在微信开发者工具里发现的。

最后,别迷信AI的“智能”。至少在uni-app这个领域,人类的“手动调”仍然是不可替代的——因为那些坑,AI永远填不上。

继续阅读 »

去年接了个uni-app外包,想着用AI编程助手冲一下效率,结果差点翻车翻到沟里。

Copilot、Cursor、通义灵码挨个试了一遍,它们写页面模板确实快——一个列表页、一个表单,几秒就给你整出来。但一碰到uni-app的跨端细节,AI就像个刚毕业的实习生:代码看起来能跑,一部署到小程序就崩给你看。下面这5个坑,是我亲手填过的,AI永远填不上。

  1. 条件编译:AI把它当普通if-else用

uni-app的#ifdef和#ifndef是跨端生存的关键,但AI写代码时,经常把平台逻辑混在一起,甚至直接写个if (process.env.PLATFORM === 'h5')——这玩意儿在微信小程序里根本跑不动。

真实翻车现场:我需要在小程序端调wx.getUserProfile,H5端用uni.getUserInfo。AI甩给我一段代码:

javascript
uni.getUserProfile({
desc: '用于完善资料',
success: (res) => console.log(res.userInfo)
});

在H5端直接报错——getUserProfile不存在。我手动拆成条件编译:

javascript
// #ifdef MP-WEIXIN
uni.getUserProfile({
desc: '用于完善资料',
success: (res) => this.userInfo = res.userInfo
});
// #endif

// #ifdef H5
uni.getUserInfo({
provider: 'weixin',
success: (infoRes) => this.userInfo = infoRes.userInfo
});
// #endif

血泪教训:AI经常把#ifndef H5写成#ifndef MP-WEIXIN,逻辑反了。每次写完条件编译,我都手动跑一遍所有目标端的模拟器,别信AI的“智能判断”。

  1. 自定义组件传值:小程序对props类型有洁癖

uni-app支持Vue的props,但在小程序端,props不能传函数类型。AI不懂这个,生成了这样的代码:

vue
// 子组件
点击
props: {
onSuccess: Function
}

微信开发者工具直接报警告:“props不能接受Function类型”。我改成$emit事件机制才搞定:

vue
// 父组件

// 子组件
点击
export default {
emits: ['success'],
methods: {
emitSuccess() {
this.$emit('success', { data: '...' });
}
}
}

坑点:AI生成的组件代码里,emits选项经常漏掉,事件传递直接失效。手动补上emits: ['success'],否则在微信开发者工具里会一直报“未注册事件”。

  1. uni.request封装:AI给的拦截器有毒

AI推荐用axios封装请求,但uni-app在小程序端不支持axios(没有XMLHttpRequest)。AI给的代码是:

javascript
import axios from 'axios';
const instance = axios.create({ baseURL: '...' });

H5端能跑,小程序端直接报错。我换成uni.request手动封装:

javascript
// request.js
const baseURL = 'https://api.example.com';

function request(options) {
const token = uni.getStorageSync('token');
const header = {
'Content-Type': 'application/json',
...(token ? { 'Authorization': Bearer ${token} } : {})
};

return new Promise((resolve, reject) => {
uni.request({
url: baseURL options.url,
method: options.method || 'GET',
data: options.data,
header: { ...header, ...options.header },
success: (res) => {
if (res.statusCode === 200) resolve(res.data);
else if (res.statusCode === 401) {
uni.navigateTo({ url: '/pages/login/login' });
reject(new Error('Unauthorized'));
} else reject(new Error('Request failed'));
},
fail: (err) => {
uni.showToast({ title: '网络异常', icon: 'none' });
reject(err);
}
});
});
}

export default request;

隐藏坑:AI生成的代码经常忘记处理401,或者把uni.navigateTo写成uni.redirectTo,导致登录后无法返回。另外,uni.request的header在小程序端有长度限制(大概4KB),AI不会告诉你——我遇到过token太长直接请求失败的情况。

  1. 页面生命周期混用:onLoad和mounted分不清

uni-app的onLoad和Vue的mounted执行顺序不同,AI经常把需要页面参数的处理逻辑塞进mounted里。

真实翻车:从列表页跳转详情页,需要获取options.id。AI写了:

javascript
mounted() {
const id = this.$route.query.id; // H5端能用,小程序端this.$route不存在
}

小程序端直接报错。我统一改为onLoad:

javascript
export default {
onLoad(options) {
this.id = options.id;
this.fetchDetail();
},
mounted() {
// 只做DOM初始化,比如图表渲染
// 不依赖this.id
}
}

坑点:AI经常在onLoad和mounted里都调用fetchDetail,导致接口请求两次。我习惯只保留onLoad里的请求,mounted只做DOM操作。

  1. 样式兼容:rpx和px混用成灾难

uni-app推荐用rpx做响应式单位,但AI生成的样式里px、rpx、vw混用,在不同屏幕尺寸下布局直接崩。

我的手动规则:

  • 字体大小:font-size: 28rpx(用rpx)
  • 固定宽高(按钮、图标):width: 100rpx(用rpx)
  • 边框、阴影:border: 1px solid #ccc(用px)
  • 百分比布局:width: 100%或flex

AI翻车案例:一个卡片组件,AI写了padding: 20px,在iPhone 12上正常,iPhone SE上右边距过大。改成padding: 30rpx后适配了所有屏幕。

坑点:AI有时生成calc(100vw - 40rpx)这种混合表达式,小程序端编译报错。统一用rpx,需要百分比宽度时用width: 100%配合padding。


最后说点实在的

别把AI当救世主。它写模板、列表页、基础逻辑确实快,但uni-app的跨端细节、条件编译、原生API调用、生命周期管理,这些坑它一个都填不上。

我的工作流:

  1. 只把AI当草稿生成器:用它写页面结构和基础逻辑,然后手动检查跨端兼容性。
  2. 建自己的代码片段库:把条件编译、请求封装、组件传值这些高频模式整理成模板,比让AI每次重新生成靠谱得多。
  3. 多在小程序端测试:AI生成的代码在H5端往往能跑,小程序端才是真正的“照妖镜”——我至少有一半的bug是在微信开发者工具里发现的。

最后,别迷信AI的“智能”。至少在uni-app这个领域,人类的“手动调”仍然是不可替代的——因为那些坑,AI永远填不上。

收起阅读 »

Google Play 审核到底是机器还是人工?

uni-app

Google Play 审核到底是机器还是人工?——DCloud 开发者实战分析

背景:Google Play 审核,开发者心中的“黑箱”

作为一名长期使用 uni-app 开发跨平台应用的开发者,我深知 Google Play 审核的复杂性。很多同行在 DCloud 社区里抱怨,明明在本地测试一切正常,提交到 Google Play 却被拒绝,而且拒绝理由往往模糊不清,比如“违反广告政策”“隐私政策不完整”或“权限使用不当”。更让人困惑的是,有时修改一个小问题后秒过,有时却要等上几天,甚至反复提交多次才能通过。于是,一个核心问题浮出水面:Google Play 的审核到底是由机器自动完成的,还是有人工介入?这个问题直接关系到我们如何优化应用、避免踩坑。

实际上,Google Play 的审核机制并非单一模式,而是机器与人工结合的混合系统。机器负责初步筛选和自动化检查,人工则处理复杂、模糊或高风险的案例。对于使用 uni-app 框架的开发者来说,理解这一机制至关重要,因为我们的应用通常依赖 WebView、原生插件和第三方 SDK,这些都可能触发机器审核的“雷区”。本文将从实战角度,分析 Google Play 审核的底层逻辑,并给出针对 uni-app 开发者的具体解决方案。

问题分析:机器审核的“硬伤”与人工审核的“盲区”

机器审核:速度与规则的博弈
机器审核是 Google Play 的第一道防线,它通过自动化脚本和算法扫描应用的代码、元数据、权限声明等。机器擅长处理确定性规则,比如检查是否包含恶意代码、是否使用了违规的 API(如非必要的后台位置权限),或者是否在描述中夸大了功能。例如,如果你的 uni-app 应用在 manifest.json 中声明了 android.permission.ACCESS_BACKGROUND_LOCATION,但实际功能不涉及后台定位,机器会直接标记为“权限滥用”,并拒绝提交。

机器审核的优点是速度快,通常在几分钟到几小时内完成。但它的缺点也很明显:缺乏上下文理解。比如,你的应用使用了一个第三方推送 SDK,该 SDK 在运行时动态申请了 READ_PHONE_STATE 权限,机器会认为这是应用主动申请的,从而触发“隐私政策不完整”的警告。实际上,这个权限只是 SDK 用于设备标识,但机器不会去深究 SDK 的文档。

人工审核:深度与效率的妥协
当机器无法做出明确判断时,或者应用被用户举报、被系统标记为高风险(如涉及金融、医疗或儿童内容),审核就会转交给人工团队。人工审核员会手动安装应用、测试功能、阅读隐私政策,甚至模拟用户行为。例如,如果你的 uni-app 应用包含一个 WebView 内嵌的 H5 页面,该页面加载了第三方广告,人工审核员可能会检查广告内容是否合规,或者是否在用户不知情的情况下收集了数据。

人工审核的优点是能处理复杂场景,比如判断某段代码是“功能需要”还是“恶意收集”。但它的缺点是效率低,且主观性强。同一款应用,不同审核员可能给出不同结论,这就是为什么有时修改一个标点符号就能通过,而有时调整整个功能架构却依然被拒。

对 uni-app 开发者的特殊影响
uni-app 应用本质上是混合架构,前端代码运行在 WebView 中,原生功能通过插件桥接。这种架构容易让机器审核产生误判。例如,机器可能将 WebView 的 JavaScript 代码视为动态加载内容,从而怀疑应用存在“代码混淆”或“动态下载”行为。而人工审核员如果不懂跨平台技术,可能会错误地认为 WebView 加载的 H5 页面是“第三方内容”,要求提供额外的合规证明。因此,理解审核机制后,我们需要针对性地调整开发策略。

解决方案:如何让 uni-app 应用顺利通过审核

  1. 优化权限声明,避免“机器误判”
    机器审核最敏感的就是权限。在 uni-app 中,权限声明集中在 manifest.json 的 distribute 节点下。很多开发者为了方便,直接复制网上的权限列表,结果包含了大量不必要的权限。例如,android.permission.CAMERA 和 android.permission.RECORD_AUDIO 只应在实际用到时声明。

步骤:

  • 检查 manifest.json 中的 标签,只保留应用核心功能所需的权限。
  • 对于第三方插件(如推送、支付),逐一查看其官方文档,确认它们实际需要的权限,并在代码中通过 plus.android.requestPermissions 动态申请,而非在清单中静态声明。
  • 如果必须使用敏感权限(如位置、相机),确保在隐私政策中明确说明用途,并在代码中实现“权限申请弹窗”,解释为什么需要该权限。

代码示例(在 uni-app 中动态申请权限):
javascript
// 在页面加载时,检查并申请相机权限
plus.android.requestPermissions(
['android.permission.CAMERA'],
function(e) {
if (e.deniedAlways.length > 0) {
// 权限被永久拒绝,引导用户去设置
uni.showModal({
title: '提示',
content: '需要相机权限才能扫码,请前往设置开启',
success: (res) => {
if (res.confirm) {
plus.runtime.openURL('app-settings://');
}
}
});
} else if (e.deniedPresent.length > 0) {
// 权限被临时拒绝,可再次请求
// do nothing
} else {
// 权限已授予,执行扫码逻辑
}
}
);

  1. 谨慎使用 WebView,避免“动态加载”嫌疑
    机器审核对动态加载内容非常警惕,因为它可能被用于绕过审核(如加载违规页面)。在 uni-app 中, 组件是常见的功能载体,但如果你将其指向一个外部 URL,机器可能会标记为“不可控内容”。

步骤:

  • 尽量使用本地 HTML 文件,而非远程 URL。例如,将 H5 页面打包到应用资源中,通过 src="/static/page.html" 加载。
  • 如果必须加载远程页面,确保该域名在隐私政策中列出,并且页面内容不包含违规信息(如赌博、色情)。同时,在 manifest.json 中配置 标签,明确声明你访问的域名。
  • 避免在 WebView 中执行复杂的 JavaScript 代码,例如动态创建 iframe 或加载外部脚本。这些行为容易被机器视为“恶意注入”。
  1. 完善隐私政策,应对人工审核
    人工审核员会仔细阅读你的隐私政策,并对照应用的实际行为。很多 uni-app 开发者直接在隐私政策中粘贴模板,然后忘记更新,导致与实际功能脱节。例如,你使用了友盟统计,但隐私政策中只字不提,人工审核员会直接拒绝。

步骤:

  • 在隐私政策中逐条列出所有收集数据的 SDK,包括名称、用途、收集的数据类型(如设备 ID、IP 地址)。对于 uni-app 常用的插件(如 uni-push、uni-AD),也要单独说明。
  • 在应用首次启动时,实现一个“隐私政策弹窗”,用户必须点击“同意”后才能进入主界面。这个弹窗需要包含“阅读并同意”按钮,并且链接到完整的隐私政策页面。
  • 如果应用涉及用户生成内容(UGC),在隐私政策中明确声明你的审查机制,例如“用户发布的内容会经过人工审核”。

代码示例(uni-app 中实现隐私政策弹窗):
vue

        欢迎使用本应用,请阅读并同意隐私政策  
        查看隐私政策  
        同意  

export default {
data() {
return { privacyAgreed: false };
},
methods: {
agreePrivacy() {
// 将同意状态存储到本地,下次启动不再弹窗
uni.setStorageSync('privacyAgreed', true);
this.privacyAgreed = true;
}
},
onLoad() {
this.privacyAgreed = uni.getStorageSync('privacyAgreed') || false;
}
}

  1. 避免代码混淆与动态下载
    机器审核会扫描 APK 中的代码结构,如果发现大量混淆或动态加载的 Dex 文件,会认为应用存在“隐形行为”。uni-app 应用本身使用 V8 引擎执行 JavaScript,这部分代码相对透明,但如果你引入了原生插件,这些插件可能经过混淆。

步骤:

  • 对于自定义原生插件,尽量使用 ProGuard 的默认规则,避免过度混淆。特别是插件暴露给 JS 层的接口,不要混淆,否则会导致运行时错误。
  • 不要在运行时从远程服务器下载代码或资源(如更新插件)。如果需要热更新,使用 uni-app 的官方热更新机制(通过 App 资源更新),而非自行实现动态加载。
  • 如果必须使用动态加载(如加载 A/B 测试代码),确保相关逻辑在隐私政策中说明,并限制在非核心功能上。
  1. 提前测试,模拟机器审核
    在提交前,使用 Google Play Console 的“预发布报告”功能,它能模拟机器审核的扫描结果。另外,也可以借助第三方工具,如 AppBrain 或 DCloud 的打包检测工具,检查权限和 API 使用情况。

步骤:

  • 在 Google Play Console 中,进入“发布”->“测试”,上传 APK 或 AAB 后,查看“预发布报告”。它会列出潜在的违规项,如权限滥用、SSL 错误等。
  • 使用 Android Studio 的 Profile 工具,查看应用运行时实际申请的权限,与 manifest.json 中的声明对比,删除冗余项。
  • 在 DCloud 社区中,搜索“Google Play 审核”关键词,查看其他开发者的经验贴,特别是针对同一插件(如 uni-push)的审核案例。

注意事项:常见“雷区”与应对策略

  • 不要忽视 SDK 版本更新:Google Play 每年都会更新审核规则,例如要求所有应用必须适配 Android 13 的“通知权限”或“照片选择器”。如果你的 uni-app 应用使用的旧版 SDK(如旧版 uni-push)没有适配,机器会直接拒绝。建议定期检查 DCloud 插件市场,更新到最新版本。
  • 避免在描述中夸大功能:机器审核会读取应用标题、描述和关键词。如果你写“免费无限存储”,但实际只有 1GB 空间,会被标记为“虚假描述”。人工审核员也会对比描述和实际功能。
  • 注意多语言版本:如果你的应用支持多语言,确保每个语言版本的隐私政策和权限声明都同步更新。否则,人工审核员测试中文版时发现权限描述与英文版不一致,同样会被拒绝。
  • 处理用户举报:一旦应用被用户举报(如收集隐私数据),审核会立即转交人工处理。此时,你需要在 Google Play Console 中提供详细的技术说明,解释数据收集的必要性,并附上隐私政策链接。

总结:知己知彼,百战不殆

回到最初的问题:Google Play 审核到底是机器还是人工?答案是两者皆有,且分工明确。机器负责快速过滤硬性违规,人工则处理需要判断力的复杂情况。对于 uni-app 开发者来说,理解这一机制的核心意义在于:将应用设计成“机器友好”且“人工可解释”。

具体来说,你需要做到:

  • 精简权限声明,让机器一眼看出你的应用是“无辜”的。
  • 完善隐私政策,让人工审核员觉得你的应用是“透明”的。
  • 避免动态加载和过度混淆,让代码路径清晰可见。
  • 提前使用工具测试,把问题消灭在提交前。

最后,记住 Google Play 审核不是一次性的,而是一个持续的过程。随着应用更新,审核规则也可能变化。作为 DCloud 社区的一员,我建议你定期关注官方文档和社区动态,及时调整开发策略。毕竟,让应用顺利上架,才是我们共同的目标。

继续阅读 »

Google Play 审核到底是机器还是人工?——DCloud 开发者实战分析

背景:Google Play 审核,开发者心中的“黑箱”

作为一名长期使用 uni-app 开发跨平台应用的开发者,我深知 Google Play 审核的复杂性。很多同行在 DCloud 社区里抱怨,明明在本地测试一切正常,提交到 Google Play 却被拒绝,而且拒绝理由往往模糊不清,比如“违反广告政策”“隐私政策不完整”或“权限使用不当”。更让人困惑的是,有时修改一个小问题后秒过,有时却要等上几天,甚至反复提交多次才能通过。于是,一个核心问题浮出水面:Google Play 的审核到底是由机器自动完成的,还是有人工介入?这个问题直接关系到我们如何优化应用、避免踩坑。

实际上,Google Play 的审核机制并非单一模式,而是机器与人工结合的混合系统。机器负责初步筛选和自动化检查,人工则处理复杂、模糊或高风险的案例。对于使用 uni-app 框架的开发者来说,理解这一机制至关重要,因为我们的应用通常依赖 WebView、原生插件和第三方 SDK,这些都可能触发机器审核的“雷区”。本文将从实战角度,分析 Google Play 审核的底层逻辑,并给出针对 uni-app 开发者的具体解决方案。

问题分析:机器审核的“硬伤”与人工审核的“盲区”

机器审核:速度与规则的博弈
机器审核是 Google Play 的第一道防线,它通过自动化脚本和算法扫描应用的代码、元数据、权限声明等。机器擅长处理确定性规则,比如检查是否包含恶意代码、是否使用了违规的 API(如非必要的后台位置权限),或者是否在描述中夸大了功能。例如,如果你的 uni-app 应用在 manifest.json 中声明了 android.permission.ACCESS_BACKGROUND_LOCATION,但实际功能不涉及后台定位,机器会直接标记为“权限滥用”,并拒绝提交。

机器审核的优点是速度快,通常在几分钟到几小时内完成。但它的缺点也很明显:缺乏上下文理解。比如,你的应用使用了一个第三方推送 SDK,该 SDK 在运行时动态申请了 READ_PHONE_STATE 权限,机器会认为这是应用主动申请的,从而触发“隐私政策不完整”的警告。实际上,这个权限只是 SDK 用于设备标识,但机器不会去深究 SDK 的文档。

人工审核:深度与效率的妥协
当机器无法做出明确判断时,或者应用被用户举报、被系统标记为高风险(如涉及金融、医疗或儿童内容),审核就会转交给人工团队。人工审核员会手动安装应用、测试功能、阅读隐私政策,甚至模拟用户行为。例如,如果你的 uni-app 应用包含一个 WebView 内嵌的 H5 页面,该页面加载了第三方广告,人工审核员可能会检查广告内容是否合规,或者是否在用户不知情的情况下收集了数据。

人工审核的优点是能处理复杂场景,比如判断某段代码是“功能需要”还是“恶意收集”。但它的缺点是效率低,且主观性强。同一款应用,不同审核员可能给出不同结论,这就是为什么有时修改一个标点符号就能通过,而有时调整整个功能架构却依然被拒。

对 uni-app 开发者的特殊影响
uni-app 应用本质上是混合架构,前端代码运行在 WebView 中,原生功能通过插件桥接。这种架构容易让机器审核产生误判。例如,机器可能将 WebView 的 JavaScript 代码视为动态加载内容,从而怀疑应用存在“代码混淆”或“动态下载”行为。而人工审核员如果不懂跨平台技术,可能会错误地认为 WebView 加载的 H5 页面是“第三方内容”,要求提供额外的合规证明。因此,理解审核机制后,我们需要针对性地调整开发策略。

解决方案:如何让 uni-app 应用顺利通过审核

  1. 优化权限声明,避免“机器误判”
    机器审核最敏感的就是权限。在 uni-app 中,权限声明集中在 manifest.json 的 distribute 节点下。很多开发者为了方便,直接复制网上的权限列表,结果包含了大量不必要的权限。例如,android.permission.CAMERA 和 android.permission.RECORD_AUDIO 只应在实际用到时声明。

步骤:

  • 检查 manifest.json 中的 标签,只保留应用核心功能所需的权限。
  • 对于第三方插件(如推送、支付),逐一查看其官方文档,确认它们实际需要的权限,并在代码中通过 plus.android.requestPermissions 动态申请,而非在清单中静态声明。
  • 如果必须使用敏感权限(如位置、相机),确保在隐私政策中明确说明用途,并在代码中实现“权限申请弹窗”,解释为什么需要该权限。

代码示例(在 uni-app 中动态申请权限):
javascript
// 在页面加载时,检查并申请相机权限
plus.android.requestPermissions(
['android.permission.CAMERA'],
function(e) {
if (e.deniedAlways.length > 0) {
// 权限被永久拒绝,引导用户去设置
uni.showModal({
title: '提示',
content: '需要相机权限才能扫码,请前往设置开启',
success: (res) => {
if (res.confirm) {
plus.runtime.openURL('app-settings://');
}
}
});
} else if (e.deniedPresent.length > 0) {
// 权限被临时拒绝,可再次请求
// do nothing
} else {
// 权限已授予,执行扫码逻辑
}
}
);

  1. 谨慎使用 WebView,避免“动态加载”嫌疑
    机器审核对动态加载内容非常警惕,因为它可能被用于绕过审核(如加载违规页面)。在 uni-app 中, 组件是常见的功能载体,但如果你将其指向一个外部 URL,机器可能会标记为“不可控内容”。

步骤:

  • 尽量使用本地 HTML 文件,而非远程 URL。例如,将 H5 页面打包到应用资源中,通过 src="/static/page.html" 加载。
  • 如果必须加载远程页面,确保该域名在隐私政策中列出,并且页面内容不包含违规信息(如赌博、色情)。同时,在 manifest.json 中配置 标签,明确声明你访问的域名。
  • 避免在 WebView 中执行复杂的 JavaScript 代码,例如动态创建 iframe 或加载外部脚本。这些行为容易被机器视为“恶意注入”。
  1. 完善隐私政策,应对人工审核
    人工审核员会仔细阅读你的隐私政策,并对照应用的实际行为。很多 uni-app 开发者直接在隐私政策中粘贴模板,然后忘记更新,导致与实际功能脱节。例如,你使用了友盟统计,但隐私政策中只字不提,人工审核员会直接拒绝。

步骤:

  • 在隐私政策中逐条列出所有收集数据的 SDK,包括名称、用途、收集的数据类型(如设备 ID、IP 地址)。对于 uni-app 常用的插件(如 uni-push、uni-AD),也要单独说明。
  • 在应用首次启动时,实现一个“隐私政策弹窗”,用户必须点击“同意”后才能进入主界面。这个弹窗需要包含“阅读并同意”按钮,并且链接到完整的隐私政策页面。
  • 如果应用涉及用户生成内容(UGC),在隐私政策中明确声明你的审查机制,例如“用户发布的内容会经过人工审核”。

代码示例(uni-app 中实现隐私政策弹窗):
vue

        欢迎使用本应用,请阅读并同意隐私政策  
        查看隐私政策  
        同意  

export default {
data() {
return { privacyAgreed: false };
},
methods: {
agreePrivacy() {
// 将同意状态存储到本地,下次启动不再弹窗
uni.setStorageSync('privacyAgreed', true);
this.privacyAgreed = true;
}
},
onLoad() {
this.privacyAgreed = uni.getStorageSync('privacyAgreed') || false;
}
}

  1. 避免代码混淆与动态下载
    机器审核会扫描 APK 中的代码结构,如果发现大量混淆或动态加载的 Dex 文件,会认为应用存在“隐形行为”。uni-app 应用本身使用 V8 引擎执行 JavaScript,这部分代码相对透明,但如果你引入了原生插件,这些插件可能经过混淆。

步骤:

  • 对于自定义原生插件,尽量使用 ProGuard 的默认规则,避免过度混淆。特别是插件暴露给 JS 层的接口,不要混淆,否则会导致运行时错误。
  • 不要在运行时从远程服务器下载代码或资源(如更新插件)。如果需要热更新,使用 uni-app 的官方热更新机制(通过 App 资源更新),而非自行实现动态加载。
  • 如果必须使用动态加载(如加载 A/B 测试代码),确保相关逻辑在隐私政策中说明,并限制在非核心功能上。
  1. 提前测试,模拟机器审核
    在提交前,使用 Google Play Console 的“预发布报告”功能,它能模拟机器审核的扫描结果。另外,也可以借助第三方工具,如 AppBrain 或 DCloud 的打包检测工具,检查权限和 API 使用情况。

步骤:

  • 在 Google Play Console 中,进入“发布”->“测试”,上传 APK 或 AAB 后,查看“预发布报告”。它会列出潜在的违规项,如权限滥用、SSL 错误等。
  • 使用 Android Studio 的 Profile 工具,查看应用运行时实际申请的权限,与 manifest.json 中的声明对比,删除冗余项。
  • 在 DCloud 社区中,搜索“Google Play 审核”关键词,查看其他开发者的经验贴,特别是针对同一插件(如 uni-push)的审核案例。

注意事项:常见“雷区”与应对策略

  • 不要忽视 SDK 版本更新:Google Play 每年都会更新审核规则,例如要求所有应用必须适配 Android 13 的“通知权限”或“照片选择器”。如果你的 uni-app 应用使用的旧版 SDK(如旧版 uni-push)没有适配,机器会直接拒绝。建议定期检查 DCloud 插件市场,更新到最新版本。
  • 避免在描述中夸大功能:机器审核会读取应用标题、描述和关键词。如果你写“免费无限存储”,但实际只有 1GB 空间,会被标记为“虚假描述”。人工审核员也会对比描述和实际功能。
  • 注意多语言版本:如果你的应用支持多语言,确保每个语言版本的隐私政策和权限声明都同步更新。否则,人工审核员测试中文版时发现权限描述与英文版不一致,同样会被拒绝。
  • 处理用户举报:一旦应用被用户举报(如收集隐私数据),审核会立即转交人工处理。此时,你需要在 Google Play Console 中提供详细的技术说明,解释数据收集的必要性,并附上隐私政策链接。

总结:知己知彼,百战不殆

回到最初的问题:Google Play 审核到底是机器还是人工?答案是两者皆有,且分工明确。机器负责快速过滤硬性违规,人工则处理需要判断力的复杂情况。对于 uni-app 开发者来说,理解这一机制的核心意义在于:将应用设计成“机器友好”且“人工可解释”。

具体来说,你需要做到:

  • 精简权限声明,让机器一眼看出你的应用是“无辜”的。
  • 完善隐私政策,让人工审核员觉得你的应用是“透明”的。
  • 避免动态加载和过度混淆,让代码路径清晰可见。
  • 提前使用工具测试,把问题消灭在提交前。

最后,记住 Google Play 审核不是一次性的,而是一个持续的过程。随着应用更新,审核规则也可能变化。作为 DCloud 社区的一员,我建议你定期关注官方文档和社区动态,及时调整开发策略。毕竟,让应用顺利上架,才是我们共同的目标。

收起阅读 »

如何解决Navigation路由调用pop后onPop回调代码不执行的问题

问题现象
使用Navigation构建路由,从pageOne通过pushPath跳转到pageTwo,期望pageOne的onPop回调在pageTwo返回时被触发,但效果未达预期。

问题代码示例参考如下:

class ParamWithOp {
operation: number = 1
count: number = 10
}

@Entry
@Component
struct PageOne {
pageInfo: NavPathStack = new NavPathStack();
@State message: string = 'Hello World'

@Builder
pageMap(name: string, params: Object) {
if (name === 'pageTwo') {
PageTwo()
}
}

build() {
Navigation(this.pageInfo) {
Column() {
Text(this.message)
.width('80%')
.height(50)
.margin(10)

    Button('pushPath', { stateEffect: true, type: ButtonType.Capsule })  
      .width('80%')  
      .height(40)  
      .margin(10)  
      .onClick(() => {  
        // 将name指定的NavDestination页面信息入栈,传递的数据为param,添加接收处理结果的onPop回调。  
        this.pageInfo.pushPath({  
          name: 'pageTwo', param: new ParamWithOp(), onPop: (popInfo: PopInfo) => {  
            this.message = `[pushPath]last page is: ${popInfo.info.name} result: ${JSON.stringify(popInfo.result)}`  
          }  
        });  
      })  
  }.width('100%').height('100%')  
}.navDestination(this.pageMap)  
.title('pageOne')  

}
}

@Component
struct PageTwo {
pathStack: NavPathStack = new NavPathStack()

build() {
NavDestination() {
Column() {
Button('pop', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
// 回退到上一个页面,此处代码,在pop回pageOne页面时,未传参数
this.pathStack.pop();
})
}.width('100%').height('100%')
}.title('pageTwo')
.onReady((context: NavDestinationContext) => {
this.pathStack = context.pathStack
})
}
}
效果预览
点击放大

背景知识
Navigation组件是路由导航的根视图容器,结合导航控制器NavPathStack可实现组件导航。

pushPath:将info指定的NavDestination页面信息入栈。可设置onPop回调函数来接收参数。
pop:弹出路由栈栈顶元素,并触发onPop回调传入页面处理结果。
问题定位
点击放大

查阅官方文档关于pushPath方法的NavPathInfo入参说明,其中的onPop回调函数仅pop、popToName、popToIndex中设置result参数后触发。

分析结论
onPop回调函数需要使用pop、popToName、popToIndex方法返回时设置result参数才会触发,否则不会执行onPop回调。

修改建议
按上节所述,只需在pageTwo中调用pop方法时,传入result参数,即可在pageOne中成功收到onPop的回调。修改问题代码如下:

// 回退到上一个页面,随便传个result即可触发onPop回调
this.pathStack.pop(1);
修改后的运行效果参见效果预览,可以看到,当pageTwo调用pop返回时传入了result参数,在pageOne成功执行了onPop回调,并接收到相关参数。
https://pastebin.com/NRD7bMyP
https://pastebin.com/yDaZxFwG
https://pastebin.com/EDcLGLPS
https://pastebin.com/U3ZxgwDi
https://pastebin.com/DR63sAxA
https://pastebin.com/nSnDNVPq
https://pastebin.com/GjtSqPNS
https://pastebin.com/saHctNS5
https://pastebin.com/kNHqdQnW
https://pastebin.com/JMnQaXK7
https://pastebin.com/vSexS64Q
https://pastebin.com/tRQUHv5L
https://pastebin.com/3JqBJ8wq
https://pastebin.com/xEkHBut4
https://pastebin.com/skZmkkFw
https://pastebin.com/7V0wmLFb
https://pastebin.com/BTZfE8Bd
https://pastebin.com/svjxZfvM
https://pastebin.com/xXYHPU8b
https://pastebin.com/zMLjuJuV
https://pastebin.com/ffbfvSna
https://pastebin.com/C9Zr1K2M
https://pastebin.com/ac9xpyfm
https://pastebin.com/WX45yC8p
https://pastebin.com/YeUrZij0
https://pastebin.com/zKW8HRQb
https://pastebin.com/6KBWHYZG
https://pastebin.com/MTtnz3t8
https://pastebin.com/E7SBb1wr
https://pastebin.com/eib2hRUq
https://pastebin.com/SEeERuCT
https://pastebin.com/8tLt3B2T
https://pastebin.com/R4NJytjp
https://pastebin.com/EauQE9nS
https://pastebin.com/fsCXAP9r
https://pastebin.com/9vFQE41K
https://pastebin.com/xyKY9DF0
https://pastebin.com/4duLr15i
https://pastebin.com/Epk3dbmH
https://pastebin.com/GehEwq1u
https://pastebin.com/HyQm4h82
https://pastebin.com/D5vZs1MG
https://pastebin.com/L2KryS4C
https://pastebin.com/QBtyQipw
https://pastebin.com/FNxMPZ51
https://pastebin.com/trw5d8cC
https://pastebin.com/rPFzkgK8
https://pastebin.com/uCBsSjNm
https://pastebin.com/RGW6X15L
https://pastebin.com/S7cBiuDb
https://pastebin.com/HFyJtEKK
https://pastebin.com/0ubhVneF
https://pastebin.com/RW5trQzf
https://pastebin.com/vCSGjNpZ
https://pastebin.com/SffvfhGR
https://pastebin.com/tt0tYGFq
https://pastebin.com/tMm7YFVa
https://pastebin.com/ibcT2Lf2
https://pastebin.com/JqjbbVRf
https://pastebin.com/HVKWqHGk
https://pastebin.com/s5vt779b
https://pastebin.com/B0TAyxn6
https://pastebin.com/fuPRhvhd
https://pastebin.com/beLXK35p
https://pastebin.com/GFBwqxu6
https://pastebin.com/9VWb8miy
https://pastebin.com/grHVbPND
https://pastebin.com/CYaQEmTS
https://pastebin.com/ZkmuiH18
https://pastebin.com/f2xJpBQ9
https://pastebin.com/2W5h2q09
https://pastebin.com/rAC3TU64
https://pastebin.com/K9pyh9gi
https://pastebin.com/uz28wUPg
https://pastebin.com/cVAkkb9F
https://pastebin.com/RbNxE2S2
https://pastebin.com/hrxFwi1b
https://pastebin.com/6dxqezH3
https://pastebin.com/SQmcrc6u
https://pastebin.com/DmzbU6WX
https://pastebin.com/RQzvD6gG
https://pastebin.com/nWdqiQr9
https://pastebin.com/qSiWmiq9
https://pastebin.com/HbjBAg6q
https://pastebin.com/7sg5aa6M
https://pastebin.com/cGDzhrRb
https://pastebin.com/Q3D2BmaQ
https://pastebin.com/tvZNCwN8
https://pastebin.com/XB4HwUsz
https://pastebin.com/bZDH1VHh
https://pastebin.com/xCPMHyqK
https://pastebin.com/mJJ4U9bH
https://pastebin.com/HrakBMBz
https://pastebin.com/GN9npU0W
https://pastebin.com/nr9hJd8p
https://pastebin.com/2CgD5tqb
https://pastebin.com/tXtkUfHd
https://pastebin.com/wkFA8iGP
https://pastebin.com/7z45qQzr
https://pastebin.com/w5RGPRen
https://pastebin.com/WLJewYNA
https://pastebin.com/bEbNLpBi
https://pastebin.com/Chn8rUkh
https://pastebin.com/jz4F2APq
https://pastebin.com/iRWuPNFz
https://pastebin.com/GmHAMH6f
https://pastebin.com/uJMGcsEG
https://pastebin.com/jPB68a0D
https://pastebin.com/MjC66wkv
https://pastebin.com/S7RDTAM9
https://pastebin.com/0bvuDUM1
https://pastebin.com/8czdKG36
https://pastebin.com/usWbPMRE
https://pastebin.com/kywEMqJp
https://pastebin.com/XJFyGMVu
https://pastebin.com/QZm2TaPm
https://pastebin.com/Qtuytbx1
https://pastebin.com/JCBYPdpc
https://pastebin.com/cVLahV0P
https://pastebin.com/0na2hGU9
https://pastebin.com/qpAgP7Mi
https://pastebin.com/k75PXkp7
https://pastebin.com/DZtU9neL
https://pastebin.com/7Yz1D9Fq
https://pastebin.com/xCwndzDR
https://pastebin.com/bGRUESTt
https://pastebin.com/fsnvCaCQ
https://pastebin.com/NcTVLKBf
https://pastebin.com/dXFykLyA
https://pastebin.com/N2w0m4pJ
https://pastebin.com/XEcYb0wP
https://pastebin.com/r7ZVGBz0
https://pastebin.com/Tz3svy7W
https://pastebin.com/CV8Ety7V
https://pastebin.com/sE3tU6ZT
https://pastebin.com/fCeY1t0n
https://pastebin.com/NXaPbSUG
https://pastebin.com/qt5yxi3M
https://pastebin.com/aTKPHQrG
https://pastebin.com/9SAkknhv
https://pastebin.com/Jx0dN32i
https://pastebin.com/7SyfLKbJ
https://pastebin.com/EX2bMjNe
https://pastebin.com/tdx0tmft
https://pastebin.com/gg9V1ytb
https://pastebin.com/BrBEcy26
https://pastebin.com/PZjmp9Bq
https://pastebin.com/k2TdbGrr
https://pastebin.com/yTV7fVKm
https://pastebin.com/P1a8Qn8J
https://pastebin.com/aekNrH9G
https://pastebin.com/LHvEPcc4
https://pastebin.com/LNzusSGs
https://pastebin.com/sMeA2PNw
https://pastebin.com/kRSKWe7H
https://pastebin.com/T8hXpM8Q
https://pastebin.com/STXJwKnY
https://pastebin.com/EZMpBXrs
https://pastebin.com/eqK7fyHg
https://pastebin.com/JJaAFQJa
https://pastebin.com/DrZAxjY8
https://pastebin.com/QFimQBUz
https://pastebin.com/z1FcAaJv
https://pastebin.com/7BcKWKaL
https://pastebin.com/FcphACWj
https://pastebin.com/BLd4XZSU
https://pastebin.com/NdHCmLXc
https://pastebin.com/ufeKaAdj
https://pastebin.com/U2fQcdaL
https://pastebin.com/XDbEhrEt
https://pastebin.com/gscukzCs
https://pastebin.com/epqMfKhi
https://pastebin.com/n6kDJLQe
https://pastebin.com/K3pCsdsT
https://pastebin.com/29L18WQV
https://pastebin.com/dweu6hGY
https://pastebin.com/i5UhDU4b
https://pastebin.com/4hqqvGJ3
https://pastebin.com/EPm7s12S
https://pastebin.com/L8iZAZmK
https://pastebin.com/jv4NNhvR
https://pastebin.com/igpmbMZv
https://pastebin.com/a3Q6qpTQ
https://pastebin.com/hq9kLWQN
https://pastebin.com/uSD7HbRh
https://pastebin.com/Q04Dqr3D

继续阅读 »

问题现象
使用Navigation构建路由,从pageOne通过pushPath跳转到pageTwo,期望pageOne的onPop回调在pageTwo返回时被触发,但效果未达预期。

问题代码示例参考如下:

class ParamWithOp {
operation: number = 1
count: number = 10
}

@Entry
@Component
struct PageOne {
pageInfo: NavPathStack = new NavPathStack();
@State message: string = 'Hello World'

@Builder
pageMap(name: string, params: Object) {
if (name === 'pageTwo') {
PageTwo()
}
}

build() {
Navigation(this.pageInfo) {
Column() {
Text(this.message)
.width('80%')
.height(50)
.margin(10)

    Button('pushPath', { stateEffect: true, type: ButtonType.Capsule })  
      .width('80%')  
      .height(40)  
      .margin(10)  
      .onClick(() => {  
        // 将name指定的NavDestination页面信息入栈,传递的数据为param,添加接收处理结果的onPop回调。  
        this.pageInfo.pushPath({  
          name: 'pageTwo', param: new ParamWithOp(), onPop: (popInfo: PopInfo) => {  
            this.message = `[pushPath]last page is: ${popInfo.info.name} result: ${JSON.stringify(popInfo.result)}`  
          }  
        });  
      })  
  }.width('100%').height('100%')  
}.navDestination(this.pageMap)  
.title('pageOne')  

}
}

@Component
struct PageTwo {
pathStack: NavPathStack = new NavPathStack()

build() {
NavDestination() {
Column() {
Button('pop', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
// 回退到上一个页面,此处代码,在pop回pageOne页面时,未传参数
this.pathStack.pop();
})
}.width('100%').height('100%')
}.title('pageTwo')
.onReady((context: NavDestinationContext) => {
this.pathStack = context.pathStack
})
}
}
效果预览
点击放大

背景知识
Navigation组件是路由导航的根视图容器,结合导航控制器NavPathStack可实现组件导航。

pushPath:将info指定的NavDestination页面信息入栈。可设置onPop回调函数来接收参数。
pop:弹出路由栈栈顶元素,并触发onPop回调传入页面处理结果。
问题定位
点击放大

查阅官方文档关于pushPath方法的NavPathInfo入参说明,其中的onPop回调函数仅pop、popToName、popToIndex中设置result参数后触发。

分析结论
onPop回调函数需要使用pop、popToName、popToIndex方法返回时设置result参数才会触发,否则不会执行onPop回调。

修改建议
按上节所述,只需在pageTwo中调用pop方法时,传入result参数,即可在pageOne中成功收到onPop的回调。修改问题代码如下:

// 回退到上一个页面,随便传个result即可触发onPop回调
this.pathStack.pop(1);
修改后的运行效果参见效果预览,可以看到,当pageTwo调用pop返回时传入了result参数,在pageOne成功执行了onPop回调,并接收到相关参数。
https://pastebin.com/NRD7bMyP
https://pastebin.com/yDaZxFwG
https://pastebin.com/EDcLGLPS
https://pastebin.com/U3ZxgwDi
https://pastebin.com/DR63sAxA
https://pastebin.com/nSnDNVPq
https://pastebin.com/GjtSqPNS
https://pastebin.com/saHctNS5
https://pastebin.com/kNHqdQnW
https://pastebin.com/JMnQaXK7
https://pastebin.com/vSexS64Q
https://pastebin.com/tRQUHv5L
https://pastebin.com/3JqBJ8wq
https://pastebin.com/xEkHBut4
https://pastebin.com/skZmkkFw
https://pastebin.com/7V0wmLFb
https://pastebin.com/BTZfE8Bd
https://pastebin.com/svjxZfvM
https://pastebin.com/xXYHPU8b
https://pastebin.com/zMLjuJuV
https://pastebin.com/ffbfvSna
https://pastebin.com/C9Zr1K2M
https://pastebin.com/ac9xpyfm
https://pastebin.com/WX45yC8p
https://pastebin.com/YeUrZij0
https://pastebin.com/zKW8HRQb
https://pastebin.com/6KBWHYZG
https://pastebin.com/MTtnz3t8
https://pastebin.com/E7SBb1wr
https://pastebin.com/eib2hRUq
https://pastebin.com/SEeERuCT
https://pastebin.com/8tLt3B2T
https://pastebin.com/R4NJytjp
https://pastebin.com/EauQE9nS
https://pastebin.com/fsCXAP9r
https://pastebin.com/9vFQE41K
https://pastebin.com/xyKY9DF0
https://pastebin.com/4duLr15i
https://pastebin.com/Epk3dbmH
https://pastebin.com/GehEwq1u
https://pastebin.com/HyQm4h82
https://pastebin.com/D5vZs1MG
https://pastebin.com/L2KryS4C
https://pastebin.com/QBtyQipw
https://pastebin.com/FNxMPZ51
https://pastebin.com/trw5d8cC
https://pastebin.com/rPFzkgK8
https://pastebin.com/uCBsSjNm
https://pastebin.com/RGW6X15L
https://pastebin.com/S7cBiuDb
https://pastebin.com/HFyJtEKK
https://pastebin.com/0ubhVneF
https://pastebin.com/RW5trQzf
https://pastebin.com/vCSGjNpZ
https://pastebin.com/SffvfhGR
https://pastebin.com/tt0tYGFq
https://pastebin.com/tMm7YFVa
https://pastebin.com/ibcT2Lf2
https://pastebin.com/JqjbbVRf
https://pastebin.com/HVKWqHGk
https://pastebin.com/s5vt779b
https://pastebin.com/B0TAyxn6
https://pastebin.com/fuPRhvhd
https://pastebin.com/beLXK35p
https://pastebin.com/GFBwqxu6
https://pastebin.com/9VWb8miy
https://pastebin.com/grHVbPND
https://pastebin.com/CYaQEmTS
https://pastebin.com/ZkmuiH18
https://pastebin.com/f2xJpBQ9
https://pastebin.com/2W5h2q09
https://pastebin.com/rAC3TU64
https://pastebin.com/K9pyh9gi
https://pastebin.com/uz28wUPg
https://pastebin.com/cVAkkb9F
https://pastebin.com/RbNxE2S2
https://pastebin.com/hrxFwi1b
https://pastebin.com/6dxqezH3
https://pastebin.com/SQmcrc6u
https://pastebin.com/DmzbU6WX
https://pastebin.com/RQzvD6gG
https://pastebin.com/nWdqiQr9
https://pastebin.com/qSiWmiq9
https://pastebin.com/HbjBAg6q
https://pastebin.com/7sg5aa6M
https://pastebin.com/cGDzhrRb
https://pastebin.com/Q3D2BmaQ
https://pastebin.com/tvZNCwN8
https://pastebin.com/XB4HwUsz
https://pastebin.com/bZDH1VHh
https://pastebin.com/xCPMHyqK
https://pastebin.com/mJJ4U9bH
https://pastebin.com/HrakBMBz
https://pastebin.com/GN9npU0W
https://pastebin.com/nr9hJd8p
https://pastebin.com/2CgD5tqb
https://pastebin.com/tXtkUfHd
https://pastebin.com/wkFA8iGP
https://pastebin.com/7z45qQzr
https://pastebin.com/w5RGPRen
https://pastebin.com/WLJewYNA
https://pastebin.com/bEbNLpBi
https://pastebin.com/Chn8rUkh
https://pastebin.com/jz4F2APq
https://pastebin.com/iRWuPNFz
https://pastebin.com/GmHAMH6f
https://pastebin.com/uJMGcsEG
https://pastebin.com/jPB68a0D
https://pastebin.com/MjC66wkv
https://pastebin.com/S7RDTAM9
https://pastebin.com/0bvuDUM1
https://pastebin.com/8czdKG36
https://pastebin.com/usWbPMRE
https://pastebin.com/kywEMqJp
https://pastebin.com/XJFyGMVu
https://pastebin.com/QZm2TaPm
https://pastebin.com/Qtuytbx1
https://pastebin.com/JCBYPdpc
https://pastebin.com/cVLahV0P
https://pastebin.com/0na2hGU9
https://pastebin.com/qpAgP7Mi
https://pastebin.com/k75PXkp7
https://pastebin.com/DZtU9neL
https://pastebin.com/7Yz1D9Fq
https://pastebin.com/xCwndzDR
https://pastebin.com/bGRUESTt
https://pastebin.com/fsnvCaCQ
https://pastebin.com/NcTVLKBf
https://pastebin.com/dXFykLyA
https://pastebin.com/N2w0m4pJ
https://pastebin.com/XEcYb0wP
https://pastebin.com/r7ZVGBz0
https://pastebin.com/Tz3svy7W
https://pastebin.com/CV8Ety7V
https://pastebin.com/sE3tU6ZT
https://pastebin.com/fCeY1t0n
https://pastebin.com/NXaPbSUG
https://pastebin.com/qt5yxi3M
https://pastebin.com/aTKPHQrG
https://pastebin.com/9SAkknhv
https://pastebin.com/Jx0dN32i
https://pastebin.com/7SyfLKbJ
https://pastebin.com/EX2bMjNe
https://pastebin.com/tdx0tmft
https://pastebin.com/gg9V1ytb
https://pastebin.com/BrBEcy26
https://pastebin.com/PZjmp9Bq
https://pastebin.com/k2TdbGrr
https://pastebin.com/yTV7fVKm
https://pastebin.com/P1a8Qn8J
https://pastebin.com/aekNrH9G
https://pastebin.com/LHvEPcc4
https://pastebin.com/LNzusSGs
https://pastebin.com/sMeA2PNw
https://pastebin.com/kRSKWe7H
https://pastebin.com/T8hXpM8Q
https://pastebin.com/STXJwKnY
https://pastebin.com/EZMpBXrs
https://pastebin.com/eqK7fyHg
https://pastebin.com/JJaAFQJa
https://pastebin.com/DrZAxjY8
https://pastebin.com/QFimQBUz
https://pastebin.com/z1FcAaJv
https://pastebin.com/7BcKWKaL
https://pastebin.com/FcphACWj
https://pastebin.com/BLd4XZSU
https://pastebin.com/NdHCmLXc
https://pastebin.com/ufeKaAdj
https://pastebin.com/U2fQcdaL
https://pastebin.com/XDbEhrEt
https://pastebin.com/gscukzCs
https://pastebin.com/epqMfKhi
https://pastebin.com/n6kDJLQe
https://pastebin.com/K3pCsdsT
https://pastebin.com/29L18WQV
https://pastebin.com/dweu6hGY
https://pastebin.com/i5UhDU4b
https://pastebin.com/4hqqvGJ3
https://pastebin.com/EPm7s12S
https://pastebin.com/L8iZAZmK
https://pastebin.com/jv4NNhvR
https://pastebin.com/igpmbMZv
https://pastebin.com/a3Q6qpTQ
https://pastebin.com/hq9kLWQN
https://pastebin.com/uSD7HbRh
https://pastebin.com/Q04Dqr3D

收起阅读 »

如何实现不同分组间元素拖拽切换效果

问题现象
如下图所示,需求是现在有A、B两个组,A、B两组中的元素可以拖动,并且A组中的元素可以拖动到B组,B组的元素同样可以拖动到A组,请问如何实现这种多组之间相互拖拽的效果?

点击放大

背景知识
使用Grid组件构建网格元素布局,启动editMode编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem。
onItemDragStart和onItemDrop事件分别在开始拖拽网格元素时触发和停止拖拽时触发,通过事件的组合完成交换数组位置的逻辑。
解决方案
使用Grid布局构建界面。其中,columnsTemplate可设置当前网格布局列的数量、固定列宽或最小列宽值;columnsGap可设置列与列的间距;rowsGap可设置行与行的间距。
Grid(this.scroller) {
GridItem() {
Text('标题' + this.numbers.length)
.fontSize(16)
.width('100%')
.height(30)
.padding({ left: 10 })
.textAlign(TextAlign.Start);
}
.onClick(() => {
this.isShowGroup1 = !this.isShowGroup1;
});

if (this.isShowGroup1) {
ForEach(this.numbers, (day: string) => {
GridItem() {
Text(day)
.fontSize(16)
.backgroundColor('#f1f3f5')
.width('100%')
.height(30)
.textAlign(TextAlign.Center);
};
});
}
GridItem() {
Text('标题' + this.numbers2.length)
.fontSize(16)
.width('100%')
.height(30)
.padding({ left: 10 })
.textAlign(TextAlign.Start);
}
.onClick(() => {
this.isShowGroup2 = !this.isShowGroup2;
});

if (this.isShowGroup2) {
ForEach(this.numbers2, (day: string) => {
GridItem() {
Text(day)
.fontSize(16)
.backgroundColor('#f1f3f5')
.width('100%')
.height(30)
.textAlign(TextAlign.Center);
};
});
}
}
.columnsTemplate('1fr')
.columnsGap(2)
.rowsGap(2)
.scrollBar(BarState.Off)
给Grid组件设置editMode为true,即Grid进入编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem。设置supportAnimation为true,即Grid拖拽元素时支持动画。
.supportAnimation(true)
.editMode(true) // 设置Grid是否进入编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem
定义GridItem被拖拽时元素的样式,元素被拖拽时展示浮动内容。
@Builder
pixelMapBuilder() { // 拖拽过程样式
Column() {
Text('浮动内容' + this.text)
.fontSize(16)
.backgroundColor('#f1f3f5')
.width('90%')
.height(30)
.textAlign(TextAlign.Center);
};
}
判断当前移动的是不是标题,如果是标题禁止拖动。
// 判断是否是标题
isTitle(index: number) {
if (index === 0) {
return true;
}
if (!this.isShowGroup1 && index === 1) {
return true;
}
if (this.isShowGroup1 && index === this.numbers.length + 1) {
return true;
}
return false;
}
定义拖拽过程中的数组交换逻辑。
moveToIndex(index1: number, index2: number) { // 将index1位置的元素移动至 index2位置
let temp = '';
let num1Length = 1 + (this.numbers.length - 1) * Number(this.isShowGroup1);
if (index1 <= num1Length) {
temp = this.numbers.splice(index1 - 1, 1)[0];
} else {
index1 = index1 - this.numbers.length - 1;
temp = this.numbers2.splice(index1 - 1, 1)[0];
}
if (index2 <= num1Length) {
this.numbers.splice(index2 - 1, 0, temp);
} else {
index2 = index2 - this.numbers.length - 1;
this.numbers2.splice(index2 - 1, 0, temp);
}
}
给Grid组件绑定onItemDragStart和onItemDrop事件,在onItemDragStart回调中设置拖拽过程中显示的图片,并在onItemDrop中完成交换数组位置的逻辑。
.onItemDragStart((event: ItemDragInfo, itemIndex: number) => { // 第一次拖拽此事件绑定的组件时,触发回调。
if (this.isTitle(itemIndex)) {
return;
}
if (itemIndex > this.numbers.length) {
itemIndex = itemIndex - this.numbers.length - 1;
this.text = this.numbers2[itemIndex - 1];
} else {
this.text = this.numbers[itemIndex - 1];
}
return this.pixelMapBuilder(); // 设置拖拽过程中显示的图片。
})
.onItemDrop((event: ItemDragInfo, itemIndex: number,
insertIndex: number) => { // 绑定此事件的组件可作为拖拽释放目标,当在本组件范围内停止拖拽行为时,触发回调。
console.info(eventX: ${event.x});
this.moveToIndex(itemIndex, insertIndex);
});
完整示例如下:

@Entry
@Component
struct GridDemo {
@State numbers: string[] = [];
@State numbers2: string[] = [];
@State isShowGroup1: boolean = true;
@State isShowGroup2: boolean = true;
@State text: string = 'drag';
scroller: Scroller = new Scroller();

@Builder
pixelMapBuilder() { // 拖拽过程样式
Column() {
Text('浮动内容' + this.text)
.fontSize(16)
.backgroundColor('#f1f3f5')
.width('90%')
.height(30)
.textAlign(TextAlign.Center);
};
}

aboutToAppear() {
for (let i = 1; i <= 15; i++) {
this.numbers.push('组' + i);
this.numbers2.push('组' + i);
}
}

moveToIndex(index1: number, index2: number) { // 将index1位置的元素移动至 index2位置
let temp = '';
let num1Length = 1 + (this.numbers.length - 1) * Number(this.isShowGroup1);
if (index1 <= num1Length) {
temp = this.numbers.splice(index1 - 1, 1)[0];
} else {
index1 = index1 - this.numbers.length - 1;
temp = this.numbers2.splice(index1 - 1, 1)[0];
}
if (index2 <= num1Length) {
this.numbers.splice(index2 - 1, 0, temp);
} else {
index2 = index2 - this.numbers.length - 1;
this.numbers2.splice(index2 - 1, 0, temp);
}
}

// 判断是否是标题
isTitle(index: number) {
if (index === 0) {
return true;
}
if (!this.isShowGroup1 && index === 1) {
return true;
}
if (this.isShowGroup1 && index === this.numbers.length + 1) {
return true;
}
return false;
}

build() {
Column({ space: 5 }) {
Column() {
Grid(this.scroller) {
GridItem() {
Text('标题' + this.numbers.length)
.fontSize(16)
.width('100%')
.height(30)
.padding({ left: 10 })
.textAlign(TextAlign.Start);
}
.onClick(() => {
this.isShowGroup1 = !this.isShowGroup1;
});

      if (this.isShowGroup1) {  
        ForEach(this.numbers, (day: string) => {  
          GridItem() {  
            Text(day)  
              .fontSize(16)  
              .backgroundColor('#f1f3f5')  
              .width('100%')  
              .height(30)  
              .textAlign(TextAlign.Center);  
          };  
        });  
      }  
      GridItem() {  
        Text('标题' + this.numbers2.length)  
          .fontSize(16)  
          .width('100%')  
          .height(30)  
          .padding({ left: 10 })  
          .textAlign(TextAlign.Start);  
      }  
      .onClick(() => {  
        this.isShowGroup2 = !this.isShowGroup2;  
      });  

      if (this.isShowGroup2) {  
        ForEach(this.numbers2, (day: string) => {  
          GridItem() {  
            Text(day)  
              .fontSize(16)  
              .backgroundColor('#f1f3f5')  
              .width('100%')  
              .height(30)  
              .textAlign(TextAlign.Center);  
          };  
        });  
      }  
    }  
    .columnsTemplate('1fr')  
    .columnsGap(2)  
    .rowsGap(2)  
    .scrollBar(BarState.Off)  
    .onScrollIndex((first: number) => {  
      console.info(first.toString());  
    })  
    .width('90%')  
    .supportAnimation(true)  
    .editMode(true) // 设置Grid是否进入编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem  
    .onItemDragStart((event: ItemDragInfo, itemIndex: number) => { // 第一次拖拽此事件绑定的组件时,触发回调。  
      if (this.isTitle(itemIndex)) {  
        return;  
      }  
      if (itemIndex > this.numbers.length) {  
        itemIndex = itemIndex - this.numbers.length - 1;  
        this.text = this.numbers2[itemIndex - 1];  
      } else {  
        this.text = this.numbers[itemIndex - 1];  
      }  
      return this.pixelMapBuilder(); // 设置拖拽过程中显示的图片。  
    })  
    .onItemDrop((event: ItemDragInfo, itemIndex: number,  
      insertIndex: number) => { // 绑定此事件的组件可作为拖拽释放目标,当在本组件范围内停止拖拽行为时,触发回调。  
      console.info(`eventX: ${event.x}`);  
      this.moveToIndex(itemIndex, insertIndex);  
    });  

  };  
}.width('100%').margin({ top: 5 });  

}
}
https://pastebin.com/ALZ1heJ8
https://pastebin.com/NUA8cAMq
https://pastebin.com/DU7BW7SP
https://pastebin.com/Sh2K4277
https://pastebin.com/MKNCS5xM
https://pastebin.com/jBqWTCsQ
https://pastebin.com/TNhaEKd7
https://pastebin.com/pAcyuLWk
https://pastebin.com/BccwcGVK
https://pastebin.com/ETXeKqiW
https://pastebin.com/DRxMx41K
https://pastebin.com/L15SkLc9
https://pastebin.com/Pyz2h7xc
https://pastebin.com/LWtxbvUn
https://pastebin.com/S7wbsPSF
https://pastebin.com/XmPzcDSQ
https://pastebin.com/zJ2kJ7ZA
https://pastebin.com/wknFnVct
https://pastebin.com/Jx9edSj2
https://pastebin.com/s7w3uv0a
https://pastebin.com/JHwj1NaL
https://pastebin.com/RzdjWfNv
https://pastebin.com/9G3P7TeA
https://pastebin.com/sG5wxN1u
https://pastebin.com/hJ2c716v
https://pastebin.com/4exdhVkn
https://pastebin.com/hUVhcvMP
https://pastebin.com/A4LnBDtM
https://pastebin.com/L6FwB3N9
https://pastebin.com/pt0KRF9P
https://pastebin.com/93GGDNJv
https://pastebin.com/DHdskaAf
https://pastebin.com/1QH5dm9R
https://pastebin.com/P30Qqeif
https://pastebin.com/ngQjFN3W
https://pastebin.com/BRAv0z5d
https://pastebin.com/fdYZAvMX
https://pastebin.com/ciXgk5F6
https://pastebin.com/RjC4wjc9
https://pastebin.com/kEaUKCTq
https://pastebin.com/C3JuxCrQ
https://pastebin.com/y0KUAU4u
https://pastebin.com/nW5AFBcM
https://pastebin.com/yR9yyNZ5
https://pastebin.com/zvH6TX45
https://pastebin.com/RsL54ULk
https://pastebin.com/TRTYXt4k
https://pastebin.com/zCGW0vif
https://pastebin.com/6drbFd0j
https://pastebin.com/D7urfsq6
https://pastebin.com/119esj24
https://pastebin.com/ndHUJCr4
https://pastebin.com/ciSWy0ut
https://pastebin.com/iKf50her
https://pastebin.com/RkE5yyn8
https://pastebin.com/jj99Wa7u
https://pastebin.com/iwJ5us6j
https://pastebin.com/hSuvYeqx
https://pastebin.com/6YQNbNZ6
https://pastebin.com/SsUF39yN
https://pastebin.com/xLFVFWSd
https://pastebin.com/8C1YCkb7
https://pastebin.com/PBRiQwEE
https://pastebin.com/CZw00CFD
https://pastebin.com/MhC3vvEH
https://pastebin.com/NGucrr8R
https://pastebin.com/5inLc8eS
https://pastebin.com/rnNKvRei
https://pastebin.com/LDXYYnhG
https://pastebin.com/5YpXtQeT
https://pastebin.com/mb5JFUs5
https://pastebin.com/HDzxgFHY
https://pastebin.com/Ssx5zf6i
https://pastebin.com/qMCRpKuL
https://pastebin.com/YMyidNzi
https://pastebin.com/vSwjFPu0
https://pastebin.com/yFPJgYtk
https://pastebin.com/5FLbpDXJ
https://pastebin.com/vRj3ezdr
https://pastebin.com/mYUwfwNk
https://pastebin.com/dTx5DQSb
https://pastebin.com/e7jXBCMc
https://pastebin.com/dxgE2bcC
https://pastebin.com/STW6QhxS
https://pastebin.com/bZhrAr9R
https://pastebin.com/nfYTP7Au
https://pastebin.com/aWunSSVB
https://pastebin.com/zh4pAmyd
https://pastebin.com/3pqzBPyc
https://pastebin.com/ddwcDZHC
https://pastebin.com/2ZqNLikt
https://pastebin.com/56U4rWjG
https://pastebin.com/w24SbjAy
https://pastebin.com/by3Jn4Pq
https://pastebin.com/9UkiAZMK
https://pastebin.com/MxhA05RB
https://pastebin.com/4Jshmpfb
https://pastebin.com/L5sHYH9N
https://pastebin.com/0bJWTV7h
https://pastebin.com/AwM6iDiL
https://pastebin.com/Lsr4Cxkg
https://pastebin.com/LAfLC3a3
https://pastebin.com/f7wTnKEC
https://pastebin.com/HSEbHCyh
https://pastebin.com/uv6Hqjs4
https://pastebin.com/i2V8QUcy
https://pastebin.com/ZAPYfBNk
https://pastebin.com/waMXDnj3
https://pastebin.com/GMUKBZUs
https://pastebin.com/pezF3Vbk
https://pastebin.com/n7DYUQ5v
https://pastebin.com/cyT65uSE
https://pastebin.com/sx2iJqcn
https://pastebin.com/ZPqkWKaR
https://pastebin.com/HEyb77U9
https://pastebin.com/4FZufveK
https://pastebin.com/uExv5h76
https://pastebin.com/Ac7CHJc7
https://pastebin.com/M4s8ZR79
https://pastebin.com/nys7xPvA
https://pastebin.com/PehqarxQ
https://pastebin.com/RbdWT272
https://pastebin.com/UfFk8r7t
https://pastebin.com/y1XYanaB
https://pastebin.com/VmifaeiF
https://pastebin.com/2dDAckiN
https://pastebin.com/FAfELYhc
https://pastebin.com/QCFiEVnQ
https://pastebin.com/QbgF4PzX
https://pastebin.com/X4yVdKxp
https://pastebin.com/cuqhnADr
https://pastebin.com/8iUfUnL8
https://pastebin.com/xhFdDU3m
https://pastebin.com/5Yt1eLAQ
https://pastebin.com/XNDHeeM8
https://pastebin.com/kJXfXZ8R
https://pastebin.com/hxwe46wW
https://pastebin.com/B4mB17YB
https://pastebin.com/GQbTpcbY
https://pastebin.com/DpYauvxs
https://pastebin.com/1gsWBrLW
https://pastebin.com/g9WsSEzm
https://pastebin.com/hzqj45RD
https://pastebin.com/vWKXzBJ9
https://pastebin.com/GTajTfJB
https://pastebin.com/d52vdzg9
https://pastebin.com/w1xQpURf
https://pastebin.com/GVzvf2jD
https://pastebin.com/U5DNXZR5
https://pastebin.com/bBDcpTNV
https://pastebin.com/kaQ390Yd
https://pastebin.com/RHfrn44V
https://pastebin.com/ycM6hx0X
https://pastebin.com/sSUD6yBD
https://pastebin.com/vYZwLY68
https://pastebin.com/hsu9R9jj
https://pastebin.com/G6HZUJ4J
https://pastebin.com/tr8ae7XA
https://pastebin.com/j8cttQZk
https://pastebin.com/YeKMiByw
https://pastebin.com/j6Nnu9Qe
https://pastebin.com/LVGaaVQa
https://pastebin.com/JJWVEiS5
https://pastebin.com/GEFCqsT7
https://pastebin.com/3ZjJhLsT
https://pastebin.com/eRuatTXc
https://pastebin.com/6dLRhyiJ
https://pastebin.com/VS65myh8
https://pastebin.com/NK7i1SnE
https://pastebin.com/6Akg12TS
https://pastebin.com/Qs3xpNS5
https://pastebin.com/RjrrPqe0
https://pastebin.com/urvjTD8V
https://pastebin.com/63sbx7mm
https://pastebin.com/CxznX5Js
https://pastebin.com/yL6vTZKF
https://pastebin.com/ExT07Ygf
https://pastebin.com/QeQZdYcJ
https://pastebin.com/BY7bpMhj
https://pastebin.com/yAYUGpkf
https://pastebin.com/YXCzrDjU
https://pastebin.com/S10HgVGn
https://pastebin.com/LY6WLkTu
https://pastebin.com/3pfYGW1q
https://pastebin.com/3pV0G6nk
https://pastebin.com/CW4g7TJv
https://pastebin.com/M8J73bkP
https://pastebin.com/L9vcxB0W
https://pastebin.com/buUr51Ls

继续阅读 »

问题现象
如下图所示,需求是现在有A、B两个组,A、B两组中的元素可以拖动,并且A组中的元素可以拖动到B组,B组的元素同样可以拖动到A组,请问如何实现这种多组之间相互拖拽的效果?

点击放大

背景知识
使用Grid组件构建网格元素布局,启动editMode编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem。
onItemDragStart和onItemDrop事件分别在开始拖拽网格元素时触发和停止拖拽时触发,通过事件的组合完成交换数组位置的逻辑。
解决方案
使用Grid布局构建界面。其中,columnsTemplate可设置当前网格布局列的数量、固定列宽或最小列宽值;columnsGap可设置列与列的间距;rowsGap可设置行与行的间距。
Grid(this.scroller) {
GridItem() {
Text('标题' + this.numbers.length)
.fontSize(16)
.width('100%')
.height(30)
.padding({ left: 10 })
.textAlign(TextAlign.Start);
}
.onClick(() => {
this.isShowGroup1 = !this.isShowGroup1;
});

if (this.isShowGroup1) {
ForEach(this.numbers, (day: string) => {
GridItem() {
Text(day)
.fontSize(16)
.backgroundColor('#f1f3f5')
.width('100%')
.height(30)
.textAlign(TextAlign.Center);
};
});
}
GridItem() {
Text('标题' + this.numbers2.length)
.fontSize(16)
.width('100%')
.height(30)
.padding({ left: 10 })
.textAlign(TextAlign.Start);
}
.onClick(() => {
this.isShowGroup2 = !this.isShowGroup2;
});

if (this.isShowGroup2) {
ForEach(this.numbers2, (day: string) => {
GridItem() {
Text(day)
.fontSize(16)
.backgroundColor('#f1f3f5')
.width('100%')
.height(30)
.textAlign(TextAlign.Center);
};
});
}
}
.columnsTemplate('1fr')
.columnsGap(2)
.rowsGap(2)
.scrollBar(BarState.Off)
给Grid组件设置editMode为true,即Grid进入编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem。设置supportAnimation为true,即Grid拖拽元素时支持动画。
.supportAnimation(true)
.editMode(true) // 设置Grid是否进入编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem
定义GridItem被拖拽时元素的样式,元素被拖拽时展示浮动内容。
@Builder
pixelMapBuilder() { // 拖拽过程样式
Column() {
Text('浮动内容' + this.text)
.fontSize(16)
.backgroundColor('#f1f3f5')
.width('90%')
.height(30)
.textAlign(TextAlign.Center);
};
}
判断当前移动的是不是标题,如果是标题禁止拖动。
// 判断是否是标题
isTitle(index: number) {
if (index === 0) {
return true;
}
if (!this.isShowGroup1 && index === 1) {
return true;
}
if (this.isShowGroup1 && index === this.numbers.length + 1) {
return true;
}
return false;
}
定义拖拽过程中的数组交换逻辑。
moveToIndex(index1: number, index2: number) { // 将index1位置的元素移动至 index2位置
let temp = '';
let num1Length = 1 + (this.numbers.length - 1) * Number(this.isShowGroup1);
if (index1 <= num1Length) {
temp = this.numbers.splice(index1 - 1, 1)[0];
} else {
index1 = index1 - this.numbers.length - 1;
temp = this.numbers2.splice(index1 - 1, 1)[0];
}
if (index2 <= num1Length) {
this.numbers.splice(index2 - 1, 0, temp);
} else {
index2 = index2 - this.numbers.length - 1;
this.numbers2.splice(index2 - 1, 0, temp);
}
}
给Grid组件绑定onItemDragStart和onItemDrop事件,在onItemDragStart回调中设置拖拽过程中显示的图片,并在onItemDrop中完成交换数组位置的逻辑。
.onItemDragStart((event: ItemDragInfo, itemIndex: number) => { // 第一次拖拽此事件绑定的组件时,触发回调。
if (this.isTitle(itemIndex)) {
return;
}
if (itemIndex > this.numbers.length) {
itemIndex = itemIndex - this.numbers.length - 1;
this.text = this.numbers2[itemIndex - 1];
} else {
this.text = this.numbers[itemIndex - 1];
}
return this.pixelMapBuilder(); // 设置拖拽过程中显示的图片。
})
.onItemDrop((event: ItemDragInfo, itemIndex: number,
insertIndex: number) => { // 绑定此事件的组件可作为拖拽释放目标,当在本组件范围内停止拖拽行为时,触发回调。
console.info(eventX: ${event.x});
this.moveToIndex(itemIndex, insertIndex);
});
完整示例如下:

@Entry
@Component
struct GridDemo {
@State numbers: string[] = [];
@State numbers2: string[] = [];
@State isShowGroup1: boolean = true;
@State isShowGroup2: boolean = true;
@State text: string = 'drag';
scroller: Scroller = new Scroller();

@Builder
pixelMapBuilder() { // 拖拽过程样式
Column() {
Text('浮动内容' + this.text)
.fontSize(16)
.backgroundColor('#f1f3f5')
.width('90%')
.height(30)
.textAlign(TextAlign.Center);
};
}

aboutToAppear() {
for (let i = 1; i <= 15; i++) {
this.numbers.push('组' + i);
this.numbers2.push('组' + i);
}
}

moveToIndex(index1: number, index2: number) { // 将index1位置的元素移动至 index2位置
let temp = '';
let num1Length = 1 + (this.numbers.length - 1) * Number(this.isShowGroup1);
if (index1 <= num1Length) {
temp = this.numbers.splice(index1 - 1, 1)[0];
} else {
index1 = index1 - this.numbers.length - 1;
temp = this.numbers2.splice(index1 - 1, 1)[0];
}
if (index2 <= num1Length) {
this.numbers.splice(index2 - 1, 0, temp);
} else {
index2 = index2 - this.numbers.length - 1;
this.numbers2.splice(index2 - 1, 0, temp);
}
}

// 判断是否是标题
isTitle(index: number) {
if (index === 0) {
return true;
}
if (!this.isShowGroup1 && index === 1) {
return true;
}
if (this.isShowGroup1 && index === this.numbers.length + 1) {
return true;
}
return false;
}

build() {
Column({ space: 5 }) {
Column() {
Grid(this.scroller) {
GridItem() {
Text('标题' + this.numbers.length)
.fontSize(16)
.width('100%')
.height(30)
.padding({ left: 10 })
.textAlign(TextAlign.Start);
}
.onClick(() => {
this.isShowGroup1 = !this.isShowGroup1;
});

      if (this.isShowGroup1) {  
        ForEach(this.numbers, (day: string) => {  
          GridItem() {  
            Text(day)  
              .fontSize(16)  
              .backgroundColor('#f1f3f5')  
              .width('100%')  
              .height(30)  
              .textAlign(TextAlign.Center);  
          };  
        });  
      }  
      GridItem() {  
        Text('标题' + this.numbers2.length)  
          .fontSize(16)  
          .width('100%')  
          .height(30)  
          .padding({ left: 10 })  
          .textAlign(TextAlign.Start);  
      }  
      .onClick(() => {  
        this.isShowGroup2 = !this.isShowGroup2;  
      });  

      if (this.isShowGroup2) {  
        ForEach(this.numbers2, (day: string) => {  
          GridItem() {  
            Text(day)  
              .fontSize(16)  
              .backgroundColor('#f1f3f5')  
              .width('100%')  
              .height(30)  
              .textAlign(TextAlign.Center);  
          };  
        });  
      }  
    }  
    .columnsTemplate('1fr')  
    .columnsGap(2)  
    .rowsGap(2)  
    .scrollBar(BarState.Off)  
    .onScrollIndex((first: number) => {  
      console.info(first.toString());  
    })  
    .width('90%')  
    .supportAnimation(true)  
    .editMode(true) // 设置Grid是否进入编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem  
    .onItemDragStart((event: ItemDragInfo, itemIndex: number) => { // 第一次拖拽此事件绑定的组件时,触发回调。  
      if (this.isTitle(itemIndex)) {  
        return;  
      }  
      if (itemIndex > this.numbers.length) {  
        itemIndex = itemIndex - this.numbers.length - 1;  
        this.text = this.numbers2[itemIndex - 1];  
      } else {  
        this.text = this.numbers[itemIndex - 1];  
      }  
      return this.pixelMapBuilder(); // 设置拖拽过程中显示的图片。  
    })  
    .onItemDrop((event: ItemDragInfo, itemIndex: number,  
      insertIndex: number) => { // 绑定此事件的组件可作为拖拽释放目标,当在本组件范围内停止拖拽行为时,触发回调。  
      console.info(`eventX: ${event.x}`);  
      this.moveToIndex(itemIndex, insertIndex);  
    });  

  };  
}.width('100%').margin({ top: 5 });  

}
}
https://pastebin.com/ALZ1heJ8
https://pastebin.com/NUA8cAMq
https://pastebin.com/DU7BW7SP
https://pastebin.com/Sh2K4277
https://pastebin.com/MKNCS5xM
https://pastebin.com/jBqWTCsQ
https://pastebin.com/TNhaEKd7
https://pastebin.com/pAcyuLWk
https://pastebin.com/BccwcGVK
https://pastebin.com/ETXeKqiW
https://pastebin.com/DRxMx41K
https://pastebin.com/L15SkLc9
https://pastebin.com/Pyz2h7xc
https://pastebin.com/LWtxbvUn
https://pastebin.com/S7wbsPSF
https://pastebin.com/XmPzcDSQ
https://pastebin.com/zJ2kJ7ZA
https://pastebin.com/wknFnVct
https://pastebin.com/Jx9edSj2
https://pastebin.com/s7w3uv0a
https://pastebin.com/JHwj1NaL
https://pastebin.com/RzdjWfNv
https://pastebin.com/9G3P7TeA
https://pastebin.com/sG5wxN1u
https://pastebin.com/hJ2c716v
https://pastebin.com/4exdhVkn
https://pastebin.com/hUVhcvMP
https://pastebin.com/A4LnBDtM
https://pastebin.com/L6FwB3N9
https://pastebin.com/pt0KRF9P
https://pastebin.com/93GGDNJv
https://pastebin.com/DHdskaAf
https://pastebin.com/1QH5dm9R
https://pastebin.com/P30Qqeif
https://pastebin.com/ngQjFN3W
https://pastebin.com/BRAv0z5d
https://pastebin.com/fdYZAvMX
https://pastebin.com/ciXgk5F6
https://pastebin.com/RjC4wjc9
https://pastebin.com/kEaUKCTq
https://pastebin.com/C3JuxCrQ
https://pastebin.com/y0KUAU4u
https://pastebin.com/nW5AFBcM
https://pastebin.com/yR9yyNZ5
https://pastebin.com/zvH6TX45
https://pastebin.com/RsL54ULk
https://pastebin.com/TRTYXt4k
https://pastebin.com/zCGW0vif
https://pastebin.com/6drbFd0j
https://pastebin.com/D7urfsq6
https://pastebin.com/119esj24
https://pastebin.com/ndHUJCr4
https://pastebin.com/ciSWy0ut
https://pastebin.com/iKf50her
https://pastebin.com/RkE5yyn8
https://pastebin.com/jj99Wa7u
https://pastebin.com/iwJ5us6j
https://pastebin.com/hSuvYeqx
https://pastebin.com/6YQNbNZ6
https://pastebin.com/SsUF39yN
https://pastebin.com/xLFVFWSd
https://pastebin.com/8C1YCkb7
https://pastebin.com/PBRiQwEE
https://pastebin.com/CZw00CFD
https://pastebin.com/MhC3vvEH
https://pastebin.com/NGucrr8R
https://pastebin.com/5inLc8eS
https://pastebin.com/rnNKvRei
https://pastebin.com/LDXYYnhG
https://pastebin.com/5YpXtQeT
https://pastebin.com/mb5JFUs5
https://pastebin.com/HDzxgFHY
https://pastebin.com/Ssx5zf6i
https://pastebin.com/qMCRpKuL
https://pastebin.com/YMyidNzi
https://pastebin.com/vSwjFPu0
https://pastebin.com/yFPJgYtk
https://pastebin.com/5FLbpDXJ
https://pastebin.com/vRj3ezdr
https://pastebin.com/mYUwfwNk
https://pastebin.com/dTx5DQSb
https://pastebin.com/e7jXBCMc
https://pastebin.com/dxgE2bcC
https://pastebin.com/STW6QhxS
https://pastebin.com/bZhrAr9R
https://pastebin.com/nfYTP7Au
https://pastebin.com/aWunSSVB
https://pastebin.com/zh4pAmyd
https://pastebin.com/3pqzBPyc
https://pastebin.com/ddwcDZHC
https://pastebin.com/2ZqNLikt
https://pastebin.com/56U4rWjG
https://pastebin.com/w24SbjAy
https://pastebin.com/by3Jn4Pq
https://pastebin.com/9UkiAZMK
https://pastebin.com/MxhA05RB
https://pastebin.com/4Jshmpfb
https://pastebin.com/L5sHYH9N
https://pastebin.com/0bJWTV7h
https://pastebin.com/AwM6iDiL
https://pastebin.com/Lsr4Cxkg
https://pastebin.com/LAfLC3a3
https://pastebin.com/f7wTnKEC
https://pastebin.com/HSEbHCyh
https://pastebin.com/uv6Hqjs4
https://pastebin.com/i2V8QUcy
https://pastebin.com/ZAPYfBNk
https://pastebin.com/waMXDnj3
https://pastebin.com/GMUKBZUs
https://pastebin.com/pezF3Vbk
https://pastebin.com/n7DYUQ5v
https://pastebin.com/cyT65uSE
https://pastebin.com/sx2iJqcn
https://pastebin.com/ZPqkWKaR
https://pastebin.com/HEyb77U9
https://pastebin.com/4FZufveK
https://pastebin.com/uExv5h76
https://pastebin.com/Ac7CHJc7
https://pastebin.com/M4s8ZR79
https://pastebin.com/nys7xPvA
https://pastebin.com/PehqarxQ
https://pastebin.com/RbdWT272
https://pastebin.com/UfFk8r7t
https://pastebin.com/y1XYanaB
https://pastebin.com/VmifaeiF
https://pastebin.com/2dDAckiN
https://pastebin.com/FAfELYhc
https://pastebin.com/QCFiEVnQ
https://pastebin.com/QbgF4PzX
https://pastebin.com/X4yVdKxp
https://pastebin.com/cuqhnADr
https://pastebin.com/8iUfUnL8
https://pastebin.com/xhFdDU3m
https://pastebin.com/5Yt1eLAQ
https://pastebin.com/XNDHeeM8
https://pastebin.com/kJXfXZ8R
https://pastebin.com/hxwe46wW
https://pastebin.com/B4mB17YB
https://pastebin.com/GQbTpcbY
https://pastebin.com/DpYauvxs
https://pastebin.com/1gsWBrLW
https://pastebin.com/g9WsSEzm
https://pastebin.com/hzqj45RD
https://pastebin.com/vWKXzBJ9
https://pastebin.com/GTajTfJB
https://pastebin.com/d52vdzg9
https://pastebin.com/w1xQpURf
https://pastebin.com/GVzvf2jD
https://pastebin.com/U5DNXZR5
https://pastebin.com/bBDcpTNV
https://pastebin.com/kaQ390Yd
https://pastebin.com/RHfrn44V
https://pastebin.com/ycM6hx0X
https://pastebin.com/sSUD6yBD
https://pastebin.com/vYZwLY68
https://pastebin.com/hsu9R9jj
https://pastebin.com/G6HZUJ4J
https://pastebin.com/tr8ae7XA
https://pastebin.com/j8cttQZk
https://pastebin.com/YeKMiByw
https://pastebin.com/j6Nnu9Qe
https://pastebin.com/LVGaaVQa
https://pastebin.com/JJWVEiS5
https://pastebin.com/GEFCqsT7
https://pastebin.com/3ZjJhLsT
https://pastebin.com/eRuatTXc
https://pastebin.com/6dLRhyiJ
https://pastebin.com/VS65myh8
https://pastebin.com/NK7i1SnE
https://pastebin.com/6Akg12TS
https://pastebin.com/Qs3xpNS5
https://pastebin.com/RjrrPqe0
https://pastebin.com/urvjTD8V
https://pastebin.com/63sbx7mm
https://pastebin.com/CxznX5Js
https://pastebin.com/yL6vTZKF
https://pastebin.com/ExT07Ygf
https://pastebin.com/QeQZdYcJ
https://pastebin.com/BY7bpMhj
https://pastebin.com/yAYUGpkf
https://pastebin.com/YXCzrDjU
https://pastebin.com/S10HgVGn
https://pastebin.com/LY6WLkTu
https://pastebin.com/3pfYGW1q
https://pastebin.com/3pV0G6nk
https://pastebin.com/CW4g7TJv
https://pastebin.com/M8J73bkP
https://pastebin.com/L9vcxB0W
https://pastebin.com/buUr51Ls

收起阅读 »