HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

搭建三角洲陪玩护航小程序服务号平台需要什么?一站式打包部署支持微信小程序+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")         
    }  

}

收起阅读 »

我们公司找兼职开发,有没有会iOS开发的(跨平台最好),QQ752562065

iOS打包 iOS

我们公司找兼职开发,有没有会iOS开发的(跨平台最好),QQ752562065

我们公司找兼职开发,有没有会iOS开发的(跨平台最好),QQ752562065

苹果马甲包上架被拒 4.3 的原因有哪些?分享一次成功过审的处理经验

移动APP

苹果马甲包上架被拒 4.3 的原因有哪些?分享一次成功过审的处理经验

做过 iOS 上架的人都知道,马甲包最容易遇到的问题就是苹果审核 4.3。

很多开发者认为马甲包就是换个 Logo、改个名称、换套截图重新提交,但实际上现在的苹果审核机制早已经不是几年前的标准了。尤其是近两年,苹果对于相似应用、批量应用以及模板化应用的审核越来越严格,导致大量马甲包项目收到 4.3(a) 或 4.3(b) 的审核反馈。

最近我们处理了一个工具类马甲包项目,从第一次收到 4.3,到最终成功通过审核,整个过程经历了多轮分析和调整。今天结合这次实际案例,聊聊苹果马甲包为什么会收到 4.3,以及有哪些相对有效的处理思路。

什么是苹果审核 4.3?

4.3 属于苹果审核指南中的 Design - Spam 条款。

简单来说,苹果认为当前提交的应用与 App Store 中已有应用过于相似,缺乏独立价值,或者开发者正在批量提交同类型产品。

通常会出现两种情况:

4.3(a):应用本身相似度过高。

4.3(b):开发者批量提交多个类似应用。

而马甲包恰恰是最容易触发这两类审核的问题。

马甲包为什么容易收到 4.3?

产品结构过于相似

这是最常见的原因。

很多马甲包项目本质上就是同一套后台、同一套代码、同一套功能。

例如:

A应用是记账工具;

B应用是生活记账;

C应用是家庭记账。

名称不同,但功能完全一致。

首页布局、功能入口、用户操作路径几乎没有变化。

对于苹果来说,这些产品本质上属于同一个应用。

即使换了图标和启动页,也很容易被判定为相似应用。

UI修改不够彻底

很多开发者认为:

换 Logo;

换颜色;

换截图;

就属于全新产品。

实际上苹果审核团队更关注产品结构。

如果导航栏、首页布局、核心功能入口完全一致,仅修改视觉元素通常很难通过审核。

尤其是一些模板化项目,很容易被识别出相同特征。

代码特征高度重复

很多人认为苹果不会分析代码。

实际上从大量案例来看,苹果不仅看前端界面,还会结合项目资源结构、功能模块以及应用整体特征进行判断。

特别是大量使用同一套源码快速生成多个 App 的项目,更容易出现审核风险。

同账号存在多个同类产品

如果开发者账号下面已经存在:

  • 多个小说 App
  • 多个壁纸 App
  • 多个工具 App

即使新提交的应用进行了部分修改,也有可能触发 4.3(b)。

因为苹果会认为开发者正在利用多个应用覆盖同一业务场景。

我们遇到的案例

前段时间有客户提交一款工具类马甲包。

第一次审核收到 4.3(a)。

客户自行修改了 Logo 和截图后再次提交。

结果第二次审核直接升级为 4.3(b)。

后来我们接手分析后发现,问题并不在素材层面,而是整个产品逻辑与已上线版本几乎完全一致。

首页结构相同;

功能路径相同;

会员体系相同;

甚至帮助中心内容都基本一致。

从审核角度来看,这实际上还是同一个产品。

我们是如何处理的?

重新规划产品定位

首先重新梳理产品场景。

让新版本拥有独立的使用场景,而不是简单复制旧版本。

苹果更愿意接受不同业务方向的产品,而不是同一产品的多次包装。

调整首页和功能结构

重点不是换颜色,而是改变用户使用流程。

包括:

  • 首页模块重组
  • 功能入口调整
  • 用户路径优化
  • 新增独立功能模块

让审核人员打开 App 后能够明显感受到产品差异。

重做应用素材

包括:

  • App Icon
  • 启动页
  • 引导页
  • 预览图
  • 应用描述

避免与历史版本形成明显关联。

完善审核说明

很多开发者忽略审核备注。

实际上对于马甲包项目来说,一份清晰的审核说明非常重要。

需要明确说明:

  • 产品定位
  • 目标用户
  • 核心功能
  • 与历史版本的区别

帮助审核人员理解产品价值。

收到 4.3 后不要做什么?

根据我们的经验,以下几种操作最容易浪费时间。

第一种是收到反馈后直接重新提交。

如果问题没有解决,审核结果通常不会改变。

第二种是只修改 Logo 和名称。

对于现在的审核机制来说,这种调整已经很难解决根本问题。

第三种是连续提交多个版本碰碰运气。

苹果审核记录会长期保存,多次提交相同内容往往只会增加后续审核难度。

总结

苹果马甲包收到 4.3,本质上并不是名称或者 Logo 的问题,而是苹果认为应用缺乏独立价值。

过去通过简单换壳就能上架的时代已经结束,现在苹果更关注产品定位、用户价值、功能差异以及整体体验。

如果收到 4.3(a) 或 4.3(b),建议不要急着重新提交,而是先分析问题来源。重点检查产品结构、功能逻辑、应用场景以及与历史版本的差异程度。

从我们处理过的大量案例来看,真正能够解决 4.3 的项目,往往不是改得最多的,而是差异化做得最彻底的。

对于马甲包来说,审核通过的关键从来不是“换壳”,而是让苹果相信这是一款独立且有价值的新产品。

继续阅读 »

苹果马甲包上架被拒 4.3 的原因有哪些?分享一次成功过审的处理经验

做过 iOS 上架的人都知道,马甲包最容易遇到的问题就是苹果审核 4.3。

很多开发者认为马甲包就是换个 Logo、改个名称、换套截图重新提交,但实际上现在的苹果审核机制早已经不是几年前的标准了。尤其是近两年,苹果对于相似应用、批量应用以及模板化应用的审核越来越严格,导致大量马甲包项目收到 4.3(a) 或 4.3(b) 的审核反馈。

最近我们处理了一个工具类马甲包项目,从第一次收到 4.3,到最终成功通过审核,整个过程经历了多轮分析和调整。今天结合这次实际案例,聊聊苹果马甲包为什么会收到 4.3,以及有哪些相对有效的处理思路。

什么是苹果审核 4.3?

4.3 属于苹果审核指南中的 Design - Spam 条款。

简单来说,苹果认为当前提交的应用与 App Store 中已有应用过于相似,缺乏独立价值,或者开发者正在批量提交同类型产品。

通常会出现两种情况:

4.3(a):应用本身相似度过高。

4.3(b):开发者批量提交多个类似应用。

而马甲包恰恰是最容易触发这两类审核的问题。

马甲包为什么容易收到 4.3?

产品结构过于相似

这是最常见的原因。

很多马甲包项目本质上就是同一套后台、同一套代码、同一套功能。

例如:

A应用是记账工具;

B应用是生活记账;

C应用是家庭记账。

名称不同,但功能完全一致。

首页布局、功能入口、用户操作路径几乎没有变化。

对于苹果来说,这些产品本质上属于同一个应用。

即使换了图标和启动页,也很容易被判定为相似应用。

UI修改不够彻底

很多开发者认为:

换 Logo;

换颜色;

换截图;

就属于全新产品。

实际上苹果审核团队更关注产品结构。

如果导航栏、首页布局、核心功能入口完全一致,仅修改视觉元素通常很难通过审核。

尤其是一些模板化项目,很容易被识别出相同特征。

代码特征高度重复

很多人认为苹果不会分析代码。

实际上从大量案例来看,苹果不仅看前端界面,还会结合项目资源结构、功能模块以及应用整体特征进行判断。

特别是大量使用同一套源码快速生成多个 App 的项目,更容易出现审核风险。

同账号存在多个同类产品

如果开发者账号下面已经存在:

  • 多个小说 App
  • 多个壁纸 App
  • 多个工具 App

即使新提交的应用进行了部分修改,也有可能触发 4.3(b)。

因为苹果会认为开发者正在利用多个应用覆盖同一业务场景。

我们遇到的案例

前段时间有客户提交一款工具类马甲包。

第一次审核收到 4.3(a)。

客户自行修改了 Logo 和截图后再次提交。

结果第二次审核直接升级为 4.3(b)。

后来我们接手分析后发现,问题并不在素材层面,而是整个产品逻辑与已上线版本几乎完全一致。

首页结构相同;

功能路径相同;

会员体系相同;

甚至帮助中心内容都基本一致。

从审核角度来看,这实际上还是同一个产品。

我们是如何处理的?

重新规划产品定位

首先重新梳理产品场景。

让新版本拥有独立的使用场景,而不是简单复制旧版本。

苹果更愿意接受不同业务方向的产品,而不是同一产品的多次包装。

调整首页和功能结构

重点不是换颜色,而是改变用户使用流程。

包括:

  • 首页模块重组
  • 功能入口调整
  • 用户路径优化
  • 新增独立功能模块

让审核人员打开 App 后能够明显感受到产品差异。

重做应用素材

包括:

  • App Icon
  • 启动页
  • 引导页
  • 预览图
  • 应用描述

避免与历史版本形成明显关联。

完善审核说明

很多开发者忽略审核备注。

实际上对于马甲包项目来说,一份清晰的审核说明非常重要。

需要明确说明:

  • 产品定位
  • 目标用户
  • 核心功能
  • 与历史版本的区别

帮助审核人员理解产品价值。

收到 4.3 后不要做什么?

根据我们的经验,以下几种操作最容易浪费时间。

第一种是收到反馈后直接重新提交。

如果问题没有解决,审核结果通常不会改变。

第二种是只修改 Logo 和名称。

对于现在的审核机制来说,这种调整已经很难解决根本问题。

第三种是连续提交多个版本碰碰运气。

苹果审核记录会长期保存,多次提交相同内容往往只会增加后续审核难度。

总结

苹果马甲包收到 4.3,本质上并不是名称或者 Logo 的问题,而是苹果认为应用缺乏独立价值。

过去通过简单换壳就能上架的时代已经结束,现在苹果更关注产品定位、用户价值、功能差异以及整体体验。

如果收到 4.3(a) 或 4.3(b),建议不要急着重新提交,而是先分析问题来源。重点检查产品结构、功能逻辑、应用场景以及与历史版本的差异程度。

从我们处理过的大量案例来看,真正能够解决 4.3 的项目,往往不是改得最多的,而是差异化做得最彻底的。

对于马甲包来说,审核通过的关键从来不是“换壳”,而是让苹果相信这是一款独立且有价值的新产品。

收起阅读 »

uni-app中app(安卓端实测)中<editor>组件光标使用按钮插入特定符号(如【】)时定位到特定字符中间的可行方案

editor

uni-app app端(安卓实测)<editor>组件光标使用按钮插入特定符号(如【】)时定位到特定字符中间的可行方案

因为在做文本编辑器(符号快捷成对(如【】这种的)输入)需用到editor组件。
因为不能直接使用DOM,因网上搜不出解决方案,借助AI后实测出一种方案和一种待验证方案。
思路简单得离谱(主要还是对edtior这个组件不了解):

首先说一下editor

<editor> 组件底层是基于 Quill.js,所以不能使用原生输入框的 selectionStart/setSelectionRange。光标操作只能通过 Quill API 或标准 DOM API 实现。

方案一:Quill API(推荐这种)

获取 Quill 实例

const editor = document.getElementById('editor')  
const quill = editor.__quill || editor.querySelector('.ql-editor')?.__quill

核心示例:插入配对符号并定位光标

例如:一个按钮直接插入【】,你需要光标在【】的中间

const range = quill.getSelection()  
const pos = range ? range.index : 0  
const half = Math.ceil(pair.length / 2)  
quill.insertText(pos, pair)  
quill.setSelection(pos + half)

优缺点

优点

  • API 简洁高效
  • 与编辑器状态(Delta)完全同步
  • 不影响撤销/重做

缺点

  • 依赖 Quill 内部属性 __quill
  • 不同版本 uni-app 兼容性不确定
  • 需在 renderjs 中运行

方案二:原生 DOM API(降级方案:这个未验证,因为第一种方案基本解决,这是ai给的降级兜底方案)

核心思路

使用 document.createTreeWalker 遍历文本节点计算全局偏移,通过 Range + Selection API 操作光标。

获取光标全局偏移

function getCursorIndex(container) {  
    const sel = window.getSelection()  
    if (!sel?.rangeCount) return -1  
    const node = sel.anchorNode, off = sel.anchorOffset  
    if (!node || !container.contains(node)) return -1  
    const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT)  
    let count = 0  
    while (walker.nextNode()) {  
        if (walker.currentNode === node) return count + off  
        count += walker.currentNode.textContent.length  
    }  
    return count  
}

设置光标到指定位置

function setCursorIndex(container, index) {  
    const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT)  
    let count = 0  
    while (walker.nextNode()) {  
        const len = walker.currentNode.textContent.length  
        if (count + len >= index) {  
            const range = document.createRange()  
            range.setStart(walker.currentNode, index - count)  
            range.collapse(true)  
            const sel = window.getSelection()  
            sel.removeAllRanges()  
            sel.addRange(range)  
            return  
        }  
        count += len  
    }  
}

插入文本并设置光标

const text = foundNode.textContent  
foundNode.textContent = text.slice(0, offset) + pair + text.slice(offset)  
const range = document.createRange()  
range.setStart(foundNode, offset + half)  
range.collapse(true)  
window.getSelection().removeAllRanges()  
window.getSelection().addRange(range)

优缺点

优点

  • 纯标准 DOM API,兼容性最好
  • 不依赖任何框架内部属性

缺点

  • 代码较复杂
  • 直接改 textContent 可能破坏 Quill Delta 同步
  • 富文本元素(图片等)偏移计算需额外处理

两种方案对比

代码量

  • Quill API:3 行
  • DOM API:30+ 行
  • Delta 同步

  • Quill API:✅ 完全同步
  • DOM API:❌ 可能不同步

撤销/重做

  • Quill API:✅ 正常
  • DOM API:❌ 可能受影响

WebView 兼容性

  • Quill API:⭐⭐⭐(依赖 Quill 版本)
  • DOM API:⭐⭐⭐⭐⭐

uni-app App 端实践要点

  • 逻辑层无 DOM,所有 DOM 操作必须通过 renderjs 执行
  • renderjs 通过 change:prop 模板绑定接收逻辑层数据,通过 ownerVm.callMethod() 回传状态
  • 方法签名:methodName(newVal, oldVal, ownerVm, vm)
  • 推荐双方案:优先 Quill API → 失败则降级 DOM API
继续阅读 »

uni-app app端(安卓实测)<editor>组件光标使用按钮插入特定符号(如【】)时定位到特定字符中间的可行方案

因为在做文本编辑器(符号快捷成对(如【】这种的)输入)需用到editor组件。
因为不能直接使用DOM,因网上搜不出解决方案,借助AI后实测出一种方案和一种待验证方案。
思路简单得离谱(主要还是对edtior这个组件不了解):

首先说一下editor

<editor> 组件底层是基于 Quill.js,所以不能使用原生输入框的 selectionStart/setSelectionRange。光标操作只能通过 Quill API 或标准 DOM API 实现。

方案一:Quill API(推荐这种)

获取 Quill 实例

const editor = document.getElementById('editor')  
const quill = editor.__quill || editor.querySelector('.ql-editor')?.__quill

核心示例:插入配对符号并定位光标

例如:一个按钮直接插入【】,你需要光标在【】的中间

const range = quill.getSelection()  
const pos = range ? range.index : 0  
const half = Math.ceil(pair.length / 2)  
quill.insertText(pos, pair)  
quill.setSelection(pos + half)

优缺点

优点

  • API 简洁高效
  • 与编辑器状态(Delta)完全同步
  • 不影响撤销/重做

缺点

  • 依赖 Quill 内部属性 __quill
  • 不同版本 uni-app 兼容性不确定
  • 需在 renderjs 中运行

方案二:原生 DOM API(降级方案:这个未验证,因为第一种方案基本解决,这是ai给的降级兜底方案)

核心思路

使用 document.createTreeWalker 遍历文本节点计算全局偏移,通过 Range + Selection API 操作光标。

获取光标全局偏移

function getCursorIndex(container) {  
    const sel = window.getSelection()  
    if (!sel?.rangeCount) return -1  
    const node = sel.anchorNode, off = sel.anchorOffset  
    if (!node || !container.contains(node)) return -1  
    const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT)  
    let count = 0  
    while (walker.nextNode()) {  
        if (walker.currentNode === node) return count + off  
        count += walker.currentNode.textContent.length  
    }  
    return count  
}

设置光标到指定位置

function setCursorIndex(container, index) {  
    const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT)  
    let count = 0  
    while (walker.nextNode()) {  
        const len = walker.currentNode.textContent.length  
        if (count + len >= index) {  
            const range = document.createRange()  
            range.setStart(walker.currentNode, index - count)  
            range.collapse(true)  
            const sel = window.getSelection()  
            sel.removeAllRanges()  
            sel.addRange(range)  
            return  
        }  
        count += len  
    }  
}

插入文本并设置光标

const text = foundNode.textContent  
foundNode.textContent = text.slice(0, offset) + pair + text.slice(offset)  
const range = document.createRange()  
range.setStart(foundNode, offset + half)  
range.collapse(true)  
window.getSelection().removeAllRanges()  
window.getSelection().addRange(range)

优缺点

优点

  • 纯标准 DOM API,兼容性最好
  • 不依赖任何框架内部属性

缺点

  • 代码较复杂
  • 直接改 textContent 可能破坏 Quill Delta 同步
  • 富文本元素(图片等)偏移计算需额外处理

两种方案对比

代码量

  • Quill API:3 行
  • DOM API:30+ 行
  • Delta 同步

  • Quill API:✅ 完全同步
  • DOM API:❌ 可能不同步

撤销/重做

  • Quill API:✅ 正常
  • DOM API:❌ 可能受影响

WebView 兼容性

  • Quill API:⭐⭐⭐(依赖 Quill 版本)
  • DOM API:⭐⭐⭐⭐⭐

uni-app App 端实践要点

  • 逻辑层无 DOM,所有 DOM 操作必须通过 renderjs 执行
  • renderjs 通过 change:prop 模板绑定接收逻辑层数据,通过 ownerVm.callMethod() 回传状态
  • 方法签名:methodName(newVal, oldVal, ownerVm, vm)
  • 推荐双方案:优先 Quill API → 失败则降级 DOM API
收起阅读 »

苹果审核被拒 5.2.3 怎么办?分享一次真实项目成功过审经历

应用上架

最近在处理一个客户的 App Store 上架项目时,遇到了苹果审核反馈的 5.2.3 问题。很多开发者第一次收到 5.2.3 都会比较疑惑,因为应用功能正常、没有崩溃、也没有明显违规内容,但依然被苹果拒绝审核。  

实际上,5.2.3 主要涉及知识产权、品牌授权、内容来源以及相关资质证明问题。如果处理不当,不仅会导致应用长期无法上架,还可能增加开发者账号后续审核风险。  

今天结合这次真实项目的处理经历,分享一下我们是如何解决 5.2.3 并最终审核通过的。  

收到的审核反馈  

客户提交的是一款工具类应用,第一次提交审核后,很快收到了苹果的 5.2.3 反馈。  

审核内容大致意思是:  

苹果无法确认应用中涉及的部分内容、功能描述或者第三方相关资源是否获得合法授权,希望开发者提供对应的证明材料。  

收到反馈后,客户最开始认为只是普通审核问题,计划直接再次提交碰碰运气。  

但从我们多年的上架经验来看,这种处理方式风险非常高。  

因为苹果审核记录是长期保留的,同一个问题如果没有得到解决,仅仅修改版本号或者简单回复几句话再次提交,审核结果通常不会发生变化。  

我们是如何排查问题的  

收到反馈后,我们第一时间没有选择重新提交,而是对整个项目进行了全面检查。  

经过排查发现,应用介绍中涉及部分第三方品牌名称,同时后台内容来源也无法直接向苹果证明拥有合法授权关系。  

从开发者角度来看,这些内容可能没有实际侵权行为,但对于审核人员来说,如果无法确认内容来源是否合法,就有可能触发 5.2.3。  

因此问题的核心并不是应用能不能运行,而是开发者能否证明拥有合法使用权限。  

我们做了哪些调整  

确定问题方向后,我们主要进行了三个方面的处理。  

首先,重新整理了应用涉及的所有内容来源,并删除了部分容易引起审核误解的第三方描述内容。  

其次,补充了企业主体资料,包括官方网站、备案信息以及相关业务资质证明。  

最后,针对苹果提出的问题准备了一份完整的审核说明文件,详细解释应用功能来源、内容来源以及运营主体信息。  

同时在应用内部增加了版权声明、内容来源说明以及用户协议相关条款。  

完成以上调整后,我们再次提交审核。  

两天后,应用顺利通过审核。  

为什么很多开发者一直过不了 5.2.3  

在实际处理过程中,我们发现很多开发者收到 5.2.3 后都会犯一个共同错误。  

那就是没有解决问题就反复提交审核。  

例如:  

第一次被拒;  

修改一句审核备注;  

再次提交;  

继续被拒;  

然后换个审核说明继续提交。  

实际上,这种方式往往只会不断累积审核记录。  

苹果审核团队每天会看到应用的历史审核记录,如果多次提交的版本没有实质性变化,审核人员通常会认为开发者并没有解决问题。  

对于涉及知识产权、版权归属、品牌授权等问题的应用来说,反复提交并不会提高通过率。  

相反,可能会导致应用进入更严格的人工审核流程。  

没有资质证明,不建议继续提交  

这里也是很多开发者最容易忽略的一点。  

如果苹果明确要求提供:  

* 商标授权书  
* 内容授权协议  
* 品牌合作证明  
* 版权证明材料  
* 相关经营资质  

而开发者暂时无法提供这些材料,那么建议先暂停提交审核。  

因为苹果关注的重点不是开发者的解释,而是实际证明文件。  

如果无法提供对应资质,即使连续提交十次,审核团队也很难改变判断结果。  

根据我们的实际经验,有部分开发者在没有解决问题的情况下连续提交多个版本,最终导致账号审核周期明显变长,后续版本审核难度也不断增加。  

因此,收到 5.2.3 后最正确的处理方式并不是立即重新提交,而是先确认苹果需要什么材料,再准备对应证明文件。  

我们最终提交给苹果的说明  

在重新提交审核时,我们向苹果提供了完整说明,包括:  

应用运营主体信息;  

官方网站及备案信息;  

内容来源说明;  

版权声明;  

授权证明材料;  

相关业务资质文件。  

通过这些资料,审核团队能够明确确认应用内容来源合法,最终顺利通过审核。  

总结  

苹果审核 5.2.3 本质上属于知识产权和授权合规问题。  

很多开发者认为只要应用没有侵权就一定能够通过审核,但实际上苹果更关注的是开发者是否能够提供对应证明材料。  

如果收到 5.2.3,不建议盲目反复提交审核。尤其是在无法提供授权证明、版权证明或者相关资质文件的情况下,多次提交往往无法解决问题,反而可能增加后续审核风险。  

正确的做法应该是先定位问题来源,准备完整的授权材料和资质证明,待问题真正解决后再重新提交审核。这样不仅能够提高审核通过率,也有助于开发者账号长期稳定运营。  

从我们实际处理的案例来看,大部分 5.2.3 问题最终都能够解决,关键并不是提交次数,而是是否真正准备好了苹果需要看到的证明材料。   

继续阅读 »

最近在处理一个客户的 App Store 上架项目时,遇到了苹果审核反馈的 5.2.3 问题。很多开发者第一次收到 5.2.3 都会比较疑惑,因为应用功能正常、没有崩溃、也没有明显违规内容,但依然被苹果拒绝审核。  

实际上,5.2.3 主要涉及知识产权、品牌授权、内容来源以及相关资质证明问题。如果处理不当,不仅会导致应用长期无法上架,还可能增加开发者账号后续审核风险。  

今天结合这次真实项目的处理经历,分享一下我们是如何解决 5.2.3 并最终审核通过的。  

收到的审核反馈  

客户提交的是一款工具类应用,第一次提交审核后,很快收到了苹果的 5.2.3 反馈。  

审核内容大致意思是:  

苹果无法确认应用中涉及的部分内容、功能描述或者第三方相关资源是否获得合法授权,希望开发者提供对应的证明材料。  

收到反馈后,客户最开始认为只是普通审核问题,计划直接再次提交碰碰运气。  

但从我们多年的上架经验来看,这种处理方式风险非常高。  

因为苹果审核记录是长期保留的,同一个问题如果没有得到解决,仅仅修改版本号或者简单回复几句话再次提交,审核结果通常不会发生变化。  

我们是如何排查问题的  

收到反馈后,我们第一时间没有选择重新提交,而是对整个项目进行了全面检查。  

经过排查发现,应用介绍中涉及部分第三方品牌名称,同时后台内容来源也无法直接向苹果证明拥有合法授权关系。  

从开发者角度来看,这些内容可能没有实际侵权行为,但对于审核人员来说,如果无法确认内容来源是否合法,就有可能触发 5.2.3。  

因此问题的核心并不是应用能不能运行,而是开发者能否证明拥有合法使用权限。  

我们做了哪些调整  

确定问题方向后,我们主要进行了三个方面的处理。  

首先,重新整理了应用涉及的所有内容来源,并删除了部分容易引起审核误解的第三方描述内容。  

其次,补充了企业主体资料,包括官方网站、备案信息以及相关业务资质证明。  

最后,针对苹果提出的问题准备了一份完整的审核说明文件,详细解释应用功能来源、内容来源以及运营主体信息。  

同时在应用内部增加了版权声明、内容来源说明以及用户协议相关条款。  

完成以上调整后,我们再次提交审核。  

两天后,应用顺利通过审核。  

为什么很多开发者一直过不了 5.2.3  

在实际处理过程中,我们发现很多开发者收到 5.2.3 后都会犯一个共同错误。  

那就是没有解决问题就反复提交审核。  

例如:  

第一次被拒;  

修改一句审核备注;  

再次提交;  

继续被拒;  

然后换个审核说明继续提交。  

实际上,这种方式往往只会不断累积审核记录。  

苹果审核团队每天会看到应用的历史审核记录,如果多次提交的版本没有实质性变化,审核人员通常会认为开发者并没有解决问题。  

对于涉及知识产权、版权归属、品牌授权等问题的应用来说,反复提交并不会提高通过率。  

相反,可能会导致应用进入更严格的人工审核流程。  

没有资质证明,不建议继续提交  

这里也是很多开发者最容易忽略的一点。  

如果苹果明确要求提供:  

* 商标授权书  
* 内容授权协议  
* 品牌合作证明  
* 版权证明材料  
* 相关经营资质  

而开发者暂时无法提供这些材料,那么建议先暂停提交审核。  

因为苹果关注的重点不是开发者的解释,而是实际证明文件。  

如果无法提供对应资质,即使连续提交十次,审核团队也很难改变判断结果。  

根据我们的实际经验,有部分开发者在没有解决问题的情况下连续提交多个版本,最终导致账号审核周期明显变长,后续版本审核难度也不断增加。  

因此,收到 5.2.3 后最正确的处理方式并不是立即重新提交,而是先确认苹果需要什么材料,再准备对应证明文件。  

我们最终提交给苹果的说明  

在重新提交审核时,我们向苹果提供了完整说明,包括:  

应用运营主体信息;  

官方网站及备案信息;  

内容来源说明;  

版权声明;  

授权证明材料;  

相关业务资质文件。  

通过这些资料,审核团队能够明确确认应用内容来源合法,最终顺利通过审核。  

总结  

苹果审核 5.2.3 本质上属于知识产权和授权合规问题。  

很多开发者认为只要应用没有侵权就一定能够通过审核,但实际上苹果更关注的是开发者是否能够提供对应证明材料。  

如果收到 5.2.3,不建议盲目反复提交审核。尤其是在无法提供授权证明、版权证明或者相关资质文件的情况下,多次提交往往无法解决问题,反而可能增加后续审核风险。  

正确的做法应该是先定位问题来源,准备完整的授权材料和资质证明,待问题真正解决后再重新提交审核。这样不仅能够提高审核通过率,也有助于开发者账号长期稳定运营。  

从我们实际处理的案例来看,大部分 5.2.3 问题最终都能够解决,关键并不是提交次数,而是是否真正准备好了苹果需要看到的证明材料。   

收起阅读 »

苹果上架被拒 4.3(a)、4.3(b) 怎么办?分享一次成功过审的处理经验

uni-app 移动APP 苹果内购 苹果审核 应用上架

最近帮一个客户处理 App Store 上架问题时,连续遇到了苹果审核反馈的 4.3(a) 和 4.3(b)。最开始客户认为只是简单修改一下 Logo 和应用名称就能解决,但连续提交两次后依然被拒。经过一轮完整排查和调整后,最终顺利通过审核。今天就结合这次实际案例,聊聊苹果 4.3(a)、4.3(b) 的审核逻辑以及我们最终的处理思路,希望能给正在上架的开发者一些参考。  

