写了个VSCode插件,用Claude把uni-app页面截图直接转成可运行的Vue代码
用VSCode插件+Claude实现uni-app截图转代码的实战记录
上周设计师又扔过来一沓截图,我对着“个人中心”页面的设计稿,手指在键盘上悬了三秒——又要从头写一遍套的布局吗?作为跨端项目里的“截图搬运工”,我太清楚这种重复劳动有多磨人了:打开截图,对照着像素调间距,一行一行敲template,一个普通列表页半小时起步。更扎心的是,这种工作做完就忘,下次换个颜色改个文案,又得重来。
某天下午,我盯着桌面上几十张截图发呆,突然想到:Claude的多模态能力已经能识别图片内容,为什么不能让它直接吐出可运行的uni-app代码?但每次手动拖图进Claude、复制代码、粘贴到项目里,中间至少有七八步操作。于是,一个“截图→右键→生成Vue文件”的念头冒了出来,我花了两个晚上搓了个VSCode插件,把整个流程塞进了一个右键菜单里。
为什么截图转代码是个“看起来简单,做起来翻车”的事
动手前,我列了几个绕不过去的坎。不是所有截图AI都能看懂,也不是所有生成的代码都能直接跑起来。
第一个坑:uni-app的语法不是标准Vue。 AI如果没见过、这些组件,大概率会甩出一堆,然后你手动改成,再调整移动端适配。更烦人的是,uni.xxx API 和 Vue Router 完全是两套东西,AI 生成的页面跳转逻辑基本得重写。
第二个坑:截图里的样式信息是“伪3D”。 AI只能靠视觉猜测间距和颜色,但实际项目里我们用flex布局、rpx单位、scoped样式。它生成的代码经常是margin: 20px硬撑,换个屏幕就崩了。最离谱的一次,Claude给我生成了font-size: 4rpx,我盯着那个小到肉眼看不见的字,哭笑不得。
第三个坑:代码只有壳,没有魂。 登录页截图上显示着输入框和按钮,但AI不会帮你写表单校验、不会处理提交后的跳转、不会绑定v-model。它只能画个皮,里面的逻辑还得你自己填。
我的目标很明确:不是要一个“一键生成完整App”的噱头,而是让开发者从“对着截图敲布局”变成“对着截图改逻辑”。把最无聊的部分——写template、调间距——交给AI,剩下的业务逻辑自己掌控。
插件怎么串起来的:右键→AI→生成文件
核心思路超级简单:在VSCode里选中一张截图,右键点击,Claude API 生成代码,然后自动创建.vue文件并打开。全程不用切窗口,不用复制粘贴。
技术选型上的取舍
- 插件框架:VSCode Extension API + TypeScript,注册一个右键菜单命令,只在图片文件上显示。
- AI模型:Claude 3.5 Sonnet。我试过GPT-4o和Gemini,Claude在视觉理解和代码格式规范上最平衡,而且system prompt能很好地约束输出结构。
- 图片处理:直接传base64给API,省去文件上传的麻烦。
实现流程里的三个关键节点
-
注册命令
在package.json里定义extension.uniImageToCode命令,绑定右键菜单,通过vscode.window.activeTextEditor获取选中文件路径。 -
Prompt设计——这是最烧脑的部分
我写了一个详细的system prompt,核心要求包括:- 必须用uni-app组件(view、scroll-view、uni-list等),禁止
- 样式用rpx单位,字体至少28rpx,布局优先flex
- 输出完整的++三段式
- 输入框用uni-easyinput并给出v-model绑定
- 列表用uni-list和uni-list-item,并定义基础的data数组
特别加了一句反例提示:“不要使用,应该用”,因为Claude经常把中划线写成驼峰。
- 处理AI返回的不完整代码
第一次测试时,Claude只返回了template,没有script。后来我在prompt里加了“请确保输出完整的单文件组件代码,不要省略任何部分”,并在后端做了校验:如果返回内容不包含export default,就重新请求一次。另外,API的max_tokens设成4000,因为uni-app组件代码通常较长,设小了会被截断。
核心代码片段(简化版)
typescript
// 读取图片转base64
const imageBuffer = fs.readFileSync(imagePath);
const base64Image = imageBuffer.toString('base64');
// 调用Claude API
const response = await axios.post('https://api.anthropic.com/v1/messages', {
model: 'claude-3-5-sonnet-20241022',
max_tokens: 4000,
messages: [{
role: 'user',
content: [
{ type: 'text', text: systemPrompt },
{ type: 'image', source: { type: 'base64', media_type: mimeType, data: base64Image } }
]
}]
});
// 提取代码块并创建文件
const code = response.data.content[0].text;
const template = code.match(/[\s\S]*?/)[0];
const script = code.match(//)[0];
const style = code.match(//)[0];
const fullCode = ${template}\n${script}\n${style};
// 写入新文件(如 login.png → login.vue)
vscode.workspace.fs.writeFile(newUri, Buffer.from(fullCode));
vscode.window.showTextDocument(newUri);
API key没有硬编码,通过VSCode配置项让用户自行设置,也支持从环境变量读取。
实战体验:哪些场景真香,哪些翻了车
插件写完后,我拿真实项目里的截图测了一遍,结果有惊喜也有槽点。
真香场景:结构清晰的静态页面,比如设置页、个人中心、商品列表。生成的代码基本可以直接跑,样式还原度大概70%——间距、颜色、字体大小都挺准,布局结构从几十行flex缩减到微调几处。最省时间的是表单页,输入框、按钮、校验框架都给你搭好了,改改字段名就能用。
翻车现场:
- 复杂交互页面:带tab切换、下拉刷新、上拉加载的,Claude生成的代码只是样子像,逻辑基本不可用。比如tab切换它用了v-if硬切换,没有缓存也没有动画,需要自己重写。
- 第三方组件库:截图里用了uView或ThorUI的组件,Claude不认识,直接生成普通的view替代,失去了组件化的优势。这点目前无解,除非我在prompt里把组件库的API全写进去。
- 特殊字体和图标:截图里的图标,Claude会用硬写Unicode字符,而不是用字体图标组件,导致样式和交互都丢失。
我的使用建议:插件最适合生成静态页面或简单的表单/列表页。对于复杂交互的页面,先让它生成骨架,然后对照截图手动补充事件和API调用。另外,生成后一定要检查样式里的rpx值,Claude偶尔会抽风给出不合理的数值(比如字体4rpx,明显是错的)。
效果对比与下一步计划
从手动截图+写代码的30分钟,缩短到生成+微调的5分钟,这是最直观的收益。虽然不完美,但方向对了——把开发者从“像素级还原”中解放出来,专注于业务逻辑和交互设计。
接下来几个想加的功能:
- 支持选择生成风格:比如生成uView组件版本还是原生uni-app版本,通过prompt里的组件库约束实现。
- 增量更新:如果已经有一个.vue文件,只更新template部分,保留原有的script逻辑。这需要解析AST,比较复杂,但很实用。
- 本地模型支持:考虑接入Ollama运行小模型,避免每次都调用云端API,既省钱又保护隐私。不过本地模型的视觉能力目前还差一些,可能只适合简单的页面。
代码已经放在GitHub上了(链接就不放了,搜“uni-app截图转代码”应该能找到)。如果你也在做uni-app开发,并且经常被截图折磨,可以试试这个思路。记住,工具只是辅助,真正的价值在你的业务逻辑和交互设计上。
用VSCode插件+Claude实现uni-app截图转代码的实战记录
上周设计师又扔过来一沓截图,我对着“个人中心”页面的设计稿,手指在键盘上悬了三秒——又要从头写一遍套的布局吗?作为跨端项目里的“截图搬运工”,我太清楚这种重复劳动有多磨人了:打开截图,对照着像素调间距,一行一行敲template,一个普通列表页半小时起步。更扎心的是,这种工作做完就忘,下次换个颜色改个文案,又得重来。
某天下午,我盯着桌面上几十张截图发呆,突然想到:Claude的多模态能力已经能识别图片内容,为什么不能让它直接吐出可运行的uni-app代码?但每次手动拖图进Claude、复制代码、粘贴到项目里,中间至少有七八步操作。于是,一个“截图→右键→生成Vue文件”的念头冒了出来,我花了两个晚上搓了个VSCode插件,把整个流程塞进了一个右键菜单里。
为什么截图转代码是个“看起来简单,做起来翻车”的事
动手前,我列了几个绕不过去的坎。不是所有截图AI都能看懂,也不是所有生成的代码都能直接跑起来。
第一个坑:uni-app的语法不是标准Vue。 AI如果没见过、这些组件,大概率会甩出一堆,然后你手动改成,再调整移动端适配。更烦人的是,uni.xxx API 和 Vue Router 完全是两套东西,AI 生成的页面跳转逻辑基本得重写。
第二个坑:截图里的样式信息是“伪3D”。 AI只能靠视觉猜测间距和颜色,但实际项目里我们用flex布局、rpx单位、scoped样式。它生成的代码经常是margin: 20px硬撑,换个屏幕就崩了。最离谱的一次,Claude给我生成了font-size: 4rpx,我盯着那个小到肉眼看不见的字,哭笑不得。
第三个坑:代码只有壳,没有魂。 登录页截图上显示着输入框和按钮,但AI不会帮你写表单校验、不会处理提交后的跳转、不会绑定v-model。它只能画个皮,里面的逻辑还得你自己填。
我的目标很明确:不是要一个“一键生成完整App”的噱头,而是让开发者从“对着截图敲布局”变成“对着截图改逻辑”。把最无聊的部分——写template、调间距——交给AI,剩下的业务逻辑自己掌控。
插件怎么串起来的:右键→AI→生成文件
核心思路超级简单:在VSCode里选中一张截图,右键点击,Claude API 生成代码,然后自动创建.vue文件并打开。全程不用切窗口,不用复制粘贴。
技术选型上的取舍
- 插件框架:VSCode Extension API + TypeScript,注册一个右键菜单命令,只在图片文件上显示。
- AI模型:Claude 3.5 Sonnet。我试过GPT-4o和Gemini,Claude在视觉理解和代码格式规范上最平衡,而且system prompt能很好地约束输出结构。
- 图片处理:直接传base64给API,省去文件上传的麻烦。
实现流程里的三个关键节点
-
注册命令
在package.json里定义extension.uniImageToCode命令,绑定右键菜单,通过vscode.window.activeTextEditor获取选中文件路径。 -
Prompt设计——这是最烧脑的部分
我写了一个详细的system prompt,核心要求包括:- 必须用uni-app组件(view、scroll-view、uni-list等),禁止
- 样式用rpx单位,字体至少28rpx,布局优先flex
- 输出完整的++三段式
- 输入框用uni-easyinput并给出v-model绑定
- 列表用uni-list和uni-list-item,并定义基础的data数组
特别加了一句反例提示:“不要使用,应该用”,因为Claude经常把中划线写成驼峰。
- 处理AI返回的不完整代码
第一次测试时,Claude只返回了template,没有script。后来我在prompt里加了“请确保输出完整的单文件组件代码,不要省略任何部分”,并在后端做了校验:如果返回内容不包含export default,就重新请求一次。另外,API的max_tokens设成4000,因为uni-app组件代码通常较长,设小了会被截断。
核心代码片段(简化版)
typescript
// 读取图片转base64
const imageBuffer = fs.readFileSync(imagePath);
const base64Image = imageBuffer.toString('base64');
// 调用Claude API
const response = await axios.post('https://api.anthropic.com/v1/messages', {
model: 'claude-3-5-sonnet-20241022',
max_tokens: 4000,
messages: [{
role: 'user',
content: [
{ type: 'text', text: systemPrompt },
{ type: 'image', source: { type: 'base64', media_type: mimeType, data: base64Image } }
]
}]
});
// 提取代码块并创建文件
const code = response.data.content[0].text;
const template = code.match(/[\s\S]*?/)[0];
const script = code.match(//)[0];
const style = code.match(//)[0];
const fullCode = ${template}\n${script}\n${style};
// 写入新文件(如 login.png → login.vue)
vscode.workspace.fs.writeFile(newUri, Buffer.from(fullCode));
vscode.window.showTextDocument(newUri);
API key没有硬编码,通过VSCode配置项让用户自行设置,也支持从环境变量读取。
实战体验:哪些场景真香,哪些翻了车
插件写完后,我拿真实项目里的截图测了一遍,结果有惊喜也有槽点。
真香场景:结构清晰的静态页面,比如设置页、个人中心、商品列表。生成的代码基本可以直接跑,样式还原度大概70%——间距、颜色、字体大小都挺准,布局结构从几十行flex缩减到微调几处。最省时间的是表单页,输入框、按钮、校验框架都给你搭好了,改改字段名就能用。
翻车现场:
- 复杂交互页面:带tab切换、下拉刷新、上拉加载的,Claude生成的代码只是样子像,逻辑基本不可用。比如tab切换它用了v-if硬切换,没有缓存也没有动画,需要自己重写。
- 第三方组件库:截图里用了uView或ThorUI的组件,Claude不认识,直接生成普通的view替代,失去了组件化的优势。这点目前无解,除非我在prompt里把组件库的API全写进去。
- 特殊字体和图标:截图里的图标,Claude会用硬写Unicode字符,而不是用字体图标组件,导致样式和交互都丢失。
我的使用建议:插件最适合生成静态页面或简单的表单/列表页。对于复杂交互的页面,先让它生成骨架,然后对照截图手动补充事件和API调用。另外,生成后一定要检查样式里的rpx值,Claude偶尔会抽风给出不合理的数值(比如字体4rpx,明显是错的)。
效果对比与下一步计划
从手动截图+写代码的30分钟,缩短到生成+微调的5分钟,这是最直观的收益。虽然不完美,但方向对了——把开发者从“像素级还原”中解放出来,专注于业务逻辑和交互设计。
接下来几个想加的功能:
- 支持选择生成风格:比如生成uView组件版本还是原生uni-app版本,通过prompt里的组件库约束实现。
- 增量更新:如果已经有一个.vue文件,只更新template部分,保留原有的script逻辑。这需要解析AST,比较复杂,但很实用。
- 本地模型支持:考虑接入Ollama运行小模型,避免每次都调用云端API,既省钱又保护隐私。不过本地模型的视觉能力目前还差一些,可能只适合简单的页面。
代码已经放在GitHub上了(链接就不放了,搜“uni-app截图转代码”应该能找到)。如果你也在做uni-app开发,并且经常被截图折磨,可以试试这个思路。记住,工具只是辅助,真正的价值在你的业务逻辑和交互设计上。
试了一圈AI编程助手,发现写uni-app时还是得自己动手调这几个坑
去年接了个uni-app外包,想着用AI编程助手冲一下效率,结果差点翻车翻到沟里。
Copilot、Cursor、通义灵码挨个试了一遍,它们写页面模板确实快——一个列表页、一个表单,几秒就给你整出来。但一碰到uni-app的跨端细节,AI就像个刚毕业的实习生:代码看起来能跑,一部署到小程序就崩给你看。下面这5个坑,是我亲手填过的,AI永远填不上。
- 条件编译:AI把它当普通if-else用
uni-app的#ifdef和#ifndef是跨端生存的关键,但AI写代码时,经常把平台逻辑混在一起,甚至直接写个if (process.env.PLATFORM === 'h5')——这玩意儿在微信小程序里根本跑不动。
真实翻车现场:我需要在小程序端调wx.getUserProfile,H5端用uni.getUserInfo。AI甩给我一段代码:
javascript
uni.getUserProfile({
desc: '用于完善资料',
success: (res) => console.log(res.userInfo)
});
在H5端直接报错——getUserProfile不存在。我手动拆成条件编译:
javascript
// #ifdef MP-WEIXIN
uni.getUserProfile({
desc: '用于完善资料',
success: (res) => this.userInfo = res.userInfo
});
// #endif
// #ifdef H5
uni.getUserInfo({
provider: 'weixin',
success: (infoRes) => this.userInfo = infoRes.userInfo
});
// #endif
血泪教训:AI经常把#ifndef H5写成#ifndef MP-WEIXIN,逻辑反了。每次写完条件编译,我都手动跑一遍所有目标端的模拟器,别信AI的“智能判断”。
- 自定义组件传值:小程序对props类型有洁癖
uni-app支持Vue的props,但在小程序端,props不能传函数类型。AI不懂这个,生成了这样的代码:
vue
// 子组件
点击
props: {
onSuccess: Function
}
微信开发者工具直接报警告:“props不能接受Function类型”。我改成$emit事件机制才搞定:
vue
// 父组件
// 子组件
点击
export default {
emits: ['success'],
methods: {
emitSuccess() {
this.$emit('success', { data: '...' });
}
}
}
坑点:AI生成的组件代码里,emits选项经常漏掉,事件传递直接失效。手动补上emits: ['success'],否则在微信开发者工具里会一直报“未注册事件”。
- uni.request封装:AI给的拦截器有毒
AI推荐用axios封装请求,但uni-app在小程序端不支持axios(没有XMLHttpRequest)。AI给的代码是:
javascript
import axios from 'axios';
const instance = axios.create({ baseURL: '...' });
H5端能跑,小程序端直接报错。我换成uni.request手动封装:
javascript
// request.js
const baseURL = 'https://api.example.com';
function request(options) {
const token = uni.getStorageSync('token');
const header = {
'Content-Type': 'application/json',
...(token ? { 'Authorization': Bearer ${token} } : {})
};
return new Promise((resolve, reject) => {
uni.request({
url: baseURL options.url,
method: options.method || 'GET',
data: options.data,
header: { ...header, ...options.header },
success: (res) => {
if (res.statusCode === 200) resolve(res.data);
else if (res.statusCode === 401) {
uni.navigateTo({ url: '/pages/login/login' });
reject(new Error('Unauthorized'));
} else reject(new Error('Request failed'));
},
fail: (err) => {
uni.showToast({ title: '网络异常', icon: 'none' });
reject(err);
}
});
});
}
export default request;
隐藏坑:AI生成的代码经常忘记处理401,或者把uni.navigateTo写成uni.redirectTo,导致登录后无法返回。另外,uni.request的header在小程序端有长度限制(大概4KB),AI不会告诉你——我遇到过token太长直接请求失败的情况。
- 页面生命周期混用:onLoad和mounted分不清
uni-app的onLoad和Vue的mounted执行顺序不同,AI经常把需要页面参数的处理逻辑塞进mounted里。
真实翻车:从列表页跳转详情页,需要获取options.id。AI写了:
javascript
mounted() {
const id = this.$route.query.id; // H5端能用,小程序端this.$route不存在
}
小程序端直接报错。我统一改为onLoad:
javascript
export default {
onLoad(options) {
this.id = options.id;
this.fetchDetail();
},
mounted() {
// 只做DOM初始化,比如图表渲染
// 不依赖this.id
}
}
坑点:AI经常在onLoad和mounted里都调用fetchDetail,导致接口请求两次。我习惯只保留onLoad里的请求,mounted只做DOM操作。
- 样式兼容:rpx和px混用成灾难
uni-app推荐用rpx做响应式单位,但AI生成的样式里px、rpx、vw混用,在不同屏幕尺寸下布局直接崩。
我的手动规则:
- 字体大小:font-size: 28rpx(用rpx)
- 固定宽高(按钮、图标):width: 100rpx(用rpx)
- 边框、阴影:border: 1px solid #ccc(用px)
- 百分比布局:width: 100%或flex
AI翻车案例:一个卡片组件,AI写了padding: 20px,在iPhone 12上正常,iPhone SE上右边距过大。改成padding: 30rpx后适配了所有屏幕。
坑点:AI有时生成calc(100vw - 40rpx)这种混合表达式,小程序端编译报错。统一用rpx,需要百分比宽度时用width: 100%配合padding。
最后说点实在的
别把AI当救世主。它写模板、列表页、基础逻辑确实快,但uni-app的跨端细节、条件编译、原生API调用、生命周期管理,这些坑它一个都填不上。
我的工作流:
- 只把AI当草稿生成器:用它写页面结构和基础逻辑,然后手动检查跨端兼容性。
- 建自己的代码片段库:把条件编译、请求封装、组件传值这些高频模式整理成模板,比让AI每次重新生成靠谱得多。
- 多在小程序端测试:AI生成的代码在H5端往往能跑,小程序端才是真正的“照妖镜”——我至少有一半的bug是在微信开发者工具里发现的。
最后,别迷信AI的“智能”。至少在uni-app这个领域,人类的“手动调”仍然是不可替代的——因为那些坑,AI永远填不上。
去年接了个uni-app外包,想着用AI编程助手冲一下效率,结果差点翻车翻到沟里。
Copilot、Cursor、通义灵码挨个试了一遍,它们写页面模板确实快——一个列表页、一个表单,几秒就给你整出来。但一碰到uni-app的跨端细节,AI就像个刚毕业的实习生:代码看起来能跑,一部署到小程序就崩给你看。下面这5个坑,是我亲手填过的,AI永远填不上。
- 条件编译:AI把它当普通if-else用
uni-app的#ifdef和#ifndef是跨端生存的关键,但AI写代码时,经常把平台逻辑混在一起,甚至直接写个if (process.env.PLATFORM === 'h5')——这玩意儿在微信小程序里根本跑不动。
真实翻车现场:我需要在小程序端调wx.getUserProfile,H5端用uni.getUserInfo。AI甩给我一段代码:
javascript
uni.getUserProfile({
desc: '用于完善资料',
success: (res) => console.log(res.userInfo)
});
在H5端直接报错——getUserProfile不存在。我手动拆成条件编译:
javascript
// #ifdef MP-WEIXIN
uni.getUserProfile({
desc: '用于完善资料',
success: (res) => this.userInfo = res.userInfo
});
// #endif
// #ifdef H5
uni.getUserInfo({
provider: 'weixin',
success: (infoRes) => this.userInfo = infoRes.userInfo
});
// #endif
血泪教训:AI经常把#ifndef H5写成#ifndef MP-WEIXIN,逻辑反了。每次写完条件编译,我都手动跑一遍所有目标端的模拟器,别信AI的“智能判断”。
- 自定义组件传值:小程序对props类型有洁癖
uni-app支持Vue的props,但在小程序端,props不能传函数类型。AI不懂这个,生成了这样的代码:
vue
// 子组件
点击
props: {
onSuccess: Function
}
微信开发者工具直接报警告:“props不能接受Function类型”。我改成$emit事件机制才搞定:
vue
// 父组件
// 子组件
点击
export default {
emits: ['success'],
methods: {
emitSuccess() {
this.$emit('success', { data: '...' });
}
}
}
坑点:AI生成的组件代码里,emits选项经常漏掉,事件传递直接失效。手动补上emits: ['success'],否则在微信开发者工具里会一直报“未注册事件”。
- uni.request封装:AI给的拦截器有毒
AI推荐用axios封装请求,但uni-app在小程序端不支持axios(没有XMLHttpRequest)。AI给的代码是:
javascript
import axios from 'axios';
const instance = axios.create({ baseURL: '...' });
H5端能跑,小程序端直接报错。我换成uni.request手动封装:
javascript
// request.js
const baseURL = 'https://api.example.com';
function request(options) {
const token = uni.getStorageSync('token');
const header = {
'Content-Type': 'application/json',
...(token ? { 'Authorization': Bearer ${token} } : {})
};
return new Promise((resolve, reject) => {
uni.request({
url: baseURL options.url,
method: options.method || 'GET',
data: options.data,
header: { ...header, ...options.header },
success: (res) => {
if (res.statusCode === 200) resolve(res.data);
else if (res.statusCode === 401) {
uni.navigateTo({ url: '/pages/login/login' });
reject(new Error('Unauthorized'));
} else reject(new Error('Request failed'));
},
fail: (err) => {
uni.showToast({ title: '网络异常', icon: 'none' });
reject(err);
}
});
});
}
export default request;
隐藏坑:AI生成的代码经常忘记处理401,或者把uni.navigateTo写成uni.redirectTo,导致登录后无法返回。另外,uni.request的header在小程序端有长度限制(大概4KB),AI不会告诉你——我遇到过token太长直接请求失败的情况。
- 页面生命周期混用:onLoad和mounted分不清
uni-app的onLoad和Vue的mounted执行顺序不同,AI经常把需要页面参数的处理逻辑塞进mounted里。
真实翻车:从列表页跳转详情页,需要获取options.id。AI写了:
javascript
mounted() {
const id = this.$route.query.id; // H5端能用,小程序端this.$route不存在
}
小程序端直接报错。我统一改为onLoad:
javascript
export default {
onLoad(options) {
this.id = options.id;
this.fetchDetail();
},
mounted() {
// 只做DOM初始化,比如图表渲染
// 不依赖this.id
}
}
坑点:AI经常在onLoad和mounted里都调用fetchDetail,导致接口请求两次。我习惯只保留onLoad里的请求,mounted只做DOM操作。
- 样式兼容:rpx和px混用成灾难
uni-app推荐用rpx做响应式单位,但AI生成的样式里px、rpx、vw混用,在不同屏幕尺寸下布局直接崩。
我的手动规则:
- 字体大小:font-size: 28rpx(用rpx)
- 固定宽高(按钮、图标):width: 100rpx(用rpx)
- 边框、阴影:border: 1px solid #ccc(用px)
- 百分比布局:width: 100%或flex
AI翻车案例:一个卡片组件,AI写了padding: 20px,在iPhone 12上正常,iPhone SE上右边距过大。改成padding: 30rpx后适配了所有屏幕。
坑点:AI有时生成calc(100vw - 40rpx)这种混合表达式,小程序端编译报错。统一用rpx,需要百分比宽度时用width: 100%配合padding。
最后说点实在的
别把AI当救世主。它写模板、列表页、基础逻辑确实快,但uni-app的跨端细节、条件编译、原生API调用、生命周期管理,这些坑它一个都填不上。
我的工作流:
- 只把AI当草稿生成器:用它写页面结构和基础逻辑,然后手动检查跨端兼容性。
- 建自己的代码片段库:把条件编译、请求封装、组件传值这些高频模式整理成模板,比让AI每次重新生成靠谱得多。
- 多在小程序端测试:AI生成的代码在H5端往往能跑,小程序端才是真正的“照妖镜”——我至少有一半的bug是在微信开发者工具里发现的。
最后,别迷信AI的“智能”。至少在uni-app这个领域,人类的“手动调”仍然是不可替代的——因为那些坑,AI永远填不上。
收起阅读 »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/





