HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

uniapp换肤最佳实践

换肤

以下均已实际验证过:

uniapp换肤最佳实践

有问题可以关注下公众号:一诺滚雪球,一起学习交流!

以下均已实际验证过:

uniapp换肤最佳实践

有问题可以关注下公众号:一诺滚雪球,一起学习交流!

iOS 上架流程详细指南 苹果应用发布步骤、ipa 文件上传 打包上架实战经验

iOS

'''应用开发完成后,摆在开发者面前的最大问题就是如何完成 iOS 上架流程
与 Android 平台相比,苹果 App Store 应用发布 过程更加复杂,从 开发者账号注册、证书申请、应用打包、ipa 文件上传、测试分发到 App Store 审核,每一步都严格把关。
尤其是使用 uni-app 跨平台开发的团队,虽然开发阶段效率很高,但在 iOS 上架 阶段依然需要严格遵循规范。

本文将结合实战案例,系统讲解 iOS 上架流程,并介绍多工具组合(Xcode、Transporter、Appuploader、Fastlane)的应用,帮助开发者快速高效完成苹果应用发布。


一、iOS 上架流程第一步:开发者账号与证书

1. Apple 开发者账号

  • 个人账号:99 美元/年,适合独立开发者。
  • 企业账号:适合公司和团队,支持更广泛的分发方式。

2. iOS 证书与描述文件

  • 开发证书:用于真机测试。
  • 发布证书:用于 TestFlight 和 App Store 上架。
  • 描述文件:控制应用分发方式(Ad Hoc、App Store)。

工具选择

  • Xcode:Mac 用户可直接生成证书与描述文件。
  • Appuploader:跨平台证书管理工具,支持 Windows/Linux 用户申请 .p12.mobileprovision 文件。

二、uni-app 打包方式:iOS 上架流程的核心环节

1. HBuilderX 云打包

  • 上传证书与描述文件,云端自动生成 ipa 文件。
  • 无需 Mac,适合小团队或快速版本迭代。

2. Xcode 本地打包

  • 从 HBuilderX 导出 Xcode 工程,在 Mac 上 Archive 打包生成 ipa。
  • 灵活性更强,适合正式版本。

实战经验:小版本更新用云打包,大版本发布用 Xcode 打包,兼顾效率与稳定性。


三、ipa 文件上传:iOS 上架流程的关键步骤

生成 ipa 文件后,必须上传到苹果服务器才能进入审核环节。

上传方式对比

  • Xcode 上传:操作直观,但大文件容易失败。
  • Transporter App:苹果官方工具,支持大文件上传,稳定性更高。
  • Appuploader:支持 Windows/Linux/Mac,免 Mac 上传 ipa 文件。
  • Fastlane:命令行工具,适合 CI/CD 自动化上传。

推荐组合

  • 独立开发者:Xcode + Transporter。
  • 团队开发:Fastlane 自动化上传,Appuploader 备用。

四、测试分发:确保应用稳定性与兼容性

在正式发布前,需要通过多轮测试验证应用的稳定性。

  1. Ad Hoc 分发
    • 限制 100 台设备,适合小范围测试。
  2. TestFlight 内测
    • 最多支持 25 名团队成员,适合功能调试。
  3. TestFlight 外测
    • 最多 10,000 用户,适合大规模测试。
  4. 二维码安装
    • 使用 Appuploader 生成二维码,方便产品和运营快速安装体验。

五、App Store 审核:iOS 上架流程的最后一关

苹果审核环节是最严格的阶段,常见驳回原因包括:

  • 套壳应用嫌疑:uni-app 应用如果仅简单封装 H5 页面,容易被拒。
  • 素材不足:缺少截图、关键词或多语言描述。
  • 权限说明不清:如相机、定位、麦克风用途未明确说明。

审核优化建议

  • 确保应用功能完整,避免“套壳”嫌疑。
  • App Store Connect 上传多语言截图和完整描述。
  • 使用 Appuploader 批量上传截图,减少重复劳动。
  • 在 Info.plist 文件中详细写明权限用途。
  • 遇到紧急情况可申请 加急审核

六、实战案例:团队的 iOS 上架经验分享

一个 8 人团队开发的教育类应用,他们的 iOS 上架流程如下:

  1. 运维人员在 Windows 使用 Appuploader 生成证书。
  2. 开发人员通过 HBuilderX 云打包生成 ipa 文件。
  3. 测试人员使用 Transporter 上传 ipa 至 TestFlight,进行设备兼容性测试。
  4. 产品经理在 App Store Connect 上传截图和多语言描述。
  5. 应用审核一次性通过,成功上架 App Store。

这种流程让团队即使没有大量 Mac 设备,也能高效完成 iOS 上架。


七、经验总结

  1. 账号与证书必须规范管理,避免签名错误。
  2. 打包方式结合使用,云打包适合快速迭代,本地打包适合正式版本。
  3. 上传工具多样化,Xcode、Transporter、Appuploader、Fastlane 各有优势。
  4. 测试分发分阶段,逐步从内部到外部扩大范围。
  5. 审核准备要充分,功能完整、素材齐全、权限说明到位。

iOS 上架流程 虽然复杂,但并不是不可掌握的难题。
通过合理使用 HBuilderX、Xcode、Transporter、Appuploader、Fastlane 等工具,开发者完全可以高效完成 从应用开发到 App Store 发布 的全过程。
无论是独立开发者还是团队,掌握这些经验,才能让应用更快、更稳地进入市场。'''

继续阅读 »

