sound-record.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. <template>
  2. <view class="sound-record-wrapper">
  3. <template v-if="type === 'big'">
  4. <view class="progress-box" v-if="file_url&&wavBlob">
  5. <text> {{ currentTime }} </text>
  6. <slider v-model="sliderValue" activeColor="#000000" block-size="12" @change="onSliderChange" />
  7. <text> {{ duration }} </text>
  8. </view>
  9. <view class="sound-item-box">
  10. <view class="sound-item">
  11. <view class="sound-item-svg" @click="handlePlay">
  12. <svg :fill="[wavBlob?'#1D1D1D':'#C3C3C3']" :style="{'width':iconSize,'height':iconSize}">
  13. <use :xlink:href="playing ? '#icon-stop' : '#icon-triangle'"></use>
  14. </svg>
  15. </view>
  16. <text class="sound-item-text" :style="{'color':wavBlob?'#1D1D1D':'#8C8C8C'}">回放</text>
  17. </view>
  18. <view v-if="viewPopup" class="sound-item" :style="{'opacity':disabled?'0.5':'1'}">
  19. <view class="sound-item-svg sound-item-microphone" @click="handleStart">
  20. <svg fill="#ffffff" :style="{'width':iconSize,'height':iconSize}">
  21. <use xlink:href="#icon-microphone"></use>
  22. </svg>
  23. </view>
  24. <text class="sound-item-text">录音</text>
  25. </view>
  26. <view v-else class="sound-item" :style="{'opacity':disabled?'0.5':'1'}">
  27. <view v-if="microphoneStatus" class="sound-item-img" @click="microphone">
  28. <img class="voice-play" src="static/record-ing-hasBg.png" />
  29. </view>
  30. <view v-else class="sound-item-svg sound-item-microphone" @click="microphone">
  31. <svg fill="#ffffff" :style="{'width':iconSize,'height':iconSize}">
  32. <use xlink:href="#icon-microphone"></use>
  33. </svg>
  34. </view>
  35. <text class="sound-item-text">
  36. {{ microphoneStatus ? secondFormatConversion(recordTimes) : '录音' }}
  37. </text>
  38. </view>
  39. <view class="sound-item" :style="{'opacity':disabled?'0.5':'1'}">
  40. <view class="sound-item-svg" :style="{'background-color':type === 'big'?'#f9f8f9':'#ffffff'}"
  41. @click="deleteWav">
  42. <svg :fill="[wavBlob?'#1D1D1D':'#C3C3C3']" :style="{'width':iconSize,'height':iconSize}">
  43. <use xlink:href="#icon-deletefile"></use>
  44. </svg>
  45. </view>
  46. <text v-if="type === 'big'" class="sound-item-text">删除</text>
  47. </view>
  48. </view>
  49. </template>
  50. <template v-else>
  51. <view class="sound-item-box">
  52. <view class="sound-item" :style="{'opacity':disabled?'0.5':'1'}">
  53. <view class="sound-item-svg sound-item-microphone" @click="handleStart">
  54. <svg fill="#ffffff" :style="{'width':iconSize,'height':iconSize}">
  55. <use xlink:href="#icon-microphone"></use>
  56. </svg>
  57. </view>
  58. </view>
  59. <view class="sound-item">
  60. <view class="progress-box" v-if="file_url&&wavBlob">
  61. <text> {{ currentTime }} </text>
  62. <slider v-model="sliderValue" activeColor="#000000" block-size="12" @change="onSliderChange" />
  63. <text> {{ duration }} </text>
  64. </view>
  65. <text class="sound-item-text" v-else>
  66. {{ secondFormatConversion(recordTimes) }}
  67. </text>
  68. </view>
  69. <view class="sound-item">
  70. <view class="sound-item-svg" style="background-color:#ffffff" @click="handlePlay">
  71. <svg :fill="[wavBlob?'#1D1D1D':'#C3C3C3']" :style="{'width':iconSize,'height':iconSize}">
  72. <use :xlink:href="playing ? '#icon-stop' : '#icon-triangle'"></use>
  73. </svg>
  74. </view>
  75. </view>
  76. <view class="sound-item" :style="{'opacity':disabled?'0.5':'1'}">
  77. <view class="sound-item-svg" :style="{'background-color':type === 'big'?'#f9f8f9':'#ffffff'}"
  78. @click="deleteWav">
  79. <svg :fill="[wavBlob?'#1D1D1D':'#C3C3C3']" :style="{'width':iconSize,'height':iconSize}">
  80. <use xlink:href="#icon-deletefile"></use>
  81. </svg>
  82. </view>
  83. </view>
  84. </view>
  85. </template>
  86. <uni-popup v-show="viewPopup" ref="popup" :mask-click="false">
  87. <view class="canvas-area">
  88. <text class="canvas-title">录音中...
  89. <text>{{ secondFormatConversion(recordTimes) }}</text>
  90. </text>
  91. <canvas canvas-id="recordCanvas" id="recordCanvas" type="2d" ref="record"></canvas>
  92. <view class="sound-item-svg" @click="handleStop">
  93. <svg fill="#ffffff">
  94. <use xlink:href="#icon-audio-stop"></use>
  95. </svg>
  96. </view>
  97. </view>
  98. </uni-popup>
  99. </view>
  100. </template>
  101. <script>
  102. import Recorder from 'js-audio-recorder'; // 录音插件
  103. import {
  104. GetStaticResources,
  105. GetFileStoreInfo
  106. } from '@/api/api.js';
  107. import {
  108. secondFormatConversion
  109. } from '@/utils/transform';
  110. export default {
  111. name: 'sound-record',
  112. props: {
  113. wavBlob: {
  114. type: String,
  115. default: '',
  116. },
  117. itemIndex: {
  118. type: Number,
  119. default: null,
  120. },
  121. type: {
  122. type: String,
  123. default: 'big',
  124. },
  125. notOnTheCurPage: {
  126. type: Boolean,
  127. default: false,
  128. },
  129. disabled: {
  130. type: Boolean,
  131. default: false,
  132. },
  133. viewPopup: {
  134. type: Boolean,
  135. default: true,
  136. }
  137. },
  138. data() {
  139. return {
  140. secondFormatConversion,
  141. microphoneStatus: false, //是否在录音
  142. recorder: new Recorder({
  143. sampleBits: 16, // 采样位数,支持 8 或 16,默认是16
  144. sampleRate: 16000, // 采样率,支持 11025、16000、22050、24000、44100、48000,根据浏览器默认值
  145. numChannels: 1, // 声道,支持 1 或 2, 默认是1
  146. }),
  147. timer: null, // 计时器
  148. recordTimes: 0,
  149. playtime: 0, //播放时间
  150. drawRecordId: null,
  151. audio: uni.createInnerAudioContext(),
  152. playing: false,
  153. file_url: '',
  154. duration: '00:00', //音频时长
  155. currentTime: '00:00', //当前时长
  156. sliderValue: 0,
  157. };
  158. },
  159. computed: {
  160. iconSize() {
  161. return this.type === 'big' ? 24 : 16;
  162. },
  163. },
  164. watch: {
  165. wavBlob: {
  166. handler(val) {
  167. if (!val) return;
  168. this.sliderValue = 0;
  169. GetFileStoreInfo({
  170. file_id: val
  171. }).then(({
  172. file_url,
  173. media_duration
  174. }) => {
  175. this.file_url = file_url;
  176. this.recordTime = media_duration;
  177. this.recordTimes = JSON.parse(JSON.parse(media_duration));
  178. this.audio.src = file_url;
  179. this.audio.onCanplay(() => {
  180. if (this.audio.duration !== 0) {
  181. clearInterval(this.intervalID);
  182. this.duration = this.secondFormatConversion(this.audio.duration) // 总时长
  183. this.currentTime = this.secondFormatConversion(this.audio.currentTime) // 当前时长
  184. }
  185. })
  186. this.audio.onTimeUpdate(() => {
  187. this.currentTime = this.secondFormatConversion(this.audio.currentTime);
  188. if (!this.dragging) { // 避免拖动滑块时自动更新
  189. this.sliderValue = (this.audio.currentTime / this.audio.duration) * 100;
  190. }
  191. })
  192. });
  193. },
  194. immediate: true,
  195. },
  196. },
  197. methods: {
  198. //不弹窗录音
  199. microphone() {
  200. if (this.disabled) return;
  201. if (this.microphoneStatus) {
  202. this.recorder.stop();
  203. this.sliderValue = 0;
  204. clearInterval(this.timer); //清除计时器
  205. this.microphoneStatus = false;
  206. // 录音结束,获取录音数据
  207. let wav = this.recorder.getWAVBlob(); // 获取 WAV 数据
  208. let reader = new window.FileReader();
  209. reader.readAsDataURL(wav);
  210. reader.onloadend = () => {
  211. let MethodName = 'file_store_manager-SaveFileByteBase64Text';
  212. let data = {
  213. base64_text: reader.result.replace('data:audio/wav;base64,', ''),
  214. file_suffix_name: 'mp3',
  215. };
  216. GetStaticResources(MethodName, data).then((res) => {
  217. if (res.status === 1) {
  218. this.$emit('update:wavBlob', res.file_id);
  219. this.$emit('setAudioId', res.file_id);
  220. this.audio.src = res.file_url;
  221. // console.log('文件', res);
  222. }
  223. });
  224. };
  225. } else {
  226. this.$emit('update:wavBlob', '');
  227. //获取录音权限
  228. Recorder.getPermission().then(() => {
  229. // 开始录音
  230. this.recorder.start().then(() => {
  231. this.microphoneStatus = true;
  232. this.sliderValue = 0;
  233. this.recordTimes = 0;
  234. clearInterval(this.timer);
  235. this.timer = setInterval(() => {
  236. this.recordTimes += 1;
  237. // console.log(this.recordTimes);
  238. }, 1000);
  239. // console.log('开始录音');
  240. })
  241. }, (error) => {
  242. console.log(`${error.name} : ${error.message}`)
  243. })
  244. }
  245. },
  246. // 开始录音
  247. handleStart() {
  248. if (this.disabled) return;
  249. //获取录音权限
  250. Recorder.getPermission().then(() => {
  251. this.recorder.start().then(() => {
  252. this.microphoneStatus = true;
  253. this.sliderValue = 0;
  254. //打开录音弹窗
  255. if (this.notOnTheCurPage) {
  256. uni.$emit('openOrClosePopup', true);
  257. } else {
  258. this.$refs.popup.open('center');
  259. }
  260. this.drawRecord();
  261. this.recordTimes = 0;
  262. clearInterval(this.timer);
  263. this.timer = setInterval(() => {
  264. this.recordTimes += 1;
  265. }, 1000);
  266. // console.log('开始录音');
  267. })
  268. }, (error) => {
  269. console.log(`${error.name} : ${error.message}`)
  270. })
  271. },
  272. // 停止录音
  273. handleStop() {
  274. this.recorder.stop();
  275. this.sliderValue = 0;
  276. //取消录音动画
  277. this.drawRecordId && cancelAnimationFrame(this.drawRecordId);
  278. this.drawRecordId = null;
  279. clearInterval(this.timer); //清除计时器
  280. this.microphoneStatus = false;
  281. //关闭录音弹窗
  282. if (!this.notOnTheCurPage) {
  283. this.$refs.popup.close();
  284. }
  285. // 录音结束,获取录音数据
  286. let wav = this.recorder.getWAVBlob(); // 获取 WAV 数据
  287. let reader = new window.FileReader();
  288. reader.readAsDataURL(wav);
  289. reader.onloadend = () => {
  290. let MethodName = 'file_store_manager-SaveFileByteBase64Text';
  291. let data = {
  292. base64_text: reader.result.replace('data:audio/wav;base64,', ''),
  293. file_suffix_name: 'mp3',
  294. };
  295. GetStaticResources(MethodName, data).then((res) => {
  296. if (res.status === 1) {
  297. this.$emit('update:wavBlob', res.file_id);
  298. this.$emit('setAudioId', res.file_id);
  299. this.audio.src = res.file_url;
  300. // console.log('文件', res);
  301. }
  302. });
  303. };
  304. },
  305. // 播放录音
  306. handlePlay() {
  307. if (this.wavBlob) {
  308. let totalTime = JSON.parse(JSON.stringify(this.recordTime));
  309. if (this.playing) {
  310. this.audio.pause();
  311. clearInterval(this.timer);
  312. } else {
  313. this.audio.play();
  314. clearInterval(this.timer);
  315. // 启动定时器,每秒更新一次进度条位置
  316. this.timer = setInterval(() => {
  317. if (this.audio.currentTime > 0 && this.audio.currentTime < this.audio.duration) {
  318. this.sliderValue = (this.audio.currentTime / this.audio.duration) * 100;
  319. } else {
  320. this.playing = !this.playing;
  321. clearInterval(this.timer);
  322. }
  323. }, 1000);
  324. }
  325. this.playing = !this.playing;
  326. }
  327. },
  328. // 删除录音
  329. deleteWav() {
  330. if (this.disabled) return;
  331. this.audio.pause();
  332. this.playing = false;
  333. this.microphoneStatus = false;
  334. this.playtime = 0;
  335. this.recordTimes = 0;
  336. this.file_url = '';
  337. clearInterval(this.timer);
  338. this.$emit('update:wavBlob', '');
  339. this.$emit('setAudioId', '');
  340. },
  341. // 录音波浪图
  342. drawRecord() {
  343. // 用requestAnimationFrame稳定60fps绘制
  344. this.drawRecordId = requestAnimationFrame(this.drawRecord);
  345. this.drawWave({
  346. dataArray: this.recorder.getRecordAnalyseData(),
  347. });
  348. },
  349. // 绘制波形图
  350. drawWave({
  351. dataArray
  352. }) {
  353. const ctx = uni.createCanvasContext('recordCanvas');
  354. const bufferLength = dataArray.length;
  355. ctx.rect(0, 0, 200, 64); //绘制矩形
  356. ctx.setFillStyle("#000000"); // 填充背景色
  357. // 设定波形,绘制颜色
  358. ctx.setLineWidth(2);
  359. ctx.setStrokeStyle("#ffffff");
  360. ctx.beginPath();
  361. var sliceWidth = (200 * 1.0) / bufferLength; // 一个点占多少位置,共有bufferLength个点要绘制
  362. var x = 0; // 绘制点的x轴位置
  363. for (var i = 0; i < bufferLength; i++) {
  364. var v = dataArray[i] / 128.0;
  365. var y = (v * 64) / 2;
  366. if (i === 0) {
  367. // 第一个点
  368. ctx.moveTo(x, y);
  369. } else {
  370. // 剩余的点
  371. ctx.lineTo(x, y);
  372. }
  373. // 依次平移,绘制所有点
  374. x += sliceWidth;
  375. }
  376. ctx.lineTo(200, 64 / 2);
  377. ctx.stroke();
  378. ctx.draw();
  379. },
  380. onSliderChange(event) {
  381. const value = event.detail.value;
  382. if (this.audio && this.audio.duration > 0) {
  383. // 验证 value 是否是有效的百分比值(0-100之间)
  384. if (isNaN(value) || value < 0 || value > 100) {
  385. // console.error("Invalid value:", value);
  386. return;
  387. }
  388. // 计算新的位置
  389. const newPosition = (value / 100) * this.audio.duration;
  390. // 验证 newPosition 是否是有效的数字,不是 NaN,并且在有效范围内
  391. if (!isNaN(newPosition) && newPosition >= 0 && newPosition <= this.audio.duration) {
  392. // 设置音频播放位置
  393. this.audio.seek(newPosition);
  394. if (!this.dragging) { // 避免拖动滑块时自动更新
  395. this.sliderValue = value;
  396. }
  397. } else {
  398. console.error("Invalid newPosition:", newPosition);
  399. }
  400. }
  401. },
  402. }
  403. };
  404. </script>
  405. <style lang="scss" scoped>
  406. .sound-record-wrapper {
  407. display: flex;
  408. flex-direction: column;
  409. margin-top: 32rpx;
  410. .progress-box {
  411. display: flex;
  412. justify-content: space-between;
  413. align-items: center;
  414. background-color: $uni-bg-color-grey;
  415. padding: 8rpx 16rpx;
  416. border-radius: 80rpx;
  417. font-size: 24rpx;
  418. column-gap: 26rpx;
  419. margin-bottom: 32rpx;
  420. slider {
  421. flex: 1;
  422. margin: 0;
  423. }
  424. }
  425. .sound-item-box {
  426. display: flex;
  427. align-items: center;
  428. justify-content: space-between;
  429. column-gap: 8rpx;
  430. .sound-item {
  431. text-align: center;
  432. .progress-box {
  433. margin: 0;
  434. slider {
  435. width: 100rpx;
  436. flex: 1;
  437. margin: 0;
  438. }
  439. }
  440. .sound-item-svg {
  441. padding: 20rpx;
  442. border-radius: 160rpx;
  443. background-color: $uni-bg-color-grey;
  444. display: flex;
  445. justify-content: center;
  446. align-items: center;
  447. }
  448. .sound-item-microphone {
  449. background-color: #7346d3;
  450. }
  451. .sound-item-img {
  452. display: flex;
  453. .voice-play {
  454. width: 84rpx;
  455. height: 84rpx;
  456. }
  457. }
  458. .sound-item-text {
  459. font-size: 24rpx;
  460. line-height: 40rpx;
  461. }
  462. }
  463. }
  464. .canvas-area {
  465. width: 308rpx;
  466. height: 312rpx;
  467. text-align: center;
  468. background-color: rgba(0, 0, 0, 0.8);
  469. border-radius: 32rpx;
  470. position: absolute;
  471. left: 50%;
  472. top: 50%;
  473. transform: translate(-50%, -50%);
  474. padding: 48rpx 32rpx;
  475. z-index: 111;
  476. .canvas-title {
  477. color: #ffffff;
  478. font-size: 28rpx;
  479. }
  480. canvas {
  481. width: 300rpx;
  482. height: 92rpx;
  483. margin: 46rpx 0;
  484. }
  485. .sound-item-svg {
  486. width: 96rpx;
  487. height: 96rpx;
  488. border-radius: 160rpx;
  489. background-color: #7346d3;
  490. display: flex;
  491. justify-content: center;
  492. align-items: center;
  493. margin: 0 auto;
  494. svg {
  495. width: 44rpx;
  496. height: 44rpx;
  497. }
  498. }
  499. }
  500. }
  501. </style>