HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

uniCloud

uniCloud

背景和优势
20年前,restful接口开发开始流行,服务器编写接口,客户端调用接口,传输json。

现在,替代restful的新模式来了。

云对象,服务器编写API,客户端调用API,不再开发传输json的接口。思路更清晰、代码更精简。

比如服务端编写一个云对象todo,该对象有add、get、remove、update等方法。客户端的js则可以直接import这个todo云对象,直接调用add等方法。

服务器示例代码如下:

HBuilderX中在uniCloud/cloudfunctions目录新建云函数,选择类型为云对象,起名为todo。打开云对象入口index.obj.js,添加一个add方法。

// 云对象名:todo
module.exports = {
add(title, content) {
title = title.trim()
content = content.trim()
if(!title || !content) {
return {
errCode: 'INVALID_TODO',
errMsg: 'TODO标题或内容不可为空'
}
}
// ...其他逻辑
return {
errCode: 0,
errMsg: '创建成功'
}
}
}
复制代码
然后在客户端的js中,import这个todo对象,调用它的add方法

const todo = uniCloud.importObject('todo') //第一步导入云对象
async function addTodo () {
try {
const res = await todo.add('title demo', 'content demo') //导入云对象后就可以直接调用该对象的方法了,注意使用异步await
uni.showToast({
title: '创建成功'
})
} catch (e) {
// 符合uniCloud响应体规范 https://doc.dcloud.net.cn/uniCloud/cf-functions?id=resformat,自动抛出此错误
uni.showModal({
title: '创建失败',
content: e.errMsg,
showCancel: false
})
}
}
复制代码
可以看到云对象的代码非常清晰,代码行数也只有33行。

而同样的逻辑,使用传统的接口方式则需要更多代码,见下:

// 传统方式调用云函数-云函数代码
// 云函数名:todo
// 云函数入口index.js内容如下
'use strict';
exports.main = async (event, context) => {
const {
method,
params
} = event
switch(method) {
case 'add': {
let {
title,
content
} = params
title = title.trim()
content = content.trim()
if(!title || !content) {
return {
errCode: 'INVALID_TODO',
errMsg: 'TODO标题或内容不可为空'
}
}
// ...省略其他逻辑
return {
errCode: 0,
errMsg: '创建成功'
}
}
}
return {
errCode: 'METHOD_NOT_FOUND',
errMsg: Method[${method}] not found
}
};

// 传统方式调用云函数-客户端代码
async function addToDo () {
try {
const res = await uniCloud.callFunction({
name: 'todo',
data: {
method: 'add',
params: {
title: 'title demo',
content: 'content demo'
}
}
})
const {
errCode,
errMsg
} = res.result
if(errCode) {
uni.showModal({
title: '创建失败',
content: errMsg,
showCancel: false
})
return
}
uni.showToast({
title: '创建成功'
})
} catch (e) {
uni.showModal({
title: '创建失败',
content: e.message,
showCancel: false
})
}
}
复制代码
以上传统开发需要68行代码,对比云对象的33行代码,不但工作量大,而且逻辑也不如云对象清晰。

_注:以上例子仅用于方便初学者理解。实际开发中对于简单的数据库操作,使用clientDB在前端直接操作数据库是更简单、代码更少的方案,都不需要写云端代码。

总结下云对象带来的好处:

更清晰的逻辑
更精简的代码
更少的协作成本(以及矛盾~)
客户端调用时在ide里有完善的代码提示,方法参数均可提示。(传输json可没法在ide里提示)
默认支持uniCloud响应体规范,方便错误拦截和统一处理
快速上手
创建云对象
云对象,其实是对云函数的封装。和创建云函数一样,在uniCloud/cloudfunctions目录右键新建云函数,选择云对象类型,输入云对象名称创建云对象,此处以云对象todo为例,创建的云对象包含一个index.obj.js。

创建云对象

一个空的云对象内容如下

// cloudfunctions/todo/index.obj.js
module.exports = {

}
复制代码
默认云对象模板是不包含任何方法的,我们为此对象添加一个add方法作为示例。

// cloudfunctions/todo/index.obj.js
module.exports = {
add: function(title = '', content = '') {
title = title.trim()
content = content.trim()
if(!title || !content) {
return {
errCode: 'INVALID_TODO',
errMsg: 'TODO标题或内容不可为空'
}
}
// ...其他逻辑,如操作todo数据表添加数据
return {
errCode: 0,
errMsg: '创建成功'
}
}
}
复制代码
至此云对象todo已经有了一个可以访问的方法了。接下来看如何使用客户端调用此云对象内的方法

客户端调用
客户端通过uniCloud.importObject方法获取云对象的实例,并可以通过此实例调用云对象内的方法。用法如下

const todo = uniCloud.importObject('todo')
const res = await todo.add('title demo', 'content demo')
复制代码
通过代码块cco可以快捷的输入以下代码:

const todo = uniCloud.importObject('todo')
复制代码
实际业务中需要考虑错误捕获,调用方式有两种:

try catch
const todo = uniCloud.importObject('todo')
try {
const res = await todo.add('title demo', 'content demo') //导入云对象后就可以直接调用该对象的方法了,注意使用异步await
console.log(res)
} catch (e) {
console.log(e.errCode)
console.log(e.errMsg)
}
复制代码
then catch(promise写法)
const todo = uniCloud.importObject('todo')
todo.add('title demo', 'content demo').then(res => {
console.log(res)
}).catch(e => {
console.log(e.errCode)
console.log(e.errMsg)
})
复制代码
importObject参数说明

interface ImportObjectOptions {
/**

  • 是否移除自动展示的ui
    */
    customUI?: boolean;
    /**
  • loading界面配置
    */
    loadingOptions?: ImportObjectLoadingOptions;
    /**
  • 错误提示配置
    */
    errorOptions?: ImportObjectErrorOptions;
    /**
  • 使用安全网络的方法及安全网络类型
    */
    secretMethods?: Record<string, 'none' | 'request' | 'response' | 'both'>;
    /**
  • 转化云对象内未捕获的错误或客户端网络错误
    */
    parseSystemError?: (params: ParseSystemErrorParams) => Promise<ParsedSystemError> | ParsedSystemError;
    }
    复制代码
    云对象的API
    云对象作为云函数的一种,可以调用所有node的API和uniCloud的API。

uniCloud有众多API,另见:uniCloud的API清单

除上述API之外,云对象的this对象还有一批专用方法来获取当前请求的上下文信息。

与云函数入参时的context不同,云对象没有context。它通过this对象挂载的几个内置方法来获取上下文信息。请注意开发者避免在this上挂载同名方法。

获取客户端信息
HBuilderX 3.4.9起此接口可获取所有客户端getSystemInfo返回的客户端信息,完整字段列表参考:getSystemInfo

接口形式

this.getClientInfo()

示例:

module.exports = {
add: function() {
const clientInfo = this.getClientInfo()
// clientInfo = {
// clientIP,
// appId,
// deviceId,
// source,
// //... 其他getSystemInfoSync返回值
// }
}
}
复制代码
返回值

getClientInfo返回的信息,是在客户端的uni.getSystemInfo的基础之上,增加了一些额外的信息。

除了getSystemInfo返回字段外,还包含以下信息

属性名 类型 说明
clientIP string 客户端ip
userAgent string 客户端ua,注意非本地运行环境下客户端getSystemInfoSync也会获取ua参数并上传给云对象,但是云对象会从http请求头里面获取ua而不是clientInfo里面的ua
source string 调用来源,返回值见下。HBuilderX 3.5.1+
requestId string 请求id。HBuilderX 3.5.1+
scene string 场景值。客户端uni.getLaunchOptionsSync返回的scene参数,新增于HBuilderX 3.5.1
getClientInfo().source,返回云函数调用来源,它的值域为:

取值 说明
client uni-app客户端导入云对象调用
function 由其他云函数或云对象调用
http 云对象URL化后通过http访问调用 HBuilderX 3.5.2+
timing 定时任务调用云对象 HBuilderX 3.5.2+
server 云函数上传并运行
注意事项

客户端上报的信息在理论上存在被篡改可能,实际业务中应验证前端传来的数据的合法性
除了clientIP外,其他客户端信息只有使用uni-app客户端以云对象的方式调用才能获取
云对象与云函数内获取客户端platform稍有不同,云函数未拉齐vue2、vue3版本app平台的platform值,vue2为app-plus,vue3为app。云对象无论客户端是vue2还是vue3,在app平台获取的platform均为app。这一点在使用uni-id时需要特别注意,详情见:uni-id文档 preferedAppPlatform
获取云端信息
接口形式

this.getCloudInfo()

示例

继续阅读 »

背景和优势
20年前,restful接口开发开始流行,服务器编写接口,客户端调用接口,传输json。

现在,替代restful的新模式来了。

云对象,服务器编写API,客户端调用API,不再开发传输json的接口。思路更清晰、代码更精简。

比如服务端编写一个云对象todo,该对象有add、get、remove、update等方法。客户端的js则可以直接import这个todo云对象,直接调用add等方法。

服务器示例代码如下:

HBuilderX中在uniCloud/cloudfunctions目录新建云函数,选择类型为云对象,起名为todo。打开云对象入口index.obj.js,添加一个add方法。

// 云对象名:todo
module.exports = {
add(title, content) {
title = title.trim()
content = content.trim()
if(!title || !content) {
return {
errCode: 'INVALID_TODO',
errMsg: 'TODO标题或内容不可为空'
}
}
// ...其他逻辑
return {
errCode: 0,
errMsg: '创建成功'
}
}
}
复制代码
然后在客户端的js中,import这个todo对象,调用它的add方法

const todo = uniCloud.importObject('todo') //第一步导入云对象
async function addTodo () {
try {
const res = await todo.add('title demo', 'content demo') //导入云对象后就可以直接调用该对象的方法了,注意使用异步await
uni.showToast({
title: '创建成功'
})
} catch (e) {
// 符合uniCloud响应体规范 https://doc.dcloud.net.cn/uniCloud/cf-functions?id=resformat,自动抛出此错误
uni.showModal({
title: '创建失败',
content: e.errMsg,
showCancel: false
})
}
}
复制代码
可以看到云对象的代码非常清晰,代码行数也只有33行。

而同样的逻辑,使用传统的接口方式则需要更多代码,见下:

// 传统方式调用云函数-云函数代码
// 云函数名:todo
// 云函数入口index.js内容如下
'use strict';
exports.main = async (event, context) => {
const {
method,
params
} = event
switch(method) {
case 'add': {
let {
title,
content
} = params
title = title.trim()
content = content.trim()
if(!title || !content) {
return {
errCode: 'INVALID_TODO',
errMsg: 'TODO标题或内容不可为空'
}
}
// ...省略其他逻辑
return {
errCode: 0,
errMsg: '创建成功'
}
}
}
return {
errCode: 'METHOD_NOT_FOUND',
errMsg: Method[${method}] not found
}
};

// 传统方式调用云函数-客户端代码
async function addToDo () {
try {
const res = await uniCloud.callFunction({
name: 'todo',
data: {
method: 'add',
params: {
title: 'title demo',
content: 'content demo'
}
}
})
const {
errCode,
errMsg
} = res.result
if(errCode) {
uni.showModal({
title: '创建失败',
content: errMsg,
showCancel: false
})
return
}
uni.showToast({
title: '创建成功'
})
} catch (e) {
uni.showModal({
title: '创建失败',
content: e.message,
showCancel: false
})
}
}
复制代码
以上传统开发需要68行代码,对比云对象的33行代码,不但工作量大,而且逻辑也不如云对象清晰。

_注:以上例子仅用于方便初学者理解。实际开发中对于简单的数据库操作,使用clientDB在前端直接操作数据库是更简单、代码更少的方案,都不需要写云端代码。

总结下云对象带来的好处:

更清晰的逻辑
更精简的代码
更少的协作成本(以及矛盾~)
客户端调用时在ide里有完善的代码提示,方法参数均可提示。(传输json可没法在ide里提示)
默认支持uniCloud响应体规范,方便错误拦截和统一处理
快速上手
创建云对象
云对象,其实是对云函数的封装。和创建云函数一样,在uniCloud/cloudfunctions目录右键新建云函数,选择云对象类型,输入云对象名称创建云对象,此处以云对象todo为例,创建的云对象包含一个index.obj.js。

创建云对象

一个空的云对象内容如下

// cloudfunctions/todo/index.obj.js
module.exports = {

}
复制代码
默认云对象模板是不包含任何方法的,我们为此对象添加一个add方法作为示例。

// cloudfunctions/todo/index.obj.js
module.exports = {
add: function(title = '', content = '') {
title = title.trim()
content = content.trim()
if(!title || !content) {
return {
errCode: 'INVALID_TODO',
errMsg: 'TODO标题或内容不可为空'
}
}
// ...其他逻辑,如操作todo数据表添加数据
return {
errCode: 0,
errMsg: '创建成功'
}
}
}
复制代码
至此云对象todo已经有了一个可以访问的方法了。接下来看如何使用客户端调用此云对象内的方法

客户端调用
客户端通过uniCloud.importObject方法获取云对象的实例,并可以通过此实例调用云对象内的方法。用法如下

const todo = uniCloud.importObject('todo')
const res = await todo.add('title demo', 'content demo')
复制代码
通过代码块cco可以快捷的输入以下代码:

const todo = uniCloud.importObject('todo')
复制代码
实际业务中需要考虑错误捕获,调用方式有两种:

try catch
const todo = uniCloud.importObject('todo')
try {
const res = await todo.add('title demo', 'content demo') //导入云对象后就可以直接调用该对象的方法了,注意使用异步await
console.log(res)
} catch (e) {
console.log(e.errCode)
console.log(e.errMsg)
}
复制代码
then catch(promise写法)
const todo = uniCloud.importObject('todo')
todo.add('title demo', 'content demo').then(res => {
console.log(res)
}).catch(e => {
console.log(e.errCode)
console.log(e.errMsg)
})
复制代码
importObject参数说明

interface ImportObjectOptions {
/**

  • 是否移除自动展示的ui
    */
    customUI?: boolean;
    /**
  • loading界面配置
    */
    loadingOptions?: ImportObjectLoadingOptions;
    /**
  • 错误提示配置
    */
    errorOptions?: ImportObjectErrorOptions;
    /**
  • 使用安全网络的方法及安全网络类型
    */
    secretMethods?: Record<string, 'none' | 'request' | 'response' | 'both'>;
    /**
  • 转化云对象内未捕获的错误或客户端网络错误
    */
    parseSystemError?: (params: ParseSystemErrorParams) => Promise<ParsedSystemError> | ParsedSystemError;
    }
    复制代码
    云对象的API
    云对象作为云函数的一种,可以调用所有node的API和uniCloud的API。