'''应用开发完成后,摆在开发者面前的最大问题就是如何完成 iOS 上架流程
与 Android 平台相比,苹果 App Store 应用发布 过程更加复杂,从 开发者账号注册、证书申请、应用打包、ipa 文件上传、测试分发到 App Store 审核,每一步都严格把关。
尤其是使用 uni-app 跨平台开发的团队,虽然开发阶段效率很高,但在 iOS 上架 阶段依然需要严格遵循规范。

本文将结合实战案例,系统讲解 iOS 上架流程,并介绍多工具组合(Xcode、Transporter、Appuploader、Fastlane)的应用,帮助开发者快速高效完成苹果应用发布。


一、iOS 上架流程第一步:开发者账号与证书

1. Apple 开发者账号

  • 个人账号:99 美元/年,适合独立开发者。
  • 企业账号:适合公司和团队,支持更广泛的分发方式。

2. iOS 证书与描述文件

  • 开发证书:用于真机测试。
  • 发布证书:用于 TestFlight 和 App Store 上架。
  • 描述文件:控制应用分发方式(Ad Hoc、App Store)。

工具选择

  • Xcode:Mac 用户可直接生成证书与描述文件。
  • Appuploader:跨平台证书管理工具,支持 Windows/Linux 用户申请 .p12.mobileprovision 文件。

二、uni-app 打包方式:iOS 上架流程的核心环节

1. HBuilderX 云打包

  • 上传证书与描述文件,云端自动生成 ipa 文件。
  • 无需 Mac,适合小团队或快速版本迭代。

2. Xcode 本地打包

  • 从 HBuilderX 导出 Xcode 工程,在 Mac 上 Archive 打包生成 ipa。
  • 灵活性更强,适合正式版本。

实战经验:小版本更新用云打包,大版本发布用 Xcode 打包,兼顾效率与稳定性。


三、ipa 文件上传:iOS 上架流程的关键步骤

生成 ipa 文件后,必须上传到苹果服务器才能进入审核环节。

上传方式对比

  • Xcode 上传:操作直观,但大文件容易失败。
  • Transporter App:苹果官方工具,支持大文件上传,稳定性更高。
  • Appuploader:支持 Windows/Linux/Mac,免 Mac 上传 ipa 文件。
  • Fastlane:命令行工具,适合 CI/CD 自动化上传。

推荐组合

  • 独立开发者:Xcode + Transporter。
  • 团队开发:Fastlane 自动化上传,Appuploader 备用。

四、测试分发:确保应用稳定性与兼容性

在正式发布前,需要通过多轮测试验证应用的稳定性。

  1. Ad Hoc 分发
    • 限制 100 台设备,适合小范围测试。
  2. TestFlight 内测
    • 最多支持 25 名团队成员,适合功能调试。
  3. TestFlight 外测
    • 最多 10,000 用户,适合大规模测试。
  4. 二维码安装
    • 使用 Appuploader 生成二维码,方便产品和运营快速安装体验。

五、App Store 审核:iOS 上架流程的最后一关

苹果审核环节是最严格的阶段,常见驳回原因包括:

  • 套壳应用嫌疑:uni-app 应用如果仅简单封装 H5 页面,容易被拒。
  • 素材不足:缺少截图、关键词或多语言描述。
  • 权限说明不清:如相机、定位、麦克风用途未明确说明。

审核优化建议

  • 确保应用功能完整,避免“套壳”嫌疑。
  • App Store Connect 上传多语言截图和完整描述。
  • 使用 Appuploader 批量上传截图,减少重复劳动。
  • 在 Info.plist 文件中详细写明权限用途。
  • 遇到紧急情况可申请 加急审核

六、实战案例:团队的 iOS 上架经验分享

一个 8 人团队开发的教育类应用,他们的 iOS 上架流程如下:

  1. 运维人员在 Windows 使用 Appuploader 生成证书。
  2. 开发人员通过 HBuilderX 云打包生成 ipa 文件。
  3. 测试人员使用 Transporter 上传 ipa 至 TestFlight,进行设备兼容性测试。
  4. 产品经理在 App Store Connect 上传截图和多语言描述。
  5. 应用审核一次性通过,成功上架 App Store。

这种流程让团队即使没有大量 Mac 设备,也能高效完成 iOS 上架。


七、经验总结

  1. 账号与证书必须规范管理,避免签名错误。
  2. 打包方式结合使用,云打包适合快速迭代,本地打包适合正式版本。
  3. 上传工具多样化,Xcode、Transporter、Appuploader、Fastlane 各有优势。
  4. 测试分发分阶段,逐步从内部到外部扩大范围。
  5. 审核准备要充分,功能完整、素材齐全、权限说明到位。

iOS 上架流程 虽然复杂,但并不是不可掌握的难题。
通过合理使用 HBuilderX、Xcode、Transporter、Appuploader、Fastlane 等工具,开发者完全可以高效完成 从应用开发到 App Store 发布 的全过程。
无论是独立开发者还是团队,掌握这些经验,才能让应用更快、更稳地进入市场。'''

收起阅读 »

苹果手机怎么导出App数据目录,iOS文件管理、应用沙盒访问、日志缓存导出与性能调试实战(uni-app开发者指南)

iOS

'''在 iOS 平台,应用数据目录(App 沙盒) 对开发者来说至关重要。
这里保存着 用户数据、缓存文件、配置文件、数据库、日志 等内容,是排查性能问题、分析用户反馈和调试插件的关键。

然而,由于 苹果沙盒机制的限制,普通用户很难直接访问 App 数据目录。
那么,开发者或测试人员该如何导出这些目录呢?本文将结合多种工具与方法,系统介绍 苹果手机导出 App 数据目录的流程与实战案例


一、iOS App 数据目录的核心结构

  1. Documents
    • 用于存储用户持久化数据,如下载的音频、文档、笔记。
  2. Library/Caches
    • 存放缓存文件,可由系统自动清理。
  3. Library/Preferences
    • 应用配置文件,通常以 .plist 存在。
  4. tmp
    • 临时文件目录,系统随时可能清理。
  5. 系统日志与崩溃文件
    • 存放在设备中,需要通过开发工具或第三方工具导出。

二、常用的导出工具与方法

工具 功能定位 适用人群
Xcode Devices & Simulators 官方方式,导出 App Container,访问完整沙盒目录 开发者
克魔 (KeyMob) 无需越狱即可跨平台访问沙盒目录,支持文件导出、日志分析 开发/测试
iMazing / itools 图形化工具,适合导出文档、缓存、数据库文件,操作直观 测试人员
Crashlytics 收集线上崩溃日志,间接辅助问题定位 运维人员

三、实战案例一:验证插件文件写入

背景

某 uni-app 音乐应用,用户反馈下载的音频文件无法播放。

解决流程

  1. Xcode Devices:导出沙盒目录,发现文件写入到 tmp/,被系统清理。
  2. 克魔:导出并对比目录结构,确认路径错误。
  3. 优化方案:修改插件写入到 Library/Caches
  4. 效果:文件持久化保存,用户可正常播放。

四、实战案例二:缓存文件导致存储膨胀

背景

某 uni-app 新闻类应用,占用存储空间过大。

解决流程

  1. iMazing:导出缓存目录,发现图片缓存超过 2GB。
  2. 克魔:监控缓存增长曲线,验证问题持续存在。
  3. 优化方案:增加缓存清理机制,定期压缩文件。
  4. 效果:存储占用减少,应用运行流畅度提升。

五、实战案例三:崩溃日志定位问题

背景

某 uni-app 教育应用频繁崩溃,但本地无法复现。

解决流程

  1. 克魔:导出崩溃日志与系统日志,结合符号化工具定位问题模块。
  2. iMazing:提取数据库文件,发现表结构异常。
  3. 优化方案:修复数据库迁移逻辑,增加异常处理。
  4. 效果:崩溃率下降 85%。

六、推荐的导出与分析流程

[开发阶段] → Xcode 导出 App Container,调试插件与数据存储    
[测试阶段] → 克魔 导出缓存、日志与数据库文件,验证多设备表现    
[验证阶段] → iMazing/itools 快速检查文件大小与增长趋势    
[运维阶段] → Crashlytics 收集线上崩溃日志,辅助问题回溯  
  • 开发:确保文件写入路径正确;
  • 测试:验证缓存与日志文件是否合理增长;
  • 运维:收集线上日志,避免问题扩大化。

在 iOS 平台,导出 App 数据目录是性能调试与问题定位的关键
通过 Xcode、克魔 KeyMob、iMazing/itools、Crashlytics 的组合,开发团队可以:

  • 查看并导出完整的应用沙盒目录;
  • 验证插件写入与缓存策略是否正确;
  • 收集日志与数据库文件,快速定位性能与崩溃问题。

对于 uni-app 开发者 而言,这样的工具链能大幅提升调试效率,避免常见的文件与性能陷阱。'''

