Google Play 审核到底是机器还是人工?
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 应用顺利通过审核
- 优化权限声明,避免“机器误判”
机器审核最敏感的就是权限。在 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 {
// 权限已授予,执行扫码逻辑
}
}
);
- 谨慎使用 WebView,避免“动态加载”嫌疑
机器审核对动态加载内容非常警惕,因为它可能被用于绕过审核(如加载违规页面)。在 uni-app 中, 组件是常见的功能载体,但如果你将其指向一个外部 URL,机器可能会标记为“不可控内容”。
步骤:
- 尽量使用本地 HTML 文件,而非远程 URL。例如,将 H5 页面打包到应用资源中,通过 src="/static/page.html" 加载。
- 如果必须加载远程页面,确保该域名在隐私政策中列出,并且页面内容不包含违规信息(如赌博、色情)。同时,在 manifest.json 中配置 标签,明确声明你访问的域名。
- 避免在 WebView 中执行复杂的 JavaScript 代码,例如动态创建 iframe 或加载外部脚本。这些行为容易被机器视为“恶意注入”。
- 完善隐私政策,应对人工审核
人工审核员会仔细阅读你的隐私政策,并对照应用的实际行为。很多 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;
}
}
- 避免代码混淆与动态下载
机器审核会扫描 APK 中的代码结构,如果发现大量混淆或动态加载的 Dex 文件,会认为应用存在“隐形行为”。uni-app 应用本身使用 V8 引擎执行 JavaScript,这部分代码相对透明,但如果你引入了原生插件,这些插件可能经过混淆。
步骤:
- 对于自定义原生插件,尽量使用 ProGuard 的默认规则,避免过度混淆。特别是插件暴露给 JS 层的接口,不要混淆,否则会导致运行时错误。
- 不要在运行时从远程服务器下载代码或资源(如更新插件)。如果需要热更新,使用 uni-app 的官方热更新机制(通过 App 资源更新),而非自行实现动态加载。
- 如果必须使用动态加载(如加载 A/B 测试代码),确保相关逻辑在隐私政策中说明,并限制在非核心功能上。
- 提前测试,模拟机器审核
在提交前,使用 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 应用顺利通过审核
- 优化权限声明,避免“机器误判”
机器审核最敏感的就是权限。在 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 {
// 权限已授予,执行扫码逻辑
}
}
);
- 谨慎使用 WebView,避免“动态加载”嫌疑
机器审核对动态加载内容非常警惕,因为它可能被用于绕过审核(如加载违规页面)。在 uni-app 中, 组件是常见的功能载体,但如果你将其指向一个外部 URL,机器可能会标记为“不可控内容”。
步骤:
- 尽量使用本地 HTML 文件,而非远程 URL。例如,将 H5 页面打包到应用资源中,通过 src="/static/page.html" 加载。
- 如果必须加载远程页面,确保该域名在隐私政策中列出,并且页面内容不包含违规信息(如赌博、色情)。同时,在 manifest.json 中配置 标签,明确声明你访问的域名。
- 避免在 WebView 中执行复杂的 JavaScript 代码,例如动态创建 iframe 或加载外部脚本。这些行为容易被机器视为“恶意注入”。
- 完善隐私政策,应对人工审核
人工审核员会仔细阅读你的隐私政策,并对照应用的实际行为。很多 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;
}
}
- 避免代码混淆与动态下载
机器审核会扫描 APK 中的代码结构,如果发现大量混淆或动态加载的 Dex 文件,会认为应用存在“隐形行为”。uni-app 应用本身使用 V8 引擎执行 JavaScript,这部分代码相对透明,但如果你引入了原生插件,这些插件可能经过混淆。
步骤:
- 对于自定义原生插件,尽量使用 ProGuard 的默认规则,避免过度混淆。特别是插件暴露给 JS 层的接口,不要混淆,否则会导致运行时错误。
- 不要在运行时从远程服务器下载代码或资源(如更新插件)。如果需要热更新,使用 uni-app 的官方热更新机制(通过 App 资源更新),而非自行实现动态加载。
- 如果必须使用动态加载(如加载 A/B 测试代码),确保相关逻辑在隐私政策中说明,并限制在非核心功能上。
- 提前测试,模拟机器审核
在提交前,使用 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
App 已经成功上架,为什么苹果开发者账号还是被封了?这是我后来才明白的事
App 已经成功上架,为什么苹果开发者账号还是被封了?这是我后来才明白的事
很多开发者都会觉得:
只要 App 能成功上架,说明账号已经安全了。
我以前也是这样认为的。
毕竟审核都通过了,应用也已经在 App Store 正常下载,按理来说后续应该只是更新版本、维护功能,不会再有什么大问题。
直到后来,一个已经稳定运营了一段时间的开发者账号突然被终止,我才开始重新研究苹果的审核逻辑。
事实证明,审核通过和账号安全,并不是一回事。
当时到底发生了什么?
那个账号并不是第一次提交应用。
前面的版本都顺利通过审核,产品也一直正常运营。
后来只是一次普通的版本更新,原本以为几天就能审核完成,结果审核时间越来越长,随后收到了苹果关于账号的处理通知。
刚开始我一直觉得是不是这次代码写错了。
后来复盘整个账号,才发现问题并没有这么简单。
苹果真正评估的是整个开发者账号
很多开发者只关注这一次提交的 IPA。
实际上,苹果更关注的是整个开发者账号的表现。
例如:
- 提交过哪些应用
- 每个应用之间是否存在高度相似
- 是否经常因为同类问题被拒
- 是否存在重复业务
- 应用更新是否长期保持稳定
- 是否存在误导性的元数据
这些信息都会不断累积。
也就是说,账号是有”历史记录”的。
并不会因为一次审核通过,就把之前所有情况全部清零。
我后来发现,真正危险的是”长期积累”
有一次审核通过,并不能说明以后一直都会通过。
真正容易出现问题的,往往是下面这些情况。
相似应用越来越多
刚开始只有一个工具类 App。
后来增加第二个。
第三个。
第四个。
如果这些应用只是换了名称、Logo 或主题,而底层实现高度一致,那么随着数量增加,被关注的概率也会增加。
很多开发者觉得:
“以前都是这么做的,也通过了。”
但实际上,审核标准、审核模型以及账号历史都会不断变化。
每次都只是为了过审
还有一种情况,就是每次修改都只是为了让审核通过。
哪里被拒改哪里。
功能没有真正优化,业务没有真正完善。
长时间下来,账号会留下大量审核记录。
虽然每一次都可能成功,但整体表现未必理想。
更新越来越频繁
为了赶进度,有时候一天提交一次。
甚至一天修改两三次。
这种方式虽然能够提高试错速度,但也容易让账号产生大量审核记录。
如果再叠加其他因素,风险自然会上升。
后来我们的做法变了
经历过那次之后,我们已经不会再等审核发现问题。
每次准备提交之前,都会提前检查:
- 功能是否真正独立
- 页面是否存在大量重复
- 是否新增了容易触发审核的问题
- 元数据是否准确描述功能
- 新版本是否与历史版本保持合理演进
虽然准备时间更长了一点,但后面的审核明显稳定了很多。
最后想说
很多开发者把苹果审核理解成一次考试。
其实更像是一份长期档案。
每一次提交、每一次修改、每一次被拒,都会成为账号历史的一部分。
所以,真正需要维护的,不只是某一个 App,而是整个开发者账号的长期信誉。
只有把账号当成长期资产去运营,而不是只想着快速通过一次审核,后续更新才会越来越顺利。
更多文章看我主页
App 已经成功上架,为什么苹果开发者账号还是被封了?这是我后来才明白的事
很多开发者都会觉得:
只要 App 能成功上架,说明账号已经安全了。
我以前也是这样认为的。
毕竟审核都通过了,应用也已经在 App Store 正常下载,按理来说后续应该只是更新版本、维护功能,不会再有什么大问题。
直到后来,一个已经稳定运营了一段时间的开发者账号突然被终止,我才开始重新研究苹果的审核逻辑。
事实证明,审核通过和账号安全,并不是一回事。
当时到底发生了什么?
那个账号并不是第一次提交应用。
前面的版本都顺利通过审核,产品也一直正常运营。
后来只是一次普通的版本更新,原本以为几天就能审核完成,结果审核时间越来越长,随后收到了苹果关于账号的处理通知。
刚开始我一直觉得是不是这次代码写错了。
后来复盘整个账号,才发现问题并没有这么简单。
苹果真正评估的是整个开发者账号
很多开发者只关注这一次提交的 IPA。
实际上,苹果更关注的是整个开发者账号的表现。
例如:
- 提交过哪些应用
- 每个应用之间是否存在高度相似
- 是否经常因为同类问题被拒
- 是否存在重复业务
- 应用更新是否长期保持稳定
- 是否存在误导性的元数据
这些信息都会不断累积。
也就是说,账号是有”历史记录”的。
并不会因为一次审核通过,就把之前所有情况全部清零。
我后来发现,真正危险的是”长期积累”
有一次审核通过,并不能说明以后一直都会通过。
真正容易出现问题的,往往是下面这些情况。
相似应用越来越多
刚开始只有一个工具类 App。
后来增加第二个。
第三个。
第四个。
如果这些应用只是换了名称、Logo 或主题,而底层实现高度一致,那么随着数量增加,被关注的概率也会增加。
很多开发者觉得:
“以前都是这么做的,也通过了。”
但实际上,审核标准、审核模型以及账号历史都会不断变化。
每次都只是为了过审
还有一种情况,就是每次修改都只是为了让审核通过。
哪里被拒改哪里。
功能没有真正优化,业务没有真正完善。
长时间下来,账号会留下大量审核记录。
虽然每一次都可能成功,但整体表现未必理想。
更新越来越频繁
为了赶进度,有时候一天提交一次。
甚至一天修改两三次。
这种方式虽然能够提高试错速度,但也容易让账号产生大量审核记录。
如果再叠加其他因素,风险自然会上升。
后来我们的做法变了
经历过那次之后,我们已经不会再等审核发现问题。
每次准备提交之前,都会提前检查:
- 功能是否真正独立
- 页面是否存在大量重复
- 是否新增了容易触发审核的问题
- 元数据是否准确描述功能
- 新版本是否与历史版本保持合理演进
虽然准备时间更长了一点,但后面的审核明显稳定了很多。
最后想说
很多开发者把苹果审核理解成一次考试。
其实更像是一份长期档案。
每一次提交、每一次修改、每一次被拒,都会成为账号历史的一部分。
所以,真正需要维护的,不只是某一个 App,而是整个开发者账号的长期信誉。
只有把账号当成长期资产去运营,而不是只想着快速通过一次审核,后续更新才会越来越顺利。
更多文章看我主页
收起阅读 »解决Row容器空间不足时子组件消失的问题
问题现象
在Row组件中放置两个Text组件,左侧Text(动态标题)需自适应宽度,空间不足时末尾省略显示(TextOverflow.Ellipsis),右侧Text(如(99))需始终完整显示,但实际效果中,空间不足时左侧Text直接消失,而非显示省略号。
问题代码示例参考如下:
@Entry
@Component
struct Index {
@State title: string = '长标题文本长标题文本长标题文本';
build() {
Column() {
Row() {
Text(this.title)
.fontSize(17)
.fontWeight(500)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.textAlign(TextAlign.Center)
.onClick(() => {
this.title += '加加';
})
Text('(99)')
.fontSize(17)
.fontWeight(500)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.textAlign(TextAlign.Start)
.displayPriority(2)
}
.justifyContent(FlexAlign.Center)
.width('100%')
.margin({ top: 280 })
}
}
}
问题效果预览:
点击放大
效果预览
点击放大
背景知识
displayPriority属性机制:当父容器空间不足时,系统按优先级隐藏子组件(值越小优先级越高),若某一优先级组件被隐藏,更低优先级的组件会全部被隐藏(即使空间足够),右侧Text设置displayPriority(低优先级),左侧未设置(默认0,高优先级),但隐藏逻辑导致左侧异常消失。
弹性布局压缩规则:Row基于Flex布局,子组件默认flexShrink:0(禁止压缩),文本省略需同时满足:设置maxLines和textOverflow,组件flexShrink:1(允许压缩)且有明确宽度约束。
问题定位
隐藏机制冲突:右侧displayPriority激活了隐藏逻辑,空间不足时触发低优先级组件隐藏链,导致左侧被连带隐藏,左侧虽设置省略样式,但flexShrink默认为0,未触发压缩流程,直接跳过省略进入隐藏。
布局约束缺失:左侧Text未明确允许压缩,右侧未禁止压缩,两者在空间争夺中行为未定义,justifyContent(FlexAlign.Center)强制居中分配空间,加剧宽度计算冲突。
分析结论
根本矛盾在于:displayPriority的组件级隐藏机制与textOverflow的文本级压缩机制互斥,当空间不足时,系统优先触发displayPriority的隐藏逻辑,而非文本压缩。
修改建议
核心方案:弃用displayPriority,改用弹性压缩控制。
@Entry
@Component
struct LongText {
@State title: string = '长标题文本长标题文本长标题文本';
build() {
Column() {
// 关键修改1:使用Flex替代Row,明确弹性规则
Flex({
direction: FlexDirection.Row,
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.Start // 左对齐
}) {
Text(this.title)
.fontSize(17)
.fontWeight(500)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.flexShrink(1) // 关键修改2:允许压缩
.onClick(() => {
this.title += '追加文本';
})
Text('(99)')
.fontSize(17)
.fontWeight(500)
.flexShrink(0) // 关键修改3:禁止压缩
}
.width('100%')
.margin({ top: 280 })
}
}
}
https://pastebin.com/zVxDU61E
https://pastebin.com/nzLueSAz
https://pastebin.com/ezBMAig0
https://pastebin.com/YnzFdFKL
https://pastebin.com/7NqfmRSc
https://pastebin.com/XnhHvhjC
https://pastebin.com/5vt1ShF0
https://pastebin.com/jimpxmtQ
https://pastebin.com/0MKBBtSb
https://pastebin.com/hYPcWX3A
https://pastebin.com/JGZi6dqX
https://pastebin.com/Eg0QkGxK
https://pastebin.com/avBQHdmv
https://pastebin.com/9K1kr69e
https://pastebin.com/Ft3Qmf4p
https://pastebin.com/ECQydf9d
https://pastebin.com/w6AjGy7B
https://pastebin.com/pUAfNajs
https://pastebin.com/PdVL3eGz
https://pastebin.com/mTMqjn2v
https://pastebin.com/3h4gwTdv
https://pastebin.com/gjNjuKYf
https://pastebin.com/KFucmjDt
https://pastebin.com/6bWkRuLA
https://pastebin.com/7NibmTdB
https://pastebin.com/YfQPgbwm
https://pastebin.com/LJSs1Hdj
https://pastebin.com/PSvAdQ53
https://pastebin.com/KgSmzqhM
https://pastebin.com/BnDxzKWh
https://pastebin.com/aF99ZhZq
https://pastebin.com/bqhqtcei
https://pastebin.com/sXTWrCAD
https://pastebin.com/7yCNwek9
https://pastebin.com/naX7WgBh
https://pastebin.com/DkhB099s
https://pastebin.com/zkQ8Pr8B
https://pastebin.com/N3DaSPpi
https://pastebin.com/uTy41bXJ
https://pastebin.com/7YJE2nuP
https://pastebin.com/nz8wUJUM
https://pastebin.com/2REs5Q33
https://pastebin.com/tQ28rQXE
https://pastebin.com/DMCWY9XS
https://pastebin.com/nRcXfz4e
https://pastebin.com/S4NzD1sa
https://pastebin.com/QgVJE8JN
https://pastebin.com/EvAFi0b5
https://pastebin.com/Nz6N6kSC
https://pastebin.com/n5xFt0XA
https://pastebin.com/AFNPdqRC
https://pastebin.com/wC1fQXzb
https://pastebin.com/ttzSHyy5
https://pastebin.com/6y9J8aZd
https://pastebin.com/gP7ZHPhR
https://pastebin.com/xhVy1Dr7
https://pastebin.com/7tMFe3Z8
https://pastebin.com/aFthfCSF
https://pastebin.com/26QsabND
https://pastebin.com/hbZqkTfN
https://pastebin.com/M0ZMtszR
https://pastebin.com/F62d211T
https://pastebin.com/LH6eA95e
https://pastebin.com/6mP60iDt
https://pastebin.com/gRhsLLQt
https://pastebin.com/a7rmhfm0
https://pastebin.com/sj10sst6
https://pastebin.com/dty8sxD5
https://pastebin.com/dHEXCxFt
https://pastebin.com/pE6ePUAY
https://pastebin.com/2U1LgwWz
https://pastebin.com/WQpk7jSa
https://pastebin.com/G8LPQVtP
https://pastebin.com/VRpjsRTK
https://pastebin.com/aCy0qhBi
https://pastebin.com/NFQMuiXH
https://pastebin.com/S064raxa
https://pastebin.com/4s7w0BBW
https://pastebin.com/m7UDFUJ6
https://pastebin.com/UBeiNw6e
https://pastebin.com/kms3qfYM
https://pastebin.com/dYjjH5yP
https://pastebin.com/6cvgDvgL
https://pastebin.com/KPmn6RCf
https://pastebin.com/DiPdkuXM
https://pastebin.com/53nPeHWZ
https://pastebin.com/WNdjQBtb
https://pastebin.com/gD3BbuEt
https://pastebin.com/K1djmuru
https://pastebin.com/uqMvn9Uq
https://pastebin.com/paibxsw8
https://pastebin.com/Una68Ba5
https://pastebin.com/P7CrcBfR
https://pastebin.com/UiQ5wXCw
https://pastebin.com/LYHMErrw
https://pastebin.com/T3Fjh1Er
https://pastebin.com/m7Zx61yT
https://pastebin.com/dRmeRwsB
https://pastebin.com/vN5zyV7U
https://pastebin.com/hqms8PnB
https://pastebin.com/VJJ9tacg
https://pastebin.com/gnHE24Vx
https://pastebin.com/2DN8jUYE
https://pastebin.com/NYYLtSrA
https://pastebin.com/FJcvCjYv
https://pastebin.com/C50vS8E5
https://pastebin.com/x8G6uZzB
https://pastebin.com/xwqDW20Q
https://pastebin.com/bbrPiHh4
https://pastebin.com/KwcxyrV3
https://pastebin.com/8Yq1Mwpv
https://pastebin.com/rdXkVZnR
https://pastebin.com/X1QpPxj6
https://pastebin.com/rk4jhKwM
https://pastebin.com/AYH757Hd
https://pastebin.com/arrJtdPC
https://pastebin.com/x1a0QkrE
https://pastebin.com/ZPzMV6KV
https://pastebin.com/qZPAArab
https://pastebin.com/dDiiWq7r
https://pastebin.com/wH1CZA3w
https://pastebin.com/9CHyNi7a
https://pastebin.com/Wkzwp2vS
https://pastebin.com/MVpzkpRy
https://pastebin.com/4AuW9a4S
https://pastebin.com/42tkYqcy
https://pastebin.com/XeksS1ku
https://pastebin.com/L110ndKg
https://pastebin.com/4Axgsuj0
https://pastebin.com/eAA2mxp4
https://pastebin.com/aU4WMaqg
https://pastebin.com/f1hun2kM
https://pastebin.com/wiqMJgk3
https://pastebin.com/uEN8vUKX
https://pastebin.com/D5qiKNFB
https://pastebin.com/F65rvR1J
https://pastebin.com/rasLrbYn
https://pastebin.com/21n3MQq5
https://pastebin.com/bXDj2phg
https://pastebin.com/0PVahXVh
https://pastebin.com/7JtdfNGH
https://pastebin.com/0986iXre
https://pastebin.com/NUb3eRVe
https://pastebin.com/gvz7gKTZ
https://pastebin.com/UusP3Pyp
https://pastebin.com/QSMmWQi9
https://pastebin.com/jLtF7m6h
https://pastebin.com/csKNzJ6w
https://pastebin.com/wBeZafkw
https://pastebin.com/KESDZmpi
https://pastebin.com/mppjXxUF
https://pastebin.com/MepfrkCE
https://pastebin.com/ibwAybQc
https://pastebin.com/WuELbpHn
https://pastebin.com/hTM9NQ5N
https://pastebin.com/jj0Cp5Ux
https://pastebin.com/LzZFQwaZ
https://pastebin.com/tY8wgRRh
https://pastebin.com/XhgJKNVx
https://pastebin.com/zmytyHRD
https://pastebin.com/ps3FBe7w
https://pastebin.com/hKEiN00Z
https://pastebin.com/0krAGqa8
https://pastebin.com/bmN1vmY5
https://pastebin.com/tXEzETbZ
https://pastebin.com/sxvMRT7d
https://pastebin.com/PBYx1zjX
https://pastebin.com/YL3YtvW4
https://pastebin.com/wYkL6tyV
https://pastebin.com/G9yg2jbz
https://pastebin.com/YS9qiTPE
https://pastebin.com/zRzdakmk
https://pastebin.com/cPbmqAwR
https://pastebin.com/k6VnHXDj
https://pastebin.com/xaU4Lde8
https://pastebin.com/T3qhtZbi
https://pastebin.com/ti5Gv5Uy
https://pastebin.com/p9B6cLzp
https://pastebin.com/0McP0kzG
https://pastebin.com/QRsCLz7H
https://pastebin.com/rTnmVGim
https://pastebin.com/dirbSPyj
https://pastebin.com/QJ1gdh7k
https://pastebin.com/qgCPAp80
https://pastebin.com/H6YUkDVV
https://pastebin.com/YzEYse2d
https://pastebin.com/kUbmfLr0
https://pastebin.com/DmfhXUWK
https://pastebin.com/rxfuCib1
https://pastebin.com/wCR9BHRT
https://pastebin.com/rfJqDQph
https://pastebin.com/UVXE7g9P
https://pastebin.com/vB8FW842
https://pastebin.com/93JiaU8B
https://pastebin.com/40fKbQF1
https://pastebin.com/UpbtX9Vs
https://pastebin.com/GwhrB2rt
https://pastebin.com/CMt33cpf
https://pastebin.com/qPsRkEzz
https://pastebin.com/RR7a27Wk
https://pastebin.com/zU5CC8qs
https://pastebin.com/D3aJGYQk
https://pastebin.com/hx9ya1cr
https://pastebin.com/2GgcbCuP
https://pastebin.com/gYczTsfK
https://pastebin.com/VXsw9xTt
https://pastebin.com/R7DhK5Tq
https://pastebin.com/hHKGpdvA
https://pastebin.com/xREcgGy2
问题现象
在Row组件中放置两个Text组件,左侧Text(动态标题)需自适应宽度,空间不足时末尾省略显示(TextOverflow.Ellipsis),右侧Text(如(99))需始终完整显示,但实际效果中,空间不足时左侧Text直接消失,而非显示省略号。
问题代码示例参考如下:
@Entry
@Component
struct Index {
@State title: string = '长标题文本长标题文本长标题文本';
build() {
Column() {
Row() {
Text(this.title)
.fontSize(17)
.fontWeight(500)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.textAlign(TextAlign.Center)
.onClick(() => {
this.title += '加加';
})
Text('(99)')
.fontSize(17)
.fontWeight(500)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.textAlign(TextAlign.Start)
.displayPriority(2)
}
.justifyContent(FlexAlign.Center)
.width('100%')
.margin({ top: 280 })
}
}
}
问题效果预览:
点击放大
效果预览
点击放大
背景知识
displayPriority属性机制:当父容器空间不足时,系统按优先级隐藏子组件(值越小优先级越高),若某一优先级组件被隐藏,更低优先级的组件会全部被隐藏(即使空间足够),右侧Text设置displayPriority(低优先级),左侧未设置(默认0,高优先级),但隐藏逻辑导致左侧异常消失。
弹性布局压缩规则:Row基于Flex布局,子组件默认flexShrink:0(禁止压缩),文本省略需同时满足:设置maxLines和textOverflow,组件flexShrink:1(允许压缩)且有明确宽度约束。
问题定位
隐藏机制冲突:右侧displayPriority激活了隐藏逻辑,空间不足时触发低优先级组件隐藏链,导致左侧被连带隐藏,左侧虽设置省略样式,但flexShrink默认为0,未触发压缩流程,直接跳过省略进入隐藏。
布局约束缺失:左侧Text未明确允许压缩,右侧未禁止压缩,两者在空间争夺中行为未定义,justifyContent(FlexAlign.Center)强制居中分配空间,加剧宽度计算冲突。
分析结论
根本矛盾在于:displayPriority的组件级隐藏机制与textOverflow的文本级压缩机制互斥,当空间不足时,系统优先触发displayPriority的隐藏逻辑,而非文本压缩。
修改建议
核心方案:弃用displayPriority,改用弹性压缩控制。
@Entry
@Component
struct LongText {
@State title: string = '长标题文本长标题文本长标题文本';
build() {
Column() {
// 关键修改1:使用Flex替代Row,明确弹性规则
Flex({
direction: FlexDirection.Row,
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.Start // 左对齐
}) {
Text(this.title)
.fontSize(17)
.fontWeight(500)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.flexShrink(1) // 关键修改2:允许压缩
.onClick(() => {
this.title += '追加文本';
})
Text('(99)')
.fontSize(17)
.fontWeight(500)
.flexShrink(0) // 关键修改3:禁止压缩
}
.width('100%')
.margin({ top: 280 })
}
}
}
https://pastebin.com/zVxDU61E
https://pastebin.com/nzLueSAz
https://pastebin.com/ezBMAig0
https://pastebin.com/YnzFdFKL
https://pastebin.com/7NqfmRSc
https://pastebin.com/XnhHvhjC
https://pastebin.com/5vt1ShF0
https://pastebin.com/jimpxmtQ
https://pastebin.com/0MKBBtSb
https://pastebin.com/hYPcWX3A
https://pastebin.com/JGZi6dqX
https://pastebin.com/Eg0QkGxK
https://pastebin.com/avBQHdmv
https://pastebin.com/9K1kr69e
https://pastebin.com/Ft3Qmf4p
https://pastebin.com/ECQydf9d
https://pastebin.com/w6AjGy7B
https://pastebin.com/pUAfNajs
https://pastebin.com/PdVL3eGz
https://pastebin.com/mTMqjn2v
https://pastebin.com/3h4gwTdv
https://pastebin.com/gjNjuKYf
https://pastebin.com/KFucmjDt
https://pastebin.com/6bWkRuLA
https://pastebin.com/7NibmTdB
https://pastebin.com/YfQPgbwm
https://pastebin.com/LJSs1Hdj
https://pastebin.com/PSvAdQ53
https://pastebin.com/KgSmzqhM
https://pastebin.com/BnDxzKWh
https://pastebin.com/aF99ZhZq
https://pastebin.com/bqhqtcei
https://pastebin.com/sXTWrCAD
https://pastebin.com/7yCNwek9
https://pastebin.com/naX7WgBh
https://pastebin.com/DkhB099s
https://pastebin.com/zkQ8Pr8B
https://pastebin.com/N3DaSPpi
https://pastebin.com/uTy41bXJ
https://pastebin.com/7YJE2nuP
https://pastebin.com/nz8wUJUM
https://pastebin.com/2REs5Q33
https://pastebin.com/tQ28rQXE
https://pastebin.com/DMCWY9XS
https://pastebin.com/nRcXfz4e
https://pastebin.com/S4NzD1sa
https://pastebin.com/QgVJE8JN
https://pastebin.com/EvAFi0b5
https://pastebin.com/Nz6N6kSC
https://pastebin.com/n5xFt0XA
https://pastebin.com/AFNPdqRC
https://pastebin.com/wC1fQXzb
https://pastebin.com/ttzSHyy5
https://pastebin.com/6y9J8aZd
https://pastebin.com/gP7ZHPhR
https://pastebin.com/xhVy1Dr7
https://pastebin.com/7tMFe3Z8
https://pastebin.com/aFthfCSF
https://pastebin.com/26QsabND
https://pastebin.com/hbZqkTfN
https://pastebin.com/M0ZMtszR
https://pastebin.com/F62d211T
https://pastebin.com/LH6eA95e
https://pastebin.com/6mP60iDt
https://pastebin.com/gRhsLLQt
https://pastebin.com/a7rmhfm0
https://pastebin.com/sj10sst6
https://pastebin.com/dty8sxD5
https://pastebin.com/dHEXCxFt
https://pastebin.com/pE6ePUAY
https://pastebin.com/2U1LgwWz
https://pastebin.com/WQpk7jSa
https://pastebin.com/G8LPQVtP
https://pastebin.com/VRpjsRTK
https://pastebin.com/aCy0qhBi
https://pastebin.com/NFQMuiXH
https://pastebin.com/S064raxa
https://pastebin.com/4s7w0BBW
https://pastebin.com/m7UDFUJ6
https://pastebin.com/UBeiNw6e
https://pastebin.com/kms3qfYM
https://pastebin.com/dYjjH5yP
https://pastebin.com/6cvgDvgL
https://pastebin.com/KPmn6RCf
https://pastebin.com/DiPdkuXM
https://pastebin.com/53nPeHWZ
https://pastebin.com/WNdjQBtb
https://pastebin.com/gD3BbuEt
https://pastebin.com/K1djmuru
https://pastebin.com/uqMvn9Uq
https://pastebin.com/paibxsw8
https://pastebin.com/Una68Ba5
https://pastebin.com/P7CrcBfR
https://pastebin.com/UiQ5wXCw
https://pastebin.com/LYHMErrw
https://pastebin.com/T3Fjh1Er
https://pastebin.com/m7Zx61yT
https://pastebin.com/dRmeRwsB
https://pastebin.com/vN5zyV7U
https://pastebin.com/hqms8PnB
https://pastebin.com/VJJ9tacg
https://pastebin.com/gnHE24Vx
https://pastebin.com/2DN8jUYE
https://pastebin.com/NYYLtSrA
https://pastebin.com/FJcvCjYv
https://pastebin.com/C50vS8E5
https://pastebin.com/x8G6uZzB
https://pastebin.com/xwqDW20Q
https://pastebin.com/bbrPiHh4
https://pastebin.com/KwcxyrV3
https://pastebin.com/8Yq1Mwpv
https://pastebin.com/rdXkVZnR
https://pastebin.com/X1QpPxj6
https://pastebin.com/rk4jhKwM
https://pastebin.com/AYH757Hd
https://pastebin.com/arrJtdPC
https://pastebin.com/x1a0QkrE
https://pastebin.com/ZPzMV6KV
https://pastebin.com/qZPAArab
https://pastebin.com/dDiiWq7r
https://pastebin.com/wH1CZA3w
https://pastebin.com/9CHyNi7a
https://pastebin.com/Wkzwp2vS
https://pastebin.com/MVpzkpRy
https://pastebin.com/4AuW9a4S
https://pastebin.com/42tkYqcy
https://pastebin.com/XeksS1ku
https://pastebin.com/L110ndKg
https://pastebin.com/4Axgsuj0
https://pastebin.com/eAA2mxp4
https://pastebin.com/aU4WMaqg
https://pastebin.com/f1hun2kM
https://pastebin.com/wiqMJgk3
https://pastebin.com/uEN8vUKX
https://pastebin.com/D5qiKNFB
https://pastebin.com/F65rvR1J
https://pastebin.com/rasLrbYn
https://pastebin.com/21n3MQq5
https://pastebin.com/bXDj2phg
https://pastebin.com/0PVahXVh
https://pastebin.com/7JtdfNGH
https://pastebin.com/0986iXre
https://pastebin.com/NUb3eRVe
https://pastebin.com/gvz7gKTZ
https://pastebin.com/UusP3Pyp
https://pastebin.com/QSMmWQi9
https://pastebin.com/jLtF7m6h
https://pastebin.com/csKNzJ6w
https://pastebin.com/wBeZafkw
https://pastebin.com/KESDZmpi
https://pastebin.com/mppjXxUF
https://pastebin.com/MepfrkCE
https://pastebin.com/ibwAybQc
https://pastebin.com/WuELbpHn
https://pastebin.com/hTM9NQ5N
https://pastebin.com/jj0Cp5Ux
https://pastebin.com/LzZFQwaZ
https://pastebin.com/tY8wgRRh
https://pastebin.com/XhgJKNVx
https://pastebin.com/zmytyHRD
https://pastebin.com/ps3FBe7w
https://pastebin.com/hKEiN00Z
https://pastebin.com/0krAGqa8
https://pastebin.com/bmN1vmY5
https://pastebin.com/tXEzETbZ
https://pastebin.com/sxvMRT7d
https://pastebin.com/PBYx1zjX
https://pastebin.com/YL3YtvW4
https://pastebin.com/wYkL6tyV
https://pastebin.com/G9yg2jbz
https://pastebin.com/YS9qiTPE
https://pastebin.com/zRzdakmk
https://pastebin.com/cPbmqAwR
https://pastebin.com/k6VnHXDj
https://pastebin.com/xaU4Lde8
https://pastebin.com/T3qhtZbi
https://pastebin.com/ti5Gv5Uy
https://pastebin.com/p9B6cLzp
https://pastebin.com/0McP0kzG
https://pastebin.com/QRsCLz7H
https://pastebin.com/rTnmVGim
https://pastebin.com/dirbSPyj
https://pastebin.com/QJ1gdh7k
https://pastebin.com/qgCPAp80
https://pastebin.com/H6YUkDVV
https://pastebin.com/YzEYse2d
https://pastebin.com/kUbmfLr0
https://pastebin.com/DmfhXUWK
https://pastebin.com/rxfuCib1
https://pastebin.com/wCR9BHRT
https://pastebin.com/rfJqDQph
https://pastebin.com/UVXE7g9P
https://pastebin.com/vB8FW842
https://pastebin.com/93JiaU8B
https://pastebin.com/40fKbQF1
https://pastebin.com/UpbtX9Vs
https://pastebin.com/GwhrB2rt
https://pastebin.com/CMt33cpf
https://pastebin.com/qPsRkEzz
https://pastebin.com/RR7a27Wk
https://pastebin.com/zU5CC8qs
https://pastebin.com/D3aJGYQk
https://pastebin.com/hx9ya1cr
https://pastebin.com/2GgcbCuP
https://pastebin.com/gYczTsfK
https://pastebin.com/VXsw9xTt
https://pastebin.com/R7DhK5Tq
https://pastebin.com/hHKGpdvA
https://pastebin.com/xREcgGy2
跳转其他页面返回时,如何设置Tabs的默认显示页面
问题现象
使用Tabs组件,如何实现当前显示页签为1的页面内容,点击某个页签使用router跳转到其他页面再返回时,显示的依然是页签为1的页面内容?
效果预览
点击放大
背景知识
onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景,仅@Entry装饰的自定义组件作为页面时生效。
Tabs:通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图。
getRouter().pushUrl():跳转到应用内的指定页面,通过Promise获取跳转异常的返回结果。
解决方案
@Entry修饰的页面在显示时会触发onPageShow生命周期回调,可以在此回调中设置页面显示的初始状态,Tabs组件显示具体哪个页面由Tabs内的currentIndex参数决定的,当页面返回时在onPageShow()方法内重设currentIndex的值使其显示对应页面。运行下述示例需要自行创建一个简单的PageA页面。
class TabBar {
title: string;
index: number;
constructor(title: string, index: number) {
this.title = title;
this.index = index;
}
}
@Entry
@Component
struct TabsTestPage {
uiContext = this.getUIContext();
// 当前选中Tabs的索引
@State currentIndex: number = 1;
// 判断Tabs是否选中(用于自定义Tabs列表的选中状态)
@State selectedIndex: number = 0;
private tabsController: TabsController = new TabsController();
private tabBars: TabBar[] = [
new TabBar('翻译机', 0),
new TabBar('首页', 1),
new TabBar('推荐', 2),
];
// 页面显示时初始化状态
onPageShow(): void {
this.currentIndex = 1;
}
// 自定义Tabs组件构建函数
@Builder
TabBuilder() {
List() {
ForEach(this.tabBars, (item: TabBar, index: number) => {
ListItem() {
Column() {
Text(item.title) // 根据选中状态改变文字颜色
.fontColor(this.currentIndex === item.index ? '#0A59F7' : Color.Black)
.fontSize(20)
.align(Alignment.Center);
}
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Start)
.margin({ left: 10 })
.onClick(() => {
// 更新Tabs组件的选中状态
this.currentIndex = index;
});
}.height(100);
});
}.height(100)
.listDirection(Axis.Horizontal)
.scrollBar(BarState.Off);
}
build() {
Column() {
Flex({ alignItems: ItemAlign.Center }) {
this.TabBuilder();
}.width('100%').height(100);
Tabs({ barPosition: BarPosition.Start, index: this.currentIndex, controller: this.tabsController }) {
TabContent() {
Text('翻译机的内容')
.fontSize(30)
.onClick(() => {
let promptShow = this.uiContext.getPromptAction();
promptShow.showToast({
message: '翻译机跳转'
});
// 需要自行创建一个PageA的@Entry页面
this.uiContext.getRouter().pushUrl({ url: 'pages/PageA' });
});
};
TabContent() {
Text('首页的内容')
.fontSize(30);
};
TabContent() {
Text('推荐的内容')
.fontSize(30);
};
}.barHeight(0)
.onAnimationStart((targetIndex: number) => {
this.currentIndex = targetIndex;
})
.onChange((index: number) => {
// currentIndex控制TabContent显示页签
this.currentIndex = index;
this.selectedIndex = index;
});
}.height('100%').width('100%');
}
}
总结
单例模式跳转时,由于也是复用路由栈内已有的页面实例,也可在本方案所述的页面生命周期内实现。
https://pastebin.com/dty8sxD5
https://pastebin.com/dHEXCxFt
https://pastebin.com/pE6ePUAY
https://pastebin.com/2U1LgwWz
https://pastebin.com/WQpk7jSa
https://pastebin.com/G8LPQVtP
https://pastebin.com/VRpjsRTK
https://pastebin.com/aCy0qhBi
https://pastebin.com/NFQMuiXH
https://pastebin.com/S064raxa
https://pastebin.com/4s7w0BBW
https://pastebin.com/m7UDFUJ6
https://pastebin.com/UBeiNw6e
https://pastebin.com/kms3qfYM
https://pastebin.com/dYjjH5yP
https://pastebin.com/6cvgDvgL
https://pastebin.com/KPmn6RCf
https://pastebin.com/DiPdkuXM
https://pastebin.com/53nPeHWZ
https://pastebin.com/WNdjQBtb
https://pastebin.com/gD3BbuEt
https://pastebin.com/K1djmuru
https://pastebin.com/uqMvn9Uq
https://pastebin.com/paibxsw8
https://pastebin.com/Una68Ba5
https://pastebin.com/P7CrcBfR
https://pastebin.com/UiQ5wXCw
https://pastebin.com/LYHMErrw
https://pastebin.com/T3Fjh1Er
https://pastebin.com/m7Zx61yT
https://pastebin.com/dRmeRwsB
https://pastebin.com/vN5zyV7U
https://pastebin.com/hqms8PnB
https://pastebin.com/VJJ9tacg
https://pastebin.com/gnHE24Vx
https://pastebin.com/2DN8jUYE
https://pastebin.com/NYYLtSrA
https://pastebin.com/FJcvCjYv
https://pastebin.com/C50vS8E5
https://pastebin.com/x8G6uZzB
https://pastebin.com/xwqDW20Q
https://pastebin.com/bbrPiHh4
https://pastebin.com/KwcxyrV3
https://pastebin.com/8Yq1Mwpv
https://pastebin.com/rdXkVZnR
https://pastebin.com/X1QpPxj6
https://pastebin.com/rk4jhKwM
https://pastebin.com/AYH757Hd
https://pastebin.com/arrJtdPC
https://pastebin.com/x1a0QkrE
https://pastebin.com/ZPzMV6KV
https://pastebin.com/qZPAArab
https://pastebin.com/dDiiWq7r
https://pastebin.com/wH1CZA3w
https://pastebin.com/9CHyNi7a
https://pastebin.com/Wkzwp2vS
https://pastebin.com/MVpzkpRy
https://pastebin.com/4AuW9a4S
https://pastebin.com/42tkYqcy
https://pastebin.com/XeksS1ku
https://pastebin.com/L110ndKg
https://pastebin.com/4Axgsuj0
https://pastebin.com/eAA2mxp4
https://pastebin.com/aU4WMaqg
https://pastebin.com/f1hun2kM
https://pastebin.com/wiqMJgk3
https://pastebin.com/uEN8vUKX
https://pastebin.com/D5qiKNFB
https://pastebin.com/F65rvR1J
https://pastebin.com/rasLrbYn
https://pastebin.com/21n3MQq5
https://pastebin.com/bXDj2phg
https://pastebin.com/0PVahXVh
https://pastebin.com/7JtdfNGH
https://pastebin.com/0986iXre
https://pastebin.com/NUb3eRVe
https://pastebin.com/gvz7gKTZ
https://pastebin.com/UusP3Pyp
https://pastebin.com/QSMmWQi9
https://pastebin.com/jLtF7m6h
https://pastebin.com/csKNzJ6w
https://pastebin.com/wBeZafkw
https://pastebin.com/KESDZmpi
https://pastebin.com/mppjXxUF
https://pastebin.com/MepfrkCE
https://pastebin.com/ibwAybQc
https://pastebin.com/WuELbpHn
https://pastebin.com/hTM9NQ5N
https://pastebin.com/jj0Cp5Ux
https://pastebin.com/LzZFQwaZ
https://pastebin.com/tY8wgRRh
https://pastebin.com/XhgJKNVx
https://pastebin.com/zmytyHRD
https://pastebin.com/ps3FBe7w
https://pastebin.com/hKEiN00Z
https://pastebin.com/0krAGqa8
https://pastebin.com/bmN1vmY5
https://pastebin.com/tXEzETbZ
https://pastebin.com/sxvMRT7d
https://pastebin.com/PBYx1zjX
https://pastebin.com/YL3YtvW4
https://pastebin.com/wYkL6tyV
https://pastebin.com/G9yg2jbz
https://pastebin.com/YS9qiTPE
https://pastebin.com/zRzdakmk
https://pastebin.com/cPbmqAwR
https://pastebin.com/k6VnHXDj
https://pastebin.com/xaU4Lde8
https://pastebin.com/T3qhtZbi
https://pastebin.com/ti5Gv5Uy
https://pastebin.com/p9B6cLzp
https://pastebin.com/0McP0kzG
https://pastebin.com/QRsCLz7H
https://pastebin.com/rTnmVGim
https://pastebin.com/dirbSPyj
https://pastebin.com/QJ1gdh7k
https://pastebin.com/qgCPAp80
https://pastebin.com/H6YUkDVV
https://pastebin.com/YzEYse2d
https://pastebin.com/kUbmfLr0
https://pastebin.com/DmfhXUWK
https://pastebin.com/rxfuCib1
https://pastebin.com/wCR9BHRT
https://pastebin.com/rfJqDQph
https://pastebin.com/UVXE7g9P
https://pastebin.com/vB8FW842
https://pastebin.com/93JiaU8B
https://pastebin.com/40fKbQF1
https://pastebin.com/UpbtX9Vs
https://pastebin.com/GwhrB2rt
https://pastebin.com/CMt33cpf
https://pastebin.com/qPsRkEzz
https://pastebin.com/RR7a27Wk
https://pastebin.com/zU5CC8qs
https://pastebin.com/D3aJGYQk
https://pastebin.com/hx9ya1cr
https://pastebin.com/2GgcbCuP
https://pastebin.com/gYczTsfK
https://pastebin.com/VXsw9xTt
https://pastebin.com/R7DhK5Tq
https://pastebin.com/hHKGpdvA
https://pastebin.com/xREcgGy2
问题现象
使用Tabs组件,如何实现当前显示页签为1的页面内容,点击某个页签使用router跳转到其他页面再返回时,显示的依然是页签为1的页面内容?
效果预览
点击放大
背景知识
onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景,仅@Entry装饰的自定义组件作为页面时生效。
Tabs:通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图。
getRouter().pushUrl():跳转到应用内的指定页面,通过Promise获取跳转异常的返回结果。
解决方案
@Entry修饰的页面在显示时会触发onPageShow生命周期回调,可以在此回调中设置页面显示的初始状态,Tabs组件显示具体哪个页面由Tabs内的currentIndex参数决定的,当页面返回时在onPageShow()方法内重设currentIndex的值使其显示对应页面。运行下述示例需要自行创建一个简单的PageA页面。
class TabBar {
title: string;
index: number;
constructor(title: string, index: number) {
this.title = title;
this.index = index;
}
}
@Entry
@Component
struct TabsTestPage {
uiContext = this.getUIContext();
// 当前选中Tabs的索引
@State currentIndex: number = 1;
// 判断Tabs是否选中(用于自定义Tabs列表的选中状态)
@State selectedIndex: number = 0;
private tabsController: TabsController = new TabsController();
private tabBars: TabBar[] = [
new TabBar('翻译机', 0),
new TabBar('首页', 1),
new TabBar('推荐', 2),
];
// 页面显示时初始化状态
onPageShow(): void {
this.currentIndex = 1;
}
// 自定义Tabs组件构建函数
@Builder
TabBuilder() {
List() {
ForEach(this.tabBars, (item: TabBar, index: number) => {
ListItem() {
Column() {
Text(item.title) // 根据选中状态改变文字颜色
.fontColor(this.currentIndex === item.index ? '#0A59F7' : Color.Black)
.fontSize(20)
.align(Alignment.Center);
}
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Start)
.margin({ left: 10 })
.onClick(() => {
// 更新Tabs组件的选中状态
this.currentIndex = index;
});
}.height(100);
});
}.height(100)
.listDirection(Axis.Horizontal)
.scrollBar(BarState.Off);
}
build() {
Column() {
Flex({ alignItems: ItemAlign.Center }) {
this.TabBuilder();
}.width('100%').height(100);
Tabs({ barPosition: BarPosition.Start, index: this.currentIndex, controller: this.tabsController }) {
TabContent() {
Text('翻译机的内容')
.fontSize(30)
.onClick(() => {
let promptShow = this.uiContext.getPromptAction();
promptShow.showToast({
message: '翻译机跳转'
});
// 需要自行创建一个PageA的@Entry页面
this.uiContext.getRouter().pushUrl({ url: 'pages/PageA' });
});
};
TabContent() {
Text('首页的内容')
.fontSize(30);
};
TabContent() {
Text('推荐的内容')
.fontSize(30);
};
}.barHeight(0)
.onAnimationStart((targetIndex: number) => {
this.currentIndex = targetIndex;
})
.onChange((index: number) => {
// currentIndex控制TabContent显示页签
this.currentIndex = index;
this.selectedIndex = index;
});
}.height('100%').width('100%');
}
}
总结
单例模式跳转时,由于也是复用路由栈内已有的页面实例,也可在本方案所述的页面生命周期内实现。
https://pastebin.com/dty8sxD5
https://pastebin.com/dHEXCxFt
https://pastebin.com/pE6ePUAY
https://pastebin.com/2U1LgwWz
https://pastebin.com/WQpk7jSa
https://pastebin.com/G8LPQVtP
https://pastebin.com/VRpjsRTK
https://pastebin.com/aCy0qhBi
https://pastebin.com/NFQMuiXH
https://pastebin.com/S064raxa
https://pastebin.com/4s7w0BBW
https://pastebin.com/m7UDFUJ6
https://pastebin.com/UBeiNw6e
https://pastebin.com/kms3qfYM
https://pastebin.com/dYjjH5yP
https://pastebin.com/6cvgDvgL
https://pastebin.com/KPmn6RCf
https://pastebin.com/DiPdkuXM
https://pastebin.com/53nPeHWZ
https://pastebin.com/WNdjQBtb
https://pastebin.com/gD3BbuEt
https://pastebin.com/K1djmuru
https://pastebin.com/uqMvn9Uq
https://pastebin.com/paibxsw8
https://pastebin.com/Una68Ba5
https://pastebin.com/P7CrcBfR
https://pastebin.com/UiQ5wXCw
https://pastebin.com/LYHMErrw
https://pastebin.com/T3Fjh1Er
https://pastebin.com/m7Zx61yT
https://pastebin.com/dRmeRwsB
https://pastebin.com/vN5zyV7U
https://pastebin.com/hqms8PnB
https://pastebin.com/VJJ9tacg
https://pastebin.com/gnHE24Vx
https://pastebin.com/2DN8jUYE
https://pastebin.com/NYYLtSrA
https://pastebin.com/FJcvCjYv
https://pastebin.com/C50vS8E5
https://pastebin.com/x8G6uZzB
https://pastebin.com/xwqDW20Q
https://pastebin.com/bbrPiHh4
https://pastebin.com/KwcxyrV3
https://pastebin.com/8Yq1Mwpv
https://pastebin.com/rdXkVZnR
https://pastebin.com/X1QpPxj6
https://pastebin.com/rk4jhKwM
https://pastebin.com/AYH757Hd
https://pastebin.com/arrJtdPC
https://pastebin.com/x1a0QkrE
https://pastebin.com/ZPzMV6KV
https://pastebin.com/qZPAArab
https://pastebin.com/dDiiWq7r
https://pastebin.com/wH1CZA3w
https://pastebin.com/9CHyNi7a
https://pastebin.com/Wkzwp2vS
https://pastebin.com/MVpzkpRy
https://pastebin.com/4AuW9a4S
https://pastebin.com/42tkYqcy
https://pastebin.com/XeksS1ku
https://pastebin.com/L110ndKg
https://pastebin.com/4Axgsuj0
https://pastebin.com/eAA2mxp4
https://pastebin.com/aU4WMaqg
https://pastebin.com/f1hun2kM
https://pastebin.com/wiqMJgk3
https://pastebin.com/uEN8vUKX
https://pastebin.com/D5qiKNFB
https://pastebin.com/F65rvR1J
https://pastebin.com/rasLrbYn
https://pastebin.com/21n3MQq5
https://pastebin.com/bXDj2phg
https://pastebin.com/0PVahXVh
https://pastebin.com/7JtdfNGH
https://pastebin.com/0986iXre
https://pastebin.com/NUb3eRVe
https://pastebin.com/gvz7gKTZ
https://pastebin.com/UusP3Pyp
https://pastebin.com/QSMmWQi9
https://pastebin.com/jLtF7m6h
https://pastebin.com/csKNzJ6w
https://pastebin.com/wBeZafkw
https://pastebin.com/KESDZmpi
https://pastebin.com/mppjXxUF
https://pastebin.com/MepfrkCE
https://pastebin.com/ibwAybQc
https://pastebin.com/WuELbpHn
https://pastebin.com/hTM9NQ5N
https://pastebin.com/jj0Cp5Ux
https://pastebin.com/LzZFQwaZ
https://pastebin.com/tY8wgRRh
https://pastebin.com/XhgJKNVx
https://pastebin.com/zmytyHRD
https://pastebin.com/ps3FBe7w
https://pastebin.com/hKEiN00Z
https://pastebin.com/0krAGqa8
https://pastebin.com/bmN1vmY5
https://pastebin.com/tXEzETbZ
https://pastebin.com/sxvMRT7d
https://pastebin.com/PBYx1zjX
https://pastebin.com/YL3YtvW4
https://pastebin.com/wYkL6tyV
https://pastebin.com/G9yg2jbz
https://pastebin.com/YS9qiTPE
https://pastebin.com/zRzdakmk
https://pastebin.com/cPbmqAwR
https://pastebin.com/k6VnHXDj
https://pastebin.com/xaU4Lde8
https://pastebin.com/T3qhtZbi
https://pastebin.com/ti5Gv5Uy
https://pastebin.com/p9B6cLzp
https://pastebin.com/0McP0kzG
https://pastebin.com/QRsCLz7H
https://pastebin.com/rTnmVGim
https://pastebin.com/dirbSPyj
https://pastebin.com/QJ1gdh7k
https://pastebin.com/qgCPAp80
https://pastebin.com/H6YUkDVV
https://pastebin.com/YzEYse2d
https://pastebin.com/kUbmfLr0
https://pastebin.com/DmfhXUWK
https://pastebin.com/rxfuCib1
https://pastebin.com/wCR9BHRT
https://pastebin.com/rfJqDQph
https://pastebin.com/UVXE7g9P
https://pastebin.com/vB8FW842
https://pastebin.com/93JiaU8B
https://pastebin.com/40fKbQF1
https://pastebin.com/UpbtX9Vs
https://pastebin.com/GwhrB2rt
https://pastebin.com/CMt33cpf
https://pastebin.com/qPsRkEzz
https://pastebin.com/RR7a27Wk
https://pastebin.com/zU5CC8qs
https://pastebin.com/D3aJGYQk
https://pastebin.com/hx9ya1cr
https://pastebin.com/2GgcbCuP
https://pastebin.com/gYczTsfK
https://pastebin.com/VXsw9xTt
https://pastebin.com/R7DhK5Tq
https://pastebin.com/hHKGpdvA
https://pastebin.com/xREcgGy2
uni-app路由管理神器:@meng-xi/uni-router
为 uni-app (Vue 3) 提供类似 vue-router 风格的路由管理系统(uni_modules 版本)。
仅支持 Vue 3 — 基于 Vue 3 Composition API,不支持 Vue 2 项目。
特性
- vue-router 风格 API -
push/replace/relaunch/back - 路由守卫 -
beforeEach/beforeResolve/afterEach/beforeEnter,支持guardRoute冷启动补执行 - 页面间通信 -
useUniEventChannel内置通信管理器,粘性缓存确保时序安全 - 声明式组件 -
RouterLink/TabBar/TabBarItem,easycom 自动注册 - 页面参数传递 -
params传递复杂数据,back()后自动保留 - 查询参数增强 -
queryInt()/queryNumber()/queryBool() - 错误处理 -
RouterError/NavigationFailure/UniApiError,instanceof精准判断
安装
将 mxuni-router 目录复制到项目的 uni_modules 目录下即可,无需 npm 安装。
快速开始
// main.ts
import { createSSRApp } from 'vue'
import { createRouter } from './uni_modules/mxuni-router/js_sdk/index.js'
import App from './App.vue'
const router = createRouter({
routes: [
{ path: 'pages/index/index', name: 'home', meta: { title: '首页' } },
{ path: 'pages/about/about', name: 'about', meta: { requireAuth: true } }
],
interceptUniApi: true
})
export function createApp() {
const app = createSSRApp(App)
app.use(router)
return { app }
}
组件在 uni_modules 中自动注册,直接使用即可:
<RouterLink to="/pages/about/about">关于</RouterLink>
<TabBar selected-color="#007aff">
<TabBarItem to="/pages/index/index" text="首页" />
<TabBarItem to="/pages/about/about" text="关于" :badge="5" />
</TabBar>
文档
📖 https://mengxi-studio.github.io/uni-router/v1/
License
为 uni-app (Vue 3) 提供类似 vue-router 风格的路由管理系统(uni_modules 版本)。
仅支持 Vue 3 — 基于 Vue 3 Composition API,不支持 Vue 2 项目。
特性
- vue-router 风格 API -
push/replace/relaunch/back - 路由守卫 -
beforeEach/beforeResolve/afterEach/beforeEnter,支持guardRoute冷启动补执行 - 页面间通信 -
useUniEventChannel内置通信管理器,粘性缓存确保时序安全 - 声明式组件 -
RouterLink/TabBar/TabBarItem,easycom 自动注册 - 页面参数传递 -
params传递复杂数据,back()后自动保留 - 查询参数增强 -
queryInt()/queryNumber()/queryBool() - 错误处理 -
RouterError/NavigationFailure/UniApiError,instanceof精准判断
安装
将 mxuni-router 目录复制到项目的 uni_modules 目录下即可,无需 npm 安装。
快速开始
// main.ts
import { createSSRApp } from 'vue'
import { createRouter } from './uni_modules/mxuni-router/js_sdk/index.js'
import App from './App.vue'
const router = createRouter({
routes: [
{ path: 'pages/index/index', name: 'home', meta: { title: '首页' } },
{ path: 'pages/about/about', name: 'about', meta: { requireAuth: true } }
],
interceptUniApi: true
})
export function createApp() {
const app = createSSRApp(App)
app.use(router)
return { app }
}
组件在 uni_modules 中自动注册,直接使用即可:
<RouterLink to="/pages/about/about">关于</RouterLink>
<TabBar selected-color="#007aff">
<TabBarItem to="/pages/index/index" text="首页" />
<TabBarItem to="/pages/about/about" text="关于" :badge="5" />
</TabBar>
文档
📖 https://mengxi-studio.github.io/uni-router/v1/
License
收起阅读 »并发接口拦截器中如何实现全局弹窗
问题现象
在应用中,每个接口都可能返回响应码Code,需要在接口拦截器中实现一个全局弹窗。由于接口是并发的,弹窗只能弹出一次。那么如何实现这个全局弹窗呢?此外,由于弹窗可能会在多个页面弹出(如启动页、登录页、主页等),这些页面可能会被销毁,这会导致弹窗无法正常显示。
效果预览
点击放大 点击放大 点击放大
背景知识
使用弹窗组件时,可优先考虑自定义弹窗,便于自定义弹窗的样式与内容。通过CustomDialogController类显示自定义弹窗,不支持直接在类中定义和使用。通常需要将弹框逻辑封装成Builder或其他组件,以便在需要时调用。
可以使用@StorageLink与AppStorage中的key对应的属性建立双向数据同步,该属性可以和UI组件同步,且可以在应用业务逻辑中被访问。
解决方案
在并发接口拦截器中,由于弹窗弹出位置不确定且仅弹出一次,因此需要维护一个全局变量来保证弹窗的弹出状态。可以在AppStorage中定义弹窗弹出状态,并通过@StorageLink来获取弹窗是否曾弹出,具体实现可参考以下示例:
EntryAbility.ets的onWindowStageCreate方法里通过AppStorage定义关于弹框显示的全局属性,默认false不显示:
windowStage.loadContent('pages/Index', (err) => {
AppStorage.setOrCreate('showGlobalCustomDialog', false);
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
封装1个弹框实例类CustomDialogLayout.ets:
@CustomDialog
export struct CustomDialogLayout {
controller?: CustomDialogController;
build() {
Column() {
Text('Global Custom Dialog Test');
}
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.height(400);
}
}
Index.ets,在该页面import并创建实例import { CustomDialogLayout } from './CustomDialogLayout',并且监听showGlobalCustomDialog属性值的改变并进行拉起弹窗动作:
import { CustomDialogLayout } from './CustomDialogLayout';
@Entry
@Component
struct Index {
@Provide('pathStack') pathStack: NavPathStack = new NavPathStack();
@StorageLink('showGlobalCustomDialog') @Watch('globalCustomDialogStateChange') showGlobalCustomDialog: boolean = false;
globalCustomDialogStateChange() {
if (this.showGlobalCustomDialog) {
if (this.dialogController != null) {
this.dialogController.open();
AppStorage.setOrCreate('showGlobalCustomDialog', false);
}
}
}
dialogController: CustomDialogController | null = new CustomDialogController({
builder: CustomDialogLayout({}),
autoCancel: true,
alignment: DialogAlignment.Center,
});
build() {
Navigation(this.pathStack) {
RelativeContainer() {
Button('跳转其他页面')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: 'container', align: VerticalAlign.Center },
middle: { anchor: 'container', align: HorizontalAlign.Center }
})
.onClick(() => {
this.pathStack.pushPathByName('DetailPage', null);
});
}
.height('100%')
.width('100%');
}
.mode(NavigationMode.Stack);
}
}
DetailPage.ets,在该页面设置showGlobalCustomDialog全局属性为true即可调起弹框:
@Builder
export function DetailPageBuilder() {
DetailPage();
}
@Component
export struct DetailPage {
@Consume('pathStack') pathStack: NavPathStack;
build() {
NavDestination() {
RelativeContainer() {
Button('promptAction弹窗')
.onClick(() => {
AppStorage.setOrCreate('showGlobalCustomDialog', true);
})
.alignRules({
center: { anchor: 'container', align: VerticalAlign.Center },
middle: { anchor: 'container', align: HorizontalAlign.Center }
});
};
}.title('DetailPage');
}
}
总结
在并发场景下,为了实现条件判断和控制弹窗弹出的行为,可以通过在AppStorage中维护一个全局变量,并使用@StorageLink进行同步监听。这种方法可以保证弹窗只弹出一次,从而避免重复弹出问题。
https://pastebin.com/JihHB9qj
https://pastebin.com/pp4XgNHN
https://pastebin.com/gFi7Ty8i
https://pastebin.com/6nsKZsrR
https://pastebin.com/ZC5tJmyd
https://pastebin.com/vAsXffGW
https://pastebin.com/ha4pYMBs
https://pastebin.com/Lef3jLKc
https://pastebin.com/TJ2Pj2vx
https://pastebin.com/RhihyVw7
https://pastebin.com/erh6eVzC
https://pastebin.com/bKGKK1dm
https://pastebin.com/2UFK6D1y
https://pastebin.com/7R29tEfq
https://pastebin.com/xrH49CPL
https://pastebin.com/gdw9yUrU
https://pastebin.com/EUVjawaY
https://pastebin.com/4TuGak9p
https://pastebin.com/EqWheA0Z
https://pastebin.com/nAWfX2Ce
https://pastebin.com/CAFaEHu9
https://pastebin.com/xCXmycSc
https://pastebin.com/WmenYTsS
https://pastebin.com/z9spTDji
https://pastebin.com/uVp7Nsp1
https://pastebin.com/TEaDnLGQ
https://pastebin.com/mURyCtEV
https://pastebin.com/9zMixq54
https://pastebin.com/YPKWWGwt
https://pastebin.com/BbpwnY0b
https://pastebin.com/kzXJ2Sj5
https://pastebin.com/98nCcftH
https://pastebin.com/g4bA9DML
https://pastebin.com/vjf4hZig
https://pastebin.com/pQPff8MH
https://pastebin.com/2iXVx5dF
https://pastebin.com/prBWGhpk
https://pastebin.com/EJKznYt6
https://pastebin.com/Waa8ezGu
https://pastebin.com/Hje8V6t1
https://pastebin.com/awWHMgZY
https://pastebin.com/EiUjbuN0
https://pastebin.com/XaW0WUTn
https://pastebin.com/JRnZe7Tm
https://pastebin.com/507BPDNz
https://pastebin.com/LKaevb2Y
https://pastebin.com/Sptc9XfX
https://pastebin.com/0e52nKM3
https://pastebin.com/pRFAKR9p
https://pastebin.com/WR98YgEn
https://pastebin.com/MHZyyxsk
https://pastebin.com/zygnUg3G
https://pastebin.com/Y1R09XKN
https://pastebin.com/vWzeCqM7
https://pastebin.com/DNTXXrcn
https://pastebin.com/BfqGBiUU
https://pastebin.com/LwskiZkg
https://pastebin.com/ZwvqpJFq
https://pastebin.com/FQeL9Lfv
https://pastebin.com/s79uWV97
https://pastebin.com/vzVbnZtK
https://pastebin.com/WjTV0ewV
https://pastebin.com/jyVBYEik
https://pastebin.com/Uzcz0PVy
https://pastebin.com/eQxuA195
https://pastebin.com/RpRNfuyG
https://pastebin.com/ZYkLnSEJ
https://pastebin.com/dbvxYAEL
https://pastebin.com/Jr3BFC6q
https://pastebin.com/AK3Fkc6N
https://pastebin.com/eR15KBgQ
https://pastebin.com/MiNxqBcV
https://pastebin.com/0hgraL2f
https://pastebin.com/BpkhJeBE
https://pastebin.com/CtEt6Jhh
https://pastebin.com/XbCJyFi6
https://pastebin.com/K3kM1H0t
https://pastebin.com/szcQwiUx
https://pastebin.com/fBNMQweP
https://pastebin.com/a5BFSqKk
https://pastebin.com/ugaAe5bM
https://pastebin.com/X7cTkwjx
https://pastebin.com/eYM3ucew
https://pastebin.com/pfti4VKT
https://pastebin.com/hsHQwwgQ
https://pastebin.com/z17PcPCS
https://pastebin.com/6gjv06Ri
https://pastebin.com/XE9Bajbu
https://pastebin.com/3wVx4KqR
https://pastebin.com/586hmSqD
https://pastebin.com/qEzZahup
https://pastebin.com/3WGJ7PKM
https://pastebin.com/CzRJGMNg
https://pastebin.com/Pfb1e6ww
https://pastebin.com/kSt2e5bn
https://pastebin.com/WUApxj6r
https://pastebin.com/xEagAfbb
https://pastebin.com/cvkQfAjy
https://pastebin.com/sBFEPgwx
https://pastebin.com/DeThFfPT
https://pastebin.com/j7HGLNrn
https://pastebin.com/mK2WQPEr
https://pastebin.com/Kf6eEfSp
https://pastebin.com/Cd9cBH5m
https://pastebin.com/g0X42ybn
https://pastebin.com/mZyGx93u
https://pastebin.com/VkfwmAZR
https://pastebin.com/0gTGQkqF
https://pastebin.com/pqFk4DeF
https://pastebin.com/5y7Vq3Hb
https://pastebin.com/MWBqZUM0
https://pastebin.com/KU878iEv
https://pastebin.com/zXqt3ek3
https://pastebin.com/c0Bg5wWE
https://pastebin.com/UVdGibVC
https://pastebin.com/QpGAkSny
https://pastebin.com/SUUDWFHy
https://pastebin.com/xyLjSgQV
https://pastebin.com/8PbH8qL1
https://pastebin.com/Hk1ALAEc
https://pastebin.com/eXtjEzhZ
https://pastebin.com/nfC0PSKj
https://pastebin.com/MYfr25pM
https://pastebin.com/mBfJkAaY
https://pastebin.com/W7Z00fYY
https://pastebin.com/fPRnwJds
https://pastebin.com/856Bn5dS
问题现象
在应用中,每个接口都可能返回响应码Code,需要在接口拦截器中实现一个全局弹窗。由于接口是并发的,弹窗只能弹出一次。那么如何实现这个全局弹窗呢?此外,由于弹窗可能会在多个页面弹出(如启动页、登录页、主页等),这些页面可能会被销毁,这会导致弹窗无法正常显示。
效果预览
点击放大 点击放大 点击放大
背景知识
使用弹窗组件时,可优先考虑自定义弹窗,便于自定义弹窗的样式与内容。通过CustomDialogController类显示自定义弹窗,不支持直接在类中定义和使用。通常需要将弹框逻辑封装成Builder或其他组件,以便在需要时调用。
可以使用@StorageLink与AppStorage中的key对应的属性建立双向数据同步,该属性可以和UI组件同步,且可以在应用业务逻辑中被访问。
解决方案
在并发接口拦截器中,由于弹窗弹出位置不确定且仅弹出一次,因此需要维护一个全局变量来保证弹窗的弹出状态。可以在AppStorage中定义弹窗弹出状态,并通过@StorageLink来获取弹窗是否曾弹出,具体实现可参考以下示例:
EntryAbility.ets的onWindowStageCreate方法里通过AppStorage定义关于弹框显示的全局属性,默认false不显示:
windowStage.loadContent('pages/Index', (err) => {
AppStorage.setOrCreate('showGlobalCustomDialog', false);
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
封装1个弹框实例类CustomDialogLayout.ets:
@CustomDialog
export struct CustomDialogLayout {
controller?: CustomDialogController;
build() {
Column() {
Text('Global Custom Dialog Test');
}
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.height(400);
}
}
Index.ets,在该页面import并创建实例import { CustomDialogLayout } from './CustomDialogLayout',并且监听showGlobalCustomDialog属性值的改变并进行拉起弹窗动作:
import { CustomDialogLayout } from './CustomDialogLayout';
@Entry
@Component
struct Index {
@Provide('pathStack') pathStack: NavPathStack = new NavPathStack();
@StorageLink('showGlobalCustomDialog') @Watch('globalCustomDialogStateChange') showGlobalCustomDialog: boolean = false;
globalCustomDialogStateChange() {
if (this.showGlobalCustomDialog) {
if (this.dialogController != null) {
this.dialogController.open();
AppStorage.setOrCreate('showGlobalCustomDialog', false);
}
}
}
dialogController: CustomDialogController | null = new CustomDialogController({
builder: CustomDialogLayout({}),
autoCancel: true,
alignment: DialogAlignment.Center,
});
build() {
Navigation(this.pathStack) {
RelativeContainer() {
Button('跳转其他页面')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: 'container', align: VerticalAlign.Center },
middle: { anchor: 'container', align: HorizontalAlign.Center }
})
.onClick(() => {
this.pathStack.pushPathByName('DetailPage', null);
});
}
.height('100%')
.width('100%');
}
.mode(NavigationMode.Stack);
}
}
DetailPage.ets,在该页面设置showGlobalCustomDialog全局属性为true即可调起弹框:
@Builder
export function DetailPageBuilder() {
DetailPage();
}
@Component
export struct DetailPage {
@Consume('pathStack') pathStack: NavPathStack;
build() {
NavDestination() {
RelativeContainer() {
Button('promptAction弹窗')
.onClick(() => {
AppStorage.setOrCreate('showGlobalCustomDialog', true);
})
.alignRules({
center: { anchor: 'container', align: VerticalAlign.Center },
middle: { anchor: 'container', align: HorizontalAlign.Center }
});
};
}.title('DetailPage');
}
}
总结
在并发场景下,为了实现条件判断和控制弹窗弹出的行为,可以通过在AppStorage中维护一个全局变量,并使用@StorageLink进行同步监听。这种方法可以保证弹窗只弹出一次,从而避免重复弹出问题。
https://pastebin.com/JihHB9qj
https://pastebin.com/pp4XgNHN
https://pastebin.com/gFi7Ty8i
https://pastebin.com/6nsKZsrR
https://pastebin.com/ZC5tJmyd
https://pastebin.com/vAsXffGW
https://pastebin.com/ha4pYMBs
https://pastebin.com/Lef3jLKc
https://pastebin.com/TJ2Pj2vx
https://pastebin.com/RhihyVw7
https://pastebin.com/erh6eVzC
https://pastebin.com/bKGKK1dm
https://pastebin.com/2UFK6D1y
https://pastebin.com/7R29tEfq
https://pastebin.com/xrH49CPL
https://pastebin.com/gdw9yUrU
https://pastebin.com/EUVjawaY
https://pastebin.com/4TuGak9p
https://pastebin.com/EqWheA0Z
https://pastebin.com/nAWfX2Ce
https://pastebin.com/CAFaEHu9
https://pastebin.com/xCXmycSc
https://pastebin.com/WmenYTsS
https://pastebin.com/z9spTDji
https://pastebin.com/uVp7Nsp1
https://pastebin.com/TEaDnLGQ
https://pastebin.com/mURyCtEV
https://pastebin.com/9zMixq54
https://pastebin.com/YPKWWGwt
https://pastebin.com/BbpwnY0b
https://pastebin.com/kzXJ2Sj5
https://pastebin.com/98nCcftH
https://pastebin.com/g4bA9DML
https://pastebin.com/vjf4hZig
https://pastebin.com/pQPff8MH
https://pastebin.com/2iXVx5dF
https://pastebin.com/prBWGhpk
https://pastebin.com/EJKznYt6
https://pastebin.com/Waa8ezGu
https://pastebin.com/Hje8V6t1
https://pastebin.com/awWHMgZY
https://pastebin.com/EiUjbuN0
https://pastebin.com/XaW0WUTn
https://pastebin.com/JRnZe7Tm
https://pastebin.com/507BPDNz
https://pastebin.com/LKaevb2Y
https://pastebin.com/Sptc9XfX
https://pastebin.com/0e52nKM3
https://pastebin.com/pRFAKR9p
https://pastebin.com/WR98YgEn
https://pastebin.com/MHZyyxsk
https://pastebin.com/zygnUg3G
https://pastebin.com/Y1R09XKN
https://pastebin.com/vWzeCqM7
https://pastebin.com/DNTXXrcn
https://pastebin.com/BfqGBiUU
https://pastebin.com/LwskiZkg
https://pastebin.com/ZwvqpJFq
https://pastebin.com/FQeL9Lfv
https://pastebin.com/s79uWV97
https://pastebin.com/vzVbnZtK
https://pastebin.com/WjTV0ewV
https://pastebin.com/jyVBYEik
https://pastebin.com/Uzcz0PVy
https://pastebin.com/eQxuA195
https://pastebin.com/RpRNfuyG
https://pastebin.com/ZYkLnSEJ
https://pastebin.com/dbvxYAEL
https://pastebin.com/Jr3BFC6q
https://pastebin.com/AK3Fkc6N
https://pastebin.com/eR15KBgQ
https://pastebin.com/MiNxqBcV
https://pastebin.com/0hgraL2f
https://pastebin.com/BpkhJeBE
https://pastebin.com/CtEt6Jhh
https://pastebin.com/XbCJyFi6
https://pastebin.com/K3kM1H0t
https://pastebin.com/szcQwiUx
https://pastebin.com/fBNMQweP
https://pastebin.com/a5BFSqKk
https://pastebin.com/ugaAe5bM
https://pastebin.com/X7cTkwjx
https://pastebin.com/eYM3ucew
https://pastebin.com/pfti4VKT
https://pastebin.com/hsHQwwgQ
https://pastebin.com/z17PcPCS
https://pastebin.com/6gjv06Ri
https://pastebin.com/XE9Bajbu
https://pastebin.com/3wVx4KqR
https://pastebin.com/586hmSqD
https://pastebin.com/qEzZahup
https://pastebin.com/3WGJ7PKM
https://pastebin.com/CzRJGMNg
https://pastebin.com/Pfb1e6ww
https://pastebin.com/kSt2e5bn
https://pastebin.com/WUApxj6r
https://pastebin.com/xEagAfbb
https://pastebin.com/cvkQfAjy
https://pastebin.com/sBFEPgwx
https://pastebin.com/DeThFfPT
https://pastebin.com/j7HGLNrn
https://pastebin.com/mK2WQPEr
https://pastebin.com/Kf6eEfSp
https://pastebin.com/Cd9cBH5m
https://pastebin.com/g0X42ybn
https://pastebin.com/mZyGx93u
https://pastebin.com/VkfwmAZR
https://pastebin.com/0gTGQkqF
https://pastebin.com/pqFk4DeF
https://pastebin.com/5y7Vq3Hb
https://pastebin.com/MWBqZUM0
https://pastebin.com/KU878iEv
https://pastebin.com/zXqt3ek3
https://pastebin.com/c0Bg5wWE
https://pastebin.com/UVdGibVC
https://pastebin.com/QpGAkSny
https://pastebin.com/SUUDWFHy
https://pastebin.com/xyLjSgQV
https://pastebin.com/8PbH8qL1
https://pastebin.com/Hk1ALAEc
https://pastebin.com/eXtjEzhZ
https://pastebin.com/nfC0PSKj
https://pastebin.com/MYfr25pM
https://pastebin.com/mBfJkAaY
https://pastebin.com/W7Z00fYY
https://pastebin.com/fPRnwJds
https://pastebin.com/856Bn5dS
uni-app路由管理神器:vue-router风格体验
@meng-xi/uni-router
为 uni-app (Vue 3) 提供类似 vue-router 风格的路由管理系统(uni_modules 版本)。
⚠️ 仅支持 Vue 3 — 本库基于 Vue 3 Composition API(
inject/ref/defineProps/defineEmits)和app.provide等 Vue 3 专属 API,不支持 uni-app Vue 2 项目。
特性
- vue-router 风格 API -
push/replace/relaunch/back,零学习成本 - 路由守卫 -
beforeEach/beforeResolve/afterEach/beforeEnter,支持next(location, { mode })指定重定向方式 - 命名路由 & 路由元信息 - 通过
name导航,meta携带自定义数据 - TypeScript 类型提示 - 路由名称和路径自动补全与类型检查
- uni API 拦截 - 可选拦截原生导航 API,统一守卫流程(
interceptUniApi) - 页面间通信 -
useUniEventChannel启用后所有导航方式均支持eventChannel双向通信,目标页通过usePageChannel()获取通道,基于uni.$emit全局事件总线,粘性缓存确保时序安全 - 声明式导航 -
RouterLink组件,基于 uninavigator封装,支持导航参数、动画、页面通信 - 页面参数传递 -
params传递复杂数据,不暴露在 URL,支持persistent持久化 - 查询参数增强 -
queryInt()/queryNumber()/queryBool()便捷解析 - 导航动画 -
push/replace/back支持动画参数,仅 App 端生效 - 路由状态同步 -
syncRoute()处理浏览器后退、物理返回键等场景 - 错误处理 - 完整的
RouterError/NavigationFailure体系,onError全局捕获 - 组合式 API -
useRouter()/useRoute()/usePageChannel()响应式访问路由与通信通道
安装
uni_modules(推荐)
将 mxuni-router 目录复制到项目的 uni_modules 目录下即可,无需 npm 安装。
npm
pnpm add @meng-xi/uni-router
npm 方式需将导入路径改为
@meng-xi/uni-router。
快速开始
1. 创建路由器
// main.ts
import { createSSRApp } from 'vue'
import { createRouter } from './uni_modules/mxuni-router/js_sdk/index.js'
import App from './App.vue'
const router = createRouter({
routes: [
{ path: 'pages/index/index', name: 'home', meta: { title: '首页' } },
{ path: 'pages/about/about', name: 'about', meta: { title: '关于', requireAuth: true } }
],
strict: true,
interceptUniApi: true, // 拦截 uni 原生导航 API,确保守卫生效
useUniEventChannel: true // 启用内置通信管理器,所有导航方式支持页面间双向通信
})
export function createApp() {
const app = createSSRApp(App)
app.use(router)
return { app }
}
2. 路由导航
import { useRouter, useRoute } from './uni_modules/mxuni-router/js_sdk/index.js'
const router = useRouter()
const route = useRoute() // 返回响应式引用,路由变化时自动更新
// 路径导航
await router.push({ path: '/pages/about/about', query: { id: '1' } })
// 命名导航
await router.push({ name: 'about' })
// 页面参数传递(params 不暴露在 URL,支持复杂数据)
await router.push({ path: '/pages/detail/detail', params: { info: { name: 'Tom' } } })
// 返回(执行完整守卫链)
await router.back()
3. 页面间通信
启用 useUniEventChannel 后,所有导航方式均返回 eventChannel,目标页通过 usePageChannel() 获取通道:
// ===== 发起页 =====
const result = await router.push({ path: '/pages/detail/detail' })
result.eventChannel?.on('ready', data => console.log('目标页已就绪:', data))
result.eventChannel?.emit('data', { msg: '发给目标页的数据' })
// ===== 目标页(detail.vue)=====
import { usePageChannel } from './uni_modules/mxuni-router/js_sdk/index.js'
const channel = usePageChannel() // 无 __nav_id 时返回 noopChannel,无需判空
channel.on('data', data => console.log('收到发起页数据:', data))
channel.emit('ready', { status: 'ok' }) // 粘性缓存:发起页尚未注册 on 也能收到
4. 路由守卫
router.beforeEach((to, from, next) => {
if (to.meta.requireAuth && !isLoggedIn()) {
// 使用 replace 模式重定向,避免登录页之后残留受保护页面的历史
next({ name: 'login', query: { redirect: to.fullPath } }, { mode: 'replace' })
} else {
next()
}
})
5. 自动生成路由配置(推荐)
配合 @meng-xi/vite-plugin 的 generateRouter 插件,可从 pages.json 自动生成路由配置和类型声明:
pnpm add @meng-xi/vite-plugin -D
// vite.config.ts
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
import { generateRouter } from '@meng-xi/vite-plugin'
export default defineConfig({
plugins: [
uni(),
generateRouter({
pagesJsonPath: 'src/pages.json',
outputPath: 'src/router.config.ts',
dts: true,
metaMapping: {
navigationBarTitleText: 'title',
requireAuth: 'requireAuth'
}
})
]
})
然后在 main.ts 中导入生成的路由配置:
import { createRouter } from './uni_modules/mxuni-router/js_sdk/index.js'
import routes from './router.config'
const router = createRouter({ routes })
文档
完整的 API 参考、配置项说明、RouterLink 组件属性、类型定义等请查阅官方网站:
📖 https://mengxi-studio.github.io/uni-router/
License
@meng-xi/uni-router
为 uni-app (Vue 3) 提供类似 vue-router 风格的路由管理系统(uni_modules 版本)。
⚠️ 仅支持 Vue 3 — 本库基于 Vue 3 Composition API(
inject/ref/defineProps/defineEmits)和app.provide等 Vue 3 专属 API,不支持 uni-app Vue 2 项目。
特性
- vue-router 风格 API -
push/replace/relaunch/back,零学习成本 - 路由守卫 -
beforeEach/beforeResolve/afterEach/beforeEnter,支持next(location, { mode })指定重定向方式 - 命名路由 & 路由元信息 - 通过
name导航,meta携带自定义数据 - TypeScript 类型提示 - 路由名称和路径自动补全与类型检查
- uni API 拦截 - 可选拦截原生导航 API,统一守卫流程(
interceptUniApi) - 页面间通信 -
useUniEventChannel启用后所有导航方式均支持eventChannel双向通信,目标页通过usePageChannel()获取通道,基于uni.$emit全局事件总线,粘性缓存确保时序安全 - 声明式导航 -
RouterLink组件,基于 uninavigator封装,支持导航参数、动画、页面通信 - 页面参数传递 -
params传递复杂数据,不暴露在 URL,支持persistent持久化 - 查询参数增强 -
queryInt()/queryNumber()/queryBool()便捷解析 - 导航动画 -
push/replace/back支持动画参数,仅 App 端生效 - 路由状态同步 -
syncRoute()处理浏览器后退、物理返回键等场景 - 错误处理 - 完整的
RouterError/NavigationFailure体系,onError全局捕获 - 组合式 API -
useRouter()/useRoute()/usePageChannel()响应式访问路由与通信通道
安装
uni_modules(推荐)
将 mxuni-router 目录复制到项目的 uni_modules 目录下即可,无需 npm 安装。
npm
pnpm add @meng-xi/uni-router
npm 方式需将导入路径改为
@meng-xi/uni-router。
快速开始
1. 创建路由器
// main.ts
import { createSSRApp } from 'vue'
import { createRouter } from './uni_modules/mxuni-router/js_sdk/index.js'
import App from './App.vue'
const router = createRouter({
routes: [
{ path: 'pages/index/index', name: 'home', meta: { title: '首页' } },
{ path: 'pages/about/about', name: 'about', meta: { title: '关于', requireAuth: true } }
],
strict: true,
interceptUniApi: true, // 拦截 uni 原生导航 API,确保守卫生效
useUniEventChannel: true // 启用内置通信管理器,所有导航方式支持页面间双向通信
})
export function createApp() {
const app = createSSRApp(App)
app.use(router)
return { app }
}
2. 路由导航
import { useRouter, useRoute } from './uni_modules/mxuni-router/js_sdk/index.js'
const router = useRouter()
const route = useRoute() // 返回响应式引用,路由变化时自动更新
// 路径导航
await router.push({ path: '/pages/about/about', query: { id: '1' } })
// 命名导航
await router.push({ name: 'about' })
// 页面参数传递(params 不暴露在 URL,支持复杂数据)
await router.push({ path: '/pages/detail/detail', params: { info: { name: 'Tom' } } })
// 返回(执行完整守卫链)
await router.back()
3. 页面间通信
启用 useUniEventChannel 后,所有导航方式均返回 eventChannel,目标页通过 usePageChannel() 获取通道:
// ===== 发起页 =====
const result = await router.push({ path: '/pages/detail/detail' })
result.eventChannel?.on('ready', data => console.log('目标页已就绪:', data))
result.eventChannel?.emit('data', { msg: '发给目标页的数据' })
// ===== 目标页(detail.vue)=====
import { usePageChannel } from './uni_modules/mxuni-router/js_sdk/index.js'
const channel = usePageChannel() // 无 __nav_id 时返回 noopChannel,无需判空
channel.on('data', data => console.log('收到发起页数据:', data))
channel.emit('ready', { status: 'ok' }) // 粘性缓存:发起页尚未注册 on 也能收到
4. 路由守卫
router.beforeEach((to, from, next) => {
if (to.meta.requireAuth && !isLoggedIn()) {
// 使用 replace 模式重定向,避免登录页之后残留受保护页面的历史
next({ name: 'login', query: { redirect: to.fullPath } }, { mode: 'replace' })
} else {
next()
}
})
5. 自动生成路由配置(推荐)
配合 @meng-xi/vite-plugin 的 generateRouter 插件,可从 pages.json 自动生成路由配置和类型声明:
pnpm add @meng-xi/vite-plugin -D
// vite.config.ts
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
import { generateRouter } from '@meng-xi/vite-plugin'
export default defineConfig({
plugins: [
uni(),
generateRouter({
pagesJsonPath: 'src/pages.json',
outputPath: 'src/router.config.ts',
dts: true,
metaMapping: {
navigationBarTitleText: 'title',
requireAuth: 'requireAuth'
}
})
]
})
然后在 main.ts 中导入生成的路由配置:
import { createRouter } from './uni_modules/mxuni-router/js_sdk/index.js'
import routes from './router.config'
const router = createRouter({ routes })
文档
完整的 API 参考、配置项说明、RouterLink 组件属性、类型定义等请查阅官方网站:
📖 https://mengxi-studio.github.io/uni-router/
License
收起阅读 »做AI给我做好了啊 ,傻逼玩意儿,开发者也是脑残
1.AI无法撤回到历史聊天记录
2.你们的AI只要一个项目,打开多个vue,会一起更改
3.你说你们有历史会话,但是他麻痹的会压缩,AI根本读不完
4.AI改了代码,不明确修改了哪里,我tm找了半天找到按钮,点击 接受 更改,哎你猜怎么着,无法接受,点了和没点一样,依然显示xxx行更改,
5.总结,你们tm要做AI给我做好了啊,基础的东西都没有,就en改啊
1.AI无法撤回到历史聊天记录
2.你们的AI只要一个项目,打开多个vue,会一起更改
3.你说你们有历史会话,但是他麻痹的会压缩,AI根本读不完
4.AI改了代码,不明确修改了哪里,我tm找了半天找到按钮,点击 接受 更改,哎你猜怎么着,无法接受,点了和没点一样,依然显示xxx行更改,
5.总结,你们tm要做AI给我做好了啊,基础的东西都没有,就en改啊
收起阅读 »App Store 审核遇到 4.3(a) 怎么办?一次真实处理思路分享
可以,换平台发就不要和上一篇太像。下面这版标题、结构、开头都重新换了,关键词还是保留 苹果4.3(a)、App Store审核、马甲包上架、申诉,适合发 CSDN、知乎、掘金、百家号这类平台。
App Store 审核遇到 4.3(a) 怎么办?一次真实处理思路分享
做 iOS 上架的人,基本都绕不开苹果审核 4.3(a)。尤其是工具类、教育类、内容类、马甲包上架项目,只要 App 和已有产品相似度比较高,就有可能被苹果认为是重复应用。很多开发者收到 4.3(a) 后,第一反应是直接申诉,但实际处理下来,单靠申诉通常很难解决问题。
我更建议把 4.3(a) 当成一次“产品独立性检查”。也就是说,苹果不是只看你有没有换名字、换图标,而是看这个 App 是否真的有独立的功能、独立的内容、独立的用户场景。
- 4.3(a) 被拒,问题通常出在哪里?
苹果 4.3(a) 被拒,常见原因不是单点问题,而是多个相似点叠加造成的。
常见风险包括:
- App 功能和旧版本或同类产品太接近;
- 页面结构、首页布局、底部导航高度相似;
- App Store 截图、标题、描述、关键词重复;
- IPA 包里的资源文件、图片、模块命名相似;
- 只是换了 icon、启动图、主题色,没有改变核心体验;
- App 内还有旧项目名称、旧接口、旧隐私政策或旧文案。
很多时候,开发者觉得自己已经改了不少,但从审核人员视角看,可能还是同一个模板。
- 为什么直接申诉效果不好?
收到 4.3(a) 后,如果直接回复“我们的 App 是原创的,请重新审核”,通常说服力不够。因为苹果更关注 App 本身的实际变化,而不是开发者的口头解释。
如果 App 没有明显改动,截图没换,功能没变,描述还是原来的表达,那么即使申诉,也很容易继续被拒。
所以处理顺序应该是:
先找相似点,再做差异化,再写申诉说明,最后重新提交。
- 真正有效的处理方式是什么?
第一步,先改定位。
不要只把 App 描述成“工具”“学习软件”“生活服务”。要具体到目标人群和使用场景,比如儿童启蒙、考试练习、口语跟读、企业内部管理、个人效率记录等。定位清楚以后,审核备注和产品描述才有支撑。
第二步,改核心功能。
如果只是首页、分类、详情、会员、我的这几个页面来回套,很容易被认为模板化。可以调整功能入口、内容分类、任务流程、数据展示方式,让用户使用路径发生变化。
第三步,改 UI 和截图。
UI 不要只换颜色。首页结构、图标风格、按钮样式、卡片布局、空页面、引导页都应该重新设计。App Store 截图也要单独做,不能几个包共用一套模板。
第四步,清理资源和旧内容。
重点检查图片、音频、banner、课程、文章、接口路径、旧产品名称、旧客服信息、旧隐私政策链接。这些细节很容易让审核认为 App 是复制出来的。
第五步,检查 IPA 相似度。
如果是从旧项目改出来的,最好在提交前做一次包体检查,看看资源结构、模块命名、配置文件、无用代码是否还保留太多旧项目痕迹。
- 申诉应该怎么写?
4.3(a) 的申诉不要写得太硬,也不要说“我们没有问题”。更合适的写法是承认苹果的审核关注点,然后说明你已经完成了哪些调整。
可以这样写:
Hello App Review Team,
Thank you for your review.
We understand the concern regarding Guideline 4.3(a). After receiving the rejection, we carefully reviewed the app and made several updates to better reflect its independent value.
This app is designed for [目标用户] and focuses on [核心使用场景]. In this version, we updated the user interface, adjusted the feature structure, revised the App Store metadata, redesigned screenshots, and removed unrelated legacy content.
The app now provides a clearer user experience and independent functionality for its intended users.
We kindly ask you to review the updated version again. Thank you.
这类申诉的重点不是解释太多,而是让审核人员看到你已经针对问题做了实际调整。
- 重新提交前的检查清单
提交前建议逐项检查:
-
App 名称、副标题、关键词是否独立;
-
描述是否围绕当前 App 重新写;
-
截图是否重新设计;
-
首页和核心页面是否有明显变化;
-
功能流程是否不是简单复制;
-
是否删除旧项目残留信息;
-
隐私政策和用户协议是否匹配;
-
权限申请是否合理;
-
是否存在空功能、假功能、不可用功能;
-
IPA 包内资源和代码是否相似度过高。
-
总结
苹果审核 4.3(a) 并不是不能解决,但不能只靠一句申诉。真正有效的办法,是把 App 从产品定位、功能结构、UI 设计、资源内容、元数据文案、包体结构几个方面重新整理。
对于马甲包上架来说,最重要的一点是:不要让它看起来像复制包,而要让它成为一个有独立场景、独立功能、独立价值的 App。这样再配合清晰的申诉说明,重新提交通过的概率才会更高。
可以,换平台发就不要和上一篇太像。下面这版标题、结构、开头都重新换了,关键词还是保留 苹果4.3(a)、App Store审核、马甲包上架、申诉,适合发 CSDN、知乎、掘金、百家号这类平台。
App Store 审核遇到 4.3(a) 怎么办?一次真实处理思路分享
做 iOS 上架的人,基本都绕不开苹果审核 4.3(a)。尤其是工具类、教育类、内容类、马甲包上架项目,只要 App 和已有产品相似度比较高,就有可能被苹果认为是重复应用。很多开发者收到 4.3(a) 后,第一反应是直接申诉,但实际处理下来,单靠申诉通常很难解决问题。
我更建议把 4.3(a) 当成一次“产品独立性检查”。也就是说,苹果不是只看你有没有换名字、换图标,而是看这个 App 是否真的有独立的功能、独立的内容、独立的用户场景。
- 4.3(a) 被拒,问题通常出在哪里?
苹果 4.3(a) 被拒,常见原因不是单点问题,而是多个相似点叠加造成的。
常见风险包括:
- App 功能和旧版本或同类产品太接近;
- 页面结构、首页布局、底部导航高度相似;
- App Store 截图、标题、描述、关键词重复;
- IPA 包里的资源文件、图片、模块命名相似;
- 只是换了 icon、启动图、主题色,没有改变核心体验;
- App 内还有旧项目名称、旧接口、旧隐私政策或旧文案。
很多时候,开发者觉得自己已经改了不少,但从审核人员视角看,可能还是同一个模板。
- 为什么直接申诉效果不好?
收到 4.3(a) 后,如果直接回复“我们的 App 是原创的,请重新审核”,通常说服力不够。因为苹果更关注 App 本身的实际变化,而不是开发者的口头解释。
如果 App 没有明显改动,截图没换,功能没变,描述还是原来的表达,那么即使申诉,也很容易继续被拒。
所以处理顺序应该是:
先找相似点,再做差异化,再写申诉说明,最后重新提交。
- 真正有效的处理方式是什么?
第一步,先改定位。
不要只把 App 描述成“工具”“学习软件”“生活服务”。要具体到目标人群和使用场景,比如儿童启蒙、考试练习、口语跟读、企业内部管理、个人效率记录等。定位清楚以后,审核备注和产品描述才有支撑。
第二步,改核心功能。
如果只是首页、分类、详情、会员、我的这几个页面来回套,很容易被认为模板化。可以调整功能入口、内容分类、任务流程、数据展示方式,让用户使用路径发生变化。
第三步,改 UI 和截图。
UI 不要只换颜色。首页结构、图标风格、按钮样式、卡片布局、空页面、引导页都应该重新设计。App Store 截图也要单独做,不能几个包共用一套模板。
第四步,清理资源和旧内容。
重点检查图片、音频、banner、课程、文章、接口路径、旧产品名称、旧客服信息、旧隐私政策链接。这些细节很容易让审核认为 App 是复制出来的。
第五步,检查 IPA 相似度。
如果是从旧项目改出来的,最好在提交前做一次包体检查,看看资源结构、模块命名、配置文件、无用代码是否还保留太多旧项目痕迹。
- 申诉应该怎么写?
4.3(a) 的申诉不要写得太硬,也不要说“我们没有问题”。更合适的写法是承认苹果的审核关注点,然后说明你已经完成了哪些调整。
可以这样写:
Hello App Review Team,
Thank you for your review.
We understand the concern regarding Guideline 4.3(a). After receiving the rejection, we carefully reviewed the app and made several updates to better reflect its independent value.
This app is designed for [目标用户] and focuses on [核心使用场景]. In this version, we updated the user interface, adjusted the feature structure, revised the App Store metadata, redesigned screenshots, and removed unrelated legacy content.
The app now provides a clearer user experience and independent functionality for its intended users.
We kindly ask you to review the updated version again. Thank you.
这类申诉的重点不是解释太多,而是让审核人员看到你已经针对问题做了实际调整。
- 重新提交前的检查清单
提交前建议逐项检查:
-
App 名称、副标题、关键词是否独立;
-
描述是否围绕当前 App 重新写;
-
截图是否重新设计;
-
首页和核心页面是否有明显变化;
-
功能流程是否不是简单复制;
-
是否删除旧项目残留信息;
-
隐私政策和用户协议是否匹配;
-
权限申请是否合理;
-
是否存在空功能、假功能、不可用功能;
-
IPA 包内资源和代码是否相似度过高。
-
总结
苹果审核 4.3(a) 并不是不能解决,但不能只靠一句申诉。真正有效的办法,是把 App 从产品定位、功能结构、UI 设计、资源内容、元数据文案、包体结构几个方面重新整理。
对于马甲包上架来说,最重要的一点是:不要让它看起来像复制包,而要让它成为一个有独立场景、独立功能、独立价值的 App。这样再配合清晰的申诉说明,重新提交通过的概率才会更高。
收起阅读 »