uniCloud有众多API,另见:uniCloud的API清单

除上述API之外,云对象的this对象还有一批专用方法来获取当前请求的上下文信息。

与云函数入参时的context不同,云对象没有context。它通过this对象挂载的几个内置方法来获取上下文信息。请注意开发者避免在this上挂载同名方法。

获取客户端信息
HBuilderX 3.4.9起此接口可获取所有客户端getSystemInfo返回的客户端信息,完整字段列表参考:getSystemInfo

接口形式

this.getClientInfo()

示例:

module.exports = {
add: function() {
const clientInfo = this.getClientInfo()
// clientInfo = {
// clientIP,
// appId,
// deviceId,
// source,
// //... 其他getSystemInfoSync返回值
// }
}
}
复制代码
返回值

getClientInfo返回的信息,是在客户端的uni.getSystemInfo的基础之上,增加了一些额外的信息。

除了getSystemInfo返回字段外,还包含以下信息

属性名 类型 说明
clientIP string 客户端ip
userAgent string 客户端ua,注意非本地运行环境下客户端getSystemInfoSync也会获取ua参数并上传给云对象,但是云对象会从http请求头里面获取ua而不是clientInfo里面的ua
source string 调用来源,返回值见下。HBuilderX 3.5.1+
requestId string 请求id。HBuilderX 3.5.1+
scene string 场景值。客户端uni.getLaunchOptionsSync返回的scene参数,新增于HBuilderX 3.5.1
getClientInfo().source,返回云函数调用来源,它的值域为:

取值 说明
client uni-app客户端导入云对象调用
function 由其他云函数或云对象调用
http 云对象URL化后通过http访问调用 HBuilderX 3.5.2+
timing 定时任务调用云对象 HBuilderX 3.5.2+
server 云函数上传并运行
注意事项

客户端上报的信息在理论上存在被篡改可能,实际业务中应验证前端传来的数据的合法性
除了clientIP外,其他客户端信息只有使用uni-app客户端以云对象的方式调用才能获取
云对象与云函数内获取客户端platform稍有不同,云函数未拉齐vue2、vue3版本app平台的platform值,vue2为app-plus,vue3为app。云对象无论客户端是vue2还是vue3,在app平台获取的platform均为app。这一点在使用uni-id时需要特别注意,详情见:uni-id文档 preferedAppPlatform
获取云端信息
接口形式

this.getCloudInfo()

示例

收起阅读 »

2

2

2

从 3 天开发到收到苹果 4.3:一个 AI 项目的上架翻车记录

苹果审核

从 3 天开发到收到苹果 4.3:一个 AI 项目的上架翻车记录

最近接到一个客户的项目。

项目很简单。

客户使用 Cursor 配合 ChatGPT 开发,前后只用了不到一周时间,一个 AI 工具类 APP 就已经完成。

页面漂亮,功能正常,测试也没有发现明显问题。

客户原本以为提交审核后几天就能上线。

结果收到的第一条消息却是:

Guideline 4.3 - Design - Spam

很多开发者看到这里都会有一个疑问:

同样是 AI 项目,为什么别人能过,而自己却被判定为重复内容?

实际上问题往往不是出在 AI,而是出在开发过程中那些容易被忽略的细节。

这类项目通常长什么样?

我们拆包分析后发现,很多 AI 生成项目都有一个共同特点。

首页是聊天界面。

第二页是模型列表。

第三页是会员中心。

第四页是个人中心。

换句话说,虽然 APP 名称不同,但整体结构几乎一致。

今天叫 AI 助手。

明天叫 AI 创作。

后天叫 AI 写作。

从用户角度看可能是不同产品。

但从苹果审核角度看,它们更像是同一个产品的多个版本。

这也是很多开发者第一次收到 4.3 的主要原因。

苹果真的会检测代码吗?

很多人认为苹果审核员只是打开 APP 看几分钟。

实际上远不止如此。

一个 IPA 上传后,苹果首先进行自动化分析。

检测内容通常包括:

  • Bundle 信息
  • 资源文件
  • Framework 依赖
  • 页面结构
  • 类名与方法名
  • 权限申请情况
  • 历史提交记录

特别是 AI 生成代码。

由于大量开发者使用相同提示词生成项目,因此经常会出现类似情况:

HomePage
ChatPage
MinePage
VipPage

甚至整个目录结构都高度一致。

虽然用户看不到这些内容,但审核系统是可以分析到的。

UI 改了为什么还是收到 4.3?

这是很多人踩过的坑。

收到 4.3 后第一反应通常是:

换图标。

换配色。

换截图。

换启动页。

然后重新提交。

结果还是被拒。

原因很简单。

苹果审核从来不是只看 UI。

很多项目表面已经改头换面。

但实际上:

功能没变。

代码没变。

业务逻辑没变。

用户路径没变。

审核员看到后仍然会认为属于重复内容。

一个真实案例

去年有个 AI 写作项目。

第一次提交时:

功能包括:

  • AI 对话
  • AI 写作
  • AI 翻译

结果收到 4.3。

后来我们重新梳理产品定位。

保留 AI 能力的同时增加:

  • 写作素材库
  • 行业模板中心
  • 历史创作管理
  • 文案分类系统
  • 每日创作任务

同时重新调整页面结构。

将聊天入口从首页移动到二级页面。

增加内容运营模块。

重新设计会员体系。

第二次提交后顺利通过审核。

整个过程中代码修改量并不算大。

真正变化的是产品定位。

AI 项目最容易忽略的问题

很多开发者把重点放在代码上。

实际上苹果更关注的是产品本身。

如果审核员打开 APP 后发现:

第一页聊天。

第二页会员。

第三页个人中心。

无论你用的是 AI 写的代码,还是程序员手写的代码,最终都会面临同样的问题。

因为苹果审核的是产品,而不是开发工具。

我的建议

如果准备使用 AI 开发 APP 上架 App Store,可以把 AI 当成开发助手,而不是产品经理。

代码可以让 AI 写。

页面可以让 AI 画。

但是产品定位一定要自己设计。

在提交审核之前,至少要回答下面三个问题:

第一,这个 APP 解决什么问题?

第二,它和市场上已有产品有什么区别?

第三,用户为什么要下载它而不是别人?

如果这三个问题答不出来。

即使没有收到 4.3,也只是时间问题。

写在最后

很多人觉得 AI 降低了开发门槛。

这一点没错。

但同时也降低了同质化产品出现的门槛。

过去做一个 APP 需要一个团队。

现在一个人加一套 AI 工具就能完成。

这也是为什么近两年苹果对于 4.3 的审核越来越严格。

被拒的原因从来不是因为用了 AI。

而是因为太多人用 AI 做出了相似的产品。

真正能通过审核的项目,往往不是代码写得最复杂的,而是定位最清晰、差异化最明显的。

继续阅读 »

从 3 天开发到收到苹果 4.3:一个 AI 项目的上架翻车记录

最近接到一个客户的项目。

项目很简单。

客户使用 Cursor 配合 ChatGPT 开发,前后只用了不到一周时间,一个 AI 工具类 APP 就已经完成。

页面漂亮,功能正常,测试也没有发现明显问题。

客户原本以为提交审核后几天就能上线。

结果收到的第一条消息却是:

Guideline 4.3 - Design - Spam

很多开发者看到这里都会有一个疑问:

同样是 AI 项目,为什么别人能过,而自己却被判定为重复内容?

实际上问题往往不是出在 AI,而是出在开发过程中那些容易被忽略的细节。

这类项目通常长什么样?

我们拆包分析后发现,很多 AI 生成项目都有一个共同特点。

首页是聊天界面。

第二页是模型列表。

第三页是会员中心。

第四页是个人中心。

换句话说,虽然 APP 名称不同,但整体结构几乎一致。

今天叫 AI 助手。

明天叫 AI 创作。

后天叫 AI 写作。

从用户角度看可能是不同产品。

但从苹果审核角度看,它们更像是同一个产品的多个版本。

这也是很多开发者第一次收到 4.3 的主要原因。

苹果真的会检测代码吗?

很多人认为苹果审核员只是打开 APP 看几分钟。

实际上远不止如此。

一个 IPA 上传后,苹果首先进行自动化分析。

检测内容通常包括:

  • Bundle 信息
  • 资源文件
  • Framework 依赖
  • 页面结构
  • 类名与方法名
  • 权限申请情况
  • 历史提交记录

特别是 AI 生成代码。

由于大量开发者使用相同提示词生成项目,因此经常会出现类似情况:

HomePage
ChatPage
MinePage
VipPage

甚至整个目录结构都高度一致。

虽然用户看不到这些内容,但审核系统是可以分析到的。

UI 改了为什么还是收到 4.3?

这是很多人踩过的坑。

收到 4.3 后第一反应通常是:

换图标。

换配色。

换截图。

换启动页。

然后重新提交。

结果还是被拒。

原因很简单。

苹果审核从来不是只看 UI。

很多项目表面已经改头换面。

但实际上:

功能没变。

代码没变。

业务逻辑没变。

用户路径没变。

审核员看到后仍然会认为属于重复内容。

一个真实案例

去年有个 AI 写作项目。

第一次提交时:

功能包括:

  • AI 对话
  • AI 写作
  • AI 翻译

结果收到 4.3。

后来我们重新梳理产品定位。

保留 AI 能力的同时增加:

  • 写作素材库
  • 行业模板中心
  • 历史创作管理
  • 文案分类系统
  • 每日创作任务

同时重新调整页面结构。

将聊天入口从首页移动到二级页面。

增加内容运营模块。

重新设计会员体系。

第二次提交后顺利通过审核。

整个过程中代码修改量并不算大。

真正变化的是产品定位。

AI 项目最容易忽略的问题

很多开发者把重点放在代码上。

实际上苹果更关注的是产品本身。

如果审核员打开 APP 后发现:

第一页聊天。

第二页会员。

第三页个人中心。

无论你用的是 AI 写的代码,还是程序员手写的代码,最终都会面临同样的问题。

因为苹果审核的是产品,而不是开发工具。

我的建议

如果准备使用 AI 开发 APP 上架 App Store,可以把 AI 当成开发助手,而不是产品经理。

代码可以让 AI 写。

页面可以让 AI 画。

但是产品定位一定要自己设计。

在提交审核之前,至少要回答下面三个问题:

第一,这个 APP 解决什么问题?

第二,它和市场上已有产品有什么区别?

第三,用户为什么要下载它而不是别人?

如果这三个问题答不出来。

即使没有收到 4.3,也只是时间问题。

写在最后

很多人觉得 AI 降低了开发门槛。

这一点没错。

但同时也降低了同质化产品出现的门槛。

过去做一个 APP 需要一个团队。

现在一个人加一套 AI 工具就能完成。

这也是为什么近两年苹果对于 4.3 的审核越来越严格。

被拒的原因从来不是因为用了 AI。

而是因为太多人用 AI 做出了相似的产品。

真正能通过审核的项目,往往不是代码写得最复杂的,而是定位最清晰、差异化最明显的。

收起阅读 »

1

1

1

图片URL格式化工具

对图片相对路径自动拼接CDN域名,富文本图片批量处理 HTML实体解码

详细描述:## 功能

  1. formatImage(url) — 智能补全图片地址

    • 完整 http/https 地址原样返回
    • 协议相对地址 //xxx.com 转为 https://xxx.com
    • 相对路径自动拼接 CDN 域名
  2. formatRichText(html) — 富文本处理

    • 批量替换图片 src 为完整 CDN 地址
    • 自动添加图片自适应样式(max-width:100%)
    • 解码 HTML 实体(& < > 等)

适用场景

  • 商品详情、文章内容的富文本展示
  • 用户头像、商品图片等资源路径补全
  • 后端返回的图片相对路径转完整 URL

使用示例


import { setImageOrigin, formatImage } from '@/utils/image'  

// 1. 在 App.vue 中配置 CDN 域名  
setImageOrigin('https://cdn.example.com')  

// 2. 格式化图片  
formatImage('/uploads/photo.jpg')  
// → https://cdn.example.com/uploads/photo.jpg  

// 3. 处理富文本  
formatRichText('<img src="/uploads/1.jpg" />')  

兼容性  
支持 uni-app 全平台(H5、iOS App、Android App、各小程序)  
继续阅读 »

对图片相对路径自动拼接CDN域名,富文本图片批量处理 HTML实体解码

详细描述:## 功能

  1. formatImage(url) — 智能补全图片地址

    • 完整 http/https 地址原样返回
    • 协议相对地址 //xxx.com 转为 https://xxx.com
    • 相对路径自动拼接 CDN 域名
  2. formatRichText(html) — 富文本处理

    • 批量替换图片 src 为完整 CDN 地址
    • 自动添加图片自适应样式(max-width:100%)
    • 解码 HTML 实体(& < > 等)

适用场景

  • 商品详情、文章内容的富文本展示
  • 用户头像、商品图片等资源路径补全
  • 后端返回的图片相对路径转完整 URL

使用示例


import { setImageOrigin, formatImage } from '@/utils/image'  

// 1. 在 App.vue 中配置 CDN 域名  
setImageOrigin('https://cdn.example.com')  

// 2. 格式化图片  
formatImage('/uploads/photo.jpg')  
// → https://cdn.example.com/uploads/photo.jpg  

// 3. 处理富文本  
formatRichText('<img src="/uploads/1.jpg" />')  

兼容性  
支持 uni-app 全平台(H5、iOS App、Android App、各小程序)  
收起阅读 »

4人团队全职接单,从广州回南宁创业,10年以上经验,时间充裕,可上门考察,直接技术一对一,求靠谱单子!

外包接单

【南宁本地·全栈开发团队】10年老兵返乡创业,4人全职,无销售套路,直接技术一对一!

✅ 我们能做: 小程序 · APP · 网站 · 公众号 · 后台管理系统 · 定制化企业软件
✅ 我们的底气: 前广州一线全栈,人均10年+开发经验,全部技术骨干出身
✅ 您的权益: 源码一次性全交付 · 后台不加密不锁死 · 同城可上门面谈,全国线上协作
✅ 特别说明: 初创期精力过剩,同行大量甩单也接,价格美丽

📍 南宁本地可考察,欢迎随时来坐坐
📱 微信:anma1001(加我备注“接单”,直接发需求)

继续阅读 »

【南宁本地·全栈开发团队】10年老兵返乡创业,4人全职,无销售套路,直接技术一对一!

