普通视图

发现新文章,点击刷新页面。
昨天 — 2025年10月23日首页
昨天以前首页

喜欢做马甲包的有福了~现在多了一招续费方式!

作者 iOS研究院
2025年10月15日 19:31

背景

经常做马甲包的朋友都是知道,在账号没有被封之前是好的。

苹果过审的时候,你是心高气傲。封号3,2f的时候,你是爱莫能助。如有需要请后台留言,专注AppStore各种疑难杂症!

被封禁的账号除了要做好,基本上物理隔离、收款隔离,还有一点就是付款隔离。(目前这块,其实并不严格。但是求稳肯定是隔离了问心无愧。)

省流版本:

今天就分享一个新的付款支付方式-抖*支*付!

选择入口

前往设置 -》 Apple账户 -》 付款与配送 -》 添加新的付款方式

绑定跳转

选择支付方式-》抖*,即将前往App。

绑定完毕

绑定完毕,可以查看绑定账户信息。同时,可以支付设置中配置支付信息。

绑定完成

添加完成示意图

账号申请不易,3.2f亦是雪上加霜。愿各位且行且珍惜,多行大道,毕竟陷入囧地!

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

相关推荐

# 苹果开发者续费大坑及成功续费方案!亲测有效

# AppStore敏感词排查手册,多维度分析Guideline 2.3.1隐藏功能,轻松过审。

# Pingpong和连连的平替,让AppStore收款无需新增持有人。

# 苹果加急审核是“绿色通道”还是“死亡陷阱”?

# 苹果开发者邮箱,突然收到11.2通知严重么?

# 不想被苹果卡审最好错开这两个提审时间

# 手撕苹果审核4.3是代码问题还是设计问题?

# 有幸和Appstore审核人员进行了一场视频会议特此记录。

🔥开源零配置!10 分钟上手:create-uni + uView Pro 快速搭建企业级 uni-app 项目

2025年10月14日 13:30

推荐阅读:

🔥 uView Pro 正式开源!70+ Vue3 组件重构完成,uni-app 组件库新晋之星

本文面向希望快速搭建 uni-app 项目的开发者与团队,介绍如何使用 create-uni 脚手架一键创建项目,如何在项目中引入并配置 uView Pro 组件库,以及如何利用 uni-helper 系列插件(vite-plugin、unocss 等)提高开发效率。

一、为什么选择 create-uni + uView Pro?

在 uniapp 构建的多端工程中,速度与一致性至关重要。

create-uni 提供一键生成、模板丰富的项目引导能力,而 uView Pro 则是基于 Vue3 + TypeScript 全面重构的高质量 uni-app 组件库。两者结合,能带来:

  • 快速上手:一行命令生成标准化项目结构;
  • 现代开发体验:Vite + Vue3 + TS,热更新快、类型友好;
  • 丰富组件:70+ 高质量组件覆盖主流业务场景;
  • 高度可扩展:uni-helper 插件体系支持文件路由、按需组件、布局系统等;
  • 企业友好:模板、样式、规范一致,便于团队协作与维护。

6.png

0.png

二、准备工作(环境与工具)

在开始之前,建议准备以下环境:

  • Node.js(建议 LTS 版本,如 18.x 或 20.x)
  • pnpm / npm / yarn(推荐 pnpm,速度更快且适合 monorepo)
  • VS Code + Volar(强烈推荐,Vue3 + TypeScript 最佳搭配,禁用 Vetur)
  • HBuilderX(如果需要使用 HBuilderX 工具链或插件市场,非必要不使用)

确保全局工具可用:

# 建议使用 pnpm
npm install -g pnpm
# 若需要全局安装脚手架(可选)
npm install -g @dcloudio/uni-app

三、使用 create-uni 快速创建项目(一步到位)

create-uni 是一套现代化脚手架,支持选择模板、快速集成 uView Pro 组件库等,下面给出用 pnpm create 的推荐流程:

# 使用 create-uni(交互式选择项目模板)
pnpm create uni@latest
cd my-uni-project
pnpm install

# 启动开发(以 H5 为例)
pnpm run dev:h5

在交互式选择时,选择需要的插件和库、选择需要的组件库 uView Pro ,可以让项目开箱即用:根据您的选择可以帮助您自动集成 uView Pro、UnoCSS、uni-helper 等插件,省去大量配置时间。

示例:

  • 选择需要的 vite 插件时勾选必要的插件:

    • vite-plugin-uni-pages(提供基于文件系统的路由)
    • vite-plugin-uni-components(按需自动引入组件)
    • vite-plugin-uni-layouts(提供类 nuxt 的 layouts 系统)
    • vite-plugin-uni-manifest(自动生成 manifest.json 文件)
  • 选择需要的库时勾选必要的库:

    • Pinia
    • Unocss
  • 选择 UI 组件库时勾选 uView Pro

通过以上选择完成后,脚手架会自动创建包含以下内容的项目:

  • Vite + uni-app 项目骨架
  • uview-pro 依赖与全局样式引入(index.scss / theme.scss)
  • 推荐的 tsconfig.jsonvite.config.ts 配置
  • UnoCSS 与 uni-helper 插件预配置

1.png

四、手动在已存在项目中安装 uView Pro(npm 或 uni_modules)

如果你已用其它方式创建项目,并不是使用 create-uni,下面是两种常见安装方式,分别适用于 CLI 项目(npm)与 HBuilderX 项目(uni_modules)。

1. CLI(npm / pnpm)方式(推荐团队/CLI 项目)

pnpm add uview-pro
# 或者 npm install uview-pro --save

在 Vue3 项目中,全局引入并注册:

// main.ts
import { createSSRApp } from "vue";
import uViewPro from "uview-pro";

export function createApp() {
  const app = createSSRApp(App);
  app.use(uViewPro);
  return {
    app,
  };
}

uni.scss 中引入主题:

@import "uview-pro/theme.scss";

在  App.vue  首行引入基础样式:

<style lang="scss">
  @import "uview-pro/index.scss";
</style>

pages.json / vite 的 easycom 配置中添加:

"easycom": {
  "autoscan": true,
  "custom": {
    "^u-(.*)": "uview-pro/components/u-$1/u-$1.vue"
  }
}

也可以使用@uni-helper/vite-plugin-uni-components(基于文件的按需组件引入)插件来替换 easycom 的方式,详细使用方式见下述介绍。

注:CLI npm 方式更易管理版本、配合 TypeScript 与 Volar 获得更好类型提示体验。

2. HBuilderX(uni_modules)方式(推荐 HBuilderX 项目)

uview-pro 目录放入项目 uni_modules 下(或通过插件市场安装);

DCloud 插件市场:ext.dcloud.net.cn/plugin?id=2…

main.ts全局引入并注册

// main.ts
import { createSSRApp } from 'vue'
import uViewPro from "@/uni_modules/uview-pro";

export function createApp() {
  const app = createSSRApp(App)
  app.use(uViewPro)
  return {
    app
  }
}

pages.json 中配置 easycom:

"easycom": {
  "autoscan": true,
  "custom": {
    "^u-(.*)": "@/uni_modules/uview-pro/components/u-$1/u-$1.vue"
  }
}

uni.scss 中引入主题:

@import "@/uni_modules/uview-pro/theme.scss";

在  App.vue  首行引入基础样式:

<style lang="scss">
  @import "@/uni_modules/uview-pro/index.scss";
</style>

HBuilderX 下,uni_modules 更符合编辑器和打包器的约定,部分原生插件或小程序构建会更兼容。

因此:建议 CLI 项目使用 npm/pnpm 方式,HBuilderX 项目使用 uni_modules 方式

五、结合 uni-helper 插件提升开发效率

uni-helper 系列插件在 vite + uni-app 生态下提供了大量现代化的便利能力。下面按插件逐一介绍它们的作用、安装、配置示例、与 uView Pro 的配合要点以及常见注意事项。

2.png

更多用法及插件请访问 uni-helper 官网文档:uni-helper.js.org/

1. @uni-helper/vite-plugin-uni-pages(文件系统路由)

