更新记录

0.5.1(2025-03-27)

  1. 同步bsx-recorder-api 版本 0.5.5 支持发送wav格式(一般语音聊天不建议aac)
  2. 修复小问题

0.5.0(2024-12-31)

首发版本 v 0.5.0


平台兼容性

uni-app x

Chrome Safari Android iOS 鸿蒙 微信小程序
- - 5.0 × × ×

其他

多语言 暗黑模式 宽屏模式 蒸汽模式
× × × √

bsx-voicechat-wx 一款类微信发语音的页面组件,适用于移动端聊天发语音

重要

  • 此组件基于bsx-recorder-api插件开发,使用前需先购买插件.bsx-recorder-api
  • 暂仅支持Android 端
  • 因使用bsx-recorder-api, 故需要打自定义基座
  • 除录音和播放操作外,本组件使用uts相关api的能力,放心使用.

开发文档

UTS 页面组件

作者

aoaobaba 傲傲爸爸~

介绍

基于bsx-recorder-api 开发的类微信发语音界面。

功能

  • [X] 长按唤起语音界面说话录音
  • [X] 滑动到响应的位置指定操作
  • [X] 取消语音
  • [X] 播放已录制语音
  • [X] 松手发送语音
  • [X] 手动发送语音
  • [X] 短时间录音无效

说明

本组件插件依赖bsx-recorder-api, 使用前请先购买授权并导入项目。 组件不能脱离bsx-recorder-api 单独使用!!

布局:以下是组件在项目页面中的位置示意。可以看到,组件需要放置在一个dialogPage里; dialogPage用于遮罩整个app,起到遮罩层的功能。 图示

事件:app的page里面需要设置用户的touch操作相关的事件,并触发组件专有的事件;dialogPage里面监听事件后再调用组件暴露的专有事件,组件在接收到处理请求时 开始处理相应的业务逻辑。例如,用户touchmove事件时,组件会根据手指的移动位置,分别响应处理【取消】或【更多操作】;当组件处理了事件后,dialogPage里可以 callback再进行处理。例如,在组件中提供了@closeCallback,用于关闭时在dialogPage内提供更多操作,关闭时会返回录音文件全路径

图示

注意!!本组件没有提供dialogPage, 但是依赖一个承载的父页面,你可以把组件直接放在Page里,但那样效果很糟糕。 下面示例会提供作者项目内的dialogPage相关代码,以供参考!

类型

type bsxVoiceChatWxComUiStyle = {
    visualizerViewBackgroundColor?: string  // 可视化视图背景色
    visualizerBaseColor?: string //可视化颜色
}
type bsxVoiceChatWxComFuntions = {
    enableAudioVisualizer?: boolean | null  //是否启用音频实时 可视化
    enableSilenceHandle?: boolean | null   // 是否启用静音处理
    maxDuration?: number | null  // 最大录音时长,默认最大30分钟 超过自动停止
    silenceThreshold?: number | null  // 静音检测阈值 
    silenceThresholdMs?: number | null  // 静音检测持续时长阈值
    fileDirectoryName? : string | null  //文件存储目录 如果空 则创建在BsxRecorder 目录下
    fileNameSuffix? : string | null  // 录音aac文件名前缀 完整的文件名为前缀_日期时间  默认bsx_recorder
    fileType: string // 录音文件类型 支持 aac和wav  未指定或未匹配的默认aac
}
组件参数
属性 类型 默认值 功能
pageInstance UniPage 必须传 组件的父页面实例
uiStyle bsxVoiceChatWxComUiStyle 参考类型 组件UI
comFunctions bsxVoiceChatWxComFuntions 参考类型 组件功能性属性
@closeCallback callback: () => UTSJSONObject 可选 组件关闭时的回调里面包含了3种action值,分别为cancel,send,tooShort
@onStartRecord .. 可选 开始录音回调
@onRecording .. 可选 录音中回调
@onFailRecord .. 可选 录音失败回调
@onStopRecord .. 可选 录音停止回调
@onStartPlay .. 可选 开始播放回调
@onStopPlay .. 可选 停止播放回调
@onEndPlay .. 可选 播放完回调
组件方法

通过组件的Ref 引用,可以调用!

