uniapp搭建vue2微信小程序项目,集成coze,实战版!手把手教
1. uniapp创建vue2项目,建好基础页面。
2. 安装coze依赖,
- pnpm install @coze/uniapp-api
- pnpm install @coze/api
- pnpm install reconnecting-websocket
3.获取coze的token和botId;方式如下:
- 登录coze后台,找到项目管理,没有项目去左侧新建项目,有则省略;
- botId获取方法:点击你新建项目,浏览器地址后面的数字,都是botID;
-[ ](https://code.coze.cn/playground)
- token获取方法:点击左侧菜单栏:API&SDK-授权-个人访问令牌-添加-确定;
- 看不懂的根据图片进行操作就行,推荐全选或者根据自己需求而定;
4. vue2代码如下,写的很草率,手下留情!
js代码如下
<script>
import {
WsChatClient,
WsChatEventNames
} from '../../node_modules/@coze/uniapp-api/dist/ws-tool'
const chatClient = new WsChatClient({
token: '您的tooken',
botId: '您的botId',
audioMutedDefault: true,
playbackMutedDefault: false,
});
export default {
data() {
return {
isAiReplying: false,
isConnected: false,
isRecording: false,
isPlayingVoice: false,
voicePlaybackTimer: null,
btnLockTimer: null
}
},
onLoad() {
this.registerChatEvents();
// 核心修改1:页面加载时主动建立连接,不开启录音
this.initChatConnect();
},
onUnload() {
chatClient.disconnect();
clearTimeout(this.voicePlaybackTimer);
clearTimeout(this.btnLockTimer);
},
methods: {
// 新增:初始化建立WebSocket连接(仅连接,不录音)
async initChatConnect() {
try {
if (!this.isConnected) {
await chatClient.connect();
console.log('页面初始化:WebSocket连接建立成功');
}
} catch (err) {
console.error('页面初始化:WebSocket连接建立失败', err);
}
},
registerChatEvents() {
chatClient.on(WsChatEventNames.CONNECTED, () => {
this.isConnected = true;
});
chatClient.on(WsChatEventNames.DISCONNECTED, () => {
this.isConnected = false;
});
chatClient.on(WsChatEventNames.PLAYBACK_STARTED, () => {
this.isPlayingVoice = true;
this.isAiReplying = true;
});
chatClient.on(WsChatEventNames.PLAYBACK_COMPLETED, () => {
this.isPlayingVoice = false;
});
chatClient.on(WsChatEventNames.ERROR, (err) => {
console.error('交互错误', err);
this.isRecording = false;
});
},
async streamChat() {
// 仅录音中禁止重复触发,语音播报时允许
if (this.isRecording) return;
try {
// 1. 中断当前语音(替代方案:静音+重置状态)
if (this.isPlayingVoice) {
// 用“设置播放静音”替代stopPlayback(兼容当前SDK)
if (chatClient.setPlaybackMuted) {
await chatClient.setPlaybackMuted(true);
}
// 重置状态,强制停止动画和文字显示
this.isPlayingVoice = false;
this.isAiReplying = false;
}
// 核心修改2:移除“首次连接”判断(页面加载时已连接)
// 无需再判断 !this.isConnected,直接开启录音即可
// 3. 开启新录音(仅用户点击时触发,符合需求)
await chatClient.startRecord();
this.isRecording = true;
console.log('麦克风开启,正在听...');
// 4. 模拟5秒录音
setTimeout(async () => {
await chatClient.stopRecord();
this.isRecording = false;
// 开启新的AI回复状态
this.isAiReplying = true;
this.isPlayingVoice = true;
// 恢复播放音量(避免新语音也静音)
if (chatClient.setPlaybackMuted) {
await chatClient.setPlaybackMuted(false);
}
console.log('停止录音,进入新的AI回复状态');
}, 5000);
} catch (err) {
console.error('语音交互失败', err);
this.isRecording = false;
}
},
async stopAiReply() {
console.log('用户手动点击停止');
const that = this;
uni.showModal({
title: '提示',
content: '确定终止当前对话?',
async success(res) {
if (res.confirm) {
await chatClient.disconnect();
that.isAiReplying = false;
that.isPlayingVoice = false;
that.isRecording = false;
setTimeout(() => {
uni.navigateBack();
}, 1000);
}
}
});
}
}
};
</script>
HTML代码如下:
<view class="chat-container">
<!-- 渐变圆形 + 小圆点 -->
<view class="voice-circle">
<view class="voice-dots" :class="{
recording: isRecording,
replying: isAiReplying && isPlayingVoice
}">
<view class="dot" v-for="i in 10" :key="i"></view>
</view>
</view>
<!-- 按钮容器 -->
<view class="btn-group">
<view class="anniu start-btn" @click="streamChat" :class="{
disabled: isRecording, // 仅录音中禁用,语音播报时可点击
recording: isRecording
}">
<image src="/static/imgs/tel1.png" mode="widthFix" style="width: 40rpx;height: auto;" v-if="isRecording"></image>
<image src="/static/imgs/tel2.png" mode="widthFix" style="width: 40rpx;height: auto;" v-if="!isRecording"></image>
</view>
<view class="status-text">
{{ isRecording ? '正在听' : (isAiReplying && isPlayingVoice ? '智能回复中' : '点击麦克风开始对话') }}
</view>
<view class="anniu stop-btn" @click="stopAiReply">
<image src="/static/imgs/tel.png" mode="widthFix" style="width: 60rpx;height: auto;"></image>
</view>
</view>
</view>
</template>
css代码如下:
.chat-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
background-color: #ffffff;
padding: 0 30px;
box-sizing: border-box;
}
.voice-circle {
width: 200px;
height: 200px;
border-radius: 50%;
background: linear-gradient(135deg, #409EFF 0%, #13C2C2 100%);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 200px;
}
.voice-dots {
display: flex;
gap: 4px;
align-items: center;
opacity: 1;
}
.voice-dots .dot {
width: 4px;
height: 4px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.9);
animation: none;
}
.voice-dots.recording .dot {
animation: voice-shake 1s infinite ease-in-out !important;
}
.voice-dots.replying .dot {
animation: voice-shake 1.2s infinite ease-in-out !important;
}
@keyframes voice-shake {
0%, 100% { transform: scale(1); opacity: 0.8; }
50% { transform: scale(2); opacity: 1; }
}
.voice-dots .dot:nth-child(1) { animation-delay: 0s; }
.voice-dots .dot:nth-child(2) { animation-delay: 0.1s; }
.voice-dots .dot:nth-child(3) { animation-delay: 0.2s; }
.voice-dots .dot:nth-child(4) { animation-delay: 0.3s; }
.voice-dots .dot:nth-child(5) { animation-delay: 0.4s; }
.voice-dots .dot:nth-child(6) { animation-delay: 0.5s; }
.voice-dots .dot:nth-child(7) { animation-delay: 0.6s; }
.voice-dots .dot:nth-child(8) { animation-delay: 0.7s; }
.voice-dots .dot:nth-child(9) { animation-delay: 0.8s; }
.voice-dots .dot:nth-child(10) { animation-delay: 0.9s; }
.status-text {
font-size: 24rpx;
color: #333333;
font-weight: 500;
transition: all 0.3s ease;
text-align: center;
}
.btn-group {
display: flex;
justify-content: space-between;
width: 630rpx;
align-items: center;
position: fixed;
bottom: 100rpx;
left: 30px;
}
.anniu {
text-align: center;
color: #ffffff;
border-radius: 30px;
height: 128rpx;
width: 128rpx;
line-height: 128rpx;
font-size: 16px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.start-btn {
background: rgba(240, 240, 240, 1);
color: #000;
}
.stop-btn {
background: rgba(240, 240, 240, 1);
color: #000;
}
.stop-btn.active {
background: #FF4D4F;
color: #fff;
}
</style>
样式随便写的,根据您自己的需求而定;