继续阅读 »

'''在 iOS 平台,应用数据目录(App 沙盒) 对开发者来说至关重要。
这里保存着 用户数据、缓存文件、配置文件、数据库、日志 等内容,是排查性能问题、分析用户反馈和调试插件的关键。

然而,由于 苹果沙盒机制的限制,普通用户很难直接访问 App 数据目录。
那么,开发者或测试人员该如何导出这些目录呢?本文将结合多种工具与方法,系统介绍 苹果手机导出 App 数据目录的流程与实战案例


一、iOS App 数据目录的核心结构

  1. Documents
    • 用于存储用户持久化数据,如下载的音频、文档、笔记。
  2. Library/Caches
    • 存放缓存文件,可由系统自动清理。
  3. Library/Preferences
    • 应用配置文件,通常以 .plist 存在。
  4. tmp
    • 临时文件目录,系统随时可能清理。
  5. 系统日志与崩溃文件
    • 存放在设备中,需要通过开发工具或第三方工具导出。

二、常用的导出工具与方法

工具 功能定位 适用人群
Xcode Devices & Simulators 官方方式,导出 App Container,访问完整沙盒目录 开发者
克魔 (KeyMob) 无需越狱即可跨平台访问沙盒目录,支持文件导出、日志分析 开发/测试
iMazing / itools 图形化工具,适合导出文档、缓存、数据库文件,操作直观 测试人员
Crashlytics 收集线上崩溃日志,间接辅助问题定位 运维人员

三、实战案例一:验证插件文件写入

背景

某 uni-app 音乐应用,用户反馈下载的音频文件无法播放。

解决流程

  1. Xcode Devices:导出沙盒目录,发现文件写入到 tmp/,被系统清理。
  2. 克魔:导出并对比目录结构,确认路径错误。
  3. 优化方案:修改插件写入到 Library/Caches
  4. 效果:文件持久化保存,用户可正常播放。

四、实战案例二:缓存文件导致存储膨胀

背景

某 uni-app 新闻类应用,占用存储空间过大。

解决流程

  1. iMazing:导出缓存目录,发现图片缓存超过 2GB。
  2. 克魔:监控缓存增长曲线,验证问题持续存在。
  3. 优化方案:增加缓存清理机制,定期压缩文件。
  4. 效果:存储占用减少,应用运行流畅度提升。

五、实战案例三:崩溃日志定位问题

背景

某 uni-app 教育应用频繁崩溃,但本地无法复现。

解决流程

  1. 克魔:导出崩溃日志与系统日志,结合符号化工具定位问题模块。
  2. iMazing:提取数据库文件,发现表结构异常。
  3. 优化方案:修复数据库迁移逻辑,增加异常处理。
  4. 效果:崩溃率下降 85%。

六、推荐的导出与分析流程

[开发阶段] → Xcode 导出 App Container,调试插件与数据存储    
[测试阶段] → 克魔 导出缓存、日志与数据库文件,验证多设备表现    
[验证阶段] → iMazing/itools 快速检查文件大小与增长趋势    
[运维阶段] → Crashlytics 收集线上崩溃日志,辅助问题回溯  
  • 开发:确保文件写入路径正确;
  • 测试:验证缓存与日志文件是否合理增长;
  • 运维:收集线上日志,避免问题扩大化。

在 iOS 平台,导出 App 数据目录是性能调试与问题定位的关键
通过 Xcode、克魔 KeyMob、iMazing/itools、Crashlytics 的组合,开发团队可以:

  • 查看并导出完整的应用沙盒目录;
  • 验证插件写入与缓存策略是否正确;
  • 收集日志与数据库文件,快速定位性能与崩溃问题。

对于 uni-app 开发者 而言,这样的工具链能大幅提升调试效率,避免常见的文件与性能陷阱。'''

收起阅读 »

让UniApp支持React

h5 App 小程序

首先dcloud官方花费了巨大的资源保证了全端的api、开发体验保持一致、对于前端开发来说抹平了太多的平台差异,非常之牛逼!!!
uniapp的跨平台能力和生态都非常不错,尤其是在当你需要夸各种平台的时候一套完整的api是多么的珍贵,如果不考虑小程序环境的话那开发起来还是非常顺手的,但是当用uniapp开发小程序的时候,由于小程序环境的限制,太多的特性不能使用,尤其是JSX的能力,这样的话开发起来就束手束脚,很多非常平常的功能,比如说全局的Toast,Modal等等都需要以一个非常受限的方式实现!!!

假设下面这段代码可以直接运行在uniapp中那么开发起来就会无比丝滑

