SsRecording.vue 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. <template>
  2. <view class="ss-recording-wrapper">
  3. <view class="ss-recording" @tap="clickMicBtn">
  4. <uni-icons :type="micIcon" size="24" color="#101333"></uni-icons>
  5. </view>
  6. <uni-popup ref="recordPopup" background-color="#fff" :is-mask-click="false">
  7. <view class="record-dialog">
  8. <view class="record-title">录音将在{{ countDown }}秒后自动结束</view>
  9. <view class="record-animation" :class="'show-bg-' + decibel">
  10. <view class="mic p-center">
  11. <uni-icons type="mic" :size="24" color="#2943d6" />
  12. </view>
  13. <template v-for="i in 10">
  14. <view class="mic-bg p-center" :class="'bg-' + i"></view>
  15. </template>
  16. </view>
  17. <view class="record-buttons">
  18. <button class="record-cancel" @tap="cancelRecord">取消</button>
  19. <button class="record-start" @tap="recorded">
  20. <view v-if="recordStatus == 3" class="record-loading">
  21. <uni-icons type="spinner-cycle" size="32rpx" color="#ffffff" />
  22. </view>
  23. <view v-if="recordStatus == 2" class="record-stop"></view>
  24. {{ recordStatus == 2 ? '我说完了' : recordStatus == 3 ? '音频处理中' : '开始录音' }}
  25. </button>
  26. </view>
  27. </view>
  28. </uni-popup>
  29. </view>
  30. </template>
  31. <script>
  32. import { RequestApi } from '@/api/requestApi';
  33. export default {
  34. data() {
  35. return {
  36. recordFlag: true,
  37. recordStatus: 1, // 1、待开始录音;2、录音中;3、处理中;
  38. recorderManager: null,
  39. isRecording: false,
  40. recordOption: {
  41. sampleRate: 8000, // 采样率
  42. numberOfChannels: 1, // 单声道
  43. format: 'mp3', // 格式为mp3获取原始音频数据
  44. frameSize: 1, // 帧大小
  45. frameBufferSize: 10 // 每次回调积累的帧数
  46. },
  47. recordDuration: 60,
  48. useTime: 0,
  49. countDownTimeout: null
  50. };
  51. },
  52. computed: {
  53. micIcon() {
  54. return this.recordFlag ? 'mic' : 'micoff';
  55. },
  56. maxDuration() {
  57. return this.recordDuration;
  58. },
  59. countDown() {
  60. let residue = this.maxDuration - parseInt(this.useTime / 10);
  61. return residue > 0 ? residue : 0;
  62. },
  63. decibel() {
  64. return this.useTime % 10;
  65. }
  66. },
  67. mounted() {
  68. this.canvasContext = uni.createCanvasContext('waveCanvas', this);
  69. this.initRecorder();
  70. },
  71. methods: {
  72. initRecorder() {
  73. this.recorderManager = uni.getRecorderManager();
  74. // 录音完成,上传解析
  75. this.recorderManager.onStop((res) => {
  76. this.stopCountDown();
  77. if (this.recordStatus == 2) {
  78. this.recordStatus = 3;
  79. console.log('临时文件路径:', res.tempFilePath);
  80. this.sendVideoFile(res.tempFilePath);
  81. }
  82. });
  83. },
  84. sendVideoFile(filePath) {
  85. RequestApi.transcribe(filePath)
  86. .then((res) => {
  87. this.recordStatus = 1;
  88. if (res.text == '(空内容)') {
  89. uni.showToast({
  90. title: '未检测到你说了什么',
  91. icon: 'none'
  92. });
  93. } else {
  94. this.$emit('setMessage', res.text);
  95. }
  96. })
  97. .catch((error) => {
  98. uni.showToast({
  99. title: '语音识别错误',
  100. icon: 'none'
  101. });
  102. })
  103. .finally(() => {
  104. if (this.$refs.recordPopup) {
  105. this.$refs.recordPopup.close();
  106. }
  107. });
  108. },
  109. clickMicBtn() {
  110. if (this.recordStatus == 1) {
  111. this.startRecord();
  112. }
  113. },
  114. startCountDown() {
  115. if (this.countDownTimeout) {
  116. clearTimeout(this.countDownTimeout);
  117. }
  118. this.countDownTimeout = setTimeout(() => {
  119. this.useTime++;
  120. this.countDownTimeout = null;
  121. if (this.useTime >= this.maxDuration * 10) {
  122. this.recorderManager.stop();
  123. } else {
  124. this.startCountDown();
  125. }
  126. }, 99);
  127. },
  128. stopCountDown() {
  129. if (this.countDownTimeout) {
  130. clearTimeout(this.countDownTimeout);
  131. }
  132. },
  133. startRecord() {
  134. uni.authorize({
  135. scope: 'scope.record',
  136. success: () => {
  137. this.recordFlag = true;
  138. this.isRecording = true;
  139. this.recordStatus = 2;
  140. this.recorderManager.start({
  141. ...this.recordOption,
  142. duration: this.recordDuration * 1000,
  143. success: () => {
  144. this.useTime = 0;
  145. this.startCountDown();
  146. if (this.$refs.recordPopup) {
  147. this.$refs.recordPopup.open('bottom');
  148. }
  149. },
  150. fail: () => {
  151. uni.showToast({
  152. title: '录音启动失败',
  153. icon: 'none'
  154. });
  155. }
  156. });
  157. },
  158. fail: () => {
  159. this.recordStatus = 1;
  160. this.recordFlag = false;
  161. uni.showToast({
  162. title: '请授权麦克风权限',
  163. icon: 'none'
  164. });
  165. }
  166. });
  167. },
  168. recorded() {
  169. if (this.recordStatus == 1) {
  170. this.startRecord();
  171. }
  172. if (this.recordStatus == 2) {
  173. this.recorderManager.stop();
  174. }
  175. },
  176. cancelRecord() {
  177. this.recordStatus = 1;
  178. this.recorderManager.stop();
  179. if (this.animationId) {
  180. cancelAnimationFrame(this.animationId);
  181. }
  182. if (this.$refs.recordPopup) {
  183. this.$refs.recordPopup.close();
  184. }
  185. }
  186. }
  187. };
  188. </script>
  189. <style scoped lang="scss">
  190. @keyframes spin {
  191. 0% {
  192. transform: rotate(0deg);
  193. }
  194. 100% {
  195. transform: rotate(360deg);
  196. }
  197. }
  198. .ss-recording-wrapper {
  199. display: inline-flex;
  200. align-items: center;
  201. }
  202. .ss-recording {
  203. display: inline-flex;
  204. align-items: center;
  205. }
  206. .record-dialog {
  207. background-color: #ffffff;
  208. overflow: hidden;
  209. padding: 32rpx;
  210. .record-title {
  211. font-size: 32rpx;
  212. color: #101333;
  213. margin-bottom: 32rpx;
  214. }
  215. .record-animation {
  216. height: 280rpx;
  217. margin-bottom: 32rpx;
  218. position: relative;
  219. .p-center {
  220. position: absolute;
  221. top: 50%;
  222. left: 50%;
  223. transform: translate(-50%, -50%);
  224. }
  225. .mic {
  226. width: 64rpx;
  227. height: 64rpx;
  228. border-radius: 64rpx;
  229. border: 4rpx solid #2943d6;
  230. line-height: 64rpx;
  231. text-align: center;
  232. z-index: 100;
  233. }
  234. @for $i from 1 through 10 {
  235. &.show-bg-#{$i} {
  236. @for $j from 1 through 10 {
  237. .mic-bg {
  238. &.bg-#{$j} {
  239. opacity: if($j < $i, 1, 0);
  240. }
  241. }
  242. }
  243. }
  244. }
  245. .mic-bg {
  246. border: 2rpx solid #2943d6;
  247. transition: opacity 100ms linear;
  248. @for $i from 1 through 10 {
  249. &.bg-#{$i} {
  250. z-index: 100 - $i;
  251. width: 80rpx + $i * 20;
  252. height: 80rpx + $i * 20;
  253. border-radius: 40rpx + $i * 10;
  254. border-color: rgba(41, 67, 214, 1 - ($i * 0.1));
  255. opacity: 0;
  256. }
  257. }
  258. }
  259. }
  260. .record-canvas {
  261. width: 100%;
  262. height: 240rpx;
  263. margin-bottom: 60rpx;
  264. }
  265. .record-buttons {
  266. display: flex;
  267. justify-content: center;
  268. .record-cancel {
  269. border-radius: 50rpx;
  270. border: 1px solid #2943d6;
  271. color: #2943d6;
  272. margin-right: 16rpx;
  273. padding: 0 56rpx;
  274. background-color: #ffffff;
  275. &::after {
  276. background-color: transparent;
  277. }
  278. }
  279. .record-start {
  280. display: flex;
  281. align-items: center;
  282. justify-content: center;
  283. border-radius: 50rpx;
  284. border: 0;
  285. color: #ffffff;
  286. background-color: #2943d6;
  287. padding: 0 32rpx;
  288. width: 340rpx;
  289. .record-stop {
  290. width: 32rpx;
  291. height: 32rpx;
  292. background-color: #ffffff;
  293. margin-right: 16rpx;
  294. border-radius: 6rpx;
  295. }
  296. .record-loading {
  297. margin-right: 8rpx;
  298. animation: spin 3s linear infinite;
  299. }
  300. &::after {
  301. background-color: transparent;
  302. }
  303. }
  304. }
  305. }
  306. </style>