nb-voice-record.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. <template>
  2. <view>
  3. <view class="record-btn" @longpress="startRecord" @touchstart="touchStart" @touchmove="touchMove" @touchend="endRecord" hover-class="record-btn-hover" hover-start-time="200" hover-stay-time="150" :style="[btnStyle, { '--btn-hover-fontcolor': btnHoverFontcolor, '--btn-hover-bgcolor': btnHoverBgcolor }]">
  4. <image v-if="btnIconShow" :src="btnIconSrc" :style="[btnIconStyle]"></image>
  5. <text>{{ btnTextContent }}</text>
  6. </view>
  7. <view class="record-popup" :style="{ '--popup-height': popupHeight, '--popup-width': upx2px(popupMaxWidth), '--popup-bottom': upx2px(popupFixBottom), '--popup-bg-color': popupBgColor }">
  8. <view class="inner-content" v-if="recordPopupShow">
  9. <view class="title">{{ popupTitle }}</view>
  10. <view class="recordTime" v-if="isShowRecordTime">{{'已录时长:' + recordTime + 's'}}</view>
  11. <view class="voice-line-wrap" v-if="recording" :style="{ '--line-height': upx2px(lineHeight), '--line-start-color': lineStartColor, '--line-end-color': lineEndColor }">
  12. <view class="voice-line one"></view>
  13. <view class="voice-line two"></view>
  14. <view class="voice-line three"></view>
  15. <view class="voice-line four"></view>
  16. <view class="voice-line five"></view>
  17. <view class="voice-line six"></view>
  18. <view class="voice-line seven"></view>
  19. <view class="voice-line six"></view>
  20. <view class="voice-line five"></view>
  21. <view class="voice-line four"></view>
  22. <view class="voice-line three"></view>
  23. <view class="voice-line two"></view>
  24. <view class="voice-line one"></view>
  25. </view>
  26. <view class="cancel-icon" v-if="!recording">+</view>
  27. <view class="tips">{{ recording ? popupDefaultTips : popupCancelTips }}</view>
  28. </view>
  29. </view>
  30. </view>
  31. </template>
  32. <script>
  33. var that;
  34. const recorderManager = uni.getRecorderManager();
  35. // #ifdef APP-PLUS
  36. // 引入权限判断
  37. import permision from '../../js_sdk/wa-permission/permission.js';
  38. // #endif
  39. export default {
  40. name: 'nbVoiceRecord',
  41. /**
  42. * 录音交互动效组件
  43. * @property {Object} recordOptions 录音配置
  44. * @property {Object} btnStyle 按钮样式
  45. * @property {Object} btnHoverFontcolor 按钮长按时字体颜色
  46. * @property {String} btnHoverBgcolor 按钮长按时背景颜色
  47. * @property {String} btnDefaultText 按钮初始文字
  48. * @property {String} btnRecordingText 录制时按钮文字
  49. * @property {Boolean} btnIconShow 按钮图标是否显示
  50. * @property {String} btnIconSrc 按钮图标路径
  51. * @property {Object} btnIconStyle 按钮图标路径样式
  52. * @property {Boolean} vibrate 弹窗时是否震动
  53. * @property {String} popupTitle 弹窗顶部文字
  54. * @property {String} popupDefaultTips 录制时弹窗底部提示
  55. * @property {String} popupCancelTips 滑动取消时弹窗底部提示
  56. * @property {String} popupMaxWidth 弹窗展开后宽度
  57. * @property {String} popupMaxHeight 弹窗展开后高度
  58. * @property {String} popupFixBottom 弹窗展开后距底部高度
  59. * @property {String} popupBgColor 弹窗背景颜色
  60. * @property {String} lineHeight 声波高度
  61. * @property {String} lineStartColor 声波波谷时颜色色值
  62. * @property {String} lineEndColor 声波波峰时颜色色值
  63. * @property {Boolean} isShowRecordTime 是否在弹窗中显示录音时长
  64. * @event {Function} startRecord 开始录音回调
  65. * @event {Function} endRecord 结束录音回调
  66. * @event {Function} cancelRecord 滑动取消录音回调
  67. * @event {Function} stopRecord 主动停止录音
  68. */
  69. props: {
  70. recordOptions: {
  71. type: Object,
  72. default () {
  73. return {
  74. duration: 600000,
  75. format: 'mp3'
  76. }; // 请自行查看各端的的支持情况,这里全部使用默认配置
  77. }
  78. },
  79. btnStyle: {
  80. type: Object,
  81. default () {
  82. return {
  83. width: '200rpx',
  84. height: '48rpx',
  85. borderRadius: '20rpx',
  86. backgroundColor: '#3E6EFF',
  87. color: '#fff',
  88. border: '1rpx solid whitesmoke',
  89. permisionState: false
  90. };
  91. }
  92. },
  93. btnIconShow:{
  94. type: Boolean,
  95. default: false
  96. },
  97. btnIconSrc: {
  98. type: String,
  99. default: ''
  100. },
  101. btnIconStyle: {
  102. type: Object,
  103. default () {
  104. return {
  105. width: '30rpx',
  106. height: '30rpx',
  107. marginRight: '10rpx'
  108. };
  109. }
  110. },
  111. btnHoverFontcolor: {
  112. type: String,
  113. default: '#fff' // 颜色名称或16进制色值
  114. },
  115. btnHoverBgcolor: {
  116. type: String,
  117. default: '#3e6ffd' // 颜色名称或16进制色值
  118. },
  119. btnDefaultText: {
  120. type: String,
  121. default: '长按开始录音'
  122. },
  123. btnRecordingText: {
  124. type: String,
  125. default: '录音中'
  126. },
  127. vibrate: {
  128. type: Boolean,
  129. default: true
  130. },
  131. popupTitle: {
  132. type: String,
  133. default: '正在录制音频'
  134. },
  135. popupDefaultTips: {
  136. type: String,
  137. default: '左右滑动后松手完成录音'
  138. },
  139. popupCancelTips: {
  140. type: String,
  141. default: '松手取消录音'
  142. },
  143. popupMaxWidth: {
  144. type: Number,
  145. default: 600 // 单位为rpx
  146. },
  147. popupMaxHeight: {
  148. type: Number,
  149. default: 300 // 单位为rpx
  150. },
  151. popupFixBottom: {
  152. type: Number,
  153. default: 200 // 单位为rpx
  154. },
  155. popupBgColor: {
  156. type: String,
  157. default: 'whitesmoke'
  158. },
  159. lineHeight: {
  160. type: Number,
  161. default: 50 // 单位为rpx
  162. },
  163. lineStartColor: {
  164. type: String,
  165. default: 'royalblue' // 颜色名称或16进制色值
  166. },
  167. lineEndColor: {
  168. type: String,
  169. default: 'indianred' // 颜色名称或16进制色值
  170. },
  171. isShowRecordTime: {
  172. type: Boolean,
  173. default: false // 是否在弹窗中显示录音时长
  174. }
  175. },
  176. data() {
  177. return {
  178. stopStatus: false, // 是否已被父页面通知主动结束录音
  179. btnTextContent: this.btnDefaultText,
  180. startTouchData: {},
  181. popupHeight: '0px', // 这是初始的高度
  182. recording: true, // 录音中
  183. recordPopupShow: false,
  184. recordTimeout: null, // 录音定时器
  185. recordTimeInterval: null, // 录音时长定时器
  186. recordTime: 0, // 实时录音时长
  187. };
  188. },
  189. created() {
  190. that = this;
  191. // 请求权限
  192. this.checkPermission();
  193. recorderManager.onStop(res => {
  194. // 判断是否用户主动结束录音
  195. if (that.recordTimeout !== null) {
  196. // 延时未结束,则是主动结束录音
  197. clearTimeout(that.recordTimeout);
  198. that.recordTimeout = null; // 恢复状态
  199. }
  200. // 结束实时录音时长定时器
  201. clearInterval(that.recordTimeInterval);
  202. that.recordTimeInterval = null;
  203. that.recordTime = 0;
  204. // 继续判断是否为取消录音
  205. if (that.recording) {
  206. that.$emit('endRecord', res);
  207. } else {
  208. // 用户向上滑动,此时松手后响应的是取消录音的回调
  209. that.recording = true; // 恢复状态
  210. that.$emit('cancelRecord');
  211. }
  212. });
  213. recorderManager.onError(err => {
  214. console.log('err:', err);
  215. });
  216. },
  217. computed: {},
  218. methods: {
  219. upx2px(upx) {
  220. return uni.upx2px(upx) + 'px';
  221. },
  222. async checkPermission() {
  223. // #ifdef APP-PLUS
  224. // 先判断os
  225. let os = uni.getSystemInfoSync().osName;
  226. if (os == 'ios') {
  227. this.permisionState = await permision.judgeIosPermission('record');
  228. } else {
  229. this.permisionState = await permision.requestAndroidPermission('android.permission.RECORD_AUDIO');
  230. }
  231. if (this.permisionState !== true && this.permisionState !== 1) {
  232. uni.showToast({
  233. title: '请先授权使用录音',
  234. icon: 'none'
  235. });
  236. return;
  237. }
  238. // #endif
  239. // #ifndef APP-PLUS
  240. uni.authorize({
  241. scope: 'scope.record',
  242. success: () => {
  243. this.permisionState = true;
  244. },
  245. fail() {
  246. uni.showModal({
  247. title: '请求授权使用录音',
  248. content: '录音需要使用您的麦克风,请前往授权',
  249. success: (res) => {
  250. if (res.confirm) {
  251. uni.openSetting(); // 打开地图权限设置
  252. } else if (res.cancel) {
  253. uni.showToast({
  254. title: '如需使用录音,请前往设置授权',
  255. icon: 'none',
  256. duration: 3000
  257. });
  258. }
  259. }
  260. });
  261. }
  262. });
  263. // #endif
  264. },
  265. startRecord() {
  266. if (!this.permisionState) {
  267. this.checkPermission();
  268. return;
  269. }
  270. this.stopStatus = false;
  271. setTimeout(() => {
  272. this.popupHeight = this.upx2px(this.popupMaxHeight);
  273. setTimeout(() => {
  274. this.recordPopupShow = true;
  275. this.btnTextContent = this.btnRecordingText;
  276. if (this.vibrate) {
  277. // #ifdef APP-PLUS
  278. plus.device.vibrate(35);
  279. // #endif
  280. // #ifdef MP-WEIXIN
  281. uni.vibrateShort();
  282. // #endif
  283. }
  284. // 开始录音
  285. recorderManager.start(this.recordOptions);
  286. // 设置定时器
  287. this.recordTimeout = setTimeout(
  288. () => {
  289. // 如果定时器先结束,则说明此时录音时间超限
  290. this.stopRecord(); // 结束录音动画(实际上录音的end回调已经先执行)
  291. this.recordTimeout = null; // 重置
  292. },
  293. this.recordOptions.duration ? this.recordOptions.duration : 600000
  294. );
  295. // 设置定时器显示录音时长
  296. this.recordTimeInterval = setInterval(() => {
  297. this.recordTime++;
  298. }, 1000)
  299. this.$emit('startRecord');
  300. }, 100);
  301. }, 200);
  302. },
  303. endRecord() {
  304. if (this.stopStatus) {
  305. return;
  306. }
  307. this.popupHeight = '0px';
  308. this.recordPopupShow = false;
  309. this.btnTextContent = this.btnDefaultText;
  310. recorderManager.stop();
  311. },
  312. stopRecord() {
  313. // 用法如你录音限制了时间,那么将在结束时强制停止组件的显示
  314. this.endRecord();
  315. this.stopStatus = true;
  316. },
  317. touchStart(e) {
  318. this.startTouchData.clientX = e.changedTouches[0].clientX; //手指按下时的X坐标
  319. this.startTouchData.clientY = e.changedTouches[0].clientY; //手指按下时的Y坐标
  320. },
  321. touchMove(e) {
  322. let touchData = e.touches[0]; //滑动过程中,手指滑动的坐标信息 返回的是Objcet对象
  323. let moveX = touchData.clientX - this.startTouchData.clientX;
  324. let moveY = touchData.clientY - this.startTouchData.clientY;
  325. if (moveY < -50) {
  326. if (this.vibrate && this.recording) {
  327. // #ifdef APP-PLUS
  328. plus.device.vibrate(35);
  329. // #endif
  330. // #ifdef MP-WEIXIN
  331. uni.vibrateShort();
  332. // #endif
  333. }
  334. this.recording = false;
  335. } else {
  336. this.recording = true;
  337. }
  338. }
  339. }
  340. };
  341. </script>
  342. <style lang="scss">
  343. .record-btn {
  344. color: #000;
  345. font-size: 24rpx;
  346. display: flex;
  347. align-items: center;
  348. justify-content: center;
  349. transition: 0.25s all;
  350. border: 1rpx solid whitesmoke;
  351. }
  352. .record-btn-hover {
  353. color: var(--btn-hover-fontcolor) !important;
  354. background-color: var(--btn-hover-bgcolor) !important;
  355. }
  356. .record-popup {
  357. position: absolute;
  358. bottom: var(--popup-bottom);
  359. left: calc(50vw - calc(var(--popup-width) / 2));
  360. z-index: 9;
  361. width: var(--popup-width);
  362. height: var(--popup-height);
  363. display: flex;
  364. align-items: center;
  365. justify-content: center;
  366. border-radius: 10rpx;
  367. box-shadow: 0 5rpx 10rpx 0 rgba(0, 0, 0, 0.2);
  368. background: var(--popup-bg-color);
  369. color: #000;
  370. transition: 0.2s height;
  371. .inner-content {
  372. height: var(--popup-height);
  373. font-size: 24rpx;
  374. display: flex;
  375. flex-direction: column;
  376. align-items: center;
  377. justify-content: space-between;
  378. .title {
  379. font-weight: bold;
  380. padding: 20rpx 0;
  381. }
  382. .recordTime {
  383. padding-bottom: 20rpx;
  384. color: #999;
  385. }
  386. .tips {
  387. color: #999;
  388. padding: 20rpx 0;
  389. }
  390. }
  391. }
  392. .cancel-icon {
  393. width: 100rpx;
  394. height: 100rpx;
  395. display: flex;
  396. align-items: center;
  397. justify-content: center;
  398. color: #fff;
  399. font-size: 44rpx;
  400. line-height: 44rpx;
  401. background-color: pink;
  402. border-radius: 50%;
  403. transform: rotate(45deg);
  404. }
  405. .voice-line-wrap {
  406. display: flex;
  407. align-items: center;
  408. .voice-line {
  409. width: 5rpx;
  410. height: var(--line-height);
  411. border-radius: 3rpx;
  412. margin: 0 5rpx;
  413. }
  414. .one {
  415. animation: wave 0.4s 1s linear infinite alternate;
  416. }
  417. .two {
  418. animation: wave 0.4s 0.9s linear infinite alternate;
  419. }
  420. .three {
  421. animation: wave 0.4s 0.8s linear infinite alternate;
  422. }
  423. .four {
  424. animation: wave 0.4s 0.7s linear infinite alternate;
  425. }
  426. .five {
  427. animation: wave 0.4s 0.6s linear infinite alternate;
  428. }
  429. .six {
  430. animation: wave 0.4s 0.5s linear infinite alternate;
  431. }
  432. .seven {
  433. animation: wave 0.4s linear infinite alternate;
  434. }
  435. }
  436. @keyframes wave {
  437. 0% {
  438. transform: scale(1, 1);
  439. background-color: var(--line-start-color);
  440. }
  441. 100% {
  442. transform: scale(1, 0.2);
  443. background-color: var(--line-end-color);
  444. }
  445. }
  446. </style>