✅ 我们能做: 小程序 · APP · 网站 · 公众号 · 后台管理系统 · 定制化企业软件
✅ 我们的底气: 前广州一线全栈,人均10年+开发经验,全部技术骨干出身
✅ 您的权益: 源码一次性全交付 · 后台不加密不锁死 · 同城可上门面谈,全国线上协作
✅ 特别说明: 初创期精力过剩,同行大量甩单也接,价格美丽

📍 南宁本地可考察,欢迎随时来坐坐
📱 微信:anma1001(加我备注“接单”,直接发需求)

收起阅读 »

iOS 性能监控与调试工具选择 整合iOS性能监控和调试

iOS

'''开发同学应该都遇到过这种情况:排查 App 性能问题时,Mac 上开着 Instruments 跑耗时分析,Xcode 控制台挂着看日志输出,再用 iMazing 导一份沙箱文件出来——几个工具来回切,一套流程走下来半天就过去了。这还只是性能排查,如果还要分析崩溃日志、检查文件完整性、解密 App 数据,工具链还会拉得更长,每个工具都有自己的连接方式和操作逻辑。

常见方案:各司其职,但互不连通

Instruments 是苹果官方的性能分析工具,Time Profiler 查 CPU 热点、Allocations 看内存分配、GPU Driver 看渲染性能,每个模板都对应一个独立场景。工具本身功能强大,但启动慢、调试流程偏重,适合做深度优化时用,日常快速查看性能数据显得有些笨重。

Xcode Console 配合 OSLog 能输出 App 运行时日志,但日志量大时筛选过滤不太顺手,也没法保留长时间的历史记录。Crash 日志分析依赖 symbolicatecrash 脚本或者第三方平台,手动符号化操作比较繁琐。

文件管理这块,iMazing 和爱思助手能查看 iOS 设备的文件系统,但主要面向媒体文件备份和传输,对 App 沙箱目录的访问能力有限。要提取 App 运行时生成的数据文件,通常还需要越狱配合。

KeyMob:整合调试流程的另一种选择

KeyMob 把这几类需求集成到了一个工具里。性能监控面板能实时显示 CPU、GPU、内存占用和 FPS 帧率,数据以图表形式展示,时间线上的波动一目了然。卡顿检测会记录掉帧时的堆栈信息,不用像用 Instruments 那样先配置再重现。跑微信小程序或者 Unity3D 开发的 App 时同样能监控,兼容性覆盖得比较全。另外还能查看设备长达六个月的使用记录,每个 App 的启动时间、运行时长和电量消耗都有据可查。

日志查看功能可以按进程和 App 名称过滤输出,定位问题比在 Xcode Console 翻原始输出更直接。崩溃日志模块能直接导出设备的 crash report,自动完成符号化——设备连上电脑、选中日志、点击导出,三步操作就行,不需要跑 symbolicatecrash 命令。

文件管理部分可以浏览 App 的沙箱目录结构,把配置文件和数据缓存拖到电脑本地分析。解密功能提取 App 运行中保存的图片、音频和配置文件,查看数据存储格式和内容比较方便。USB 连接即可操作,不需要越狱。App 管理功能还能直接安装、卸载应用,查看进程信息和网络请求状态。

选择思路

Instruments 在深度性能分析上仍然不可替代。日常调试和快速排查场景下,KeyMob 把几个高频需求整合到一起,能减少工具间切换的成本。可以根据项目阶段和调试深度来搭配使用,各取所长就行。'''

继续阅读 »

'''开发同学应该都遇到过这种情况:排查 App 性能问题时,Mac 上开着 Instruments 跑耗时分析,Xcode 控制台挂着看日志输出,再用 iMazing 导一份沙箱文件出来——几个工具来回切,一套流程走下来半天就过去了。这还只是性能排查,如果还要分析崩溃日志、检查文件完整性、解密 App 数据,工具链还会拉得更长,每个工具都有自己的连接方式和操作逻辑。

常见方案:各司其职,但互不连通

Instruments 是苹果官方的性能分析工具,Time Profiler 查 CPU 热点、Allocations 看内存分配、GPU Driver 看渲染性能,每个模板都对应一个独立场景。工具本身功能强大,但启动慢、调试流程偏重,适合做深度优化时用,日常快速查看性能数据显得有些笨重。

Xcode Console 配合 OSLog 能输出 App 运行时日志,但日志量大时筛选过滤不太顺手,也没法保留长时间的历史记录。Crash 日志分析依赖 symbolicatecrash 脚本或者第三方平台,手动符号化操作比较繁琐。

文件管理这块,iMazing 和爱思助手能查看 iOS 设备的文件系统,但主要面向媒体文件备份和传输,对 App 沙箱目录的访问能力有限。要提取 App 运行时生成的数据文件,通常还需要越狱配合。

KeyMob:整合调试流程的另一种选择

KeyMob 把这几类需求集成到了一个工具里。性能监控面板能实时显示 CPU、GPU、内存占用和 FPS 帧率,数据以图表形式展示,时间线上的波动一目了然。卡顿检测会记录掉帧时的堆栈信息,不用像用 Instruments 那样先配置再重现。跑微信小程序或者 Unity3D 开发的 App 时同样能监控,兼容性覆盖得比较全。另外还能查看设备长达六个月的使用记录,每个 App 的启动时间、运行时长和电量消耗都有据可查。

日志查看功能可以按进程和 App 名称过滤输出,定位问题比在 Xcode Console 翻原始输出更直接。崩溃日志模块能直接导出设备的 crash report,自动完成符号化——设备连上电脑、选中日志、点击导出,三步操作就行,不需要跑 symbolicatecrash 命令。

文件管理部分可以浏览 App 的沙箱目录结构,把配置文件和数据缓存拖到电脑本地分析。解密功能提取 App 运行中保存的图片、音频和配置文件,查看数据存储格式和内容比较方便。USB 连接即可操作,不需要越狱。App 管理功能还能直接安装、卸载应用,查看进程信息和网络请求状态。

选择思路

Instruments 在深度性能分析上仍然不可替代。日常调试和快速排查场景下,KeyMob 把几个高频需求整合到一起,能减少工具间切换的成本。可以根据项目阶段和调试深度来搭配使用,各取所长就行。'''

收起阅读 »

希望官方开放 uniappx 的 页面路由

例如页面跳转 uni.navigateTo

原生的页面转场很多丝滑表现,
例如 安卓里面的转场很丝滑,包含他头部的 标题栏 就像是增量切换一样,而uniappx 和uniapp里面的就像是一块布硬生生的拖过去

iOS 的 页面转场则更多丝滑效果

等等 路由api,就那么几个动画,方式,很基础,很多原生的特性度无法使用,希望官方能把 页面路由的切换开放,提供给我们开发者自定义处理页面的路由跳转

继续阅读 »

例如页面跳转 uni.navigateTo

原生的页面转场很多丝滑表现,
例如 安卓里面的转场很丝滑,包含他头部的 标题栏 就像是增量切换一样,而uniappx 和uniapp里面的就像是一块布硬生生的拖过去

iOS 的 页面转场则更多丝滑效果

等等 路由api,就那么几个动画,方式,很基础,很多原生的特性度无法使用,希望官方能把 页面路由的切换开放,提供给我们开发者自定义处理页面的路由跳转

收起阅读 »

搭建三角洲陪玩护航小程序服务号平台需要什么?一站式打包部署支持微信小程序+h5+app

电竞陪玩、游戏代练和俱乐部陪练正在告别零散的‘人工接单时代’,一套功能齐全、支持独立部署的源码系统,是迈向‘系统化运营时代’的关键一步。

废话不多说,直接上干货!

一、需要资质

1.营业执照:个体户和企业执照都行,主要用于各种认证、小程序服务号及域名的备案等

2.法人信息:认证信息需要

3.对公账户:企业执照需要对公账户,个体户可用法人的银行卡;申请微信商户号用于平台收款

4.邮箱、手机号:申请各个平台的账号并登录

二、需要第三方产品

1.服务器:安装并承载系统的运行

2.域名:需备案,用于访问系统

3.服务号、小程序:都需要认证,且小程序要备案

三、源码系统

后端源码:Thinkphp6

前端源码:UNI-APP vue2.0(支持一键式发布 H5、微信小程序、安卓/iOS 苹果 app)

宝塔 HTTP://BT.CN: 安装后台、管理网站

下载护航陪玩系统源码

www.51duoke.cn/games/?id=1

✨ 核心功能模块

非租用版,源码交付、永久域名授权使用;独立部署、自主运营,不抽流水无年费,功能齐全可二开。电竞陪玩、游戏代练、三角洲行动护航、俱乐部陪练和工作室派单,正在从“人工接单时代”进入“系统化运营时代”。真正能长期跑起来的电竞护航系统,不只是做一个下单页面,也不是简单堆几个聊天和支付功能,而是要把用户获客、大神展示、订单派发、客服验收、工作室管理、会员体系、内容风控、数据分析和持续迭代全部串成闭环。

用户端功能

游戏分类与选择
护航手选择/发布需求
订单状态跟踪
在线沟通(集成自建 IM)
微信支付托管
老板端:高效下单,灵活选服​ 老板可快速筛选热门游戏类目,按需选择服务模式 —— 支持指定心仪打手、等待打手抢单或由平台客服专业派单,无需繁琐操作,3 步即可完成下单,精准匹配游戏需求。

大神端

接单管理
上传截图/完成证明
保证金押金系统
收益提现
打手排行榜
打手端:便捷接单,打造个人 IP​ 打手接收订单实时提示,可自主抢单或响应派单;完成服务后上传截图即可申请结算,流程简洁高效。此外,支持发布游戏动态、管理个人游戏卡片(展示战绩、擅长英雄等),助力打造个人品牌,吸引更多订单。

客服端

派单调度
订单监管
审核进度
线下发单
客服端:全程管控,保障履约​ 作为订单枢纽,客户端支持与老板、打手实时沟通,同步订单进度;提供派单分配、线下报单录入功能,同时通过审核打手完成截图的方式,确保服务质量,最终实现安全结单

管理后台

仪表盘数据可视化(订单量、交易额、用户增长)
用户与护航手管理(增删改查、封禁/解封)
订单管理与纠纷处理
财务管理(对账、佣金设置、提现审核)
内容管理(Banner、公告、活动发布)
审核管理(护航手资料审核)
系统设置(参数配置、游戏库管理)

适用人群

该系统适用于:

代练公司/工作室 - 快速建立私域客户,实现变现
游戏主播 - 直播间引流客户,实现下单管理
中小型网络公司 - 无需打手即可开展业务
工会 - 管理和运营打手团队
下载护航陪玩系统源码

继续阅读 »

电竞陪玩、游戏代练和俱乐部陪练正在告别零散的‘人工接单时代’,一套功能齐全、支持独立部署的源码系统,是迈向‘系统化运营时代’的关键一步。

废话不多说,直接上干货!

一、需要资质

1.营业执照:个体户和企业执照都行,主要用于各种认证、小程序服务号及域名的备案等

2.法人信息:认证信息需要

3.对公账户:企业执照需要对公账户,个体户可用法人的银行卡;申请微信商户号用于平台收款

4.邮箱、手机号:申请各个平台的账号并登录

二、需要第三方产品

1.服务器:安装并承载系统的运行

2.域名:需备案,用于访问系统

3.服务号、小程序:都需要认证,且小程序要备案

三、源码系统

后端源码:Thinkphp6

前端源码:UNI-APP vue2.0(支持一键式发布 H5、微信小程序、安卓/iOS 苹果 app)

宝塔 HTTP://BT.CN: 安装后台、管理网站

下载护航陪玩系统源码

www.51duoke.cn/games/?id=1

✨ 核心功能模块

非租用版,源码交付、永久域名授权使用;独立部署、自主运营,不抽流水无年费,功能齐全可二开。电竞陪玩、游戏代练、三角洲行动护航、俱乐部陪练和工作室派单,正在从“人工接单时代”进入“系统化运营时代”。真正能长期跑起来的电竞护航系统,不只是做一个下单页面,也不是简单堆几个聊天和支付功能,而是要把用户获客、大神展示、订单派发、客服验收、工作室管理、会员体系、内容风控、数据分析和持续迭代全部串成闭环。

用户端功能

游戏分类与选择
护航手选择/发布需求
订单状态跟踪
在线沟通(集成自建 IM)
微信支付托管
老板端:高效下单,灵活选服​ 老板可快速筛选热门游戏类目,按需选择服务模式 —— 支持指定心仪打手、等待打手抢单或由平台客服专业派单,无需繁琐操作,3 步即可完成下单,精准匹配游戏需求。

大神端

接单管理
上传截图/完成证明
保证金押金系统
收益提现
打手排行榜
打手端:便捷接单,打造个人 IP​ 打手接收订单实时提示,可自主抢单或响应派单;完成服务后上传截图即可申请结算,流程简洁高效。此外,支持发布游戏动态、管理个人游戏卡片(展示战绩、擅长英雄等),助力打造个人品牌,吸引更多订单。

客服端

派单调度
订单监管
审核进度
线下发单
客服端:全程管控,保障履约​ 作为订单枢纽,客户端支持与老板、打手实时沟通,同步订单进度;提供派单分配、线下报单录入功能,同时通过审核打手完成截图的方式,确保服务质量,最终实现安全结单

管理后台

仪表盘数据可视化(订单量、交易额、用户增长)
用户与护航手管理(增删改查、封禁/解封)
订单管理与纠纷处理
财务管理(对账、佣金设置、提现审核)
内容管理(Banner、公告、活动发布)
审核管理(护航手资料审核)
系统设置(参数配置、游戏库管理)

适用人群

该系统适用于:

代练公司/工作室 - 快速建立私域客户,实现变现
游戏主播 - 直播间引流客户,实现下单管理
中小型网络公司 - 无需打手即可开展业务
工会 - 管理和运营打手团队
下载护航陪玩系统源码

收起阅读 »

全平台(安卓,iOS,鸿蒙,微信小程序,h5)自定义+动态Tabbar+tabbar页面导航栏样式+tabbr数字角标 +动态主题+3种可选tabbar点击类型

点击查看插件

sunrains-tabbar 自定义 TabBar 插件使用指南

点击体验h5端

点击查看安卓端录屏效果

点击查看安卓端演示安装包

目录

  • [1. 基础配置]
  • [2. pages.json 配置]
  • [3. 动态配置 TabBar]
  • [4. 数字角标功能]
  • [5. 小程序端特殊配置]
  • [6. 主题配置]
  • [7. page页跳转自定义tab页]
  • [8. 拓展底部tabbar类型,(切换、弹出、进入)页面]

1. 基础配置