作用:

  • 自动扫描 src/pagespages 目录,基于文件系统生成路由配置,替代手动维护 pages.json 的繁琐流程;
  • 支持页面元数据、分组、全局样式定义和路由扩展;
  • 提供 virtual:uni-pages 等虚拟模块用于在代码中读取页面信息,便于构建菜单、统计或自动化文档。

安装:

pnpm add -D @uni-helper/vite-plugin-uni-pages

基本配置(vite.config.ts):

import { defineConfig } from "vite";
import Uni from "@uni-helper/plugin-uni";
import UniPages from "@uni-helper/vite-plugin-uni-pages";

export default defineConfig({
  plugins: [UniPages(), Uni()],
});

pages 配置示例(pages.config.ts):

import { defineUniPages } from "@uni-helper/vite-plugin-uni-pages";

export default defineUniPages({
  pages: [],
  globalStyle: {
    navigationBarTextStyle: "black",
    navigationBarTitleText: "MyApp",
  },
  subPackages: [],
});

在代码中获取页面元数据:

/// <reference types="@uni-helper/vite-plugin-uni-pages/client" />
import { pages } from "virtual:uni-pages";
console.log(pages);

与 uView Pro 的配合要点:

  • 结合 uView Pro Starter,路由自动化能让示例页面、文档 demo 与项目页面保持一致;
  • 当需要在页面自动注入组件演示或 demo 链接时,pages 元数据非常方便。

注意事项:

  • 如果同时存在手动维护的 pages.json,请确认插件优先级与覆盖规则;
  • 某些小程序平台对动态生成的路由有特殊限制,发布前务必在目标平台做真机测试。

2. @uni-helper/vite-plugin-uni-components(基于文件的按需组件引入)

作用:

  • 基于文件系统实现组件按需自动引入,类似于 Vue 的 unplugin-vue-components,但针对 uni-app 场景优化;
  • 可以替代 easycom 的全局扫描,减少启动扫描成本并提升按需加载精度;
  • 支持自定义规则、扩展第三方组件库的映射。

安装:

pnpm add -D @uni-helper/vite-plugin-uni-components

配置示例,已经支持 uView Pro Resolver:

import { defineConfig } from "vite";
import Uni from "@uni-helper/plugin-uni";
import UniComponents from "@uni-helper/vite-plugin-uni-components";
import { uViewProResolver } from "@uni-helper/vite-plugin-uni-components/resolvers";

export default defineConfig({
  plugins: [
    UniComponents({
      dts: true,
      resolvers: [uViewProResolver()],
    }),
    Uni(),
  ],
});

与 uView Pro 的配合要点:

  • 使用此插件可避免在 pages.json 中重复写 easycom 规则;
  • 当配合 uview-pro 时,需要引入 uViewProResolver 使用;
  • 有助于实现按需打包,减小 H5 与小程序包体积。

注意事项:

  • 部分平台(例如 HBuilderX 的旧版本)可能仍需要 pages.json 的支持,务必在迁移前做兼容性验证;
  • 对于同名组件(不同来源)要明确命名或使用手动 import 以避免歧义。

3. @uni-helper/vite-plugin-uni-layouts(布局系统)

作用:

  • 在 uni-app 中实现类似 Nuxt 的布局机制(layouts),支持多个 layout 组件、slot、以及按页面应用布局;
  • 自动扫描 src/layouts 并将页面包裹在指定布局下,简化头部/尾部/侧边栏等公共区域维护。

安装:

pnpm add -D @uni-helper/vite-plugin-uni-layouts

配置示例:

import { defineConfig } from "vite";
import Uni from "@uni-helper/plugin-uni";
import UniLayouts from "@uni-helper/vite-plugin-uni-layouts";

export default defineConfig({
  plugins: [UniLayouts(), Uni()],
});

使用示例:

  • src/layouts/default.vue 中定义布局:
<template>
  <div class="layout">
    <slot name="header">默认头部</slot>
    <slot>主内容</slot>
    <slot name="footer">默认底部</slot>
  </div>
</template>
  • 在页面中指定布局(definePage):
<script setup>
definePage({ layout: "default" });
</script>

与 uView Pro 的配合要点:

  • 布局中可直接使用 uView Pro 的导航栏、Tabbar、Footer 等组件,保证风格统一;
  • 结合 uView Pro Starter,布局示例通常已经内置,直接复用即可。

注意事项:

  • 在微信小程序中如果页面使用 web-view,布局插件的包裹机制可能不生效;
  • 动态切换布局时注意保持页面状态。

4. @uni-helper/vite-plugin-uni-manifest(用 TypeScript 管理 manifest)

作用:

  • 允许使用 TypeScript 编写 manifest.json(如 manifest.config.ts),享受类型提示与可组合的配置方式;
  • 在构建时自动生成标准 manifest.json,并支持按平台差异化配置。

安装:

pnpm add -D @uni-helper/vite-plugin-uni-manifest

配置示例:

import Uni from "@uni-helper/plugin-uni";
import UniManifest from "@uni-helper/vite-plugin-uni-manifest";

export default defineConfig({
  plugins: [UniManifest(), Uni()],
});

示例 manifest.config.ts

import { defineManifestConfig } from "@uni-helper/vite-plugin-uni-manifest";

export default defineManifestConfig({
  appid: "your-appid",
  name: "MyApp",
  versionName: "1.0.0",
  h5: {
    devServer: {
      port: 8080,
    },
  },
});

与 uView Pro 的配合要点:

  • 将 theme 或构建相关的配置以类型化方式管理,便于在不同环境(dev/staging/prod)间切换;
  • 在企业项目中能更方便地实现 CI 自动化生成不同渠道包的 manifest 配置。

注意事项:

  • 生成的 manifest.json 应在真机或云打包平台上验证,避免配置项平台不兼容。

5. @uni-helper/vite-plugin-uni-platform(按平台文件替换)

作用:

  • 支持基于文件名的按平台编译,例如 index.h5.vueindex.mp-weixin.vueindex.app.vue 等,构建时自动替换为对应平台文件;
  • 便于按平台做差异化实现,同时保持统一的项目结构与代码管理。

安装:

pnpm add -D @uni-helper/vite-plugin-uni-platform

配置示例:

import Uni from "@uni-helper/plugin-uni";
import UniPlatform from "@uni-helper/vite-plugin-uni-platform";

export default defineConfig({
  plugins: [UniPlatform(), Uni()],
});

使用说明:

  • 在项目中创建文件如 pages/index.h5.vue 针对 H5 的实现,pages/index.mp-weixin.vue 针对微信小程序的实现;
  • 在编译目标为 H5 时,会优先使用 index.h5.vue,否则退回 index.vue

与 uView Pro 的配合要点:

  • 当使用 uView Pro 的某些平台相关适配(例如原生 SDK 或特定 API)时,可以在平台特定文件中做针对性封装;
  • 结合 uni-pages,能更方便地管理平台差异化页面列表。

注意事项:

  • 使用大量平台特异化文件会增加维护成本,建议仅在必要场景使用。

6. @uni-helper/unocss-preset-uni(UnoCSS 预设)

作用:

  • 为 uni-app 定制的 UnoCSS 预设,开箱即用的原子类工具集,支持属性化写法与按平台样式差异;
  • 极大减少重复样式、提高开发速度,同时配合 Uno 的即时编译,开发体验流畅。

安装:

pnpm add -D @uni-helper/unocss-preset-uni unocss unocss-applet

vite 配置示例:

import { defineConfig } from "vite";
import Uni from "@uni-helper/plugin-uni";
import UnoCSS from "unocss/vite";

export default defineConfig({
  plugins: [Uni(), UnoCSS()],
});

uno.config.ts 配置

import { presetUni } from "@uni-helper/unocss-preset-uni";

import {
  defineConfig,
  presetIcons,
  transformerDirectives,
  transformerVariantGroup,
} from "unocss";

export default defineConfig({
  presets: [
    presetUni({
      attributify: {
        // UnoCSS的解析规则可与uView Pro组件库内置样式冲突
        ignoreAttributes: ["size"],
      },
    }),
  ],
  transformers: [transformerDirectives(), transformerVariantGroup()],
});

