voice.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. <template>
  2. <view class="container">
  3. <view class="textbox">
  4. <view class="header-tips">请朗读以下文字</view>
  5. <view class="textbox-con" :style="{height: textHeight}">{{voiceText || ''}}</view>
  6. </view>
  7. <view class="voice-footer">
  8. <view class="voice-footer-tips">1、选择安静的录音环境,可在房间或车内录音。</view>
  9. <view class="voice-footer-tips">2、保持20cm距离,避免手机太远录音不清晰。</view>
  10. <view class="voice-footer-tips">3、使用普通话朗读,语速适中,吐字清晰。</view>
  11. <view class="voice-footer-btnbox">
  12. <view class="tabs">
  13. <u-tabs
  14. :scrollable="false"
  15. :list="tabs"
  16. :current="current"
  17. lineColor="#FF5C03"
  18. @change="tabChange">
  19. </u-tabs>
  20. <view class="mask" v-show="status=='start'||status=='end'" @click.stop>
  21. <view @click.stop="tabclick(0)"></view>
  22. <view @click.stop="tabclick(1)"></view>
  23. </view>
  24. </view>
  25. <view class="voice-footer-con">
  26. <view class="btnbox-item" :style="{visibility: voicePath ? 'visible' : 'hidden',color: status=='start' ? '#ccc !important':''}">
  27. <view class="iconsbox" @tap="playVoice" :style="{borderColor: status=='start' ? '#ccc' :isVoicePlay ? 'red':''}">
  28. <uni-icons type="sound-filled" size="30" :color="status=='start' ? '#ccc' :isVoicePlay ? 'red':'#757575'"></uni-icons>
  29. </view>
  30. <view>试听录音</view>
  31. </view>
  32. <view class="btnbox-item">
  33. <view class="iconsbox iconsbox-voice" :style="{backgroundColor: status=='end'|| status=='ok' ? 'red':''}" @tap="handleRecord">
  34. <uni-icons v-show="status=='stop'|| status=='end' || status=='ok'" :type="current==1?'smallcircle-filled':'mic-filled'" size="35" color="#fff"></uni-icons>
  35. <image v-show="current!=1&&status=='start'" src="@/static/images/play_icon.png" mode="aspectFill"></image>
  36. <view v-show="current==1&&status=='start'"><u-loading-icon color="#fff"></u-loading-icon></view>
  37. </view>
  38. <view v-show="status=='stop'">{{current==1?'点击生成':'点击录制'}}</view>
  39. <view v-show="status=='start'">{{current==1?'生成中':'点击停止'}}</view>
  40. <view v-show="status=='end'|| status=='ok'">{{current==1?'重新生成':'重新录制'}}</view>
  41. </view>
  42. <view class="btnbox-item" :style="{visibility: voicePath&&status!='ok'&&current!=1 ? 'visible' : 'hidden',color: status=='start' ? '#ccc !important':''}">
  43. <button class="iconsbox" :disabled="btnLoading" @tap="onSubmit">
  44. <uni-icons type="checkmarkempty" size="30" :color="status=='start' ? '#ccc' : '#757575'"></uni-icons>
  45. </button>
  46. <view>提交</view>
  47. </view>
  48. </view>
  49. </view>
  50. </view>
  51. </view>
  52. </template>
  53. <script>
  54. import {companyUserVoiceNew,companyUserVoice,queryDetail} from '@/api/companyUser'
  55. export default {
  56. data() {
  57. return {
  58. recorderManager: null,
  59. innerAudioContext: null,
  60. textHeight: '',
  61. statusBarHeight: uni.getSystemInfoSync().statusBarHeight + 'px',
  62. screenHeight: uni.getSystemInfoSync().windowHeight + 'px',
  63. voicePath: '',
  64. status: "stop",
  65. isVoicePlay: false,
  66. btnLoading: false,
  67. tabs:[
  68. {
  69. id:1,
  70. name:'自己录制'
  71. },
  72. {
  73. id:2,
  74. name:'AI生成'
  75. }
  76. ],
  77. current: 0,
  78. id: null,
  79. voiceText: '',
  80. recordType: 0, // 是否已采集
  81. }
  82. },
  83. onLoad(option) {
  84. this.id = option.id || null
  85. this.getDetail()
  86. this.initDate()
  87. },
  88. onHide() {
  89. if(this.innerAudioContext){
  90. this.innerAudioContext.stop();
  91. }
  92. },
  93. onUnload() {
  94. this.recorderManager = null
  95. if(this.innerAudioContext) {
  96. this.innerAudioContext.destroy()
  97. this.innerAudioContext = null
  98. }
  99. },
  100. methods: {
  101. tabclick(type) {
  102. if(type == this.current) return
  103. if(this.btnLoading || this.status == 'start') {
  104. uni.showToast({
  105. title: '生成中,请勿进行其他操作!',
  106. icon: 'none'
  107. })
  108. } else if(this.status == 'end') {
  109. const that = this
  110. uni.showModal({
  111. title: '提示',
  112. content: '当亲录制已完成,切换类型需要重新录制,确认切换吗?',
  113. success: function (res) {
  114. if (res.confirm) {
  115. const item = {
  116. index: type,
  117. ...that.tabs[type]
  118. }
  119. that.tabChange(item)
  120. } else if (res.cancel) {
  121. console.log('用户点击取消');
  122. }
  123. }
  124. });
  125. }
  126. },
  127. tabChange(item){
  128. if(this.current == item.index) return
  129. if(this.btnLoading || this.status == 'start') {
  130. uni.showToast({
  131. title: '生成中,请勿进行其他操作!',
  132. icon: 'none'
  133. })
  134. }
  135. this.current = item.index
  136. this.recorderManager = null
  137. if(this.innerAudioContext) {
  138. this.innerAudioContext.stop();
  139. this.innerAudioContext.destroy()
  140. this.innerAudioContext = null
  141. }
  142. this.voicePath = this.recordType == 1 ? this.voicePath : ''
  143. this.status = this.recordType == 1 ? this.status : "stop"
  144. this.isVoicePlay = false
  145. this.btnLoading = false
  146. this.initDate()
  147. },
  148. initDate() {
  149. // #ifndef H5
  150. this.recorderManager = uni.getRecorderManager();
  151. this.innerAudioContext = uni.createInnerAudioContext();
  152. this.innerAudioContext.autoplay = true;
  153. let self = this;
  154. if(this.recorderManager) {
  155. this.recorderManager.onStart(()=>{
  156. this.status = 'start'
  157. this.voicePath = ''
  158. })
  159. this.recorderManager.onStop((res)=> {
  160. // console.log('recorder stop' + JSON.stringify(res));
  161. this.status = 'end'
  162. self.voicePath = res.tempFilePath;
  163. });
  164. }
  165. if(this.innerAudioContext) {
  166. this.innerAudioContext.onPlay(() => {
  167. // console.log('开始播放');
  168. this.isVoicePlay = true
  169. });
  170. this.innerAudioContext.onStop(() => {
  171. // console.log('停止播放');
  172. this.isVoicePlay = false
  173. });
  174. this.innerAudioContext.onEnded(() => {
  175. // console.log('播放结束');
  176. this.isVoicePlay = false
  177. });
  178. this.innerAudioContext.onError((res) => {
  179. this.isVoicePlay = false
  180. });
  181. }
  182. // #endif
  183. },
  184. handleRecord() {
  185. if(this.current == 1) {
  186. if(this.status == 'stop') {
  187. this.creatVoice()
  188. } else if(this.status == 'start') {
  189. uni.showToast({
  190. title: '生成中,请勿进行其他操作!',
  191. icon: 'none'
  192. })
  193. } else if(this.status == 'end'||this.status == 'ok') {
  194. this.creatVoice()
  195. }
  196. } else {
  197. if(this.status == 'stop') {
  198. this.startRecord()
  199. } else if(this.status == 'start') {
  200. this.endRecord()
  201. } else if(this.status == 'end'||this.status == 'ok') {
  202. this.startRecord()
  203. }
  204. }
  205. },
  206. startRecord() {
  207. // console.log('开始录音');
  208. this.recorderManager.start({
  209. format: 'mp3',
  210. duration: 10000
  211. });
  212. },
  213. endRecord() {
  214. // console.log('录音结束');
  215. this.recorderManager.stop();
  216. },
  217. playVoice() {
  218. if(this.status == "start") return
  219. if (this.voicePath) {
  220. if(this.isVoicePlay == false) {
  221. this.innerAudioContext.src = this.voicePath;
  222. this.innerAudioContext.play();
  223. } else {
  224. this.innerAudioContext.stop();
  225. }
  226. }
  227. },
  228. onSubmit() {
  229. if(this.status == "start") return
  230. this.btnLoading = true
  231. uni.showLoading({
  232. title:"提交中..."
  233. })
  234. if(this.current == 1) {
  235. this.creatVoice()
  236. return
  237. }
  238. uni.uploadFile({
  239. url: uni.getStorageSync('requestPath')+'/app/common/uploadOSS', //仅为示例,非真实的接口地址
  240. filePath: this.voicePath,
  241. name: 'file',
  242. success: (uploadFileRes) => {
  243. // console.log(JSON.parse(uploadFileRes.data).url)
  244. let voicePrintUrl = JSON.parse(uploadFileRes.data).url
  245. companyUserVoiceNew({userVoiceUrl: voicePrintUrl,id:this.id}).then(res=>{
  246. uni.hideLoading()
  247. this.btnLoading = false
  248. if(res.code==200){
  249. uni.showToast({
  250. icon:'none',
  251. title: '提交成功',
  252. });
  253. uni.$emit('refreshVoiceList')
  254. this.$navBack()
  255. }else{
  256. uni.showToast({
  257. icon:'none',
  258. title: res.msg,
  259. });
  260. }
  261. }).catch(()=>{
  262. uni.hideLoading()
  263. this.btnLoading = false
  264. })
  265. },
  266. fail: ()=>{
  267. uni.hideLoading()
  268. this.btnLoading = false
  269. }
  270. });
  271. },
  272. getDetail() {
  273. queryDetail(this.id).then(res=>{
  274. if(res.code==200){
  275. this.voiceText = res.data.voiceTxt
  276. this.recordType = res.data.recordType
  277. if(this.recordType == 1) {
  278. this.voicePath = res.data.wavUrl||''
  279. this.status = 'ok'
  280. if(this.voicePath&&this.voicePath.match(/\.mp3$/)) {
  281. this.current = 0
  282. } else {
  283. this.current = 1
  284. }
  285. }
  286. this.$nextTick(()=>{
  287. const query = uni.createSelectorQuery().in(this);
  288. query
  289. .select(".voice-footer")
  290. .boundingClientRect((data) => {
  291. this.textHeight = `calc(${this.screenHeight} - ${data.height}px - ${this.statusBarHeight} - 45px - ${uni.upx2px(120)}px)`
  292. })
  293. .exec();
  294. })
  295. }else{
  296. uni.showToast({
  297. icon:'none',
  298. title: res.msg,
  299. });
  300. }
  301. })
  302. },
  303. // 智能生成
  304. creatVoice() {
  305. if(this.status == "start") return
  306. this.btnLoading = true
  307. this.status = "start"
  308. this.voicePath = ''
  309. uni.showLoading({
  310. title:"生成中..."
  311. })
  312. companyUserVoice({
  313. id: this.id,
  314. }).then(res=>{
  315. uni.hideLoading()
  316. this.btnLoading = false
  317. if(res.code==200){
  318. this.status = "end"
  319. this.voicePath = res.data.wavUrl
  320. uni.showToast({
  321. icon:'none',
  322. title: '生成成功',
  323. });
  324. uni.$emit('refreshVoiceList')
  325. this.$navBack()
  326. }else{
  327. this.status = "stop"
  328. uni.showToast({
  329. icon:'none',
  330. title: res.msg,
  331. });
  332. }
  333. }).catch(()=>{
  334. uni.hideLoading()
  335. this.btnLoading = false
  336. this.status = "stop"
  337. })
  338. }
  339. }
  340. }
  341. </script>
  342. <style scoped lang="scss">
  343. @mixin u-flex($flexD, $alignI, $justifyC) {
  344. display: flex;
  345. flex-direction: $flexD;
  346. align-items: $alignI;
  347. justify-content: $justifyC;
  348. }
  349. .tabs {
  350. position: relative;
  351. width: 100%;
  352. .mask {
  353. position: absolute;
  354. top: 0;
  355. left: 0;
  356. width: 100%;
  357. height: 100%;
  358. z-index: 3;
  359. @include u-flex(row,flex-start,flex-start);
  360. view {
  361. flex: 1;
  362. height: 100%;
  363. }
  364. }
  365. }
  366. .container {
  367. position: relative;
  368. .header-tips {
  369. padding-bottom: 24rpx;
  370. box-sizing: border-box;
  371. font-family: PingFang SC, PingFang SC;
  372. font-weight: 400;
  373. font-size: 24rpx;
  374. color: #757575;
  375. // color: $mainThemeHColor;
  376. text-align: center;
  377. }
  378. .textbox {
  379. padding: 24rpx 50rpx;
  380. box-sizing: border-box;
  381. &-con {
  382. padding: 32rpx;
  383. box-sizing: border-box;
  384. background-color: #fff;
  385. border-radius: 16rpx 16rpx 16rpx 16rpx;
  386. font-family: PingFang SC, PingFang SC;
  387. font-weight: 500;
  388. font-size: 40rpx;
  389. color: #222222;
  390. overflow-y: auto;
  391. line-height: 60rpx;
  392. letter-spacing: 4rpx;
  393. }
  394. }
  395. .voice-footer {
  396. position: fixed;
  397. bottom: 0;
  398. left: 0;
  399. width: 100%;
  400. box-sizing: border-box;
  401. &-tips {
  402. padding: 0 50rpx;
  403. box-sizing: border-box;
  404. font-family: PingFang SC, PingFang SC;
  405. font-weight: 400;
  406. font-size: 24rpx;
  407. color: #757575;
  408. margin-bottom: 10rpx;
  409. }
  410. }
  411. }
  412. .voice-footer-btnbox {
  413. margin-top: 100rpx;
  414. background-color: #fff;
  415. text-align: center;
  416. font-family: PingFang SC, PingFang SC;
  417. font-weight: 400;
  418. font-size: 30rpx;
  419. color: #333;
  420. .voice-footer-con {
  421. padding: 50rpx 24rpx;
  422. box-sizing: border-box;
  423. @include u-flex(row,flex-end,space-evenly);
  424. }
  425. .iconsbox {
  426. height: 110rpx;
  427. width: 110rpx;
  428. margin: 0;
  429. margin-bottom: 20rpx;
  430. box-sizing: border-box;
  431. @include u-flex(row,center,center);
  432. border-radius: 50%;
  433. background-color: #fff;
  434. border: 1rpx solid #CCCCCC;
  435. &::after {
  436. border: none;
  437. }
  438. &-voice {
  439. height: 150rpx;
  440. width: 150rpx;
  441. background-color: #FF5C03;
  442. border: 1rpx solid #FF5C03;
  443. image {
  444. height: 60rpx;
  445. width: 60rpx;
  446. }
  447. }
  448. }
  449. .btnbox-item {
  450. flex: 1;
  451. @include u-flex(column,center,center);
  452. }
  453. }
  454. </style>