## 苹果 4.3(a) 和 4.3(b) 到底是什么?  

很多开发者第一次看到 4.3 的时候都会比较懵,因为苹果给出的反馈内容通常比较笼统。但实际上,4.3(a) 和 4.3(b) 虽然同属于 Design - Spam 范畴,关注的重点却并不完全一样。  

4.3(a) 更多针对应用本身。苹果认为当前提交的 App 与 App Store 中已有应用存在较高相似度,缺乏独立产品价值。简单来说,就是审核团队觉得你的应用像是另一个应用的复制版本,即使更换了图标、名称或者启动页,也无法体现明显差异。  

而 4.3(b) 则更偏向于开发者维度。苹果会认为开发者正在批量提交多个功能类似的应用,从而试图占据更多搜索结果或榜单位置。这种情况经常出现在马甲包项目、矩阵项目或者同类型产品批量上架的场景中。  

## 本次项目遇到的问题  

这次客户提交的是一款工具类应用,功能本身没有违规内容,隐私协议和用户协议也已经配置完整。但提交审核后,很快收到了苹果反馈的 4.3(a)。  

最开始客户按照网上很多文章的做法,仅修改了应用图标、应用名称以及部分截图素材,然后再次提交审核。然而第二次审核结果并没有变化,依然被苹果拒绝。  

在进一步分析项目后,我们发现问题并不是出现在 Logo 或截图上,而是整个产品结构与市场上已有同类应用过于接近。从首页布局到导航方式,再到用户操作流程,都能够找到大量相似案例。  

后来苹果又补充反馈了 4.3(b),说明审核团队已经开始从开发者维度评估项目,而不仅仅是应用本身。  

## 我们是如何排查问题的?  

收到 4.3 之后,最忌讳的事情就是盲目重复提交。因为每一次提交都会留下审核记录,如果问题没有真正解决,多次拒审反而会增加后续审核难度。  

我们的第一步是先对项目进行相似度分析,而不是直接修改代码。通过对应用页面结构、资源文件以及整体业务流程进行检查后,发现应用存在几个明显问题。  

首先是首页布局与市场上同类产品高度接近。虽然颜色和图标不同,但整体模块排列方式基本一致。其次是功能入口设计过于模板化,用户打开 App 后看到的核心内容与多个竞品非常相似。最后是项目使用了一套较为常见的开发模板,代码结构和资源组织方式也存在明显的模板特征。  

这些问题单独来看可能并不严重,但叠加在一起后,就容易被苹果判定为缺乏独立性。  

## 最终做了哪些调整?  

确定问题方向后,我们并没有选择简单改几个页面,而是从产品层面重新梳理差异化方案。  

首先调整了首页架构。将原本传统的功能导航模式重新设计,增加了新的内容展示区域,并改变了用户进入核心功能的路径。这样做的目的并不是单纯修改界面,而是让整个产品体验与原有方案形成明显区别。  

其次针对部分核心功能进行了重新包装。很多开发者喜欢直接使用行业通用名称,但实际上苹果更关注产品价值。因此我们重新定义了部分功能模块的定位,并增加了一些辅助功能,使整体产品逻辑更加完整。  

资源文件方面也进行了全面优化,包括启动页、引导页、截图素材以及部分图标资源。虽然这些内容不是审核重点,但能够帮助审核人员更直观地感受到产品差异化。  

最后针对代码层面进行了整理和优化。对于一些模板化特征明显的项目结构进行了调整,同时优化了部分资源引用方式,降低项目整体重复特征。  

## 为什么很多项目改了 UI 还是过不了?  

这是很多开发者最容易踩的坑。  

不少文章会告诉你收到 4.3 后换个 Logo、改个颜色、换套截图就可以重新提交,但实际上现在的苹果审核已经远不止看界面这么简单。  

苹果会综合分析应用的产品定位、功能结构、页面逻辑、资源文件甚至历史提交记录。如果一个项目本质上还是原来的产品,只是换了一层外观,那么审核团队依然有可能认为这是同一类应用。  

尤其是近几年 AI 编程工具普及之后,大量项目使用类似框架和模板开发,导致整体结构越来越接近。这也是为什么很多开发者明明修改了界面,却依然收到 4.3 的原因。  

## 关于 4.3(b) 的一些经验  

如果收到的是 4.3(b),除了应用本身之外,还需要关注提交环境和账号关联问题。  

苹果会记录开发者账号、证书信息、提交设备以及历史审核记录。如果短时间内连续提交多个功能类似的应用,即使应用本身没有明显问题,也有可能被认定为批量提交相似产品。  

因此在规划产品矩阵时,不建议采用简单复制的方式进行上架。不同应用之间最好具备独立的业务定位、独立的功能价值以及相对独立的运营方向,而不是简单更换名称和图标。  

## 总结  

从这次项目的处理结果来看,苹果 4.3(a) 和 4.3(b) 并不是单纯的代码问题,也不是简单修改 UI 就能够彻底解决的问题。真正影响审核结果的,往往是产品本身是否具备独立价值,以及苹果是否认可这是一款全新的应用。  

如果收到 4.3 反馈,建议先分析问题来源,再针对产品结构、功能设计、资源素材和代码特征进行系统调整,而不是盲目重复提交。很多时候,方向找对了,一次调整就能顺利通过审核;方向错了,即使提交十次,结果也不会发生太大变化。  

希望这次真实项目的处理经验,能够帮助正在被 4.3 困扰的开发者少走一些弯路。  

###最后审核通过的截图  

继续阅读 »

最近帮一个客户处理 App Store 上架问题时,连续遇到了苹果审核反馈的 4.3(a) 和 4.3(b)。最开始客户认为只是简单修改一下 Logo 和应用名称就能解决,但连续提交两次后依然被拒。经过一轮完整排查和调整后,最终顺利通过审核。今天就结合这次实际案例,聊聊苹果 4.3(a)、4.3(b) 的审核逻辑以及我们最终的处理思路,希望能给正在上架的开发者一些参考。  

## 苹果 4.3(a) 和 4.3(b) 到底是什么?  

很多开发者第一次看到 4.3 的时候都会比较懵,因为苹果给出的反馈内容通常比较笼统。但实际上,4.3(a) 和 4.3(b) 虽然同属于 Design - Spam 范畴,关注的重点却并不完全一样。  

4.3(a) 更多针对应用本身。苹果认为当前提交的 App 与 App Store 中已有应用存在较高相似度,缺乏独立产品价值。简单来说,就是审核团队觉得你的应用像是另一个应用的复制版本,即使更换了图标、名称或者启动页,也无法体现明显差异。  

而 4.3(b) 则更偏向于开发者维度。苹果会认为开发者正在批量提交多个功能类似的应用,从而试图占据更多搜索结果或榜单位置。这种情况经常出现在马甲包项目、矩阵项目或者同类型产品批量上架的场景中。  

## 本次项目遇到的问题  

这次客户提交的是一款工具类应用,功能本身没有违规内容,隐私协议和用户协议也已经配置完整。但提交审核后,很快收到了苹果反馈的 4.3(a)。  

最开始客户按照网上很多文章的做法,仅修改了应用图标、应用名称以及部分截图素材,然后再次提交审核。然而第二次审核结果并没有变化,依然被苹果拒绝。  

在进一步分析项目后,我们发现问题并不是出现在 Logo 或截图上,而是整个产品结构与市场上已有同类应用过于接近。从首页布局到导航方式,再到用户操作流程,都能够找到大量相似案例。  

后来苹果又补充反馈了 4.3(b),说明审核团队已经开始从开发者维度评估项目,而不仅仅是应用本身。  

## 我们是如何排查问题的?  

收到 4.3 之后,最忌讳的事情就是盲目重复提交。因为每一次提交都会留下审核记录,如果问题没有真正解决,多次拒审反而会增加后续审核难度。  

我们的第一步是先对项目进行相似度分析,而不是直接修改代码。通过对应用页面结构、资源文件以及整体业务流程进行检查后,发现应用存在几个明显问题。  

首先是首页布局与市场上同类产品高度接近。虽然颜色和图标不同,但整体模块排列方式基本一致。其次是功能入口设计过于模板化,用户打开 App 后看到的核心内容与多个竞品非常相似。最后是项目使用了一套较为常见的开发模板,代码结构和资源组织方式也存在明显的模板特征。  

这些问题单独来看可能并不严重,但叠加在一起后,就容易被苹果判定为缺乏独立性。  

## 最终做了哪些调整?  

确定问题方向后,我们并没有选择简单改几个页面,而是从产品层面重新梳理差异化方案。  

首先调整了首页架构。将原本传统的功能导航模式重新设计,增加了新的内容展示区域,并改变了用户进入核心功能的路径。这样做的目的并不是单纯修改界面,而是让整个产品体验与原有方案形成明显区别。  

其次针对部分核心功能进行了重新包装。很多开发者喜欢直接使用行业通用名称,但实际上苹果更关注产品价值。因此我们重新定义了部分功能模块的定位,并增加了一些辅助功能,使整体产品逻辑更加完整。  

资源文件方面也进行了全面优化,包括启动页、引导页、截图素材以及部分图标资源。虽然这些内容不是审核重点,但能够帮助审核人员更直观地感受到产品差异化。  

最后针对代码层面进行了整理和优化。对于一些模板化特征明显的项目结构进行了调整,同时优化了部分资源引用方式,降低项目整体重复特征。  

## 为什么很多项目改了 UI 还是过不了?  

这是很多开发者最容易踩的坑。  

不少文章会告诉你收到 4.3 后换个 Logo、改个颜色、换套截图就可以重新提交,但实际上现在的苹果审核已经远不止看界面这么简单。  

苹果会综合分析应用的产品定位、功能结构、页面逻辑、资源文件甚至历史提交记录。如果一个项目本质上还是原来的产品,只是换了一层外观,那么审核团队依然有可能认为这是同一类应用。  

尤其是近几年 AI 编程工具普及之后,大量项目使用类似框架和模板开发,导致整体结构越来越接近。这也是为什么很多开发者明明修改了界面,却依然收到 4.3 的原因。  

## 关于 4.3(b) 的一些经验  

如果收到的是 4.3(b),除了应用本身之外,还需要关注提交环境和账号关联问题。  

苹果会记录开发者账号、证书信息、提交设备以及历史审核记录。如果短时间内连续提交多个功能类似的应用,即使应用本身没有明显问题,也有可能被认定为批量提交相似产品。  

因此在规划产品矩阵时,不建议采用简单复制的方式进行上架。不同应用之间最好具备独立的业务定位、独立的功能价值以及相对独立的运营方向,而不是简单更换名称和图标。  

## 总结  

从这次项目的处理结果来看,苹果 4.3(a) 和 4.3(b) 并不是单纯的代码问题,也不是简单修改 UI 就能够彻底解决的问题。真正影响审核结果的,往往是产品本身是否具备独立价值,以及苹果是否认可这是一款全新的应用。  

如果收到 4.3 反馈,建议先分析问题来源,再针对产品结构、功能设计、资源素材和代码特征进行系统调整,而不是盲目重复提交。很多时候,方向找对了,一次调整就能顺利通过审核;方向错了,即使提交十次,结果也不会发生太大变化。  

希望这次真实项目的处理经验,能够帮助正在被 4.3 困扰的开发者少走一些弯路。  

###最后审核通过的截图  

收起阅读 »

客户端网络请求(SM2签名,SM4加密)与java服务端之前加密通信

sunrains-request

注:客户端与服务端之间通信 数据 加密,签名,保证数据安全与防串改(完整功能依赖付费插件sunrains-smutil)

客户端与服务端(java)之间通信数据加密、签名,保证数据安全与防篡改
支持平台:微信小程序、H5、Android、iOS、鸿蒙
请求方式支持:GET、POST(application/json、application/x-www-form-urlencoded、multipart/form-data(计划中))

服务端(java)点击 sunrains-java


模块目录结构

sunrains-request/  
├── index.uts                          # 模块入口,统一导出  
├── js_sdk/  
│   ├── inutils/  
│   │   └── requestApi.uts             # 底层网络请求封装(uni.request / uni.uploadFile)  
│   └── openutil/  
│       ├── base64Util.uts             # Base64 编解码工具  
│       ├── randomUtil.uts             # 随机字符串生成  
│       └── request/  
│           ├── Request.uts            # 请求入口(ReqApi)  
│           └── reqHandle/  
│               ├── ReqHandle.uts      # 请求处理抽象基类  
│               ├── handle.uts         # 策略工厂(根据 reqFlag 选择 Handler)  
│               └── handle/  
│                   ├── VoidHandle.uts           # 无加密无签名模式  
│                   ├── SignHandle.uts           # 仅签名模式  
│                   ├── EncryptHandle.uts        # 仅SM4加密模式  
│                   └── SignWithEncryptHandle.uts # 签名+SM4加密模式

导出 API 一览

导出函数 来源文件 说明
ReqApi request/Request.uts 高层请求入口,自动根据 reqFlag 完成签名/加密/发送/解密
RequestApi inutils/requestApi.uts 底层请求入口,直接发起 uni.request 并返回 Promise\<Response>
uploadFile inutils/requestApi.uts 文件上传,返回 Promise\<Response>
stringToBase64 openutil/base64Util.uts 字符串 → Base64 编码
base64ToString openutil/base64Util.uts Base64 → 字符串解码
random openutil/randomUtil.uts 生成指定长度的随机字符串(字母+数字),默认16位

网络请求架构(核心)

整体流程

调用方  
  │  
  ▼  
ReqApi(frontVo: FrontRequestVo)          ← 高层入口  
  │  
  ├─ 1. selectHandle(reqFlag)            ← 根据模式选择 Handler  
  │  
  ├─ 2. handle.handleHeaderAndBody(vo)   ← 构造请求头 + 处理请求体(签名/加密)  
  │  
  ├─ 3. RequestApi(req)                  ← 底层发起 HTTP 请求  
  │  
  └─ 4. handle.handleRes(req, res)       ← 处理响应(解密/验签)  
  │  
  ▼  
返回 Response

四种请求模式(ReqFlag)

通过 FrontRequestVo.reqFlag 指定,决定请求的安全处理策略:

reqFlag Handler 签名 请求体SM4加密 响应SM4解密 需要priKey 需要pubKey
"void" VoidHandle
"sign" SignHandle ✓ SM2签名
"sm4Encrypt" EncryptHandle ✓ SM4加密 ✓ SM4解密
"signwithenSM2-Sm4Encrypt" SignWithEncryptHandle ✓ SM2签名 ✓ SM4加密 ✓ SM4解密

请求头处理(所有模式共用)

无论哪种模式,handleSignHeader 都会执行以下操作:

  1. SM2加密SM4密钥:生成随机 SM4 key + iv,用服务端公钥(pubKey)通过 SM2 加密后放入 header["key"]
  2. 条件签名(sign / signwithenSM2-Sm4Encrypt 模式):
    • 生成 timestamp(时间戳)和 nonceStr(16位随机串)
    • 将所有业务参数 + timestamp + nonceStr 按 key 字典序排列,拼接为 key=value& 格式
    • 使用客户端私钥(priKey)对拼接字符串进行 SM2 签名
    • 签名结果放入 header["sign"],同时放入 header["timestamp"]header["nonceStr"]

请求体处理(按 HTTP Method + contentType)

GET 请求:

  • void / sign 模式:参数拼接到 URL query string
  • sm4Encrypt / signwithenSM2-Sm4Encrypt 模式:参数 JSON 序列化后 SM4 加密,拼接到 ?data=加密结果

POST 请求(按 contentType):

contentType 处理方式
"json" Content-Type: application/json,加密模式下 body 整体 SM4 加密为 {"data":"加密结果"}
"urlencoded" Content-Type: application/x-www-form-urlencoded,加密模式下 data=加密结果
"form-data" Content-Type: multipart/form-data(暂未实现加密)

