建材商城系统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.

629 lines
17 KiB

6 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('index.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: 1500,
  377. success: () => {
  378. setTimeout(() => {
  379. // 跳转到订单列表页
  380. this.$utils.redirectTo('/pages_order/order/orderList');
  381. }, 1500);
  382. }
  383. });
  384. } else {
  385. uni.showModal({
  386. title: '提示',
  387. content: res.message || '下单失败',
  388. showCancel: false
  389. });
  390. }
  391. }, err => {
  392. // 错误处理
  393. uni.hideLoading();
  394. this.isUploading = false;
  395. console.error('下单请求失败', err);
  396. this.handleUploadFailed('网络请求失败,请检查网络连接');
  397. });
  398. },
  399. // 处理上传失败情况
  400. handleUploadFailed(message) {
  401. uni.hideLoading();
  402. this.isUploading = false;
  403. this.uploadProgress = 0;
  404. uni.showModal({
  405. title: '上传失败',
  406. content: message,
  407. showCancel: false
  408. });
  409. },
  410. // 格式化文件大小
  411. formatFileSize(size) {
  412. if (size < 1024) {
  413. this.audioSize = size + 'B';
  414. } else if (size < 1024 * 1024) {
  415. this.audioSize = (size / 1024).toFixed(2) + 'KB';
  416. } else {
  417. this.audioSize = (size / (1024 * 1024)).toFixed(2) + 'MB';
  418. }
  419. },
  420. // 格式化日期为字符串
  421. formatDate(date) {
  422. const pad = (n) => n < 10 ? '0' + n : n;
  423. return pad(date.getMonth() + 1) + pad(date.getDate());
  424. }
  425. }
  426. }
  427. </script>
  428. <style scoped lang="scss">
  429. .hand-top{
  430. background-color: #ffffff;
  431. .voice-top{
  432. color: #333333;
  433. height: 100rpx;
  434. display: flex;
  435. align-items: center;
  436. background-color: #ffffff;
  437. .left-icon{
  438. background-color: #D03F25;
  439. display: inline-block;
  440. width: 10rpx;
  441. height: 30rpx;
  442. border-radius: 100rpx;
  443. margin-left: 50rpx;
  444. margin-right: 20rpx;
  445. padding-bottom: 5rpx;
  446. }
  447. }
  448. .voice-upload{
  449. .long-speak{
  450. color: #DC2828;
  451. border: 1rpx solid #DC2828;
  452. width: 85%;
  453. height: 80rpx;
  454. margin: auto;
  455. margin-top: 60rpx;
  456. display: flex;
  457. align-items: center;
  458. justify-content: center;
  459. border-radius: 100rpx;
  460. &.recording {
  461. background-color: rgba(220, 40, 40, 0.1);
  462. border: 1rpx solid #DC2828;
  463. }
  464. }
  465. .recording-status {
  466. width: 85%;
  467. margin: auto;
  468. margin-top: 20rpx;
  469. text-align: center;
  470. color: #DC2828;
  471. font-size: 28rpx;
  472. display: flex;
  473. justify-content: center;
  474. align-items: center;
  475. .recording-time {
  476. margin-left: 10rpx;
  477. font-weight: bold;
  478. }
  479. }
  480. .voice-wave {
  481. width: 85%;
  482. height: 120rpx;
  483. margin: 20rpx auto;
  484. display: flex;
  485. justify-content: space-between;
  486. align-items: flex-end;
  487. .wave-item {
  488. width: 10rpx;
  489. background-color: #DC2828;
  490. border-radius: 10rpx;
  491. transition: height 0.1s ease-in-out;
  492. }
  493. }
  494. .recording-file{
  495. height: 250rpx;
  496. display: flex;
  497. align-items: center;
  498. justify-content: center;
  499. position: relative;
  500. margin-top: 30rpx;
  501. .file{
  502. background-color: #F4F4F4;
  503. width: 95%;
  504. height: 200rpx;
  505. border-radius: 20rpx;
  506. .record{
  507. position: absolute;
  508. height: 80rpx;
  509. width: 80rpx;
  510. top: 65rpx;
  511. left: 60rpx;
  512. }
  513. .file-start{
  514. position: absolute;
  515. height: 30rpx;
  516. width: 30rpx;
  517. top: 60rpx;
  518. right: 100rpx;
  519. }
  520. .file-delete{
  521. position: absolute;
  522. height: 30rpx;
  523. width: 30rpx;
  524. top: 60rpx;
  525. right: 50rpx;
  526. }
  527. .file-top{
  528. position: absolute;
  529. width: 280rpx;
  530. top: 60rpx;
  531. left: 160rpx;
  532. }
  533. .file-bottom{
  534. position: absolute;
  535. width: 88%;
  536. height: 30rpx;
  537. display: flex;
  538. align-items: center;
  539. top: 160rpx;
  540. left: 60rpx;
  541. .schedule{
  542. width: 80%;
  543. height: 10rpx;
  544. border-radius: 30rpx;
  545. background-color: #D03F25;
  546. margin-right: 15rpx;
  547. }
  548. }
  549. }
  550. }
  551. .text-upload{
  552. height: 100rpx;
  553. text-align: center;
  554. line-height: 100rpx;
  555. color: #666666;
  556. margin-bottom: 100rpx;
  557. }
  558. }
  559. .voice-button{
  560. color: #ffffff;
  561. background-color: #DC2828;
  562. width: 85%;
  563. height: 100rpx;
  564. margin: auto;
  565. display: flex;
  566. align-items: center;
  567. justify-content: center;
  568. border-radius: 100rpx;
  569. }
  570. }
  571. </style>