与 uView Pro 的配合要点:

  • UnoCSS 非侵入式,可与 uView Pro 的 SCSS 主题变量共存;
  • 在快速原型或设计系统中,Uno 的原子类能极大提升迭代速度;
  • 推荐将设计变量(颜色、间距)同步到 uView Pro 的 theme.scss,并在 Uno 配置中复用。
  • 注意 UnoCSS 的解析规则可能会与 uView Pro 组件库内置样式冲突

注意事项:

  • UnoCSS 从 v0.59 起只提供 ESM 支持,某些老旧构建环境需降级或额外配置;
  • 在使用 apis 或小程序特性时,注意属性名与平台限制。

7. 插件组合示例(完整 vite.config.ts)

下面给出一个常见的 vite.config.ts 组合示例,展示如何把上面插件整合到同一个工程中:

import { fileURLToPath, URL } from "node:url";

import Uni from "@uni-helper/plugin-uni";
import Components from "@uni-helper/vite-plugin-uni-components";
import { uViewProResolver } from "@uni-helper/vite-plugin-uni-components/resolvers";
import UniLayouts from "@uni-helper/vite-plugin-uni-layouts";
import UniManifest from "@uni-helper/vite-plugin-uni-manifest";
import UniMiddleware from "@uni-helper/vite-plugin-uni-middleware";
import UniPages from "@uni-helper/vite-plugin-uni-pages";
import UniPlatform from "@uni-helper/vite-plugin-uni-platform";
import UniPlatformModifier from "@uni-helper/vite-plugin-uni-platform-modifier";
import UniRoot from "@uni-ku/root";
import UnoCSS from "unocss/vite";
import { defineConfig } from "vite";

export default defineConfig({
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
  plugins: [
    Components({
      dts: true,
      resolvers: [uViewProResolver()],
    }),
    UniPages(),
    UniLayouts(),
    UniManifest(),
    UniPlatform(),
    UniPlatformModifier(),
    UniMiddleware(),
    UniRoot(),
    Uni(),
    UnoCSS(),
  ],
});

8. 常见故障排查(针对插件集成)

  • uni-pages 未识别页面:确认目录结构、文件后缀以及 pages.config.* 是否存在语法错误;
  • uni-components 未按需引入:检查插件 dirs 配置与组件命名是否匹配,或手动添加 resolver;
  • layouts 无效:确认页面是否使用 definePage({ layout: 'xxx' }) 或 pages.json 的 layout 配置被覆盖;
  • manifest 生成错误:在本地构建时查看生成的 manifest.json,并在真机或云打包平台验证;
  • UnoCSS 样式不生效:确认 UnoCSS 是否在 plugins 列表中且 preset 已正确加载;
  • uView Pro 组件样式错乱:确认 UnoCss 解析规则是否与组件库存在冲突问题;

六、uView Pro Starter:开箱即用的项目模板

uView Pro Starter 是官方维护的快速启动模板,目前集成了 create-uni、uView Pro、UnoCSS 与 uni-helper 常用插件,适合作为企业或个人项目的起点。核心优势包括:

  • 规范的项目结构与开发脚本;
  • 预配置的 linter、格式化、TypeScript 与 Volar 支持;
  • UnoCSS 和主题变量已集成,支持快速定制风格;
  • 常用页面、布局、示例组件齐全,便于二次开发。

快速使用:

# 直接 clone
git clone https://github.com/anyup/uView-Pro-Starter.git
cd uView-Pro-Starter
pnpm install
pnpm run dev:h5

后面可以通过 create-uni 直接选择 uView Pro Starter 模板,目前还没建设完成。

Starter 的目的是把工程化、规范、常见实践都“开箱即用”,让团队把精力集中在业务实现上,而不是基础设施搭建。

3.png

5.png

4.png

七、uView Pro 与 uni-helper 的协同最佳实践(总结)

  • 使用 uView Pro Starter 作为项目模板,默认预集成了大部分插件配置,能让团队开箱即用;
  • 对于页面与组件的自动化引入,优先考虑 uni-pages + uni-components,降低重复维护成本;
  • uni-components 中为 u- 前缀做显式 resolver,避免与其他库冲突;
  • 将 uView Pro 的主题变量与 UnoCSS 的设计 tokens 做映射,保证样式统一且可维护;
  • 在 CI 中加入 pnpm install --frozen-lockfile、lint、typecheck 步骤,保证团队一致性;
  • 做好平台差异化管理(合理使用 uni-platform)但尽量减少全平台分支,以降低维护成本。

八、注意事项

1. 样式、sass 与版本兼容建议

在实际项目中,sass 与 sass-loader 的版本兼容性常会引发样式构建问题。建议在团队内统一并锁定版本,减少“本地能跑、CI 失败”的尴尬。

推荐版本(uView Pro 社区实践验证):

"sass": "1.63.2",
"sass-loader": "10.4.1"

同时,注意 uView Pro 的内部样式及主题文件采用 @import 形式引入。所以一定要注意 sass 的版本,

如使用 @use / @forward 语法引入 uView Pro 的样式文件,可能会导致样式丢失,报错,所以请使用 @import 引入。

2. TypeScript、Volar 与类型提示体验

uView Pro 自带 TypeScript 类型声明文件,结合 Volar 能获得良好的组件属性、事件、插槽的代码补全与类型校验。以下为推荐配置:

  1. 确保 VS Code 安装 Volar,并禁用 Vetur
  2. tsconfig.json 中添加:
{
  "compilerOptions": {
    "types": ["uview-pro/types"],
    "skipLibCheck": true
  }
}
  1. 在团队中统一 tsconfig 与 VS Code 推荐扩展配置(.vscode/extensions.json),减少“我的能提示你的不能提示”的现象。

3. 按需加载、tree-shaking 与打包优化

为减小包体积,建议:

  • 优先按需导入工具函数与业务组件(避免全局引入全部组件),
  • 使用 uni-helper 的 uni-components 或配合 Vite 的按需加载插件实现自动 tree-shaking,
  • 对大型列表使用虚拟滚动、分页或懒加载,
  • 在生产构建时开启压缩、静态资源缓存以及 CDN/边缘分发。

示例:按需引入工具函数

import { deepClone } from "uview-pro";
const copy = deepClone(obj);

4. 与其他组件库共存的注意事项

项目中若存在 uview-plusuView 1.xuView 2.x 或其他同类库,可能会出现 easycom 冲突、样式覆盖或工具命名冲突。解决建议:

  • 在迁移期避免自动扫描多个组件库的同名规则;
  • 调整 easycom.custom 规则,只指向 uview-pro 或具体库路径;
  • 团队层面统一组件库选型,减少冲突成本。

5. 常见问题与排查清单

  • 组件没有样式?→ 检查 theme.scssindex.scss 是否正确引入;
  • easycom 无效?→ 检查 pages.jsoncustom 配置与路径;
  • Volar 无补全?→ 禁用 Vetur、重启 VS Code、确认 tsconfig.json 设置;
  • Sass 语法报错?→ 检查 sasssass-loader 版本并统一锁定;
  • 依赖冲突?→ 清理 node_modules / pnpm install --frozen-lockfile 并统一依赖来源。

更多常见问题请参考社区网站,实时更新:uviewpro.cn/zh/guide/fa…

九、uView Pro(为开源而生)

uView Pro 是一款免费、开源、面向个人和企业的组件库。希望通过 uView Pro Startercreate-uni 的结合,降低团队上手成本,提高项目启动速度。

同时欢迎企业与开发者在 GitHub / Gitee 提交 PR、Issue,参与组件优化、示例补全与文档改进。

项目地址

十、结语:把时间交给业务,把基础交给 uView Pro

通过 create-uni + uView Pro + uni-helper 插件体系,你可以在极短的时间内搭建一个现代化、可维护、类型安全的 uni-app 项目。无论是单人项目、快速原型,还是企业级多团队协作,这套组合都能显著降低启动成本、提高开发效率。

所以,强烈建议你:

  • 使用 uView Pro Starter,将其作为项目起点;或者使用 create-uni 创建新项目时选择包含 uView Pro 的模板;
  • 合理使用 uni-helper 插件系统,减少重复工作;
  • 在团队内推广统一模板与依赖锁定策略;

欢迎访问与关注:

基于UniApp实现DeepSeek AI对话:流式数据传输与实时交互技术解析