const Sub = (props: any) => {  
      const { children } = props  
      const [value, setValue] = useState(100)  
      useEffect(() => {  
        const timer = setInterval(() => {  
          setValue((value) => {  
            if (value >= 110) {  
              clearInterval(timer)  
            }  
            return value   1  
          })  
        }, 1000)  
        return () => {  
          clearInterval(timer)  
        }  
      }, [])  
      return (  
        <View>  
          sub  
          {value}  
          {value % 2 === 0 ? children : null}  
        </View>  
      )  
    }  
    const Hello = (props: any) => {  
      const [value, setValue] = useState(200)  
      const { unmount } = props  
      const [visible, setVisible] = useState(true)  
      useEffect(() => {  
        if (!visible) {  
          setTimeout(() => {  
            unmount()  
          }, 500)  
        }  
      }, [visible])  
      return (  
        <wd-popup  
          model-value={visible}  
          root-portal={true}  
          position="bottom"  
          onEnter={(event) => {  
            console.log('enter')  
          }}  
          onClose={() => {  
            console.log('close')  
          }}  
        >  
          <Sub a={2}>  
            <View>  
              {true}  
              {false}  
            </View>  
          </Sub>  
          {value % 2 === 0 ? <View>hello react</View> : null}  
          <View>{value}</View>  
          <Button onClick={() => setValue((v) => v   1)}>count  </Button>  
          <Button onClick={() => setValue((v) => v - 1)}>count -</Button>  
          <Button  
            onClick={(event) => {  
              console.log('remove', event)  
              setVisible(false)  
            }}  
          >  
            remove  
          </Button>  
        </wd-popup>  
      )  
    }  

    const id = renderRef.value?.render(  
      <Hello  
        unmount={() => {  
          renderRef.value?.unmount(id)  
        }}  
      />  
    )  

基于这个理由我又造了个轮子,可以让下面的代码运行在各个平台上成了现实,感兴趣的伙伴可以试用下面这个包

安装插件

# 安装插件包  
npm i @js-css/uni-app-react  
# 安装依赖包  
npm i preact @types/react

vite.config.ts 中添加如下配置

import { defineConfig } from 'vite'  
import uni from '@dcloudio/vite-plugin-uni'  
import { UniAppReact } from '@js-css/uni-app-react/dist/plugins/jsx'  
import * as path from 'node:path'  

// https://vitejs.dev/config/  
export default defineConfig({  
  plugins: [  
    // 添加插件  
    UniAppReact(),  
    uni(),  
  ],  
  resolve: {  
    alias: {  
      '@': '/src',  
      // 添加下面四个alias  
      react: path.resolve(__dirname, './node_modules/preact/compat'),  
      'react-is': path.resolve(__dirname, './node_modules/preact/compat'),  
      'react-dom': path.resolve(__dirname, './node_modules/preact/compat'),  
      '@js-css/uni-app-react': path.resolve(  
        __dirname,  
        './node_modules/@js-css/uni-app-react'  
      ),  
    },  
  },  
})

pages.json 中添加一个全局组件 "document": "/document" 该组件由插件自动注入

{  
  "pages": [  
    ...  
  ],  
  "globalStyle": {  
    "navigationBarTextStyle": "black",  
    ...  
    // 添加一个固定的全局组件,该组件由插件自动注入,只需要添加配置即可  
    "usingComponents": {  
      "document": "/document"  
    }  
  }  
}  
继续阅读 »

首先dcloud官方花费了巨大的资源保证了全端的api、开发体验保持一致、对于前端开发来说抹平了太多的平台差异,非常之牛逼!!!
uniapp的跨平台能力和生态都非常不错,尤其是在当你需要夸各种平台的时候一套完整的api是多么的珍贵,如果不考虑小程序环境的话那开发起来还是非常顺手的,但是当用uniapp开发小程序的时候,由于小程序环境的限制,太多的特性不能使用,尤其是JSX的能力,这样的话开发起来就束手束脚,很多非常平常的功能,比如说全局的Toast,Modal等等都需要以一个非常受限的方式实现!!!

假设下面这段代码可以直接运行在uniapp中那么开发起来就会无比丝滑

const Sub = (props: any) => {  
      const { children } = props  
      const [value, setValue] = useState(100)  
      useEffect(() => {  
        const timer = setInterval(() => {  
          setValue((value) => {  
            if (value >= 110) {  
              clearInterval(timer)  
            }  
            return value   1  
          })  
        }, 1000)  
        return () => {  
          clearInterval(timer)  
        }  
      }, [])  
      return (  
        <View>  
          sub  
          {value}  
          {value % 2 === 0 ? children : null}  
        </View>  
      )  
    }  
    const Hello = (props: any) => {  
      const [value, setValue] = useState(200)  
      const { unmount } = props  
      const [visible, setVisible] = useState(true)  
      useEffect(() => {  
        if (!visible) {  
          setTimeout(() => {  
            unmount()  
          }, 500)  
        }  
      }, [visible])  
      return (  
        <wd-popup  
          model-value={visible}  
          root-portal={true}  
          position="bottom"  
          onEnter={(event) => {  
            console.log('enter')  
          }}  
          onClose={() => {  
            console.log('close')  
          }}  
        >  
          <Sub a={2}>  
            <View>  
              {true}  
              {false}  
            </View>  
          </Sub>  
          {value % 2 === 0 ? <View>hello react</View> : null}  
          <View>{value}</View>  
          <Button onClick={() => setValue((v) => v   1)}>count  </Button>  
          <Button onClick={() => setValue((v) => v - 1)}>count -</Button>  
          <Button  
            onClick={(event) => {  
              console.log('remove', event)  
              setVisible(false)  
            }}  
          >  
            remove  
          </Button>  
        </wd-popup>  
      )  
    }  

    const id = renderRef.value?.render(  
      <Hello  
        unmount={() => {  
          renderRef.value?.unmount(id)  
        }}  
      />  
    )  

基于这个理由我又造了个轮子,可以让下面的代码运行在各个平台上成了现实,感兴趣的伙伴可以试用下面这个包

安装插件

# 安装插件包  
npm i @js-css/uni-app-react  
# 安装依赖包  
npm i preact @types/react

vite.config.ts 中添加如下配置

import { defineConfig } from 'vite'  
import uni from '@dcloudio/vite-plugin-uni'  
import { UniAppReact } from '@js-css/uni-app-react/dist/plugins/jsx'  
import * as path from 'node:path'  