方法名 参数 功能 调用示例
show 无 组件展现 Ref.value?.show?.()
close 无 关闭组件
actionsAnimatingByTouchMoveEvent (screenX, screenY) 处理touchmove事件
actionsConsumeTouchEndEvent (screenX, screenY) 处理touchend事件 例如松开发送,或指定区域的操作
actionsConsumeTouchCancelEvent 处理意外中断touch事件 例如系统行为中断了用户的touch事件时触发

使用

  1. 新建一个页面并注册,作为dialogPage使用,用做遮罩层
  2. 在dialogPage里面放置bsx-voicechat-wx组件,并设置相关属性或回调处理
  3. 在业务页面响应用户操作并打开dialogPage显示组件,业务页面需要触发touch相关的组件专用事件!
步骤一、二 新增的页面并注册到page.json 用于dialogPage
<template>
    <!-- !!遮罩层 当前页是一个dialogpage 页面 用于展现BsxRecorderView 组件时 的应用界面遮罩
    * https://doc.dcloud.net.cn/uni-app-x/api/dialog-page.html
    * 1. 初始化时 给了全屏的宽高
    * 2. 设置了背景色 background-color: rgba(0, 0, 0, .82)
    * 3. 其父页面为调用打开当前dialog的页面
    * 4. bsx-voicechat-wx 组件放置在此dialog page 内
    * 5. 当使用类wechat的触摸操作语音的模式时,注意!!!!父组件的touchmove和touchend 事件触发是必要的!因为组件需要响应触摸处理!
    * 6. 与父页面的通信 使用事件总线!如果添加了事件触发和订阅,请在适当时机off掉!
    *  
    * 更多信息请参阅组件插件的开发文档
    -->
    <view id="bsx-recorder-demo"  class="" style="background-color: rgba(72, 72, 72, .96);" :style="`width:${screenWidht};height:${screenHight}`">
        <!-- bsx 仿微信语音聊天组件 -->
        <bsx-voicechat-wx ref="bsxVoicechatWxRef" :pageInstance="dialogPage"  :uiStyle ="recorderUi" :comFunctions="recorderFun" @closeCallback="closePage"
            @onStartRecord="onStartRecord" @onStopRecord = "onStopRecord"
        ></bsx-voicechat-wx>
    </view>
</template>