作者 BumBle
2025年10月14日 17:49

在移动应用开发中,集成AI对话功能已成为提升用户体验的重要手段。本文将详细介绍如何在UniApp中实现类似DeepSeek的AI对话界面,重点讲解流式数据传输、实时交互等核心技术。

【最后附上完整源代码】

实现效果

image.png

核心技术实现

1. 流式数据传输核心

流式数据传输是实现实时AI对话的关键,我们使用微信小程序的enableChunked配置来启用分块传输:

sendChats(params, isFirstTime) {
  const requestTask = wx.request({
    url: `${empInterfaceUrl}/gateway/basics/aiDialog/sendMsg`,
    timeout: 60000,
    responseType: 'text', // 必须设置为text才能处理流式数据
    method: 'POST',
    enableChunked: true, // 关键配置:启用分块传输
    header: {
      Accept: 'text/event-stream', // 接受服务器推送事件
      'Content-Type': 'application/json',
    },
    data: params,
  })
}

2. 流式数据实时处理

通过onChunkReceived监听器实时处理服务器推送的数据块:

this.chunkListener = (res) => {
  // 将二进制数据转换为文本
  const uint8Array = new Uint8Array(res.data)
  let text = String.fromCharCode.apply(null, uint8Array)
  text = decodeURIComponent(escape(text))
  
  // 解析SSE格式数据
  const messages = text.split('data:')
  
  messages.forEach(message => {
    if (!message.trim()) return
    
    const data = JSON.parse(message)
    
    // 处理AI回复数据
    if (data.data && data.data.answer) {
      const lastChat = this.chatArr[this.chatArr.length - 1]
      
      // 分离思考过程和实际回复
      const cleanedAnswer = data.data.answer.replace(/<think>[\s\S]*?<\/think>/g, '')
      const thinkContent = data.data.answer.match(/<think>([\s\S]*?)<\/think>/g)
        ?.map(tag => tag.replace(/<\/?think>/g, ''))
        ?.join(' ')
      
      // 实时更新UI
      if (lastChat && lastChat.type === 'robot' && cleanedAnswer) {
        lastChat.content = cleanedAnswer
        this.scrollToLower() // 自动滚动到底部
      }
    }
  })
}

// 注册监听器
requestTask.onChunkReceived(this.chunkListener)

3. 双模式参数构建

支持普通对话和产品话术两种模式:

getParams(item, content) {
  let data = {
    rootShopId: this.empShopInfo.rootShop,
    shopId: this.empShopInfo.shopId
  }
  
  if (this.sessionId) data.sessionId = this.sessionId
  
  if (this.type === 'product') {
    // 产品模式参数
    data = {
      ...data,
      msgType: 'prod',
      prodMsgType: this.sessionId ? item.value : '1',
      msg: this.productInfo.itemTitle,
      prodId: this.productInfo.itemId,
    }
  } else {
    // 普通对话模式参数
    data = {
      ...data,
      msgType: 'ai',
      msg: content || this.content
    }
  }
  return data
}

4. 消息生成控制

防止重复请求和实现重新生成功能:

generate(item, index) {
  // 防止重复请求
  if (this.isListening) {
    let msg = this.sessionId ? '当前会话未结束' : '服务器繁忙,请稍后再试'
    this.$alert(msg)
    return
  }
  
  let content
  // 重新生成时从历史消息获取原始提问
  if (index !== undefined) {
    for (let i = index - 1; i >= 0; i--) {
      if (this.chatArr[i].type === 'self') {
        content = this.chatArr[i].content
        break
      }
    }
  }
  
  // 添加用户消息到对话列表
  this.chatArr.push({
    type: 'self',
    content
  })
}

5. 自动滚动机制

确保新消息始终可见:

scrollToLower() {
  this.scrollIntoView = ''
  // 异步确保滚动生效
  setTimeout(() => {
    this.scrollIntoView = 'lower'
  }, 250)
}

完整源代码

以下是完整的组件代码,包含详细注释:

<template>
  <view class="ai">
    <scroll-view class="ai-scroll"  :scroll-into-view="scrollIntoView" scroll-y scroll-with-animation>
      <view class="ai-tips flex-c-c">
        <view class="ai-tips-content">{{ type === 'product' ? '请在下面点击选择您想生成的内容' : '请在下面输入框输入您想生成的内容' }}</view>
      </view>
      <view style="padding: 0 20rpx ">
        <view class="ai-product" v-if="type === 'product'">
          <image :src="productInfo.miniMainImage || productInfo.mainImage" class="ai-product-img" mode="aspectFill" />
          <view class="ai-product-info">
            <view>{{ productInfo.itemTitle }}</view>
            <view class="ai-product-info-price">¥{{ productInfo.spePrice }}</view>
          </view>
        </view>
      </view>
      <view class="ai-chat" v-for="(item, index) in chatArr" :key="index">
        <view class="ai-chat-item self" v-if="item.type === 'self'">
          <view class="ai-chat-content">{{ item.content}}</view>
          <image class="ai-chat-avatar" :src="empUserInfo.avatarUrl || DEFAULT_AVATAR_URL"></image>
        </view>
        <view class="ai-chat-item robot" v-if="item.type === 'robot'">
          <image class="ai-chat-avatar" :src="`${mdFileBaseUrl}/stand/emp/AI/icon_avatar.png`"></image>
          <view class="ai-chat-content">
            <view class="ai-chat-content-box flex-c content-think" @click="switchExpand(item)">
              {{ item.isListening ? '正在思考中...' : '已推理' }}
              <MDIcon :name="item.expand ? 'arrowUp' : 'arrowDown'" color="#919099" left="8" />
            </view>
            <text class="ai-chat-content-box  content-think" v-if="item.expand">{{ item.think }}</text>
            <text class="ai-chat-content-box">{{ item.content }}</text>
            <view class="ai-chat-opt flex-c">
              <template v-if="item.isListening">
                <view class="ai-chat-opt-btn pause-btn flex-c-c" hover-class="h-c" @click="pauseAnswer(index)">
                  <image class="ai-chat-opt-icon" :src="`${mdFileBaseUrl}/stand/emp/AI/icon_pause.png`"></image>
                  暂停回答
                </view>
              </template>
              <template v-else>
                <view class="ai-chat-opt-btn flex-c-c" hover-class="h-c" @click="generate(item, index)">
                  <image class="ai-chat-opt-icon" :src="`${mdFileBaseUrl}/stand/emp/AI/icon_reset.png`"></image>
                  重新生成
                </view>
                <view class="ai-chat-opt-btn flex-c-c" hover-class="h-c" @click="copyAnswer(item.content)">
                  <image class="ai-chat-opt-icon" :src="`${mdFileBaseUrl}/stand/emp/AI/icon_copy.png`"></image>
                  复制回答
                </view>
              </template>
            </view>
          </view>
        </view>
      </view>
      <view id="lower" class="lower"></view>
    </scroll-view>
    <view class="ai-footer">
      <view class="ai-footer-buttons flex-c" v-if="type === 'product'">
        <view class="ai-footer-buttons-btn flex-c-c" v-for="x in footerBtnList" :key="x.value" hover-class="h-c" @click="generate(x)">
          {{ x.label }}
        </view>
      </view>
      <template v-else>
        <view class="ai-keyboard">
          <textarea class="ai-keyboard-inp" v-model="content" cursor-spacing="30" maxlength="-1" placeholder="请输入相关产品信息" @confirm="generate()"></textarea>
        </view>
        <view class="ai-send flex-c-c" hover-class="h-c" @click="generate()">
          <image class="ai-send-icon" :src="`${mdFileBaseUrl}/stand/emp/AI/icon_send.png`"></image>
          开始生成
        </view>
      </template>
    </view>
  </view>
</template>

<script>
import { empInterfaceUrl } from '@/config'