// https://vitejs.dev/config/  
export default defineConfig({  
  plugins: [  
    // 添加插件  
    UniAppReact(),  
    uni(),  
  ],  
  resolve: {  
    alias: {  
      '@': '/src',  
      // 添加下面四个alias  
      react: path.resolve(__dirname, './node_modules/preact/compat'),  
      'react-is': path.resolve(__dirname, './node_modules/preact/compat'),  
      'react-dom': path.resolve(__dirname, './node_modules/preact/compat'),  
      '@js-css/uni-app-react': path.resolve(  
        __dirname,  
        './node_modules/@js-css/uni-app-react'  
      ),  
    },  
  },  
})

pages.json 中添加一个全局组件 "document": "/document" 该组件由插件自动注入

{  
  "pages": [  
    ...  
  ],  
  "globalStyle": {  
    "navigationBarTextStyle": "black",  
    ...  
    // 添加一个固定的全局组件,该组件由插件自动注入,只需要添加配置即可  
    "usingComponents": {  
      "document": "/document"  
    }  
  }  
}  
收起阅读 »

基于vue3.5+vite7+electron38仿微信/QQ电脑端聊天应用

vite vue.js vue3

vue3-electron38-wechat:一款最新原创跨平台electron38+vite7.0+vue3 setup+pinia3+element-plus等技术构建的仿微信电脑端聊天系统。包含了聊天、通讯录、收藏、朋友圈、短视频、我的等模块。

技术框架

  • 前端框架:vite7.1.2+vue3.5.18+vue-router4.5.1
  • 跨平台框架:electron38.0.0
  • 组件库:element-plus^2.11.2
  • 状态管理:pinia^3.0.3
  • 存储服务:pinia-plugin-persistedstate^4.5.0
  • 打包构建:electron-builder^24.13.3
  • electron结合vite插件:vite-plugin-electron^0.29.0

项目框架结构目录

最新跨平台框架electron38+vite7创建项目模板,vue3 setup语法编码。

electron-vue3-winchat聊天项目已经同步到我的原创作品集。

Electron38+Vue3+ElementPlus仿微信客户端聊天系统

热文推荐

uniapp-vue3-os手机oa系统|uni-app+vue3跨三端os后台管理模板
最新版uni-app+vue3+uv-ui跨三端仿微信app聊天应用【h5+小程序+app端】
Flutter3-MacOS桌面OS系统|flutter3.32+window_manager客户端OS模板
最新研发flutter3.27+bitsdojo_window+getx客户端仿微信聊天Exe应用
最新版Flutter3.32+Dart3.8跨平台仿微信app聊天界面|朋友圈
最新版uniapp+vue3+uv-ui跨三端短视频+直播+聊天【H5+小程序+App端】
原创uniapp+vue3+deepseek+uv-ui跨端实战仿deepseek/豆包流式ai聊天对话助手。
vue3-webseek网页版AI问答|Vite6+DeepSeek+Arco流式ai聊天打字效果
Electron35-DeepSeek桌面端AI系统|vue3.5+electron+arco客户端ai模板
uniapp+vue3酒店预订|vite5+uniapp预约订房系统模板(h5+小程序+App端)
Electron32-Vue3OS桌面版os系统|vue3+electron+arco客户端OS管理模板
Tauri2.0+Vite5聊天室|vue3+tauri2+element-plus仿微信|tauri聊天应用
tauri2.0-admin桌面端后台系统|Tauri2+Vite5+ElementPlus管理后台EXE程序

继续阅读 »

vue3-electron38-wechat:一款最新原创跨平台electron38+vite7.0+vue3 setup+pinia3+element-plus等技术构建的仿微信电脑端聊天系统。包含了聊天、通讯录、收藏、朋友圈、短视频、我的等模块。

技术框架

  • 前端框架:vite7.1.2+vue3.5.18+vue-router4.5.1
  • 跨平台框架:electron38.0.0
  • 组件库:element-plus^2.11.2
  • 状态管理:pinia^3.0.3
  • 存储服务:pinia-plugin-persistedstate^4.5.0
  • 打包构建:electron-builder^24.13.3
  • electron结合vite插件:vite-plugin-electron^0.29.0

项目框架结构目录

最新跨平台框架electron38+vite7创建项目模板,vue3 setup语法编码。

electron-vue3-winchat聊天项目已经同步到我的原创作品集。

Electron38+Vue3+ElementPlus仿微信客户端聊天系统

热文推荐

uniapp-vue3-os手机oa系统|uni-app+vue3跨三端os后台管理模板
最新版uni-app+vue3+uv-ui跨三端仿微信app聊天应用【h5+小程序+app端】
Flutter3-MacOS桌面OS系统|flutter3.32+window_manager客户端OS模板
最新研发flutter3.27+bitsdojo_window+getx客户端仿微信聊天Exe应用
最新版Flutter3.32+Dart3.8跨平台仿微信app聊天界面|朋友圈
最新版uniapp+vue3+uv-ui跨三端短视频+直播+聊天【H5+小程序+App端】
原创uniapp+vue3+deepseek+uv-ui跨端实战仿deepseek/豆包流式ai聊天对话助手。
vue3-webseek网页版AI问答|Vite6+DeepSeek+Arco流式ai聊天打字效果
Electron35-DeepSeek桌面端AI系统|vue3.5+electron+arco客户端ai模板
uniapp+vue3酒店预订|vite5+uniapp预约订房系统模板(h5+小程序+App端)
Electron32-Vue3OS桌面版os系统|vue3+electron+arco客户端OS管理模板
Tauri2.0+Vite5聊天室|vue3+tauri2+element-plus仿微信|tauri聊天应用
tauri2.0-admin桌面端后台系统|Tauri2+Vite5+ElementPlus管理后台EXE程序

收起阅读 »

关于音频播放器切换多个音频播放会卡死的问题

uniapp

两套代码 自己选一套用就能解决