<script setup lang="uts">
    import {bsxVoiceChatWxComUiStyle, bsxVoiceChatWxComFuntions } from '@/uni_modules/bsx-voicechat-wx/components/bsx-voicechat-wx/bsxVoiceChatWxType.uts'

    const recorderUi = ref<bsxVoiceChatWxComUiStyle>({
        visualizerViewBackgroundColor: '#95ec6a',  // #95ec6a wechat可视化语音背景色
        visualizerBaseColor: '#575D57'    // 音效柱状图颜色 支持安卓Color支持的色值: 参考   转换失败将使用默认值
        // https://developer.android.google.cn/guide/topics/resources/more-resources?hl=zh-cn#Color 参见官方Color 文档
    })

    const recorderFun = ref<bsxVoiceChatWxComFuntions>({
        enableAudioVisualizer: true,  // 是否启用实时音频可视化
        enableSilenceHandle: false, // 是否开启静音检测
        maxDuration: 30 * 60 * 1000,  // 最大录音时间 超过则自动停止录音
        silenceThreshold: 80,  // 静音检测阈值 低于此值时 silenceThresholdMs 将会作用,例如静音超过silenceThresholdMs时长 则自动停止录音!
        silenceThresholdMs: 2 * 1000 , // 静音持续时长阈值
        fileType: 'wav' // 文件类型 aac 或 wav
    })

    // 应用视图模板  可以从父页面调用当前dialogpage时通过url参数传值以切换不同模式 base or wechat
    const applyMode = 'base'

    // bsx-recorder-view 组件引用
    const bsxVoicechatWxRef = ref<BsxVoicechatWxComponentPublicInstance | null>(null)
    const screenHight = ref(0)
    const screenWidht = ref(0)

    // 当前的dialog page 实例
    const dialogPage = ref<UniPage | null>(null)

    onLoad(() => {
        console.log('设备信息', uni.getWindowInfo())
        let wi = uni.getWindowInfo()
        screenHight.value = wi.screenHeight -45
        screenWidht.value = wi.screenWidth
    })

    // 进行一些必要实例获取
    onReady(() => {
        const currentInstance = getCurrentInstance()
        dialogPage.value = currentInstance?.proxy?.$page
        console.log("dialog page 的主页面实例", dialogPage.value)
        // 打开录音机组件 这一定要稍后等待获取 dialogPage 实例完成
        setTimeout(() => {
            bsxVoicechatWxRef.value?.show?.()
        }, 300)

        // 订阅父页面的相关touchmove 事件
        uni.$on('bsxRecorderViewParentPageTouchMove', (screenX:number, screenY: number) => {
            // 组件内部会响应touch move 的changed X Y值进行相应的功能处理
            bsxVoicechatWxRef.value?.actionsAnimatingByTouchMoveEvent?.(screenX, screenY)
        })

        // 订阅父页面的touchend 事件
        uni.$on('bsxRecorderViewParentPageTouchEnd', (screenX:number, screenY: number) => {
            // 组件内部会响应touch emd 的changed X Y值进行相应的功能处理 wechat 模式时 取消发送语音 或 更多操作
            bsxVoicechatWxRef.value?.actionsConsumeTouchEndEvent?.(screenX, screenY)
        })

        // 订阅父页面的touchcancel 事件
        uni.$on('bsxRecorderViewParentPageTouchCancel', () => {
            bsxVoicechatWxRef.value?.actionsConsumeTouchCancelEvent?.()
        })
    })

    // 关闭dialog page
    const closePage = (json:UTSJSONObject) => {

        // 触发关闭的action
        console.log(`关闭事件页面:`, json)
        // 给录音机组件一个 动画处理的时间
        setTimeout(() => {
            uni.closeDialogPage({
                success: () => {
                    console.log("dialogPage 已关闭")
                },
                fail: (err) => {
                    console.log("dialogPage 关闭错误", err)
                },
                complete: (res) => {
                    console.log("dialogPage 关闭完成", res)
                }
            })
        }, 300) 
    }

    const testClick = () => {
        bsxVoicechatWxRef.value?.show?.()
    }

    const touchStart = (e: UniEvent) => {
        // console.log("触发了touchstart 事件", e)
    }

    const touchMove = (event: UniTouchEvent) => {
        console.log("触发了touchMove 事件", event.changedTouches[0])
        return true
    }

    const touchEnd = () => {
        console.log("结束触摸")
    }

    const touchCancel = () => {
        console.log("取消触摸")
    }

    const onStartRecord= () => {
        console.log('page页面接收到回调,开始录音了---------------')
    }

    const onStopRecord= (stop:UTSJSONObject) => {
        console.log('page页面接收到回调,录音停止了---------------', stop)
        // 手动停止的 不要关闭dialogpage
        if (stop['reason'] != 'manual')
        closePage({
            action: 'auto',
            filePath: stop['filePath'],
            reason: stop['reason']
        })
    }

</script>

<style>

</style>
步骤三 业务页面的处理
<template>
    <!-- #ifdef APP -->
    <scroll-view style="flex:1">
    <!-- #endif -->
        <view class="chat-box">
            <view class="" v-for="i in 3">
                <view class="bsx-chat" style="margin-top: 10;">
                    <view class="" style="margin-left: 10;border-radius: 30;height: 45;width: 45;">
                        <image src="/static/BSX_mini.png" mode="" style=" height: 45;width: 45;"></image>
                    </view>
                    <view class=""
                        style="margin-top: 15; height: 30; margin-left: 10;padding: 3;border: 1px solid gainsboro;border-radius: 20;">
                        <text style="padding: 5;font-size: 15;">BSX 专注uniapp X的Bootstrap框架。</text>
                    </view>

                </view>
                <view class="me-chat">
                    <view class=""
                        style="margin-top: 15; height:60; margin-right: 10;padding: 3;border: 1px solid gainsboro;border-radius: 20;">
                        <text style="padding: 5;font-size: 15;width: 160;">清风徐来,水波不兴;
                            好好学习,天天向上。</text>
                    </view>
                    <view class="" style="margin-right: 10;border-radius: 30;height: 45;width: 45;">
                        <image src="/static/BSX_mini.png" mode="" style=" height: 45;width: 45;"></image>
                    </view>
                </view>
            </view>

        </view>
        <view @longpress="longPress" @touchstart="touchStart" @touchcancel="touchCancel" @touchmove="touchMove" @touchend="touchEnd" class=""
            style="width: 100%;display: flex;justify-content: center; position: fixed;height: 45;bottom: 0; background-color: ghostwhite;">
            <text style="text-align: center; font-size: 16;">长按说话</text>
        </view>
    <!-- #ifdef APP -->
    </scroll-view>
    <!-- #endif -->