export default {
  data() {
    return {
      content: '', // 内容
      type: 'normal', // 类型:normal-普通,product-产品
      productInfo: {},
      footerBtnList: [
        { label: '首次分享话术', value: '1' },
        { label: '破冰话术', value: '2' },
        { label: '产品介绍', value: '3' },
        { label: '产品优点', value: '4' }
      ],
      requestTask: null,
      sessionId: '',
      isListening: false, // 添加状态变量
      chatArr: [],
      scrollIntoView: 'lower',
      chunkListener: null
    }
  },
  methods: {
    scrollToLower() {
      this.scrollIntoView = ''
      setTimeout(() => {
        this.scrollIntoView = 'lower'
      }, 250)
    },
    switchExpand(item) {
      item.expand = !item.expand
      this.$forceUpdate()
    },
    copyAnswer(content) {
      uni.setClipboardData({
        data: content,
        success: () => {
          uni.showToast({ title: '复制成功', icon: 'none' })
        }
      })
    },
    getParams(item, content) {
      let data = {
        rootShopId: this.empShopInfo.rootShop,
        shopId: this.empShopInfo.shopId
      }
      if (this.sessionId) data.sessionId = this.sessionId
      if (this.type === 'product') {
        data = {
          ...data,
          msgType: 'prod',
          prodMsgType: this.sessionId ? item.value : '1',
          msg: this.productInfo.itemTitle,
          prodId: this.productInfo.itemId,
        }
        // 如果是重新生成,获取上一个的提问内容的value
        if (content) {
          const footerValue = this.footerBtnList.find(x => x.label === content).value
          data.prodMsgType = footerValue
        }
      } else {
        data = {
          ...data,
          msgType: 'ai',
          msg: content || this.content // 第一次:'' , ai模式:1.this.content 2.重新生成content
        }
      }
      return data
    },
    // 开始生成
    // 第一个参数为按钮信息(product模式),第二个参数为重新生成需要的index
    generate(item, index) {
      if (this.isListening) {
        let msg = this.sessionId ? '当前会话未结束' : '服务器繁忙,请稍后再试'
        this.$alert(msg)
        return
      }
      if (this.type === 'normal' && !this.content.trim() && !index) {
        return uni.showToast({ title: '请输入相关产品信息', icon: 'none' })
      }
      let content
      // 如果是重新生成,获取上一个的提问内容
      if (index !== undefined) {
        for (let i = index - 1; i >= 0; i--) {
          if (this.chatArr[i].type === 'self') {
            content = this.chatArr[i].content
            break
          }
        }
      } else {
        content = this.type === 'product' ? item.label : this.content
      }
      this.chatArr.push({
        type: 'self',
        content
      })
      this.scrollToLower()
      const params = this.getParams(item, content)
      this.content = ''
      this.isListening = true
      this.sendChats(params)
    },

    sendChats(params, isFirstTime) {
      let chatIndex // 获取新添加的robot消息的索引
      // 取消之前的请求
      if (this.requestTask) {
        this.requestTask.abort()
        this.requestTask = null
      }
      if (!isFirstTime) {
        this.chatArr.push({
          type: 'robot',
          think:'',
          expand: false,
          content: '',
          isListening: true
        })
        chatIndex = this.chatArr.length - 1
      }
      this.scrollToLower()
      const requestTask = wx.request({
        url: `${empInterfaceUrl}/gateway/basics/aiDialog/sendMsg`,
        timeout: 60000,
        responseType: 'text',
        method: 'POST',
        enableChunked: true,
        header: {
          Accept: 'text/event-stream',
          'Content-Type': 'application/json',
          'root-shop-id': this.empShopInfo.rootShop,
          Authorization: this.$store.getters.empBaseInfo.token
        },
        data: params,
        fail: () => {
          this.isListening = false
          if (chatIndex !== undefined) {
            this.chatArr[chatIndex].isListening = false
          }
        }
      })
      // 移除之前的监听器
      if (this.chunkListener && this.requestTask) {
        this.requestTask.offChunkReceived(this.chunkListener)
      }
      // 添加新的监听器
      this.chunkListener = (res) => {
        if (!this.isListening) {
          requestTask.abort()
          return
        }
        const uint8Array = new Uint8Array(res.data)
        let text = String.fromCharCode.apply(null, uint8Array)
        text = decodeURIComponent(escape(text))
        const messages = text.split('data:')
        messages.forEach(message => {
          if (!message.trim()) {
            return
          }
          const data = JSON.parse(message)
          if (data.data === true) {
            this.pauseAnswer(chatIndex, isFirstTime)
            return
          }
          if (data.data && data.data.session_id && isFirstTime) {
            this.sessionId = data.data.session_id
            this.isListening = false
            return
          }
          if (data.data && data.data.answer) {
            const lastChat = this.chatArr[this.chatArr.length - 1]
            const cleanedAnswer = data.data.answer.replace(/<think>[\s\S]*?<\/think>/g, '')
            const thinkContent = data.data.answer.match(/<think>([\s\S]*?)<\/think>/g)?.map(tag => tag.replace(/<\/?think>/g, ''))?.join(' ')
            if (lastChat && lastChat.type === 'robot' && cleanedAnswer) {
              lastChat.content = cleanedAnswer
              this.scrollToLower()
            }
            if (thinkContent) {
              lastChat.think = thinkContent
              this.scrollToLower()
            }
          }
        })
      }
      requestTask.onChunkReceived(this.chunkListener)
      this.requestTask = requestTask
    },
    pauseAnswer(index, isFirstTime) {
      if (this.requestTask) {
        this.requestTask.abort()
        this.requestTask.offChunkReceived(this.chunkListener)
        this.requestTask = null
      }
      this.isListening = false
      if (!isFirstTime) {
        this.chatArr[index].isListening = false
      }
    },
    getAiSessionId() {
      const params = this.getParams()
      this.isListening = true
      this.sendChats(params, true)
    }
  },
  onLoad(options) {
    this.type = options.type || 'normal'
    this.$store.dispatch('checkLoginHandle').then(() => {
      if (options.type === 'product') {
        this.productInfo = uni.getStorageSync('productInfo')
        uni.removeStorageSync('subShopInfo')
      }
      this.getAiSessionId()
    })
  },
  beforeDestroy() {
    // 移除之前的监听器
    if (this.requestTask) {
      this.requestTask.abort()
      if (this.chunkListener) {
        this.requestTask.offChunkReceived(this.chunkListener)
      }
      this.requestTask = null
    }
  }
}
</script>

<style lang="scss">
page {
  background: #f5f5f5;
}
.ai {
  padding-top: 20rpx;
  &-scroll {
    height: calc(100vh - 120rpx);
    overflow: auto;
  }
  &-tips {
    &-content {
      padding: 0 8rpx;
      height: 36rpx;
      background: #eeeeee;
      font-size: 24rpx;
      color: #999999;
    }
  }
  &-product {
    padding: 20rpx;
    background: #fff;
    border-radius: 8rpx;
    margin: 24rpx 0;
    display: flex;
    &-img {
      flex-shrink: 0;
      width: 120rpx;
      height: 120rpx;
      background: #EEEEEE;
      border-radius: 4rpx 4rpx 4rpx 4rpx;
      margin-right: 16rpx;
    }
    &-info {
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      &-price {
        font-weight: 700;
        color: #FF451C;
      }
    }
  }
  &-chat {
    padding: 0 20rpx;
    &-item {
      margin-top: 40rpx;
      display: flex;
      &.self {
        .ai-chat-content {
          background: $uni-base-color;
          color: #ffffff;
          margin-right: 10rpx;
          margin-left: 0rpx;
        }
      }
    }
    &-content {
      background: #fff;
      border-radius: 14rpx;
      padding:27rpx 20rpx;
      font-size: 28rpx;
      color: #333;
      line-height: 33rpx;
      word-break: break-all;
      flex: 1;
      margin-left: 10rpx;
      .content-think {
        color: #919099;
        margin-bottom: 8rpx;
      }
    }
    &-avatar {
      width: 88rpx;
      height: 88rpx;
      border-radius: 14rpx;
    }
    &-opt {
      justify-content: flex-end;
      margin-top: 40rpx;
      border-top: 1px solid #eeeeee;
      padding-top: 20rpx;
      &-btn {
        padding: 0 16rpx;
        height: 64rpx;
        border-radius: 8rpx;
        border: 1px solid $uni-base-color;
        font-size: 24rpx;
        color: $uni-base-color;
        &:last-child {
          background: $uni-base-color;
          margin-left: 20rpx;
          color: #fff;
        }
        &.pause-btn {
          border: 2rpx solid $uni-base-color;
          color: $uni-base-color;
          background: none;
        }
      }
      &-icon {
        width: 32rpx;
        height: 32rpx;
        margin-right: 8rpx;
      }
    }
  }
  &-footer {
    min-height: 120rpx;
    position: fixed;
    bottom: 0;
    background: #fff;
    left: 0;
    right: 0;
    z-index: 1;
    padding: 20rpx;
    &-buttons {
      &-btn {
        width: 163rpx;
        height: 64rpx;
        font-size: 24rpx;
        color: #FFFFFF;
        line-height: 28rpx;
        background: $uni-base-color;
        border-radius: 8rpx 8rpx 8rpx 8rpx;
        &:not(:last-child) {
          margin-right: 20rpx;
        }
      }
    }
  }
  &-keyboard {
    background: #f5f5f5;
    border-radius: 8rpx;
    padding: 20rpx;
    &-inp {
      font-size: 28rpx;
      height: 146rpx;
      box-sizing: border-box;
      display: block;
      width: 100%;
    }
  }
  &-send {
    height: 72rpx;
    background: $uni-base-color;
    border-radius: 8rpx;
    margin-top: 18rpx;
    color: #ffffff;
    &-icon {
      width: 36rpx;
      height: 36rpx;
      margin-right: 8px;
    }
  }
  .lower {
    height: 350rpx;
    width: 750rpx;
  }
}
</style>