export class Audio {  
    constructor() {  
        this.audio = uni.createInnerAudioContext()  
        this.audio.onCanplay(() => {  
            uni.hideLoading()  
        });  
        this.audio.onTimeUpdate(() => {  
            uni.$emit('time', this.audio.currentTime)  
        });  
        this.audio.onPlay(() => {  

        });  
        this.audio.onPause(() => {});  
        this.audio.onEnded(() => {  
            this.audio.playbackRate = 1  
            uni.$emit('playend')  
        })  
    }  
    changePlaySudu(sudu) {  
        this.audio.playbackRate = sudu  
    }  
    play(url) {  
        if (this.audio.src == url) {  
            this.audio.seek(0)  
        } else {  
            this.audio.src = url  
        }  
        this.audio.play()  
    }  
    end() {  
        this.audio.pause()  
    }  
    dele() {  
        this.audio.pause();  
        this.audio.destroy();  
        this.audio = null;  
    }  
}  
export class Audios {  
    constructor() {  
        this.audio = uni.createInnerAudioContext();  
        this.audio.autoplay = true;  
        this.subscribers = {};  
        this.startSlider = 0;  
        this.endSlider = 0;  
        this.isPaused = false;  
        this.loadInitialAudio();  
    }  
    async loadInitialAudio() {  
        this.audio.onCanplay(() => {  
            if (this.startSlider) {  
                this.audio.pause();  
                this.audio.seek(this.startSlider);  
                this.audio.play();  
            }  
            this.emit('duration', this.audio.duration);  
            uni.hideLoading();  
        });  
        this.audio.onTimeUpdate(() => {  
            console.log(this.audio.currentTime);  
            this.emit('time', this.audio.currentTime, this.audio.duration);  
            if (this.endSlider > 0 && this.audio.currentTime > this.endSlider) {  
                this.audio.pause();  
            }  
        });  
        this.audio.onPlay(() => this.emit('alignPlay'));  
        this.audio.onPause(() => this.emit('playPause'));  
        this.audio.onEnded(() => this.emit('playend'));  
    }  
    // 切换音频 重新播放  
    playAudio(audioSrc, play = true, start, end) {  
        this.audio.pause();  
        if (this.audio.src === audioSrc && !this.isPaused) {  
            this.audio.play();  
            return;  
        }  
        if (this.audio.src === audioSrc && this.isPaused) {  
            this.audio.pause();  
            this.audio.seek(0);  
            this.audio.play();  
            return;  
        }  
        this.audio.src = audioSrc;  
    }  
    //切换播放时间点  
    changeAudioPlayCurrentTime(time) {  
        this.audio.pause();  
        this.audio.seek(time);  
        this.audio.play();  
    }  
    //暂停  
    pause() {  
        this.audio.pause();  
    }  
    //继续播放  
    play() {  
        if (this.audio.src && !this.isPaused) {  
            this.audio.play();  
            return;  
        }  
    }  
    //静音  
    toggleMute() {  
        this.audio.volume = this.audio.volume == 0 ? 1 : 0;  
    }  
    //播放速度  
    changePlaybackRate(value) {  
        if (this.audio) this.audio.playbackRate = value;  
    }  
    on(event, callback) {  
        if (!this.subscribers[event]) {  
            this.subscribers[event] = [];  
        }  
        this.subscribers[event].push(callback);  
    }  
    emit(event, ...args) {  
        if (this.subscribers[event]) {  
            this.subscribers[event].forEach((callback) => callback(...args));  
        }  
    }  
    onDestroy() {  
        this.audio.pause();  
        this.audio.offEnded();  
        this.audio.offPlay();  
        this.audio.offPause();  
        this.audio.offCanplay();  
        this.audio.offTimeUpdate();  
        this.audio.destroy();  
        this.audio = null;  
        this.subscribers = {};  
    }  
}
继续阅读 »

两套代码 自己选一套用就能解决

export class Audio {  
    constructor() {  
        this.audio = uni.createInnerAudioContext()  
        this.audio.onCanplay(() => {  
            uni.hideLoading()  
        });  
        this.audio.onTimeUpdate(() => {  
            uni.$emit('time', this.audio.currentTime)  
        });  
        this.audio.onPlay(() => {  

        });  
        this.audio.onPause(() => {});  
        this.audio.onEnded(() => {  
            this.audio.playbackRate = 1  
            uni.$emit('playend')  
        })  
    }  
    changePlaySudu(sudu) {  
        this.audio.playbackRate = sudu  
    }  
    play(url) {  
        if (this.audio.src == url) {  
            this.audio.seek(0)  
        } else {  
            this.audio.src = url  
        }  
        this.audio.play()  
    }  
    end() {  
        this.audio.pause()  
    }  
    dele() {  
        this.audio.pause();  
        this.audio.destroy();  
        this.audio = null;  
    }  
}  
export class Audios {  
    constructor() {  
        this.audio = uni.createInnerAudioContext();  
        this.audio.autoplay = true;  
        this.subscribers = {};  
        this.startSlider = 0;  
        this.endSlider = 0;  
        this.isPaused = false;  
        this.loadInitialAudio();  
    }  
    async loadInitialAudio() {  
        this.audio.onCanplay(() => {  
            if (this.startSlider) {  
                this.audio.pause();  
                this.audio.seek(this.startSlider);  
                this.audio.play();  
            }  
            this.emit('duration', this.audio.duration);  
            uni.hideLoading();  
        });  
        this.audio.onTimeUpdate(() => {  
            console.log(this.audio.currentTime);  
            this.emit('time', this.audio.currentTime, this.audio.duration);  
            if (this.endSlider > 0 && this.audio.currentTime > this.endSlider) {  
                this.audio.pause();  
            }  
        });  
        this.audio.onPlay(() => this.emit('alignPlay'));  
        this.audio.onPause(() => this.emit('playPause'));  
        this.audio.onEnded(() => this.emit('playend'));  
    }  
    // 切换音频 重新播放  
    playAudio(audioSrc, play = true, start, end) {  
        this.audio.pause();  
        if (this.audio.src === audioSrc && !this.isPaused) {  
            this.audio.play();  
            return;  
        }  
        if (this.audio.src === audioSrc && this.isPaused) {  
            this.audio.pause();  
            this.audio.seek(0);  
            this.audio.play();  
            return;  
        }  
        this.audio.src = audioSrc;  
    }  
    //切换播放时间点  
    changeAudioPlayCurrentTime(time) {  
        this.audio.pause();  
        this.audio.seek(time);  
        this.audio.play();  
    }  
    //暂停  
    pause() {  
        this.audio.pause();  
    }  
    //继续播放  
    play() {  
        if (this.audio.src && !this.isPaused) {  
            this.audio.play();  
            return;  
        }  
    }  
    //静音  
    toggleMute() {  
        this.audio.volume = this.audio.volume == 0 ? 1 : 0;  
    }  
    //播放速度  
    changePlaybackRate(value) {  
        if (this.audio) this.audio.playbackRate = value;  
    }  
    on(event, callback) {  
        if (!this.subscribers[event]) {  
            this.subscribers[event] = [];  
        }  
        this.subscribers[event].push(callback);  
    }  
    emit(event, ...args) {  
        if (this.subscribers[event]) {  
            this.subscribers[event].forEach((callback) => callback(...args));  
        }  
    }  
    onDestroy() {  
        this.audio.pause();  
        this.audio.offEnded();  
        this.audio.offPlay();  
        this.audio.offPause();  
        this.audio.offCanplay();  
        this.audio.offTimeUpdate();  
        this.audio.destroy();  
        this.audio = null;  
        this.subscribers = {};  
    }  
}
收起阅读 »