响应处理

  • 无加密模式(void / sign):直接返回 Response
  • 加密模式(sm4Encrypt / signwithenSM2-Sm4Encrypt):
    1. 先校验外层 res.code == resSuccessCode()(当前定义为 0
    2. 取出 res.data(SM4 加密的字符串)
    3. 用本次请求生成的 SM4 key + iv 解密
    4. 解密结果为内层 Response,返回给调用方

底层请求(RequestApi)

RequestApi(req: RequestVo) → Promise<Response>
  • 基于 uni.request 封装
  • 超时时间:req.timeout ?? 6000ms
  • 成功(statusCode=200):resolve Response 对象
    • Android 平台特殊处理:通过 JSON.parse(JSON.stringify(res.data)) 转换
    • 其他平台:直接 res.data as Response
  • 失败(非200):reject CustOtherFailError(包含 code + errorMsg)
  • 网络异常:reject CustOtherFailError(包含 errCode + errMsg)

文件上传(uploadFile)

uploadFile(req: UploadFileOptions) → Promise<Response>
  • 基于 uni.uploadFile 封装
  • 超时时间:req.timeout ?? 12000ms
  • 成功时解析 res.data 为 Response
  • 失败时 reject err.errMsg

类型定义(依赖 sunrains-common-type)

FrontRequestVo(前置请求参数)

type FrontRequestVo = {  
  method: 'GET' | 'POST',           // HTTP 方法  
  url: string,                       // 请求地址  
  data: UTSJSONObject | null,        // 请求数据  
  header: UTSJSONObject | null,      // 自定义请求头(会与签名/加密头合并)  
  reqFlag: ReqFlag,                  // 安全模式标识  
  priKey: string,                    // SM2 私钥(签名时使用)  
  pubKey: string,                    // SM2 公钥(加密SM4密钥时使用)  
  contentType: "json" | "urlencoded" | "form-data" | null,  // POST内容类型  
  timeout: number | null,            // 超时时间(ms)  
  filePathParamName: string | null   // 上传文件时,文件路径的参数名  
}

Response(统一响应)

type Response = {  
  success: boolean | null,    // 是否成功  
  msg: string | null,         // 提示信息  
  code: number,               // 状态码(0=成功)  
  errorMsg: string | null,    // 错误详情  
  data: any | null            // 业务数据(加密模式下为密文字符串)  
}

RequestVo(底层请求参数)

type RequestVo = {  
  url: string,  
  method?: 'GET' | 'POST' | null,  
  header: UTSJSONObject,  
  timeout?: number | null,  
  data: any  
}

Sm4Key(SM4密钥)

type Sm4Key = {  
  key: string,  
  iv: string  
}

错误处理(依赖 sunrains-common-err)

所有请求错误统一通过 UniError 抛出:

错误来源 errSubject errCode message
SM2私钥为空 SM2_PRI_KEY_BLANK 预定义码 预定义信息
SM2公钥为空 SM2_PUB_KEY_BLANK 预定义码 预定义信息
SM2签名失败 SM2_SIGN_FAIL 预定义码 预定义信息
HTTP非200 "请求失败" 服务端code errorMsg / msg
网络异常 "请求失败" err.errCode err.errMsg
加密模式响应码非0 "请求失败" res.code errorMsg / msg

调用方 catch 示例:

try {  
  const res = await ReqApi(frontVo)  
} catch (err) {  
  // 跨平台安全取值(避免 iOS 上 err.message 崩溃)  
  console.log("请求异常:", `${err}`)  
  // 或通过 err.message 获取错误信息(err 为 UniError 实例)  
}

工具函数

Base64 编解码

import { stringToBase64, base64ToString } from "@/uni_modules/sunrains-request"  

const encoded = stringToBase64("Hello")   // "SGVsbG8="  
const decoded = base64ToString("SGVsbG8=") // "Hello"

随机字符串

import { random } from "@/uni_modules/sunrains-request"  

const nonce = random(16)   // 生成16位随机字符串

依赖模块

模块 用途
sunrains-smutil SM2签名/加解密、SM4加解密
sunrains-common-type 类型定义(FrontRequestVo、Response、RequestVo、Sm4Key、ReqFlag)
sunrains-common-err 统一错误类(CustFailError、CustOtherFailError)

使用示例

1. 无加密请求(void)

import { ReqApi } from "@/uni_modules/sunrains-request"  

const frontVo: FrontRequestVo = {  
  method: "POST",  
  url: "http://your-api.com/test",  
  data: { name: "张三", age: 18 },  
  header: null,  
  reqFlag: "void",  
  priKey: "",  
  pubKey: "",  
  contentType: "json",  
  timeout: null,  
  filePathParamName: null  
}  
const res = await ReqApi(frontVo)

2. SM2签名请求(sign)

const frontVo: FrontRequestVo = {  
  method: "POST",  
  url: "http://your-api.com/test",  
  data: { name: "张三", age: 18 },  
  header: null,  
  reqFlag: "sign",  
  priKey: "客户端SM2私钥",  
  pubKey: "服务端SM2公钥",  
  contentType: "json",  
  timeout: null,  
  filePathParamName: null  
}  
const res = await ReqApi(frontVo)

3. 签名 + SM4加密请求(signwithenSM2-Sm4Encrypt)

const frontVo: FrontRequestVo = {  
  method: "POST",  
  url: "http://your-api.com/test",  
  data: { name: "张三", age: 18 },  
  header: null,  
  reqFlag: "signwithenSM2-Sm4Encrypt",  
  priKey: "客户端SM2私钥",  
  pubKey: "服务端SM2公钥",  
  contentType: "json",  
  timeout: null,  
  filePathParamName: null  
}  
const res = await ReqApi(frontVo)  
// res.data 已自动解密

4. 文件上传(计划中)


继续阅读 »

sunrains-request

注:客户端与服务端之间通信 数据 加密,签名,保证数据安全与防串改(完整功能依赖付费插件sunrains-smutil)

客户端与服务端(java)之间通信数据加密、签名,保证数据安全与防篡改
支持平台:微信小程序、H5、Android、iOS、鸿蒙
请求方式支持:GET、POST(application/json、application/x-www-form-urlencoded、multipart/form-data(计划中))

服务端(java)点击 sunrains-java


模块目录结构

sunrains-request/  
├── index.uts                          # 模块入口,统一导出  
├── js_sdk/  
│   ├── inutils/  
│   │   └── requestApi.uts             # 底层网络请求封装(uni.request / uni.uploadFile)  
│   └── openutil/  
│       ├── base64Util.uts             # Base64 编解码工具  
│       ├── randomUtil.uts             # 随机字符串生成  
│       └── request/  
│           ├── Request.uts            # 请求入口(ReqApi)  
│           └── reqHandle/  
│               ├── ReqHandle.uts      # 请求处理抽象基类  
│               ├── handle.uts         # 策略工厂(根据 reqFlag 选择 Handler)  
│               └── handle/  
│                   ├── VoidHandle.uts           # 无加密无签名模式  
│                   ├── SignHandle.uts           # 仅签名模式  
│                   ├── EncryptHandle.uts        # 仅SM4加密模式  
│                   └── SignWithEncryptHandle.uts # 签名+SM4加密模式

导出 API 一览

导出函数 来源文件 说明
ReqApi request/Request.uts 高层请求入口,自动根据 reqFlag 完成签名/加密/发送/解密
RequestApi inutils/requestApi.uts 底层请求入口,直接发起 uni.request 并返回 Promise\<Response>
uploadFile inutils/requestApi.uts 文件上传,返回 Promise\<Response>
stringToBase64 openutil/base64Util.uts 字符串 → Base64 编码
base64ToString openutil/base64Util.uts Base64 → 字符串解码
random openutil/randomUtil.uts 生成指定长度的随机字符串(字母+数字),默认16位

网络请求架构(核心)

整体流程

调用方  
  │  
  ▼  
ReqApi(frontVo: FrontRequestVo)          ← 高层入口  
  │  
  ├─ 1. selectHandle(reqFlag)            ← 根据模式选择 Handler  
  │  
  ├─ 2. handle.handleHeaderAndBody(vo)   ← 构造请求头 + 处理请求体(签名/加密)  
  │  
  ├─ 3. RequestApi(req)                  ← 底层发起 HTTP 请求  
  │  
  └─ 4. handle.handleRes(req, res)       ← 处理响应(解密/验签)  
  │  
  ▼  
返回 Response

四种请求模式(ReqFlag)

通过 FrontRequestVo.reqFlag 指定,决定请求的安全处理策略:

reqFlag Handler 签名 请求体SM4加密 响应SM4解密 需要priKey 需要pubKey
"void" VoidHandle
"sign" SignHandle ✓ SM2签名
"sm4Encrypt" EncryptHandle ✓ SM4加密 ✓ SM4解密
"signwithenSM2-Sm4Encrypt" SignWithEncryptHandle ✓ SM2签名 ✓ SM4加密 ✓ SM4解密

请求头处理(所有模式共用)

无论哪种模式,handleSignHeader 都会执行以下操作:

  1. SM2加密SM4密钥:生成随机 SM4 key + iv,用服务端公钥(pubKey)通过 SM2 加密后放入 header["key"]
  2. 条件签名(sign / signwithenSM2-Sm4Encrypt 模式):
    • 生成 timestamp(时间戳)和 nonceStr(16位随机串)
    • 将所有业务参数 + timestamp + nonceStr 按 key 字典序排列,拼接为 key=value& 格式
    • 使用客户端私钥(priKey)对拼接字符串进行 SM2 签名
    • 签名结果放入 header["sign"],同时放入 header["timestamp"]header["nonceStr"]

请求体处理(按 HTTP Method + contentType)

GET 请求:

  • void / sign 模式:参数拼接到 URL query string
  • sm4Encrypt / signwithenSM2-Sm4Encrypt 模式:参数 JSON 序列化后 SM4 加密,拼接到 ?data=加密结果

POST 请求(按 contentType):

contentType 处理方式
"json" Content-Type: application/json,加密模式下 body 整体 SM4 加密为 {"data":"加密结果"}
"urlencoded" Content-Type: application/x-www-form-urlencoded,加密模式下 data=加密结果
"form-data" Content-Type: multipart/form-data(暂未实现加密)

响应处理

  • 无加密模式(void / sign):直接返回 Response
  • 加密模式(sm4Encrypt / signwithenSM2-Sm4Encrypt):
    1. 先校验外层 res.code == resSuccessCode()(当前定义为 0
    2. 取出 res.data(SM4 加密的字符串)
    3. 用本次请求生成的 SM4 key + iv 解密
    4. 解密结果为内层 Response,返回给调用方

底层请求(RequestApi)

RequestApi(req: RequestVo) → Promise<Response>
  • 基于 uni.request 封装
  • 超时时间:req.timeout ?? 6000ms
  • 成功(statusCode=200):resolve Response 对象
    • Android 平台特殊处理:通过 JSON.parse(JSON.stringify(res.data)) 转换
    • 其他平台:直接 res.data as Response
  • 失败(非200):reject CustOtherFailError(包含 code + errorMsg)
  • 网络异常:reject CustOtherFailError(包含 errCode + errMsg)

文件上传(uploadFile)

uploadFile(req: UploadFileOptions) → Promise<Response>
  • 基于 uni.uploadFile 封装
  • 超时时间:req.timeout ?? 12000ms
  • 成功时解析 res.data 为 Response
  • 失败时 reject err.errMsg

类型定义(依赖 sunrains-common-type)

FrontRequestVo(前置请求参数)

type FrontRequestVo = {  
  method: 'GET' | 'POST',           // HTTP 方法  
  url: string,                       // 请求地址  
  data: UTSJSONObject | null,        // 请求数据  
  header: UTSJSONObject | null,      // 自定义请求头(会与签名/加密头合并)  
  reqFlag: ReqFlag,                  // 安全模式标识  
  priKey: string,                    // SM2 私钥(签名时使用)  
  pubKey: string,                    // SM2 公钥(加密SM4密钥时使用)  
  contentType: "json" | "urlencoded" | "form-data" | null,  // POST内容类型  
  timeout: number | null,            // 超时时间(ms)  
  filePathParamName: string | null   // 上传文件时,文件路径的参数名  
}

Response(统一响应)

type Response = {  
  success: boolean | null,    // 是否成功  
  msg: string | null,         // 提示信息  
  code: number,               // 状态码(0=成功)  
  errorMsg: string | null,    // 错误详情  
  data: any | null            // 业务数据(加密模式下为密文字符串)  
}

RequestVo(底层请求参数)

type RequestVo = {  
  url: string,  
  method?: 'GET' | 'POST' | null,  
  header: UTSJSONObject,  
  timeout?: number | null,  
  data: any  
}

Sm4Key(SM4密钥)

type Sm4Key = {  
  key: string,  
  iv: string  
}

错误处理(依赖 sunrains-common-err)

所有请求错误统一通过 UniError 抛出:

错误来源 errSubject errCode message
SM2私钥为空 SM2_PRI_KEY_BLANK 预定义码 预定义信息
SM2公钥为空 SM2_PUB_KEY_BLANK 预定义码 预定义信息
SM2签名失败 SM2_SIGN_FAIL 预定义码 预定义信息
HTTP非200 "请求失败" 服务端code errorMsg / msg
网络异常 "请求失败" err.errCode err.errMsg
加密模式响应码非0 "请求失败" res.code errorMsg / msg

调用方 catch 示例:

try {  
  const res = await ReqApi(frontVo)  
} catch (err) {  
  // 跨平台安全取值(避免 iOS 上 err.message 崩溃)  
  console.log("请求异常:", `${err}`)  
  // 或通过 err.message 获取错误信息(err 为 UniError 实例)  
}

工具函数

Base64 编解码

import { stringToBase64, base64ToString } from "@/uni_modules/sunrains-request"  

const encoded = stringToBase64("Hello")   // "SGVsbG8="  
const decoded = base64ToString("SGVsbG8=") // "Hello"

随机字符串

import { random } from "@/uni_modules/sunrains-request"  

const nonce = random(16)   // 生成16位随机字符串

依赖模块

模块 用途
sunrains-smutil SM2签名/加解密、SM4加解密
sunrains-common-type 类型定义(FrontRequestVo、Response、RequestVo、Sm4Key、ReqFlag)
sunrains-common-err 统一错误类(CustFailError、CustOtherFailError)

使用示例

1. 无加密请求(void)

import { ReqApi } from "@/uni_modules/sunrains-request"  

const frontVo: FrontRequestVo = {  
  method: "POST",  
  url: "http://your-api.com/test",  
  data: { name: "张三", age: 18 },  
  header: null,  
  reqFlag: "void",  
  priKey: "",  
  pubKey: "",  
  contentType: "json",  
  timeout: null,  
  filePathParamName: null  
}  
const res = await ReqApi(frontVo)

2. SM2签名请求(sign)

const frontVo: FrontRequestVo = {  
  method: "POST",  
  url: "http://your-api.com/test",  
  data: { name: "张三", age: 18 },  
  header: null,  
  reqFlag: "sign",  
  priKey: "客户端SM2私钥",  
  pubKey: "服务端SM2公钥",  
  contentType: "json",  
  timeout: null,  
  filePathParamName: null  
}  
const res = await ReqApi(frontVo)

3. 签名 + SM4加密请求(signwithenSM2-Sm4Encrypt)

const frontVo: FrontRequestVo = {  
  method: "POST",  
  url: "http://your-api.com/test",  
  data: { name: "张三", age: 18 },  
  header: null,  
  reqFlag: "signwithenSM2-Sm4Encrypt",  
  priKey: "客户端SM2私钥",  
  pubKey: "服务端SM2公钥",  
  contentType: "json",  
  timeout: null,  
  filePathParamName: null  
}  
const res = await ReqApi(frontVo)  
// res.data 已自动解密

4. 文件上传(计划中)


收起阅读 »

uniapp x 自定义+动态Tabbar+自定义tabbar 支持服务端动态配置的自定义Tabbar 主题切换(tabbar切换无闪烁问题)

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

点击体验h5端

📋 目录

  • [1. 基础配置]
  • [2. pages.json 配置]
  • [3. 动态配置 TabBar]
  • [4. 数字角标功能]
  • [5. 小程序端特殊配置]
  • [6. 主题配置]
  • [7. page页跳转自定义tab页]

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 文件,用于注册全局组件和配置导航栏。

完整示例:

// 1. 导入所有自定义 tabbar 页面组件并注册  
import Recommend from '@/pages/home/recommend.uvue'  
import Order from '@/pages/order/order.uvue'  
import Mine from '@/pages/mine/mine.uvue'  
import {   
  setNavbarConfig,   
  NavBarConfig,   
  registerGlobalComponent,   
  setDefaultTabItems,   
  TabItem   
} from '@/uni_modules/sunrains-tabbar/types.uts'  

// 2. 注册全局组件  
registerGlobalComponent('home', Recommend)  
registerGlobalComponent('order', Order)  
registerGlobalComponent('mine', Mine)  

// 3. 配置导航栏(可选,不设置会使用默认配置)  
setNavbarConfig(new NavBarConfig(  
  true,       // 是否显示导航栏  
  17,         // 字体大小  
  'bold',     // 字体粗细  
  '#333333',  // 文字颜色  
  '#ffffff',  // 背景颜色  
  '',         // 背景图片(支持网络图片)  
  56,         // 导航栏高度  
))  

// 4. 设置默认 Tab 列表(至少设置一个固定项)  
// 其他 Tab 可通过服务端接口动态配置  
setDefaultTabItems([  
  new TabItem(  
    'home',                              // 唯一标识  
    '首页',                              // 显示名称  
    '/static/tab/main1.png',             // 未选中图标  
    '/static/tab/main1_active.png',      // 选中图标  
    '@/pages/home/recommend.uvue',       // 页面路径  
    Recommend                            // 组件引用  
  ),  
  new TabItem(  
    'mine',  
    '我的',  
    '/static/tab/main3.png',  
    '/static/tab/main3_active.png'  
  )  
])

关键步骤说明:

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

2. pages.json 配置

2.1 必需配置

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

{  
  "path": "uni_modules/sunrains-tabbar/index",  
  "style": {  
    "navigationBarTitleText": "自定义tabbar",  
    "navigationStyle": "custom"  
  }  
}

2.2 完整示例

{  
  "pages": [  
    {  
      "path": "uni_modules/sunrains-tabbar/index",  
      "style": {  
        "navigationBarTitleText": "自定义tabbar",  
        "navigationStyle": "custom"  
      }  
    },  
    {  
      "path": "pages/home/recommend",  
      "style": {  
        "navigationBarTitleText": "首页"  
      }  
    },  
    {  
      "path": "pages/mine/mine",  
      "style": {  
        "navigationBarTitleText": "我的"  
      }  
    },  
    {  
      "path": "pages/order/order",  
      "style": {  
        "navigationBarTitleText": "订单"  
      }  
    },  
    {  
      "path": "pages/detail/detail",  
      "style": {  
        "navigationBarTitleText": "详情"  
      }  
    }  
  ],  
  "globalStyle": {  
    "backgroundColor": "#ffffff"  
  }  
}

注意事项:

  • 自定义 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 class="renderer-wrap">  
    <!-- App 端:使用动态组件 -->  
    <!-- #ifndef MP-WEIXIN -->  
    <template v-for="item in items" :key="item.key">  
      <component  
        v-if="currentTab === item.key"  
        :is="item.component"  
        :key="`${item.key}-${renderKey}`"  
      />  
    </template>  
    <!-- #endif -->  

    <!-- 微信小程序端:使用静态声明 -->  
    <!-- #ifdef MP-WEIXIN -->  
    <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%;" />  
    <!-- #endif -->  
  </view>  
</template>  

<script setup lang="uts">  
import { TabItem } from './utssdk/types.uts'  

// #ifdef MP-WEIXIN  
// 只有在小程序端才需要这些导入,App 端会自动忽略  
import Recommend from '@/pages/home/recommend.uvue'  
import Order from '@/pages/order/order.uvue'  
import Mine from '@/pages/mine/mine.uvue'  
// #endif  

interface Props {  
  currentTab: string  
  items: TabItem[]  
  renderKey: number  
}  

defineProps<Props>()  
</script>  

<style scoped>  
.renderer-wrap {  
  width: 100%;  
  height: 100%;  
}  
</style>

配置说明:

  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",  
   //文字颜色  
    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>  
        </view>  

    </scroll-view>  
</template>  

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

// 用于自动化测试的数据暴露 - 使用 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() {  
        uni.removeStorageSync("CUSTOMTHEME")  
    }  

    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')  
        ]);  
    }  
    function refresh2() {  
        console.log("动态tabbar(首页、我的)")  
        refreshTabbar([  
            new TabItem('home', 12, "red", "yellow", "bold", '首页', '/static/tab/main1.png', '/static/tab/main1_active.png'),  
            new TabItem('mine', 12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png')  
        ])  
    }  
    function refresh3() {  
        console.log("动态tabbar(订单、我的)")  

        refreshTabbar( [  
            new TabItem('order', 12, "red", "yellow", "bold", '订单', '/static/tab/main2.png', '/static/tab/main2_active.png'),  
            new TabItem('mine', 12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png')  
        ])  
    }  
    function refresh4() {  
        console.log("动态tabbar(我的、订单、首页)")  

        refreshTabbar([  
            new TabItem('mine', 12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png'),  
            new TabItem('order', 12, "red", "yellow", "bold", '订单', '/static/tab/main2.png', '/static/tab/main2_active.png'),  
            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')  
        ])  
    }  

    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",  
            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",  
            20,  
            "green",  
            "red",  
            "bold"  
        )  
        let tabbarIcon1 = new TabbarIcon("home",  
            "/static/sy1.png",  
            "/static/sy.png",  
            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 = "/static/777.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",  
            20,  
            "green",  
            "red",  
            "bold"  
        )  
        let tabbarIcon1 = new TabbarIcon("home",  
            "/static/sy1.png",  
            "/static/sy.png",  
            16,  
            "#1afa29",  
            "yellow",  
            "bold"  
        )  
        let tabbarIcons : Map<string, TabbarIcon> = new Map();  
        tabbarIcons.set("mine", tabbarIcon)  
        tabbarIcons.set("home", tabbarIcon1)  
        let tabbarStyle : TabbarStyle = new TabbarStyle(200, "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 class="detail-item">  
                <text class="label">订单编号:</text>  
                <text class="value">{{ orderId }}</text>  
            </view>  
            <view class="detail-item">  
                <text class="label">订单状态:</text>  
                <text class="value">{{ orderStatus }}</text>  
            </view>  
            <view class="detail-desc">  
                <text>这是订单{{ orderId }}的详细信息页面</text>  
            </view>  
            <view>  
                <input v-model="content" placeholder="请输入。。" />  
                <text>{{sbase64}}</text>  
                <text>{{base64yostr}}</text>  
            </view>  

            <view>  
                <button type="primary" @click="req">访问testvoid</button>  
                <button type="primary" @click="reqSm2">访问testSm2</button>  
                <button type="primary" @click="reqSm4">访问testSm4</button>  
                <button type="primary" @click="reqSm2Sm4">访问testreqSm2Sm4</button>  
                <input style="height: 50px; border:1px solid grey;margin: 5px 0;" v-model="addr" placeholder="输入跳转tabbar key"/>  
                <button type="default" @click="tz">跳转指定tab页</button>  
            </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"  
    const addr = ref("")  
    function tz(){  
        if(addr.value.length<1){  
            uni.showToast({  
                title: '请输入跳转的tabbar key'  
            });  
        }else{  
            pageToTab(addr.value)  
        }  

    }  

    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;  
        font-size: 14px;  
        color: #999;  
    }  
</style>  

⚠️ 常见问题

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 一致)
  • 是否重新赋值了整个数组(直接修改对象属性不会触发响应式)

📝 总结

  1. 基础配置三要素:注册组件 → 配置导航栏 → 设置默认 Tab
  2. pages.json:自定义 TabBar 入口必须放在第一位
  3. 动态刷新:通过 uni.$emit('refreshNewTabbar', [...]) 实现
  4. 数字角标:通过 uni.$emit('setTabBadge', {"key": "...", "badge": number}) 设置
  5. 小程序兼容:需要在 tab-content-renderer.uvue 中静态声明组件
  6. 主题配置 通过 uni.$emit('setTheme', new CustomTheme(bgType, bg, navBarConfig, tabbarStyle)) 动态修改
继续阅读 »

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

点击体验h5端

📋 目录

  • [1. 基础配置]
  • [2. pages.json 配置]
  • [3. 动态配置 TabBar]
  • [4. 数字角标功能]
  • [5. 小程序端特殊配置]
  • [6. 主题配置]
  • [7. page页跳转自定义tab页]

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 文件,用于注册全局组件和配置导航栏。

完整示例:

// 1. 导入所有自定义 tabbar 页面组件并注册  
import Recommend from '@/pages/home/recommend.uvue'  
import Order from '@/pages/order/order.uvue'  
import Mine from '@/pages/mine/mine.uvue'  
import {   
  setNavbarConfig,   
  NavBarConfig,   
  registerGlobalComponent,   
  setDefaultTabItems,   
  TabItem   
} from '@/uni_modules/sunrains-tabbar/types.uts'  

// 2. 注册全局组件  
registerGlobalComponent('home', Recommend)  
registerGlobalComponent('order', Order)  
registerGlobalComponent('mine', Mine)  

// 3. 配置导航栏(可选,不设置会使用默认配置)  
setNavbarConfig(new NavBarConfig(  
  true,       // 是否显示导航栏  
  17,         // 字体大小  
  'bold',     // 字体粗细  
  '#333333',  // 文字颜色  
  '#ffffff',  // 背景颜色  
  '',         // 背景图片(支持网络图片)  
  56,         // 导航栏高度  
))  

// 4. 设置默认 Tab 列表(至少设置一个固定项)  
// 其他 Tab 可通过服务端接口动态配置  
setDefaultTabItems([  
  new TabItem(  
    'home',                              // 唯一标识  
    '首页',                              // 显示名称  
    '/static/tab/main1.png',             // 未选中图标  
    '/static/tab/main1_active.png',      // 选中图标  
    '@/pages/home/recommend.uvue',       // 页面路径  
    Recommend                            // 组件引用  
  ),  
  new TabItem(  
    'mine',  
    '我的',  
    '/static/tab/main3.png',  
    '/static/tab/main3_active.png'  
  )  
])

关键步骤说明:

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

2. pages.json 配置

2.1 必需配置

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

{  
  "path": "uni_modules/sunrains-tabbar/index",  
  "style": {  
    "navigationBarTitleText": "自定义tabbar",  
    "navigationStyle": "custom"  
  }  
}

2.2 完整示例

{  
  "pages": [  
    {  
      "path": "uni_modules/sunrains-tabbar/index",  
      "style": {  
        "navigationBarTitleText": "自定义tabbar",  
        "navigationStyle": "custom"  
      }  
    },  
    {  
      "path": "pages/home/recommend",  
      "style": {  
        "navigationBarTitleText": "首页"  
      }  
    },  
    {  
      "path": "pages/mine/mine",  
      "style": {  
        "navigationBarTitleText": "我的"  
      }  
    },  
    {  
      "path": "pages/order/order",  
      "style": {  
        "navigationBarTitleText": "订单"  
      }  
    },  
    {  
      "path": "pages/detail/detail",  
      "style": {  
        "navigationBarTitleText": "详情"  
      }  
    }  
  ],  
  "globalStyle": {  
    "backgroundColor": "#ffffff"  
  }  
}

注意事项:

  • 自定义 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 class="renderer-wrap">  
    <!-- App 端:使用动态组件 -->  
    <!-- #ifndef MP-WEIXIN -->  
    <template v-for="item in items" :key="item.key">  
      <component  
        v-if="currentTab === item.key"  
        :is="item.component"  
        :key="`${item.key}-${renderKey}`"  
      />  
    </template>  
    <!-- #endif -->  

    <!-- 微信小程序端:使用静态声明 -->  
    <!-- #ifdef MP-WEIXIN -->  
    <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%;" />  
    <!-- #endif -->  
  </view>  
</template>  

<script setup lang="uts">  
import { TabItem } from './utssdk/types.uts'  

// #ifdef MP-WEIXIN  
// 只有在小程序端才需要这些导入,App 端会自动忽略  
import Recommend from '@/pages/home/recommend.uvue'  
import Order from '@/pages/order/order.uvue'  
import Mine from '@/pages/mine/mine.uvue'  
// #endif  

interface Props {  
  currentTab: string  
  items: TabItem[]  
  renderKey: number  
}  

defineProps<Props>()  
</script>  

<style scoped>  
.renderer-wrap {  
  width: 100%;  
  height: 100%;  
}  
</style>

配置说明:

  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",  
   //文字颜色  
    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>  
        </view>  

    </scroll-view>  
</template>  

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

// 用于自动化测试的数据暴露 - 使用 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() {  
        uni.removeStorageSync("CUSTOMTHEME")  
    }  

    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')  
        ]);  
    }  
    function refresh2() {  
        console.log("动态tabbar(首页、我的)")  
        refreshTabbar([  
            new TabItem('home', 12, "red", "yellow", "bold", '首页', '/static/tab/main1.png', '/static/tab/main1_active.png'),  
            new TabItem('mine', 12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png')  
        ])  
    }  
    function refresh3() {  
        console.log("动态tabbar(订单、我的)")  

        refreshTabbar( [  
            new TabItem('order', 12, "red", "yellow", "bold", '订单', '/static/tab/main2.png', '/static/tab/main2_active.png'),  
            new TabItem('mine', 12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png')  
        ])  
    }  
    function refresh4() {  
        console.log("动态tabbar(我的、订单、首页)")  

        refreshTabbar([  
            new TabItem('mine', 12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png'),  
            new TabItem('order', 12, "red", "yellow", "bold", '订单', '/static/tab/main2.png', '/static/tab/main2_active.png'),  
            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')  
        ])  
    }  

    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",  
            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",  
            20,  
            "green",  
            "red",  
            "bold"  
        )  
        let tabbarIcon1 = new TabbarIcon("home",  
            "/static/sy1.png",  
            "/static/sy.png",  
            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 = "/static/777.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",  
            20,  
            "green",  
            "red",  
            "bold"  
        )  
        let tabbarIcon1 = new TabbarIcon("home",  
            "/static/sy1.png",  
            "/static/sy.png",  
            16,  
            "#1afa29",  
            "yellow",  
            "bold"  
        )  
        let tabbarIcons : Map<string, TabbarIcon> = new Map();  
        tabbarIcons.set("mine", tabbarIcon)  
        tabbarIcons.set("home", tabbarIcon1)  
        let tabbarStyle : TabbarStyle = new TabbarStyle(200, "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 class="detail-item">  
                <text class="label">订单编号:</text>  
                <text class="value">{{ orderId }}</text>  
            </view>  
            <view class="detail-item">  
                <text class="label">订单状态:</text>  
                <text class="value">{{ orderStatus }}</text>  
            </view>  
            <view class="detail-desc">  
                <text>这是订单{{ orderId }}的详细信息页面</text>  
            </view>  
            <view>  
                <input v-model="content" placeholder="请输入。。" />  
                <text>{{sbase64}}</text>  
                <text>{{base64yostr}}</text>  
            </view>  

            <view>  
                <button type="primary" @click="req">访问testvoid</button>  
                <button type="primary" @click="reqSm2">访问testSm2</button>  
                <button type="primary" @click="reqSm4">访问testSm4</button>  
                <button type="primary" @click="reqSm2Sm4">访问testreqSm2Sm4</button>  
                <input style="height: 50px; border:1px solid grey;margin: 5px 0;" v-model="addr" placeholder="输入跳转tabbar key"/>  
                <button type="default" @click="tz">跳转指定tab页</button>  
            </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"  
    const addr = ref("")  
    function tz(){  
        if(addr.value.length<1){  
            uni.showToast({  
                title: '请输入跳转的tabbar key'  
            });  
        }else{  
            pageToTab(addr.value)  
        }  

    }  

    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;  
        font-size: 14px;  
        color: #999;  
    }  
</style>  

⚠️ 常见问题

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 一致)
  • 是否重新赋值了整个数组(直接修改对象属性不会触发响应式)

📝 总结

  1. 基础配置三要素:注册组件 → 配置导航栏 → 设置默认 Tab
  2. pages.json:自定义 TabBar 入口必须放在第一位
  3. 动态刷新:通过 uni.$emit('refreshNewTabbar', [...]) 实现
  4. 数字角标:通过 uni.$emit('setTabBadge', {"key": "...", "badge": number}) 设置
  5. 小程序兼容:需要在 tab-content-renderer.uvue 中静态声明组件
  6. 主题配置 通过 uni.$emit('setTheme', new CustomTheme(bgType, bg, navBarConfig, tabbarStyle)) 动态修改
收起阅读 »

苹果4.3(a)被拒怎么办?码尚友科技分享IPA相似度检测经验

苹果内购 苹果审核

很多开发者认为,苹果反馈 4.3(a) 之后,只要换个图标、改个名称、调整几个页面就能重新提交。

但实际审核过程中,大量应用即使已经更换 UI,依然会再次收到 4.3(a) 拒绝。

根据码尚友科技近两年处理的上架案例统计,苹果对于应用相似性的判断,早已不仅限于界面层面,而是从代码结构、资源文件、业务逻辑、提交环境等多个维度进行综合分析。

苹果4.3(a)到底在检测什么?

苹果审核团队希望每个应用都具备独立价值,而不是简单复制已有产品。

从实际案例来看,相似性检测主要集中在以下几个维度:

检测维度

权重占比(经验数据)

常见问题

代码结构

35%

相同工程模板、相同类结构

资源文件

25%

图片、音频、字体高度重复

功能逻辑

20%

页面流程一致

账号关联

10%

开发者账号存在关联

提交环境

10%

IP、设备环境异常

根据码尚友上架系统统计,在近1000+个处理案例中,仅修改UI但未优化代码结构的项目,二次收到4.3(a)反馈的比例超过72%。

为什么改了界面还是会收到4.3(a)?
很多开发团队只修改:

Logo
App名称
首页配色
部分文案
但实际上:

Controller结构未变化
API调用逻辑未变化
资源目录未变化
工程架构未变化
苹果机审依然能够识别出较高相似度。

因此:

UI差异 ≠ 应用差异

码尚友检测系统发现的高风险项
在项目提交前,我们会通过内部IPA分析工具进行检测。

重点检查:

1、代码相似度分析
检测内容:

类名结构
方法调用链
Framework引用情况
工程目录结构
示例:

检测项

风险等级

类名重复率85%

高风险

页面结构重复率78%

高风险

Framework一致率92%

高风险

图上是2个不相关的APP,所以相似度特别低,事实证明,相似度检测的确可以检测出2个app是否有关联,是否会被4.3a被拒

2、资源包检测
系统会扫描:

图片Hash值
音频文件
视频资源
字体资源
部分项目虽然更换了图片名称,但实际文件内容未变,依然会被识别。

3、业务流程分析
重点分析:

页面跳转路径
功能触发逻辑
用户使用流程
例如:

A应用:

首页 → 会员 → AI生成 → 保存

B应用:

首页 → VIP → AI创作 → 导出

虽然名称不同,但业务路径完全一致,仍然存在较高相似风险。

码尚友科技的解决思路
针对4.3(a)问题,我们通常从四个层面进行优化:

第一层:代码层优化
工程结构重组
模块重新划分
方法逻辑重构
类结构调整
第二层:资源层优化
图片重新设计
资源目录重建
字体资源调整
启动页重构
第三层:业务层优化
增加独立功能模块
调整用户路径
增加业务价值点
第四层:提交层优化
提交环境隔离
审核资料检测
开发者账号评估
历史版本关联分析
数据统计
根据码尚友科技2025-2026年上架案例统计:

项目类型

初次通过率

仅修改UI

28%

UI+资源优化

47%

UI+资源+代码优化

76%

完整检测后提交

结语
苹果4.3(a)并不是简单的“换壳检测”。

从近年的审核趋势来看,苹果对于代码结构、资源内容、业务逻辑以及开发环境的综合分析越来越严格。

如果项目已经连续收到4.3(a)反馈,建议在重新提交前进行完整的IPA检测和风险评估,而不是仅修改图标或界面。

码尚友科技自主研发的IPA检测系统,可针对代码结构、资源文件、业务逻辑及提交环境进行多维度分析,帮助开发者提前发现高风险项,提高审核通过率。

继续阅读 »

很多开发者认为,苹果反馈 4.3(a) 之后,只要换个图标、改个名称、调整几个页面就能重新提交。

但实际审核过程中,大量应用即使已经更换 UI,依然会再次收到 4.3(a) 拒绝。

根据码尚友科技近两年处理的上架案例统计,苹果对于应用相似性的判断,早已不仅限于界面层面,而是从代码结构、资源文件、业务逻辑、提交环境等多个维度进行综合分析。

苹果4.3(a)到底在检测什么?

苹果审核团队希望每个应用都具备独立价值,而不是简单复制已有产品。

从实际案例来看,相似性检测主要集中在以下几个维度:

检测维度

权重占比(经验数据)

常见问题

代码结构

35%

相同工程模板、相同类结构

资源文件

25%

图片、音频、字体高度重复

功能逻辑

20%

页面流程一致

账号关联

10%

开发者账号存在关联

提交环境

10%

IP、设备环境异常

根据码尚友上架系统统计,在近1000+个处理案例中,仅修改UI但未优化代码结构的项目,二次收到4.3(a)反馈的比例超过72%。

为什么改了界面还是会收到4.3(a)?
很多开发团队只修改:

Logo
App名称
首页配色
部分文案
但实际上:

Controller结构未变化
API调用逻辑未变化
资源目录未变化
工程架构未变化
苹果机审依然能够识别出较高相似度。

因此:

UI差异 ≠ 应用差异

码尚友检测系统发现的高风险项
在项目提交前,我们会通过内部IPA分析工具进行检测。

重点检查:

1、代码相似度分析
检测内容:

类名结构
方法调用链
Framework引用情况
工程目录结构
示例:

检测项

风险等级

类名重复率85%

高风险

页面结构重复率78%

高风险

Framework一致率92%

高风险

图上是2个不相关的APP,所以相似度特别低,事实证明,相似度检测的确可以检测出2个app是否有关联,是否会被4.3a被拒

2、资源包检测
系统会扫描:

图片Hash值
音频文件
视频资源
字体资源
部分项目虽然更换了图片名称,但实际文件内容未变,依然会被识别。

3、业务流程分析
重点分析:

页面跳转路径
功能触发逻辑
用户使用流程
例如:

A应用:

首页 → 会员 → AI生成 → 保存

B应用:

首页 → VIP → AI创作 → 导出

虽然名称不同,但业务路径完全一致,仍然存在较高相似风险。

码尚友科技的解决思路
针对4.3(a)问题,我们通常从四个层面进行优化:

第一层:代码层优化
工程结构重组
模块重新划分
方法逻辑重构
类结构调整
第二层:资源层优化
图片重新设计
资源目录重建
字体资源调整
启动页重构
第三层:业务层优化
增加独立功能模块
调整用户路径
增加业务价值点
第四层:提交层优化
提交环境隔离
审核资料检测
开发者账号评估
历史版本关联分析
数据统计
根据码尚友科技2025-2026年上架案例统计:

项目类型

初次通过率

仅修改UI

28%

UI+资源优化

47%

UI+资源+代码优化

76%

完整检测后提交

结语
苹果4.3(a)并不是简单的“换壳检测”。

从近年的审核趋势来看,苹果对于代码结构、资源内容、业务逻辑以及开发环境的综合分析越来越严格。

如果项目已经连续收到4.3(a)反馈,建议在重新提交前进行完整的IPA检测和风险评估,而不是仅修改图标或界面。

码尚友科技自主研发的IPA检测系统,可针对代码结构、资源文件、业务逻辑及提交环境进行多维度分析,帮助开发者提前发现高风险项,提高审核通过率。

收起阅读 »

uniapp x插件 SM2 SM3 SM4 AES RSA 加解密 签名 验签 客户端 服务端 全平台解决方案

sunrains-smutil 插件使用指南

本插件提供SM2、SM3 、 SM4、AES、RSA 客户端(ios,安卓,鸿蒙,微信小程序,h5),服务端(java)SM2、SM3 、 SM4、AES、RSA 算法的跨平台实现,支持以下功能:
SM2 非对称加密算法
✅ SM2 密钥对生成
✅ SM2 数字签名
✅ SM2 签名验证
✅ SM2 数据加密
✅ SM2 数据解密
SM3 密码杂凑算法
✅ SM3 哈希计算(字符串)
✅ HMAC-SM3 密钥哈希
✅ 文件 SM3 哈希
✅ 哈希值验证
SM4 对称加密算法
✅ SM4 密钥生成(含 IV)
✅ SM4 数据加密(CBC 模式)
✅ SM4 数据解密(CBC 模式)
AES 对称加密算法
✅ AES 密钥和 IV 生成
✅ AES 数据加密(CBC 模式, PKCS7/PKCS5 填充)
✅ AES 数据解密(CBC 模式, PKCS7/PKCS5 填充)
✅ 跨平台兼容(Android/iOS/HarmonyOS/H5/微信小程序)
RSA 非对称加密算法
✅ RSA 密钥对生成(2048位)
✅ RSA-OAEP-SHA256 数据加密
✅ RSA-OAEP-SHA256 数据解密
✅ SHA256withRSA 数字签名(PKCS#1 v1.5)
✅ SHA256withRSA 签名验证(PKCS#1 v1.5)
✅ 跨平台兼容(Android/iOS/HarmonyOS/H5)
⚠️ 微信小程序端暂不支持RSA-OAEP-SHA256加密解密,仅支持签名验签

调用方式

本插件同时支持回调风格异步风格两种调用方式:

方式一:回调风格(传统方式)

import { sm2Sign } from "@/uni_modules/sunrains-util";  

sm2Sign({  
  data: "需要签名的数据",  
  privateKey: "你的私钥",  
  success: (res) => {  
    console.log("签名结果:", res.signature);  
  },  
  fail: (err) => {  
    console.error("签名失败:", err);  
  }  
});

方式二:异步风格(推荐,更简洁)

import { sm2SignAsync } from "@/uni_modules/sunrains-util";  

async function doSign() {  
  try {  
    const result = await sm2SignAsync("需要签名的数据", "你的私钥")  
    console.log("签名结果:", result.signature)  
  } catch (error) {  
    console.error("签名失败:", error)  
  }  
}

平台支持

平台 SM2 实现方式 SM4 实现方式 AES 实现方式 RSA 实现方式 状态
Android BouncyCastle BouncyCastle Java Crypto API Java Crypto API (RSA-OAEP-SHA256) ✅ 完全支持
iOS GMObjC GMObjC CommonCrypto Security Framework (RSA-OAEP-SHA256) ✅ 完全支持
HarmonyOS cryptoFramework cryptoFramework cryptoFramework cryptoFramework (RSA-OAEP-SHA256) ✅ 完全支持
H5 sm-crypto sm-crypto Web Crypto API Web Crypto API (RSA-OAEP-SHA256) ✅ 完全支持
微信小程序 miniprogram-sm-crypto miniprogram-sm-crypto crypto-js jsrsasign (仅签名验签) ⚠️ 部分支持

使用示例

SM2 相关操作

1. 生成 SM2 密钥对

同步方式:

import { sm2GenerateKeyPair } from "@/uni_modules/sunrains-util";  

const result = sm2GenerateKeyPair();  
console.log("公钥:", result.publicKey);  // 04开头的130位十六进制字符串  
console.log("私钥:", result.privateKey); // 64位十六进制字符串

异步方式:

import { sm2GenerateKeyPairAsync } from "@/uni_modules/sunrains-util";  

async function generateKeys() {  
  const result = await sm2GenerateKeyPairAsync()  
  console.log("公钥:", result.publicKey)  
  console.log("私钥:", result.privateKey)  
}

2. SM2 签名

回调风格:

import { sm2Sign } from "@/uni_modules/sunrains-util";  

sm2Sign({  
  data: "需要签名的数据",  
  privateKey: "你的私钥",  
  success: (res) => {  
    console.log("签名结果:", res.signature);  
  },  
  fail: (err) => {  
    console.error("签名失败:", err);  
  }  
});

异步风格(推荐):

import { sm2SignAsync } from "@/uni_modules/sunrains-util";  

async function doSign() {  
  try {  
    const result = await sm2SignAsync("需要签名的数据", "你的私钥")  
    console.log("签名结果:", result.signature)  
  } catch (error) {  
    console.error("签名失败:", error)  
  }  
}

3. SM2 验签

回调风格:

import { sm2Verify } from "@/uni_modules/sunrains-util";  

sm2Verify({  
  data: "原始数据",  
  signature: "签名值",  
  publicKey: "你的公钥",  
  success: (res) => {  
    console.log("验签结果:", res.valid); // true 或 false  
  },  
  fail: (err) => {  
    console.error("验签失败:", err);  
  }  
});

异步风格(推荐):

import { sm2VerifyAsync } from "@/uni_modules/sunrains-util";  

async function doVerify() {  
  try {  
    const result = await sm2VerifyAsync("原始数据", "签名值", "你的公钥")  
    console.log("验签结果:", result.valid) // true 或 false  
  } catch (error) {  
    console.error("验签失败:", error)  
  }  
}

完整实战示例

以下是一个完整的国密算法测试页面示例,及服务端(java)工具类,展示了所有功能的实际使用:

客户端 Vue 3 Composition API 示例

 <template>  
  <view class="order-content">  
    <view class="order-header">  
      <text class="order-title">国密算法测试</text>  
    </view>  

    <scroll-view class="order-list" scroll-y="true">  
      <view class="section">  
        <view class="section-title">SM4 对称加密</view>  

        <view class="input-group">  
          <text class="label">待加密内容:</text>  
          <input v-model="sm4Content" placeholder="请输入要加密的内容" class="input" />  
        </view>  

        <button @click="testSm4Encrypt" class="btn btn-primary">SM4 加密</button>  

        <view class="result-box" v-if="sm4Key.key">  
          <text class="result-label">密钥 (key):</text>  
          <text selectable="true" class="result-value">{{ sm4Key.key }}</text>  
        </view>  

        <view class="result-box" v-if="sm4Key.iv">  
          <text class="result-label">初始向量 (iv):</text>  
          <text selectable="true" class="result-value">{{ sm4Key.iv }}</text>  
        </view>  

        <view class="result-box" v-if="sm4Encrypted">  
          <text class="result-label">加密结果:</text>  
          <text selectable="true" class="result-value">{{ sm4Encrypted }}</text>  
        </view>  
        <button @click="testSm4Decrypt" :disabled="sm4Encrypted.length==0" class="btn btn-secondary">SM4 解密</button>  
        <button @click="testSm4Decrypt" :disabled="sm4Encrypted === ''" :class="sm4Encrypted === '' ? 'btn btn-secondary disabled' : 'btn btn-secondary'" class="btn btn-secondary">SM4 解密</button>  

        <view class="result-box" v-if="sm4Decrypted">  
          <text class="result-label">解密结果:</text>  
          <text selectable="true" class="result-value success">{{ sm4Decrypted }}</text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">SM2 数字签名</view>  

        <view class="input-group">  
          <text class="label">待签名内容:</text>  
          <input v-model="sm2SignContent" placeholder="请输入要签名的内容" class="input" />  
        </view>  

        <button @click="testSm2Sign" class="btn btn-primary">生成密钥对并签名</button>  

        <view class="result-box" v-if="sm2KeyPair.publicKey">  
          <text class="result-label">公钥:</text>  
          <text selectable="true" class="result-value small">{{ sm2KeyPair.publicKey }}</text>  
        </view>  

        <view class="result-box" v-if="sm2KeyPair.privateKey">  
          <text class="result-label">私钥:</text>  
          <text selectable="true" class="result-value small">{{ sm2KeyPair.privateKey }}</text>  
        </view>  

        <view class="result-box" v-if="sm2Signature">  
          <text class="result-label">签名结果:</text>  
          <text selectable="true" class="result-value small">{{ sm2Signature }}</text>  
        </view>  

        <button @click="testSm2Verify" :disabled="sm2Signature.length==0" class="btn btn-secondary">验签</button>  

        <view class="result-box" v-if="sm2VerifyResult !== null">  
          <text class="result-label">验签结果:</text>  
          <text class="result-value" :class="sm2VerifyResult ==true? 'success' : 'error'">  
            {{ sm2VerifyResult ==true ? '✅ 验签成功' : '❌ 验签失败' }}  
          </text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">SM2 非对称加密</view>  

        <view class="input-group">  
          <text class="label">待加密内容:</text>  
          <input v-model="sm2EncryptContent" placeholder="请输入要加密的内容" class="input" />  
        </view>  

        <button @click="testSm2Encrypt" class="btn btn-primary">SM2 加密</button>  

        <view class="result-box" v-if="sm2EncryptKeyPair.publicKey">  
          <text class="result-label">公钥:</text>  
          <text selectable="true" class="result-value small">{{ sm2EncryptKeyPair.publicKey }}</text>  
        </view>  

        <view class="result-box" v-if="sm2EncryptKeyPair.privateKey">  
          <text class="result-label">私钥:</text>  
          <text selectable="true" class="result-value small">{{ sm2EncryptKeyPair.privateKey }}</text>  
        </view>  

        <view class="result-box" v-if="sm2Encrypted">  
          <text class="result-label">加密结果:</text>  
          <text selectable="true" class="result-value small">{{ sm2Encrypted }}</text>  
        </view>  

        <button @click="testSm2Decrypt" :disabled="sm2Encrypted.length==0" class="btn btn-secondary">SM2 解密</button>  

        <view class="result-box" v-if="sm2Decrypted">  
          <text class="result-label">解密结果:</text>  
          <text selectable="true" class="result-value success">{{ sm2Decrypted }}</text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">SM3 哈希算法</view>  

        <!-- 1. SM3 哈希 -->  
        <view class="input-group">  
          <text class="label">待哈希内容:</text>  
          <input v-model="sm3Content" placeholder="请输入要哈希的内容" class="input" />  
        </view>  

        <button @click="testSm3Hash" class="btn btn-primary">SM3 哈希</button>  

        <view class="result-box" v-if="sm3HashResult">  
          <text class="result-label">SM3 哈希值:</text>  
          <text selectable="true" class="result-value small">{{ sm3HashResult }}</text>  
        </view>  

        <!-- 2. SM3 哈希验证 -->  
        <view class="divider"></view>  
        <view class="sub-title">SM3 哈希验证</view>  

        <view class="input-group">  
          <text class="label">验证 - 原始内容:</text>  
          <input v-model="sm3VerifyContent" placeholder="请输入原始内容" class="input" />  
        </view>  

        <button @click="testSm3Verify" :disabled="sm3VerifyContent.length == 0" class="btn btn-secondary">验证哈希值</button>  

        <view class="result-box" v-if="sm3HashMatchResult !== null">  
          <text class="result-label">哈希匹配结果:</text>  
          <text class="result-value" :class="sm3HashMatchResult == true ? 'success' : 'error'">  
            {{ sm3HashMatchResult == true ? '✅ 哈希值匹配' : '❌ 哈希值不匹配' }}  
          </text>  
        </view>  

        <!-- 3. HMAC-SM3 哈希 -->  
        <view class="divider"></view>  
        <view class="sub-title">HMAC-SM3 哈希</view>  

        <view class="input-group">  
          <text class="label">HMAC-SM3 待哈希内容:</text>  
          <input v-model="sm3HmacContent" placeholder="请输入要哈希的内容" class="input" />  
        </view>  

        <view class="input-group">  
          <text class="label">HMAC-SM3 密钥(HEX):</text>  
          <input v-model="sm3HmacKey" placeholder="请输入HMAC密钥(HEX格式)" class="input" />  
        </view>  

        <button @click="testSm3HmacHash" :disabled="sm3HmacContent.length == 0 || sm3HmacKey.length == 0" class="btn btn-primary">HMAC-SM3 哈希</button>  

        <view class="result-box" v-if="sm3HmacHashResult">  
          <text class="result-label">HMAC-SM3 哈希值:</text>  
          <text selectable="true" class="result-value small">{{ sm3HmacHashResult }}</text>  
        </view>  

        <!-- 4. HMAC-SM3 哈希验证 -->  
        <view class="divider"></view>  
        <view class="sub-title">HMAC-SM3 哈希验证</view>  

        <view class="input-group">  
          <text class="label">验证 - 原始内容:</text>  
          <input v-model="sm3HmacVerifyContent" placeholder="请输入原始内容" class="input" />  
        </view>  

        <view class="input-group">  
          <text class="label">验证 - HMAC 密钥:</text>  
          <input v-model="sm3HmacVerifyKey" placeholder="请输入HMAC密钥" class="input" />  
        </view>  

        <view class="input-group">  
          <text class="label">验证 - 期望哈希值:</text>  
          <input v-model="sm3HmacExpectedHash" placeholder="请输入期望的哈希值" class="input" />  
        </view>  

        <button @click="testSm3HmacHashWithExpected" :disabled="sm3HmacVerifyContent.length == 0 || sm3HmacVerifyKey.length == 0 || sm3HmacExpectedHash.length == 0" class="btn btn-secondary">验证 HMAC-SM3 哈希值</button>  

        <view class="result-box" v-if="sm3HmacHashMatchResult !== null">  
          <text class="result-label">HMAC-SM3 哈希匹配结果:</text>  
          <text class="result-value" :class="sm3HmacHashMatchResult == true ? 'success' : 'error'">  
            {{ sm3HmacHashMatchResult == true ? '✅ 哈希值匹配' : '❌ 哈希值不匹配' }}  
          </text>  
        </view>  

        <!-- 5. 文件 SM3 哈希 -->  
        <view class="divider"></view>  
        <view class="sub-title">文件 SM3 哈希</view>  

        <button @click="chooseImageAndHash" class="btn btn-primary">选择图片并计算 SM3</button>  
        <button @click="chooseFileAndHash" class="btn btn-primary">选择文件并计算 SM3</button>  
        <view class="result-box" v-if="sm3FileName">  
          <text class="result-label">文件名:</text>  
          <text class="result-value">{{ sm3FileName }}</text>  
        </view>  

        <view class="result-box" v-if="sm3FileHashResult">  
          <text class="result-label">文件 SM3 哈希值:</text>  
          <text selectable="true" class="result-value small">{{ sm3FileHashResult }}</text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">AES 对称加密</view>  

        <view class="input-group">  
          <text class="label">待加密内容:</text>  
          <input v-model="aesContent" placeholder="请输入要加密的内容" class="input" />  
        </view>  

        <button @click="testAesEncrypt" class="btn btn-primary">AES 加密</button>  

        <view class="result-box" v-if="aesKey.key">  
          <text class="result-label">密钥 (key):</text>  
          <text selectable="true" class="result-value">{{ aesKey.key }}</text>  
        </view>  

        <view class="result-box" v-if="aesKey.iv">  
          <text class="result-label">初始向量 (iv):</text>  
          <text selectable="true" class="result-value">{{ aesKey.iv }}</text>  
        </view>  

        <view class="result-box" v-if="aesEncrypted">  
          <text class="result-label">加密结果:</text>  
          <text selectable="true" class="result-value">{{ aesEncrypted }}</text>  
        </view>  

        <button @click="testAesDecrypt" :disabled="aesEncrypted.length==0" class="btn btn-secondary">AES 解密</button>  

        <view class="result-box" v-if="aesDecrypted">  
          <text class="result-label">解密结果:</text>  
          <text selectable="true" class="result-value success">{{ aesDecrypted }}</text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">RSA 非对称加密</view>  

        <view class="input-group">  
          <text class="label">待加密内容:</text>  
          <input v-model="rsaContent" placeholder="请输入要加密的内容" class="input" />  
        </view>  

        <button @click="testRsaEncrypt" class="btn btn-primary">RSA 加密</button>  

        <view class="result-box" v-if="rsaKeyPair.publicKey">  
          <text class="result-label">公钥:</text>  
          <text selectable="true" class="result-value small">{{ rsaKeyPair.publicKey }}</text>  
        </view>  

        <view class="result-box" v-if="rsaKeyPair.privateKey">  
          <text class="result-label">私钥:</text>  
          <text selectable="true" class="result-value small">{{ rsaKeyPair.privateKey }}</text>  
        </view>  

        <view class="result-box" v-if="rsaEncrypted">  
          <text class="result-label">加密结果:</text>  
          <text selectable="true" class="result-value small">{{ rsaEncrypted }}</text>  
        </view>  

        <button @click="testRsaDecrypt" :disabled="rsaEncrypted.length==0" class="btn btn-secondary">RSA 解密</button>  

        <view class="result-box" v-if="rsaDecrypted">  
          <text class="result-label">解密结果:</text>  
          <text selectable="true" class="result-value success">{{ rsaDecrypted }}</text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">RSA 数字签名</view>  

        <view class="input-group">  
          <text class="label">待签名内容:</text>  
          <input v-model="rsaSignContent" placeholder="请输入要签名的内容" class="input" />  
        </view>  

        <button @click="testRsaSign" class="btn btn-primary">生成密钥对并签名</button>  

        <view class="result-box" v-if="rsaSignKeyPair.publicKey">  
          <text class="result-label">公钥:</text>  
          <text selectable="true" class="result-value small">{{ rsaSignKeyPair.publicKey }}</text>  
        </view>  

        <view class="result-box" v-if="rsaSignKeyPair.privateKey">  
          <text class="result-label">私钥:</text>  
          <text selectable="true" class="result-value small">{{ rsaSignKeyPair.privateKey }}</text>  
        </view>  

        <view class="result-box" v-if="rsaSignature">  
          <text class="result-label">签名结果:</text>  
          <text selectable="true" class="result-value small">{{ rsaSignature }}</text>  
        </view>  

        <button @click="testRsaVerify" :disabled="rsaSignature.length==0" class="btn btn-secondary">验签</button>  

        <view class="result-box" v-if="rsaVerifyResult !== null">  
          <text class="result-label">验签结果:</text>  
          <text class="result-value" :class="rsaVerifyResult ==true? 'success' : 'error'">  
            {{ rsaVerifyResult ==true ? '✅ 验签成功' : '❌ 验签失败' }}  
          </text>  
        </view>  
      </view>  

      <view class="order-item" @click="goToDetail(1002, '已完成')">  
        <view class="order-info">  
          <text class="order-id">订单编号:1002</text>  
          <text class="order-status">状态:已完成</text>  
        </view>  
        <view class="order-btn">  
          <text class="go-detail">查看详情</text>  
        </view>  
      </view>  
    </scroll-view>  
  </view>  
</template>  

<script setup lang="uts">  
  import {IdcardOcrModel}from "@/uni_modules/sunrains-idcard-ocr/utssdk/interface.uts"  
  import {Response} from "@/uni_modules/sunrains-utils/index.uts"  

  import * as SM from "@/uni_modules/sunrains-smutil"  

  // ==================== SM4 相关数据 ====================  
  const sm4Content = ref("Hello World")  
  const sm4Key = ref<SM.Sm4Key>({ key: '', iv: '' })  
  const sm4Encrypted = ref("")  
  const sm4Decrypted = ref("")  

  // ==================== SM2 签名验签相关数据 ====================  
  const sm2SignContent = ref("123")  
  const sm2KeyPair = ref<SM.Sm2KeyPair>({ publicKey: '', privateKey: '' })  
  const sm2Signature = ref("")  
  const sm2VerifyResult = ref<boolean | null>(null)  

  // ==================== SM2 加密解密相关数据 ====================  
  const sm2EncryptContent = ref("Hello SM2")  
  const sm2EncryptKeyPair = ref<SM.Sm2KeyPair>({ publicKey: '', privateKey: '' })  
  const sm2Encrypted = ref("")  
  const sm2Decrypted = ref("")  

  // ==================== SM3 相关数据 ====================  
  const sm3Content = ref("Hello SM3")  
  const sm3HashResult = ref("")  
  const sm3VerifyContent = ref("")  
  const sm3HashMatchResult = ref<boolean | null>(null)  

  const sm3HmacContent = ref("Hello HMAC-SM3")  
  const sm3HmacKey = ref("0123456789abcdef0123456789abcdef")  // 默认 HMAC 密钥(32字节HEX)  
  const sm3HmacHashResult = ref("")  
  const sm3HmacVerifyContent = ref("")  
  const sm3HmacVerifyKey = ref("")  
  const sm3HmacExpectedHash = ref("")  
  const sm3HmacHashMatchResult = ref<boolean | null>(null)  
  const sm3VerifyResult = ref<boolean|null>(null)  

  const sm3FileName = ref("")  
  const sm3FileHashResult = ref("")  

  // ==================== AES 相关数据 ====================  
  const aesContent = ref("Hello AES")  
  const aesKey = ref<SM.AesKey>({ key: '', iv: '' })  
  const aesEncrypted = ref("")  
  const aesDecrypted = ref("")  

  // ==================== RSA 加密解密相关数据 ====================  
  const rsaContent = ref("Hello RSA")  
  const rsaKeyPair = ref<SM.RsaKeyPair>({ publicKey: '', privateKey: '' })  
  const rsaEncrypted = ref("")  
  const rsaDecrypted = ref("")  

  // ==================== RSA 签名验签相关数据 ====================  
  const rsaSignContent = ref("需要签名的数据")  
  const rsaSignKeyPair = ref<SM.RsaKeyPair>({ publicKey: '', privateKey: '' })  
  const rsaSignature = ref("")  
  const rsaVerifyResult = ref<boolean | null>(null)  

  // ==================== SM4 加密 ====================  
  async function testSm4Encrypt() {  
      if (sm4Content.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥  
          const key = await SM.sm4GenerateKeyAsync()  
          sm4Key.value = { key: key.key, iv: key.iv }  
          console.log("SM4 密钥:", key)  

          // 加密  
          const encryptResult = await SM.sm4EncryptAsync(sm4Content.value, key.key, key.iv)  
          sm4Encrypted.value = encryptResult.result  
          console.log("SM4 加密成功:", encryptResult.result)  
          uni.showToast({ title: '加密成功', icon: 'success' })  

      } catch (e) {  
          console.error("SM4 加密失败:", e)  
          uni.showToast({ title: '加密失败', icon: 'error' })  
      }  
  }  

  // ==================== SM4 解密 ====================  
  async function testSm4Decrypt() {  
      if (sm4Encrypted.value.length==0 || sm4Key.value.key.length==0) {  
          uni.showToast({ title: '请先加密', icon: 'none' })  
          return  
      }  

      try {  
          const decryptResult = await SM.sm4DecryptAsync(sm4Encrypted.value, sm4Key.value.key, sm4Key.value.iv)  
          sm4Decrypted.value = decryptResult.result  
          console.log("SM4 解密成功:", decryptResult.result)  
          if (decryptResult.result == sm4Content.value) {  
              uni.showToast({ title: '解密成功且内容一致', icon: 'success' })  
          } else {  
              uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("SM4 解密失败:", e)  
          uni.showToast({ title: '解密失败', icon: 'error' })  
      }  
  }  

  // ==================== AES 加密 ====================  
  async function testAesEncrypt() {  
      if (aesContent.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥和 IV  
          const key = await SM.aesGenerateKeyAsync()  
          aesKey.value = { key: key.key, iv: key.iv }  
          console.log("AES 密钥:", key)  

          // 加密  
          const encryptResult = await SM.aesEncryptAsync(aesContent.value, key.key, key.iv)  
          aesEncrypted.value = encryptResult.result  
          console.log("AES 加密成功:", encryptResult.result)  
          uni.showToast({ title: '加密成功', icon: 'success' })  

      } catch (e) {  
          console.error("AES 加密失败:", e)  
          uni.showToast({ title: '加密失败', icon: 'error' })  
      }  
  }  

  // ==================== AES 解密 ====================  
  async function testAesDecrypt() {  
      if (aesEncrypted.value.length==0 || aesKey.value.key.length==0) {  
          uni.showToast({ title: '请先加密', icon: 'none' })  
          return  
      }  

      try {  
          const decryptResult = await SM.aesDecryptAsync(aesEncrypted.value, aesKey.value.key, aesKey.value.iv)  
          aesDecrypted.value = decryptResult.result  
          console.log("AES 解密成功:", decryptResult.result)  
          if (decryptResult.result == aesContent.value) {  
              uni.showToast({ title: '解密成功且内容一致', icon: 'success' })  
          } else {  
              uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("AES 解密失败:", e)  
          uni.showToast({ title: '解密失败', icon: 'error' })  
      }  
  }  

  // ==================== RSA 加密 ====================  
  async function testRsaEncrypt() {  
      if (rsaContent.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥对  
          const keyPair = await SM.rsaGenerateKeyPairAsync()  
          rsaKeyPair.value = {   
              publicKey: keyPair.publicKey,   
              privateKey: keyPair.privateKey   
          }  
          console.log("RSA 公钥:", keyPair.publicKey)  
          console.log("RSA 私钥:", keyPair.privateKey)  

          // 加密  
          const encryptResult = await SM.rsaEncryptAsync(rsaContent.value, keyPair.publicKey)  
          rsaEncrypted.value = encryptResult.result  
          console.log("RSA 加密结果:", encryptResult.result)  
          uni.showToast({ title: '加密成功', icon: 'success' })  
      } catch (e) {  
          console.error("RSA 加密失败:", e)  
          uni.showToast({ title: '加密失败', icon: 'error' })  
      }  
  }  

  // ==================== RSA 解密 ====================  
  async function testRsaDecrypt() {  
      if (rsaEncrypted.value.length==0 || rsaKeyPair.value.privateKey.length==0) {  
          uni.showToast({ title: '请先加密', icon: 'none' })  
          return  
      }  

      try {  
          const decryptResult = await SM.rsaDecryptAsync(rsaEncrypted.value, rsaKeyPair.value.privateKey)  
          rsaDecrypted.value = decryptResult.result  
          console.log("RSA 解密结果:", decryptResult.result)  

          if (decryptResult.result == rsaContent.value) {  
              uni.showToast({ title: '解密成功且内容一致', icon: 'success' })  
          } else {  
              uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("RSA 解密异常:", e)  
          uni.showToast({ title: '解密异常', icon: 'error' })  
      }  
  }  

  // ==================== RSA 签名 ====================  
  async function testRsaSign() {  
      if (rsaSignContent.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥对  
          const keyPair = await SM.rsaGenerateKeyPairAsync()  
          rsaSignKeyPair.value = {   
              publicKey: keyPair.publicKey,   
              privateKey: keyPair.privateKey   
          }  
          console.log("RSA 公钥:", keyPair.publicKey)  
          console.log("RSA 私钥:", keyPair.privateKey)  

          // 签名  
          const signResult = await SM.rsaSignAsync(rsaSignContent.value, keyPair.privateKey)  
          rsaSignature.value = signResult.signature  
          console.log("RSA 签名成功:", signResult.signature)  
          uni.showToast({ title: '签名成功', icon: 'success' })  
      } catch (e) {  
          console.error("RSA 签名失败:", e)  
          uni.showToast({ title: '签名失败', icon: 'error' })  
      }  
  }  

  // ==================== RSA 验签 ====================  
  async function testRsaVerify() {  
      if (rsaSignature.value.length==0 || rsaSignKeyPair.value.publicKey.length==0) {  
          uni.showToast({ title: '请先签名', icon: 'none' })  
          return  
      }  

      try {  
          // 验签  
          const verifyResult = await SM.rsaVerifyAsync(rsaSignContent.value, rsaSignature.value, rsaSignKeyPair.value.publicKey)  
          rsaVerifyResult.value = verifyResult.valid  
          console.log("RSA 验签结果:", verifyResult.valid)  
          if (verifyResult.valid) {  
              uni.showToast({ title: '验签成功', icon: 'success' })  
          } else {  
              uni.showToast({ title: '验签失败', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("RSA 验签异常:", e)  
          uni.showToast({ title: '验签异常', icon: 'error' })  
      }  
  }  

  // ==================== SM2 签名 ====================  
  async function testSm2Sign() {  
      if (sm2SignContent.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥对  
          const keyPair = await SM.sm2GenerateKeyPairAsync()  
          sm2KeyPair.value = {   
              publicKey: keyPair.publicKey,   
              privateKey: keyPair.privateKey   
          }  
          console.log("SM2 公钥:", keyPair.publicKey)  
          console.log("SM2 私钥:", keyPair.privateKey)  

          // 签名  
          const signResult = await SM.sm2SignAsync(sm2SignContent.value, keyPair.privateKey)  
          sm2Signature.value = signResult.signature  
          console.log("SM2 签名成功:", signResult.signature)  
          uni.showToast({ title: '签名成功', icon: 'success' })  
      } catch (e) {  
          console.error("SM2 生成密钥对失败:", e)  
          uni.showToast({ title: '生成密钥对失败', icon: 'error' })  
      }  
  }  

  // ==================== SM2 验签 ====================  
  async function testSm2Verify() {  
      if (sm2Signature.value.length==0 || sm2KeyPair.value.publicKey.length==0) {  
          uni.showToast({ title: '请先签名', icon: 'none' })  
          return  
      }  

      try {  
          // 验签  
          const verifyResult = await SM.sm2VerifyAsync(sm2SignContent.value, sm2Signature.value, sm2KeyPair.value.publicKey)  
          sm2VerifyResult.value = verifyResult.valid  
          console.log("SM2 验签结果:", verifyResult.valid)  
          if (verifyResult.valid) {  
              uni.showToast({ title: '验签成功', icon: 'success' })  
          } else {  
              uni.showToast({ title: '验签失败', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("SM2 验签异常:", e)  
          uni.showToast({ title: '验签异常', icon: 'error' })  
      }  
  }  

  // ==================== SM2 加密 ====================  
  async function testSm2Encrypt() {  
      if (sm2EncryptContent.value.length==0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥对  
          const keyPair = await SM.sm2GenerateKeyPairAsync()  
          sm2EncryptKeyPair.value = {   
              publicKey: keyPair.publicKey,   
              privateKey: keyPair.privateKey   
          }  
          console.log("SM2 加密公钥:", keyPair.publicKey)  
          console.log("SM2 加密私钥:", keyPair.privateKey)  

          // 加密  
          const encryptResult = await SM.sm2EncryptAsync(sm2EncryptContent.value, keyPair.publicKey)  
          sm2Encrypted.value = encryptResult.encrypted  
          console.log("SM2 加密结果:", encryptResult.encrypted)  
          uni.showToast({ title: '加密成功', icon: 'success' })  
      } catch (e) {  
          console.error("SM2 生成密钥对失败:", e)  
          uni.showToast({ title: '生成密钥对失败', icon: 'error' })  
      }  
  }  

  // ==================== SM2 解密 ====================  
  async function testSm2Decrypt() {  
      if (sm2Encrypted.value.length==0 || sm2EncryptKeyPair.value.privateKey.length==0) {  
          uni.showToast({ title: '请先加密', icon: 'none' })  
          return  
      }  

      try {  
          const decryptResult = await SM.sm2DecryptAsync(sm2Encrypted.value, sm2EncryptKeyPair.value.privateKey)  
          sm2Decrypted.value = decryptResult.decrypted  
          console.log("SM2 解密结果:", decryptResult.decrypted)  

          if (decryptResult.decrypted == sm2EncryptContent.value) {  
              uni.showToast({ title: '解密成功且内容一致', icon: 'success' })  
          } else {  
              uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("SM2 解密异常:", e)  
          uni.showToast({ title: '解密异常', icon: 'error' })  
      }  
  }  

  // ==================== SM3 哈希 ====================  
  async function testSm3Hash() {  
      if (sm3Content.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          const hashResult = await SM.sm3HashAsync(sm3Content.value)  
          sm3HashResult.value = hashResult.resultHash  

          console.log("SM3 哈希成功:", hashResult.resultHash)  
          uni.showToast({ title: '哈希成功', icon: 'success' })  
      } catch (e) {  
          console.error("SM3 哈希失败:", e)  
          uni.showToast({ title: '哈希失败', icon: 'error' })  
      }  
  }  

  // ==================== HMAC-SM3 哈希 ====================  
  async function testSm3HmacHash() {  
      if (sm3HmacContent.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      if (sm3HmacKey.value.length == 0) {  
          uni.showToast({ title: '请输入HMAC密钥', icon: 'none' })  
          return  
      }  

      try {  
          const hmacHashResult = await SM.sm3HmacHashAsync(sm3HmacContent.value, sm3HmacKey.value)  
          sm3HmacHashResult.value = hmacHashResult.resultHash  
          sm3HmacExpectedHash.value  =  hmacHashResult.resultHash  
          sm3HmacVerifyKey.value = sm3HmacKey.value  
          sm3HmacVerifyContent.value = sm3HmacContent.value  
          console.log("HMAC-SM3 哈希成功:", hmacHashResult.resultHash)  
          uni.showToast({ title: 'HMAC-SM3 哈希成功', icon: 'success' })  
      } catch (e) {  
          console.error("HMAC-SM3 哈希失败:", e)  
          uni.showToast({ title: 'HMAC-SM3 哈希失败', icon: 'error' })  
      }  
  }  

  // ==================== HMAC-SM3 验证(输入期望哈希值) ====================  
  async function testSm3HmacHashWithExpected() {  
      if (sm3HmacVerifyContent.value.length == 0) {  
          uni.showToast({ title: '请输入原始内容', icon: 'none' })  
          return  
      }  

      try {  
          // 计算实际 HMAC-SM3 哈希值  
          const res = await SM.sm3HmacVerifyAsync(sm3HmacVerifyContent.value, sm3HmacVerifyKey.value,sm3HmacHashResult.value)  
          if (res) {  
              uni.showToast({ title: 'HMAC-SM3 哈希值匹配', icon: 'success' })  
          } else {  
              uni.showToast({ title: 'HMAC-SM3 哈希值不匹配', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("HMAC-SM3 哈希验证异常:", e)  
          uni.showToast({ title: '验证异常', icon: 'error' })  
      }  
  }  

  // ==================== SM3 验证 ====================  
  async function testSm3Verify() {  
      if (sm3HashResult.value.length == 0 || sm3Content.value.length == 0) {  
          uni.showToast({ title: '请先计算哈希', icon: 'none' })  
          return  
      }  

      try {  
          // 重新计算哈希并验证  
          const res = await SM.sm3VerifyAsync(sm3VerifyContent.value,sm3HashResult.value)  
          sm3VerifyResult.value = res  
          console.log("SM3 验证结果:", sm3VerifyResult.value)  

          if (sm3VerifyResult.value!) {  
              uni.showToast({ title: '验证成功', icon: 'success' })  
          } else {  
              uni.showToast({ title: '验证失败', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("SM3 验证异常:", e)  
          uni.showToast({ title: '验证异常', icon: 'error' })  
      }  
  }  

  // ==================== 选择文件并计算 SM3 哈希 ====================  
  async function processFileHash(filePath: string, fileSize?: number) {  
      // 计算文件 SM3 哈希  
      uni.showLoading({ title: '计算中...' })  

      try {  
          console.log("开始计算文件 SM3 哈希")  
          console.log("文件路径:", filePath)  

          const result = await SM.sm3FileHashAsync(filePath)  
          sm3FileHashResult.value = result.resultHash  
          console.log("文件 SM3 哈希成功:", result.resultHash)  
          uni.hideLoading()  
          uni.showToast({ title: '文件哈希成功', icon: 'success' })  
      } catch (e) {  
          console.error("文件 SM3 哈希失败:", e)  
          uni.hideLoading()  
          uni.showToast({ title: '文件哈希失败', icon: 'error' })  
      }  
  }  

  function chooseFileAndHash() {  
      uni.chooseFile({  
          count: 1,  
          success: (res) => {  
              const tempFiles = res.tempFiles  
              if (tempFiles != null && tempFiles.length > 0) {  
                  const file = tempFiles[0]  
                  const fileName = file.name  
                  const filePath = file.path  
                  const fileSize = file.size  
                  sm3FileName.value = fileName != null ? fileName : "未知文件"  

                  if (filePath != null) {  
                      processFileHash(filePath, fileSize)  
                  }  
              }  
          },  
          fail: (err) => {  
              console.error("选择文件失败:", err)  
              if (err.errMsg != null && !err.errMsg.includes('cancel')) {  
                  uni.showToast({ title: '选择文件失败', icon: 'error' })  
              }  
          }  
      })  
  }  

  // ==================== 选择相册图片并计算 SM3 哈希 ====================  
  function chooseImageAndHash() {  
      uni.chooseImage({  
          count: 1,  
          sizeType: ['original'],  // 只选择原图,不压缩  
          sourceType: ['album', 'camera'],  
          success: (res) => {  
              const tempFilePaths = res.tempFilePaths  
              if (tempFilePaths != null && tempFilePaths.length > 0) {  
                  const filePath = tempFilePaths[0]  
                  const fileName = "图片文件"  
                  sm3FileName.value = fileName  
                  console.log("选择的图片(原图):", filePath)  

                  if (filePath != null) {  
                      // 调用异步函数处理文件哈希  
                      processFileHash(filePath)  
                  }  
              }  
          },  
          fail: (err) => {  
              console.error("选择图片失败:", err)  
              if (err.errMsg != null && !err.errMsg.includes('cancel')) {  
                  uni.showToast({ title: '选择图片失败', icon: 'error' })  
              }  
          }  
      })  
  }  

  const goToDetail = (orderId: number, status: string) => {  
    uni.setStorageSync('fromTabBar', true)  
    uni.navigateTo({  
      url: `/pages/detail/detail?orderId=${orderId}&status=${status}`,  
      success: () => {  
        console.log(`跳转到order${orderId}的详情页`)  
      }  
    })  
  }  

  function btn (){  

  }  

</script>  

<style scoped>  
  .order-content {  
    flex: 1;  
    display: flex;  
    flex-direction: column;  
    background-color: #f5f5f5;  
  }  

  .order-header {  
    height: 48px;  
    background-color: #fff;  
    align-items: center;  
    justify-content: center;  
    border-bottom-width: 1px;  
    border-bottom-style: solid;  
    border-bottom-color: #eee;  
  }  

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

  .order-list {  
    flex: 1;  
    padding: 10px;  
  }  

  /* 区块样式 */  
  .section {  
    background-color: #fff;  
    border-radius: 8px;  
    padding: 15px;  
    margin-bottom: 15px;  
  }  

  .section-title {  
    font-size: 16px;  
    font-weight: bold;  
    color: #333;  
    margin-bottom: 15px;  
    padding-bottom: 10px;  
    border-bottom-width: 1px;  
    border-bottom-style: solid;  
    border-bottom-color: #eee;  
  }  

  .input-group {  
    margin-bottom: 15px;  
  }  

  .label {  
    font-size: 14px;  
    color: #666;  
    margin-bottom: 8px;  
  }  

  .input {  
    height: 40px;  
    border-width: 1px;  
    border-style: solid;  
    border-color: #ddd;  
    border-radius: 4px;  
    padding: 0 10px;  
    font-size: 14px;  
    background-color: #fafafa;  
  }  

  /* 按钮样式 */  
  .btn {  
    height: 44px;  
    border-radius: 4px;  
    font-size: 15px;  
    margin-bottom: 10px;  
  }  

  .btn-primary {  
    background-color: #007aff;  
    color: #fff;  
  }  

  .btn-secondary {  
    background-color: #34c759;  
    color: #fff;  
  }  

  .btn:disabled {  
    opacity: 0.5;  
  }  

  /* 结果展示框 */  
  .result-box {  
    margin-top: 10px;  
    padding: 10px;  
    background-color: #f9f9f9;  
    border-radius: 4px;  
    border-left-width: 3px;  
    border-left-style: solid;  
    border-left-color: #007aff;  
  }  

  .result-label {  
    font-size: 12px;  
    color: #999;  
    margin-bottom: 5px;  
  }  

  .result-value {  
    font-size: 13px;  
    color: #333;  
    line-height: 1.5;  
  }  

  .result-value.small {  
    font-size: 11px;  
  }  

  .result-value.success {  
    color: #34c759;  
    font-weight: bold;  
  }  

  .result-value.error {  
    color: #ff3b30;  
    font-weight: bold;  
  }  

  .divider {  
    height: 1px;  
    background-color: #eee;  
    margin: 15px 0;  
  }  

  .sub-title {  
    font-size: 14px;  
    font-weight: bold;  
    color: #666;  
    margin-bottom: 10px;  
  }  
</style>

服务端(java工具类)

package top.sunrains;  

import org.bouncycastle.crypto.digests.SM3Digest;  
import org.bouncycastle.crypto.engines.SM2Engine;  
import org.bouncycastle.crypto.params.ECDomainParameters;  
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;  
import org.bouncycastle.crypto.params.ECPublicKeyParameters;  
import org.bouncycastle.crypto.params.ParametersWithRandom;  
import org.bouncycastle.crypto.signers.SM2Signer;  
import org.bouncycastle.jce.ECNamedCurveTable;  
import org.bouncycastle.jce.spec.ECParameterSpec;  
import org.bouncycastle.util.encoders.Hex;  

import java.math.BigInteger;  
import java.security.SecureRandom;  

/**  
 *         <dependency>  
 *             <groupId>org.bouncycastle</groupId>  
 *             <artifactId>bcprov-jdk15to18</artifactId>  
 *             <version>1.69</version>  
 *         </dependency>  
 */  
public class SM2Util {  

    private static final ECParameterSpec CURVE = ECNamedCurveTable.getParameterSpec("sm2p256v1");  
    private static final ECDomainParameters DOMAIN = new ECDomainParameters(  
            CURVE.getCurve(),  
            CURVE.getG(),  
            CURVE.getN(),  
            CURVE.getH()  
    );  

    /**  
     * 生成SM2密钥对  
     *  
     * @return Map包含publicKey和privateKey,均为小写十六进制字符串  
     */  
    public static java.util.Map<String, String> generateKeyPair() {  
        try {  
            BigInteger d = new BigInteger(256, new SecureRandom()).mod(DOMAIN.getN());  
            org.bouncycastle.math.ec.ECPoint q = DOMAIN.getG().multiply(d).normalize();  

            String x = String.format("4x", q.getAffineXCoord().toBigInteger());  
            String y = String.format("4x", q.getAffineYCoord().toBigInteger());  
            String publicKey = "04" + x + y;  
            String privateKey = String.format("4x", d);  
            java.util.Map<String, String> keyMap = new java.util.HashMap<>();  
            keyMap.put("publicKey", publicKey.toLowerCase());  
            keyMap.put("privateKey", privateKey.toLowerCase());  
            return keyMap;  
        } catch (Exception e) {  
            System.err.println("密钥生成失败: " + e.getMessage());  
            e.printStackTrace();  
            java.util.Map<String, String> keyMap = new java.util.HashMap<>();  
            keyMap.put("publicKey", "");  
            keyMap.put("privateKey", "");  
            return keyMap;  
        }  
    }  

    /**  
     * SM2签名  
     *  
     * @param data 待签名的原始数据  
     * @param privateKeyHex 私钥(64字符十六进制)  
     * @return 签名结果(DER格式,小写十六进制)  
     */  
    public static String sign(String data, String privateKeyHex) {  
        try {  
            byte[] msgBytes = data.getBytes(java.nio.charset.StandardCharsets.UTF_8);  

            BigInteger d = new BigInteger(privateKeyHex, 16);  
            ECPrivateKeyParameters priKey = new ECPrivateKeyParameters(d, DOMAIN);  

            SM2Signer signer = new SM2Signer(new SM3Digest());  
            signer.init(true, priKey);  
            signer.update(msgBytes, 0, msgBytes.length);  
            byte[] sig = signer.generateSignature();  

            return Hex.toHexString(sig).toLowerCase();  
        } catch (Exception e) {  
            System.err.println("签名失败: " + e.getMessage());  
            e.printStackTrace();  
            return "";  
        }  
    }  

    /**  
     * SM2验签  
     *  
     * @param data 原始数据  
     * @param signatureHex 签名(DER格式,十六进制)  
     * @param publicKeyHex 公钥(04开头或不含04前缀的十六进制)  
     * @return 验签是否成功  
     */  
    public static boolean verify(String data, String signatureHex, String publicKeyHex) {  
        try {  
            byte[] msgBytes = data.getBytes(java.nio.charset.StandardCharsets.UTF_8);  

            String xy = publicKeyHex.startsWith("04") ? publicKeyHex.substring(2) : publicKeyHex;  
            BigInteger x = new BigInteger(xy.substring(0, 64), 16);  
            BigInteger y = new BigInteger(xy.substring(64, 128), 16);  

            org.bouncycastle.math.ec.ECPoint pubPoint = DOMAIN.getCurve().createPoint(x, y).normalize();  
            ECPublicKeyParameters pubKey = new ECPublicKeyParameters(pubPoint, DOMAIN);  

            SM2Signer signer = new SM2Signer(new SM3Digest());  
            signer.init(false, pubKey);  
            signer.update(msgBytes, 0, msgBytes.length);  

            byte[] sigBytes = Hex.decode(signatureHex);  
            return signer.verifySignature(sigBytes);  
        } catch (Exception e) {  
            System.err.println("验签失败: " + e.getMessage());  
            e.printStackTrace();  
            return false;  
        }  
    }  

    /**  
     * SM2加密  
     *  
     * @param data 待加密的原始数据(UTF-8字符串)  
     * @param publicKeyHex 公钥(04开头或不含04前缀的十六进制)  
     * @return 密文(HEX字符串,小写)  
     */  
    public static String encrypt(String data, String publicKeyHex) {  
        try {  
            byte[] dataBytes = data.getBytes(java.nio.charset.StandardCharsets.UTF_8);  

            String xy = publicKeyHex.startsWith("04") ? publicKeyHex.substring(2) : publicKeyHex;  
            BigInteger x = new BigInteger(xy.substring(0, 64), 16);  
            BigInteger y = new BigInteger(xy.substring(64, 128), 16);  

            org.bouncycastle.math.ec.ECPoint pubPoint = DOMAIN.getCurve().createPoint(x, y).normalize();  
            ECPublicKeyParameters pubKey = new ECPublicKeyParameters(pubPoint, DOMAIN);  

            SM2Engine engine = new SM2Engine(new SM3Digest(), SM2Engine.Mode.C1C3C2);  
            engine.init(true, new ParametersWithRandom(pubKey, new SecureRandom()));  

            byte[] encrypted = engine.processBlock(dataBytes, 0, dataBytes.length);  
            return Hex.toHexString(encrypted).toLowerCase();  
        } catch (Exception e) {  
            System.err.println("加密失败: " + e.getMessage());  
            e.printStackTrace();  
            return "";  
        }  
    }  

    /**  
     * SM2解密  
     *  
     * @param encryptedHex 密文(HEX字符串)  
     * @param privateKeyHex 私钥(64字符十六进制)  
     * @return 解密后的原始数据(UTF-8字符串)  
     */  
    public static String decrypt(String encryptedHex, String privateKeyHex) {  
        try {  
            byte[] encryptedBytes = Hex.decode(encryptedHex);  

            BigInteger d = new BigInteger(privateKeyHex, 16);  
            ECPrivateKeyParameters priKey = new ECPrivateKeyParameters(d, DOMAIN);  

            SM2Engine engine = new SM2Engine(new SM3Digest(), SM2Engine.Mode.C1C3C2);  
            engine.init(false, priKey);  

            byte[] decrypted = engine.processBlock(encryptedBytes, 0, encryptedBytes.length);  
            return new String(decrypted, java.nio.charset.StandardCharsets.UTF_8);  
        } catch (Exception e) {  
            System.err.println("解密失败: " + e.getMessage());  
            e.printStackTrace();  
            return "";  
        }  
    }  

    /**  
     * 测试方法  
     */  
    public static void main(String[] args) {  
        System.out.println("=== SM2 工具类测试 ===\n");  

        java.util.Map<String, String> keyPair = generateKeyPair();  
        String publicKey = keyPair.get("publicKey");  
        String privateKey = keyPair.get("privateKey");  

        System.out.println("公钥: " + publicKey);  
        System.out.println("私钥: " + privateKey);  
        System.out.println();  

        String testData = "123";  
        System.out.println("原始数据: " + testData);  

        String signature = sign(testData, privateKey);  
        System.out.println("签名: " + signature);  
        System.out.println();  

        boolean isValid = verify(testData, signature, publicKey);  
        System.out.println("验签结果: " + isValid);  
        System.out.println();  

        if (!isValid) {  
            System.err.println("验签失败!");  
        } else {  
            System.out.println("验签成功!");  
        }  

        System.out.println("\n=== SM2 加密解密测试 ===\n");  

        // 加密解密测试  
        String originalText = "Hello SM2";  
        System.out.println("原始数据: " + originalText);  

        String encrypted = encrypt(originalText, publicKey);  
        System.out.println("原始数据: " + originalText);  

        String decrypted = decrypt(encrypted, privateKey);  
        System.out.println("解密结果: " + decrypted);  
        System.out.println();  

        if (originalText.equals(decrypted)) {  
            System.out.println("加密解密成功!");  
        } else {  
            System.err.println("加密解密失败!");  
        }  
    }  
}  

package top.sunrains;  

import org.bouncycastle.jce.provider.BouncyCastleProvider;  

import javax.crypto.Cipher;  
import javax.crypto.spec.IvParameterSpec;  
import javax.crypto.spec.SecretKeySpec;  
import java.security.SecureRandom;  
import java.security.Security;  

/**  
 *         <dependency>  
 *             <groupId>org.bouncycastle</groupId>  
 *             <artifactId>bcprov-jdk15to18</artifactId>  
 *             <version>1.69</version>  
 *         </dependency>  
 *  
 * SM4国密算法工具类  
 * 使用Bouncy Castle原生API  
 * 配置信息:  
 * - 算法:SM4  
 * - 模式:CBC(密码分组链接)  
 * - 填充:PKCS5Padding  
 * - 密钥长度:128位(16字节)  
 * - IV长度:128位(16字节)  
 */  
public class Sm4Util {  

    static {  
        try {  
            Security.addProvider(new BouncyCastleProvider());  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  

    public static void main(String[] args) throws Exception {  
//        String key = generateKeyHex();  
//        String iv = generateIvHex();  
        //String s = encryptHex("123", key, iv);  
        String key = "7f9a942a1aa85c331d3697a5369a37fe";  
        String iv = "cbc07750d33d6a3ef6d39fc7e7c4a876";  

        String s = "09187a80e9f52e7e962a3465ed70669e";  
        System.out.println(s);  
        String decryptedHex = decryptHex(s, key, iv);  
        System.out.println(decryptedHex);  
    }  

    /**  
     * 生成SM4密钥(16字节,128位)  
     * @return 十六进制编码的密钥字符串(32个字符)  
     */  
    public static String generateKeyHex() {  
        byte[] keyBytes = new byte[16];  
        new SecureRandom().nextBytes(keyBytes);  
        return byteArrayToHex(keyBytes);  
    }  

    /**  
     * 生成IV向量(16字节,128位)  
     * @return 十六进制编码的IV字符串(32个字符)  
     */  
    public static String generateIvHex() {  
        byte[] ivBytes = new byte[16];  
        new SecureRandom().nextBytes(ivBytes);  
        return byteArrayToHex(ivBytes);  
    }  

    /**  
     * SM4-CBC加密(字符串到HEX)  
     * 标准加密方式  
     *  
     * @param plaintext 明文字符串(UTF-8)  
     * @param keyHex 十六进制密钥字符串  
     * @param ivHex 十六进制IV字符串  
     * @return 加密后的HEX字符串  
     * @throws Exception 加密异常  
     */  
    public static String encryptHex(String plaintext, String keyHex, String ivHex) throws Exception {  
        byte[] key = decodeKeyHex(keyHex);  
        byte[] iv = decodeIvHex(ivHex);  
        byte[] data = plaintext.getBytes("UTF-8");  

        byte[] encrypted = encrypt(data, key, iv);  
        return byteArrayToHex(encrypted);  
    }  

    /**  
     * SM4-CBC加密  
     *  
     * @param data 原始数据字节数组  
     * @param key 密钥字节数组(16字节)  
     * @param iv IV字节数组(16字节)  
     * @return 加密后的字节数组  
     * @throws Exception 加密异常  
     */  
    public static byte[] encrypt(byte[] data, byte[] key, byte[] iv) throws Exception {  
        Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC");  
        cipher.init(Cipher.ENCRYPT_MODE,  
                new SecretKeySpec(key, "SM4"),  
                new IvParameterSpec(iv));  
        return cipher.doFinal(data);  
    }  

    /**  
     * byte[] 转 Hex  
     *  
     * @param bytes 字节数组  
     * @return HEX字符串(小写)  
     */  
    public static String byteArrayToHex(byte[] bytes) {  
        if (bytes == null || bytes.length == 0) {  
            return "";  
        }  
        StringBuilder sb = new StringBuilder();  
        for (byte b : bytes) {  
            String hex = Integer.toHexString(b & 0xFF);  
            if (hex.length() == 1) {  
                sb.append('0');  
            }  
            sb.append(hex);  
        }  
        return sb.toString().toLowerCase();  
    }  

    public static String decryptHex(String encryptedHex, String keyHex, String ivHex) throws Exception {  
        byte[] key = decodeKeyHex(keyHex);  
        byte[] iv = decodeIvHex(ivHex);  
        byte[] encryptedData = hexToByteArray(encryptedHex);  

        byte[] decrypted = decrypt(encryptedData, key, iv);  
        return new String(decrypted, "UTF-8");  
    }  

    /**  
     * SM4-CBC解密  
     *  
     * @param encryptedData 加密数据字节数组  
     * @param key 密钥字节数组(16字节)  
     * @param iv IV字节数组(16字节)  
     * @return 解密后的原始字节数组  
     * @throws Exception 解密异常  
     */  
    public static byte[] decrypt(byte[] encryptedData, byte[] key, byte[] iv) throws Exception {  
        Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC");  
        cipher.init(Cipher.DECRYPT_MODE,  
                new SecretKeySpec(key, "SM4"),  
                new IvParameterSpec(iv));  
        return cipher.doFinal(encryptedData);  
    }  

    /**  
     * 从十六进制字符串还原密钥字节数组  
     * @param keyHex 十六进制编码的密钥字符串  
     * @return 密钥字节数组(16字节)  
     */  
    public static byte[] decodeKeyHex(String keyHex) {  
        return hexToByteArray(keyHex);  
    }  

    /**  
     * 从十六进制字符串还原IV字节数组  
     * @param ivHex 十六进制编码的IV字符串  
     * @return IV字节数组(16字节)  
     */  
    public static byte[] decodeIvHex(String ivHex) {  
        return hexToByteArray(ivHex);  
    }  

    /**  
     * HEX 转 byte[]  
     *  
     * @param hex HEX字符串  
     * @return 字节数组  
     */  
    public static byte[] hexToByteArray(String hex) {  
        if (hex == null || hex.length() == 0) {  
            return new byte[0];  
        }  

        int len = hex.length() / 2;  
        byte[] bytes = new byte[len];  
        for (int i = 0; i < len; i++) {  
            int high = Character.digit(hex.charAt(i * 2), 16);  
            int low = Character.digit(hex.charAt(i * 2 + 1), 16);  
            bytes[i] = (byte) ((high << 4) | low);  
        }  
        return bytes;  
    }  

    public static String stringToHex(String str) {  
        if (str == null || str.isEmpty()) {  
            return "";  
        }  
        byte[] bytes = str.getBytes(java.nio.charset.StandardCharsets.UTF_8);  
        return byteArrayToHex(bytes);  
    }  
}  

package top.sunrains;  

import javax.crypto.Cipher;  
import javax.crypto.spec.IvParameterSpec;  
import javax.crypto.spec.SecretKeySpec;  
import java.security.SecureRandom;  
import java.util.Base64;  
import java.util.HashMap;  
import java.util.Map;  

/**  
 * AES对称加密算法工具类  
 * 使用Java原生Crypto API  
 * 配置信息:  
 * - 算法:AES-256  
 * - 模式:CBC(密码分组链接)  
 * - 填充:PKCS5Padding  
 * - 密钥长度:256位(32字节)  
 * - IV长度:128位(16字节)  
 */  
public class AesUtil {  

    /**  
     * 生成AES密钥和IV  
     * @return Map包含key (32字符) 和 iv (16字符)  
     */  
    public static Map<String, String> generateKey() {  
        try {  
            // 生成32字节的随机字符串作为密钥  
            String key = generateRandomString(32);  

            // 生成16字节的随机字符串作为IV  
            String iv = generateRandomString(16);  

            System.out.println("AES Key: " + key + " (长度: " + key.length() + ")");  
            System.out.println("AES IV: " + iv + " (长度: " + iv.length() + ")");  

            Map<String, String> result = new HashMap<>();  
            result.put("key", key);  
            result.put("iv", iv);  

            return result;  
        } catch (Exception e) {  
            System.err.println("AES 密钥生成失败: " + e.getMessage());  
            e.printStackTrace();  
            return new HashMap<>();  
        }  
    }  

    /**  
     * 生成指定长度的随机字符串  
     */  
    private static String generateRandomString(int length) {  
        String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";  
        SecureRandom random = new SecureRandom();  
        StringBuilder result = new StringBuilder(length);  
        for (int i = 0; i < length; i++) {  
            result.append(chars.charAt(random.nextInt(chars.length())));  
        }  
        return result.toString();  
    }  

    /**  
     * AES-CBC加密  
     *  
     * @param plaintext 明文字符串(UTF-8)  
     * @param keyStr 密钥字符串(32字符)  
     * @param ivStr IV字符串(16字符)  
     * @return Base64编码的密文  
     * @throws Exception 加密异常  
     */  
    public static String encrypt(String plaintext, String keyStr, String ivStr) throws Exception {  
        // 将字符串Key和IV转换为字节数组(UTF-8编码)  
        byte[] keyBytes = keyStr.getBytes("UTF-8");  
        byte[] ivBytes = ivStr.getBytes("UTF-8");  

        // 创建密钥和IV  
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");  
        IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);  

        // 创建Cipher对象(AES/CBC/PKCS5Padding)  
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");  
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);  

        // 执行加密  
        byte[] encrypted = cipher.doFinal(plaintext.getBytes("UTF-8"));  

        // 返回Base64编码的密文  
        return Base64.getEncoder().encodeToString(encrypted);  
    }  

    /**  
     * AES-CBC解密  
     *  
     * @param encryptedBase64 Base64编码的密文  
     * @param keyStr 密钥字符串(32字符)  
     * @param ivStr IV字符串(16字符)  
     * @return 解密后的明文字符串  
     * @throws Exception 解密异常  
     */  
    public static String decrypt(String encryptedBase64, String keyStr, String ivStr) throws Exception {  
        // 将字符串Key和IV转换为字节数组(UTF-8编码)  
        byte[] keyBytes = keyStr.getBytes("UTF-8");  
        byte[] ivBytes = ivStr.getBytes("UTF-8");  

        // 创建密钥和IV  
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");  
        IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);  

        // 创建Cipher对象(AES/CBC/PKCS5Padding)  
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");  
        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);  

        // 将Base64密文解码为字节  
        byte[] encryptedBytes = Base64.getDecoder().decode(encryptedBase64);  

        // 执行解密  
        byte[] decrypted = cipher.doFinal(encryptedBytes);  

        // 返回明文字符串  
        return new String(decrypted, "UTF-8");  
    }  

    /**  
     * 测试方法  
     */  
    public static void main(String[] args) throws Exception {  
        System.out.println("=== AES 工具类测试 ===\n");  

        // 生成密钥  
        Map<String, String> keyMap = generateKey();  
        String key = keyMap.get("key");  
        String iv = keyMap.get("iv");  

        System.out.println("密钥: " + key);  
        System.out.println("IV: " + iv);  
        System.out.println();  

        // 加密测试  
        String originalText = "Hello AES";  
        System.out.println("原始数据: " + originalText);  

        String encrypted = encrypt(originalText, key, iv);  
        System.out.println("加密结果: " + encrypted);  
        System.out.println();  

        // 解密测试  
        String decrypted = decrypt(encrypted, key, iv);  
        System.out.println("解密结果: " + decrypted);  
        System.out.println();  

        if (originalText.equals(decrypted)) {  
            System.out.println("✅ 加密解密成功!");  
        } else {  
            System.err.println("❌ 加密解密失败!");  
        }  
    }  
}  

package top.sunrains;  

import cn.hutool.core.text.CharSequenceUtil;  
import org.bouncycastle.crypto.digests.SM3Digest;  
import org.bouncycastle.crypto.macs.HMac;  
import org.bouncycastle.crypto.params.KeyParameter;  
import org.bouncycastle.jce.provider.BouncyCastleProvider;  

import java.io.File;  
import java.io.FileInputStream;  
import java.io.IOException;  
import java.io.InputStream;  
import java.security.Security;  
/**  
 * SM3国密哈希算法工具类  
 * 使用Bouncy Castle原生API  
 * 功能:  
 * - 字符串哈希计算  
 * - 字节数组哈希计算  
 * - 文件哈希计算  
 * - HMAC-SM3计算  
 */  
public class SM3Util {  

    static {  
        try {  
            Security.addProvider(new BouncyCastleProvider());  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  

    private static final int BUFFER_SIZE = 8192;  

    /**  
     * 计算字符串的SM3哈希值  
     *  
     * @param data 待哈希的字符串  
     * @return 哈希值(小写十六进制字符串,64个字符)  
     */  
    public static String hash(String data) {  
        if (data == null) {  
            throw new IllegalArgumentException("Data cannot be null");  
        }  
        return hash(data.getBytes(java.nio.charset.StandardCharsets.UTF_8));  
    }  

    /**  
     * 验证字符串的SM3哈希值  
     *  
     * @param data 原始数据  
     * @param expectedHash 期望的哈希值  
     * @return 是否匹配  
     */  
    public static boolean verify(String data, String expectedHash) {  
        String actualHash = hash(data);  
        return actualHash.equalsIgnoreCase(expectedHash);  
    }  

    /**  
     * 计算字节数组的SM3哈希值  
     *  
     * @param data 待哈希的字节数组  
     * @return 哈希值(小写十六进制字符串,64个字符)  
     */  
    public static String hash(byte[] data) {  
        SM3Digest digest = new SM3Digest();  
        digest.update(data, 0, data.length);  
        byte[] result = new byte[digest.getDigestSize()];  
        digest.doFinal(result, 0);  
        return byteArrayToHex(result);  
    }  

    /**  
     * 计算文件的SM3哈希值(支持大文件)  
     *  
     * @param filePath 文件路径  
     * @return 哈希值(小写十六进制字符串,64个字符)  
     * @throws IOException 文件读取异常  
     */  
    public static String hashFile(String filePath) throws IOException {  
        return hashFile(new File(filePath));  
    }  

    /**  
     * 计算文件的SM3哈希值(支持大文件)  
     *  
     * @param file 文件对象  
     * @return 哈希值(小写十六进制字符串,64个字符)  
     * @throws IOException 文件读取异常  
     */  
    public static String hashFile(File file) throws IOException {  
        if (!file.exists()) {  
            throw new IOException("File not found: " + file.getAbsolutePath());  
        }  

        SM3Digest digest = new SM3Digest();  
        byte[] buffer = new byte[BUFFER_SIZE];  

        try (InputStream is = new FileInputStream(file)) {  
            int bytesRead;  
            while ((bytesRead = is.read(buffer)) != -1) {  
                digest.update(buffer, 0, bytesRead);  
            }  
        }  

        byte[] result = new byte[digest.getDigestSize()];  
        digest.doFinal(result, 0);  
        return byteArrayToHex(result);  
    }  

    /**  
     * 计算HMAC-SM3(基于密钥的哈希消息认证码)  
     *  
     * @param data 待计算的数据  
     * @param hexKey 密钥  
     * @return HMAC-SM3结果(小写十六进制字符串,64个字符)  
     */  
    public static String hmac(String data, String hexKey) {  
        if (CharSequenceUtil.isBlank(hexKey)) {  
            throw new IllegalArgumentException("密钥不能为空");  
        }  
        return hmac(data.getBytes(java.nio.charset.StandardCharsets.UTF_8),  
                hexToByteArray(hexKey));  
    }  

    /**  
     * 计算HMAC-SM3(基于密钥的哈希消息认证码)  
     *  
     * @param data 待计算的字节数组  
     * @param hexKey hex密钥字节数组  
     * @return HMAC-SM3结果(小写十六进制字符串,64个字符)  
     */  
    public static String hmac(byte[] data, byte[] hexKey) {  
        HMac hmac = new HMac(new SM3Digest());  
        hmac.init(new KeyParameter(hexKey));  
        hmac.update(data, 0, data.length);  
        byte[] result = new byte[hmac.getMacSize()];  
        hmac.doFinal(result, 0);  
        return byteArrayToHex(result);  
    }  

    /**  
     * byte[] 转 Hex  
     *  
     * @param bytes 字节数组  
     * @return HEX字符串(小写)  
     */  
    private static String byteArrayToHex(byte[] bytes) {  
        if (bytes == null || bytes.length == 0) {  
            return "";  
        }  
        StringBuilder sb = new StringBuilder();  
        for (byte b : bytes) {  
            String hex = Integer.toHexString(b & 0xFF);  
            if (hex.length() == 1) {  
                sb.append('0');  
            }  
            sb.append(hex);  
        }  
        return sb.toString().toLowerCase();  
    }  

    /**  
     * 测试方法  
     */  
    public static void main(String[] args) throws IOException {  
        String testData = "12";  
        System.out.println("SM3: " + hash(testData));  
        String key = "1";  
        key = stringToHex(key);  
        System.out.println("HMAC-SM3 key: " + key);  
        String hmacResult = hmac(testData, key);  
        System.out.println("HMAC-SM3: " + hmacResult);  
        System.out.println("HMAC长度: " + hmacResult.length() + " 字符");  
       System.out.println("文件哈希值: " +  hashFile("/Users/sun/1/t.jpg"));  
    }  

    public static String stringToHex(String str) {  
        if (str == null || str.isEmpty()) {  
            return "";  
        }  
        byte[] bytes = str.getBytes(java.nio.charset.StandardCharsets.UTF_8);  
        return byteArrayToHex(bytes);  
    }  

    /**  
     * HEX 转 byte[]  
     *  
     * @param hex HEX字符串  
     * @return 字节数组  
     */  
    public static byte[] hexToByteArray(String hex) {  
        if (hex == null || hex.length() == 0) {  
            return new byte[0];  
        }  

        int len = hex.length() / 2;  
        byte[] bytes = new byte[len];  
        for (int i = 0; i < len; i++) {  
            int high = Character.digit(hex.charAt(i * 2), 16);  
            int low = Character.digit(hex.charAt(i * 2 + 1), 16);  
            bytes[i] = (byte) ((high << 4) | low);  
        }  
        return bytes;  
    }  
}

各平台实现说明

Android 平台

SM2 实现:

  • 依赖库:BouncyCastle (org.bouncycastle:bcprov-jdk15on:1.70)

  • 特点

    • 使用 ECKeyPairGenerator 生成密钥对
    • 使用 SM2Signer 进行签名和验签(DER 格式)
    • 使用 SM2Engine 进行加密和解密(C1C3C2 格式)
    • 不依赖 JCA Provider,避免兼容性问题

SM4 实现:

  • 依赖库:BouncyCastle

  • 特点

    • 使用 SM4Engine 进行加密和解密
    • CBC 模式 + PKCS7 填充
    • 支持自定义密钥和 IV

iOS 平台

实现方式:GMObjC 原生库

SM2 特点:

  • 使用 GMSm2Utils 进行所有操作
  • 签名自动转换为 DER 格式以兼容其他平台
  • 加密输出 C1C3C2 格式(04 开头)
  • 支持跨平台互通(Android/HarmonyOS)

SM4 特点:

  • 使用 GMSm4Utils 进行加密解密
  • CBC 模式 + PKCS7 填充
  • 密钥和 IV 为 32 位十六进制字符串

HarmonyOS 平台

实现方式: @ohos.security.cryptoFramework

SM2 特点:

  • 使用 HarmonyOS 原生 cryptoFramework API
  • 签名使用 DER 格式
  • 加密使用 C1C3C2 格式
  • 完全符合国密标准

SM3 特点:

  • 使用 createMd (MessageDigest) 进行普通哈希
  • 使用手动实现 HMAC-SM3 (RFC 2104)
  • 支持流式文件哈希处理 (4096 字节缓冲区)
  • 不依赖 createMac,避免密钥参数问题

SM4 特点:

  • 使用 createSymKeyGenerator 生成密钥
  • 使用 createCipher 进行加密解密
  • CBC 模式 + PKCS7 填充
  • 支持 SM4_128 算法

AES 特点:

  • 使用 crypto.createSymKeyGenerator('AES256') 创建密钥生成器
  • 使用 crypto.createCipher('AES|CBC|PKCS7') 进行加解密
  • CBC 模式 + PKCS7 填充
  • 密钥和 IV 为随机字符串(UTF-8 编码)

H5 平台

实现方式:sm-crypto + Web Crypto API

特点:

  • SM2/SM3/SM4:通过 npm 包 sm-crypto 实现
  • AES:使用浏览器原生 Web Crypto API 实现
  • 所有功能均在浏览器环境中运行
  • SM2 签名使用 DER 格式(der: true
  • SM2 默认 userId 为 '1234567812345678'

依赖安装:

npm install --save sm-crypto

微信小程序平台

实现方式:miniprogram-sm-crypto + crypto-js + jsrsasign

特点:

  • 专为小程序优化的国密算法库
  • 完全支持 SM2、SM4 和 AES
  • SM2:密钥生成、签名、验签、加密、解密(使用 miniprogram-sm-crypto)
  • SM4:密钥生成、加密、解密(CBC 模式,使用 miniprogram-sm-crypto)
  • AES:密钥生成、加密、解密(CBC 模式,使用 crypto-js)
  • 需要通过微信开发者工具构建 npm

依赖安装:

npm install --save miniprogram-sm-crypto crypto-js jsrsasign

重要:微信小程序 npm 构建步骤

  1. 确保项目根目录已安装上述依赖
  2. 打开微信开发者工具,勾选「详情」→「本地设置」→「使用 npm 模块」
  3. 点击菜单栏「工具」→「构建 npm」,生成 miniprogram_npm 目录
  4. 重新编译运行小程序

注意事项

1. 密钥格式

SM2 密钥:

  • 公钥:以 04 开头的 130 位十六进制字符串(非压缩格式)
  • 私钥:64 位十六进制字符串

SM4 密钥:

  • 密钥 (key) :32 位十六进制字符串(128 位)
  • 初始向量 (IV) :32 位十六进制字符串(128 位)

AES 密钥:

  • 密钥 (key) :32 字符随机字符串(字母+数字,UTF-8 编码后为 256 位)
  • 初始向量 (IV) :16 字符随机字符串(字母+数字,UTF-8 编码后为 128 位)

2. 签名格式

  • 所有平台的签名输出均为 DER 编码的十六进制字符串
  • DER 格式确保跨平台兼容性

3. 哈希格式

SM3 哈希:

  • 输出长度:64 位十六进制字符串 (256 位)
  • 格式:小写十六进制
  • 示例:a1b2c3d4e5f6... (64 个字符)

HMAC-SM3 哈希:

  • 输出长度:64 位十六进制字符串 (256 位)
  • 密钥格式:十六进制字符串
  • 格式:小写十六进制

4. 加密格式

SM2 加密:

  • 输出格式:C1C3C2(以 04 开头)
  • 兼容 Android BouncyCastle、iOS GMObjC、HarmonyOS cryptoFramework

SM4 加密:

  • 模式:CBC
  • 填充:PKCS7
  • 输出:十六进制字符串

AES 加密:

  • 模式:CBC
  • 填充:PKCS5/PKCS7
  • 密钥:32 字符随机字符串(UTF-8 编码后为 256 位)
  • IV:16 字符随机字符串(UTF-8 编码后为 128 位)
  • 输出:Base64 编码字符串

RSA 加密:

  • 算法:RSA-OAEP with SHA-256 (对应 Java 的 RSA/ECB/OAEPWithSHA-256AndMGF1Padding)
  • 密钥长度:2048 位
  • 私钥格式:PKCS#8 (Base64 编码)
  • 公钥格式:X.509/SPKI (Base64 编码)
  • 输出:Base64 编码字符串
  • 跨平台兼容:Android/iOS/HarmonyOS/H5 完全互通

RSA 签名:

  • 算法:SHA256withRSA (PKCS#1 v1.5)
  • 私钥格式:PKCS#8 (Base64 编码)
  • 公钥格式:X.509/SPKI (Base64 编码)
  • 签名输出:Base64 编码字符串
  • 跨平台兼容:Android/iOS/HarmonyOS/H5/微信小程序 完全互通

5. 跨平台兼容性

✅ 完全兼容:

  • 各平台生成的 SM2 密钥对可以互相使用
  • Android 平台签名的数据可以在 iOS/Harmony/H5 平台验证
  • SM2 加密的数据可以在不同平台间解密
  • SM4 加密的数据可以在 Android/iOS/Harmony 平台间加解密
  • AES 加密的数据可以在所有平台间加解密(Android/iOS/HarmonyOS/H5/微信小程序)
  • SM3 哈希值在所有平台保持一致(相同输入产生相同输出)
  • HMAC-SM3 支持跨平台验证
  • RSA-OAEP-SHA256 加密的数据可以在 Android/iOS/HarmonyOS/H5 平台间加解密
  • SHA256withRSA 签名可以在所有平台间验签(包括微信小程序)

⚠️ 注意事项:

  • 确保使用相同的哈希选项(默认启用)
  • SM4/AES 加密和解密必须使用相同的密钥和 IV
  • 微信小程序需要先构建 npm 才能使用
  • SM3 文件哈希需要确保文件内容完全一致
  • AES 密钥为 32 字符随机字符串,IV 为 16 字符随机字符串
  • RSA 密钥长度为 2048 位,私钥为 PKCS#8 格式,公钥为 X.509/SPKI 格式
  • RSA 加密使用 OAEP with SHA-256 填充,与其他平台互通
  • RSA 签名使用 SHA256withRSA (PKCS#1 v1.5),所有平台兼容
  • 微信小程序端暂不支持 RSA-OAEP-SHA256 加密解密,仅支持签名验签
继续阅读 »

sunrains-smutil 插件使用指南

本插件提供SM2、SM3 、 SM4、AES、RSA 客户端(ios,安卓,鸿蒙,微信小程序,h5),服务端(java)SM2、SM3 、 SM4、AES、RSA 算法的跨平台实现,支持以下功能:
SM2 非对称加密算法
✅ SM2 密钥对生成
✅ SM2 数字签名
✅ SM2 签名验证
✅ SM2 数据加密
✅ SM2 数据解密
SM3 密码杂凑算法
✅ SM3 哈希计算(字符串)
✅ HMAC-SM3 密钥哈希
✅ 文件 SM3 哈希
✅ 哈希值验证
SM4 对称加密算法
✅ SM4 密钥生成(含 IV)
✅ SM4 数据加密(CBC 模式)
✅ SM4 数据解密(CBC 模式)
AES 对称加密算法
✅ AES 密钥和 IV 生成
✅ AES 数据加密(CBC 模式, PKCS7/PKCS5 填充)
✅ AES 数据解密(CBC 模式, PKCS7/PKCS5 填充)
✅ 跨平台兼容(Android/iOS/HarmonyOS/H5/微信小程序)
RSA 非对称加密算法
✅ RSA 密钥对生成(2048位)
✅ RSA-OAEP-SHA256 数据加密
✅ RSA-OAEP-SHA256 数据解密
✅ SHA256withRSA 数字签名(PKCS#1 v1.5)
✅ SHA256withRSA 签名验证(PKCS#1 v1.5)
✅ 跨平台兼容(Android/iOS/HarmonyOS/H5)
⚠️ 微信小程序端暂不支持RSA-OAEP-SHA256加密解密,仅支持签名验签

调用方式

本插件同时支持回调风格异步风格两种调用方式:

方式一:回调风格(传统方式)

import { sm2Sign } from "@/uni_modules/sunrains-util";  

sm2Sign({  
  data: "需要签名的数据",  
  privateKey: "你的私钥",  
  success: (res) => {  
    console.log("签名结果:", res.signature);  
  },  
  fail: (err) => {  
    console.error("签名失败:", err);  
  }  
});

方式二:异步风格(推荐,更简洁)

import { sm2SignAsync } from "@/uni_modules/sunrains-util";  

async function doSign() {  
  try {  
    const result = await sm2SignAsync("需要签名的数据", "你的私钥")  
    console.log("签名结果:", result.signature)  
  } catch (error) {  
    console.error("签名失败:", error)  
  }  
}

平台支持

平台 SM2 实现方式 SM4 实现方式 AES 实现方式 RSA 实现方式 状态
Android BouncyCastle BouncyCastle Java Crypto API Java Crypto API (RSA-OAEP-SHA256) ✅ 完全支持
iOS GMObjC GMObjC CommonCrypto Security Framework (RSA-OAEP-SHA256) ✅ 完全支持
HarmonyOS cryptoFramework cryptoFramework cryptoFramework cryptoFramework (RSA-OAEP-SHA256) ✅ 完全支持
H5 sm-crypto sm-crypto Web Crypto API Web Crypto API (RSA-OAEP-SHA256) ✅ 完全支持
微信小程序 miniprogram-sm-crypto miniprogram-sm-crypto crypto-js jsrsasign (仅签名验签) ⚠️ 部分支持

使用示例

SM2 相关操作

1. 生成 SM2 密钥对

同步方式:

import { sm2GenerateKeyPair } from "@/uni_modules/sunrains-util";  

const result = sm2GenerateKeyPair();  
console.log("公钥:", result.publicKey);  // 04开头的130位十六进制字符串  
console.log("私钥:", result.privateKey); // 64位十六进制字符串

异步方式:

import { sm2GenerateKeyPairAsync } from "@/uni_modules/sunrains-util";  

async function generateKeys() {  
  const result = await sm2GenerateKeyPairAsync()  
  console.log("公钥:", result.publicKey)  
  console.log("私钥:", result.privateKey)  
}

2. SM2 签名

回调风格:

import { sm2Sign } from "@/uni_modules/sunrains-util";  

sm2Sign({  
  data: "需要签名的数据",  
  privateKey: "你的私钥",  
  success: (res) => {  
    console.log("签名结果:", res.signature);  
  },  
  fail: (err) => {  
    console.error("签名失败:", err);  
  }  
});

异步风格(推荐):

import { sm2SignAsync } from "@/uni_modules/sunrains-util";  

async function doSign() {  
  try {  
    const result = await sm2SignAsync("需要签名的数据", "你的私钥")  
    console.log("签名结果:", result.signature)  
  } catch (error) {  
    console.error("签名失败:", error)  
  }  
}

3. SM2 验签

回调风格:

import { sm2Verify } from "@/uni_modules/sunrains-util";  

sm2Verify({  
  data: "原始数据",  
  signature: "签名值",  
  publicKey: "你的公钥",  
  success: (res) => {  
    console.log("验签结果:", res.valid); // true 或 false  
  },  
  fail: (err) => {  
    console.error("验签失败:", err);  
  }  
});

异步风格(推荐):

import { sm2VerifyAsync } from "@/uni_modules/sunrains-util";  

async function doVerify() {  
  try {  
    const result = await sm2VerifyAsync("原始数据", "签名值", "你的公钥")  
    console.log("验签结果:", result.valid) // true 或 false  
  } catch (error) {  
    console.error("验签失败:", error)  
  }  
}

完整实战示例

以下是一个完整的国密算法测试页面示例,及服务端(java)工具类,展示了所有功能的实际使用:

客户端 Vue 3 Composition API 示例

 <template>  
  <view class="order-content">  
    <view class="order-header">  
      <text class="order-title">国密算法测试</text>  
    </view>  

    <scroll-view class="order-list" scroll-y="true">  
      <view class="section">  
        <view class="section-title">SM4 对称加密</view>  

        <view class="input-group">  
          <text class="label">待加密内容:</text>  
          <input v-model="sm4Content" placeholder="请输入要加密的内容" class="input" />  
        </view>  

        <button @click="testSm4Encrypt" class="btn btn-primary">SM4 加密</button>  

        <view class="result-box" v-if="sm4Key.key">  
          <text class="result-label">密钥 (key):</text>  
          <text selectable="true" class="result-value">{{ sm4Key.key }}</text>  
        </view>  

        <view class="result-box" v-if="sm4Key.iv">  
          <text class="result-label">初始向量 (iv):</text>  
          <text selectable="true" class="result-value">{{ sm4Key.iv }}</text>  
        </view>  

        <view class="result-box" v-if="sm4Encrypted">  
          <text class="result-label">加密结果:</text>  
          <text selectable="true" class="result-value">{{ sm4Encrypted }}</text>  
        </view>  
        <button @click="testSm4Decrypt" :disabled="sm4Encrypted.length==0" class="btn btn-secondary">SM4 解密</button>  
        <button @click="testSm4Decrypt" :disabled="sm4Encrypted === ''" :class="sm4Encrypted === '' ? 'btn btn-secondary disabled' : 'btn btn-secondary'" class="btn btn-secondary">SM4 解密</button>  

        <view class="result-box" v-if="sm4Decrypted">  
          <text class="result-label">解密结果:</text>  
          <text selectable="true" class="result-value success">{{ sm4Decrypted }}</text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">SM2 数字签名</view>  

        <view class="input-group">  
          <text class="label">待签名内容:</text>  
          <input v-model="sm2SignContent" placeholder="请输入要签名的内容" class="input" />  
        </view>  

        <button @click="testSm2Sign" class="btn btn-primary">生成密钥对并签名</button>  

        <view class="result-box" v-if="sm2KeyPair.publicKey">  
          <text class="result-label">公钥:</text>  
          <text selectable="true" class="result-value small">{{ sm2KeyPair.publicKey }}</text>  
        </view>  

        <view class="result-box" v-if="sm2KeyPair.privateKey">  
          <text class="result-label">私钥:</text>  
          <text selectable="true" class="result-value small">{{ sm2KeyPair.privateKey }}</text>  
        </view>  

        <view class="result-box" v-if="sm2Signature">  
          <text class="result-label">签名结果:</text>  
          <text selectable="true" class="result-value small">{{ sm2Signature }}</text>  
        </view>  

        <button @click="testSm2Verify" :disabled="sm2Signature.length==0" class="btn btn-secondary">验签</button>  

        <view class="result-box" v-if="sm2VerifyResult !== null">  
          <text class="result-label">验签结果:</text>  
          <text class="result-value" :class="sm2VerifyResult ==true? 'success' : 'error'">  
            {{ sm2VerifyResult ==true ? '✅ 验签成功' : '❌ 验签失败' }}  
          </text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">SM2 非对称加密</view>  

        <view class="input-group">  
          <text class="label">待加密内容:</text>  
          <input v-model="sm2EncryptContent" placeholder="请输入要加密的内容" class="input" />  
        </view>  

        <button @click="testSm2Encrypt" class="btn btn-primary">SM2 加密</button>  

        <view class="result-box" v-if="sm2EncryptKeyPair.publicKey">  
          <text class="result-label">公钥:</text>  
          <text selectable="true" class="result-value small">{{ sm2EncryptKeyPair.publicKey }}</text>  
        </view>  

        <view class="result-box" v-if="sm2EncryptKeyPair.privateKey">  
          <text class="result-label">私钥:</text>  
          <text selectable="true" class="result-value small">{{ sm2EncryptKeyPair.privateKey }}</text>  
        </view>  

        <view class="result-box" v-if="sm2Encrypted">  
          <text class="result-label">加密结果:</text>  
          <text selectable="true" class="result-value small">{{ sm2Encrypted }}</text>  
        </view>  

        <button @click="testSm2Decrypt" :disabled="sm2Encrypted.length==0" class="btn btn-secondary">SM2 解密</button>  

        <view class="result-box" v-if="sm2Decrypted">  
          <text class="result-label">解密结果:</text>  
          <text selectable="true" class="result-value success">{{ sm2Decrypted }}</text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">SM3 哈希算法</view>  

        <!-- 1. SM3 哈希 -->  
        <view class="input-group">  
          <text class="label">待哈希内容:</text>  
          <input v-model="sm3Content" placeholder="请输入要哈希的内容" class="input" />  
        </view>  

        <button @click="testSm3Hash" class="btn btn-primary">SM3 哈希</button>  

        <view class="result-box" v-if="sm3HashResult">  
          <text class="result-label">SM3 哈希值:</text>  
          <text selectable="true" class="result-value small">{{ sm3HashResult }}</text>  
        </view>  

        <!-- 2. SM3 哈希验证 -->  
        <view class="divider"></view>  
        <view class="sub-title">SM3 哈希验证</view>  

        <view class="input-group">  
          <text class="label">验证 - 原始内容:</text>  
          <input v-model="sm3VerifyContent" placeholder="请输入原始内容" class="input" />  
        </view>  

        <button @click="testSm3Verify" :disabled="sm3VerifyContent.length == 0" class="btn btn-secondary">验证哈希值</button>  

        <view class="result-box" v-if="sm3HashMatchResult !== null">  
          <text class="result-label">哈希匹配结果:</text>  
          <text class="result-value" :class="sm3HashMatchResult == true ? 'success' : 'error'">  
            {{ sm3HashMatchResult == true ? '✅ 哈希值匹配' : '❌ 哈希值不匹配' }}  
          </text>  
        </view>  

        <!-- 3. HMAC-SM3 哈希 -->  
        <view class="divider"></view>  
        <view class="sub-title">HMAC-SM3 哈希</view>  

        <view class="input-group">  
          <text class="label">HMAC-SM3 待哈希内容:</text>  
          <input v-model="sm3HmacContent" placeholder="请输入要哈希的内容" class="input" />  
        </view>  

        <view class="input-group">  
          <text class="label">HMAC-SM3 密钥(HEX):</text>  
          <input v-model="sm3HmacKey" placeholder="请输入HMAC密钥(HEX格式)" class="input" />  
        </view>  

        <button @click="testSm3HmacHash" :disabled="sm3HmacContent.length == 0 || sm3HmacKey.length == 0" class="btn btn-primary">HMAC-SM3 哈希</button>  

        <view class="result-box" v-if="sm3HmacHashResult">  
          <text class="result-label">HMAC-SM3 哈希值:</text>  
          <text selectable="true" class="result-value small">{{ sm3HmacHashResult }}</text>  
        </view>  

        <!-- 4. HMAC-SM3 哈希验证 -->  
        <view class="divider"></view>  
        <view class="sub-title">HMAC-SM3 哈希验证</view>  

        <view class="input-group">  
          <text class="label">验证 - 原始内容:</text>  
          <input v-model="sm3HmacVerifyContent" placeholder="请输入原始内容" class="input" />  
        </view>  

        <view class="input-group">  
          <text class="label">验证 - HMAC 密钥:</text>  
          <input v-model="sm3HmacVerifyKey" placeholder="请输入HMAC密钥" class="input" />  
        </view>  

        <view class="input-group">  
          <text class="label">验证 - 期望哈希值:</text>  
          <input v-model="sm3HmacExpectedHash" placeholder="请输入期望的哈希值" class="input" />  
        </view>  

        <button @click="testSm3HmacHashWithExpected" :disabled="sm3HmacVerifyContent.length == 0 || sm3HmacVerifyKey.length == 0 || sm3HmacExpectedHash.length == 0" class="btn btn-secondary">验证 HMAC-SM3 哈希值</button>  

        <view class="result-box" v-if="sm3HmacHashMatchResult !== null">  
          <text class="result-label">HMAC-SM3 哈希匹配结果:</text>  
          <text class="result-value" :class="sm3HmacHashMatchResult == true ? 'success' : 'error'">  
            {{ sm3HmacHashMatchResult == true ? '✅ 哈希值匹配' : '❌ 哈希值不匹配' }}  
          </text>  
        </view>  

        <!-- 5. 文件 SM3 哈希 -->  
        <view class="divider"></view>  
        <view class="sub-title">文件 SM3 哈希</view>  

        <button @click="chooseImageAndHash" class="btn btn-primary">选择图片并计算 SM3</button>  
        <button @click="chooseFileAndHash" class="btn btn-primary">选择文件并计算 SM3</button>  
        <view class="result-box" v-if="sm3FileName">  
          <text class="result-label">文件名:</text>  
          <text class="result-value">{{ sm3FileName }}</text>  
        </view>  

        <view class="result-box" v-if="sm3FileHashResult">  
          <text class="result-label">文件 SM3 哈希值:</text>  
          <text selectable="true" class="result-value small">{{ sm3FileHashResult }}</text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">AES 对称加密</view>  

        <view class="input-group">  
          <text class="label">待加密内容:</text>  
          <input v-model="aesContent" placeholder="请输入要加密的内容" class="input" />  
        </view>  

        <button @click="testAesEncrypt" class="btn btn-primary">AES 加密</button>  

        <view class="result-box" v-if="aesKey.key">  
          <text class="result-label">密钥 (key):</text>  
          <text selectable="true" class="result-value">{{ aesKey.key }}</text>  
        </view>  

        <view class="result-box" v-if="aesKey.iv">  
          <text class="result-label">初始向量 (iv):</text>  
          <text selectable="true" class="result-value">{{ aesKey.iv }}</text>  
        </view>  

        <view class="result-box" v-if="aesEncrypted">  
          <text class="result-label">加密结果:</text>  
          <text selectable="true" class="result-value">{{ aesEncrypted }}</text>  
        </view>  

        <button @click="testAesDecrypt" :disabled="aesEncrypted.length==0" class="btn btn-secondary">AES 解密</button>  

        <view class="result-box" v-if="aesDecrypted">  
          <text class="result-label">解密结果:</text>  
          <text selectable="true" class="result-value success">{{ aesDecrypted }}</text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">RSA 非对称加密</view>  

        <view class="input-group">  
          <text class="label">待加密内容:</text>  
          <input v-model="rsaContent" placeholder="请输入要加密的内容" class="input" />  
        </view>  

        <button @click="testRsaEncrypt" class="btn btn-primary">RSA 加密</button>  

        <view class="result-box" v-if="rsaKeyPair.publicKey">  
          <text class="result-label">公钥:</text>  
          <text selectable="true" class="result-value small">{{ rsaKeyPair.publicKey }}</text>  
        </view>  

        <view class="result-box" v-if="rsaKeyPair.privateKey">  
          <text class="result-label">私钥:</text>  
          <text selectable="true" class="result-value small">{{ rsaKeyPair.privateKey }}</text>  
        </view>  

        <view class="result-box" v-if="rsaEncrypted">  
          <text class="result-label">加密结果:</text>  
          <text selectable="true" class="result-value small">{{ rsaEncrypted }}</text>  
        </view>  

        <button @click="testRsaDecrypt" :disabled="rsaEncrypted.length==0" class="btn btn-secondary">RSA 解密</button>  

        <view class="result-box" v-if="rsaDecrypted">  
          <text class="result-label">解密结果:</text>  
          <text selectable="true" class="result-value success">{{ rsaDecrypted }}</text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">RSA 数字签名</view>  

        <view class="input-group">  
          <text class="label">待签名内容:</text>  
          <input v-model="rsaSignContent" placeholder="请输入要签名的内容" class="input" />  
        </view>  

        <button @click="testRsaSign" class="btn btn-primary">生成密钥对并签名</button>  

        <view class="result-box" v-if="rsaSignKeyPair.publicKey">  
          <text class="result-label">公钥:</text>  
          <text selectable="true" class="result-value small">{{ rsaSignKeyPair.publicKey }}</text>  
        </view>  

        <view class="result-box" v-if="rsaSignKeyPair.privateKey">  
          <text class="result-label">私钥:</text>  
          <text selectable="true" class="result-value small">{{ rsaSignKeyPair.privateKey }}</text>  
        </view>  

        <view class="result-box" v-if="rsaSignature">  
          <text class="result-label">签名结果:</text>  
          <text selectable="true" class="result-value small">{{ rsaSignature }}</text>  
        </view>  

        <button @click="testRsaVerify" :disabled="rsaSignature.length==0" class="btn btn-secondary">验签</button>  

        <view class="result-box" v-if="rsaVerifyResult !== null">  
          <text class="result-label">验签结果:</text>  
          <text class="result-value" :class="rsaVerifyResult ==true? 'success' : 'error'">  
            {{ rsaVerifyResult ==true ? '✅ 验签成功' : '❌ 验签失败' }}  
          </text>  
        </view>  
      </view>  

      <view class="order-item" @click="goToDetail(1002, '已完成')">  
        <view class="order-info">  
          <text class="order-id">订单编号:1002</text>  
          <text class="order-status">状态:已完成</text>  
        </view>  
        <view class="order-btn">  
          <text class="go-detail">查看详情</text>  
        </view>  
      </view>  
    </scroll-view>  
  </view>  
</template>  

<script setup lang="uts">  
  import {IdcardOcrModel}from "@/uni_modules/sunrains-idcard-ocr/utssdk/interface.uts"  
  import {Response} from "@/uni_modules/sunrains-utils/index.uts"  

  import * as SM from "@/uni_modules/sunrains-smutil"  

  // ==================== SM4 相关数据 ====================  
  const sm4Content = ref("Hello World")  
  const sm4Key = ref<SM.Sm4Key>({ key: '', iv: '' })  
  const sm4Encrypted = ref("")  
  const sm4Decrypted = ref("")  

  // ==================== SM2 签名验签相关数据 ====================  
  const sm2SignContent = ref("123")  
  const sm2KeyPair = ref<SM.Sm2KeyPair>({ publicKey: '', privateKey: '' })  
  const sm2Signature = ref("")  
  const sm2VerifyResult = ref<boolean | null>(null)  

  // ==================== SM2 加密解密相关数据 ====================  
  const sm2EncryptContent = ref("Hello SM2")  
  const sm2EncryptKeyPair = ref<SM.Sm2KeyPair>({ publicKey: '', privateKey: '' })  
  const sm2Encrypted = ref("")  
  const sm2Decrypted = ref("")  

  // ==================== SM3 相关数据 ====================  
  const sm3Content = ref("Hello SM3")  
  const sm3HashResult = ref("")  
  const sm3VerifyContent = ref("")  
  const sm3HashMatchResult = ref<boolean | null>(null)  

  const sm3HmacContent = ref("Hello HMAC-SM3")  
  const sm3HmacKey = ref("0123456789abcdef0123456789abcdef")  // 默认 HMAC 密钥(32字节HEX)  
  const sm3HmacHashResult = ref("")  
  const sm3HmacVerifyContent = ref("")  
  const sm3HmacVerifyKey = ref("")  
  const sm3HmacExpectedHash = ref("")  
  const sm3HmacHashMatchResult = ref<boolean | null>(null)  
  const sm3VerifyResult = ref<boolean|null>(null)  

  const sm3FileName = ref("")  
  const sm3FileHashResult = ref("")  

  // ==================== AES 相关数据 ====================  
  const aesContent = ref("Hello AES")  
  const aesKey = ref<SM.AesKey>({ key: '', iv: '' })  
  const aesEncrypted = ref("")  
  const aesDecrypted = ref("")  

  // ==================== RSA 加密解密相关数据 ====================  
  const rsaContent = ref("Hello RSA")  
  const rsaKeyPair = ref<SM.RsaKeyPair>({ publicKey: '', privateKey: '' })  
  const rsaEncrypted = ref("")  
  const rsaDecrypted = ref("")  

  // ==================== RSA 签名验签相关数据 ====================  
  const rsaSignContent = ref("需要签名的数据")  
  const rsaSignKeyPair = ref<SM.RsaKeyPair>({ publicKey: '', privateKey: '' })  
  const rsaSignature = ref("")  
  const rsaVerifyResult = ref<boolean | null>(null)  

  // ==================== SM4 加密 ====================  
  async function testSm4Encrypt() {  
      if (sm4Content.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥  
          const key = await SM.sm4GenerateKeyAsync()  
          sm4Key.value = { key: key.key, iv: key.iv }  
          console.log("SM4 密钥:", key)  

          // 加密  
          const encryptResult = await SM.sm4EncryptAsync(sm4Content.value, key.key, key.iv)  
          sm4Encrypted.value = encryptResult.result  
          console.log("SM4 加密成功:", encryptResult.result)  
          uni.showToast({ title: '加密成功', icon: 'success' })  

      } catch (e) {  
          console.error("SM4 加密失败:", e)  
          uni.showToast({ title: '加密失败', icon: 'error' })  
      }  
  }  

  // ==================== SM4 解密 ====================  
  async function testSm4Decrypt() {  
      if (sm4Encrypted.value.length==0 || sm4Key.value.key.length==0) {  
          uni.showToast({ title: '请先加密', icon: 'none' })  
          return  
      }  

      try {  
          const decryptResult = await SM.sm4DecryptAsync(sm4Encrypted.value, sm4Key.value.key, sm4Key.value.iv)  
          sm4Decrypted.value = decryptResult.result  
          console.log("SM4 解密成功:", decryptResult.result)  
          if (decryptResult.result == sm4Content.value) {  
              uni.showToast({ title: '解密成功且内容一致', icon: 'success' })  
          } else {  
              uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("SM4 解密失败:", e)  
          uni.showToast({ title: '解密失败', icon: 'error' })  
      }  
  }  

  // ==================== AES 加密 ====================  
  async function testAesEncrypt() {  
      if (aesContent.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥和 IV  
          const key = await SM.aesGenerateKeyAsync()  
          aesKey.value = { key: key.key, iv: key.iv }  
          console.log("AES 密钥:", key)  

          // 加密  
          const encryptResult = await SM.aesEncryptAsync(aesContent.value, key.key, key.iv)  
          aesEncrypted.value = encryptResult.result  
          console.log("AES 加密成功:", encryptResult.result)  
          uni.showToast({ title: '加密成功', icon: 'success' })  

      } catch (e) {  
          console.error("AES 加密失败:", e)  
          uni.showToast({ title: '加密失败', icon: 'error' })  
      }  
  }  

  // ==================== AES 解密 ====================  
  async function testAesDecrypt() {  
      if (aesEncrypted.value.length==0 || aesKey.value.key.length==0) {  
          uni.showToast({ title: '请先加密', icon: 'none' })  
          return  
      }  

      try {  
          const decryptResult = await SM.aesDecryptAsync(aesEncrypted.value, aesKey.value.key, aesKey.value.iv)  
          aesDecrypted.value = decryptResult.result  
          console.log("AES 解密成功:", decryptResult.result)  
          if (decryptResult.result == aesContent.value) {  
              uni.showToast({ title: '解密成功且内容一致', icon: 'success' })  
          } else {  
              uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("AES 解密失败:", e)  
          uni.showToast({ title: '解密失败', icon: 'error' })  
      }  
  }  

  // ==================== RSA 加密 ====================  
  async function testRsaEncrypt() {  
      if (rsaContent.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥对  
          const keyPair = await SM.rsaGenerateKeyPairAsync()  
          rsaKeyPair.value = {   
              publicKey: keyPair.publicKey,   
              privateKey: keyPair.privateKey   
          }  
          console.log("RSA 公钥:", keyPair.publicKey)  
          console.log("RSA 私钥:", keyPair.privateKey)  

          // 加密  
          const encryptResult = await SM.rsaEncryptAsync(rsaContent.value, keyPair.publicKey)  
          rsaEncrypted.value = encryptResult.result  
          console.log("RSA 加密结果:", encryptResult.result)  
          uni.showToast({ title: '加密成功', icon: 'success' })  
      } catch (e) {  
          console.error("RSA 加密失败:", e)  
          uni.showToast({ title: '加密失败', icon: 'error' })  
      }  
  }  

  // ==================== RSA 解密 ====================  
  async function testRsaDecrypt() {  
      if (rsaEncrypted.value.length==0 || rsaKeyPair.value.privateKey.length==0) {  
          uni.showToast({ title: '请先加密', icon: 'none' })  
          return  
      }  

      try {  
          const decryptResult = await SM.rsaDecryptAsync(rsaEncrypted.value, rsaKeyPair.value.privateKey)  
          rsaDecrypted.value = decryptResult.result  
          console.log("RSA 解密结果:", decryptResult.result)  

          if (decryptResult.result == rsaContent.value) {  
              uni.showToast({ title: '解密成功且内容一致', icon: 'success' })  
          } else {  
              uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("RSA 解密异常:", e)  
          uni.showToast({ title: '解密异常', icon: 'error' })  
      }  
  }  

  // ==================== RSA 签名 ====================  
  async function testRsaSign() {  
      if (rsaSignContent.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥对  
          const keyPair = await SM.rsaGenerateKeyPairAsync()  
          rsaSignKeyPair.value = {   
              publicKey: keyPair.publicKey,   
              privateKey: keyPair.privateKey   
          }  
          console.log("RSA 公钥:", keyPair.publicKey)  
          console.log("RSA 私钥:", keyPair.privateKey)  

          // 签名  
          const signResult = await SM.rsaSignAsync(rsaSignContent.value, keyPair.privateKey)  
          rsaSignature.value = signResult.signature  
          console.log("RSA 签名成功:", signResult.signature)  
          uni.showToast({ title: '签名成功', icon: 'success' })  
      } catch (e) {  
          console.error("RSA 签名失败:", e)  
          uni.showToast({ title: '签名失败', icon: 'error' })  
      }  
  }  

  // ==================== RSA 验签 ====================  
  async function testRsaVerify() {  
      if (rsaSignature.value.length==0 || rsaSignKeyPair.value.publicKey.length==0) {  
          uni.showToast({ title: '请先签名', icon: 'none' })  
          return  
      }  

      try {  
          // 验签  
          const verifyResult = await SM.rsaVerifyAsync(rsaSignContent.value, rsaSignature.value, rsaSignKeyPair.value.publicKey)  
          rsaVerifyResult.value = verifyResult.valid  
          console.log("RSA 验签结果:", verifyResult.valid)  
          if (verifyResult.valid) {  
              uni.showToast({ title: '验签成功', icon: 'success' })  
          } else {  
              uni.showToast({ title: '验签失败', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("RSA 验签异常:", e)  
          uni.showToast({ title: '验签异常', icon: 'error' })  
      }  
  }  

  // ==================== SM2 签名 ====================  
  async function testSm2Sign() {  
      if (sm2SignContent.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥对  
          const keyPair = await SM.sm2GenerateKeyPairAsync()  
          sm2KeyPair.value = {   
              publicKey: keyPair.publicKey,   
              privateKey: keyPair.privateKey   
          }  
          console.log("SM2 公钥:", keyPair.publicKey)  
          console.log("SM2 私钥:", keyPair.privateKey)  

          // 签名  
          const signResult = await SM.sm2SignAsync(sm2SignContent.value, keyPair.privateKey)  
          sm2Signature.value = signResult.signature  
          console.log("SM2 签名成功:", signResult.signature)  
          uni.showToast({ title: '签名成功', icon: 'success' })  
      } catch (e) {  
          console.error("SM2 生成密钥对失败:", e)  
          uni.showToast({ title: '生成密钥对失败', icon: 'error' })  
      }  
  }  

  // ==================== SM2 验签 ====================  
  async function testSm2Verify() {  
      if (sm2Signature.value.length==0 || sm2KeyPair.value.publicKey.length==0) {  
          uni.showToast({ title: '请先签名', icon: 'none' })  
          return  
      }  

      try {  
          // 验签  
          const verifyResult = await SM.sm2VerifyAsync(sm2SignContent.value, sm2Signature.value, sm2KeyPair.value.publicKey)  
          sm2VerifyResult.value = verifyResult.valid  
          console.log("SM2 验签结果:", verifyResult.valid)  
          if (verifyResult.valid) {  
              uni.showToast({ title: '验签成功', icon: 'success' })  
          } else {  
              uni.showToast({ title: '验签失败', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("SM2 验签异常:", e)  
          uni.showToast({ title: '验签异常', icon: 'error' })  
      }  
  }  

  // ==================== SM2 加密 ====================  
  async function testSm2Encrypt() {  
      if (sm2EncryptContent.value.length==0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥对  
          const keyPair = await SM.sm2GenerateKeyPairAsync()  
          sm2EncryptKeyPair.value = {   
              publicKey: keyPair.publicKey,   
              privateKey: keyPair.privateKey   
          }  
          console.log("SM2 加密公钥:", keyPair.publicKey)  
          console.log("SM2 加密私钥:", keyPair.privateKey)  

          // 加密  
          const encryptResult = await SM.sm2EncryptAsync(sm2EncryptContent.value, keyPair.publicKey)  
          sm2Encrypted.value = encryptResult.encrypted  
          console.log("SM2 加密结果:", encryptResult.encrypted)  
          uni.showToast({ title: '加密成功', icon: 'success' })  
      } catch (e) {  
          console.error("SM2 生成密钥对失败:", e)  
          uni.showToast({ title: '生成密钥对失败', icon: 'error' })  
      }  
  }  

  // ==================== SM2 解密 ====================  
  async function testSm2Decrypt() {  
      if (sm2Encrypted.value.length==0 || sm2EncryptKeyPair.value.privateKey.length==0) {  
          uni.showToast({ title: '请先加密', icon: 'none' })  
          return  
      }  

      try {  
          const decryptResult = await SM.sm2DecryptAsync(sm2Encrypted.value, sm2EncryptKeyPair.value.privateKey)  
          sm2Decrypted.value = decryptResult.decrypted  
          console.log("SM2 解密结果:", decryptResult.decrypted)  

          if (decryptResult.decrypted == sm2EncryptContent.value) {  
              uni.showToast({ title: '解密成功且内容一致', icon: 'success' })  
          } else {  
              uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("SM2 解密异常:", e)  
          uni.showToast({ title: '解密异常', icon: 'error' })  
      }  
  }  

  // ==================== SM3 哈希 ====================  
  async function testSm3Hash() {  
      if (sm3Content.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          const hashResult = await SM.sm3HashAsync(sm3Content.value)  
          sm3HashResult.value = hashResult.resultHash  

          console.log("SM3 哈希成功:", hashResult.resultHash)  
          uni.showToast({ title: '哈希成功', icon: 'success' })  
      } catch (e) {  
          console.error("SM3 哈希失败:", e)  
          uni.showToast({ title: '哈希失败', icon: 'error' })  
      }  
  }  

  // ==================== HMAC-SM3 哈希 ====================  
  async function testSm3HmacHash() {  
      if (sm3HmacContent.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      if (sm3HmacKey.value.length == 0) {  
          uni.showToast({ title: '请输入HMAC密钥', icon: 'none' })  
          return  
      }  

      try {  
          const hmacHashResult = await SM.sm3HmacHashAsync(sm3HmacContent.value, sm3HmacKey.value)  
          sm3HmacHashResult.value = hmacHashResult.resultHash  
          sm3HmacExpectedHash.value  =  hmacHashResult.resultHash  
          sm3HmacVerifyKey.value = sm3HmacKey.value  
          sm3HmacVerifyContent.value = sm3HmacContent.value  
          console.log("HMAC-SM3 哈希成功:", hmacHashResult.resultHash)  
          uni.showToast({ title: 'HMAC-SM3 哈希成功', icon: 'success' })  
      } catch (e) {  
          console.error("HMAC-SM3 哈希失败:", e)  
          uni.showToast({ title: 'HMAC-SM3 哈希失败', icon: 'error' })  
      }  
  }  

  // ==================== HMAC-SM3 验证(输入期望哈希值) ====================  
  async function testSm3HmacHashWithExpected() {  
      if (sm3HmacVerifyContent.value.length == 0) {  
          uni.showToast({ title: '请输入原始内容', icon: 'none' })  
          return  
      }  

      try {  
          // 计算实际 HMAC-SM3 哈希值  
          const res = await SM.sm3HmacVerifyAsync(sm3HmacVerifyContent.value, sm3HmacVerifyKey.value,sm3HmacHashResult.value)  
          if (res) {  
              uni.showToast({ title: 'HMAC-SM3 哈希值匹配', icon: 'success' })  
          } else {  
              uni.showToast({ title: 'HMAC-SM3 哈希值不匹配', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("HMAC-SM3 哈希验证异常:", e)  
          uni.showToast({ title: '验证异常', icon: 'error' })  
      }  
  }  

  // ==================== SM3 验证 ====================  
  async function testSm3Verify() {  
      if (sm3HashResult.value.length == 0 || sm3Content.value.length == 0) {  
          uni.showToast({ title: '请先计算哈希', icon: 'none' })  
          return  
      }  

      try {  
          // 重新计算哈希并验证  
          const res = await SM.sm3VerifyAsync(sm3VerifyContent.value,sm3HashResult.value)  
          sm3VerifyResult.value = res  
          console.log("SM3 验证结果:", sm3VerifyResult.value)  

          if (sm3VerifyResult.value!) {  
              uni.showToast({ title: '验证成功', icon: 'success' })  
          } else {  
              uni.showToast({ title: '验证失败', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("SM3 验证异常:", e)  
          uni.showToast({ title: '验证异常', icon: 'error' })  
      }  
  }  

  // ==================== 选择文件并计算 SM3 哈希 ====================  
  async function processFileHash(filePath: string, fileSize?: number) {  
      // 计算文件 SM3 哈希  
      uni.showLoading({ title: '计算中...' })  

      try {  
          console.log("开始计算文件 SM3 哈希")  
          console.log("文件路径:", filePath)  

          const result = await SM.sm3FileHashAsync(filePath)  
          sm3FileHashResult.value = result.resultHash  
          console.log("文件 SM3 哈希成功:", result.resultHash)  
          uni.hideLoading()  
          uni.showToast({ title: '文件哈希成功', icon: 'success' })  
      } catch (e) {  
          console.error("文件 SM3 哈希失败:", e)  
          uni.hideLoading()  
          uni.showToast({ title: '文件哈希失败', icon: 'error' })  
      }  
  }  

  function chooseFileAndHash() {  
      uni.chooseFile({  
          count: 1,  
          success: (res) => {  
              const tempFiles = res.tempFiles  
              if (tempFiles != null && tempFiles.length > 0) {  
                  const file = tempFiles[0]  
                  const fileName = file.name  
                  const filePath = file.path  
                  const fileSize = file.size  
                  sm3FileName.value = fileName != null ? fileName : "未知文件"  

                  if (filePath != null) {  
                      processFileHash(filePath, fileSize)  
                  }  
              }  
          },  
          fail: (err) => {  
              console.error("选择文件失败:", err)  
              if (err.errMsg != null && !err.errMsg.includes('cancel')) {  
                  uni.showToast({ title: '选择文件失败', icon: 'error' })  
              }  
          }  
      })  
  }  

  // ==================== 选择相册图片并计算 SM3 哈希 ====================  
  function chooseImageAndHash() {  
      uni.chooseImage({  
          count: 1,  
          sizeType: ['original'],  // 只选择原图,不压缩  
          sourceType: ['album', 'camera'],  
          success: (res) => {  
              const tempFilePaths = res.tempFilePaths  
              if (tempFilePaths != null && tempFilePaths.length > 0) {  
                  const filePath = tempFilePaths[0]  
                  const fileName = "图片文件"  
                  sm3FileName.value = fileName  
                  console.log("选择的图片(原图):", filePath)  

                  if (filePath != null) {  
                      // 调用异步函数处理文件哈希  
                      processFileHash(filePath)  
                  }  
              }  
          },  
          fail: (err) => {  
              console.error("选择图片失败:", err)  
              if (err.errMsg != null && !err.errMsg.includes('cancel')) {  
                  uni.showToast({ title: '选择图片失败', icon: 'error' })  
              }  
          }  
      })  
  }  

  const goToDetail = (orderId: number, status: string) => {  
    uni.setStorageSync('fromTabBar', true)  
    uni.navigateTo({  
      url: `/pages/detail/detail?orderId=${orderId}&status=${status}`,  
      success: () => {  
        console.log(`跳转到order${orderId}的详情页`)  
      }  
    })  
  }  

  function btn (){  

  }  

</script>  

<style scoped>  
  .order-content {  
    flex: 1;  
    display: flex;  
    flex-direction: column;  
    background-color: #f5f5f5;  
  }  

  .order-header {  
    height: 48px;  
    background-color: #fff;  
    align-items: center;  
    justify-content: center;  
    border-bottom-width: 1px;  
    border-bottom-style: solid;  
    border-bottom-color: #eee;  
  }  

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

  .order-list {  
    flex: 1;  
    padding: 10px;  
  }  

  /* 区块样式 */  
  .section {  
    background-color: #fff;  
    border-radius: 8px;  
    padding: 15px;  
    margin-bottom: 15px;  
  }  

  .section-title {  
    font-size: 16px;  
    font-weight: bold;  
    color: #333;  
    margin-bottom: 15px;  
    padding-bottom: 10px;  
    border-bottom-width: 1px;  
    border-bottom-style: solid;  
    border-bottom-color: #eee;  
  }  

  .input-group {  
    margin-bottom: 15px;  
  }  

  .label {  
    font-size: 14px;  
    color: #666;  
    margin-bottom: 8px;  
  }  

  .input {  
    height: 40px;  
    border-width: 1px;  
    border-style: solid;  
    border-color: #ddd;  
    border-radius: 4px;  
    padding: 0 10px;  
    font-size: 14px;  
    background-color: #fafafa;  
  }  

  /* 按钮样式 */  
  .btn {  
    height: 44px;  
    border-radius: 4px;  
    font-size: 15px;  
    margin-bottom: 10px;  
  }  

  .btn-primary {  
    background-color: #007aff;  
    color: #fff;  
  }  

  .btn-secondary {  
    background-color: #34c759;  
    color: #fff;  
  }  

  .btn:disabled {  
    opacity: 0.5;  
  }  

  /* 结果展示框 */  
  .result-box {  
    margin-top: 10px;  
    padding: 10px;  
    background-color: #f9f9f9;  
    border-radius: 4px;  
    border-left-width: 3px;  
    border-left-style: solid;  
    border-left-color: #007aff;  
  }  

  .result-label {  
    font-size: 12px;  
    color: #999;  
    margin-bottom: 5px;  
  }  

  .result-value {  
    font-size: 13px;  
    color: #333;  
    line-height: 1.5;  
  }  

  .result-value.small {  
    font-size: 11px;  
  }  

  .result-value.success {  
    color: #34c759;  
    font-weight: bold;  
  }  

  .result-value.error {  
    color: #ff3b30;  
    font-weight: bold;  
  }  

  .divider {  
    height: 1px;  
    background-color: #eee;  
    margin: 15px 0;  
  }  

  .sub-title {  
    font-size: 14px;  
    font-weight: bold;  
    color: #666;  
    margin-bottom: 10px;  
  }  
</style>

服务端(java工具类)

package top.sunrains;  

import org.bouncycastle.crypto.digests.SM3Digest;  
import org.bouncycastle.crypto.engines.SM2Engine;  
import org.bouncycastle.crypto.params.ECDomainParameters;  
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;  
import org.bouncycastle.crypto.params.ECPublicKeyParameters;  
import org.bouncycastle.crypto.params.ParametersWithRandom;  
import org.bouncycastle.crypto.signers.SM2Signer;  
import org.bouncycastle.jce.ECNamedCurveTable;  
import org.bouncycastle.jce.spec.ECParameterSpec;  
import org.bouncycastle.util.encoders.Hex;  

import java.math.BigInteger;  
import java.security.SecureRandom;  

/**  
 *         <dependency>  
 *             <groupId>org.bouncycastle</groupId>  
 *             <artifactId>bcprov-jdk15to18</artifactId>  
 *             <version>1.69</version>  
 *         </dependency>  
 */  
public class SM2Util {  

    private static final ECParameterSpec CURVE = ECNamedCurveTable.getParameterSpec("sm2p256v1");  
    private static final ECDomainParameters DOMAIN = new ECDomainParameters(  
            CURVE.getCurve(),  
            CURVE.getG(),  
            CURVE.getN(),  
            CURVE.getH()  
    );  

    /**  
     * 生成SM2密钥对  
     *  
     * @return Map包含publicKey和privateKey,均为小写十六进制字符串  
     */  
    public static java.util.Map<String, String> generateKeyPair() {  
        try {  
            BigInteger d = new BigInteger(256, new SecureRandom()).mod(DOMAIN.getN());  
            org.bouncycastle.math.ec.ECPoint q = DOMAIN.getG().multiply(d).normalize();  

            String x = String.format("4x", q.getAffineXCoord().toBigInteger());  
            String y = String.format("4x", q.getAffineYCoord().toBigInteger());  
            String publicKey = "04" + x + y;  
            String privateKey = String.format("4x", d);  
            java.util.Map<String, String> keyMap = new java.util.HashMap<>();  
            keyMap.put("publicKey", publicKey.toLowerCase());  
            keyMap.put("privateKey", privateKey.toLowerCase());  
            return keyMap;  
        } catch (Exception e) {  
            System.err.println("密钥生成失败: " + e.getMessage());  
            e.printStackTrace();  
            java.util.Map<String, String> keyMap = new java.util.HashMap<>();  
            keyMap.put("publicKey", "");  
            keyMap.put("privateKey", "");  
            return keyMap;  
        }  
    }  

    /**  
     * SM2签名  
     *  
     * @param data 待签名的原始数据  
     * @param privateKeyHex 私钥(64字符十六进制)  
     * @return 签名结果(DER格式,小写十六进制)  
     */  
    public static String sign(String data, String privateKeyHex) {  
        try {  
            byte[] msgBytes = data.getBytes(java.nio.charset.StandardCharsets.UTF_8);  

            BigInteger d = new BigInteger(privateKeyHex, 16);  
            ECPrivateKeyParameters priKey = new ECPrivateKeyParameters(d, DOMAIN);  

            SM2Signer signer = new SM2Signer(new SM3Digest());  
            signer.init(true, priKey);  
            signer.update(msgBytes, 0, msgBytes.length);  
            byte[] sig = signer.generateSignature();  

            return Hex.toHexString(sig).toLowerCase();  
        } catch (Exception e) {  
            System.err.println("签名失败: " + e.getMessage());  
            e.printStackTrace();  
            return "";  
        }  
    }  

    /**  
     * SM2验签  
     *  
     * @param data 原始数据  
     * @param signatureHex 签名(DER格式,十六进制)  
     * @param publicKeyHex 公钥(04开头或不含04前缀的十六进制)  
     * @return 验签是否成功  
     */  
    public static boolean verify(String data, String signatureHex, String publicKeyHex) {  
        try {  
            byte[] msgBytes = data.getBytes(java.nio.charset.StandardCharsets.UTF_8);  

            String xy = publicKeyHex.startsWith("04") ? publicKeyHex.substring(2) : publicKeyHex;  
            BigInteger x = new BigInteger(xy.substring(0, 64), 16);  
            BigInteger y = new BigInteger(xy.substring(64, 128), 16);  

            org.bouncycastle.math.ec.ECPoint pubPoint = DOMAIN.getCurve().createPoint(x, y).normalize();  
            ECPublicKeyParameters pubKey = new ECPublicKeyParameters(pubPoint, DOMAIN);  

            SM2Signer signer = new SM2Signer(new SM3Digest());  
            signer.init(false, pubKey);  
            signer.update(msgBytes, 0, msgBytes.length);  

            byte[] sigBytes = Hex.decode(signatureHex);  
            return signer.verifySignature(sigBytes);  
        } catch (Exception e) {  
            System.err.println("验签失败: " + e.getMessage());  
            e.printStackTrace();  
            return false;  
        }  
    }  

    /**  
     * SM2加密  
     *  
     * @param data 待加密的原始数据(UTF-8字符串)  
     * @param publicKeyHex 公钥(04开头或不含04前缀的十六进制)  
     * @return 密文(HEX字符串,小写)  
     */  
    public static String encrypt(String data, String publicKeyHex) {  
        try {  
            byte[] dataBytes = data.getBytes(java.nio.charset.StandardCharsets.UTF_8);  

            String xy = publicKeyHex.startsWith("04") ? publicKeyHex.substring(2) : publicKeyHex;  
            BigInteger x = new BigInteger(xy.substring(0, 64), 16);  
            BigInteger y = new BigInteger(xy.substring(64, 128), 16);  

            org.bouncycastle.math.ec.ECPoint pubPoint = DOMAIN.getCurve().createPoint(x, y).normalize();  
            ECPublicKeyParameters pubKey = new ECPublicKeyParameters(pubPoint, DOMAIN);  

            SM2Engine engine = new SM2Engine(new SM3Digest(), SM2Engine.Mode.C1C3C2);  
            engine.init(true, new ParametersWithRandom(pubKey, new SecureRandom()));  

            byte[] encrypted = engine.processBlock(dataBytes, 0, dataBytes.length);  
            return Hex.toHexString(encrypted).toLowerCase();  
        } catch (Exception e) {  
            System.err.println("加密失败: " + e.getMessage());  
            e.printStackTrace();  
            return "";  
        }  
    }  

    /**  
     * SM2解密  
     *  
     * @param encryptedHex 密文(HEX字符串)  
     * @param privateKeyHex 私钥(64字符十六进制)  
     * @return 解密后的原始数据(UTF-8字符串)  
     */  
    public static String decrypt(String encryptedHex, String privateKeyHex) {  
        try {  
            byte[] encryptedBytes = Hex.decode(encryptedHex);  

            BigInteger d = new BigInteger(privateKeyHex, 16);  
            ECPrivateKeyParameters priKey = new ECPrivateKeyParameters(d, DOMAIN);  

            SM2Engine engine = new SM2Engine(new SM3Digest(), SM2Engine.Mode.C1C3C2);  
            engine.init(false, priKey);  

            byte[] decrypted = engine.processBlock(encryptedBytes, 0, encryptedBytes.length);  
            return new String(decrypted, java.nio.charset.StandardCharsets.UTF_8);  
        } catch (Exception e) {  
            System.err.println("解密失败: " + e.getMessage());  
            e.printStackTrace();  
            return "";  
        }  
    }  

    /**  
     * 测试方法  
     */  
    public static void main(String[] args) {  
        System.out.println("=== SM2 工具类测试 ===\n");  

        java.util.Map<String, String> keyPair = generateKeyPair();  
        String publicKey = keyPair.get("publicKey");  
        String privateKey = keyPair.get("privateKey");  

        System.out.println("公钥: " + publicKey);  
        System.out.println("私钥: " + privateKey);  
        System.out.println();  

        String testData = "123";  
        System.out.println("原始数据: " + testData);  

        String signature = sign(testData, privateKey);  
        System.out.println("签名: " + signature);  
        System.out.println();  

        boolean isValid = verify(testData, signature, publicKey);  
        System.out.println("验签结果: " + isValid);  
        System.out.println();  

        if (!isValid) {  
            System.err.println("验签失败!");  
        } else {  
            System.out.println("验签成功!");  
        }  

        System.out.println("\n=== SM2 加密解密测试 ===\n");  

        // 加密解密测试  
        String originalText = "Hello SM2";  
        System.out.println("原始数据: " + originalText);  

        String encrypted = encrypt(originalText, publicKey);  
        System.out.println("原始数据: " + originalText);  

        String decrypted = decrypt(encrypted, privateKey);  
        System.out.println("解密结果: " + decrypted);  
        System.out.println();  

        if (originalText.equals(decrypted)) {  
            System.out.println("加密解密成功!");  
        } else {  
            System.err.println("加密解密失败!");  
        }  
    }  
}  

package top.sunrains;  

import org.bouncycastle.jce.provider.BouncyCastleProvider;  

import javax.crypto.Cipher;  
import javax.crypto.spec.IvParameterSpec;  
import javax.crypto.spec.SecretKeySpec;  
import java.security.SecureRandom;  
import java.security.Security;  

/**  
 *         <dependency>  
 *             <groupId>org.bouncycastle</groupId>  
 *             <artifactId>bcprov-jdk15to18</artifactId>  
 *             <version>1.69</version>  
 *         </dependency>  
 *  
 * SM4国密算法工具类  
 * 使用Bouncy Castle原生API  
 * 配置信息:  
 * - 算法:SM4  
 * - 模式:CBC(密码分组链接)  
 * - 填充:PKCS5Padding  
 * - 密钥长度:128位(16字节)  
 * - IV长度:128位(16字节)  
 */  
public class Sm4Util {  

    static {  
        try {  
            Security.addProvider(new BouncyCastleProvider());  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  

    public static void main(String[] args) throws Exception {  
//        String key = generateKeyHex();  
//        String iv = generateIvHex();  
        //String s = encryptHex("123", key, iv);  
        String key = "7f9a942a1aa85c331d3697a5369a37fe";  
        String iv = "cbc07750d33d6a3ef6d39fc7e7c4a876";  

        String s = "09187a80e9f52e7e962a3465ed70669e";  
        System.out.println(s);  
        String decryptedHex = decryptHex(s, key, iv);  
        System.out.println(decryptedHex);  
    }  

    /**  
     * 生成SM4密钥(16字节,128位)  
     * @return 十六进制编码的密钥字符串(32个字符)  
     */  
    public static String generateKeyHex() {  
        byte[] keyBytes = new byte[16];  
        new SecureRandom().nextBytes(keyBytes);  
        return byteArrayToHex(keyBytes);  
    }  

    /**  
     * 生成IV向量(16字节,128位)  
     * @return 十六进制编码的IV字符串(32个字符)  
     */  
    public static String generateIvHex() {  
        byte[] ivBytes = new byte[16];  
        new SecureRandom().nextBytes(ivBytes);  
        return byteArrayToHex(ivBytes);  
    }  

    /**  
     * SM4-CBC加密(字符串到HEX)  
     * 标准加密方式  
     *  
     * @param plaintext 明文字符串(UTF-8)  
     * @param keyHex 十六进制密钥字符串  
     * @param ivHex 十六进制IV字符串  
     * @return 加密后的HEX字符串  
     * @throws Exception 加密异常  
     */  
    public static String encryptHex(String plaintext, String keyHex, String ivHex) throws Exception {  
        byte[] key = decodeKeyHex(keyHex);  
        byte[] iv = decodeIvHex(ivHex);  
        byte[] data = plaintext.getBytes("UTF-8");  

        byte[] encrypted = encrypt(data, key, iv);  
        return byteArrayToHex(encrypted);  
    }  

    /**  
     * SM4-CBC加密  
     *  
     * @param data 原始数据字节数组  
     * @param key 密钥字节数组(16字节)  
     * @param iv IV字节数组(16字节)  
     * @return 加密后的字节数组  
     * @throws Exception 加密异常  
     */  
    public static byte[] encrypt(byte[] data, byte[] key, byte[] iv) throws Exception {  
        Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC");  
        cipher.init(Cipher.ENCRYPT_MODE,  
                new SecretKeySpec(key, "SM4"),  
                new IvParameterSpec(iv));  
        return cipher.doFinal(data);  
    }  

    /**  
     * byte[] 转 Hex  
     *  
     * @param bytes 字节数组  
     * @return HEX字符串(小写)  
     */  
    public static String byteArrayToHex(byte[] bytes) {  
        if (bytes == null || bytes.length == 0) {  
            return "";  
        }  
        StringBuilder sb = new StringBuilder();  
        for (byte b : bytes) {  
            String hex = Integer.toHexString(b & 0xFF);  
            if (hex.length() == 1) {  
                sb.append('0');  
            }  
            sb.append(hex);  
        }  
        return sb.toString().toLowerCase();  
    }  

    public static String decryptHex(String encryptedHex, String keyHex, String ivHex) throws Exception {  
        byte[] key = decodeKeyHex(keyHex);  
        byte[] iv = decodeIvHex(ivHex);  
        byte[] encryptedData = hexToByteArray(encryptedHex);  

        byte[] decrypted = decrypt(encryptedData, key, iv);  
        return new String(decrypted, "UTF-8");  
    }  

    /**  
     * SM4-CBC解密  
     *  
     * @param encryptedData 加密数据字节数组  
     * @param key 密钥字节数组(16字节)  
     * @param iv IV字节数组(16字节)  
     * @return 解密后的原始字节数组  
     * @throws Exception 解密异常  
     */  
    public static byte[] decrypt(byte[] encryptedData, byte[] key, byte[] iv) throws Exception {  
        Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC");  
        cipher.init(Cipher.DECRYPT_MODE,  
                new SecretKeySpec(key, "SM4"),  
                new IvParameterSpec(iv));  
        return cipher.doFinal(encryptedData);  
    }  

    /**  
     * 从十六进制字符串还原密钥字节数组  
     * @param keyHex 十六进制编码的密钥字符串  
     * @return 密钥字节数组(16字节)  
     */  
    public static byte[] decodeKeyHex(String keyHex) {  
        return hexToByteArray(keyHex);  
    }  

    /**  
     * 从十六进制字符串还原IV字节数组  
     * @param ivHex 十六进制编码的IV字符串  
     * @return IV字节数组(16字节)  
     */  
    public static byte[] decodeIvHex(String ivHex) {  
        return hexToByteArray(ivHex);  
    }  

    /**  
     * HEX 转 byte[]  
     *  
     * @param hex HEX字符串  
     * @return 字节数组  
     */  
    public static byte[] hexToByteArray(String hex) {  
        if (hex == null || hex.length() == 0) {  
            return new byte[0];  
        }  

        int len = hex.length() / 2;  
        byte[] bytes = new byte[len];  
        for (int i = 0; i < len; i++) {  
            int high = Character.digit(hex.charAt(i * 2), 16);  
            int low = Character.digit(hex.charAt(i * 2 + 1), 16);  
            bytes[i] = (byte) ((high << 4) | low);  
        }  
        return bytes;  
    }  

    public static String stringToHex(String str) {  
        if (str == null || str.isEmpty()) {  
            return "";  
        }  
        byte[] bytes = str.getBytes(java.nio.charset.StandardCharsets.UTF_8);  
        return byteArrayToHex(bytes);  
    }  
}  

package top.sunrains;  

import javax.crypto.Cipher;  
import javax.crypto.spec.IvParameterSpec;  
import javax.crypto.spec.SecretKeySpec;  
import java.security.SecureRandom;  
import java.util.Base64;  
import java.util.HashMap;  
import java.util.Map;  

/**  
 * AES对称加密算法工具类  
 * 使用Java原生Crypto API  
 * 配置信息:  
 * - 算法:AES-256  
 * - 模式:CBC(密码分组链接)  
 * - 填充:PKCS5Padding  
 * - 密钥长度:256位(32字节)  
 * - IV长度:128位(16字节)  
 */  
public class AesUtil {  

    /**  
     * 生成AES密钥和IV  
     * @return Map包含key (32字符) 和 iv (16字符)  
     */  
    public static Map<String, String> generateKey() {  
        try {  
            // 生成32字节的随机字符串作为密钥  
            String key = generateRandomString(32);  

            // 生成16字节的随机字符串作为IV  
            String iv = generateRandomString(16);  

            System.out.println("AES Key: " + key + " (长度: " + key.length() + ")");  
            System.out.println("AES IV: " + iv + " (长度: " + iv.length() + ")");  

            Map<String, String> result = new HashMap<>();  
            result.put("key", key);  
            result.put("iv", iv);  

            return result;  
        } catch (Exception e) {  
            System.err.println("AES 密钥生成失败: " + e.getMessage());  
            e.printStackTrace();  
            return new HashMap<>();  
        }  
    }  

    /**  
     * 生成指定长度的随机字符串  
     */  
    private static String generateRandomString(int length) {  
        String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";  
        SecureRandom random = new SecureRandom();  
        StringBuilder result = new StringBuilder(length);  
        for (int i = 0; i < length; i++) {  
            result.append(chars.charAt(random.nextInt(chars.length())));  
        }  
        return result.toString();  
    }  

    /**  
     * AES-CBC加密  
     *  
     * @param plaintext 明文字符串(UTF-8)  
     * @param keyStr 密钥字符串(32字符)  
     * @param ivStr IV字符串(16字符)  
     * @return Base64编码的密文  
     * @throws Exception 加密异常  
     */  
    public static String encrypt(String plaintext, String keyStr, String ivStr) throws Exception {  
        // 将字符串Key和IV转换为字节数组(UTF-8编码)  
        byte[] keyBytes = keyStr.getBytes("UTF-8");  
        byte[] ivBytes = ivStr.getBytes("UTF-8");  

        // 创建密钥和IV  
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");  
        IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);  

        // 创建Cipher对象(AES/CBC/PKCS5Padding)  
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");  
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);  

        // 执行加密  
        byte[] encrypted = cipher.doFinal(plaintext.getBytes("UTF-8"));  

        // 返回Base64编码的密文  
        return Base64.getEncoder().encodeToString(encrypted);  
    }  

    /**  
     * AES-CBC解密  
     *  
     * @param encryptedBase64 Base64编码的密文  
     * @param keyStr 密钥字符串(32字符)  
     * @param ivStr IV字符串(16字符)  
     * @return 解密后的明文字符串  
     * @throws Exception 解密异常  
     */  
    public static String decrypt(String encryptedBase64, String keyStr, String ivStr) throws Exception {  
        // 将字符串Key和IV转换为字节数组(UTF-8编码)  
        byte[] keyBytes = keyStr.getBytes("UTF-8");  
        byte[] ivBytes = ivStr.getBytes("UTF-8");  

        // 创建密钥和IV  
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");  
        IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);  

        // 创建Cipher对象(AES/CBC/PKCS5Padding)  
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");  
        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);  

        // 将Base64密文解码为字节  
        byte[] encryptedBytes = Base64.getDecoder().decode(encryptedBase64);  

        // 执行解密  
        byte[] decrypted = cipher.doFinal(encryptedBytes);  

        // 返回明文字符串  
        return new String(decrypted, "UTF-8");  
    }  

    /**  
     * 测试方法  
     */  
    public static void main(String[] args) throws Exception {  
        System.out.println("=== AES 工具类测试 ===\n");  

        // 生成密钥  
        Map<String, String> keyMap = generateKey();  
        String key = keyMap.get("key");  
        String iv = keyMap.get("iv");  

        System.out.println("密钥: " + key);  
        System.out.println("IV: " + iv);  
        System.out.println();  

        // 加密测试  
        String originalText = "Hello AES";  
        System.out.println("原始数据: " + originalText);  

        String encrypted = encrypt(originalText, key, iv);  
        System.out.println("加密结果: " + encrypted);  
        System.out.println();  

        // 解密测试  
        String decrypted = decrypt(encrypted, key, iv);  
        System.out.println("解密结果: " + decrypted);  
        System.out.println();  

        if (originalText.equals(decrypted)) {  
            System.out.println("✅ 加密解密成功!");  
        } else {  
            System.err.println("❌ 加密解密失败!");  
        }  
    }  
}  

package top.sunrains;  

import cn.hutool.core.text.CharSequenceUtil;  
import org.bouncycastle.crypto.digests.SM3Digest;  
import org.bouncycastle.crypto.macs.HMac;  
import org.bouncycastle.crypto.params.KeyParameter;  
import org.bouncycastle.jce.provider.BouncyCastleProvider;  

import java.io.File;  
import java.io.FileInputStream;  
import java.io.IOException;  
import java.io.InputStream;  
import java.security.Security;  
/**  
 * SM3国密哈希算法工具类  
 * 使用Bouncy Castle原生API  
 * 功能:  
 * - 字符串哈希计算  
 * - 字节数组哈希计算  
 * - 文件哈希计算  
 * - HMAC-SM3计算  
 */  
public class SM3Util {  

    static {  
        try {  
            Security.addProvider(new BouncyCastleProvider());  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  

    private static final int BUFFER_SIZE = 8192;  

    /**  
     * 计算字符串的SM3哈希值  
     *  
     * @param data 待哈希的字符串  
     * @return 哈希值(小写十六进制字符串,64个字符)  
     */  
    public static String hash(String data) {  
        if (data == null) {  
            throw new IllegalArgumentException("Data cannot be null");  
        }  
        return hash(data.getBytes(java.nio.charset.StandardCharsets.UTF_8));  
    }  

    /**  
     * 验证字符串的SM3哈希值  
     *  
     * @param data 原始数据  
     * @param expectedHash 期望的哈希值  
     * @return 是否匹配  
     */  
    public static boolean verify(String data, String expectedHash) {  
        String actualHash = hash(data);  
        return actualHash.equalsIgnoreCase(expectedHash);  
    }  

    /**  
     * 计算字节数组的SM3哈希值  
     *  
     * @param data 待哈希的字节数组  
     * @return 哈希值(小写十六进制字符串,64个字符)  
     */  
    public static String hash(byte[] data) {  
        SM3Digest digest = new SM3Digest();  
        digest.update(data, 0, data.length);  
        byte[] result = new byte[digest.getDigestSize()];  
        digest.doFinal(result, 0);  
        return byteArrayToHex(result);  
    }  

    /**  
     * 计算文件的SM3哈希值(支持大文件)  
     *  
     * @param filePath 文件路径  
     * @return 哈希值(小写十六进制字符串,64个字符)  
     * @throws IOException 文件读取异常  
     */  
    public static String hashFile(String filePath) throws IOException {  
        return hashFile(new File(filePath));  
    }  

    /**  
     * 计算文件的SM3哈希值(支持大文件)  
     *  
     * @param file 文件对象  
     * @return 哈希值(小写十六进制字符串,64个字符)  
     * @throws IOException 文件读取异常  
     */  
    public static String hashFile(File file) throws IOException {  
        if (!file.exists()) {  
            throw new IOException("File not found: " + file.getAbsolutePath());  
        }  

        SM3Digest digest = new SM3Digest();  
        byte[] buffer = new byte[BUFFER_SIZE];  

        try (InputStream is = new FileInputStream(file)) {  
            int bytesRead;  
            while ((bytesRead = is.read(buffer)) != -1) {  
                digest.update(buffer, 0, bytesRead);  
            }  
        }  

        byte[] result = new byte[digest.getDigestSize()];  
        digest.doFinal(result, 0);  
        return byteArrayToHex(result);  
    }  

    /**  
     * 计算HMAC-SM3(基于密钥的哈希消息认证码)  
     *  
     * @param data 待计算的数据  
     * @param hexKey 密钥  
     * @return HMAC-SM3结果(小写十六进制字符串,64个字符)  
     */  
    public static String hmac(String data, String hexKey) {  
        if (CharSequenceUtil.isBlank(hexKey)) {  
            throw new IllegalArgumentException("密钥不能为空");  
        }  
        return hmac(data.getBytes(java.nio.charset.StandardCharsets.UTF_8),  
                hexToByteArray(hexKey));  
    }  

    /**  
     * 计算HMAC-SM3(基于密钥的哈希消息认证码)  
     *  
     * @param data 待计算的字节数组  
     * @param hexKey hex密钥字节数组  
     * @return HMAC-SM3结果(小写十六进制字符串,64个字符)  
     */  
    public static String hmac(byte[] data, byte[] hexKey) {  
        HMac hmac = new HMac(new SM3Digest());  
        hmac.init(new KeyParameter(hexKey));  
        hmac.update(data, 0, data.length);  
        byte[] result = new byte[hmac.getMacSize()];  
        hmac.doFinal(result, 0);  
        return byteArrayToHex(result);  
    }  

    /**  
     * byte[] 转 Hex  
     *  
     * @param bytes 字节数组  
     * @return HEX字符串(小写)  
     */  
    private static String byteArrayToHex(byte[] bytes) {  
        if (bytes == null || bytes.length == 0) {  
            return "";  
        }  
        StringBuilder sb = new StringBuilder();  
        for (byte b : bytes) {  
            String hex = Integer.toHexString(b & 0xFF);  
            if (hex.length() == 1) {  
                sb.append('0');  
            }  
            sb.append(hex);  
        }  
        return sb.toString().toLowerCase();  
    }  

    /**  
     * 测试方法  
     */  
    public static void main(String[] args) throws IOException {  
        String testData = "12";  
        System.out.println("SM3: " + hash(testData));  
        String key = "1";  
        key = stringToHex(key);  
        System.out.println("HMAC-SM3 key: " + key);  
        String hmacResult = hmac(testData, key);  
        System.out.println("HMAC-SM3: " + hmacResult);  
        System.out.println("HMAC长度: " + hmacResult.length() + " 字符");  
       System.out.println("文件哈希值: " +  hashFile("/Users/sun/1/t.jpg"));  
    }  

    public static String stringToHex(String str) {  
        if (str == null || str.isEmpty()) {  
            return "";  
        }  
        byte[] bytes = str.getBytes(java.nio.charset.StandardCharsets.UTF_8);  
        return byteArrayToHex(bytes);  
    }  

    /**  
     * HEX 转 byte[]  
     *  
     * @param hex HEX字符串  
     * @return 字节数组  
     */  
    public static byte[] hexToByteArray(String hex) {  
        if (hex == null || hex.length() == 0) {  
            return new byte[0];  
        }  

        int len = hex.length() / 2;  
        byte[] bytes = new byte[len];  
        for (int i = 0; i < len; i++) {  
            int high = Character.digit(hex.charAt(i * 2), 16);  
            int low = Character.digit(hex.charAt(i * 2 + 1), 16);  
            bytes[i] = (byte) ((high << 4) | low);  
        }  
        return bytes;  
    }  
}

各平台实现说明

Android 平台

SM2 实现:

  • 依赖库:BouncyCastle (org.bouncycastle:bcprov-jdk15on:1.70)

  • 特点

    • 使用 ECKeyPairGenerator 生成密钥对
    • 使用 SM2Signer 进行签名和验签(DER 格式)
    • 使用 SM2Engine 进行加密和解密(C1C3C2 格式)
    • 不依赖 JCA Provider,避免兼容性问题

SM4 实现:

  • 依赖库:BouncyCastle

  • 特点

    • 使用 SM4Engine 进行加密和解密
    • CBC 模式 + PKCS7 填充
    • 支持自定义密钥和 IV

iOS 平台

实现方式:GMObjC 原生库

SM2 特点:

  • 使用 GMSm2Utils 进行所有操作
  • 签名自动转换为 DER 格式以兼容其他平台
  • 加密输出 C1C3C2 格式(04 开头)
  • 支持跨平台互通(Android/HarmonyOS)

SM4 特点:

  • 使用 GMSm4Utils 进行加密解密
  • CBC 模式 + PKCS7 填充
  • 密钥和 IV 为 32 位十六进制字符串

HarmonyOS 平台

实现方式: @ohos.security.cryptoFramework

SM2 特点:

  • 使用 HarmonyOS 原生 cryptoFramework API
  • 签名使用 DER 格式
  • 加密使用 C1C3C2 格式
  • 完全符合国密标准

SM3 特点:

  • 使用 createMd (MessageDigest) 进行普通哈希
  • 使用手动实现 HMAC-SM3 (RFC 2104)
  • 支持流式文件哈希处理 (4096 字节缓冲区)
  • 不依赖 createMac,避免密钥参数问题

SM4 特点:

  • 使用 createSymKeyGenerator 生成密钥
  • 使用 createCipher 进行加密解密
  • CBC 模式 + PKCS7 填充
  • 支持 SM4_128 算法

AES 特点:

  • 使用 crypto.createSymKeyGenerator('AES256') 创建密钥生成器
  • 使用 crypto.createCipher('AES|CBC|PKCS7') 进行加解密
  • CBC 模式 + PKCS7 填充
  • 密钥和 IV 为随机字符串(UTF-8 编码)

H5 平台

实现方式:sm-crypto + Web Crypto API

特点:

  • SM2/SM3/SM4:通过 npm 包 sm-crypto 实现
  • AES:使用浏览器原生 Web Crypto API 实现
  • 所有功能均在浏览器环境中运行
  • SM2 签名使用 DER 格式(der: true
  • SM2 默认 userId 为 '1234567812345678'

依赖安装:

npm install --save sm-crypto

微信小程序平台

实现方式:miniprogram-sm-crypto + crypto-js + jsrsasign

特点:

  • 专为小程序优化的国密算法库
  • 完全支持 SM2、SM4 和 AES
  • SM2:密钥生成、签名、验签、加密、解密(使用 miniprogram-sm-crypto)
  • SM4:密钥生成、加密、解密(CBC 模式,使用 miniprogram-sm-crypto)
  • AES:密钥生成、加密、解密(CBC 模式,使用 crypto-js)
  • 需要通过微信开发者工具构建 npm

依赖安装:

npm install --save miniprogram-sm-crypto crypto-js jsrsasign

重要:微信小程序 npm 构建步骤

  1. 确保项目根目录已安装上述依赖
  2. 打开微信开发者工具,勾选「详情」→「本地设置」→「使用 npm 模块」
  3. 点击菜单栏「工具」→「构建 npm」,生成 miniprogram_npm 目录
  4. 重新编译运行小程序

注意事项

1. 密钥格式

SM2 密钥:

  • 公钥:以 04 开头的 130 位十六进制字符串(非压缩格式)
  • 私钥:64 位十六进制字符串

SM4 密钥:

  • 密钥 (key) :32 位十六进制字符串(128 位)
  • 初始向量 (IV) :32 位十六进制字符串(128 位)

AES 密钥:

  • 密钥 (key) :32 字符随机字符串(字母+数字,UTF-8 编码后为 256 位)
  • 初始向量 (IV) :16 字符随机字符串(字母+数字,UTF-8 编码后为 128 位)

2. 签名格式

  • 所有平台的签名输出均为 DER 编码的十六进制字符串
  • DER 格式确保跨平台兼容性

3. 哈希格式

SM3 哈希:

  • 输出长度:64 位十六进制字符串 (256 位)
  • 格式:小写十六进制
  • 示例:a1b2c3d4e5f6... (64 个字符)

HMAC-SM3 哈希:

  • 输出长度:64 位十六进制字符串 (256 位)
  • 密钥格式:十六进制字符串
  • 格式:小写十六进制

4. 加密格式

SM2 加密:

  • 输出格式:C1C3C2(以 04 开头)
  • 兼容 Android BouncyCastle、iOS GMObjC、HarmonyOS cryptoFramework

SM4 加密:

  • 模式:CBC
  • 填充:PKCS7
  • 输出:十六进制字符串

AES 加密:

  • 模式:CBC
  • 填充:PKCS5/PKCS7
  • 密钥:32 字符随机字符串(UTF-8 编码后为 256 位)
  • IV:16 字符随机字符串(UTF-8 编码后为 128 位)
  • 输出:Base64 编码字符串

RSA 加密:

  • 算法:RSA-OAEP with SHA-256 (对应 Java 的 RSA/ECB/OAEPWithSHA-256AndMGF1Padding)
  • 密钥长度:2048 位
  • 私钥格式:PKCS#8 (Base64 编码)
  • 公钥格式:X.509/SPKI (Base64 编码)
  • 输出:Base64 编码字符串
  • 跨平台兼容:Android/iOS/HarmonyOS/H5 完全互通

RSA 签名:

  • 算法:SHA256withRSA (PKCS#1 v1.5)
  • 私钥格式:PKCS#8 (Base64 编码)
  • 公钥格式:X.509/SPKI (Base64 编码)
  • 签名输出:Base64 编码字符串
  • 跨平台兼容:Android/iOS/HarmonyOS/H5/微信小程序 完全互通

5. 跨平台兼容性

✅ 完全兼容:

  • 各平台生成的 SM2 密钥对可以互相使用
  • Android 平台签名的数据可以在 iOS/Harmony/H5 平台验证
  • SM2 加密的数据可以在不同平台间解密
  • SM4 加密的数据可以在 Android/iOS/Harmony 平台间加解密
  • AES 加密的数据可以在所有平台间加解密(Android/iOS/HarmonyOS/H5/微信小程序)
  • SM3 哈希值在所有平台保持一致(相同输入产生相同输出)
  • HMAC-SM3 支持跨平台验证
  • RSA-OAEP-SHA256 加密的数据可以在 Android/iOS/HarmonyOS/H5 平台间加解密
  • SHA256withRSA 签名可以在所有平台间验签(包括微信小程序)

⚠️ 注意事项:

  • 确保使用相同的哈希选项(默认启用)
  • SM4/AES 加密和解密必须使用相同的密钥和 IV
  • 微信小程序需要先构建 npm 才能使用
  • SM3 文件哈希需要确保文件内容完全一致
  • AES 密钥为 32 字符随机字符串,IV 为 16 字符随机字符串
  • RSA 密钥长度为 2048 位,私钥为 PKCS#8 格式,公钥为 X.509/SPKI 格式
  • RSA 加密使用 OAEP with SHA-256 填充,与其他平台互通
  • RSA 签名使用 SHA256withRSA (PKCS#1 v1.5),所有平台兼容
  • 微信小程序端暂不支持 RSA-OAEP-SHA256 加密解密,仅支持签名验签
收起阅读 »