技术要点总结

  1. 流式传输:通过enableChunked: trueonChunkReceived实现实时数据传输
  2. SSE协议:使用Server-Sent Events协议处理服务器推送
  3. 二进制处理:正确处理Uint8Array数据流转换
  4. 状态管理:完善的请求状态控制防止重复提交
  5. 用户体验:自动滚动、思考过程展示等细节优化

这种实现方式能够提供流畅的AI对话体验,适用于各种需要实时交互的AI应用场景。

微信小程序同声传译插件深度应用:语音合成与长文本播放优化

作者 _AaronWong
2025年10月14日 07:35

之前的文章 微信小程序同声传译插件接入实战:语音识别功能完整实现指南介绍如何使用同声传译插件进行语音识别,这篇将会讲述同声传译的另一个功能语音合成。

功能概述

微信小程序同声传译插件的语音合成(TTS)功能能将文字内容转换为语音播放,适用于内容朗读、语音提醒、无障碍阅读等场景。

核心实现架构

状态管理

const textToSpeechContent = ref("")
const textToSpeechStatus = ref(0)  // 0 未播放 1 合成中 2 正在播放

核心功能实现

语音合成主函数

function onTextToSpeech(text = "") {
  // 如果正在播放,先停止
  if(textToSpeechStatus.value > 0) {
    uni.$emit("STOP_INNER_AUDIO_CONTEXT")
  }
  
  textToSpeechStatus.value = 1
  uni.showLoading({
    title: "语音合成中...",
    mask: true,
  })
  
  // 处理文本内容
  if(text.length) {
    textToSpeechContent.value = text
  }
  
  // 分段处理长文本(微信限制每次最多200字)
  let content = textToSpeechContent.value.slice(0, 200)
  textToSpeechContent.value = textToSpeechContent.value.slice(200)
  
  if(!content) {
    uni.hideLoading()
    return
  }
  
  // 调用合成接口
  plugin.textToSpeech({
    lang: "zh_CN",
    tts: true,
    content: content,
    success: (res) => {
      handleSpeechSuccess(res)
    },
    fail: (res) => {
      handleSpeechFail(res)
    }
  })
}

合成成功处理

function handleSpeechSuccess(res) {
  uni.hideLoading()
  
  // 创建音频上下文
  innerAudioContext = uni.createInnerAudioContext()
  innerAudioContext.src = res.filename
  innerAudioContext.play()
  textToSpeechStatus.value = 2
  
  // 播放结束自动播下一段
  innerAudioContext.onEnded(() => {
    innerAudioContext = null
    textToSpeechStatus.value = 0
    onTextToSpeech() // 递归播放剩余内容
  })
  
  setupAudioControl()
}

音频控制管理

function setupAudioControl() {
  uni.$off("STOP_INNER_AUDIO_CONTEXT")
  uni.$on("STOP_INNER_AUDIO_CONTEXT", (pause) => {
    textToSpeechStatus.value = 0
    if(pause) {
      innerAudioContext?.pause()
    } else {
      innerAudioContext?.stop()
      innerAudioContext = null
      textToSpeechContent.value = ""
    }
  })
}

错误处理

function handleSpeechFail(res) {
  textToSpeechStatus.value = 0
  uni.hideLoading()
  toast("不支持合成的文字")
  console.log("fail tts", res)
}

关键技术点

1. 长文本分段处理

由于微信接口限制,单次合成最多200字,需要实现自动分段:

let content = textToSpeechContent.value.slice(0, 200)
textToSpeechContent.value = textToSpeechContent.value.slice(200)

2. 播放状态管理

通过状态值精确控制播放流程:

  • 0:未播放,可以开始新的合成
  • 1:合成中,显示loading状态
  • 2:播放中,可以暂停或停止

3. 自动连续播放

利用递归实现长文本的自动连续播放:

innerAudioContext.onEnded(() => {
  onTextToSpeech() // 播放结束继续合成下一段
})

完整代码

export function useTextToSpeech() {
  const plugin = requirePlugin('WechatSI')
  let innerAudioContext = null
  const textToSpeechContent = ref("")
  const textToSpeechStatus = ref(0)
  
  function onTextToSpeech(text = "") {
    if(textToSpeechStatus.value > 0) {
      uni.$emit("STOP_INNER_AUDIO_CONTEXT")
    }
    textToSpeechStatus.value = 1
    uni.showLoading({
      title: "语音合成中...",
      mask: true,
    })
    
    if(text.length) {
      textToSpeechContent.value = text
    }
    
    let content = textToSpeechContent.value.slice(0, 200)
    textToSpeechContent.value = textToSpeechContent.value.slice(200)
    
    if(!content) {
      uni.hideLoading()
      return
    }
    
    plugin.textToSpeech({
      lang: "zh_CN",
      tts: true,
      content: content,
      success: (res) => {
        uni.hideLoading()
        innerAudioContext = uni.createInnerAudioContext()
        innerAudioContext.src = res.filename
        innerAudioContext.play()
        textToSpeechStatus.value = 2
        
        innerAudioContext.onEnded(() => {
          innerAudioContext = null
          textToSpeechStatus.value = 0
          onTextToSpeech()
        })
        
        uni.$off("STOP_INNER_AUDIO_CONTEXT")
        uni.$on("STOP_INNER_AUDIO_CONTEXT", (pause) => {
          textToSpeechStatus.value = 0
          if(pause) {
            innerAudioContext?.pause()
          } else {
            innerAudioContext?.stop()
            innerAudioContext = null
            textToSpeechContent.value = ""
          }
        })
      },
      fail: (res) => {
        textToSpeechStatus.value = 0
        uni.hideLoading()
        toast("不支持合成的文字")
        console.log("fail tts", res)
      }
    })
  }
  
  return {
    onTextToSpeech,
    textToSpeechContent,
    textToSpeechStatus
  }
}

【uniapp】体验优化:开源工具集 uni-toolkit 发布

2025年10月13日 20:08

背景

最近在做一些 uniapp 小程序 相关的 体积优化功能补充 工作,写了几个插件觉得太分散,不好梳理和归类,于是就创建一个 github 组织 来整理我的一些工具和插件,一方面方便我的日常工作,另一方面可以搜集来自社区的想法或者建议,可以首先考虑加到 uniapp 官方仓库 中,不方便加的再通过插件等形式实现。

插件列表

目前该仓库下已经有了三个插件,如下所示

功能

名称 描述 地址
@uni_toolkit/vite-plugin-component-config 一个用于处理 Vue 文件中的 <component-config> 标签的 vite插件,将配置提取并合并到对应的 JSON 文件 中,弥补组件无法自定义 JSON 配置 的缺陷 vite-plugin-component-config
@uni_toolkit/webpack-plugin-component-config 一个用于处理 Vue 文件中的 <component-config> 标签的 webpack插件,将配置提取并合并到对应的 小程序 JSON 文件 中,弥补组件无法自定义 JSON 配置 的缺陷 webpack-plugin-component-config

