建材商城系统20241014
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

626 lines
16 KiB

7 months ago
  1. <template>
  2. <view class="hand-top">
  3. <navbar
  4. title="快捷下单"
  5. leftClick
  6. @leftClick="$utils.navigateBack"
  7. />
  8. <view class="voice-top">
  9. <view class="left-icon"></view>
  10. <text>录制语音下单</text>
  11. </view>
  12. <view class="voice-upload">
  13. <view class="long-speak"
  14. @touchstart="startRecord"
  15. @touchend="stopRecord"
  16. @touchcancel="cancelRecord"
  17. :class="{'recording': isRecording}">
  18. <uv-icon name="mic" size="45rpx" color="#DC2828"></uv-icon>
  19. <text>{{isRecording ? '松开结束录音' : '长按可说话'}}</text>
  20. </view>
  21. <view class="recording-status" v-if="isRecording">
  22. <text>正在录音中...</text>
  23. <view class="recording-time">{{recordingTime}}s</view>
  24. </view>
  25. <!-- 录音波形效果 -->
  26. <view class="voice-wave" v-if="isRecording">
  27. <view class="wave-item" v-for="(item, index) in waveItems" :key="index"
  28. :style="{height: item + 'rpx'}"></view>
  29. </view>
  30. <view class="recording-file" v-if="audioPath">
  31. <view class="file">
  32. <image src="../static/order/1.png" mode="" class="record"></image>
  33. <image src="../static/order/2.png" mode="" class="file-start" @click="playVoice"></image>
  34. <image src="../static/order/3.png" mode="" class="file-delete" @click="deleteVoice"></image>
  35. <view class="file-top">
  36. <p>{{audioName}}</p>
  37. <p style="color: #A6ADBA;">{{audioSize}}</p>
  38. </view>
  39. <view class="file-bottom">
  40. <view class="schedule" :style="{width: uploadProgress + '%'}"></view>
  41. <text>{{uploadProgress}}%</text>
  42. </view>
  43. </view>
  44. </view>
  45. <view class="text-upload">
  46. <text>(录音上传你所需要识别的产品语音)</text>
  47. </view>
  48. </view>
  49. <view class="fast-order">
  50. <view class="voice-button" @click="submitVoiceOrder" :style="audioPath ? '' : 'opacity: 0.5;'">
  51. <text>快捷下单</text>
  52. </view>
  53. </view>
  54. </view>
  55. </template>
  56. <script>
  57. export default {
  58. data() {
  59. return {
  60. recorderManager: null, // 录音管理器
  61. innerAudioContext: null, // 音频播放器
  62. isRecording: false, // 是否正在录音
  63. isPlaying: false, // 是否正在播放
  64. audioPath: '', // 录音文件路径
  65. audioName: '录音文件01.mp3', // 录音文件名称
  66. audioSize: '0KB', // 录音文件大小
  67. uploadProgress: 0, // 上传进度
  68. recordStartTime: 0, // 录音开始时间
  69. recordTimeout: null, // 录音超时计时器
  70. recordMaxDuration: 60000, // 最大录音时长(毫秒)
  71. recordingTime: 0, // 当前录音时长(秒)
  72. recordTimer: null, // 录音计时器
  73. isUploading: false, // 是否正在上传
  74. recognitionResult: null, // 语音识别结果
  75. waveTimer: null, // 波形动画计时器
  76. waveItems: [], // 波形数据
  77. audioUrl: '', // 上传后的音频URL
  78. };
  79. },
  80. onLoad() {
  81. // 初始化录音管理器
  82. this.recorderManager = uni.getRecorderManager();
  83. // 初始化波形数据
  84. this.initWaveItems();
  85. // 监听录音开始事件
  86. this.recorderManager.onStart(() => {
  87. console.log('录音开始');
  88. this.isRecording = true;
  89. this.recordStartTime = Date.now();
  90. this.recordingTime = 0;
  91. // 开始计时
  92. this.recordTimer = setInterval(() => {
  93. this.recordingTime = Math.floor((Date.now() - this.recordStartTime) / 1000);
  94. }, 1000);
  95. // 开始波形动画
  96. this.startWaveAnimation();
  97. // 设置最大录音时长
  98. this.recordTimeout = setTimeout(() => {
  99. if (this.isRecording) {
  100. this.stopRecord();
  101. }
  102. }, this.recordMaxDuration);
  103. });
  104. // 监听录音结束事件
  105. this.recorderManager.onStop((res) => {
  106. console.log('录音结束', res);
  107. this.isRecording = false;
  108. if (this.recordTimeout) {
  109. clearTimeout(this.recordTimeout);
  110. this.recordTimeout = null;
  111. }
  112. if (this.recordTimer) {
  113. clearInterval(this.recordTimer);
  114. this.recordTimer = null;
  115. }
  116. // 停止波形动画
  117. this.stopWaveAnimation();
  118. if (res.tempFilePath) {
  119. this.audioPath = res.tempFilePath;
  120. // 计算录音文件大小并格式化
  121. this.formatFileSize(res.fileSize || 0);
  122. // 生成录音文件名
  123. this.audioName = '录音文件' + this.formatDate(new Date()) + '.mp3';
  124. // 如果时长过短,提示用户
  125. if (this.recordingTime < 1) {
  126. uni.showToast({
  127. title: '录音时间太短,请重新录制',
  128. icon: 'none'
  129. });
  130. this.audioPath = '';
  131. }
  132. }
  133. });
  134. // 监听录音错误事件
  135. this.recorderManager.onError((err) => {
  136. console.error('录音错误', err);
  137. uni.showToast({
  138. title: '录音失败: ' + err.errMsg,
  139. icon: 'none'
  140. });
  141. this.isRecording = false;
  142. if (this.recordTimeout) {
  143. clearTimeout(this.recordTimeout);
  144. this.recordTimeout = null;
  145. }
  146. if (this.recordTimer) {
  147. clearInterval(this.recordTimer);
  148. this.recordTimer = null;
  149. }
  150. // 停止波形动画
  151. this.stopWaveAnimation();
  152. });
  153. // 初始化音频播放器
  154. this.innerAudioContext = uni.createInnerAudioContext();
  155. // 监听播放结束事件
  156. this.innerAudioContext.onEnded(() => {
  157. console.log('播放结束');
  158. this.isPlaying = false;
  159. });
  160. // 监听播放错误事件
  161. this.innerAudioContext.onError((err) => {
  162. console.error('播放错误', err);
  163. uni.showToast({
  164. title: '播放失败',
  165. icon: 'none'
  166. });
  167. this.isPlaying = false;
  168. });
  169. },
  170. onUnload() {
  171. // 销毁录音管理器和音频播放器
  172. if (this.innerAudioContext) {
  173. this.innerAudioContext.destroy();
  174. }
  175. if (this.recordTimeout) {
  176. clearTimeout(this.recordTimeout);
  177. this.recordTimeout = null;
  178. }
  179. if (this.recordTimer) {
  180. clearInterval(this.recordTimer);
  181. this.recordTimer = null;
  182. }
  183. // 停止波形动画
  184. this.stopWaveAnimation();
  185. },
  186. methods: {
  187. // 初始化波形数据
  188. initWaveItems() {
  189. const itemCount = 16; // 波形柱状图数量
  190. this.waveItems = Array(itemCount).fill(10); // 初始高度10rpx
  191. },
  192. // 开始波形动画
  193. startWaveAnimation() {
  194. // 停止可能已存在的动画
  195. this.stopWaveAnimation();
  196. // 随机生成波形高度
  197. this.waveTimer = setInterval(() => {
  198. const newWaveItems = [];
  199. for (let i = 0; i < this.waveItems.length; i++) {
  200. // 生成10-60之间的随机高度
  201. newWaveItems.push(Math.floor(Math.random() * 50) + 10);
  202. }
  203. this.waveItems = newWaveItems;
  204. }, 100);
  205. },
  206. // 停止波形动画
  207. stopWaveAnimation() {
  208. if (this.waveTimer) {
  209. clearInterval(this.waveTimer);
  210. this.waveTimer = null;
  211. }
  212. this.initWaveItems(); // 重置波形
  213. },
  214. // 开始录音
  215. startRecord() {
  216. if (this.isRecording) return;
  217. const options = {
  218. duration: this.recordMaxDuration, // 最大录音时长
  219. sampleRate: 44100, // 采样率
  220. numberOfChannels: 1, // 录音通道数
  221. encodeBitRate: 192000, // 编码码率
  222. format: 'mp3', // 音频格式
  223. frameSize: 50 // 指定帧大小
  224. };
  225. // 开始录音
  226. this.recorderManager.start(options);
  227. // 震动反馈
  228. uni.vibrateShort({
  229. success: function () {
  230. console.log('震动成功');
  231. }
  232. });
  233. },
  234. // 停止录音
  235. stopRecord() {
  236. if (!this.isRecording) return;
  237. this.recorderManager.stop();
  238. // 震动反馈
  239. uni.vibrateShort({
  240. success: function () {
  241. console.log('震动成功');
  242. }
  243. });
  244. },
  245. // 取消录音
  246. cancelRecord() {
  247. if (!this.isRecording) return;
  248. this.recorderManager.stop();
  249. this.audioPath = ''; // 清空录音路径
  250. uni.showToast({
  251. title: '录音已取消',
  252. icon: 'none'
  253. });
  254. },
  255. // 播放录音
  256. playVoice() {
  257. if (!this.audioPath) {
  258. uni.showToast({
  259. title: '没有可播放的录音',
  260. icon: 'none'
  261. });
  262. return;
  263. }
  264. if (this.isPlaying) {
  265. // 如果正在播放,则停止播放
  266. this.innerAudioContext.stop();
  267. this.isPlaying = false;
  268. return;
  269. }
  270. // 设置音频源
  271. this.innerAudioContext.src = this.audioPath;
  272. this.innerAudioContext.play();
  273. this.isPlaying = true;
  274. // 播放完成后自动停止
  275. setTimeout(() => {
  276. if (this.isPlaying) {
  277. this.isPlaying = false;
  278. }
  279. }, this.recordingTime * 1000 + 500); // 增加500ms缓冲时间
  280. },
  281. // 删除录音
  282. deleteVoice() {
  283. if (!this.audioPath) return;
  284. uni.showModal({
  285. title: '提示',
  286. content: '确定要删除这段录音吗?',
  287. success: (res) => {
  288. if (res.confirm) {
  289. // 如果正在播放,先停止播放
  290. if (this.isPlaying) {
  291. this.innerAudioContext.stop();
  292. this.isPlaying = false;
  293. }
  294. this.audioPath = '';
  295. this.audioName = '录音文件01.mp3';
  296. this.audioSize = '0KB';
  297. this.recordingTime = 0;
  298. this.audioUrl = '';
  299. uni.showToast({
  300. title: '录音已删除',
  301. icon: 'none'
  302. });
  303. }
  304. }
  305. });
  306. },
  307. // 提交语音订单
  308. submitVoiceOrder() {
  309. if (!this.audioPath) {
  310. uni.showToast({
  311. title: '请先录制语音',
  312. icon: 'none'
  313. });
  314. return;
  315. }
  316. if (this.isUploading) {
  317. uni.showToast({
  318. title: '正在上传中,请稍候',
  319. icon: 'none'
  320. });
  321. return;
  322. }
  323. // 显示加载提示
  324. uni.showLoading({
  325. title: '上传中...'
  326. });
  327. this.isUploading = true;
  328. this.uploadProgress = 0;
  329. // 上传音频文件
  330. this.uploadAudioFile();
  331. },
  332. // 上传音频文件
  333. uploadAudioFile() {
  334. // 模拟上传进度
  335. const simulateProgress = () => {
  336. this.uploadProgress = 0;
  337. const interval = setInterval(() => {
  338. this.uploadProgress += 5;
  339. if (this.uploadProgress >= 90) {
  340. clearInterval(interval);
  341. }
  342. }, 100);
  343. return interval;
  344. };
  345. const progressInterval = simulateProgress();
  346. // 使用OSS上传服务上传音频文件
  347. this.$Oss.ossUpload(this.audioPath).then(url => {
  348. // 上传成功
  349. clearInterval(progressInterval);
  350. this.uploadProgress = 100;
  351. this.audioUrl = url;
  352. console.log('音频上传成功', url);
  353. // 调用语音下单接口
  354. this.createVoiceOrder(url);
  355. }).catch(err => {
  356. // 上传失败
  357. clearInterval(progressInterval);
  358. console.error('音频上传失败', err);
  359. this.handleUploadFailed('音频上传失败,请重试');
  360. });
  361. },
  362. // 创建语音订单
  363. createVoiceOrder(audioUrl) {
  364. this.$api('addOrder', {
  365. voiceUrl: audioUrl,
  366. type: '2', // 2表示语音下单
  367. userId: uni.getStorageSync('userId') || ''
  368. }, res => {
  369. uni.hideLoading();
  370. this.isUploading = false;
  371. if (res.code == 200) {
  372. // 下单成功
  373. uni.showToast({
  374. title: '下单成功',
  375. icon: 'success',
  376. duration: 1000,
  377. success: () => {
  378. uni.navigateBack(-1)
  379. }
  380. });
  381. } else {
  382. uni.showModal({
  383. title: '提示',
  384. content: res.message || '下单失败',
  385. showCancel: false
  386. });
  387. }
  388. }, err => {
  389. // 错误处理
  390. uni.hideLoading();
  391. this.isUploading = false;
  392. console.error('下单请求失败', err);
  393. this.handleUploadFailed('网络请求失败,请检查网络连接');
  394. });
  395. },
  396. // 处理上传失败情况
  397. handleUploadFailed(message) {
  398. uni.hideLoading();
  399. this.isUploading = false;
  400. this.uploadProgress = 0;
  401. uni.showModal({
  402. title: '上传失败',
  403. content: message,
  404. showCancel: false
  405. });
  406. },
  407. // 格式化文件大小
  408. formatFileSize(size) {
  409. if (size < 1024) {
  410. this.audioSize = size + 'B';
  411. } else if (size < 1024 * 1024) {
  412. this.audioSize = (size / 1024).toFixed(2) + 'KB';
  413. } else {
  414. this.audioSize = (size / (1024 * 1024)).toFixed(2) + 'MB';
  415. }
  416. },
  417. // 格式化日期为字符串
  418. formatDate(date) {
  419. const pad = (n) => n < 10 ? '0' + n : n;
  420. return pad(date.getMonth() + 1) + pad(date.getDate());
  421. }
  422. }
  423. }
  424. </script>
  425. <style scoped lang="scss">
  426. .hand-top{
  427. background-color: #ffffff;
  428. .voice-top{
  429. color: #333333;
  430. height: 100rpx;
  431. display: flex;
  432. align-items: center;
  433. background-color: #ffffff;
  434. .left-icon{
  435. background-color: #D03F25;
  436. display: inline-block;
  437. width: 10rpx;
  438. height: 30rpx;
  439. border-radius: 100rpx;
  440. margin-left: 50rpx;
  441. margin-right: 20rpx;
  442. padding-bottom: 5rpx;
  443. }
  444. }
  445. .voice-upload{
  446. .long-speak{
  447. color: #DC2828;
  448. border: 1rpx solid #DC2828;
  449. width: 85%;
  450. height: 80rpx;
  451. margin: auto;
  452. margin-top: 60rpx;
  453. display: flex;
  454. align-items: center;
  455. justify-content: center;
  456. border-radius: 100rpx;
  457. &.recording {
  458. background-color: rgba(220, 40, 40, 0.1);
  459. border: 1rpx solid #DC2828;
  460. }
  461. }
  462. .recording-status {
  463. width: 85%;
  464. margin: auto;
  465. margin-top: 20rpx;
  466. text-align: center;
  467. color: #DC2828;
  468. font-size: 28rpx;
  469. display: flex;
  470. justify-content: center;
  471. align-items: center;
  472. .recording-time {
  473. margin-left: 10rpx;
  474. font-weight: bold;
  475. }
  476. }
  477. .voice-wave {
  478. width: 85%;
  479. height: 120rpx;
  480. margin: 20rpx auto;
  481. display: flex;
  482. justify-content: space-between;
  483. align-items: flex-end;
  484. .wave-item {
  485. width: 10rpx;
  486. background-color: #DC2828;
  487. border-radius: 10rpx;
  488. transition: height 0.1s ease-in-out;
  489. }
  490. }
  491. .recording-file{
  492. height: 250rpx;
  493. display: flex;
  494. align-items: center;
  495. justify-content: center;
  496. position: relative;
  497. margin-top: 30rpx;
  498. .file{
  499. background-color: #F4F4F4;
  500. width: 95%;
  501. height: 200rpx;
  502. border-radius: 20rpx;
  503. .record{
  504. position: absolute;
  505. height: 80rpx;
  506. width: 80rpx;
  507. top: 65rpx;
  508. left: 60rpx;
  509. }
  510. .file-start{
  511. position: absolute;
  512. height: 30rpx;
  513. width: 30rpx;
  514. top: 60rpx;
  515. right: 100rpx;
  516. }
  517. .file-delete{
  518. position: absolute;
  519. height: 30rpx;
  520. width: 30rpx;
  521. top: 60rpx;
  522. right: 50rpx;
  523. }
  524. .file-top{
  525. position: absolute;
  526. width: 280rpx;
  527. top: 60rpx;
  528. left: 160rpx;
  529. }
  530. .file-bottom{
  531. position: absolute;
  532. width: 88%;
  533. height: 30rpx;
  534. display: flex;
  535. align-items: center;
  536. top: 160rpx;
  537. left: 60rpx;
  538. .schedule{
  539. width: 80%;
  540. height: 10rpx;
  541. border-radius: 30rpx;
  542. background-color: #D03F25;
  543. margin-right: 15rpx;
  544. }
  545. }
  546. }
  547. }
  548. .text-upload{
  549. height: 100rpx;
  550. text-align: center;
  551. line-height: 100rpx;
  552. color: #666666;
  553. margin-bottom: 100rpx;
  554. }
  555. }
  556. .voice-button{
  557. color: #ffffff;
  558. background-color: #DC2828;
  559. width: 85%;
  560. height: 100rpx;
  561. margin: auto;
  562. display: flex;
  563. align-items: center;
  564. justify-content: center;
  565. border-radius: 100rpx;
  566. }
  567. }
  568. </style>