1.效果
微信小程序与AI语音对话
2.效果主要实现技术
①AI语音合成(阿里云平台)
②微信小程序同声传译功能
③本功能是用原生微信小程序实现的(可自行转成uniapp代码)
3.同声传译
进入微信服务市场,搜索同声传译就能找到这个插件,然后添加到自己的微信小程序中。
可在微信公众平台,设置==》第三方设置==》插件管理中看到添加的插件
4.添加同声传译插件后可在自己项目中配置,打开app.json
{"plugins": {"WechatSI": {"version": "0.3.5","provider": "wx069ba97219f66d99"}}
}
5.代码
index.wxml
<view class="container">
<!-- <privacypopup></privacypopup> --><view class="header"><view><image class="tit jello-horizontal" src="../../../assets/image/1/4.png" mode="aspectFit"></image><!-- 默认初始化 --><image wx:if="{{msgText==1}}" class="textSty" src="../../../assets/image/5.png" mode="aspectFit"></image><!-- 对话正在进行中 --><image wx:elif="{{msgText==2}}" class="textSty" src="../../../assets/image/said.png" mode="aspectFit"></image><!-- 3对话暂停 --><!-- <image wx:elif="{{msgText==3}}" class="textSty" src="../../../assets/image/pause.png" mode="aspectFit"></image> --><!-- 4对话出现问题 --><!-- <image wx:elif="{{msgText==4}}" class="textSty" src="../../../assets/image/anew.png" mode="aspectFit"></image> --></view></view><view bindtap="openAi" class="content"><view class="logos"><!-- <image class="tit1 pulsate-bck" src="../../../assets/image/cheng.png" mode="aspectFit"></image> --><view class="logos_item roll-in-blurred-left"><image class="ce1 {{ annimationFlag? 'rotate-center' : '' }}" src="../../../assets/image/ce1.png" mode="aspectFit"></image><image class="ce2 {{ annimationFlag? 'pulsate-bck' : '' }}" src="../../../assets/image/ce2.png" mode="aspectFit"></image></view><!-- 动画音律 --><view class="dajianshiBox" wx:if="{{touchstart}}"><view class="dajianshi "><span></span><span></span><span></span><span></span><span></span></view></view></view><view class="iconSty"><view class="iconSty_left"><image class="close1" bindtap="backIndex" src="../../../assets/image/close.png" mode="aspectFit"></image></view><view class="iconSty_center"><view data-flag='1' bindlongpress="touchStart" bindtouchend="touchEnd"><image class="{{touchstart == false?'microphone1':'microphone2'}}" src="../../../assets/image/{{touchstart == false?'microphone':'prohibit'}}.png" mode="aspectFit"></image><!-- <image class="microphone2" src="../../../assets/image/prohibit.png" mode="aspectFit"></image> --></view></view><view class="iconSty_right"><!-- <image bindtap="openContent" class="fish1" src="../../../assets/image/fish.png" mode="aspectFit"></image> --><image bindtap="openContent" class="fish1" src="../../../assets/image/sa.png" mode="aspectFit"></image></view></view><!-- <image class="tit2" src="../../../assets/image/1/3.png" mode="aspectFit"></image> --></view><view class="footer"><image class="end" src="../../../assets/image/end.png" mode="aspectFit"></image></view><!-- 弹窗 历史记录(不需要可删除)--><view class="modal-mask" wx:if="{{showModal}}"><view class="modal-container"><image class="modal-bg" src="../../../assets/image/contentbg.png" mode="scaleToFill"></image><!-- 这里放置需要滚动的内容 --><view class="modal-content"><!-- 多文本展示区域 padding-bottom: 30rpx;--><view class="text-content"><scroll-view id="scroll-view" scroll-into-view="{{toView}}" scroll-y="true" style="height: 655rpx;box-sizing: border-box;"><view style="box-sizing: border-box;"><view wx:for="{{msglist}}" wx:key="*this" id="item{{index}}"><!-- 右侧布局 wx:if="{{item.type === 'right'}}"--><view class="right-layout" ><view class='right-msg' bindlongpress="copyText" data-key="{{item.question}}">{{item.question}}</view><view class="right-arrow-layout"><view class="right-arrow-img"></view></view><view class="right_item"><image class="right-arrow-photo" src='../../../assets/image/missing-face.png' mode='aspectFill'></image></view></view><!-- 左侧布局 wx:elif="{{item.type === 'left'}}"--><view class="left-layout" ><view class="left_item"><image class="left-arrow-photo" src='../../../assets/image/cheng.png' mode='aspectFill'></image></view><view class="left-arrow-layout"><!-- 小尖角 --><view class="left-arrow-img"></view></view><!-- index.wxml --><view class='left-msg' bindlongpress="copyText" data-key="{{item.answer}}">{{item.answer}}</view></view></view></view></scroll-view></view></view><view class="modal-btns"><!-- <image bindtap="copyText" class="modal_img1" src="../../../assets/image/copyBtn.png" mode="scaleToFill"></image> --><image class="" src="../../../assets/image/restart.png" mode="scaleToFill" bindtap="closeModal"></image></view></view></view><!-- 登录 --><loginwin login_show="{{loginShow}}" bind:customEvent="onChildEvent"></loginwin>
</view>
index.wxss
@charset "UTF-8";.container {display: flex;flex-direction: column;height: 100%;/* 之前的样式保持不变 */background-color: #000;min-height: 100vh;position: relative;
}.container .header {padding: 20rpx;text-align: center;
}.container .header {flex: 1;display: flex;justify-content: center;align-items: center;/* 头部固定高度或其他样式 */
}.container .header image {display: block;
}.container .content {flex-grow: 1;overflow-y: auto;padding: 20rpx;text-align: center;display: flex;flex-direction: column;align-items: center;
}.container .footer {/* 底部固定高度或其他样式 */padding: 20rpx 0 40rpx 0;text-align: center;
}.tit {width: 235rpx;height: 70rpx;
}.textSty {width: 217rpx;height: 73rpx;box-sizing: border-box;margin-top: 35rpx;
}.tit1 {width: 369rpx;height: 368rpx;
}.ce1 {width: 368rpx;height: 368rpx;
}.ce2 {width: 330rpx;height: 330rpx;position: absolute;left: 19.5rpx;top: 20.8rpx;
}.iconSty {display: flex;justify-content: space-around;align-items: flex-end;height: 172rpx;padding-bottom: 48rpx;width: 79%;/* height: 327rpx; */
}.logos {text-align: center;flex: 1;position: relative;
}.logos_item {position: relative;
}.iconSty_left .close1 {width: 65rpx;height: 65rpx;
}.iconSty_center .microphone1 {width: 59rpx;height: 83rpx;
}.microphone2 {width: 75rpx;height: 107rpx;
}.iconSty_right .fish1 {width: 65rpx;height: 65rpx;
}.tit2 {width: 190rpx;height: 22rpx;margin-top: 69rpx;
}.end {width: 214rpx;height: 74rpx;
}/* 弹窗 */
.modal-mask {touch-action: none; /* 禁止触摸滚动 */position: fixed;top: 0;left: 0;right: 0;bottom: 0;z-index: 9999999999999999;background-color: rgba(0, 0, 0, 0.5);display: flex;justify-content: center;align-items: center;
}.modal-container {width: 603rpx;height: 878rpx;background-color: transparent;border-radius: 10rpx;/* overflow: hidden; */position: relative;
}.modal-bg {width: 603rpx;height: 878rpx;position: absolute;top: 0;left: 0;
}.modal-content {position: relative;box-sizing: border-box;padding: 90rpx 32rpx 20rpx 32rpx;color: #000;height: 88%;/* overflow: hidden; *//* 根据实际情况调整文本样式 */
}.text-content {/* 适当调整多文本展示区域的样式 */width: 100%;height: 100%;/* overflow-y: auto; *//* border: 1px solid red; */
}.modal-btns {display: flex;justify-content: center;padding: 47rpx 0rpx;position: absolute;bottom: 0;width: 100%;
}.modal-btns image {width: 148rpx;height: 56rpx;
}.modal_img1 {margin-right: 34rpx;
}.btn {width: 48%;height: 80rpx;line-height: 80rpx;border-radius: 10rpx;text-align: center;color: #fff;background-color: #007aff;
}/* 滚动条 */
.text-content::-webkit-scrollbar {/*滚动条整体样式*//*高宽分别对应横竖滚动条的尺寸*//* width: 3px;height: 1px; */
}.text-content::-webkit-scrollbar-thumb {/*滚动条里面小方块*//* border-radius: 10px;height: 10px;-webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);background: #4D4D4D; */
}
.text-content::-webkit-scrollbar-track {/*滚动条里面轨道*//* -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);border-radius: 5px;background: #ffffff; */
}
/* 聊天页面 */
/* 左侧布局 */
.left-layout {position: relative;display: flex;justify-content: flex-start;padding: 20rpx 60rpx 2vw 2vw;
}.left-arrow-photo {width: 60rpx;height: 60rpx;min-width: 60rpx;min-height: 60rpx;border-radius: 50%;margin-top: 5rpx;
}.left-msg {flex: 1;font-size: 25rpx;color: #444;line-height: 45rpx;display: flex;justify-content: flex-start;align-items: center;padding: 10rpx;background-color: #ccc;margin-left: -12rpx;border-radius: 10rpx;z-index: 10;
}.left-arrow-layout {position: relative;width: 35rpx;height: 65rpx;display: flex;align-items: center;z-index: 9;
}.left-arrow-img {width: 0;height: 0;border-top: 9px solid transparent;border-right: 9px solid #ccc;border-bottom: 9px solid transparent;position: absolute;top: 5rpx;left: 5px;
}/* 右侧布局 */
.right-layout {box-sizing: border-box;display: flex;justify-content: flex-end;padding: 20rpx 2vw 2vw 15vw;/* border: 1px solid green; */
}.right-arrow-photo {width: 60rpx;height: 60rpx;min-width: 60rpx;min-height: 60rpx;border-radius: 50%;margin-top: 5rpx;
}.right-msg {flex: 1;box-sizing: border-box;font-size: 25rpx;color: #444;line-height: 45rpx;display: flex;justify-content: flex-start;align-items: center;padding: 10rpx;background-color: #96EB6A;margin-right: -1rpx;border-radius: 10rpx;z-index: 10;
}.right-arrow-layout {position: relative;width: 35rpx;height: 65rpx;margin-right: 5rpx;display: flex;align-items: center;z-index: 9;
}.right-arrow-img {width: 0;height: 0;border-top: 18rpx solid transparent;border-right: 18rpx solid transparent;border-left: 18rpx solid #96EB6A;border-bottom: 18rpx solid transparent;position: absolute;top: 13rpx;left: -2rpx;
}.right_item {width: 70rpx;height: 70rpx;text-align: center;
}.left_item {width: 70rpx;height: 70rpx;text-align: center;
}/* 动画 */
.pulsate-bck {-webkit-animation: pulsate-bck 2s ease-in-out infinite both;animation: pulsate-bck 2s ease-in-out infinite both;
}@-webkit-keyframes pulsate-bck {0% {-webkit-transform: scale(1);transform: scale(1);}50% {-webkit-transform: scale(0.9);transform: scale(0.9);}100% {-webkit-transform: scale(1);transform: scale(1);}
}@keyframes pulsate-bck {0% {-webkit-transform: scale(1);transform: scale(1);}50% {-webkit-transform: scale(0.9);transform: scale(0.9);}100% {-webkit-transform: scale(1);transform: scale(1);}
}/* 旋转 */
.rotate-center {-webkit-animation: rotate-center 1.9s linear infinite;animation: rotate-center 1.9s linear infinite;
}@-webkit-keyframes rotate-center {0% {-webkit-transform: rotate(0);transform: rotate(0);}100% {-webkit-transform: rotate(360deg);transform: rotate(360deg);}
}@keyframes rotate-center {0% {-webkit-transform: rotate(0);transform: rotate(0);}100% {-webkit-transform: rotate(360deg);transform: rotate(360deg);}
}/* 音律动画 */
.dajianshi {/* margin:100px auto 0; */width: 100rpx;height: 55rpx;display: flex;margin: 0 auto;
}.dajianshi span {width: 12rpx;border-radius: 18px;margin-right: 9rpx;
}.dajianshi span:nth-child(1) {animation: bar1 2s 0.2s infinite linear;
}.dajianshi span:nth-child(2) {animation: bar2 2s 0.4s infinite linear;
}.dajianshi span:nth-child(3) {animation: bar3 2s 0.6s infinite linear;
}.dajianshi span:nth-child(4) {animation: bar4 2s 0.8s infinite linear;
}.dajianshi span:nth-child(5) {animation: bar5 2s 1.0s infinite linear;
}.dajianshi span:nth-child(6) {animation: bar6 2s 1.2s infinite linear;
}.dajianshi span:nth-child(7) {animation: bar7 2s 1.4s infinite linear;
}.dajianshi span:nth-child(8) {animation: bar8 2s 1.6s infinite linear;
}.dajianshi span:nth-child(9) {animation: bar9 2s 1.8s infinite linear;
}@keyframes bar1 {0% {background: #FF6600;margin-top: 20%;height: 10%;}50% {background: #FF6600;height: 100%;margin-top: 0%;}100% {background: #FF6600;height: 10%;margin-top: 20%;}
}@keyframes bar2 {0% {background: #FF6600;margin-top: 20%;height: 10%;}50% {background: #FF6600;height: 100%;margin-top: 0%;}100% {background: #FF6600;height: 10%;margin-top: 20%;}
}@keyframes bar3 {0% {background: #FF6600;margin-top: 20%;height: 10%;}50% {background: #FF6600;height: 100%;margin-top: 0%;}100% {background: #FF6600;height: 10%;margin-top: 20%;}
}@keyframes bar4 {0% {background: #FF6600;margin-top: 20%;height: 10%;}50% {background: #FF6600;height: 100%;margin-top: 0%;}100% {background: #FF6600;height: 10%;margin-top: 20%;}
}@keyframes bar5 {0% {background: #FF6600;margin-top: 20%;height: 10%;}50% {background: #FF6600;height: 100%;margin-top: 0%;}100% {background: #FF6600;height: 10%;margin-top: 20%;}
}.dajianshiBox {width: 100%;position: absolute;bottom: 0;
}
/* 滚动条样式设置 */
/* 设置 scroll-view 的滚动条样式 */
.scroll-view::-webkit-scrollbar {width: 8px; /* 滚动条宽度 */height: 8px; /* 滚动条高度,如果是横向滚动,这里设置高度 */
}/* 滚动条轨道 */
.scroll-view::-webkit-scrollbar-track {background-color: #4D4D4D; /* 滚动条轨道背景色 */
}/* 滚动条滑块 */
.scroll-view::-webkit-scrollbar-thumb {background-color: #999999; /* 滚动条滑块颜色 */border-radius: 10rpx; /* 滑块边框圆角 */
}/* 滚动条滑块悬停状态 */
.scroll-view::-webkit-scrollbar-thumb:hover {background-color: #555; /* 滚动条滑块悬停时的颜色 */
}
/* 滑入式动画 */
.slide-in-elliptic-top-fwd {-webkit-animation: slide-in-elliptic-top-fwd 0.7s cubic-bezier(0.250, 0.460, 0.450, 0.940) 1s both;/* Standard animation property */animation: slide-in-elliptic-top-fwd 0.7s cubic-bezier(0.250, 0.460, 0.450, 0.940) 1s both;
}
@-webkit-keyframes slide-in-elliptic-top-fwd {0% {-webkit-transform: translateY(-600px) rotateX(-30deg) scale(0);transform: translateY(-600px) rotateX(-30deg) scale(0);-webkit-transform-origin: 50% 100%;transform-origin: 50% 100%;opacity: 0;}100% {-webkit-transform: translateY(0) rotateX(0) scale(1);transform: translateY(0) rotateX(0) scale(1);-webkit-transform-origin: 50% 1400px;transform-origin: 50% 1400px;opacity: 1;}
}
@keyframes slide-in-elliptic-top-fwd {0% {-webkit-transform: translateY(-600px) rotateX(-30deg) scale(0);transform: translateY(-600px) rotateX(-30deg) scale(0);-webkit-transform-origin: 50% 100%;transform-origin: 50% 100%;opacity: 0;}100% {-webkit-transform: translateY(0) rotateX(0) scale(1);transform: translateY(0) rotateX(0) scale(1);-webkit-transform-origin: 50% 1400px;transform-origin: 50% 1400px;opacity: 1;}
}
/* 果冻 */
.jello-horizontal {-webkit-animation: jello-horizontal 0.9s 0.5s both;animation: jello-horizontal 0.9s 0.5s both;
}
@-webkit-keyframes jello-horizontal {0% {-webkit-transform: scale3d(1, 1, 1);transform: scale3d(1, 1, 1);}30% {-webkit-transform: scale3d(1.25, 0.75, 1);transform: scale3d(1.25, 0.75, 1);}40% {-webkit-transform: scale3d(0.75, 1.25, 1);transform: scale3d(0.75, 1.25, 1);}50% {-webkit-transform: scale3d(1.15, 0.85, 1);transform: scale3d(1.15, 0.85, 1);}65% {-webkit-transform: scale3d(0.95, 1.05, 1);transform: scale3d(0.95, 1.05, 1);}75% {-webkit-transform: scale3d(1.05, 0.95, 1);transform: scale3d(1.05, 0.95, 1);}100% {-webkit-transform: scale3d(1, 1, 1);transform: scale3d(1, 1, 1);}
}
@keyframes jello-horizontal {0% {-webkit-transform: scale3d(1, 1, 1);transform: scale3d(1, 1, 1);}30% {-webkit-transform: scale3d(1.25, 0.75, 1);transform: scale3d(1.25, 0.75, 1);}40% {-webkit-transform: scale3d(0.75, 1.25, 1);transform: scale3d(0.75, 1.25, 1);}50% {-webkit-transform: scale3d(1.15, 0.85, 1);transform: scale3d(1.15, 0.85, 1);}65% {-webkit-transform: scale3d(0.95, 1.05, 1);transform: scale3d(0.95, 1.05, 1);}75% {-webkit-transform: scale3d(1.05, 0.95, 1);transform: scale3d(1.05, 0.95, 1);}100% {-webkit-transform: scale3d(1, 1, 1);transform: scale3d(1, 1, 1);}
}
/* 滚入 */
.roll-in-blurred-left {-webkit-animation: roll-in-blurred-left 0.65s cubic-bezier(0.230, 1.000, 0.320, 1.000) both;animation: roll-in-blurred-left 0.65s cubic-bezier(0.230, 1.000, 0.320, 1.000) both;
}@-webkit-keyframes roll-in-blurred-left {0% {-webkit-transform: translateX(-1000px) rotate(-720deg);transform: translateX(-1000px) rotate(-720deg);-webkit-filter: blur(50px);filter: blur(50px);opacity: 0;}100% {-webkit-transform: translateX(0) rotate(0deg);transform: translateX(0) rotate(0deg);-webkit-filter: blur(0);filter: blur(0);opacity: 1;}
}
@keyframes roll-in-blurred-left {0% {-webkit-transform: translateX(-1000px) rotate(-720deg);transform: translateX(-1000px) rotate(-720deg);-webkit-filter: blur(50px);filter: blur(50px);opacity: 0;}100% {-webkit-transform: translateX(0) rotate(0deg);transform: translateX(0) rotate(0deg);-webkit-filter: blur(0);filter: blur(0);opacity: 1;}
}
index.js
// pages/ai/aiVoice/index.ts
//const { TextEncoder, TextDecoder } = require('../../../miniprogram_npm/text-encoding-shim/index.js')
//let Wxml2canvas = require('wxml2canvas/index.js');
const app = getApp();
//引入插件:微信同声传译
const plugin = requirePlugin('WechatSI');
//获取全局唯一的语音识别管理器recordRecoManager
const manager = plugin.getRecordRecognitionManager();
Page({/*** 页面的初始数据*/data: {isplay: true,onstops: true, //默认执行onStopisFlag: false, //是否点击录音到获取结果之间状态longPressTimer: null, // 用于存储长按定时器的变量touchStartTime: 0, //长按开始touchEndTime: 0, //松开结束 authsetting: false, //是否获取授权url: "地址",openid: null,islongPress: false, //是否长按ParentValue: 'Parent',loginShow: false, //登录弹窗默认关闭annimationFlag: false, //logo动画默认关闭touchstart: false, //默认没有按下toView: null,scrollTop: 0,src: '', //语音地址resultobj: {result: "",tempFilePath: ""},msgText: 1, //1默认初始化 2对话进行中 3结束对话 4对话出现问题flag: 1,haveflag: false, //防止重复点击recordState: false, //麦克风默认关闭状态msglist: [], //聊天记录showModal: false, //历时消息记录},/*** 生命周期函数--监听页面加载*/onLoad() {const str = new TextDecoder('utf-8').decode(new Uint8Array("jsfjsdfslkdf看见撒巅峰时刻京东方·1"))console.log("str",str)// 关闭主页按钮wx.hideHomeButton();wx.setNavigationBarTitle({title: "AI对话"})// 判断用户是否登录this.isLogin();// 获取语音授权this.getSeeting(1);//识别语音this.initRecord();const authset = wx.getStorageSync('AUTHSETTING');if (!authset) { //没有获取录音权限// 重新获取录音权限this.getSeeting(1);}},/*** 生命周期函数--监听页面初次渲染完成*/onReady() {//创建内部 audio 上下文 InnerAudioContext 对象。this.innerAudioContext = wx.createInnerAudioContext();this.innerAudioContext.src = '';// this.innerAudioContext.onError(function (res) {// console.log(res);// wx.showToast({// title: '语音播放失败',// icon: 'none',// })// })},/*** 生命周期函数--监听页面显示*/onShow() {},// 获取聊天记录getChartQuery() {wx.showLoading({title: '读取中...',icon: 'none',mask: true})console.log('openid', wx.getStorageSync('OPENID'));let openId = wx.getStorageSync('OPENID');// 获取语音接口wx.request({url: this.data.url + '/cyjgVoice/record',method: 'POST',data: {openId: openId},success: res1 => {wx.hideLoading();this.setData({showModal: true});let data1 = res1.data.data;if (res1.data.code == 200) {//设置语音this.setData({msglist: data1})// 模拟异步数据加载setTimeout(() => {// 页面进入时滚动到底部,给在data中定义的变量赋值this.setData({toView: `item${this.data.msglist.length - 1}`,});}); // 假设数据加载需要一定时间,这里设置一个延时} else {wx.showToast({title: res1.data.msg,icon: 'none',duration: 2000})}}})},// 判断用户是否登录isLogin() {// 获取本地存储中的 OPENIDconst openid = wx.getStorageSync('OPENID');if (openid) {// 如果本地存储中存在 OPENID,则说明用户已经登录过,可以直接使用 OPENID 进行后续操作this.setData({loginShow: false})// 这里可以进行其他操作,比如直接跳转到主页面} else {// 如果本地存储中没有 OPENID,则需要调用登录接口获取 OPENIDthis.setData({loginShow: true,openid: openid})}},onChildEvent: function (event) {this.setData({loginShow: event.detail.login_show})},// 复制粘贴copyText(e) {let key = e.currentTarget.dataset.key;wx.setClipboardData({ //设置系统剪贴板的内容data: key,success(res) {wx.getClipboardData({ // 获取系统剪贴板的内容success(res) {wx.showToast({title: '复制成功',icon: "none"})}})}})},//暂停语音backIndex() {this.setData({msgText: 1, //初始化touchstart: false, //按钮恢复初始状态annimationFlag: false,haveflag: false,'resultobj.tempFilePath': "",isplay: false,resultText: "",src: ""})this.innerAudioContext.src = " "wx.stopBackgroundAudio();this.innerAudioContext.stop(); //暂停音频// 如何判断当前是语音录制识别状态if (this.data.isFlag) {this.setData({onstops: false, //是否执行onStop})wx.showLoading({title: '关闭中...',icon: 'none',// mask: true})// 停止识别manager.stop();}},// 播放语音yuyinPlay: function (e) {console.log("播放1", e);if (this.data.src == '' && this.data.isplay) {console.log("播放2",this.data.src,'222',this.data.isplay);return;}this.setData({msgText: 2, //正在对话annimationFlag: true,haveflag: true})this.innerAudioContext.src = this.data.src; // 设置音频地址this.innerAudioContext.onError(function (res) {this.setData({msgText: 1, //初始化annimationFlag: false,haveflag: false})wx.showToast({title: '语音播放失败',icon: 'none',})})this.innerAudioContext.onTimeUpdate(() => {// console.log('音频播放进度更新', this.data.src);});this.innerAudioContext.onEnded(() => { // 添加播放结束的回调this.setData({msgText: 1, //初始化annimationFlag: false,haveflag: false})// 在这里执行播放完毕后的操作,比如关闭语音this.innerAudioContext.stop(); // 使用 stop 方法停止音频并重置播放状态});this.innerAudioContext.play(); // 播放音频},// 结束语音end: function (e) {that.setData({msgText: 1, //初始化annimationFlag: false})this.innerAudioContext.stop(); //暂停音频},//识别语音 -- 初始化initRecord() {const that = this;// 有新的识别内容返回,则会调用此事件manager.onRecognize = function (res) {console.log("有新的识别内容返回,则会调用此事件")}// 正常开始录音识别时会调用此事件manager.onStart = function (res) {console.log("成功开始录音识别", res)that.setData({// annimationFlag:true})}//识别结束事件manager.onStop = function (res) {if (!that.data.isplay) {wx.showToast({title: "请说话",icon: 'success',image: '/assets/image/no_voice.png',duration: 1000,success: function (res) {this.setData({haveflag: false,})},fail: function (res) {console.log(res);}});return false}if (res.result == '') {wx.hideLoading();// wx.showToast({// title: '听不清楚,请重新说一遍!',// icon: 'none',// duration: 2000// })// that.setData({// msgText: 1, //初始化// haveflag: false,// isFlag: false,// })that.showRecordEmptyTip()return;} else {// wx.showLoading({// title: '正在思考...',// icon: 'none',// })that.setData({resultobj: {result: res.result,tempFilePath: res.tempFilePath,msgText: 2, //正在对话annimationFlag: true}})// 调用接口// that.resultAPi();that.resultTextApi();// that.resultAPi3();}}// 识别错误事件manager.onError = function (res) {console.log("error msg", res);wx.hideLoading();wx.showToast({icon: "none",title: '请重新开始~'})that.setData({haveflag: false,msgText: 1,annimationFlag: false,isFlag: false, //当前录制语音识别状态})}},// 根据wx.getSetting判断用户是否打开了录音权限,如果没有打开,则通过wx.authorize,向用户打开授权请求,如果用户拒绝了,就给用户打开授权设置页面。getSeeting(type) {// wx.showLoading({// title: '获取录音权限',// icon: 'none',// mask: true// })const _this = thiswx.getSetting({ //获取用户当前设置success: res => {// wx.hideLoading();// console.log('获取权限', res);if (res.authSetting['scope.record']) { //查看是否授权了录音设置// console.log('获取权限1111');const authset = wx.setStorageSync('AUTHSETTING', true);_this.setData({authsetting: true})if (type == 2) {wx.showToast({title: '获取录音权限成功,点击重新开始!',icon: 'none',duration: 2000})}} else {// 用户还没有授权,向 用户发起授权请求wx.authorize({ //提前向用户发起授权请求,调用后会立刻弹窗询问用户是否同意授权小程序使用某项功能或获取用户的某些数据,但不会实际调用对应接口scope: 'scope.record',success() { //用户同意授权摄像头// console.log("同意授权");// wx.showToast({// title: '获取录音权限成功',// icon: 'none',// duration: 2000// })},fail() { //用户不同意授权摄像头_this.openSetting()}})}},fail() {// console.log('获取用户授权信息失败');wx.showToast({title: '获取权限失败',icon: 'none',duration: 2000})}})},openSetting() {wx.openSetting({success(res) {console.log(res);if (res.authSetting['scope.record']) {console.log('用户已经同意录音权限');// 在这里可以再次执行录音操作或者其他逻辑} else {console.log('用户依然拒绝录音权限');// 可以提示用户继续操作的限制或者做其他处理}},fail() {console.log('打开设置页面失败');},});},//请求语音结果resultAPi() {//调用接口wx.request({url: this.data.url + '/cyjgVoice/question',method: 'POST',data: {question: this.data.resultobj.result,openId: wx.getStorageSync('OPENID')},success: res => {let data = res.data.data;if (res.data.code == 200 && this.data.isplay) {// 获取语音接口wx.request({url: this.data.url + '/cyjgVoice/voice',method: 'POST',data: {id: data.id,code: data.code,openId: data.openId},success: res1 => {wx.hideLoading();let data1 = res1.data.data;if (res1.data.code == 200 && this.data.isplay) {console.log("语音结果成功");// //设置语音this.setData({src: data1.voiceUrl,isFlag: false,resultText: data1.answer})if (this.data.src) {// 文字转语音// this.wordYun();this.yuyinPlay();} else {console.log("没拿到");}} else if (res1.data.code != 200) {wx.showToast({title: res1.data.msg,icon: 'none',duration: 2000})}}})} else if (res.data.code != 200) {wx.hideLoading();wx.showToast({title: res.data.msg,icon: 'none',duration: 2000})this.setData({msgText: 1})}}})},//获取文字接口resultTextApi() {//调用接口const formdata = {question: this.data.resultobj.result,openId: wx.getStorageSync('OPENID'),};const requestTask = wx.request({url: this.data.url + '/cyjgVoice/ans',method: 'POST',header: {'content-type': 'application/x-www-form-urlencoded' // 或者 'application/json' 如果你的后端能接受JSON格式},data: formdata, // 直接使用formdata对象success: res => {console.log("语音1111", res);wx.hideLoading();if (res.data.code == 200 && this.data.isplay) {let data = res.data.data;console.log("语音结果成功");//设置语音this.setData({isFlag: false,resultText:data.answer,src:data.voiceUrl})if (data.voiceUrl) {// 文字转语音this.yuyinPlay();} else {console.log("没拿到");}} else if (res.data.code != 200) {wx.hideLoading();wx.showToast({title: res.data.msg,icon: 'none',duration: 2000})this.setData({msgText: 1})}}})});},//语音 --按住说话touchStart(e) {// 判断是否获取录音权限if (this.data.haveflag) { //true 请先结束语音wx.showToast({title: '请先关闭语音!',icon: 'none',duration: 2000})return false}// 当前正在识别语音,还没结束上一次识别,请先关闭再进行录音if (this.data.isFlag) { //true 请先结束语音wx.showToast({title: '请先关闭语音!',icon: 'none',duration: 2000})return false}this.setData({islongPress: true,isplay: true})var flag = Number(e.currentTarget.dataset.flag)this.setData({recordState: true, //录音状态flag: flag,touchstart: true, //按下msgText: 2, //初始化状态})// 语音开始识别manager.start({lang: 'zh_CN', // 识别的语言})},// resultAPi3() {//调用接口 获取后端请求返回的base64答案,前端转成文字,利用同声传译生成语音功能wx.request({url: this.data.url + '/cyjgVoice/question',method: 'POST',data: {question: this.data.resultobj.result,openId: wx.getStorageSync('OPENID')},success: res => {console.log("语音1111", res.data);let data = res.data.data;if (res.data.code == 200 && this.data.isplay) {// 获取语音接口wx.hideLoading();// //设置语音this.setData({// src: data.voiceUrl,isFlag: false,resultText: data.answer})this.base64ChangeVideo(data.voiceUrl);} else if (res.data.code != 200) {wx.hideLoading();wx.showToast({title: res.data.msg,icon: 'none',duration: 2000})this.setData({msgText: 1})}}})},// base64转mp3音频base64ChangeVideo(base64Data) {// const backgroundAudioManager = wx.getBackgroundAudioManager()const audioPath = wx.env.USER_DATA_PATH + '/ordernew.mp3'const fs = wx.getFileSystemManager();let that = this;fs.writeFile({filePath: audioPath,data: base64Data,encoding: 'base64',success(res) {that.setData({src: audioPath})that.yuyinPlay();},})},//语音 --松开结束touchEnd(e) {if (!(this.data.islongPress)) { //如果是长按执行下面内容return false}wx.showLoading({title: '正在思考...',icon: 'none',})if (this.data.haveflag) { //true 请先结束语音wx.hideLoading();// wx.showToast({// title: '请先关闭语音111!',// icon: 'none',// duration: 2000// })return false}this.setData({touchstart: false,recordState: false,islongPress: false, //长按初始状态isFlag: true, //判断从松手到识别录音期间状态haveflag: true})// 语音结束识别manager.stop();},// 打开弹窗openContent() {if ((this.data.msgText == 1 && !(this.data.annimationFlag)) || (this.data.msgText == 2 && this.data.annimationFlag)) { //true 请先结束语音// wx.showToast({// title: '请先关闭对话!',// icon: 'none',// duration: 2000// })this.getChartQuery();} else {return false}},// 关闭弹窗closeModal() {this.setData({showModal: false})},showRecordEmptyTip: function () {this.setData({msgText: 1, //初始化haveflag: false,isFlag: false,})wx.showToast({title: "请说话",icon: 'success',image: '/assets/image/no_voice.png',duration: 1000,success: function (res) {},fail: function (res) {console.log(res);}});},// 文字转语音wordYun: function (e) {var that = this;var content = this.data.resultText;plugin.textToSpeech({lang: "zh_CN",tts: true,content: content,success: function (res) {console.log("succ tts", res.filename);that.setData({src: res.filename})that.yuyinPlay();},fail: function (res) {console.log("fail tts", res)}})},/*** 生命周期函数--监听页面隐藏*/onHide() {this.innerAudioContext.stop();},/*** 生命周期函数--监听页面卸载*/onUnload() {this.innerAudioContext.stop();},/*** 页面相关事件处理函数--监听用户下拉动作*/onPullDownRefresh() {},/*** 页面上拉触底事件的处理函数*/onReachBottom() {},/*** 用户点击右上角分享*/onShareAppMessage() {}
})
index.json
{"usingComponents": {"loginwin": "../../../components/loginwin/index"}
}
6.注意:
2.微信同声传译点录音start后,不说话然后调用stop,需要10S以上才能返回结果,目前没有找到解决方案(有解决的小伙伴,给我说一下,谢谢~)
3.如果同声传译真机调试使用不了,报错“start:fail api scope is not declared in the privacy agreement”,需要去微信公众平台更新一下隐私协议,吧麦克风添加进去
7.微信登录组件
效果
7.1 创建loginwin组件
7.2 index.wxml代码
<view><view class="modal-mask1" wx:if="{{login_show}}"><view class="modal-container1"><!-- 微信授权流程 --><view class="popupBox1"><view class="ruleContent"><view class="loginIcon1"><image src="../../assets/image/weixin.png" class="wx-icon" mode="aspectFit"></image></view><view class="ruleTitle">微信授权登录</view></view><view class="popupClose"><image src="../../assets/image/loginBtn.png" class="login-btn" bindtap="login1" mode="widthFix"></image></view><view><view class="flex_start flex_items endSty"><checkbox-group bindchange="checkboxChange" class="checkbox-group" style="transform: scale(0.8);"><checkbox value="all" checked="{{allchecked}}" color="#00C800" class="checkbox"></checkbox>
</checkbox-group><view class="txt">阅读用户协议<text bindtap="read" class="policy-text">《用户协议及隐私政策》</text></view></view></view></view></view></view><!-- 获取微信头像部分 不需要可删除-->
<!-- 引入必要样式 -->
<view class="modal-mask" wx:if="{{getHeadShow}}" >
<view class="modal-container" wx:if="{{getHeadShow}}" style="padding:0;"><view class="popupBox"><view class="ruleContent"><view class="loginIcon"><button class="avatar-wrapper" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar"><image wx:if="userInfo.headImg" class="avatar" src="{{userInfo.headImg}}"></image><image v-else class="avatar" src="../../assets/image/cheng.png"></image><view class="up_img">上传头像</view></button></view><view class="ruleTitle"><!-- <input type="text" class="weui-input" placeholder="请输入昵称" bindinput="getNickname" /> --><input value="{{userInfo.nickName||''}}" bindchange="onInput" type="nickname" class="weui-input" placeholder="请输入昵称" /></view></view><view class="popupClose"><!-- <button class="avatar-wrapper1" open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber"><image src="../../static/loginBtn.png" mode="widthFix"></image>
</button> -->
<button >
保存
</button></view></view>
</view>
</view>
</view>
7.3 index.js代码
// pages/ai/loginpage/index.ts
Component({// "component": true,properties: {login_show: {type: Boolean,value: false, // 设置默认值observer: function (oldVal, newVal) { //每次父组件向子组件传值的时候都会调用这个函数console.log("间停止", oldVal, newVal) // 旧数据和新数据// this.Fn()}},},/*** 页面的初始数据*/data: {allchecked: false,//是否选中login_show: false,//默认不显示getHeadShow: false,//是否获取昵称及用户头像url: "地址",userInfo: {nickName: '',openId: '',headImg: '',}},onLoad() {},// 可以在这里监听属性变化observerLoginShow: function (newVal, oldVal) {console.log('监听', newVal);},/*** 生命周期函数--监听页面初次渲染完成*/onReady() {},/*** 生命周期函数--监听页面显示*/onShow() {},methods: {//隐私协议read(){wx.navigateTo({url: '/pages/ai/userAgreement/index',})},// 登录login1() {if (!this.data.allchecked) {wx.showToast({title: '请阅读并勾选用户协议',icon: 'none'})return}this.getUserInfo();},//复选框checkboxChange(n) {console.log('复选框', n)if (n.detail.value[0]) {this.setData({allchecked: true})} else {this.setData({allchecked: false})}},getUserInfo() {var that = this;wx.showLoading({ // 展示加载框title: '加载中',});let code = '';wx.login({success: (loginRes) => {console.log("登录code",loginRes)code = loginRes.code;},fail: () => {wx.showToast({title: "微信登录授权失败",icon: "none"});}});wx.getUserProfile({desc: '登录',success: (info) => {console.log('登录的openid', info)let obj = {code: code,rawData: info.rawData,signature: info.signature,encryptedData: info.encryptedData,iv: info.iv};// 登录成功wx.showLoading({mask: true,title: '登录中...'});// 登录接口wx.request({url:this.data.url+'/cyjgVoice/login',method: 'POST',data:obj,success: res => {wx.hideLoading();console.log("登录",res.data);let info = res.data;if (info.code == 200) {wx.setStorageSync('OPENID', info.data.openId);wx.setStorageSync('TOKEN', info.data.token);// 给父组件传值 关闭组件that.triggerEvent('customEvent', {login_show: false});} else {//登录出错wx.showToast({title: res.data.msg,icon: "none"});}}})},fail: (error) => {// 处理getUserProfile失败的情况}});},// 图片上传 onChooseAvatar: function (e) {console.log('图片', e)let that = this;// 获取七牛云tokengetQiniuyunToken().then(res => {if (res.code === 20000) {// 构建key值const imgurl = e.detail.avatarUrl.replace('http://tmp/', '');const key = 'ar/files/' + new Date().getTime() + '_' + imgurl;wx.showLoading({mask: true,title: '上传中...',});// 准备上传文件wx.uploadFile({url: 'https://upload-z2.qiniup.com', // 华东地区上传filePath: e.detail.avatarUrl,name: 'file',method: 'POST',formData: {key: key, // key值token: res.token, // 七牛云token值},success: (uploadFileRes) => {// 解析返回的JSON字符串const strToObj = JSON.parse(uploadFileRes.data);// 拼接域名和key值得到完整URLconst backUrl = VUE_APP_IMG_URL + strToObj.key;// 更新用户信息的头像地址that.userInfo.headImg = backUrl;wx.hideLoading();},fail: (fail) => {wx.showToast({title: '网络错误',icon: 'none',});// 注意:原代码中的data.fail(fail)调用在小程序中可能无对应逻辑,需根据实际情况调整wx.hideLoading();},complete: () => {// 完成回调,可根据需要处理}});} else {// 如果获取token失败,根据业务需求处理}}).catch(error => {wx.showToast({title: error.message || '请求失败',icon: 'none',});});},},/*** 生命周期函数--监听页面隐藏*/onHide() {},/*** 生命周期函数--监听页面卸载*/onUnload() {},/*** 页面相关事件处理函数--监听用户下拉动作*/onPullDownRefresh() {},/*** 页面上拉触底事件的处理函数*/onReachBottom() {},/*** 用户点击右上角分享*/onShareAppMessage() {}
})
7.4 index.wxss代码
/* 添加或调整样式以匹配原生小程序组件 */
.modal-mask1 {position: fixed;top: 0;left: 0;right: 0;bottom: 0;background-color: rgba(0, 0, 0, 0.5);display: flex;justify-content: center;align-items: center;z-index: 999;
}.modal-container1 {width: 500rpx;border-radius: 8rpx;background-color: #fff;padding: 40rpx;
}.wx-icon {width: 120rpx;height: 120rpx;
}.login-btn {width: 260rpx;height: 58rpx;
}/* 其他样式保持与uni-app中的样式一致,但需根据微信原生组件调整类名和属性 */
.flex_start {display: flex;align-items: center;justify-content: flex-end;
}.txt {color: #161828;font-size: 24rpx;margin-left: 7rpx;
}.txt text {color: #000;font-weight: 600;text-decoration: underline;
}.endSty {width: 100%;justify-content: center;
}.avatar-wrapper {padding: 0;width: 180px !important;border-radius: 8px;margin: auto;/* margin-top: 40px; */margin-bottom: 50rpx;position: relative;padding-bottom: 20rpx;background-color: transparent;/* text-align: center; */}
.avatar-wrapper1{padding: 0;width: 180px !important;border-radius: 8px;margin: auto;position: relative;padding-bottom: 20rpx;background-color: transparent;
}
.avatar-wrapper1::after {border: none;
}
.weui-input {font-size: 28rpx;border-bottom: 1px solid #000;width: 207rpx;margin: auto;
}.up_img {width: 120rpx;height: 34rpx;color: #fff;background-color: #000;line-height: 34rpx;text-align: center;font-size: 22rpx;border-radius: 17rpx;position: absolute;left: 50%;transform: translateX(-50%);z-index: 999;bottom: 0;
}.avatar-wrapper::after {border: none;
}.avatar {display: block;width: 150rpx;height: 150rpx;border-radius: 50%;margin: auto;
}
.popupBox1 {width: 100%;border-radius: 8rpx 8rpx;
}
.loginIcon1 {position: relative;display: flex;justify-content: center;padding: 30rpx 0 30rpx 0;
}.popupClose {box-sizing: border-box;padding: 70rpx 0 30rpx 0;text-align: center;}
.popupClose image {/* width: 250rpx;height: 58rpx; */vertical-align: middle;
}
.btnSty{
width: 260rpx !important;
height: 58rpx;
}
.ruleTitle {text-align: center;font-weight: bold;
}
7.5 index.json代码
{"usingComponents": {}
}```