uni-app官网
一、创建uni-app
我用的是vue-cli命令行创建uniapp项目。
踩坑1:执行命令报错了
npm ERR! Darwin 20.6.0
npm ERR! argv "/Users/zhuzhu/.nvm/versions/node/v6.2.0/bin/node" "/Users/zhuzhu/.nvm/versions/node/v6.2.0/bin/npm" "install"
npm ERR! node v6.2.0
npm ERR! npm v3.8.9
npm ERR! This request requires auth credentials. Run `npm login` and repeat the request.
npm ERR!
npm ERR! If you need help, you may report this error at:
npm ERR! <https://github.com/npm/npm/issues>
npm ERR! Please include the following file with any support request:
npm ERR! /Users/zhuzhu/Downloads/uni-preset-vue-vite/npm-debug.log
解决:直接访问官网的gitee,下载模板,然后npm install,之后在npm run XX运行你想要的程序就好啦。
二、开发聊天功能
实现思路
之前开发的是网页版的,现在要改成小程序,接口是算法已经写好的,直接拿来了。前端这块实现最重要的是success回调里的代码,接口返回的是流式(如图一),然后前端通过截取最后一次对话内容,通过startTyping方法实现打字机效果
图一上代码(样式和方法可直接copy用)
<template> <view class="main-dislogue"> <view class="header-suspension"> <view class="record-btn">悬浮</view> </view> <view class="content" ref="QAContent"> <scroll-view id="scrollpage" :scroll-top="scrollTop" :scroll-y="true"> <view v-for="item in dest" :key="item.id" id="msglistview"> <view class="ask" v-if="item.flag != 1"> <view class="ask-text"> <view class="ask-desc" style="word-break: break-all;"> {{ item.content }} </view> </view> <text class="ask-bulge"></text> <view class="ask-avatar"> <image class="ask-sex" v-if="sex == 1" src="/static/boy.png" fit="contain"></image> <image class="ask-sex" v-if="sex == 2" src="/static/girl.png" fit="contain"></image> </view> </view> <view class="answer"> <view class="answer-avatar"> <image class="answer-ai" src="/static/ai.png" fit="contain"></image> </view> <text class="answer-bulge"></text> <view class="answer-text"> <view class="answer-desc" ref="copyAiContent">{{item.ai_content}}</view> </view> </view> </view> </scroll-view> </view> <view class="bottom"> <input :cursorSpacing="20" class="bottom-input" name="name" placeholder="请输入" v-model="value"/> <button class="bottom-button" type="primary" :disabled="isSend" @click="handleSend">发送</button> </view> </view></template>
<style>.main-dislogue { height: calc(100vh - 70px); background: #f5f5f5; display: flex; flex-direction: column;}/* 头部悬浮 */.header-suspension { width: 100rpx; height: 300rpx; /* pointer-events: none; */ z-index: 100; position: fixed; right: 10rpx; bottom: 300rpx;}.head-image { width: 74rpx; height: 74rpx; z-index: 99; background: #d4d4d4; border-radius: 50%; padding: 6rpx; box-shadow: 0px 2rpx 20rpx rgba(0, 0, 0, 0.5);}.record-btn { width: 74rpx; height: 74rpx; background: #FFFFFF; border-radius: 50%; font-size: 26rpx; text-align: center; padding: 6rpx; box-shadow: 0px 2rpx 20rpx rgba(0, 0, 0, 0.5); color: #4A90E2; margin-top: 29rpx;}/* 内容 */.content { padding: 12rpx; padding-bottom: 100px; background: #f5f5f5;}/* #scrollpage {} *//* 问 */.ask { display: flex; justify-content: flex-end; width: 100%; margin-top: 6rpx;}.ask-avatar { width: 120rpx; margin-top: 20rpx;}.ask-sex { width: 100rpx; height: 100rpx;}.ask-bulge { position: relative; top: 41rpx; right: 23rpx; display: block; width: 0; height: 0; border: 15rpx solid #38a579; transform: rotate(45deg);}.ask-text { z-index: 1;}.ask-desc { background: #38a579; border-radius: 13rpx; padding: 15rpx; line-height: 58rpx; margin-top: 27rpx; white-space: pre-line; word-break: break-all; color: #fff; margin-left: 124rpx;}/* 答 */.answer { display: flex; justify-content: flex-start; margin-top: 6rpx;}.answer-avatar { width: 120rpx; margin-top: 20rpx;}.answer-ai { width: 100rpx; height: 100rpx;}.answer-bulge { position: relative; top: 41rpx; left: 23rpx; display: block; width: 0; height: 0; border: 15rpx solid #ffffff; transform: rotate(45deg);}.answer-text { z-index: 1;}.answer-desc { margin-right: 88rpx; border-radius: 13rpx; line-height: 58rpx; background: #fff; margin-top: 27rpx; tab-size: 12rpx; padding: 15rpx; white-space: pre-wrap; box-shadow: 0rpx 5rpx 47rpx 0rpx #97979773;}/* 尾部 */.bottom { border-top: 2rpx solid #CCCCCC; background: #f5f5f5; display: flex; padding: 10rpx; padding-bottom: 50rpx; position: fixed; bottom: 0; z-index: 99; width: 100%;}.bottom-input { flex: 1; font-size: 35rpx; border-radius: 10rpx; background: #FFFFFF; padding: 17rpx;}.bottom-button { width: 190rpx; height: 80rpx; font-size: 14px; line-height: 80rpx; margin-left: 20rpx; background: #4A90E2 !important;}</style>
<script>import Api from "@/utils/api.js";import base from '@/utils/base.js';const BASE_URL = base.baseUrl;const recorderManager = uni.getRecorderManager()export default { data() { return { sex: "", birthDate: "", generateRecordsFlag: false, dest: [], dialogue_code: "", value: "", isSend: false, scrollTop: 0, currentText: "", isSpeaking: false } }, onLoad(option) { this.sex = option.sex; this.birthDate = option.birthDate; this.dialogue_code = option.dialogue_code; }, onReady() { let _this = this; uni.getStorage({ key: 'gpt_h5_dialogue', success: function (res) { let list = res.data || ""; if (list.length) { this.dest = JSON.parse(list); if (this.dest.length >= 2) { this.generateRecordsFlag = true; } } else { setTimeout(() => { _this.handleSend(); }, 500) } } }); }, methods: { // 年龄转换 ageCalculation(date) { var today = new Date(); // 获取出生日期 var birthDate = new Date(date); // 假设出生日期为1990年1月1日 // 计算年龄 var age = today.getFullYear() - birthDate.getFullYear(); var m = today.getMonth(), d = today.getDate(); if (m < birthDate.getMonth()) { age--; } else if (m === birthDate.getMonth() && d < birthDate.getDate()) { age--; } return age; }, // 发送聊天 async handleSend() { this.preEventSource && this.preEventSource?.close(); if (this.dest.length != 0 && !this.value) { return; } let _this = this; let { prompt, model } = await Api.getPromptList({ type: 1 }); let sex = this.sex == 1 ? "男" : "女"; let age = this.ageCalculation(this.birthDate); prompt = prompt.replace('{age}', `${age}岁`).replace('{sex}', `${sex}性`); let obj = { ai_content: "...", chat_model: model, content: prompt, create_time: "2024-01-05T06:55:29.000Z", dialogue_code: this.dialogue_code, id: 450, req_time: "2024-01-05T06:55:30.000Z", res_time: null, tags: null, user_code: "00468", flag: 1 }; const diaObj = { content: this.value, ai_content: "...", chat_model: model, create_time: new Date(), dialogue_code: this.dialogue_code, id: Date.now(), tags: null, user_code: "00468", loading: false, flag: 2 }; if (this.dest.length == 0) { // 第一次 this.dest.push(obj); } else { this.dest.push(diaObj); } let params = { "dialogue_code": this.dialogue_code, "content": this.value || this.dest[0].content, "chat_model": model } this.isSend = true; _this.scrollToBottom(); let ai_content = "", startFlag = false; this.value = ""; // 置空输入框 // 从这往上可以忽略,这是我业务逻辑,不必关注。重点是uni.request success回调内容 uni.request({ url: `${BASE_URL}/hmgpt/dialogue`, data: params, method: "POST", headers: { "Content-Type": 'application/json', }, success: (res) => { let str = JSON.stringify(res.data); // 将字符串按"data: ["分割,然后取最后一个部分 const lastDataSection = str.split("data: [").pop(); // 截取最后一个JSON对象的部分 const lastJsonString = lastDataSection.split("]")[0].replace(/\\/g, ''); // 解析JSON字符串 const lastJsonObject = JSON.parse(lastJsonString); // 获取ai_content的值 const lastAiContent = lastJsonObject.ai_content; console.log(lastAiContent, 'lastAiContent'); ai_content = lastAiContent; if (lastAiContent == "") { // 返回空,则默认提示 ai_content = "目前公司GPU服务器有限,会因为调试需要临时中断出现服务不可用,请稍后重试。"; } _this.dest[_this.dest.length - 1].ai_content = ""; if (!startFlag) { startFlag = true startTyping(); } } }); function startTyping() { let currentIndex = 0; const typingSpeed = 100; // 打字速度,单位:毫秒 const timer = setInterval(() => { _this.dest[_this.dest.length - 1].ai_content += ai_content[currentIndex]; currentIndex++; _this.scrollToBottom(); if (currentIndex >= ai_content.length) { clearInterval(timer); _this.isSend = false; } }, typingSpeed); uni.setStorage({ key: 'gpt_h5_dialogue', data: JSON.stringify(_this.dest), success: function () { } }); } }, // 滚动至聊天底部 scrollToBottom() { this.$nextTick(() => { const query = uni.createSelectorQuery(); query.select('#scrollpage').boundingClientRect(); query.exec(res => { this.scrollTop = res[0].height; uni.pageScrollTo({ scrollTop: res[0].height + 170, // 将滚动位置设置为顶部 duration: 300 // 滚动到顶部的动画时长,单位为毫秒 }); }) }) } },}</script>
效果图
打字机效果可以自行试试哈,整体页面大概是这个样子