性能

名称 描述 地址
@uni_toolkit/unplugin-compress-json 一个用于压缩 JSON 文件的 unplugin 插件,支持 Vite 和 Webpack。自动压缩 JSON 文件 ,减小文件体积。 unplugin-compress-json

结语

如果这个库的插件帮助到了你,可以点个 star✨ 鼓励一下。

如果你有什么好的想法或者建议,欢迎在 github.com/uni-toolkit… 提 issue 或者 pr

Uniapp UTS插件开发实战:引入第三方SDK

作者 NoneSL
2025年10月13日 17:46

创建uts 插件

  1. 创建uni_modules目录。

image.png

  1. 新建uts插件

image.png

image.png

  1. 放置第三方依赖在 libs中,如图所示

image.png

引入第三方SDK

  1. 引入格式; import 类名 from 包;

    如:import UHFRManager from "com.handheld.uhfr.UHFRManager";

  2. 使用

// app-android/index.uts
import UHFRManager from "com.handheld.uhfr.UHFRManager";

export const myApi : MyApi = function (options : MyApiOptions) {
    console.log(options.paramA)
    const instance = UHFRManager.getInstance()
    options.success?.(instance);
    options.complete?.(res);
}
// pages/test/test.vue
<template>
<view>
    <wd-button @click="testClick"> test</wd-button>
</view>
</template>

<script setup lang="ts">
// @ts-ignore
import {myApi} from "@/uni_modules/test-uts"

function testClick(){
  myApi({
    paramA: false,
    success:(res:any)=> {
      console.log(res);
    }
  })
}
</script>
  1. 打自定义调试基座

image.png 6. uniapp使用

插件使用示例

调试

事件管理

1、uts中

广播事件

  public static registerKeyReceiver() {
    let context = UTSAndroid.getAppContext(); // 获取 Application 上下文
    let intentFilter = new IntentFilter();
    intentFilter.addAction("android.rfid.FUN_KEY");
    intentFilter.addAction("android.intent.action.FUN_KEY");
    if (this.keyReceiver == null) {
      this.keyReceiver = new KeyReceiver();
    }
    context!!.registerReceiver(this.keyReceiver, intentFilter); // 用 Application Context 进行注册
    console.log("已注册按键广播接收器3");
  }

注销广播事件

  public static unregisterKeyReceiver() {
    let context = UTSAndroid.getAppContext();
    context!!.unregisterReceiver(this.keyReceiver);
    console.log("已取消注册按键广播接收器1");
  }

registerReceiver方法需要传入BroadcastReceiver类,新建一个类并重写onReceive方法

class KeyReceiver extends BroadcastReceiver {
  constructor() {
    super();
  }

  override  onReceive(context : Context, intent : Intent) : void {
    let keyCode = intent.getIntExtra("keyCode", 0);
    console.log("keyCode = " + keyCode);

    if (keyCode === 0) {
      keyCode = intent.getIntExtra("keycode", 0);
    }

    let keyDown = intent.getBooleanExtra("keydown", false);

    if (keyDown) {
      // 按键按下时的处理逻辑(可自行添加)
    } else {
      // 按键松开时的处理逻辑
      switch (keyCode) {
        case KeyEvent.KEYCODE_F1:
          break;
        case KeyEvent.KEYCODE_F2:
          break;
        case KeyEvent.KEYCODE_F5:
          break;
        case KeyEvent.KEYCODE_F3: // C510x
        case KeyEvent.KEYCODE_F4: // 6100
        case KeyEvent.KEYCODE_F7: // H3100
          this.onReadTag();
          break;
      }
    }
  }
  //这里是事务处理
  private onReadTag():void{
    console.log("按键触发读取标签操作");
    try{
     rfidHelper.startScan().then(res=>{

       let intent = new Intent("com.example.RFID_SCAN");
       intent.putExtra("scannerdata", res.join(','));
       console.log("发送数据",res);
       console.log("--------------")
         let context = UTSAndroid.getAppContext();
       context!!.sendBroadcast(intent);
     });
    }catch(e){
      console.log("rfid读取失败");
    }
  }
}

2、uniapp 中

接收广播 并使用eventbus转发消息给订阅者

import { EventBus, EventKey } from './EventBus'

let main: any
let filter: any
let receiver: PlusAndroidInstanceObject
let tag: boolean = false
/**
 * 开始广播监听扫码
 * that:传this;
 */
function start() {
  /* #ifdef APP-PLUS */
  main.registerReceiver(receiver, filter)
  /* #endif */
}

/**
 * 停止广播监听扫码
 * that:传this;
 */
function stop() {
  /* #ifdef APP-PLUS */
  main.unregisterReceiver(receiver)
  /* #endif */
}

/**
 *  剩余下个变量已经做了全局变量
 *
 * 定义广播
 * that:传this;
 */
function init(onReceive?: any) {
  /* #ifdef APP-PLUS */
  // 获取activity
  main = plus.android.runtimeMainActivity()
  const IntentFilter = plus.android.importClass('android.content.IntentFilter') as any
  // console.log(IntentFilter)
  filter = new IntentFilter()
  // console.log(filter)
  // 扫描设置的广播名称
  filter.addAction('com.android.server.scannerservice.broadcast') // 测试用
  filter.addAction('com.example.RFID_SCAN')// rfid uts插件自定义
  filter.addAction('com.rfid.SCAN')// 扫码

  receiver = plus.android.implements('io.dcloud.feature.internal.reflect.BroadcastReceiver', {

    onReceive(context: any, intent: any) {
      console.log('广播接收')
      // console.log(context, intent)
      plus.android.importClass(intent)
      // 扫描设置的开发者选项--键值名称 scannerdata
      const code = intent.getStringExtra('scannerdata')

      //获取广播源
      const action = intent.getAction();
      switch(action){
        case 'com.android.server.scannerservice.broadcast':
          EventBus.emit(EventKey.TEST, code)
          break;
        case 'com.example.RFID_SCAN':
          EventBus.emit(EventKey.RFIDEvent, code)
          break;
        case 'com.rfid.SCAN':
          EventBus.emit(EventKey.ScanEvent, code)
          break;
      }

      // 业务
      // 防重复
      if (tag) {
        return
      }
      tag = true
      setTimeout(() => {
        tag = false
      }, 150)
      console.log(code)
      // 到这里扫描成功了,可以调用自己的业务逻辑,code就是扫描的结果    return出code进行业务处理
      if (onReceive) {
        onReceive(code)
      }
    },
  })
  /* #endif */
}

function test() {
  const Intent = plus.android.importClass('android.content.Intent') as any
  const intent = new Intent('com.android.server.scannerservice.broadcast')
  main = plus.android.runtimeMainActivity()
  intent.putExtra('scannerdata', '123456789')
  main.sendBroadcast(intent)
}
export const broadcastScan = {
  init,
  start,
  stop,
  test,
}

分享一个4.3(a)瓜,一个操作毁了公司3个月的成果。

作者 iOS研究院
2025年10月10日 09:02

背景

对Appstore来说,4.3(a)是最让人头疼的事情。

有些是故意而为之,为了快速抢占市场、做备用包或者内部赛马等等

有些是无奈之举,莫名其妙的被3.2f,又想恢复产品的权重,重新夺回阵地

简单来说:

4.3(a)并非一蹴而就,是一个综合判定后的结论! 俗称:叠buffer!

事情梗概

今天跟往常一样,有粉丝添加好友询问关于4.3的问题。简单沟通之后,分析了大概可能得原因。

略提一下,基本上备注了有偿的粉丝都会多聊几句。你懂付费的价值,我懂你的不易!

从元数据到代码,把可能踩雷的地方维度均考量了一圈。最后基本可以锁定为代码层面!

事情详述

领导给员工B,提了一个节前上架的版本。站在员工的角度来看,任务重时间紧,但是都是打工的思维。肯定是想怎么简单的解决问题,怎么来