App.uvue onLaunch 初始化  
 import {initDefaultTabItems} from '@/uni_modules/sunrains-tabbar/tabbar-config.uts'  
        onLaunch(() => {  
        console.log('App Launch')  
        initDefaultTabItems()  
        }

1.1 配置 tabbar-config.uts 文件

在插件根目录 tabbar-config.uts 文件中,配置注册全局tab组件和导航栏设置。

完整示例:

import { setNavbarConfig, NavBarConfig, registerGlobalComponent, setDefaultTabItems, TabItem } from '@/uni_modules/sunrains-tabbar/utssdk/types.uts'  
import { getStoreLoginItems,storeLoginItems } from './utssdk/handle'  
//导入所有自定义tab页  
import Recommend from '@/pages/home/recommend.uvue'  
import Order from '@/pages/order/order.uvue'  
import Mine from '@/pages/mine/mine.uvue'  
import Detail from '@/pages/detail/detail.uvue'  

//注册所有需要切换的tab页或需要弹出的页面  即类型为 tabPage:tab页面  tabPop:弹窗组件  
registerGlobalComponent('home', Recommend)  
registerGlobalComponent('order', Order)  
registerGlobalComponent('mine', Mine)  
registerGlobalComponent('detail', Detail)  

// 2. 配置导航栏(不设置会使用默认配置)  
setNavbarConfig(new NavBarConfig(true, 34, 'bold', '#333333', '#ffffff', "", 88))  

//初始化默认tabbar区域按钮  
export function initDefaultTabItems(){  
    /**  
     *  
     * 3. 设置默认 Tab 列表(至少设置一个固定的,也可以全部设置,其它可通过请求服务端接口动态配置),  
     * 或通过网络请求初始化 游客 显示哪些  有 角色的 按角色 初始化  
     * App.uvue onLaunch 初始化  
      import {initDefaultTabItems} from '@/uni_modules/sunrains-tabbar/tabbar-config.uts'  
        onLaunch(() => {  
        console.log('App Launch')  
        initDefaultTabItems()  
        }  
     */  
    let home = new TabItem('home',"", 12, "red", "yellow", "bold", '首页', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage")  
    let mine = new TabItem('mine',"", 12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage")  

    let items = getStoreLoginItems();  
    //判断是否登录  
    if(false || items != null){  
        setDefaultTabItems(items)  
    }else{  
        setDefaultTabItems([  
            home,mine  
        ])  
        storeLoginItems([home,mine])  
    }  

}  

关键步骤说明:

  1. 导入组件:将所有需要作为 Tab 的页面组件导入
  2. 注册组件:使用 registerGlobalComponent 注册每个组件(第一个参数为唯一标识)
  3. 配置导航栏:可选配置,根据项目需求调整
  4. 设置默认 Tab:至少配置一个固定 Tab,其余可动态添加

2. pages.json 配置

2.1 必需配置

必须在 pages.json 中配置自定义 TabBar 入口页面(必须放在第一个位置),tab页无需在pages.json中配置:

{  
  "path": "uni_modules/sunrains-tabbar/index",  
  "style": {  
    "navigationStyle": "custom"  
  }  
}

2.2 完整示例

{  
    "pages": [   
        {  
          "path": "uni_modules/sunrains-tabbar/utssdk/index",  
          "style": {  
            "navigationStyle": "custom"  
          }  
        },  
        {  
          "path": "pages/detail/detail",  
          "style": {  
            "navigationBarTitleText": "详情"  
          }  
        }  
    ],  
    "globalStyle": {  
        "navigationBarTextStyle": "black",  
        "navigationBarTitleText": "uni-app x",  
        "navigationBarBackgroundColor": "#F8F8F8",  
        "backgroundColor": "#F8F8F8"  
    },  
    "uniIdRouter": {}  
}  

注意事项:

  • 自定义 TabBar 入口页面必须配置在 pages 数组的第一位
  • 所有 Tab 页面不需要在 pages 中声明
  • navigationStyle 设置为 "custom" 以隐藏原生导航栏

3. 动态配置 TabBar

3.1 使用场景

当需要根据用户权限、服务端配置等动态调整 TabBar 时,可以在任意页面触发刷新。

3.2 实现示例

以下面文档 mine.uvue 页面为例:

关键点:

  • 使用 refreshTabbar方法触发 TabBar 刷新
  • 确保所有动态 Tab 对应的组件已在 tabbar-config.uts 中注册

4. 数字角标功能

4.1 功能说明

支持为任意 Tab 设置数字角标,用于显示未读消息数、待处理事项等。

特性:

  • ✅ 动态设置/更新角标
  • ✅ 超过 99 自动显示 "99+"
  • ✅ 角标为 0 或 null 时不显示
  • ✅ 响应式更新,立即生效

4.2 使用方法

方法一:通过事件设置(推荐)

在任意页面通过 调用 setTabBadge 方法:

import { setTabBadge} from '@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts'  
// 设置算法 tab 的角标为 5  
setTabBadge("order",5)  
// 清除角标(设置为 0)  
setTabBadge("order",0)  
// 设置超过 99 显示 99+  
setTabBadge("order",150)

方法二:初始化时设置

在创建 TabItem 时传入 badge 参数:

import { TabItem } from '@/uni_modules/sunrains-tabbar/utssdk/types.uts'  

// 带角标的 tab  
new TabItem(  
  'order',   
  '算法',   
  '/static/tab/main2.png',   
  '/static/tab/main2_active.png'  
  10  // badge 参数  
)  

// 不带角标的 tab  
new TabItem(  
  'home',   
  '首页',   
  '/static/tab/main1.png',   
  '/static/tab/main1_active.png'  
)

4.3 完整示例

以 文档后面 mine.uvue 页面为例,实现动态设置角标功能:

4.4 实际应用场景

场景 1:算法未读数

// 从服务端获取未读算法数  
async function loadUnreadOrderCount() {  
  const res = await getOrderCount()  
  setTabBadge("order",res.data.count)  
}  

// 页面加载时调用  
onShow(() => {  
  loadUnreadOrderCount()  
})

场景 2:消息通知

// WebSocket 收到新消息  
websocket.onMessage((msg) => {  
  if (msg.type === 'new_message') {  
    const currentBadge = getCurrentBadge('message')  
    setTabBadge("newMessage",currentBadge + 1)  
  }  
})

场景 3:购物车数量

// 添加商品到购物车后更新角标  
function addToCart(product: Product) {  
  cartItems.push(product)  
  setTabBadge("cart",cartItems.length)  
}

5. 小程序端特殊配置

4.1 配置 tab-content-renderer.uvue

注意: 仅在需要支持微信小程序时需要配置,App 端可忽略此步骤。

uni_modules/sunrains-tabbar/tab-content-renderer.uvue 中配置:

<<template>  
  <view style="width: 100%; height: 100%;">  
   <Recommend v-if="currentTab === 'home'" style="width: 100%; height: 100%;" />  
    <Order v-if="currentTab === 'order'" style="width: 100%; height: 100%;" />  
    <Mine v-if="currentTab === 'mine'" style="width: 100%; height: 100%;" />  
      <Detail v-if="currentTab === 'detail'" style="width: 100%; height: 100%;" />  
  </view>  
</template>  

<script setup lang="uts">  
import Recommend from '@/pages/home/recommend.uvue'  
import Order from '@/pages/order/order.uvue'  
import Mine from '@/pages/mine/mine.uvue'  
import Detail from '@/pages/detail/detail.uvue'  
interface Props {  
  currentTab: string  
}  
defineProps({  
    currentTab:{  
        type:String  
    }  
})  
</script>  

配置说明:

  1. 条件编译:使用 #ifndef MP-WEIXIN#ifdef MP-WEIXIN 区分平台
  2. App 端:使用 <component :is="..."> 动态渲染组件
  3. 小程序端:必须静态导入并声明所有可能的 Tab 组件
  4. 导入位置:在 #ifdef MP-WEIXIN 块内导入组件

6. 主题配置

6.1 主题功能说明

插件支持自定义 TabBar 的主题样式,支持网络背景图、本地图片背景图、纯颜色背景(支持rgba透明)、导航栏、tabbar区域背景、图标大小、图标(支持网络图标)、文字颜色、文字大小、等属性 满足不同项目的视觉需求。

主题特性:

  • ✅ 支持全局主题配置
  • ✅ 支持动态切换主题
  • ✅ 支持自定义主题参数

6.2 初始化时配置主题

tabbar-config.uts 文件中配置主题:

import {   
  setNavbarConfig,   
  NavBarConfig,   
  registerGlobalComponent,   
  setDefaultTabItems,   
  TabItem,  
  setTabBarTheme,  
  TabBarTheme  
} from '@/uni_modules/sunrains-tabbar/types.uts'  
import { setTabPageTheme} from '@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts'  

let bgType : BgType = "IMG"  
//背景 如果是图片传图片路径 颜色则传 具体颜色  
let bg : string = "/static/2.png"  
//配置导航栏(可选,不设置会使用默认配置)  
let navBarConfig : NavBarConfig = new NavBarConfig(  
    true, // 是否显示导航栏  
    34,  // 字体大小  
    'bold',  // 字体粗细  
    '#FFA500', // 文字颜色  
    '',// 背景颜色  
    "", //背景图片(支持网络图片)  
    90) // 导航栏高度  

let tabbarIcon = new TabbarIcon(  
    "mine",//自定义组件唯一标识  
    //非选中图标  
    "https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fuser%2F779083e57f84a4ce5345b17aaf7f8459.jpeg&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1781283600&t=4feca8b5a1149e3ed0f28ee030f767b4",  
   //选中图标  
    "/static/tab/main3_active.png",  
    24,//图标宽 单位 px  
    24,//图标高 单位 px  
   //文字颜色  
    12,  
    //非选中文字大小  
    "green",  
    //选择文字颜色  
    "white",  
    //选择文字粗细  
    "bold"  
)  

let tabbarIcons : Map<string,TabbarIcon> = new Map();  
tabbarIcons.set("mine",tabbarIcon)  

let tabbarStyle : TabbarStyle = new TabbarStyle(  
    //tabbar区域高度  
    120,  
    //tabbar区域背景  
    "rgba(21, 20, 31, 0.8)",  
    //tabbar图标文字样式  
    tabbarIcons)  

let theme = new CustomTheme(bgType, bg, navBarConfig, tabbarStyle);  
setTabPageTheme(theme)

6.3 动态修改主题

在任意页面通过调用 setTabPageTheme 方法动态修改主题:

import { setTabPageTheme} from '@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts'  
// 动态修改背景色和选中颜色  
let theme = new CustomTheme(bgType, bg, navBarConfig, tabbarStyle);  
setTabPageTheme(theme)

6.4 在页面中动态切换主题

mine.uvue 页面为例,实现主题切换功能:

<template>  
    <scroll-view direction="vertical" class="mine-page"  
    show-scrollbar = "false"  
    enable-back-to-top="true"  
    refresher-enabled="true"  
    refresher-default-style="none"  
    refresher-background="rgba(232, 201, 183, 0.5)"  
    :refresher-triggered="data.refreshing2"  

     refresher-max-drag-distance="200px"  
          @refresherpulling="onRefresherpulling2"  
          @refresherrefresh="onRefresherrefresh2"  
          @refresherrestore="onRefresherrestore2"  

     >  
     <uni-refresh-box  
       slot="refresher"  
       :pulling-distance="data.pullingDistance2"  
       :refreshing="data.refreshing2"  
                    loading-class="loading-dark"  
                    text-class="uni-text-class-buildin"  
                    style="flex-direction: column;height: 46px;padding-top: 6px;"  
                    pulling-text="继续下拉可刷新"  
                    loosing-text="释放后会刷新"  
                    loading-text="奋力加载中..."  
                />  

        <view class="page-title">  
            <text style="color:#ff4d40">动态设置中心</text>  
        </view>  
        <view class="tabbar">  
            <text class="title-text">tabbar设置</text>  
            <button class="tabbar-btn" @click="refresh1">动态tabbar(我的)</button>  
            <button class="tabbar-btn" @click="refresh2">动态tabbar(首页、我的)</button>  
            <button class="tabbar-btn" @click="refresh3">动态tabbar(算法、我的)</button>  
            <button class="tabbar-btn" @click="refresh4">动态tabbar(我的、算法、首页、弹出详情、进入详情)</button>  
            <text class="title-text">设置角标</text>  
            <input class="input-item" v-model="tabbar" placeholder="请输入tabbar" />  
            <input class="input-item" v-model="badge" placeholder="请输入角标" />  
            <button class="tabbar-btn" @click="setBadge">确定</button>  
            <button class="tabbar-btn" @click="theme">主题1</button>  
            <button class="tabbar-btn" @click="theme2">主题2</button>  
            <button class="tabbar-btn" @click="theme3">主题3</button>  
            <button class="tabbar-btn" @click="clear">clear</button>  
            <!-- <button class="tabbar-btn" type="default" @click="refresh">刷新</button> -->  
                    <!-- <button   type="default" @click="cPop">返回</button> -->  

        </view>  

    </scroll-view>  
</template>  

<script setup lang="uts">  
    import { ref,onMounted,onUnmounted } from 'vue'  
    import { TabItem, GLOBAL_COMPONENT_REGISTRY, CustomTheme, TabbarIcon, TabbarStyle, NavBarConfig } from '@/uni_modules/sunrains-tabbar/utssdk/types.uts'  
    import { setTabBadge,setTabbarColorHeight,refreshTabbar ,setTabPageTheme,closeTabPop,clearStorage} from '@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts'  

 function cPop(){  
    closeTabPop()  
 }  

   onMounted(() => {  
     console.log('【组件挂载====mine===】')  
   })  

   onUnmounted(() => {  
     console.log('【组件销毁====mine===】')  
   })  

// 用于自动化测试的数据暴露 - 使用 reactive 将所有 ref 状态集中暴露  
const data = reactive({  
  refreshing2: false,  
  pullingDistance2: 0  

})  

function onRefresherpulling2(e: RefresherEvent) {  
  data.pullingDistance2 = e.detail.dy  
}  
    const refreshing = ref(false)  

function onRefresherrefresh2() {  
  data.refreshing2 = true  
  console.log('触发刷新')  
  setTimeout(() => {  
    data.refreshing2 = false  
    console.log('刷新完成')  
  }, 1500)  
}  

function onRefresherrestore2() {  
  data.pullingDistance2 = 0  
}  

function onRefresherabort2() {  
  data.pullingDistance2 = 0  
}  

    // 上拉触底加载  
    const loadMore = () => {  
        console.log("滚动到底部", "info")  
    }  

    function clear() {  
        clearStorage("all")  
    }  

    const bgColor = ref<string>('pink')  
    const tabHeight = ref<string>('120')  
    const badge = ref<string>('0')  
    const ss = ref<any>("");  

    // tab 选项配置  
    const tabbar = ref<string | null>(null)  
    const selectedTab = ref<string>('mine')  
    const selectedTabIndex = ref<number>(0)  

    function setBadge() {  
        //动态设置指定tabbar 角标  
        const badgeNum = parseInt(badge.value)  
        if (isNaN(badgeNum)) {  
            uni.showToast({  
                title: '请输入有效数字',  
                icon: 'none'  
            })  
            return  
        }  
        setTabBadge(tabbar.value,badgeNum)  
        uni.showToast({  
            title: `已设置 ${tabbar.value} 角标为 ${badgeNum}`,  
            icon: 'success'  
        })  
    }  

    function setStyle() {  
        setTabbarColorHeight(bgColor.value,parseInt(tabHeight.value) )  
    }  

    function refresh1() {  
        console.log("动态tabbar(我的)")  
        refreshTabbar([  
            new TabItem('mine', "",12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage")  
        ]);  
    }  
    function refresh2() {  
        console.log("动态tabbar(首页、我的)")  
        refreshTabbar([  
            new TabItem('home', "",12, "red", "yellow", "bold", '首页', '/static/tab/main1.png', '/static/tab/main1_active.png',24,24,"tabPage"),  
            new TabItem('mine', "",12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage")  
        ])  
    }  
    function refresh3() {  
        console.log("动态tabbar(算法、我的)")  

        refreshTabbar( [  
            new TabItem('order', "",12, "red", "yellow", "bold", '算法', '/static/tab/main2.png', '/static/tab/main2_active.png',24,24,"tabPage"),  
            new TabItem('mine', "",12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage")  
        ])  
    }  
    function refresh4() {  
        console.log("动态tabbar(我的、算法、首页、弹出详情、进入详情)")  

        refreshTabbar([  
            new TabItem('mine', "",12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage"),  
            new TabItem('order', "",12, "red", "yellow", "bold", '算法', '/static/tab/main2.png', '/static/tab/main2_active.png',24,24,"tabPage"),  
            new TabItem('home', "",12, "red", "yellow", "bold", '首页', '/static/tab/main1.png',  
             'https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fuser%2F779083e57f84a4ce5345b17aaf7f8459.jpeg&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1781283600&t=4feca8b5a1149e3ed0f28ee030f767b4',  
             24,24,"tabPage"  
             ),  
             new TabItem('detail', "",12, "red", "yellow", "bold", '弹出详情', '/static/tab/main1.png',  
              'https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fuser%2F779083e57f84a4ce5345b17aaf7f8459.jpeg&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1781283600&t=4feca8b5a1149e3ed0f28ee030f767b4',  
              24,24,"tabPop"  
              ),  
              new TabItem('detail', "/pages/detail/detail",12, "red", "yellow", "bold", '进入详情', '/static/tab/main1.png',  
               'https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fuser%2F779083e57f84a4ce5345b17aaf7f8459.jpeg&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1781283600&t=4feca8b5a1149e3ed0f28ee030f767b4',  
               24,24,"page"  
               )  
        ])  
    }  

    function theme() {  
        //主题背景类型  
        let bgType = "IMG"  
        //背景 如果是图片传图片路径 颜色则传 具体颜色  
        let bg : string = "/static/2.png"  
        // let bg:string = "black"  
        let navBarConfig : NavBarConfig = new NavBarConfig(true, 34, 'bold', '#FFA500', "rgba(191, 191, 191, 0.5)", "", 90)  
        let tabbarIcon = new TabbarIcon("mine",  
            "https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fuser%2F779083e57f84a4ce5345b17aaf7f8459.jpeg&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1781283600&t=4feca8b5a1149e3ed0f28ee030f767b4",  
            "/static/tab/main3_active.png",  
            24,24,  
            12,  
            "green",  
            "white",  
            "bold"  
        )  
        let tabbarIcons : Map<string, TabbarIcon> = new Map();  
        tabbarIcons.set("mine", tabbarIcon)  
        let tabbarStyle : TabbarStyle = new TabbarStyle(120, "rgba(21, 20, 31, 0.8)", tabbarIcons)  
        let theme = new CustomTheme(bgType, bg, navBarConfig, tabbarStyle);  
        setTabPageTheme(theme)  
    }  

    function theme2() {  
        //主题背景类型  
        let bgType  = "IMG"  
        //背景 如果是图片传图片路径 颜色则传 具体颜色  
        let bg : string = "/static/sm.jpg"  
        // let bg:string = "black"  
        let navBarConfig : NavBarConfig = new NavBarConfig(false, 34, 'bold', '#0c0c0c', "", "", 90)  
        let tabbarIcon = new TabbarIcon("mine",  
            "/static/aj.png",  
            "/static/aj1.png",  
            24,24,  
            20,  
            "green",  
            "red",  
            "bold"  
        )  
        let tabbarIcon1 = new TabbarIcon("home",  
            "https://static-mp-4c53170b-e5f8-453b-a634-afcb5cf07d59.next.bspapp.com/img/icon/豪横.gif",  
            "https://static-mp-4c53170b-e5f8-453b-a634-afcb5cf07d59.next.bspapp.com/img/icon/豪横.gif",  
            50,24,  
            16,  
            "black",  
            "yellow",  
            "bold"  
        )  
        let tabbarIcons : Map<string, TabbarIcon> = new Map();  
        tabbarIcons.set("mine", tabbarIcon)  
        tabbarIcons.set("home", tabbarIcon1)  
        let tabbarStyle : TabbarStyle = new TabbarStyle(120, "rgba(21, 20, 31, 0.4)", tabbarIcons)  
        let theme = new CustomTheme(bgType, bg, navBarConfig, tabbarStyle);  
        setTabPageTheme(theme)  
    }  
    function theme3() {  
        //主题背景类型  
        let bgType  = "IMG"  
        //背景 如果是图片传图片路径 颜色则传 具体颜色  
        let bg : string = "https://static-mp-4c53170b-e5f8-453b-a634-afcb5cf07d59.next.bspapp.com/img/bizhi/端午1.png"  
        // let bg:string = "black"  
        let navBarConfig : NavBarConfig = new NavBarConfig(true, 34, 'bold', '#0c0c0c', "", "", 90)  
        let tabbarIcon = new TabbarIcon("mine",  
            "/static/aj.png",  
            "/static/aj1.png",  
            24,24,  
            16,  
            "white",  
            "red",  
            "bold"  
        )  
        let tabbarIcon1 = new TabbarIcon("home",  
            "/static/sy1.png",  
            "/static/sy.png",  
            50,24,  
            16,  
            "white",  
            "red",  
            "bold"  
        )  
        let tabbarIcon2 = new TabbarIcon("detail",  
            "/static/tab/main2_active.png",  
            "/static/tab/main2_active.png",  
            24,24,  
            16,  
            "red",  
            "red",  
            "bold"  
        )  
        let tabbarIcons : Map<string, TabbarIcon> = new Map();  
        tabbarIcons.set("mine", tabbarIcon)  
        tabbarIcons.set("home", tabbarIcon1)  
        tabbarIcons.set("detail", tabbarIcon2)  
        let tabbarStyle : TabbarStyle = new TabbarStyle(150, "rgba(10, 13, 31, 0.8)", tabbarIcons)  
        let theme = new CustomTheme(bgType, bg, navBarConfig, tabbarStyle);  
        setTabPageTheme(theme)  
    }  

    function init() {  
        uni.stopPullDownRefresh()  
        console.log('stop onPullDownRefresh=====');  
    }  
    onPullDownRefresh(() => {  
        console.log('onPullDownRefresh=====');  
        init()  

    })  

    function refresh() {  
        uni.startPullDownRefresh({  
            success: () => {  
                console.log("1111")  
            }  
        });  
        init()  
    }  
</script>  

<style scoped lang="scss">  
    .mine-page {  
        height: 100%;  
        // border: 12px solid red;  
        margin: 5px;  
    }  

    .page-title {  
        margin-bottom: 15px;  
        display: flex;  
        align-items: center;  
        justify-content: center;  
        width: 100%;  
    }  

    .title-text {  
        font-size: 18px;  
        font-weight: bold;  
        color: #333;  
    }  

    .tabbar {  
        // border: 2px solid cyan;  
        padding: 15px;  
        display: flex;  
        flex-direction: column;  
    }  

    .input-item {  
        height: 45px;  
        border: 1px solid #ccc;  
        border-radius: 6px;  
        padding-left: 10px;  
        background: #fff;  
        margin: 10px;  
        padding-left: 20px;  
    }  

    .picker-item {  
        height: 45px;  
        border: 1px solid #ccc;  
        border-radius: 6px;  
        background: #fff;  
        margin: 10px;  
        display: flex;  
        align-items: center;  
    }  

    .picker-display {  
        padding-left: 20px;  
        font-size: 16px;  
        color: #333;  
    }  

    .tabbar-btn {  
        margin-bottom: 10px;  
        border-radius: 8px;  
        background: #f5f5f5;  
        border: 1px solid #e5e5e5;  
        padding: 16px 0;  
        font-size: 16px;  
    }  

    .tabbar-btn:last-child {  
        margin-bottom: 0;  
    }  
</style>  

6.5 主题配置注意事项

  1. 颜色格式:颜色值必须为有效的 CSS 颜色格式(如 #ffffffrgb(255,255,255) 等)
  2. tabbar区域高度单位:高度值为数字类型,单位为 rpx
  3. 动态修改:通过 uni.$emit('setTheme', new CustomTheme(bgType, bg, navBarConfig, tabbarStyle)) 动态修改的主题会立即生效

7 page页跳转自定义tab页

使用 pageToTab 方法 跳转到tab页

detail.uvue 页面为例,实现主题切换功能:

<template>  

    <view class="detail-root">  

        <view class="detail-content">  
<!--   
            <view>  
                <input v-model="content" placeholder="请输入。。" />  
                <text>{{sbase64}}</text>  
                <text>{{base64yostr}}</text>  
            </view> -->  
            <view class = "con">  
                <view  class = "com">  
                    <text>网络请求</text>     
                </view>  
                <view>  

                    <button class="btn" type="primary" @click="req">访问testvoid</button>  
                    <button class="btn"  type="primary" @click="reqSm2">访问testSm2</button>  
                    <button class="btn" type="primary" @click="reqSm4">访问testSm4</button>  
                    <button class="btn" type="primary" @click="reqSm2Sm4">访问testreqSm2Sm4</button>  
                </view>  
            </view>  

            <view class = "con">  
                <view class = "com">  
                    <text>跳转tab页</text>  
                </view>  

                <view>  
                    <input style="height: 50px; border:1px solid grey;margin: 5px 0;" v-model="addr" placeholder="输入跳转tabbar key"/>  
                    <button type="warn" @click="tz">跳转tab页</button>   
                </view>  
            </view>  

        </view>  

    </view>  

</template>  

<script setup lang="uts">  
    import { ref, computed } from 'vue'  
    import { stringToBase64, base64ToString, ReqApi } from "@/uni_modules/sunrains-request"  
    import { Response, FrontRequestVo } from "@/uni_modules/sunrains-common-type"  
    import { CustFailError, getComErrorInfo } from "@/uni_modules/sunrains-common-err"  
    import { pageToTab} from "@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts"  

   onMounted(() => {  
     console.log('【组件挂载====detail===】')  
   })  

   onUnmounted(() => {  
     console.log('【组件销毁====detail===】')  
   })  
    const addr = ref("")  
    function tz(){  
        if(addr.value.length<1){  
            uni.showToast({  
                title: '请输入跳转的tabbar key'  
            });  
        }else{  
            pageToTab(addr.value)  
        }  

    }  

    function fh(){  
        uni.navigateBack()  

    }  

    let priKey = "4a4a1d48fc284fe2fc43970641880554aed2f69fdfbea63257ea417af4e65802"  
    let pubKey = "0460e86263ec52b38a3da24c7a605055d14f8aa5c7855e4f4e19819f4f8a5b72181afd7aa4ab19255eb871eaedaba30fa18cd5c71693d3d76b030a04969bd1edfe"  
    const BASEURL = "http://192.168.1.9:8081"  
    type IRootType = {  
        name : string;  
        age : number;  
    }  

    type Builds = {  
        buildingNo : string,  
        sortNum : number,  
        buildAreaId : string,  
        id : string  
    }  

    async function req() {  
        let data : UTSJSONObject = {  
            name: "张三",  
            age: 18  
        }  
        try {  

            let vo : FrontRequestVo = {  
                method: 'GET',  
                url: BASEURL + "/test/get/cs1",  
                data: data,  
                header: null,  
                reqFlag: "void",  
                priKey: priKey,  
                pubKey: pubKey,  
                //method 为POST时 不能为null  
                contentType: "urlencoded",  
                timeout: null,  
                //当为上传文件时 文件路径参数不能为空  
                filePathParamName: null  
            }  
            const res = await ReqApi(vo)  
            console.log("####reqvoid==", res.data)  
            // console.log("####==",res)  
        } catch (err) {  
            uni.showToast({  
                title: err.message ?? "请求异常",  
                icon: "error"  
            })  
            console.log("-----", err)  

            return  
        };  
    }  

    //"sign"|"sm4Encrypt"|"signwithenSM2-Sm4Encrypt"|"void";  
    async function reqSm2() {  
        let data : UTSJSONObject = {  
            name: "张三",  
            age: 18  
        }  

        try {  
            let vo : FrontRequestVo = {  
                method: 'GET',  
                url: BASEURL + "/test/get/cs1",  
                data: data,  
                header: null,  
                reqFlag: "sign",  
                priKey: priKey,  
                pubKey: pubKey,  
                //method 为POST时 不能为null  
                contentType: "urlencoded",  
                timeout: null,  
                //当为上传文件时 文件路径参数不能为空  
                filePathParamName: null  
            }  

            const res = await ReqApi(vo)  
            console.log("####reqSm2==", res.data)  
        } catch (err) {  
            uni.showToast({  
                title: err.message ?? "请求异常",  
                icon: "error"  
            })  

            //#ifndef APP-ANDROID  
            const errMsg = (err as any).errMsg ?? "未知错误";  
            const errCode = (err as any).errCode ?? -1;  
            console.log('标准化错误码:', errMsg);  
            console.log('标准化错误信息:', errCode);  
            //#else   
            if (err instanceof UniError) {  
                let sa = err as UniError  
                console.log('sa==:', sa);  
            }  
            //#endif  
            console.log("-----", err)  
            return  
        };  
    }  

    async function reqSm4() {  
        let data : UTSJSONObject = {  
            name: "张三",  
            age: 18  
        }  
        try {  
            let vo : FrontRequestVo = {  
                method: 'POST',  
                url: BASEURL + "/test/post/cs1",  
                data: data,  
                header: null,  
                reqFlag: "sm4Encrypt",  
                priKey: priKey,  
                pubKey: pubKey,  
                //method 为POST时 不能为null  
                contentType: "urlencoded",  
                timeout: null,  
                //当为上传文件时 文件路径参数不能为空  
                filePathParamName: null  
            }  

            const res = await ReqApi(vo)  
            console.log("####reqSm4==", res.data)  
        } catch (err) {  
            console.log("####reqSm4 err==", err)  
            uni.showToast({  
                title: err.message??"",  
                icon: "error"  
            })  
            return  
        };  
    }  

    async function reqSm2Sm4() {  
        let data : UTSJSONObject = {  
            name: "张三",  
            age: 18  
        }  
        try {  
            let vo : FrontRequestVo = {  
                method: 'POST',  
                url: BASEURL + "/test/post/cs1",  
                data: data,  
                header: null,  
                reqFlag: "signwithenSM2-Sm4Encrypt",  
                priKey: priKey,  
                pubKey: pubKey,  
                //method 为POST时 不能为null  
                contentType: "urlencoded",  
                timeout: null,  
                //当为上传文件时 文件路径参数不能为空  
                filePathParamName: null  
            }  

            const res = await ReqApi(vo)  
            console.log("####reqSm2Sm4==", res.data)  
        } catch (err) {  
            console.log("-----", err)  
            return  
        };  
    }  

    const orderId = ref<string>('')  
    const orderStatus = ref<string>('')  
    const content = ref<string>('')  
    const sbase64 = computed(() => {  
        return stringToBase64(content.value)  
    })  

    const base64yostr = computed(() => {  
        return base64ToString(sbase64.value)  
    })  

    onLoad((options) => {  
        const opts = options as UTSJSONObject  
        const id = options?.["orderId"] as string | null  
        const status = options?.["status"] as string | null  

        if (id != null) {  
            orderId.value = id  
        }  
        if (status != null) {  
            orderStatus.value = status  
        }  
        console.log(`接收算法参数:ID=${orderId.value},状态=${orderStatus.value}`)  
    })  

    const goBack = () => {  
        uni.navigateBack()  
    }  

    function init(){  
              // uni.stopPullDownRefresh()  
              // console.log('stop onPullDownRefresh=====');  
    }  

     //  onPullDownRefresh(() => {  
     //    console.log('onPullDownRefresh=====');  
        // init()  

     //  })  

      function refresh(){  
          uni.removeStorageSync("LOGINTABBARITEMS")  
          // uni.startPullDownRefresh({  
             //  success:()=>{  
                //   console.log("1111")  
             //  }  
          // });  
          // init()  
      }  

</script>  

<style scoped>  
    .detail-root {  
/*      flex: 1;  
        display: flex;  
        flex-direction: column; */  
        background-color: #fff;  
    }  

    .status-bar {  
        height: 44px;  
        background-color: #ffffff;  
    }  

    .detail-header {  
        height: 44px;  
        background-color: #fff;  
        flex-direction: row;  
        align-items: center;  
        padding-left: 15px;  
        padding-right: 15px;  
        border-bottom-width: 1px;  
        border-bottom-style: solid;  
        border-bottom-color: #eee;  
    }  

    .back-icon {  
        font-size: 18px;  
        color: #007aff;  
        margin-right: 10px;  
    }  

    .detail-title {  
        font-size: 16px;  
        font-weight: bold;  
        color: #333;  
    }  

    .detail-content {  
        flex: 1;  
        padding: 20px;  
    }  

    .detail-item {  
        flex-direction: row;  
        margin-bottom: 20px;  
        align-items: center;  
    }  

    .label {  
        font-size: 15px;  
        color: #666;  
        width: 80px;  
    }  

    .value {  
        font-size: 15px;  
        color: #333;  
        font-weight: bold;  
    }  

    .detail-desc {  
        margin-top: 30px;  
    }  

    .com{  
        display:flex;  
        align-items: center;  
        margin:10px 0;   
    }  
    .con{  
        margin:10px 0;   
        padding:10px;  
        background-color: #e3eda1;  
    }  
    .btn{  
        margin: 5px;  
    }  
</style>  

8 拓展底部tabbar类型,(切换、弹出、进入)页面

    let mine = new TabItem(  
           'mine', //组件或页面唯一标识  
            "", //组件或页面路径  当类型为 page 时不可为空,其它传空字符串即可  
            12,  
            "red",  
            "yellow",   
            "bold",   
            '我的',   
            '/static/tab/main3.png',  
            '/static/tab/main3_active.png',  
            24,//图标宽  
            24,//图标高  
            "tabPage" //tabbar 类型 tabPage:tab页面  tabPop:弹窗组件  page:普通页面  
        )  

注:
类型为 tabPop 打开页面 可 右滑手势(当手机开启右滑手势时)关闭、 或点击虚拟返回按键关闭 或 页面中调用方法 closeTabPop()关闭

方法导入 import { pageToTab} from "@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts"


⚠️ 常见问题

Q1: 为什么小程序端需要单独配置?

A: 微信小程序不支持动态组件渲染,必须静态声明所有可能使用的组件。

Q2: 动态 Tab 刷新失败怎么办?

A: 检查以下几点:

  • 组件是否在 tabbar-config.uts 中正确注册
  • 是否正确使用了 GLOBAL_COMPONENT_REGISTRY.get() 获取组件
  • TabItem 的参数是否完整且类型正确

Q3: 导航栏配置不生效?

A: 确保 setNavbarConfig()setDefaultTabItems() 之前调用,或在应用启动时尽早调用。

Q4: 如何设置数字角标?

A: 使用 uni.$emit('setTabBadge', {"key": "tab_key", "badge": number}) 即可,详见第 4 章。

Q5: 角标设置后不显示怎么办?

A: 检查以下几点:

  • badge 值是否大于 0(0 或负数不显示)
  • tab key 是否正确(必须与 TabItem 的 key 一致)
  • 是否重新赋值了整个数组(直接修改对象属性不会触发响应式)

Q6: 退出登录如何清除 tabbar相关缓存

A: 调用 noticetab.uts里的 clearStorage 方法

/**  
 * 清除tabbar相关缓存  
 * all 所有  
 * theme 主题  
 * tabbarItems 所有tabbar  
 * badge 角标  
 */  
export function clearStorage(type:string){  
    switch (type){  
        case "theme":  
            uni.removeStorageSync("sunrains-tabbar-customtheme")      
            break;  
        case "tabbarItems":  
            uni.removeStorageSync("sunrains-tabbar-logintabbaritems")  
            break;    
        case "badge":  
            uni.removeStorageSync("sunrains-tabbar-tabbarbadges")  
            break;    
        default:  
            uni.removeStorageSync("sunrains-tabbar-customtheme")  
            uni.removeStorageSync("sunrains-tabbar-logintabbaritems")  
            uni.removeStorageSync("sunrains-tabbar-tabbarbadges")         
    }  

}

继续阅读 »

点击查看插件

sunrains-tabbar 自定义 TabBar 插件使用指南

点击体验h5端

点击查看安卓端录屏效果

点击查看安卓端演示安装包

目录

  • [1. 基础配置]
  • [2. pages.json 配置]
  • [3. 动态配置 TabBar]
  • [4. 数字角标功能]
  • [5. 小程序端特殊配置]
  • [6. 主题配置]
  • [7. page页跳转自定义tab页]
  • [8. 拓展底部tabbar类型,(切换、弹出、进入)页面]

1. 基础配置

App.uvue onLaunch 初始化  
 import {initDefaultTabItems} from '@/uni_modules/sunrains-tabbar/tabbar-config.uts'  
        onLaunch(() => {  
        console.log('App Launch')  
        initDefaultTabItems()  
        }

1.1 配置 tabbar-config.uts 文件

在插件根目录 tabbar-config.uts 文件中,配置注册全局tab组件和导航栏设置。

完整示例:

import { setNavbarConfig, NavBarConfig, registerGlobalComponent, setDefaultTabItems, TabItem } from '@/uni_modules/sunrains-tabbar/utssdk/types.uts'  
import { getStoreLoginItems,storeLoginItems } from './utssdk/handle'  
//导入所有自定义tab页  
import Recommend from '@/pages/home/recommend.uvue'  
import Order from '@/pages/order/order.uvue'  
import Mine from '@/pages/mine/mine.uvue'  
import Detail from '@/pages/detail/detail.uvue'  

//注册所有需要切换的tab页或需要弹出的页面  即类型为 tabPage:tab页面  tabPop:弹窗组件  
registerGlobalComponent('home', Recommend)  
registerGlobalComponent('order', Order)  
registerGlobalComponent('mine', Mine)  
registerGlobalComponent('detail', Detail)  

// 2. 配置导航栏(不设置会使用默认配置)  
setNavbarConfig(new NavBarConfig(true, 34, 'bold', '#333333', '#ffffff', "", 88))  

//初始化默认tabbar区域按钮  
export function initDefaultTabItems(){  
    /**  
     *  
     * 3. 设置默认 Tab 列表(至少设置一个固定的,也可以全部设置,其它可通过请求服务端接口动态配置),  
     * 或通过网络请求初始化 游客 显示哪些  有 角色的 按角色 初始化  
     * App.uvue onLaunch 初始化  
      import {initDefaultTabItems} from '@/uni_modules/sunrains-tabbar/tabbar-config.uts'  
        onLaunch(() => {  
        console.log('App Launch')  
        initDefaultTabItems()  
        }  
     */  
    let home = new TabItem('home',"", 12, "red", "yellow", "bold", '首页', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage")  
    let mine = new TabItem('mine',"", 12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage")  

    let items = getStoreLoginItems();  
    //判断是否登录  
    if(false || items != null){  
        setDefaultTabItems(items)  
    }else{  
        setDefaultTabItems([  
            home,mine  
        ])  
        storeLoginItems([home,mine])  
    }  

}  

关键步骤说明:

  1. 导入组件:将所有需要作为 Tab 的页面组件导入
  2. 注册组件:使用 registerGlobalComponent 注册每个组件(第一个参数为唯一标识)
  3. 配置导航栏:可选配置,根据项目需求调整
  4. 设置默认 Tab:至少配置一个固定 Tab,其余可动态添加

2. pages.json 配置

2.1 必需配置

必须在 pages.json 中配置自定义 TabBar 入口页面(必须放在第一个位置),tab页无需在pages.json中配置:

{  
  "path": "uni_modules/sunrains-tabbar/index",  
  "style": {  
    "navigationStyle": "custom"  
  }  
}

2.2 完整示例

{  
    "pages": [   
        {  
          "path": "uni_modules/sunrains-tabbar/utssdk/index",  
          "style": {  
            "navigationStyle": "custom"  
          }  
        },  
        {  
          "path": "pages/detail/detail",  
          "style": {  
            "navigationBarTitleText": "详情"  
          }  
        }  
    ],  
    "globalStyle": {  
        "navigationBarTextStyle": "black",  
        "navigationBarTitleText": "uni-app x",  
        "navigationBarBackgroundColor": "#F8F8F8",  
        "backgroundColor": "#F8F8F8"  
    },  
    "uniIdRouter": {}  
}  

注意事项:

  • 自定义 TabBar 入口页面必须配置在 pages 数组的第一位
  • 所有 Tab 页面不需要在 pages 中声明
  • navigationStyle 设置为 "custom" 以隐藏原生导航栏

3. 动态配置 TabBar

3.1 使用场景

当需要根据用户权限、服务端配置等动态调整 TabBar 时,可以在任意页面触发刷新。

3.2 实现示例

以下面文档 mine.uvue 页面为例:

关键点:

  • 使用 refreshTabbar方法触发 TabBar 刷新
  • 确保所有动态 Tab 对应的组件已在 tabbar-config.uts 中注册

4. 数字角标功能

4.1 功能说明

支持为任意 Tab 设置数字角标,用于显示未读消息数、待处理事项等。

特性:

  • ✅ 动态设置/更新角标
  • ✅ 超过 99 自动显示 "99+"
  • ✅ 角标为 0 或 null 时不显示
  • ✅ 响应式更新,立即生效

4.2 使用方法

方法一:通过事件设置(推荐)

在任意页面通过 调用 setTabBadge 方法:

import { setTabBadge} from '@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts'  
// 设置算法 tab 的角标为 5  
setTabBadge("order",5)  
// 清除角标(设置为 0)  
setTabBadge("order",0)  
// 设置超过 99 显示 99+  
setTabBadge("order",150)

方法二:初始化时设置

在创建 TabItem 时传入 badge 参数:

import { TabItem } from '@/uni_modules/sunrains-tabbar/utssdk/types.uts'  

// 带角标的 tab  
new TabItem(  
  'order',   
  '算法',   
  '/static/tab/main2.png',   
  '/static/tab/main2_active.png'  
  10  // badge 参数  
)  

// 不带角标的 tab  
new TabItem(  
  'home',   
  '首页',   
  '/static/tab/main1.png',   
  '/static/tab/main1_active.png'  
)

4.3 完整示例

以 文档后面 mine.uvue 页面为例,实现动态设置角标功能:

4.4 实际应用场景

场景 1:算法未读数

// 从服务端获取未读算法数  
async function loadUnreadOrderCount() {  
  const res = await getOrderCount()  
  setTabBadge("order",res.data.count)  
}  

// 页面加载时调用  
onShow(() => {  
  loadUnreadOrderCount()  
})

场景 2:消息通知

// WebSocket 收到新消息  
websocket.onMessage((msg) => {  
  if (msg.type === 'new_message') {  
    const currentBadge = getCurrentBadge('message')  
    setTabBadge("newMessage",currentBadge + 1)  
  }  
})

场景 3:购物车数量

// 添加商品到购物车后更新角标  
function addToCart(product: Product) {  
  cartItems.push(product)  
  setTabBadge("cart",cartItems.length)  
}

5. 小程序端特殊配置

4.1 配置 tab-content-renderer.uvue

注意: 仅在需要支持微信小程序时需要配置,App 端可忽略此步骤。

uni_modules/sunrains-tabbar/tab-content-renderer.uvue 中配置:

<<template>  
  <view style="width: 100%; height: 100%;">  
   <Recommend v-if="currentTab === 'home'" style="width: 100%; height: 100%;" />  
    <Order v-if="currentTab === 'order'" style="width: 100%; height: 100%;" />  
    <Mine v-if="currentTab === 'mine'" style="width: 100%; height: 100%;" />  
      <Detail v-if="currentTab === 'detail'" style="width: 100%; height: 100%;" />  
  </view>  
</template>  

<script setup lang="uts">  
import Recommend from '@/pages/home/recommend.uvue'  
import Order from '@/pages/order/order.uvue'  
import Mine from '@/pages/mine/mine.uvue'  
import Detail from '@/pages/detail/detail.uvue'  
interface Props {  
  currentTab: string  
}  
defineProps({  
    currentTab:{  
        type:String  
    }  
})  
</script>  

配置说明:

  1. 条件编译:使用 #ifndef MP-WEIXIN#ifdef MP-WEIXIN 区分平台
  2. App 端:使用 <component :is="..."> 动态渲染组件
  3. 小程序端:必须静态导入并声明所有可能的 Tab 组件
  4. 导入位置:在 #ifdef MP-WEIXIN 块内导入组件

6. 主题配置

6.1 主题功能说明

插件支持自定义 TabBar 的主题样式,支持网络背景图、本地图片背景图、纯颜色背景(支持rgba透明)、导航栏、tabbar区域背景、图标大小、图标(支持网络图标)、文字颜色、文字大小、等属性 满足不同项目的视觉需求。

主题特性:

  • ✅ 支持全局主题配置
  • ✅ 支持动态切换主题
  • ✅ 支持自定义主题参数

6.2 初始化时配置主题

tabbar-config.uts 文件中配置主题:

import {   
  setNavbarConfig,   
  NavBarConfig,   
  registerGlobalComponent,   
  setDefaultTabItems,   
  TabItem,  
  setTabBarTheme,  
  TabBarTheme  
} from '@/uni_modules/sunrains-tabbar/types.uts'  
import { setTabPageTheme} from '@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts'  

let bgType : BgType = "IMG"  
//背景 如果是图片传图片路径 颜色则传 具体颜色  
let bg : string = "/static/2.png"  
//配置导航栏(可选,不设置会使用默认配置)  
let navBarConfig : NavBarConfig = new NavBarConfig(  
    true, // 是否显示导航栏  
    34,  // 字体大小  
    'bold',  // 字体粗细  
    '#FFA500', // 文字颜色  
    '',// 背景颜色  
    "", //背景图片(支持网络图片)  
    90) // 导航栏高度  

let tabbarIcon = new TabbarIcon(  
    "mine",//自定义组件唯一标识  
    //非选中图标  
    "https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fuser%2F779083e57f84a4ce5345b17aaf7f8459.jpeg&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1781283600&t=4feca8b5a1149e3ed0f28ee030f767b4",  
   //选中图标  
    "/static/tab/main3_active.png",  
    24,//图标宽 单位 px  
    24,//图标高 单位 px  
   //文字颜色  
    12,  
    //非选中文字大小  
    "green",  
    //选择文字颜色  
    "white",  
    //选择文字粗细  
    "bold"  
)  

let tabbarIcons : Map<string,TabbarIcon> = new Map();  
tabbarIcons.set("mine",tabbarIcon)  

let tabbarStyle : TabbarStyle = new TabbarStyle(  
    //tabbar区域高度  
    120,  
    //tabbar区域背景  
    "rgba(21, 20, 31, 0.8)",  
    //tabbar图标文字样式  
    tabbarIcons)  

let theme = new CustomTheme(bgType, bg, navBarConfig, tabbarStyle);  
setTabPageTheme(theme)

6.3 动态修改主题

在任意页面通过调用 setTabPageTheme 方法动态修改主题:

import { setTabPageTheme} from '@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts'  
// 动态修改背景色和选中颜色  
let theme = new CustomTheme(bgType, bg, navBarConfig, tabbarStyle);  
setTabPageTheme(theme)

6.4 在页面中动态切换主题

mine.uvue 页面为例,实现主题切换功能:

<template>  
    <scroll-view direction="vertical" class="mine-page"  
    show-scrollbar = "false"  
    enable-back-to-top="true"  
    refresher-enabled="true"  
    refresher-default-style="none"  
    refresher-background="rgba(232, 201, 183, 0.5)"  
    :refresher-triggered="data.refreshing2"  

     refresher-max-drag-distance="200px"  
          @refresherpulling="onRefresherpulling2"  
          @refresherrefresh="onRefresherrefresh2"  
          @refresherrestore="onRefresherrestore2"  

     >  
     <uni-refresh-box  
       slot="refresher"  
       :pulling-distance="data.pullingDistance2"  
       :refreshing="data.refreshing2"  
                    loading-class="loading-dark"  
                    text-class="uni-text-class-buildin"  
                    style="flex-direction: column;height: 46px;padding-top: 6px;"  
                    pulling-text="继续下拉可刷新"  
                    loosing-text="释放后会刷新"  
                    loading-text="奋力加载中..."  
                />  

        <view class="page-title">  
            <text style="color:#ff4d40">动态设置中心</text>  
        </view>  
        <view class="tabbar">  
            <text class="title-text">tabbar设置</text>  
            <button class="tabbar-btn" @click="refresh1">动态tabbar(我的)</button>  
            <button class="tabbar-btn" @click="refresh2">动态tabbar(首页、我的)</button>  
            <button class="tabbar-btn" @click="refresh3">动态tabbar(算法、我的)</button>  
            <button class="tabbar-btn" @click="refresh4">动态tabbar(我的、算法、首页、弹出详情、进入详情)</button>  
            <text class="title-text">设置角标</text>  
            <input class="input-item" v-model="tabbar" placeholder="请输入tabbar" />  
            <input class="input-item" v-model="badge" placeholder="请输入角标" />  
            <button class="tabbar-btn" @click="setBadge">确定</button>  
            <button class="tabbar-btn" @click="theme">主题1</button>  
            <button class="tabbar-btn" @click="theme2">主题2</button>  
            <button class="tabbar-btn" @click="theme3">主题3</button>  
            <button class="tabbar-btn" @click="clear">clear</button>  
            <!-- <button class="tabbar-btn" type="default" @click="refresh">刷新</button> -->  
                    <!-- <button   type="default" @click="cPop">返回</button> -->  

        </view>  

    </scroll-view>  
</template>  

<script setup lang="uts">  
    import { ref,onMounted,onUnmounted } from 'vue'  
    import { TabItem, GLOBAL_COMPONENT_REGISTRY, CustomTheme, TabbarIcon, TabbarStyle, NavBarConfig } from '@/uni_modules/sunrains-tabbar/utssdk/types.uts'  
    import { setTabBadge,setTabbarColorHeight,refreshTabbar ,setTabPageTheme,closeTabPop,clearStorage} from '@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts'  

 function cPop(){  
    closeTabPop()  
 }  

   onMounted(() => {  
     console.log('【组件挂载====mine===】')  
   })  

   onUnmounted(() => {  
     console.log('【组件销毁====mine===】')  
   })  

// 用于自动化测试的数据暴露 - 使用 reactive 将所有 ref 状态集中暴露  
const data = reactive({  
  refreshing2: false,  
  pullingDistance2: 0  

})  

function onRefresherpulling2(e: RefresherEvent) {  
  data.pullingDistance2 = e.detail.dy  
}  
    const refreshing = ref(false)  

function onRefresherrefresh2() {  
  data.refreshing2 = true  
  console.log('触发刷新')  
  setTimeout(() => {  
    data.refreshing2 = false  
    console.log('刷新完成')  
  }, 1500)  
}  

function onRefresherrestore2() {  
  data.pullingDistance2 = 0  
}  

function onRefresherabort2() {  
  data.pullingDistance2 = 0  
}  

    // 上拉触底加载  
    const loadMore = () => {  
        console.log("滚动到底部", "info")  
    }  

    function clear() {  
        clearStorage("all")  
    }  

    const bgColor = ref<string>('pink')  
    const tabHeight = ref<string>('120')  
    const badge = ref<string>('0')  
    const ss = ref<any>("");  

    // tab 选项配置  
    const tabbar = ref<string | null>(null)  
    const selectedTab = ref<string>('mine')  
    const selectedTabIndex = ref<number>(0)  

    function setBadge() {  
        //动态设置指定tabbar 角标  
        const badgeNum = parseInt(badge.value)  
        if (isNaN(badgeNum)) {  
            uni.showToast({  
                title: '请输入有效数字',  
                icon: 'none'  
            })  
            return  
        }  
        setTabBadge(tabbar.value,badgeNum)  
        uni.showToast({  
            title: `已设置 ${tabbar.value} 角标为 ${badgeNum}`,  
            icon: 'success'  
        })  
    }  

    function setStyle() {  
        setTabbarColorHeight(bgColor.value,parseInt(tabHeight.value) )  
    }  

    function refresh1() {  
        console.log("动态tabbar(我的)")  
        refreshTabbar([  
            new TabItem('mine', "",12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage")  
        ]);  
    }  
    function refresh2() {  
        console.log("动态tabbar(首页、我的)")  
        refreshTabbar([  
            new TabItem('home', "",12, "red", "yellow", "bold", '首页', '/static/tab/main1.png', '/static/tab/main1_active.png',24,24,"tabPage"),  
            new TabItem('mine', "",12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage")  
        ])  
    }  
    function refresh3() {  
        console.log("动态tabbar(算法、我的)")  

        refreshTabbar( [  
            new TabItem('order', "",12, "red", "yellow", "bold", '算法', '/static/tab/main2.png', '/static/tab/main2_active.png',24,24,"tabPage"),  
            new TabItem('mine', "",12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage")  
        ])  
    }  
    function refresh4() {  
        console.log("动态tabbar(我的、算法、首页、弹出详情、进入详情)")  

        refreshTabbar([  
            new TabItem('mine', "",12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage"),  
            new TabItem('order', "",12, "red", "yellow", "bold", '算法', '/static/tab/main2.png', '/static/tab/main2_active.png',24,24,"tabPage"),  
            new TabItem('home', "",12, "red", "yellow", "bold", '首页', '/static/tab/main1.png',  
             'https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fuser%2F779083e57f84a4ce5345b17aaf7f8459.jpeg&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1781283600&t=4feca8b5a1149e3ed0f28ee030f767b4',  
             24,24,"tabPage"  
             ),  
             new TabItem('detail', "",12, "red", "yellow", "bold", '弹出详情', '/static/tab/main1.png',  
              'https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fuser%2F779083e57f84a4ce5345b17aaf7f8459.jpeg&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1781283600&t=4feca8b5a1149e3ed0f28ee030f767b4',  
              24,24,"tabPop"  
              ),  
              new TabItem('detail', "/pages/detail/detail",12, "red", "yellow", "bold", '进入详情', '/static/tab/main1.png',  
               'https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fuser%2F779083e57f84a4ce5345b17aaf7f8459.jpeg&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1781283600&t=4feca8b5a1149e3ed0f28ee030f767b4',  
               24,24,"page"  
               )  
        ])  
    }  

    function theme() {  
        //主题背景类型  
        let bgType = "IMG"  
        //背景 如果是图片传图片路径 颜色则传 具体颜色  
        let bg : string = "/static/2.png"  
        // let bg:string = "black"  
        let navBarConfig : NavBarConfig = new NavBarConfig(true, 34, 'bold', '#FFA500', "rgba(191, 191, 191, 0.5)", "", 90)  
        let tabbarIcon = new TabbarIcon("mine",  
            "https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fuser%2F779083e57f84a4ce5345b17aaf7f8459.jpeg&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1781283600&t=4feca8b5a1149e3ed0f28ee030f767b4",  
            "/static/tab/main3_active.png",  
            24,24,  
            12,  
            "green",  
            "white",  
            "bold"  
        )  
        let tabbarIcons : Map<string, TabbarIcon> = new Map();  
        tabbarIcons.set("mine", tabbarIcon)  
        let tabbarStyle : TabbarStyle = new TabbarStyle(120, "rgba(21, 20, 31, 0.8)", tabbarIcons)  
        let theme = new CustomTheme(bgType, bg, navBarConfig, tabbarStyle);  
        setTabPageTheme(theme)  
    }  

    function theme2() {  
        //主题背景类型  
        let bgType  = "IMG"  
        //背景 如果是图片传图片路径 颜色则传 具体颜色  
        let bg : string = "/static/sm.jpg"  
        // let bg:string = "black"  
        let navBarConfig : NavBarConfig = new NavBarConfig(false, 34, 'bold', '#0c0c0c', "", "", 90)  
        let tabbarIcon = new TabbarIcon("mine",  
            "/static/aj.png",  
            "/static/aj1.png",  
            24,24,  
            20,  
            "green",  
            "red",  
            "bold"  
        )  
        let tabbarIcon1 = new TabbarIcon("home",  
            "https://static-mp-4c53170b-e5f8-453b-a634-afcb5cf07d59.next.bspapp.com/img/icon/豪横.gif",  
            "https://static-mp-4c53170b-e5f8-453b-a634-afcb5cf07d59.next.bspapp.com/img/icon/豪横.gif",  
            50,24,  
            16,  
            "black",  
            "yellow",  
            "bold"  
        )  
        let tabbarIcons : Map<string, TabbarIcon> = new Map();  
        tabbarIcons.set("mine", tabbarIcon)  
        tabbarIcons.set("home", tabbarIcon1)  
        let tabbarStyle : TabbarStyle = new TabbarStyle(120, "rgba(21, 20, 31, 0.4)", tabbarIcons)  
        let theme = new CustomTheme(bgType, bg, navBarConfig, tabbarStyle);  
        setTabPageTheme(theme)  
    }  
    function theme3() {  
        //主题背景类型  
        let bgType  = "IMG"  
        //背景 如果是图片传图片路径 颜色则传 具体颜色  
        let bg : string = "https://static-mp-4c53170b-e5f8-453b-a634-afcb5cf07d59.next.bspapp.com/img/bizhi/端午1.png"  
        // let bg:string = "black"  
        let navBarConfig : NavBarConfig = new NavBarConfig(true, 34, 'bold', '#0c0c0c', "", "", 90)  
        let tabbarIcon = new TabbarIcon("mine",  
            "/static/aj.png",  
            "/static/aj1.png",  
            24,24,  
            16,  
            "white",  
            "red",  
            "bold"  
        )  
        let tabbarIcon1 = new TabbarIcon("home",  
            "/static/sy1.png",  
            "/static/sy.png",  
            50,24,  
            16,  
            "white",  
            "red",  
            "bold"  
        )  
        let tabbarIcon2 = new TabbarIcon("detail",  
            "/static/tab/main2_active.png",  
            "/static/tab/main2_active.png",  
            24,24,  
            16,  
            "red",  
            "red",  
            "bold"  
        )  
        let tabbarIcons : Map<string, TabbarIcon> = new Map();  
        tabbarIcons.set("mine", tabbarIcon)  
        tabbarIcons.set("home", tabbarIcon1)  
        tabbarIcons.set("detail", tabbarIcon2)  
        let tabbarStyle : TabbarStyle = new TabbarStyle(150, "rgba(10, 13, 31, 0.8)", tabbarIcons)  
        let theme = new CustomTheme(bgType, bg, navBarConfig, tabbarStyle);  
        setTabPageTheme(theme)  
    }  

    function init() {  
        uni.stopPullDownRefresh()  
        console.log('stop onPullDownRefresh=====');  
    }  
    onPullDownRefresh(() => {  
        console.log('onPullDownRefresh=====');  
        init()  

    })  

    function refresh() {  
        uni.startPullDownRefresh({  
            success: () => {  
                console.log("1111")  
            }  
        });  
        init()  
    }  
</script>  

<style scoped lang="scss">  
    .mine-page {  
        height: 100%;  
        // border: 12px solid red;  
        margin: 5px;  
    }  

    .page-title {  
        margin-bottom: 15px;  
        display: flex;  
        align-items: center;  
        justify-content: center;  
        width: 100%;  
    }  

    .title-text {  
        font-size: 18px;  
        font-weight: bold;  
        color: #333;  
    }  

    .tabbar {  
        // border: 2px solid cyan;  
        padding: 15px;  
        display: flex;  
        flex-direction: column;  
    }  

    .input-item {  
        height: 45px;  
        border: 1px solid #ccc;  
        border-radius: 6px;  
        padding-left: 10px;  
        background: #fff;  
        margin: 10px;  
        padding-left: 20px;  
    }  

    .picker-item {  
        height: 45px;  
        border: 1px solid #ccc;  
        border-radius: 6px;  
        background: #fff;  
        margin: 10px;  
        display: flex;  
        align-items: center;  
    }  

    .picker-display {  
        padding-left: 20px;  
        font-size: 16px;  
        color: #333;  
    }  

    .tabbar-btn {  
        margin-bottom: 10px;  
        border-radius: 8px;  
        background: #f5f5f5;  
        border: 1px solid #e5e5e5;  
        padding: 16px 0;  
        font-size: 16px;  
    }  

    .tabbar-btn:last-child {  
        margin-bottom: 0;  
    }  
</style>  

6.5 主题配置注意事项

  1. 颜色格式:颜色值必须为有效的 CSS 颜色格式(如 #ffffffrgb(255,255,255) 等)
  2. tabbar区域高度单位:高度值为数字类型,单位为 rpx
  3. 动态修改:通过 uni.$emit('setTheme', new CustomTheme(bgType, bg, navBarConfig, tabbarStyle)) 动态修改的主题会立即生效

7 page页跳转自定义tab页

使用 pageToTab 方法 跳转到tab页

detail.uvue 页面为例,实现主题切换功能:

<template>  

    <view class="detail-root">  

        <view class="detail-content">  
<!--   
            <view>  
                <input v-model="content" placeholder="请输入。。" />  
                <text>{{sbase64}}</text>  
                <text>{{base64yostr}}</text>  
            </view> -->  
            <view class = "con">  
                <view  class = "com">  
                    <text>网络请求</text>     
                </view>  
                <view>  

                    <button class="btn" type="primary" @click="req">访问testvoid</button>  
                    <button class="btn"  type="primary" @click="reqSm2">访问testSm2</button>  
                    <button class="btn" type="primary" @click="reqSm4">访问testSm4</button>  
                    <button class="btn" type="primary" @click="reqSm2Sm4">访问testreqSm2Sm4</button>  
                </view>  
            </view>  

            <view class = "con">  
                <view class = "com">  
                    <text>跳转tab页</text>  
                </view>  

                <view>  
                    <input style="height: 50px; border:1px solid grey;margin: 5px 0;" v-model="addr" placeholder="输入跳转tabbar key"/>  
                    <button type="warn" @click="tz">跳转tab页</button>   
                </view>  
            </view>  

        </view>  

    </view>  

</template>  

<script setup lang="uts">  
    import { ref, computed } from 'vue'  
    import { stringToBase64, base64ToString, ReqApi } from "@/uni_modules/sunrains-request"  
    import { Response, FrontRequestVo } from "@/uni_modules/sunrains-common-type"  
    import { CustFailError, getComErrorInfo } from "@/uni_modules/sunrains-common-err"  
    import { pageToTab} from "@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts"  

   onMounted(() => {  
     console.log('【组件挂载====detail===】')  
   })  

   onUnmounted(() => {  
     console.log('【组件销毁====detail===】')  
   })  
    const addr = ref("")  
    function tz(){  
        if(addr.value.length<1){  
            uni.showToast({  
                title: '请输入跳转的tabbar key'  
            });  
        }else{  
            pageToTab(addr.value)  
        }  

    }  

    function fh(){  
        uni.navigateBack()  

    }  

    let priKey = "4a4a1d48fc284fe2fc43970641880554aed2f69fdfbea63257ea417af4e65802"  
    let pubKey = "0460e86263ec52b38a3da24c7a605055d14f8aa5c7855e4f4e19819f4f8a5b72181afd7aa4ab19255eb871eaedaba30fa18cd5c71693d3d76b030a04969bd1edfe"  
    const BASEURL = "http://192.168.1.9:8081"  
    type IRootType = {  
        name : string;  
        age : number;  
    }  

    type Builds = {  
        buildingNo : string,  
        sortNum : number,  
        buildAreaId : string,  
        id : string  
    }  

    async function req() {  
        let data : UTSJSONObject = {  
            name: "张三",  
            age: 18  
        }  
        try {  

            let vo : FrontRequestVo = {  
                method: 'GET',  
                url: BASEURL + "/test/get/cs1",  
                data: data,  
                header: null,  
                reqFlag: "void",  
                priKey: priKey,  
                pubKey: pubKey,  
                //method 为POST时 不能为null  
                contentType: "urlencoded",  
                timeout: null,  
                //当为上传文件时 文件路径参数不能为空  
                filePathParamName: null  
            }  
            const res = await ReqApi(vo)  
            console.log("####reqvoid==", res.data)  
            // console.log("####==",res)  
        } catch (err) {  
            uni.showToast({  
                title: err.message ?? "请求异常",  
                icon: "error"  
            })  
            console.log("-----", err)  

            return  
        };  
    }  

    //"sign"|"sm4Encrypt"|"signwithenSM2-Sm4Encrypt"|"void";  
    async function reqSm2() {  
        let data : UTSJSONObject = {  
            name: "张三",  
            age: 18  
        }  

        try {  
            let vo : FrontRequestVo = {  
                method: 'GET',  
                url: BASEURL + "/test/get/cs1",  
                data: data,  
                header: null,  
                reqFlag: "sign",  
                priKey: priKey,  
                pubKey: pubKey,  
                //method 为POST时 不能为null  
                contentType: "urlencoded",  
                timeout: null,  
                //当为上传文件时 文件路径参数不能为空  
                filePathParamName: null  
            }  

            const res = await ReqApi(vo)  
            console.log("####reqSm2==", res.data)  
        } catch (err) {  
            uni.showToast({  
                title: err.message ?? "请求异常",  
                icon: "error"  
            })  

            //#ifndef APP-ANDROID  
            const errMsg = (err as any).errMsg ?? "未知错误";  
            const errCode = (err as any).errCode ?? -1;  
            console.log('标准化错误码:', errMsg);  
            console.log('标准化错误信息:', errCode);  
            //#else   
            if (err instanceof UniError) {  
                let sa = err as UniError  
                console.log('sa==:', sa);  
            }  
            //#endif  
            console.log("-----", err)  
            return  
        };  
    }  

    async function reqSm4() {  
        let data : UTSJSONObject = {  
            name: "张三",  
            age: 18  
        }  
        try {  
            let vo : FrontRequestVo = {  
                method: 'POST',  
                url: BASEURL + "/test/post/cs1",  
                data: data,  
                header: null,  
                reqFlag: "sm4Encrypt",  
                priKey: priKey,  
                pubKey: pubKey,  
                //method 为POST时 不能为null  
                contentType: "urlencoded",  
                timeout: null,  
                //当为上传文件时 文件路径参数不能为空  
                filePathParamName: null  
            }  

            const res = await ReqApi(vo)  
            console.log("####reqSm4==", res.data)  
        } catch (err) {  
            console.log("####reqSm4 err==", err)  
            uni.showToast({  
                title: err.message??"",  
                icon: "error"  
            })  
            return  
        };  
    }  

    async function reqSm2Sm4() {  
        let data : UTSJSONObject = {  
            name: "张三",  
            age: 18  
        }  
        try {  
            let vo : FrontRequestVo = {  
                method: 'POST',  
                url: BASEURL + "/test/post/cs1",  
                data: data,  
                header: null,  
                reqFlag: "signwithenSM2-Sm4Encrypt",  
                priKey: priKey,  
                pubKey: pubKey,  
                //method 为POST时 不能为null  
                contentType: "urlencoded",  
                timeout: null,  
                //当为上传文件时 文件路径参数不能为空  
                filePathParamName: null  
            }  

            const res = await ReqApi(vo)  
            console.log("####reqSm2Sm4==", res.data)  
        } catch (err) {  
            console.log("-----", err)  
            return  
        };  
    }  

    const orderId = ref<string>('')  
    const orderStatus = ref<string>('')  
    const content = ref<string>('')  
    const sbase64 = computed(() => {  
        return stringToBase64(content.value)  
    })  

    const base64yostr = computed(() => {  
        return base64ToString(sbase64.value)  
    })  

    onLoad((options) => {  
        const opts = options as UTSJSONObject  
        const id = options?.["orderId"] as string | null  
        const status = options?.["status"] as string | null  

        if (id != null) {  
            orderId.value = id  
        }  
        if (status != null) {  
            orderStatus.value = status  
        }  
        console.log(`接收算法参数:ID=${orderId.value},状态=${orderStatus.value}`)  
    })  

    const goBack = () => {  
        uni.navigateBack()  
    }  

    function init(){  
              // uni.stopPullDownRefresh()  
              // console.log('stop onPullDownRefresh=====');  
    }  

     //  onPullDownRefresh(() => {  
     //    console.log('onPullDownRefresh=====');  
        // init()  

     //  })  

      function refresh(){  
          uni.removeStorageSync("LOGINTABBARITEMS")  
          // uni.startPullDownRefresh({  
             //  success:()=>{  
                //   console.log("1111")  
             //  }  
          // });  
          // init()  
      }  

</script>  

<style scoped>  
    .detail-root {  
/*      flex: 1;  
        display: flex;  
        flex-direction: column; */  
        background-color: #fff;  
    }  

    .status-bar {  
        height: 44px;  
        background-color: #ffffff;  
    }  

    .detail-header {  
        height: 44px;  
        background-color: #fff;  
        flex-direction: row;  
        align-items: center;  
        padding-left: 15px;  
        padding-right: 15px;  
        border-bottom-width: 1px;  
        border-bottom-style: solid;  
        border-bottom-color: #eee;  
    }  

    .back-icon {  
        font-size: 18px;  
        color: #007aff;  
        margin-right: 10px;  
    }  

    .detail-title {  
        font-size: 16px;  
        font-weight: bold;  
        color: #333;  
    }  

    .detail-content {  
        flex: 1;  
        padding: 20px;  
    }  

    .detail-item {  
        flex-direction: row;  
        margin-bottom: 20px;  
        align-items: center;  
    }  

    .label {  
        font-size: 15px;  
        color: #666;  
        width: 80px;  
    }  

    .value {  
        font-size: 15px;  
        color: #333;  
        font-weight: bold;  
    }  

    .detail-desc {  
        margin-top: 30px;  
    }  

    .com{  
        display:flex;  
        align-items: center;  
        margin:10px 0;   
    }  
    .con{  
        margin:10px 0;   
        padding:10px;  
        background-color: #e3eda1;  
    }  
    .btn{  
        margin: 5px;  
    }  
</style>  

8 拓展底部tabbar类型,(切换、弹出、进入)页面

    let mine = new TabItem(  
           'mine', //组件或页面唯一标识  
            "", //组件或页面路径  当类型为 page 时不可为空,其它传空字符串即可  
            12,  
            "red",  
            "yellow",   
            "bold",   
            '我的',   
            '/static/tab/main3.png',  
            '/static/tab/main3_active.png',  
            24,//图标宽  
            24,//图标高  
            "tabPage" //tabbar 类型 tabPage:tab页面  tabPop:弹窗组件  page:普通页面  
        )  

注:
类型为 tabPop 打开页面 可 右滑手势(当手机开启右滑手势时)关闭、 或点击虚拟返回按键关闭 或 页面中调用方法 closeTabPop()关闭

方法导入 import { pageToTab} from "@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts"


⚠️ 常见问题

Q1: 为什么小程序端需要单独配置?

A: 微信小程序不支持动态组件渲染,必须静态声明所有可能使用的组件。

Q2: 动态 Tab 刷新失败怎么办?

A: 检查以下几点:

  • 组件是否在 tabbar-config.uts 中正确注册
  • 是否正确使用了 GLOBAL_COMPONENT_REGISTRY.get() 获取组件
  • TabItem 的参数是否完整且类型正确

Q3: 导航栏配置不生效?

A: 确保 setNavbarConfig()setDefaultTabItems() 之前调用,或在应用启动时尽早调用。

Q4: 如何设置数字角标?

A: 使用 uni.$emit('setTabBadge', {"key": "tab_key", "badge": number}) 即可,详见第 4 章。

Q5: 角标设置后不显示怎么办?

A: 检查以下几点:

  • badge 值是否大于 0(0 或负数不显示)
  • tab key 是否正确(必须与 TabItem 的 key 一致)
  • 是否重新赋值了整个数组(直接修改对象属性不会触发响应式)

Q6: 退出登录如何清除 tabbar相关缓存

A: 调用 noticetab.uts里的 clearStorage 方法

/**  
 * 清除tabbar相关缓存  
 * all 所有  
 * theme 主题  
 * tabbarItems 所有tabbar  
 * badge 角标  
 */  
export function clearStorage(type:string){  
    switch (type){  
        case "theme":  
            uni.removeStorageSync("sunrains-tabbar-customtheme")      
            break;  
        case "tabbarItems":  
            uni.removeStorageSync("sunrains-tabbar-logintabbaritems")  
            break;    
        case "badge":  
            uni.removeStorageSync("sunrains-tabbar-tabbarbadges")  
            break;    
        default:  
            uni.removeStorageSync("sunrains-tabbar-customtheme")  
            uni.removeStorageSync("sunrains-tabbar-logintabbaritems")  
            uni.removeStorageSync("sunrains-tabbar-tabbarbadges")         
    }  

}

收起阅读 »