right.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  1. <template>
  2. <div class="o_box" ref="obox">
  3. <div class="o_top">
  4. </div>
  5. <div class="o_content">
  6. <div class="type_box" :style="{width: oWidth}">
  7. {{ getType(cjson) }}
  8. </div>
  9. <div class="word_box" v-if="cjson.type == 'word' || cjson.type == 'QA'" ref="wb">
  10. <div class="word_bbox" :style="{maxHeight: oheight}">
  11. <img class="word_img" :src="cjson.img" alt="" v-if="cjson.img" @click="previewImg(cjson.img)">
  12. <div class="word_content" v-html="cjson.content">
  13. </div>
  14. </div>
  15. </div>
  16. <div class="sentence_box" v-if="cjson.type == 'sentence'" ref="wb">
  17. <span v-html="cjson.content"></span>
  18. </div>
  19. <div class="word_box" v-if="cjson.type == 'theme'" ref="wb" style="max-height: calc(100% - 95px);">
  20. <div class="word_bbox" :style="{maxHeight: oheight}">
  21. <div class="word_content" v-html="cjson.content"></div>
  22. <div class="word_content2" v-html="cjson.content2" v-if="cjson.content2"></div>
  23. </div>
  24. </div>
  25. <div class="tips_box" v-if="cjson.type == 'theme' && !isRecord && !LuAudioUrl">提示:准备完成后,点击话筒开始录音</div>
  26. <div class="time_box" v-if="cjson.type == 'theme' && isRecord">
  27. <span>倒计时</span>
  28. <span>{{ Times.min }}:{{ Times.secode }}</span>
  29. </div>
  30. </div>
  31. <div class="o_bottom" v-loading="isloading">
  32. <div class="audio" v-if="!LuAudioUrl">
  33. <img v-if="!isRecord" src="../../../assets/icon/englishVoice/start_aduio.png" alt="" @click="startRecorder">
  34. <img v-else src="../../../assets/icon/englishVoice/stop_audio.png" alt="" @click="startRecorder">
  35. </div>
  36. <div class="audio_word" v-if="!LuAudioUrl">
  37. <span v-if="!isRecord">点击话筒开始录音</span>
  38. <span v-else>点击话筒结束录音</span>
  39. </div>
  40. <div v-if="LuAudioUrl" class="audio_b">
  41. <mini-audio :audio-source="LuAudioUrl" class="audio_class"></mini-audio>
  42. </div>
  43. <div v-if="LuAudioUrl" class="audio_rerecord" @click="restart()">
  44. <span>录音</span>
  45. </div>
  46. <div class="audio_index" v-if="!isRecord">
  47. <div class="audio_index_last" :class="{ disabled: checkType == 0 }" @click="checkIndex('-1')">
  48. <img src="../../../assets/icon/englishVoice/coin.png" alt="">
  49. </div>
  50. <div class="audio_index_content">
  51. <span>{{ checkType + 1 }}</span>
  52. <span>/</span>
  53. <span>{{ checkJson.length }}</span>
  54. </div>
  55. <div class="audio_index_next" :class="{ disabled: checkType == (checkJson.length - 1) }" @click="checkIndex('1')">
  56. <img src="../../../assets/icon/englishVoice/coin.png" alt="">
  57. </div>
  58. </div>
  59. <div v-else class="audio_ing">
  60. <span>正在录音...</span>
  61. </div>
  62. </div>
  63. </div>
  64. </template>
  65. <script>
  66. import Recorder from "js-audio-recorder";
  67. const lamejs = require("lamejs");
  68. const recorder = new Recorder({
  69. sampleBits: 16, // 采样位数,支持 8 或 16,默认是16
  70. sampleRate: 48000, // 采样率,支持 11025、16000、22050、24000、44100、48000,根据浏览器默认值,我的chrome是48000
  71. numChannels: 1, // 声道,支持 1 或 2, 默认是1
  72. // compiling: false,(0.x版本中生效,1.x增加中) // 是否边录边转换,默认是false
  73. });
  74. // 绑定事件-打印的是当前录音数据
  75. recorder.onprogress = function (params) {
  76. // console.log('--------------START---------------')
  77. // console.log('录音时长(秒)', params.duration);
  78. // console.log('录音大小(字节)', params.fileSize);
  79. // console.log('录音音量百分比(%)', params.vol);
  80. // console.log('当前录音的总数据([DataView, DataView...])', params.data);
  81. // console.log('--------------END---------------')
  82. };
  83. export default {
  84. props: {
  85. checkJson: {
  86. type: Array,
  87. },
  88. checkType: {
  89. type: Number,
  90. },
  91. work: {
  92. type: Array
  93. }
  94. },
  95. data() {
  96. return {
  97. cjson: {},
  98. LuAudioUrl: "",
  99. isRecord: false,
  100. isPlayerRecord: false,
  101. isloading: false,
  102. oheight: 'auto',
  103. oWidth: '500px',
  104. calcTimer: null,
  105. totalSeconds: 0,
  106. }
  107. },
  108. computed: {
  109. // oheight: function() {
  110. // // 获取父元素
  111. // var parentElement = this.$refs['wb'];
  112. // // 计算父元素的高度
  113. // var parentHeight = parentElement.offsetHeight;
  114. // return parentHeight + 'px'
  115. // }
  116. getType() {
  117. return function(json) {
  118. if(json.type == 'word'){
  119. return '单词/词组'
  120. }else if(json.type == 'QA'){
  121. return '问答题目'
  122. }else if(json.type == 'sentence'){
  123. return '句子/短文'
  124. }else if(json.type == 'theme'){
  125. return '主题陈述'
  126. }
  127. };
  128. },
  129. Times() {
  130. let min = this.totalSeconds ? Math.floor(this.totalSeconds / 60) : 0
  131. let secode = this.totalSeconds ? this.totalSeconds % 60 : 0
  132. let time = {
  133. min: min >= 10 ? min : '0' + min,
  134. secode: secode >= 10 ? secode : '0' + secode
  135. }
  136. return time;
  137. },
  138. },
  139. watch: {
  140. checkType: {
  141. handler: function (newVal, oldVal) {
  142. this.cjson = JSON.parse(JSON.stringify(this.checkJson[newVal]));
  143. this.LuAudioUrl = this.work[newVal] ? JSON.parse(JSON.stringify(this.work[newVal])) : ''
  144. this.oheight = 'auto'
  145. this.oWidth = '500px'
  146. const images = this.$refs['obox'].querySelectorAll('img');
  147. let loadedCount = 0;
  148. // if(images.length){
  149. // images.forEach((image) => {
  150. // image.addEventListener('load', () => {
  151. // loadedCount++;
  152. // if (loadedCount === images.length) {
  153. // this.calculateParentHeight()
  154. // }
  155. // });
  156. // });
  157. // }else{
  158. this.calculateParentHeight()
  159. // }
  160. },
  161. deep: true,
  162. },
  163. },
  164. methods: {
  165. previewImg(url) {
  166. this.$hevueImgPreview(url);
  167. },
  168. restart() {
  169. this.LuAudioUrl = ""
  170. setTimeout(() => {
  171. this.startRecorder()
  172. }, 500);
  173. },
  174. checkIndex(type) {
  175. if (type == '1') {
  176. if (this.checkType == (this.checkJson.length - 1)) {
  177. return;
  178. }
  179. this.$emit('setType', this.checkType + 1)
  180. } else {
  181. if (this.checkType == 0) {
  182. return;
  183. }
  184. this.$emit('setType', this.checkType - 1)
  185. }
  186. },
  187. // 开始录音
  188. startRecorder() {
  189. let _this = this;
  190. if (!_this.isRecord) {
  191. recorder.destroy(); // 销毁录音
  192. _this.isRecord = true;
  193. if(this.cjson.type == 'theme'){
  194. this.setSecodes()
  195. }
  196. recorder.start().then(
  197. () => { },
  198. (error) => {
  199. _this.isRecord = false;
  200. // _this.$message.error(`${error.name} : ${error.message}`);
  201. _this.$message.error(`没有找到可使用的麦克风,或者您没有允许此网页使用麦克风`);
  202. // 出错了
  203. console.log(`${error.name} : ${error.message}`);
  204. if(_this.calcTimer){
  205. clearInterval(_this.calcTimer)
  206. _this.calcTimer = null;
  207. }
  208. }
  209. );
  210. } else {
  211. if(_this.calcTimer){
  212. clearInterval(_this.calcTimer)
  213. _this.calcTimer = null;
  214. }
  215. _this.isRecord = false;
  216. recorder.stop(); // 结束录音
  217. this.getMp3Data()
  218. }
  219. },
  220. // 录音播放
  221. playRecorder() {
  222. if (!recorder.fileSize) {
  223. return;
  224. }
  225. if (!this.isPlayerRecord) {
  226. this.isPlayerRecord = true;
  227. recorder.play();
  228. } else {
  229. this.isPlayerRecord = false;
  230. recorder.stopPlay(); // 停止录音播放
  231. }
  232. recorder.onplayend = () => {
  233. this.isPlayerRecord = false;
  234. console.log("onplayend");
  235. };
  236. },
  237. /**
  238. * 文件格式转换 wav-map3
  239. * */
  240. getMp3Data() {
  241. if (!recorder.fileSize) {
  242. this.$message.error("请录音后在上传语音");
  243. return;
  244. }
  245. const mp3Blob = this.convertToMp3(recorder.getWAV());
  246. let audioFile = this.dataURLtoAudio(mp3Blob, "mp3");
  247. console.log(audioFile);
  248. this.beforeUpload1(audioFile, 3);
  249. // recorder.download(mp3Blob, "recorder", "mp3");
  250. },
  251. convertToMp3(wavDataView) {
  252. // 获取wav头信息
  253. const wav = lamejs.WavHeader.readHeader(wavDataView); // 此处其实可以不用去读wav头信息,毕竟有对应的config配置
  254. const { channels, sampleRate } = wav;
  255. const mp3enc = new lamejs.Mp3Encoder(channels, sampleRate, 128);
  256. // 获取左右通道数据
  257. const result = recorder.getChannelData();
  258. const buffer = [];
  259. const leftData =
  260. result.left &&
  261. new Int16Array(result.left.buffer, 0, result.left.byteLength / 2);
  262. const rightData =
  263. result.right &&
  264. new Int16Array(result.right.buffer, 0, result.right.byteLength / 2);
  265. const remaining = leftData.length + (rightData ? rightData.length : 0);
  266. const maxSamples = 1152;
  267. for (let i = 0; i < remaining; i += maxSamples) {
  268. const left = leftData.subarray(i, i + maxSamples);
  269. let right = null;
  270. let mp3buf = null;
  271. if (channels === 2) {
  272. right = rightData.subarray(i, i + maxSamples);
  273. mp3buf = mp3enc.encodeBuffer(left, right);
  274. } else {
  275. mp3buf = mp3enc.encodeBuffer(left);
  276. }
  277. if (mp3buf.length > 0) {
  278. buffer.push(mp3buf);
  279. }
  280. }
  281. const enc = mp3enc.flush();
  282. if (enc.length > 0) {
  283. buffer.push(enc);
  284. }
  285. return new Blob(buffer, { type: "audio/mp3" });
  286. },
  287. dataURLtoAudio(blob, filename) {
  288. return new File([blob], filename, { type: "audio/mp3" });
  289. },
  290. beforeUpload1(event, type) {
  291. this.isloading = true
  292. var file;
  293. if (type == 3) {
  294. file = event;
  295. } else {
  296. file = event.target.files[0];
  297. }
  298. var credentials = {
  299. accessKeyId: "AKIATLPEDU37QV5CHLMH",
  300. secretAccessKey: "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",
  301. }; //秘钥形式的登录上传
  302. window.AWS.config.update(credentials);
  303. window.AWS.config.region = "cn-northwest-1"; //设置区域
  304. var bucket = new window.AWS.S3({ params: { Bucket: "ccrb" } }); //选择桶
  305. var _this = this;
  306. if (file) {
  307. var params = {
  308. Key:
  309. file.name.split(".")[0] +
  310. new Date().getTime() +
  311. "." +
  312. file.name.split(".")[file.name.split(".").length - 1],
  313. ContentType: file.type,
  314. Body: file,
  315. "Access-Control-Allow-Credentials": "*",
  316. ACL: "public-read",
  317. }; //key可以设置为桶的相抵路径,Body为文件, ACL最好要设置
  318. var options = {
  319. partSize: 2048 * 1024 * 1024,
  320. queueSize: 2,
  321. leavePartsOnError: true,
  322. };
  323. bucket
  324. .upload(params, options)
  325. .on("httpUploadProgress", function (evt) {
  326. //这里可以写进度条
  327. // console.log("Uploaded : " + parseInt((evt.loaded * 80) / evt.total) + '%');
  328. // _this.progress = parseInt((evt.loaded * 80) / evt.total);
  329. })
  330. .send(function (err, data) {
  331. _this.isloading = false
  332. // _this.progress = 100;
  333. if (err) {
  334. var a = _this.$refs.upload1.uploadFiles;
  335. a.splice(a.length - 1, a.length);
  336. _this.$message.error("上传失败");
  337. } else {
  338. if (type == 3) {
  339. _this.LuAudioUrl = data.Location;
  340. _this.$emit('setWork', _this.LuAudioUrl, _this.checkType)
  341. }
  342. console.log(data.Location);
  343. }
  344. });
  345. }
  346. },
  347. calculateParentHeight() {
  348. this.$nextTick(() => {
  349. setTimeout(() => {
  350. // 获取父元素
  351. var parentElement = this.$refs['wb'];
  352. // 计算父元素的高度
  353. // var parentHeight = parentElement.offsetHeight + 1;
  354. var parentWidth = parentElement.offsetWidth;
  355. // this.oheight = parentHeight + 'px'
  356. this.oWidth = parentWidth + 'px'
  357. }, 50);
  358. });
  359. },
  360. setSecodes(){
  361. this.totalSeconds = this.checkJson[this.checkType].oTime * 60
  362. // this.totalSeconds = 10
  363. this.calcTimer = setInterval(() => {
  364. if (this.totalSeconds > 0) {
  365. this.totalSeconds--;
  366. } else {
  367. clearInterval(this.calcTimer);
  368. this.calcTimer = null
  369. this.startRecorder()
  370. console.log("倒计时结束"); // 输出日志
  371. }
  372. }, 1000);
  373. }
  374. },
  375. mounted() {
  376. this.cjson = JSON.parse(JSON.stringify(this.checkJson[this.checkType]));
  377. this.LuAudioUrl = this.work[this.checkType] ? JSON.parse(JSON.stringify(this.work[this.checkType])) : ''
  378. },
  379. }
  380. </script>
  381. <style scoped>
  382. .o_box {
  383. width: 100%;
  384. height: 100%;
  385. background-image: url('../../../assets/icon/env_background.png');
  386. background-size: cover;
  387. }
  388. .o_top {
  389. height: 65px;
  390. position: absolute;
  391. }
  392. .o_content {
  393. height: calc(100% - 185px);
  394. display: flex;
  395. align-items: center;
  396. justify-content: center;
  397. overflow: hidden;
  398. flex-direction: column;
  399. }
  400. .o_bottom {
  401. height: 185px;
  402. display: flex;
  403. flex-direction: column;
  404. align-items: center;
  405. justify-content: center;
  406. }
  407. .word_box {
  408. min-width: 500px;
  409. max-width: 70%;
  410. background: #fff;
  411. border-radius: 10px;
  412. position: relative;
  413. /* max-height: calc(100% - 40px); */
  414. max-height: calc(100% - 70px);
  415. /* overflow: auto; */
  416. }
  417. .tips_box{
  418. margin-top: 30px;
  419. color: #727272;
  420. }
  421. .word_box > .word_bbox {
  422. width: 100%;
  423. position: relative;
  424. z-index: 999;
  425. max-height: 100%;
  426. overflow: auto;
  427. }
  428. .sentence_box {
  429. background: #e0e0e04d;
  430. min-width: 500px;
  431. max-width: 70%;
  432. border-radius: 15px;
  433. /* max-height: 100%; */
  434. overflow: auto;
  435. padding: 15px;
  436. font-size: 16px;
  437. color: #000;
  438. line-height: 20px;
  439. word-break: break-word;
  440. white-space: pre-line;
  441. max-height: calc(100% - 80px);
  442. }
  443. .word_box::before {
  444. content: '';
  445. position: absolute;
  446. width: 100%;
  447. height: 100%;
  448. display: block;
  449. box-shadow: 0 0 4px 4px #1d39830d;
  450. border-radius: 10px;
  451. z-index: 2;
  452. background: #fff;
  453. }
  454. .word_box::after {
  455. content: '';
  456. position: absolute;
  457. width: 100%;
  458. height: 100%;
  459. background: #fff;
  460. display: block;
  461. box-shadow: 0 0 4px 4px #1d39830d;
  462. border-radius: 10px;
  463. z-index: 1;
  464. top: 15px;
  465. left: 15px;
  466. }
  467. .word_box > .word_bbox>.word_img {
  468. width: calc(100% - 30px);
  469. max-height: 300px;
  470. z-index: 999;
  471. position: relative;
  472. margin: 15px auto;
  473. display: block;
  474. border-radius: 10px;
  475. cursor: pointer;
  476. object-fit: contain;
  477. }
  478. .word_box > .word_bbox>.word_content {
  479. position: relative;
  480. z-index: 999;
  481. text-align: center;
  482. font-size: 36px;
  483. margin: 15px;
  484. font-weight: bold;
  485. color: #000;
  486. width: calc(100% - 30px);
  487. word-break: break-word;
  488. white-space: pre-line;
  489. }
  490. .word_box > .word_bbox>.word_content2 {
  491. position: relative;
  492. z-index: 999;
  493. text-align: left;
  494. font-size: 16px;
  495. margin: 15px;
  496. color: #727272;
  497. width: calc(100% - 30px);
  498. word-break: break-word;
  499. white-space: pre-line;
  500. /* margin-top: 10px; */
  501. }
  502. .o_bottom .audio {
  503. display: flex;
  504. align-items: center;
  505. justify-content: center;
  506. }
  507. .o_bottom .audio>img {
  508. width: 75px;
  509. height: 75px;
  510. cursor: pointer;
  511. }
  512. .o_bottom .audio_word {
  513. display: flex;
  514. align-items: center;
  515. justify-content: center;
  516. color: #00000099;
  517. font-size: 16px;
  518. margin: 10px 0 8px;
  519. }
  520. .o_bottom .audio_index {
  521. display: flex;
  522. align-items: center;
  523. justify-content: center;
  524. }
  525. .audio_index_last,
  526. .audio_index_next {
  527. height: 40px;
  528. width: 40px;
  529. background: #3681fc;
  530. border-radius: 50%;
  531. display: flex;
  532. align-items: center;
  533. justify-content: center;
  534. cursor: pointer;
  535. }
  536. .audio_index_last>img,
  537. .audio_index_next>img {
  538. width: 15px;
  539. height: auto;
  540. }
  541. .audio_index_last.disabled,
  542. .audio_index_next.disabled {
  543. opacity: .6;
  544. }
  545. .audio_index_last>img {
  546. transform: rotate(180deg);
  547. }
  548. .audio_index_last {
  549. margin-right: 20px;
  550. }
  551. .audio_index_content {
  552. color: #000;
  553. font-size: 16px;
  554. }
  555. .audio_index_next {
  556. margin-left: 20px;
  557. }
  558. .audio_ing {
  559. color: #EE3E3E;
  560. margin-top: 25px;
  561. font-size: 12px;
  562. display: flex;
  563. align-items: center;
  564. justify-content: center;
  565. }
  566. .audio_b {
  567. display: flex;
  568. align-items: center;
  569. justify-content: center;
  570. margin-bottom: 15px;
  571. }
  572. .audio_rerecord {
  573. display: flex;
  574. align-items: center;
  575. justify-content: center;
  576. margin-bottom: 15px;
  577. }
  578. .audio_rerecord>span {
  579. display: flex;
  580. border: 1px solid #3981FA;
  581. align-items: center;
  582. color: #3981FA;
  583. padding: 5px 10px;
  584. border-radius: 5px;
  585. cursor: pointer;
  586. }
  587. .audio_rerecord>span::before {
  588. content: '';
  589. width: 15px;
  590. height: 15px;
  591. background: url('../../../assets/icon/englishVoice/restart.png');
  592. display: block;
  593. background-size: 100% 100%;
  594. margin-right: 5px;
  595. }
  596. .audio_class {
  597. background: #3680fb !important;
  598. margin: 0 !important;
  599. }
  600. .audio_b >>> .vueAudioBetter span:before{
  601. color: #fff;
  602. }
  603. .audio_class>>>.slider .process {
  604. background: #000;
  605. }
  606. .audio_b >>> .vueAudioBetter .iconfont:active{
  607. position: unset !important;
  608. }
  609. .time_box{
  610. display: flex;
  611. flex-direction: column;
  612. align-items: center;
  613. justify-content: center;
  614. margin-top: 25px;
  615. }
  616. .time_box > span:nth-child(1){
  617. color: #727272;
  618. font-size: 16px;
  619. }
  620. .time_box > span:nth-child(2){
  621. /* margin-top: 10px; */
  622. font-size: 32px;
  623. color: #3581FC;
  624. }
  625. .type_box{
  626. min-width: 500px;
  627. width: 70%;
  628. text-align: right;
  629. color: #a5a5a5;
  630. margin-bottom: 10px;
  631. }
  632. </style>