在这期间还有一个小插曲,该员工B想提前调休。在确保工作不影响的情况下。但是领导因必须提供稳定版本为由,直接拒绝了,并且还吐槽其没有团队意识等等破其道心的话。(其实身份不一样,角度不一样,责任也不一样,都是打工人当面吐槽属实过分了。

该员工B为了节前调休,采用了合并代码的操作。为了快速符合测试预期。

好消息:提前完成了!

坏消息:4.3了!

事情后续

该员工B,在节后主动辞职。顺利混完国庆带薪假期。对公司而言亏损修炼了3个多月成果,导致现有版本沦为鸡肋。

食之无味,弃之可惜!

所以,都是为了讨生活没有必要苦苦相逼。领导者该有的气度要有,毕竟雇佣的员工和合伙人心境是不一样。除非钱到位了,用了钞能力!

老板以为的是合伙创业,在员工看来不过是个饭票,没了再找罢了。

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

相关推荐

# 苹果开发者续费大坑及成功续费方案!亲测有效

# AppStore敏感词排查手册,多维度分析Guideline 2.3.1隐藏功能,轻松过审。

# Pingpong和连连的平替,让AppStore收款无需新增持有人。

# 苹果加急审核是“绿色通道”还是“死亡陷阱”?

# 苹果开发者邮箱,突然收到11.2通知严重么?

# 不想被苹果卡审最好错开这两个提审时间

# 手撕苹果审核4.3是代码问题还是设计问题?

# 有幸和Appstore审核人员进行了一场视频会议特此记录。

知识星球

更多Appstore咨询问题,请关注知识星球。「提供1v1上架指导,帮助开发者解决Appstore的疑难杂症,助力每一位开发者!」

【uniapp】小程序体积优化,JSON文件压缩

2025年10月9日 17:05

背景

2025年9月30号下午,uniapp社区 有开发者发布了一个帖子 ask.dcloud.net.cn/question/21… ,希望能支持压缩小程序编译后的 JSON文件 以缓解包体积越来越大的问题,于是这个插件 github.com/chouchouji/… 便由此而生。

功能特性

  • 🗜️ 自动压缩 - 自动移除 JSON 文件中的空白字符和换行符
  • 🔧 多构建工具支持 - 支持 Vite、Webpack、Rollup 等构建工具
  • 零配置 - 开箱即用,无需额外配置
  • 🎯 精确匹配 - 只处理 .json 文件,不影响其他资源

下面是一张测试效果图,6.21KB -> 4.54KB,越大的 JSON文件 插件效果越明显。

cjs.png

安装

# npm
npm install @binbinji/unplugin-compress-json -D

# yarn
yarn add @binbinji/unplugin-compress-json -D

# pnpm
pnpm add @binbinji/unplugin-compress-json -D

使用方法

Vite

// vite.config.js
import { defineConfig } from 'vite'
import CompressJson from '@binbinji/unplugin-compress-json/vite'
import uni from '@dcloudio/vite-plugin-uni'

export default defineConfig({
  plugins: [
    uni(),
    CompressJson(),
  ],
})

Vue CLI

// vue.config.js
const CompressJson = require('@binbinji/unplugin-compress-json/webpack')

module.exports = {
  configureWebpack: {
    plugins: [
      CompressJson(),
    ],
  },
}

工作原理

插件会在构建过程中自动检测所有 .json 文件,并移除其中的:

  • 空格
  • 制表符
  • 换行符
  • 其他空白字符

压缩前:

{
  "name": "example",
  "version": "1.0.0",
  "description": "这是一个示例"
}

压缩后:

{"name":"example","version":"1.0.0","description":"这是一个示例"}

国庆假期 iOS 开发者守好邮箱 “防线”,严防恶意投诉避免产品下架

作者 iOS研究院
2025年9月30日 14:24

背景

国庆长假临近,当大家准备暂别工作享受假期时,务必提前绷紧 “产品防护” 这根弦 —— 历年假期都是恶意投诉的高发期,而开发者邮箱作为苹果审核沟通的唯一官方渠道,一旦疏忽错过关键通知,极可能导致辛苦打造的产品遭遇下架危机。

恶意行为

为何假期风险陡增?

不少竞品会利用团队休假、响应延迟的 “时间差”,通过多种不正当手段发起攻击:或是伪造 “侵权”“违规收集信息” 等虚假举报,或是组织恶意刷评、批量提交不实用户投诉,甚至故意曲解产品功能触发审核红线。苹果审核机制对投诉响应时效要求严苛,若 24 小时内未查看审核邮件、未提交澄清材料,系统可能直接判定 “默认违规”,导致产品临时下架、核心功能封禁;更严重的是,多次未及时响应还会影响账号历史信誉,后续恢复上架不仅需要反复沟通举证,流程长达 1-2 周,期间错过假期流量窗口事小,用户因无法访问流失、营收断崖式下跌才是致命打击。

防御措施

为守住产品成果,建议大家提前做好两手方面准备:

一是给开发者邮箱设置短信、APP 双重推送提醒,确保 “审核通知”“投诉预警” 类邮件实时触达,避免被淹没在垃圾邮件中;

二是团队内明确 “审核轮值机制”,每天安排 1 名成员固定花 15 分钟查看邮箱,同步审核动态到工作群,遇到紧急投诉第一时间启动响应流程。

每款产品从立项到上架,都凝聚着团队无数个日夜的心血,假期放松的同时,千万别让 “一时疏忽” 给恶意竞争可乘之机。愿大家既能安心享受假期,也能守住产品成果

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

相关推荐

# 苹果开发者续费大坑及成功续费方案!亲测有效

# Pingpong和连连的平替,让AppStore收款无需新增持有人。

# 苹果加急审核是“绿色通道”还是“死亡陷阱”?

# 苹果开发者邮箱,突然收到11.2通知严重么?

# 不想被苹果卡审最好错开这两个提审时间

# 手撕苹果审核4.3是代码问题还是设计问题?

# 有幸和Appstore审核人员进行了一场视频会议特此记录。

知识星球

更多Appstore咨询问题,请关注知识星球。「提供1v1上架指导,帮助开发者解决Appstore的疑难杂症,助力每一位开发者!」

永远不要站在用户的对立面,挑战大众的公知。

作者 iOS研究院
2025年9月16日 13:37

背景

最近闹得沸沸扬扬的西贝事件,诠释所谓的规则和大众公知的博弈。

从罗永浩质疑 “几乎全是预制菜还卖高价”,到开放后厨时被发现大量预包装食材仅需简单加热,西贝的 “非预制菜” 说辞显然与大众认知相悖。

西贝事件也诞生了众多名梗:

  • 名梗一: 一岁的宝宝吃两岁的西兰花。
  • 名梗二: 🐑:我为什么还不能转世? 😈:因为你还有1条腿在西贝。
  • 名梗三:为了不吃家里的剩饭,所以去了西贝吃了一年前的预制菜。

image.png

所以任何时候都永远不要站在用户的对立面,任何一个品牌站在用户的对立面都没有什么好下场。

简单来说:

作为一个掌门人任何时候,都要更加冷静。

如果不是贾老板的自爆,有专业的公关团队处理,那么事件不会闹得如此严重,舆情也不至于久高不下。

如果说规则是 “硬约束”,那么尊重用户就是产品的 “软实力”,是穿越周期的核心竞争力。

不管在那种情况下,都要习惯性让子弹飞一会。冷静之后再做决定,可以解决很多不必要的麻烦。

同时,也奉劝各位一言堂的老板谨言慎行,因为大多数情况下,老板的认知大概率就决定了公司认知的天花板

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

相关推荐

# 苹果开发者续费大坑及成功续费方案!亲测有效

# Pingpong和连连的平替,让AppStore收款无需新增持有人。

# 苹果加急审核是“绿色通道”还是“死亡陷阱”?

# 苹果开发者邮箱,突然收到11.2通知严重么?

# 不想被苹果卡审最好错开这两个提审时间

# 手撕苹果审核4.3是代码问题还是设计问题?

# 有幸和Appstore审核人员进行了一场视频会议特此记录。

知识星球

更多Appstore咨询问题,请关注知识星球。「提供1v1上架指导,帮助开发者解决Appstore的疑难杂症,助力每一位开发者!」

❌
❌