224 lines
5.0 KiB
Vue
224 lines
5.0 KiB
Vue
<template>
|
||
<view class="ai-chat-container">
|
||
<scroll-view
|
||
class="chat-list"
|
||
scroll-y
|
||
scroll-bottom="{{scrollBottom}}"
|
||
:style="{ height: `calc(100vh - ${inputAreaHeight}px)` }"
|
||
>
|
||
<view class="chat-item ai">
|
||
<view class="chat-avatar ai-avatar">
|
||
<text class="iconfont icon-ai"></text>
|
||
</view>
|
||
<view class="chat-content">
|
||
<view class="content-text">{{ asrResult }}</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<view class="input-area" :style="{ height: inputAreaHeight + 'px' }">
|
||
<view class="send-btn-area">
|
||
<button class="hold-send-btn" @touchstart="startRecord" @touchend="stopRecord" @touchcancel="handleTouchEnd">
|
||
<text class="btn-text">{{ isHolding ? '松开发送' : '按住 说话/发送' }}</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
// import Recorder from 'recorder-core';
|
||
export default {
|
||
data() {
|
||
return {
|
||
scrollBottom: 0,
|
||
inputAreaHeight: 80,
|
||
isHolding: false,
|
||
asrResult: '',
|
||
recorderManager: null, // 原生录音管理器
|
||
ws: null, // WebSocket连接
|
||
isRecording: false,
|
||
CHUNK_SIZE: 6400, // 200ms/块
|
||
recordFilePath: '', // 录音文件路径
|
||
fileReaderTimer: null, // 分片读取定时器
|
||
recorder: null, // 录音实例
|
||
audioStream: [], // 实时音频流缓存
|
||
sampleRate: 16000, // 采样率(常用 16000/44100)
|
||
bitRate: 16, // 位深
|
||
channels: 1 // 声道(单声道)
|
||
};
|
||
},
|
||
onLoad() {
|
||
this.initRecorder()
|
||
},
|
||
onUnload() {
|
||
// 页面卸载时销毁实例
|
||
this.destroyRecorder();
|
||
},
|
||
methods: {
|
||
// 初始化录音器
|
||
initRecorder() {
|
||
console.log('plus.audio', plus.audio);
|
||
|
||
// 确保 plus.audio 存在
|
||
if (!plus.audio) {
|
||
console.error('5+ API 音频模块未加载');
|
||
uni.showToast({ title: '录音功能初始化失败', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
// 原初始化逻辑
|
||
const options = {
|
||
filename: '_doc/audio/temp_recorder.amr',
|
||
format: 'amr',
|
||
sampleRate: 16000,
|
||
bitRate: 16,
|
||
channels: 1,
|
||
bufferSize: 4096,
|
||
onRecorded: (buffer) => {
|
||
this.handleRealTimeStream(buffer);
|
||
}
|
||
};
|
||
|
||
try {
|
||
this.recorder = plus.audio.getRecorder(options);
|
||
console.log('录音器初始化成功');
|
||
} catch (e) {
|
||
console.error('创建录音实例失败:', e);
|
||
uni.showToast({ title: '录音功能初始化失败', icon: 'none' });
|
||
}
|
||
},
|
||
|
||
// 开始录音
|
||
startRecord() {
|
||
console.log('下·', this.isRecording, !this.recorder);
|
||
if (this.isRecording || !this.recorder) return;
|
||
this.isRecording = true;
|
||
this.audioStream = []; // 清空历史流
|
||
|
||
this.recorder.record(
|
||
() => {
|
||
console.log('录音开始成功');
|
||
},
|
||
(err) => {
|
||
console.error('录音启动失败:', err);
|
||
this.isRecording = false;
|
||
}
|
||
);
|
||
},
|
||
|
||
// 停止录音
|
||
stopRecord() {
|
||
if (!this.isRecording || !this.recorder) return;
|
||
this.isRecording = false;
|
||
|
||
this.recorder.stop(
|
||
(filePath) => {
|
||
console.log('录音停止,文件路径:', filePath);
|
||
// 最终完整文件(可选)
|
||
this.handleFinalFile(filePath);
|
||
},
|
||
(err) => {
|
||
console.error('录音停止失败:', err);
|
||
}
|
||
);
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.ai-chat-container {
|
||
width: 100%;
|
||
height: 100vh;
|
||
background-color: #f5f7fa;
|
||
box-sizing: border-box;
|
||
padding-bottom: env(safe-area-inset-bottom);
|
||
}
|
||
|
||
.chat-list {
|
||
width: 100%;
|
||
padding: 20rpx 16rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.chat-item {
|
||
display: flex;
|
||
margin-bottom: 30rpx;
|
||
max-width: 85%;
|
||
}
|
||
|
||
.ai-avatar {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
margin-right: 20rpx;
|
||
background-color: #9f7aea;
|
||
}
|
||
|
||
.icon-ai {
|
||
font-size: 40rpx;
|
||
color: #fff;
|
||
}
|
||
|
||
.chat-content .content-text {
|
||
padding: 24rpx 28rpx;
|
||
border-radius: 20rpx 20rpx 20rpx 8rpx;
|
||
line-height: 1.6;
|
||
font-size: 32rpx;
|
||
color: #333;
|
||
word-wrap: break-word;
|
||
word-break: break-all;
|
||
background-color: #fff;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.input-area {
|
||
width: 100%;
|
||
background-color: #fff;
|
||
padding: 16rpx 20rpx;
|
||
box-sizing: border-box;
|
||
border-top: 1rpx solid #eee;
|
||
}
|
||
|
||
.send-btn-area {
|
||
width: 100%;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.hold-send-btn {
|
||
width: 80%;
|
||
height: 90rpx;
|
||
line-height: 90rpx;
|
||
border-radius: 45rpx;
|
||
background-color: #4299e1;
|
||
color: #fff;
|
||
font-size: 34rpx;
|
||
font-weight: 500;
|
||
box-shadow: 0 4rpx 12rpx rgba(66, 153, 225, 0.3);
|
||
transition: all 0.2s ease;
|
||
padding: 0;
|
||
margin: 0;
|
||
}
|
||
|
||
.hold-send-btn:active {
|
||
background-color: #3182ce;
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.iconfont {
|
||
font-family: 'iconfont' !important;
|
||
font-style: normal;
|
||
-webkit-font-smoothing: antialiased;
|
||
-moz-osx-font-smoothing: grayscale;
|
||
}
|
||
.icon-ai::before {
|
||
content: '\e602';
|
||
}
|
||
</style>
|