关于短信服务签名有效期管理的通知

短信

为保障短信服务的规范化和高效使用,自 2025 年 9 月 10 日起,平台将正式实施 短信签名有效期管理规则:

一、规则说明

  • 签名有效期管理
    对于连续 6 个月未活跃(包括但不限于未补充信息、未修改签名、未使用该签名发送短信等操作)的短信签名,系统将自动标记为 “失效状态”。(失效状态不会直接清理或删除数据)

  • 恢复方式
    已失效的签名需通过 uniCloud 后台 → 短信服务 → 签名管理 页面,点击 编辑 并重新提交报备申请,经审核通过后方可恢复使用。

  • 使用限制
    在签名失效期间,该签名将无法用于短信发送,请您提前做好相关业务调整。

二、风险提示与建议

为避免签名失效影响业务,建议您:

  • 定期检查
    登录 uniCloud 后台 → 短信服务 → 签名管理 页面,关注名下签名的状态及最后一次短信发送记录;

  • 保持活跃度
    若签名暂时未在业务中使用,可通过 补充信息 或 发送测试短信 等操作,维持签名的活跃状态;

  • 关注提醒
    平台将在签名即将失效前,通过邮件向您发送倒计时提醒,请根据提示及时完成必要操作。

继续阅读 »

为保障短信服务的规范化和高效使用,自 2025 年 9 月 10 日起,平台将正式实施 短信签名有效期管理规则:

一、规则说明

  • 签名有效期管理
    对于连续 6 个月未活跃(包括但不限于未补充信息、未修改签名、未使用该签名发送短信等操作)的短信签名,系统将自动标记为 “失效状态”。(失效状态不会直接清理或删除数据)

  • 恢复方式
    已失效的签名需通过 uniCloud 后台 → 短信服务 → 签名管理 页面,点击 编辑 并重新提交报备申请,经审核通过后方可恢复使用。

  • 使用限制
    在签名失效期间,该签名将无法用于短信发送,请您提前做好相关业务调整。

二、风险提示与建议

为避免签名失效影响业务,建议您:

  • 定期检查
    登录 uniCloud 后台 → 短信服务 → 签名管理 页面,关注名下签名的状态及最后一次短信发送记录;

  • 保持活跃度
    若签名暂时未在业务中使用,可通过 补充信息 或 发送测试短信 等操作,维持签名的活跃状态;

  • 关注提醒
    平台将在签名即将失效前,通过邮件向您发送倒计时提醒,请根据提示及时完成必要操作。

收起阅读 »

App 上架流程全解析 iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传 ipa 与审核经验分享

iOS

'''无论是个人开发者还是团队,应用开发完成后都必须面对一个核心问题:app上架流程
与 Android 平台的相对宽松不同,苹果 App Store 上架流程 更严格,涉及 开发者账号、证书准备、应用打包、ipa 上传、测试分发、审核发布 等多个环节。
特别是使用 uni-app 跨平台开发的团队,虽然开发阶段效率更高,但在 iOS 应用发布 时依然需要精准操作,才能顺利完成上架。

本文将结合实战经验,全面解析 app上架流程,并分享多工具组合(Xcode、Transporter、Appuploader、Fastlane)的最佳实践。


一、App 上架流程的第一步:开发者账号与证书

1. Apple 开发者账号

  • 个人账号:适合独立开发者,99 美元/年。
  • 企业账号:适合公司团队,支持更多分发方式。

2. iOS 证书与描述文件

  • 开发证书:用于真机调试。
  • 发布证书:用于 TestFlight 与 App Store 上架。
  • 描述文件:控制应用的分发方式(Ad Hoc、App Store)。

工具支持

  • Xcode:适合 Mac 用户,自动生成证书。
  • Appuploader:适合 Windows/Linux 用户,生成 .p12.mobileprovision 文件,方便团队协作。

二、uni-app 应用的打包方式

1. HBuilderX 云打包

  • 上传证书与描述文件,云端自动生成 ipa 文件。
  • 适合没有 Mac 的小团队。

2. Xcode 本地打包

  • 从 HBuilderX 导出 Xcode 工程,在 Mac 上 Archive 打包。
  • 灵活性更强,适合正式发布。

实战经验:小版本更新走云打包,大版本发布走 Xcode 打包。


三、ipa 上传:app 上架流程的核心环节

生成 ipa 后,必须上传到苹果服务器。

上传工具对比

  • Xcode 上传:最常见方式,但大文件容易失败。
  • Transporter App:苹果官方上传工具,更稳定。
  • Appuploader:跨平台支持 Windows/Linux,免 Mac 上传 ipa。
  • Fastlane:自动化上传工具,适合团队 CI/CD 流程。

推荐组合

  • 独立开发者:Xcode + Transporter。
  • 团队开发:Fastlane 自动化上传,Appuploader 备用。

四、测试分发:验证应用在不同设备上的稳定性

在正式上架之前,测试分发是必不可少的环节。

  1. Ad Hoc 分发
    • 限制 100 台设备,适合内部调试。
  2. TestFlight 内测
    • 最多 25 名团队成员,适合小范围验证。
  3. TestFlight 外测
    • 最多 10,000 用户,适合大规模测试。
  4. 二维码安装
    • 使用 Appuploader 生成二维码,方便产品与运营快速安装体验。

五、App Store 审核:app 上架流程的最后一关

苹果审核严格,常见驳回原因包括:

  • 壳应用嫌疑:uni-app 应用如果只是简单封装 H5 页面,容易被拒。
  • 素材不足:缺少截图或多语言描述。
  • 权限说明不全:如相机、麦克风、定位用途未明确说明。

审核优化技巧

  • 功能必须完整,避免“套壳”应用。
  • App Store Connect 上传完整截图和多语言描述。
  • 使用 Appuploader 批量上传截图,减少人工操作。
  • 在 Info.plist 中写明权限用途,确保合规。
  • 紧急情况下可申请 加急审核

六、实战案例:一个 uni-app 工具类应用的上架过程

一个 5 人团队开发的工具类应用,他们的 app 上架流程 如下:

  1. 运维人员在 Windows 用 Appuploader 生成证书并共享。
  2. 开发人员用 HBuilderX 云打包生成 ipa。
  3. 测试人员用 Transporter 上传 ipa 至 TestFlight,覆盖多款 iOS 设备。
  4. 产品经理在 App Store Connect 上传截图和多语言描述。
  5. 应用审核一次性通过,成功上架 App Store。

这种流程让团队即使只有一台 Mac,也能顺利完成整个流程。


七、经验总结

  1. 证书管理必须规范,避免签名错误。
  2. 打包方式要结合使用,云打包快速,本地打包稳定。
  3. 上传工具多样化,Xcode、Transporter、Appuploader、Fastlane 各有优势。
  4. 测试分发要分阶段,先内部验证,再外部扩展。
  5. 审核要准备充分,素材完整、权限说明清晰。

app 上架流程 虽然复杂,但并非不可掌握。
通过合理使用 HBuilderX、Xcode、Transporter、Appuploader、Fastlane 等工具,开发者完全可以高效完成从 应用开发到 App Store 发布 的全过程。
无论是独立开发者还是团队,掌握这些实战经验,才能让应用更顺利进入市场。'''