</template>

<script setup>
    import Manifest from 'android.Manifest'
    import ActivityCompat from 'androidx.core.app.ActivityCompat'
    import ContextCompat from 'androidx.core.content.ContextCompat'
    import PackageManager from 'android.content.pm.PackageManager'

    const touchLongTimer = ref(0)
    const touching = ref(false)
    const touchingTime = ref(0)

    onReady(() =>{
        // 申请麦克风权限
        // 检查并申请麦克风权限
        // if (ContextCompat.checkSelfPermission(UTSAndroid.getUniActivity()!, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
            // true // 权限已授予
            // 申请权限
            ActivityCompat.requestPermissions(UTSAndroid.getUniActivity()!, arrayOf(Manifest.permission.RECORD_AUDIO), (100).toInt())
        // }
    })

    onMounted(() => {

    })

    const openRecorder = () => {
        const pages = getCurrentPages()
        // 获取当前页面
        const page = pages[pages.length - 1]
        uni.openDialogPage({
            url: '/pages/bsx-recorder-wechat/bsx-recorder-wechat',
            animationType: 'fade-in',
            success: () => {
                console.log('dialog open-------------------')
            },
            fail: (res) => {
                console.log('dialog open fail--------------', res)
            },
            animationDuration: 300,
            parentPage: page
        })
    }

    const touchCancel = (event : UniTouchEvent) => {
        touching.value = false
        touchingTime.value = 0

        console.log("取消 touch")
        // 需要发送触摸事件被系统中止的事件给到组件 处理
        uni.$emit('bsxRecorderViewParentPageTouchCancel')
    }   

    const longPress = (event : UniTouchEvent) => {
        console.log("longPress", event)
        openRecorder()
    }

    const touchMove = (event : UniTouchEvent) => {
        // 触发touchmove 监听
        uni.$emit('bsxRecorderViewParentPageTouchMove', event.changedTouches[0].screenX, event.changedTouches[0].screenY)
    }

    const touchStart = () => {
        console.log('父页面 touch start ---------------------')
    }

    const touchEnd = (event : UniTouchEvent) => {
        console.log('父页面 touch end ---------------------')
        uni.$emit('bsxRecorderViewParentPageTouchEnd',  event.changedTouches[0].screenX, event.changedTouches[0].screenY)
    }

    onUnmounted(() => {
        uni.$off('bsxRecorderViewParentPageTouchMove')
        uni.$off('bsxRecorderViewParentPageTouchEnd')
    })
</script>

<style>

    .chat-box {
        display: flex;
        flex-direction: column;

    }

    .bsx-chat {
        display: flex;
        flex-direction: row;
        align-self: flex-start;
        height: 90;
    }

    .me-chat {
        display: flex;
        flex-direction: row;
        align-self: flex-end;
        height: 90;
    }
</style>

以上,为使用组件的完整示例。主要是通过dialogPage实现的应用遮罩功能,主要的区别在于组件的父组件是dialogPage 而不是你的业务页面!而业务页面变成了 dialogPage的父组件! 有关dialogPage, 请参阅官方文档dialogPage ,进一步了解。

权限

需要请求录音权限!!!

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
</manifest>

tips

  1. 重要!!!!! 需要录音权限!请配置清单。权限申请自行处理。
  2. 需要自定义基座
  3. 其他问题,欢迎指正

Happy new year ! 2025

谢谢!

隐私、权限声明

1. 本插件需要申请的系统权限列表:

<uses-permission android:name="android.permission.RECORD_AUDIO" />

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

无

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

无

暂无用户评论。