继续阅读 »

'''无论是个人开发者还是团队,应用开发完成后都必须面对一个核心问题:app上架流程
与 Android 平台的相对宽松不同,苹果 App Store 上架流程 更严格,涉及 开发者账号、证书准备、应用打包、ipa 上传、测试分发、审核发布 等多个环节。
特别是使用 uni-app 跨平台开发的团队,虽然开发阶段效率更高,但在 iOS 应用发布 时依然需要精准操作,才能顺利完成上架。

本文将结合实战经验,全面解析 app上架流程,并分享多工具组合(Xcode、Transporter、Appuploader、Fastlane)的最佳实践。


一、App 上架流程的第一步:开发者账号与证书

1. Apple 开发者账号

  • 个人账号:适合独立开发者,99 美元/年。
  • 企业账号:适合公司团队,支持更多分发方式。

2. iOS 证书与描述文件

  • 开发证书:用于真机调试。
  • 发布证书:用于 TestFlight 与 App Store 上架。
  • 描述文件:控制应用的分发方式(Ad Hoc、App Store)。

工具支持

  • Xcode:适合 Mac 用户,自动生成证书。
  • Appuploader:适合 Windows/Linux 用户,生成 .p12.mobileprovision 文件,方便团队协作。

二、uni-app 应用的打包方式

1. HBuilderX 云打包

  • 上传证书与描述文件,云端自动生成 ipa 文件。
  • 适合没有 Mac 的小团队。

2. Xcode 本地打包

  • 从 HBuilderX 导出 Xcode 工程,在 Mac 上 Archive 打包。
  • 灵活性更强,适合正式发布。

实战经验:小版本更新走云打包,大版本发布走 Xcode 打包。


三、ipa 上传:app 上架流程的核心环节

生成 ipa 后,必须上传到苹果服务器。

上传工具对比

  • Xcode 上传:最常见方式,但大文件容易失败。
  • Transporter App:苹果官方上传工具,更稳定。
  • Appuploader:跨平台支持 Windows/Linux,免 Mac 上传 ipa。
  • Fastlane:自动化上传工具,适合团队 CI/CD 流程。

推荐组合

  • 独立开发者:Xcode + Transporter。
  • 团队开发:Fastlane 自动化上传,Appuploader 备用。

四、测试分发:验证应用在不同设备上的稳定性

在正式上架之前,测试分发是必不可少的环节。

  1. Ad Hoc 分发
    • 限制 100 台设备,适合内部调试。
  2. TestFlight 内测
    • 最多 25 名团队成员,适合小范围验证。
  3. TestFlight 外测
    • 最多 10,000 用户,适合大规模测试。
  4. 二维码安装
    • 使用 Appuploader 生成二维码,方便产品与运营快速安装体验。

五、App Store 审核:app 上架流程的最后一关

苹果审核严格,常见驳回原因包括:

  • 壳应用嫌疑:uni-app 应用如果只是简单封装 H5 页面,容易被拒。
  • 素材不足:缺少截图或多语言描述。
  • 权限说明不全:如相机、麦克风、定位用途未明确说明。

审核优化技巧

  • 功能必须完整,避免“套壳”应用。
  • App Store Connect 上传完整截图和多语言描述。
  • 使用 Appuploader 批量上传截图,减少人工操作。
  • 在 Info.plist 中写明权限用途,确保合规。
  • 紧急情况下可申请 加急审核

六、实战案例:一个 uni-app 工具类应用的上架过程

一个 5 人团队开发的工具类应用,他们的 app 上架流程 如下:

  1. 运维人员在 Windows 用 Appuploader 生成证书并共享。
  2. 开发人员用 HBuilderX 云打包生成 ipa。
  3. 测试人员用 Transporter 上传 ipa 至 TestFlight,覆盖多款 iOS 设备。
  4. 产品经理在 App Store Connect 上传截图和多语言描述。
  5. 应用审核一次性通过,成功上架 App Store。

这种流程让团队即使只有一台 Mac,也能顺利完成整个流程。


七、经验总结

  1. 证书管理必须规范,避免签名错误。
  2. 打包方式要结合使用,云打包快速,本地打包稳定。
  3. 上传工具多样化,Xcode、Transporter、Appuploader、Fastlane 各有优势。
  4. 测试分发要分阶段,先内部验证,再外部扩展。
  5. 审核要准备充分,素材完整、权限说明清晰。

app 上架流程 虽然复杂,但并非不可掌握。
通过合理使用 HBuilderX、Xcode、Transporter、Appuploader、Fastlane 等工具,开发者完全可以高效完成从 应用开发到 App Store 发布 的全过程。
无论是独立开发者还是团队,掌握这些实战经验,才能让应用更顺利进入市场。'''

收起阅读 »

翔一样!

4.76更新HBuilderx定位元素失效布局变紊乱
打包必须android/iOS一起打包,中途不能停止(sb设计)

4.76更新HBuilderx定位元素失效布局变紊乱
打包必须android/iOS一起打包,中途不能停止(sb设计)