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.

1135 lines
40 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view>
<view class="content" @touchstart="hideDrawer">
<scroll-view class="msg-list" scroll-y="true" :scroll-with-animation="scrollAnimation"
:scroll-top="scrollTop" :scroll-into-view="scrollToView" @scrolltoupper="loadHistory"
upper-threshold="50">
<!-- 加载历史数据waitingUI -->
<view class="loading" v-if="loadingVisible">
<view class="spinner">
<view class="rect1"></view>
<view class="rect2"></view>
<view class="rect3"></view>
<view class="rect4"></view>
<view class="rect5"></view>
</view>
</view>
<view class="row" v-for="(row, index) in msgList" :key="index" :id="'msg' + row.msg.id">
<!-- 系统消息 -->
<block v-if="row.type == 'system'">
<view class="system">
<!-- 文字消息 -->
<view v-if="row.msg.type == 'text'" class="text">
{{ row.msg.content.text }}
</view>
<!-- 领取红包消息 -->
<view v-if="row.msg.type == 'redEnvelope'" class="red-envelope">
<image src="/static/img/red-envelope-chat.png"></image>
{{ row.msg.content.text }}
</view>
</view>
</block>
<!-- 用户消息 -->
<block v-if="row.type == 'user'">
<!-- 自己发出的消息 -->
<view class="my" v-if="row.msg.userinfo.uid == myuid">
<!-- 左-消息 -->
<view class="left">
<!-- 文字消息 -->
<view v-if="row.msg.type == 'text'" class="bubble">
<rich-text :nodes="row.msg.content.text"></rich-text>
</view>
<!-- 语言消息 -->
<view v-if="row.msg.type == 'voice'" class="bubble voice" @tap="playVoice(row.msg)"
:class="playMsgid == row.msg.id ? 'play' : ''">
<view class="length">{{ row.msg.content.length }}</view>
<view class="icon my-voice"></view>
</view>
<!-- 图片消息 -->
<view v-if="row.msg.type == 'img'" class="bubble img" @tap="showPic(row.msg)">
<image :src="row.msg.content.url"
:style="{ 'width': row.msg.content.w + 'px', 'height': row.msg.content.h + 'px' }">
</image>
</view>
</view>
<!-- 右-头像 -->
<view class="right">
<image :src="row.msg.userinfo.face"></image>
</view>
</view>
<!-- 别人发出的消息 -->
<view class="other" v-if="row.msg.userinfo.uid != myuid">
<!-- 左-头像 -->
<view class="left">
<image :src="row.msg.userinfo.face"></image>
</view>
<!-- 右-用户名称-时间-消息 -->
<view class="right">
<view class="username">
<view class="name">{{ row.msg.userinfo.username }}</view>
<view class="time">{{ row.msg.time }}</view>
</view>
<!-- 文字消息 -->
<view v-if="row.msg.type == 'text'" class="bubble">
<ver-response v-if="row.msg.md == '1'" theme="none"
:content="row.msg.content.text"></ver-response>
<rich-text v-if="row.msg.md != '1'" :nodes="row.msg.content.text"></rich-text>
<!-- <rich-text v-if="row.msg.md == '1'">
<ver-response theme="none" :content="row.msg.content.text"></ver-response>
</rich-text>
<rich-text v-if="row.msg.md != '1'" :nodes="row.msg.content.text"></rich-text> -->
</view>
<!-- 加载动画消息 -->
<view v-if="row.msg.type == 'loading'" class="bubble loading-bubble">
<view class="loading-dots">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
</view>
<!-- 语音消息 -->
<view v-if="row.msg.type == 'voice'" class="bubble voice" @tap="playVoice(row.msg)"
:class="playMsgid == row.msg.id ? 'play' : ''">
<view class="icon other-voice"></view>
<view class="length">{{ row.msg.content.length }}</view>
</view>
<!-- 图片消息 -->
<view v-if="row.msg.type == 'img'" class="bubble img" @tap="showPic(row.msg)">
<image :src="row.msg.content.url"
:style="{ 'width': row.msg.content.w + 'px', 'height': row.msg.content.h + 'px' }">
</image>
</view>
</view>
</view>
</block>
</view>
</scroll-view>
</view>
<!-- 抽屉栏 -->
<view class="popup-layer" :class="popupLayerClass" @touchmove.stop.prevent="discard">
<!-- 表情 -->
<swiper class="emoji-swiper" :class="{ hidden: hideEmoji }" indicator-dots="true" duration="150">
<swiper-item v-for="(page, pid) in emojiList" :key="pid">
<view v-for="(em, eid) in page" :key="eid" @tap="addEmoji(em)">
<image mode="widthFix" :src="'/static/img/emoji/' + em.url"></image>
</view>
</swiper-item>
</swiper>
<!-- 更多功能 相册-拍照-红包 -->
<view class="more-layer" :class="{ hidden: hideMore }">
<view class="list">
<view class="box_grid">
<view class="box" @tap="chooseImage">
<view class="icon tupian2"></view>
</view>
<view>相册</view>
</view>
<view class="box_grid">
<view class="box" @tap="camera">
<view class="icon paizhao"></view>
</view>
<view>拍摄</view>
</view>
<view class="box_grid">
<view class="box" @tap="handAlbumQ">
<view class="New-icon saoti"></view>
</view>
<view>选图做题</view>
</view>
<view class="box_grid">
<view class="box" @tap="handCameraQ">
<view class="New-icon saoti"></view>
</view>
<view>拍摄做题</view>
</view>
</view>
</view>
</view>
<!-- 底部输入栏 -->
<view class="input-box" :class="popupLayerClass" @touchmove.stop.prevent="discard">
<!-- H5下不能录音输入栏布局改动一下 -->
<!-- #ifndef H5 -->
<view class="voice">
<view class="icon" :class="isVoice ? 'jianpan' : 'yuyin'" @tap="switchVoice"></view>
</view>
<!-- #endif -->
<!-- #ifdef H5 -->
<view class="more" @tap="showMore">
<view class="icon add"></view>
</view>
<!-- #endif -->
<view class="textbox">
<view class="voice-mode" :class="[isVoice ? '' : 'hidden', recording ? 'recording' : '']"
@touchstart="voiceBegin" @touchmove.stop.prevent="voiceIng" @touchend="voiceEnd"
@touchcancel="voiceCancel">{{ voiceTis }}</view>
<view class="text-mode" :class="isVoice ? 'hidden' : ''">
<view class="box">
<textarea auto-height="true" v-model="textMsg" @focus="textareaFocus" />
</view>
<!-- <view class="em" @tap="chooseEmoji">
<view class="icon biaoqing"></view>
</view> -->
</view>
</view>
<!-- #ifndef H5 -->
<view class="more" @tap="showMore">
<view class="icon add"></view>
</view>
<!-- #endif -->
<view class="send" :class="isVoice ? 'hidden' : ''" @tap="sendText">
<view class="btn">发送</view>
</view>
</view>
<!-- 录音UI效果 -->
<view class="record" :class="recording ? '' : 'hidden'">
<view class="ing" :class="willStop ? 'hidden' : ''">
<view class="icon luyin2"></view>
</view>
<view class="cancel" :class="willStop ? '' : 'hidden'">
<view class="icon chehui"></view>
</view>
<view class="tis" :class="willStop ? 'change' : ''">{{ recordTis }}</view>
</view>
</view>
</template>
<script>
import utf8Array2Str from 'utf8array2str';
const plugin = requirePlugin("WechatSI");
const manager = plugin.getRecordRecognitionManager();
import permision from '../../common/permission.js';
import config from '@/common/config.js';
import {
getAIReply,
getChatLog,
getOSSInfo,
getImageReco
} from '@/api/services/index.js';
export default {
data() {
return {
//文字消息
textMsg: '',
//消息列表
isHistoryLoading: false,
scrollAnimation: false,
loadingVisible: false,
scrollTop: 0,
scrollToView: '',
msgList: [],
msgImgList: [],
myuid: 0,
person_id: '',
voiceStartTime: 0, // 语音开始时间戳
voiceDuration: 0, // 语音持续时间(毫秒)
page_num: 1,
page_size: 10,
//录音相关参数
// #ifndef H5
//H5不能录音
RECORDER: uni.getRecorderManager(),
// #endif
isVoice: false,
voiceTis: '按住 说话',
recordTis: "手指上滑 取消发送",
recording: false,
willStop: false,
initPoint: { identifier: 0, Y: 0 },
recordTimer: null,
recordLength: 0,
//播放语音相关参数
AUDIO: uni.createInnerAudioContext(),
playMsgid: null,
VoiceTimer: null,
// 抽屉参数
popupLayerClass: '',
// more参数
hideMore: true,
//表情定义
hideEmoji: true,
emojiList: [
[{ "url": "100.gif", alt: "[微笑]" }, { "url": "101.gif", alt: "[伤心]" }, { "url": "102.gif", alt: "[美女]" }, { "url": "103.gif", alt: "[发呆]" }, { "url": "104.gif", alt: "[墨镜]" }, { "url": "105.gif", alt: "[哭]" }, { "url": "106.gif", alt: "[羞]" }, { "url": "107.gif", alt: "[哑]" }, { "url": "108.gif", alt: "[睡]" }, { "url": "109.gif", alt: "[哭]" }, { "url": "110.gif", alt: "[囧]" }, { "url": "111.gif", alt: "[怒]" }, { "url": "112.gif", alt: "[调皮]" }, { "url": "113.gif", alt: "[笑]" }, { "url": "114.gif", alt: "[惊讶]" }, { "url": "115.gif", alt: "[难过]" }, { "url": "116.gif", alt: "[酷]" }, { "url": "117.gif", alt: "[汗]" }, { "url": "118.gif", alt: "[抓狂]" }, { "url": "119.gif", alt: "[吐]" }, { "url": "120.gif", alt: "[笑]" }, { "url": "121.gif", alt: "[快乐]" }, { "url": "122.gif", alt: "[奇]" }, { "url": "123.gif", alt: "[傲]" }],
[{ "url": "124.gif", alt: "[饿]" }, { "url": "125.gif", alt: "[累]" }, { "url": "126.gif", alt: "[吓]" }, { "url": "127.gif", alt: "[汗]" }, { "url": "128.gif", alt: "[高兴]" }, { "url": "129.gif", alt: "[闲]" }, { "url": "130.gif", alt: "[努力]" }, { "url": "131.gif", alt: "[骂]" }, { "url": "132.gif", alt: "[疑问]" }, { "url": "133.gif", alt: "[秘密]" }, { "url": "134.gif", alt: "[乱]" }, { "url": "135.gif", alt: "[疯]" }, { "url": "136.gif", alt: "[哀]" }, { "url": "137.gif", alt: "[鬼]" }, { "url": "138.gif", alt: "[打击]" }, { "url": "139.gif", alt: "[bye]" }, { "url": "140.gif", alt: "[汗]" }, { "url": "141.gif", alt: "[抠]" }, { "url": "142.gif", alt: "[鼓掌]" }, { "url": "143.gif", alt: "[糟糕]" }, { "url": "144.gif", alt: "[恶搞]" }, { "url": "145.gif", alt: "[什么]" }, { "url": "146.gif", alt: "[什么]" }, { "url": "147.gif", alt: "[累]" }],
[{ "url": "148.gif", alt: "[看]" }, { "url": "149.gif", alt: "[难过]" }, { "url": "150.gif", alt: "[难过]" }, { "url": "151.gif", alt: "[坏]" }, { "url": "152.gif", alt: "[亲]" }, { "url": "153.gif", alt: "[吓]" }, { "url": "154.gif", alt: "[可怜]" }, { "url": "155.gif", alt: "[刀]" }, { "url": "156.gif", alt: "[水果]" }, { "url": "157.gif", alt: "[酒]" }, { "url": "158.gif", alt: "[篮球]" }, { "url": "159.gif", alt: "[乒乓]" }, { "url": "160.gif", alt: "[咖啡]" }, { "url": "161.gif", alt: "[美食]" }, { "url": "162.gif", alt: "[动物]" }, { "url": "163.gif", alt: "[鲜花]" }, { "url": "164.gif", alt: "[枯]" }, { "url": "165.gif", alt: "[唇]" }, { "url": "166.gif", alt: "[爱]" }, { "url": "167.gif", alt: "[分手]" }, { "url": "168.gif", alt: "[生日]" }, { "url": "169.gif", alt: "[电]" }, { "url": "170.gif", alt: "[炸弹]" }, { "url": "171.gif", alt: "[刀子]" }],
[{ "url": "172.gif", alt: "[足球]" }, { "url": "173.gif", alt: "[瓢虫]" }, { "url": "174.gif", alt: "[翔]" }, { "url": "175.gif", alt: "[月亮]" }, { "url": "176.gif", alt: "[太阳]" }, { "url": "177.gif", alt: "[礼物]" }, { "url": "178.gif", alt: "[抱抱]" }, { "url": "179.gif", alt: "[拇指]" }, { "url": "180.gif", alt: "[贬低]" }, { "url": "181.gif", alt: "[握手]" }, { "url": "182.gif", alt: "[剪刀手]" }, { "url": "183.gif", alt: "[抱拳]" }, { "url": "184.gif", alt: "[勾引]" }, { "url": "185.gif", alt: "[拳头]" }, { "url": "186.gif", alt: "[小拇指]" }, { "url": "187.gif", alt: "[拇指八]" }, { "url": "188.gif", alt: "[食指]" }, { "url": "189.gif", alt: "[ok]" }, { "url": "190.gif", alt: "[情侣]" }, { "url": "191.gif", alt: "[爱心]" }, { "url": "192.gif", alt: "[蹦哒]" }, { "url": "193.gif", alt: "[颤抖]" }, { "url": "194.gif", alt: "[怄气]" }, { "url": "195.gif", alt: "[跳舞]" }],
[{ "url": "196.gif", alt: "[发呆]" }, { "url": "197.gif", alt: "[背着]" }, { "url": "198.gif", alt: "[伸手]" }, { "url": "199.gif", alt: "[耍帅]" }, { "url": "200.png", alt: "[微笑]" }, { "url": "201.png", alt: "[生病]" }, { "url": "202.png", alt: "[哭泣]" }, { "url": "203.png", alt: "[吐舌]" }, { "url": "204.png", alt: "[迷糊]" }, { "url": "205.png", alt: "[瞪眼]" }, { "url": "206.png", alt: "[恐怖]" }, { "url": "207.png", alt: "[忧愁]" }, { "url": "208.png", alt: "[眨眉]" }, { "url": "209.png", alt: "[闭眼]" }, { "url": "210.png", alt: "[鄙视]" }, { "url": "211.png", alt: "[阴暗]" }, { "url": "212.png", alt: "[小鬼]" }, { "url": "213.png", alt: "[礼物]" }, { "url": "214.png", alt: "[拜佛]" }, { "url": "215.png", alt: "[力量]" }, { "url": "216.png", alt: "[金钱]" }, { "url": "217.png", alt: "[蛋糕]" }, { "url": "218.png", alt: "[彩带]" }, { "url": "219.png", alt: "[礼物]" },]
],
//表情图片图床名称 ,由于我上传的第三方图床名称会有改变,所以有此数据来做对应,您实际应用中应该不需要
onlineEmoji: { "100.gif": "AbNQgA.gif", "101.gif": "AbN3ut.gif", "102.gif": "AbNM3d.gif", "103.gif": "AbN8DP.gif", "104.gif": "AbNljI.gif", "105.gif": "AbNtUS.gif", "106.gif": "AbNGHf.gif", "107.gif": "AbNYE8.gif", "108.gif": "AbNaCQ.gif", "109.gif": "AbNN4g.gif", "110.gif": "AbN0vn.gif", "111.gif": "AbNd3j.gif", "112.gif": "AbNsbV.gif", "113.gif": "AbNwgs.gif", "114.gif": "AbNrD0.gif", "115.gif": "AbNDuq.gif", "116.gif": "AbNg5F.gif", "117.gif": "AbN6ET.gif", "118.gif": "AbNcUU.gif", "119.gif": "AbNRC4.gif", "120.gif": "AbNhvR.gif", "121.gif": "AbNf29.gif", "122.gif": "AbNW8J.gif", "123.gif": "AbNob6.gif", "124.gif": "AbN5K1.gif", "125.gif": "AbNHUO.gif", "126.gif": "AbNIDx.gif", "127.gif": "AbN7VK.gif", "128.gif": "AbNb5D.gif", "129.gif": "AbNX2d.gif", "130.gif": "AbNLPe.gif", "131.gif": "AbNjxA.gif", "132.gif": "AbNO8H.gif", "133.gif": "AbNxKI.gif", "134.gif": "AbNzrt.gif", "135.gif": "AbU9Vf.gif", "136.gif": "AbUSqP.gif", "137.gif": "AbUCa8.gif", "138.gif": "AbUkGQ.gif", "139.gif": "AbUFPg.gif", "140.gif": "AbUPIS.gif", "141.gif": "AbUZMn.gif", "142.gif": "AbUExs.gif", "143.gif": "AbUA2j.gif", "144.gif": "AbUMIU.gif", "145.gif": "AbUerq.gif", "146.gif": "AbUKaT.gif", "147.gif": "AbUmq0.gif", "148.gif": "AbUuZV.gif", "149.gif": "AbUliF.gif", "150.gif": "AbU1G4.gif", "151.gif": "AbU8z9.gif", "152.gif": "AbU3RJ.gif", "153.gif": "AbUYs1.gif", "154.gif": "AbUJMR.gif", "155.gif": "AbUadK.gif", "156.gif": "AbUtqx.gif", "157.gif": "AbUUZ6.gif", "158.gif": "AbUBJe.gif", "159.gif": "AbUdIO.gif", "160.gif": "AbU0iD.gif", "161.gif": "AbUrzd.gif", "162.gif": "AbUDRH.gif", "163.gif": "AbUyQA.gif", "164.gif": "AbUWo8.gif", "165.gif": "AbU6sI.gif", "166.gif": "AbU2eP.gif", "167.gif": "AbUcLt.gif", "168.gif": "AbU4Jg.gif", "169.gif": "AbURdf.gif", "170.gif": "AbUhFS.gif", "171.gif": "AbU5WQ.gif", "172.gif": "AbULwV.gif", "173.gif": "AbUIzj.gif", "174.gif": "AbUTQs.gif", "175.gif": "AbU7yn.gif", "176.gif": "AbUqe0.gif", "177.gif": "AbUHLq.gif", "178.gif": "AbUOoT.gif", "179.gif": "AbUvYF.gif", "180.gif": "AbUjFU.gif", "181.gif": "AbaSSJ.gif", "182.gif": "AbUxW4.gif", "183.gif": "AbaCO1.gif", "184.gif": "Abapl9.gif", "185.gif": "Aba9yR.gif", "186.gif": "AbaFw6.gif", "187.gif": "Abaiex.gif", "188.gif": "AbakTK.gif", "189.gif": "AbaZfe.png", "190.gif": "AbaEFO.gif", "191.gif": "AbaVYD.gif", "192.gif": "AbamSH.gif", "193.gif": "AbaKOI.gif", "194.gif": "Abanld.gif", "195.gif": "Abau6A.gif", "196.gif": "AbaQmt.gif", "197.gif": "Abal0P.gif", "198.gif": "AbatpQ.gif", "199.gif": "Aba1Tf.gif", "200.png": "Aba8k8.png", "201.png": "AbaGtS.png", "202.png": "AbaJfg.png", "203.png": "AbaNlj.png", "204.png": "Abawmq.png", "205.png": "AbaU6s.png", "206.png": "AbaaXn.png", "207.png": "Aba000.png", "208.png": "AbarkT.png", "209.png": "AbastU.png", "210.png": "AbaB7V.png", "211.png": "Abafn1.png", "212.png": "Abacp4.png", "213.png": "AbayhF.png", "214.png": "Abag1J.png", "215.png": "Aba2c9.png", "216.png": "AbaRXR.png", "217.png": "Aba476.png", "218.png": "Abah0x.png", "219.png": "Abdg58.png" },
//红包相关参数
windowsState: '',
redenvelopeData: {
rid: null, //红包ID
from: null,
face: null,
blessing: null,
money: null
}
};
},
created() {
this.checkPermission();
this.page_num = 1;
},
onLoad(option) {
this.person_id = uni.getStorageSync('person_id');
this.getMsgList();
//语音自然播放结束
this.AUDIO.onEnded((res) => {
this.playMsgid = null;
});
// #ifndef H5
manager.onStart = () => {
console.log('==onStart==')
this.recordBegin();
};
manager.onStop = (res) => {
console.log('==onStop==')
this.recordEnd(res);
};
manager.onError = (err) => {
this.recording = false;
this.willStop = false;
this.voiceTis = '按住 说话';
this.recordTis = '手指上滑 取消发送'
uni.showToast({
title: '错误(' + err.retcode + '):无法识别语音!',
icon: 'none',
duration: 2000, //持续时间为 2秒
})
};
// #endif
},
onShow() {
// this.scrollTop = 9999999;
},
methods: {
// 接受消息(筛选处理)
screenMsg(msg) {
//从长连接处转发给这个方法,进行筛选处理
if (msg.type == 'system') {
// 系统消息
switch (msg.msg.type) {
case 'text':
this.addSystemTextMsg(msg);
break;
case 'redEnvelope':
this.addSystemRedEnvelopeMsg(msg);
break;
}
} else if (msg.type == 'user') {
// 用户消息
switch (msg.msg.type) {
case 'text':
this.addTextMsg(msg);
break;
case 'loading':
this.addTextMsg(msg);
break;
case 'voice':
this.addVoiceMsg(msg);
break;
case 'img':
this.addImgMsg(msg);
break;
case 'redEnvelope':
this.addRedEnvelopeMsg(msg);
break;
}
console.log('用户消息');
}
this.$nextTick(function () {
// 滚动到底
this.scrollToView = 'msg' + msg.msg.id
});
},
//触发滑动到顶部(加载历史信息记录)
loadHistory(e) {
if (this.isHistoryLoading) {
return;
}
this.page_num += 1;
this.loadingVisible = true;
this.isHistoryLoading = true;//参数作为进入请求标识,防止重复请求
this.scrollAnimation = false;//关闭滑动动画
let Viewid = this.msgList[0].msg.id;//记住第一个信息ID
//本地模拟请求历史记录效果
setTimeout(() => {
// 消息列表
let list = [
// { type: "user", msg: { id: 1, type: "text", time: "12:56", userinfo: { uid: 0, username: "大黑哥", face: "/static/img/face.jpg" }, content: { text: "为什么温度会相差那么大?" } } },
// { type: "user", msg: { id: 2, type: "text", time: "12:57", userinfo: { uid: 1, username: "售后客服008", face: "/static/img/im/face/face_2.jpg" }, content: { text: "这个是有偏差的,两个温度相差十几二十度是很正常的,如果相差五十度,那即是质量问题了。" } } },
// { type: "user", msg: { id: 3, type: "voice", time: "12:59", userinfo: { uid: 1, username: "售后客服008", face: "/static/img/im/face/face_2.jpg" }, content: { url: "/static/voice/1.mp3", length: "00:06" } } },
// { type: "user", msg: { id: 4, type: "voice", time: "13:05", userinfo: { uid: 0, username: "大黑哥", face: "/static/img/face.jpg" }, content: { url: "/static/voice/2.mp3", length: "00:06" } } },
]
getChatLog({
page: this.page_num,
page_size: this.page_size,
person_id: this.person_id
}).then((res) => {
const messages = [];
res.data.forEach(item => {
// 添加AI回复消息
if (item.output_type == 1) {
messages.push({
type: "user",
msg:
{
id: uni.$u.guid(),
type: "voice",
userinfo: { uid: 1, username: "芽芽星语", face: "/static/img/im/face/face_2.jpg" },
content: { url: item.audio_url, length: `${Math.ceil(item.duration)}''` }
}
});
} else if (item.output_type == 3) {
messages.push({
type: "user",
msg:
{
id: uni.$u.guid(),
type: "text",
userinfo: { uid: 1, username: "芽芽星语", face: "/static/img/im/face/face_2.jpg" },
content: { text: item.model_response }
}
});
} else {
messages.push({
type: "user",
msg:
{
id: uni.$u.guid(),
type: "text",
md: "1",
userinfo: { uid: 1, username: "芽芽星语", face: "/static/img/im/face/face_2.jpg" },
content: { text: item.model_response }
}
});
}
// 添加用户消息
if (item.input_type == 1) {
messages.push({
type: "user",
msg:
{
id: uni.$u.guid(),
type: "text",
userinfo: { uid: 0, username: "", face: "/static/img/face.jpg" },
content: { text: item.user_input }
}
});
} else {
// 先用默认尺寸添加图片消息
const msgId = uni.$u.guid();
messages.push({
type: "user",
msg: {
id: msgId,
type: "img",
userinfo: { uid: 0, username: "", face: "/static/img/face.jpg" },
content: {
url: item.user_input,
w: item.image_width,
h: item.image_height
}
}
});
}
});
this.loadingVisible = false;
// 获取消息中的图片,并处理显示尺寸
for (let i = 0; i < messages.length; i++) {
if (messages[i].type == 'user' && messages[i].msg.type == "img") {
messages[i].msg.content = this.setPicSize(messages[i].msg.content);
this.msgImgList.push(messages[i].msg.content.url);
}
}
list = messages;
// 获取消息中的图片,并处理显示尺寸
for (let i = 0; i < list.length; i++) {
if (list[i].type == 'user' && list[i].msg.type == "img") {
list[i].msg.content = this.setPicSize(list[i].msg.content);
this.msgImgList.unshift(list[i].msg.content.url);
}
list[i].msg.id = Math.floor(Math.random() * 1000 + 1);
this.msgList.unshift(list[i]);
}
//这段代码很重要,不然每次加载历史数据都会跳到顶部
this.$nextTick(function () {
this.scrollToView = 'msg' + Viewid;//跳转上次的第一行信息位置
this.$nextTick(function () {
this.scrollAnimation = true;//恢复滚动动画
});
});
this.isHistoryLoading = false;
}).catch((err) => {
console.log(err)
}).finally(() => {
// 滚动到底部
this.$nextTick(function () {
//进入页面滚动到底部
this.scrollTop = 9999;
this.$nextTick(function () {
this.scrollAnimation = true;
});
});
})
}, 1000)
},
// 加载初始页面消息
getMsgList() {
getChatLog({
page: this.page_num,
page_size: this.page_size,
person_id: this.person_id
}).then((res) => {
const messages = [];
res.data.forEach(item => {
// 添加用户消息
if (item.input_type == 1) {
messages.push({
type: "user",
msg:
{
id: uni.$u.guid(),
type: "text",
userinfo: { uid: 0, username: "", face: "/static/img/face.jpg" },
content: { text: item.user_input }
}
});
} else {
// 先用默认尺寸添加图片消息
const msgId = uni.$u.guid();
messages.push({
type: "user",
msg: {
id: msgId,
type: "img",
userinfo: { uid: 0, username: "", face: "/static/img/face.jpg" },
content: {
url: item.user_input,
w: item.image_width,
h: item.image_height
}
}
});
// 异步获取图片信息并更新
// uni.getImageInfo({
// src: item.user_input,
// success: (image) => {
// console.log(image)
// const index = this.msgList.findIndex(msg => msg.msg.id === msgId);
// console.log(index)
// if (index !== -1) {
// this.msgList[index].msg.content.w = image.width;
// this.msgList[index].msg.content.h = image.height;
// }
// console.log(this.msgList[index].msg.content.w)
// }
// });
}
// 添加AI回复消息
if (item.output_type == 1) {
messages.push({
type: "user",
msg:
{
id: uni.$u.guid(),
type: "voice",
time: item.create_time,
userinfo: { uid: 1, username: "芽芽星语", face: "/static/img/im/face/face_2.jpg" },
content: { url: item.audio_url, length: `${Math.ceil(item.duration)}''` }
}
});
} else if (item.output_type == 3) {
messages.push({
type: "user",
msg:
{
id: uni.$u.guid(),
type: "text",
time: item.create_time,
userinfo: { uid: 1, username: "芽芽星语", face: "/static/img/im/face/face_2.jpg" },
content: { text: item.model_response }
}
});
} else {
messages.push({
type: "user",
msg:
{
id: uni.$u.guid(),
md: "1",
type: "text",
time: item.create_time,
userinfo: { uid: 1, username: "芽芽星语", face: "/static/img/im/face/face_2.jpg" },
content: { text: item.model_response }
}
});
}
// 获取消息中的图片,并处理显示尺寸
for (let i = 0; i < messages.length; i++) {
if (messages[i].type == 'user' && messages[i].msg.type == "img") {
messages[i].msg.content = this.setPicSize(messages[i].msg.content);
this.msgImgList.push(messages[i].msg.content.url);
}
}
this.msgList = messages;
});
}).catch((err) => {
console.log(err)
}).finally(() => {
// 滚动到底部
// this.$nextTick(function () {
// //进入页面滚动到底部
// this.scrollTop = 9999;
// this.$nextTick(function () {
// this.scrollAnimation = true;
// });
// });
setTimeout(() => {
this.$nextTick(() => {
this.scrollTop = 9999999;
this.$nextTick(() => {
this.scrollAnimation = true;
});
});
}, 200);
})
},
//处理图片尺寸,如果不处理宽高,新进入页面加载图片时候会闪
setPicSize(content) {
// 让图片最长边等于设置的最大长度短边等比例缩小图片控件真实改变区别于aspectFit方式。
let maxW = uni.upx2px(350);//350是定义消息图片最大宽度
let maxH = uni.upx2px(350);//350是定义消息图片最大高度
if (content.w > maxW || content.h > maxH) {
let scale = content.w / content.h;
content.w = scale > 1 ? maxW : maxH * scale;
content.h = scale > 1 ? maxW / scale : maxH;
}
return content;
},
//更多功能(点击+弹出)
showMore() {
this.isVoice = false;
this.hideEmoji = true;
if (this.hideMore) {
this.hideMore = false;
this.openDrawer();
} else {
this.hideDrawer();
}
},
// 打开抽屉
openDrawer() {
this.popupLayerClass = 'showLayer';
},
// 隐藏抽屉
hideDrawer() {
this.popupLayerClass = '';
setTimeout(() => {
this.hideMore = true;
this.hideEmoji = true;
}, 150);
},
// 选择图片发送
chooseImage() {
this.getImage('album', '');
},
//拍照发送
camera() {
this.getImage('camera', '');
},
//选图做题
handAlbumQ() {
this.getImage('album', 'math');
},
//拍摄做题
handCameraQ() {
this.getImage('camera', 'math');
},
//选照片 or 拍照
getImage(type, flag) {
this.hideDrawer();
uni.chooseImage({
count: 1,
sourceType: [type],
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
success: (res) => {
let tempFilePath = res.tempFilePaths[0];
let fileExtension = tempFilePath.split('.').pop();
let keyName = uni.$u.guid() + '.' + fileExtension;
getOSSInfo().then((res) => {
const formData = {
key: 'Upload/' + keyName,
policy: res.policy,
'x-oss-signature-version': res.x_oss_signature_version,
'x-oss-credential': res.x_oss_credential,
'x-oss-date': res.x_oss_date,
'x-oss-signature': res.signature,
'x-oss-security-token': res.security_token,
success_action_status: "200"
};
uni.uploadFile({
url: config.OSS_HOST,
filePath: tempFilePath,
name: 'file',
formData: formData,
success: (uploadFileRes) => {
uni.getImageInfo({
src: tempFilePath,
success: (image) => {
let msg = { url: config.OSS_HOST + '/' + formData.key, w: image.width, h: image.height };
this.sendMsg(msg, 'img', flag);
}
});
}
});
}).catch((err) => {
uni.showToast({
title: '获取签名失败!',
icon: 'none',
duration: 2000, //持续时间为 2秒
})
});
}
});
},
// 选择表情
chooseEmoji() {
this.hideMore = true;
if (this.hideEmoji) {
this.hideEmoji = false;
this.openDrawer();
} else {
this.hideDrawer();
}
},
//添加表情
addEmoji(em) {
this.textMsg += em.alt;
},
//获取焦点如果不是选表情ing,则关闭抽屉
textareaFocus() {
if (this.popupLayerClass == 'showLayer' && this.hideMore == false) {
this.hideDrawer();
}
},
// 发送文字消息
sendText() {
this.hideDrawer();//隐藏抽屉
if (!this.textMsg) {
return;
}
let content = this.replaceEmoji(this.textMsg);
let msg = { text: this.textMsg }
this.sendMsg(msg, 'text', '');
this.textMsg = '';//清空输入框
},
//替换表情符号为图片
replaceEmoji(str) {
let replacedStr = str.replace(/\[([^(\]|\[)]*)\]/g, (item, index) => {
for (let i = 0; i < this.emojiList.length; i++) {
let row = this.emojiList[i];
for (let j = 0; j < row.length; j++) {
let EM = row[j];
if (EM.alt == item) {
let onlinePath = 'https://s2.ax1x.com/2019/04/12/'
let imgstr = '<img src="' + onlinePath + this.onlineEmoji[EM.url] + '">';
return imgstr;
}
}
}
});
return '<div style="display: flex;align-items: center;word-wrap:break-word;">' + replacedStr + '</div>';
},
sendMsg(content, type, flag) {
let lastid = uni.$u.guid();
let msg = { type: 'user', msg: { id: lastid, type: type, userinfo: { uid: 0, username: "", face: "/static/img/face.jpg" }, content: content } }
// 发送消息
this.screenMsg(msg);
// 先发送一个加载状态的消息(使用新类型 'loading'
let loadingMsgId = uni.$u.guid();
let loadingMsg = {
type: 'user',
msg: {
id: loadingMsgId,
type: 'loading', // 新的消息类型
userinfo: { uid: 1, username: "芽芽星语", face: "/static/img/im/face/face_2.jpg" },
content: {} // 内容可以为空,因为我们将在模板中直接渲染加载动画
}
}
this.screenMsg(loadingMsg);
if (type == 'text') {
getAIReply({
prompt: content.text,
person_id: this.person_id
}).then((res) => {
// 找到加载消息的索引
const loadingIndex = this.msgList.findIndex(item => item.msg.id === loadingMsgId);
if (loadingIndex !== -1) {
// 替换加载消息为实际回复
this.msgList.splice(loadingIndex, 1);
let _id = uni.$u.guid();
msg = { type: 'user', msg: { id: _id, type: 'voice', userinfo: { uid: 1, username: "芽芽星语", face: "/static/img/im/face/face_2.jpg" }, content: { url: res.url, length: `${Math.ceil(res.duration)}''` } } }
// 发送实际回复消息
this.screenMsg(msg);
}
}).catch((err) => {
const loadingIndex = this.msgList.findIndex(item => item.msg.id === loadingMsgId);
if (loadingIndex !== -1) {
// 替换加载消息为实际回复
this.msgList.splice(loadingIndex, 1);
let _id = uni.$u.guid();
msg = { type: 'user', msg: { id: _id, type: 'text', userinfo: { uid: 1, username: "芽芽星语", face: "/static/img/im/face/face_2.jpg" }, content: { text: '抱歉,获取回复失败,请重试' } } }
// 发送实际回复消息
this.screenMsg(msg);
}
})
} else {
let that = this;
const index = this.msgList.findIndex(msg => msg.msg.id === loadingMsgId);
let _path = '/aichat/recognize_content?image_url='
if (flag == 'math') {
_path = '/aichat/recognize_math?image_url='
}
console.log(config.BASE_URL + _path + content.url)
const requestTask = uni.request({
url: config.BASE_URL + _path + content.url,
method: 'GET',
timeout: 90000,
enableChunked: true, // 开启流式传输
responseType: 'text', // 指定响应的数据类型
header: {
Authorization: 'Bearer ' + uni.getStorageSync('jwt'),
},
success: (res) => {
console.log(this.msgList);
// 处理完整的响应数据
this.$nextTick(function () {
//进入页面滚动到底部
this.scrollTop = 9999;
this.$nextTick(function () {
this.scrollAnimation = true;
});
});
},
fail: (err) => {
// 处理错误情况
}
});
let accumulatedText = ""; // 存储未处理的流数据
requestTask.onHeadersReceived(function (res) {
console.log(res);
// 出现错误时返回
const loadingIndex = that.msgList.findIndex(item => item.msg.id === loadingMsgId);
if (loadingIndex !== -1) {
// 替换加载消息为实际回复
that.msgList.splice(loadingIndex, 1);
let _id = uni.$u.guid();
msg = { type: 'user', msg: { id: _id, type: 'text', userinfo: { uid: 1, username: "芽芽星语", face: "/static/img/im/face/face_2.jpg" }, content: { text: '抱歉,获取回复失败,请重试' } } }
// 发送实际回复消息
that.screenMsg(msg);
}
});
requestTask.onChunkReceived(function (res) {
// 处理接收到的数据块
// let decoder = new TextDecoder('utf-8');
// let chunkText = decoder.decode(new Uint8Array(res.data));
let chunkText = that.arrayBufferToStringManual(res.data);
accumulatedText += chunkText;
that.msgList[index].msg.type = 'text';
if (flag == 'math') {
that.msgList[index].msg.md = '1';
}
// that.msgList[index].msg.content.text = accumulatedText;
that.$set(that.msgList[index].msg.content, 'text', accumulatedText);
// 使用双重延迟确保渲染完成后滚动
setTimeout(() => {
that.$nextTick(() => {
// 强制重新计算布局后滚动
that.scrollTop = that.scrollTop + 100;
that.$nextTick(() => {
this.scrollAnimation = true;
that.scrollTop = 999999;
});
});
}, 10);
});
}
},
arrayBufferToStringManual(buffer) {
const uint8Array = new Uint8Array(buffer);
let str = '';
let i = 0;
while (i < uint8Array.length) {
const byte1 = uint8Array[i++];
if (byte1 < 0x80) {
// 单字节字符ASCII
str += String.fromCharCode(byte1);
} else if (byte1 >= 0xC0 && byte1 < 0xE0) {
// 双字节字符
const byte2 = uint8Array[i++];
str += String.fromCharCode(((byte1 & 0x1F) << 6) | (byte2 & 0x3F));
} else if (byte1 >= 0xE0 && byte1 < 0xF0) {
// 三字节字符(中文字符通常在这里)
const byte2 = uint8Array[i++];
const byte3 = uint8Array[i++];
str += String.fromCharCode(((byte1 & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F));
} else if (byte1 >= 0xF0 && byte1 < 0xF8) {
// 四字节字符
const byte2 = uint8Array[i++];
const byte3 = uint8Array[i++];
const byte4 = uint8Array[i++];
str += String.fromCharCode(((byte1 & 0x07) << 18) | ((byte2 & 0x3F) << 12) | ((byte3 & 0x3F) << 6) | (byte4 & 0x3F));
}
}
return str;
},
// 添加文字消息到列表
addTextMsg(msg) {
this.msgList.push(msg);
},
// 添加语音消息到列表
addVoiceMsg(msg) {
this.msgList.push(msg);
},
// 添加图片消息到列表
addImgMsg(msg) {
msg.msg.content = this.setPicSize(msg.msg.content);
this.msgImgList.push(msg.msg.content.url);
this.msgList.push(msg);
},
addRedEnvelopeMsg(msg) {
this.msgList.push(msg);
},
// 添加系统文字消息到列表
addSystemTextMsg(msg) {
this.msgList.push(msg);
},
sendSystemMsg(content, type) {
let lastid = this.msgList[this.msgList.length - 1].msg.id;
lastid++;
let row = { type: "system", msg: { id: lastid, type: type, content: content } };
this.screenMsg(row)
},
// 预览图片
showPic(msg) {
uni.previewImage({
indicator: "none",
current: msg.content.url,
urls: this.msgImgList
});
},
// 播放语音
playVoice(msg) {
this.playMsgid = msg.id;
this.AUDIO.src = msg.content.url;
this.$nextTick(function () {
this.AUDIO.play();
});
},
// 录音开始
voiceBegin(e) {
// 记录开始时间戳
this.voiceStartTime = Date.now();
if (e.touches.length > 1) {
return;
}
this.initPoint.Y = e.touches[0].clientY;
this.initPoint.identifier = e.touches[0].identifier;
if (!this.permisionState) {
this.checkPermission();
return;
}
manager.start({
duration: 60000,
lang: "zh_CN"
});
},
//录音开始UI效果
recordBegin() {
this.recording = true;
this.voiceTis = '松开 结束';
this.recordLength = 0;
this.recordTimer = setInterval(() => {
this.recordLength++;
}, 1000)
},
// 录音被打断
voiceCancel() {
this.recording = false;
this.voiceTis = '按住 说话';
this.recordTis = '手指上滑 取消发送'
this.willStop = true;//不发送录音
manager.stop();
},
// 录音中(判断是否触发上滑取消发送)
voiceIng(e) {
if (!this.recording) {
return;
}
let touche = e.touches[0];
//上滑一个导航栏的高度触发上滑取消发送
if (this.initPoint.Y - touche.clientY >= uni.upx2px(100)) {
this.willStop = true;
this.recordTis = '松开手指 取消发送'
} else {
this.willStop = false;
this.recordTis = '手指上滑 取消发送'
}
},
// 结束录音
voiceEnd(e) {
this.voiceDuration = Date.now() - this.voiceStartTime;
console.log('语音时长:', this.voiceDuration, '毫秒');
// if(this.voiceDuration<1000){
// uni.showToast({
// title: '录音时间太短',
// icon: 'none'
// });
// this.recording = false;
// this.willStop = false;
// this.voiceTis = '按住 说话';
// this.recordTis = '手指上滑 取消发送'
// return;
// }
if (!this.recording) {
return;
}
this.recording = false;
this.voiceTis = '按住 说话';
this.recordTis = '手指上滑 取消发送'
manager.stop();
},
//录音结束(回调文件)
recordEnd(res) {
clearInterval(this.recordTimer);
if (!this.willStop) {
// let content = this.replaceEmoji(res.result);
let msg = { text: res.result }
this.sendMsg(msg, 'text', '');
} else {
console.log('取消发送录音');
}
this.willStop = false;
},
// 切换语音/文字输入
switchVoice() {
this.hideDrawer();
this.isVoice = this.isVoice ? false : true;
},
discard() {
return;
},
async checkPermission() {
var that = this;
// #ifdef APP-PLUS
// 先判断os
let os = uni.getSystemInfoSync().osName;
if (os == 'ios') {
this.permisionState = await permision.judgeIosPermission('record');
} else {
this.permisionState = await permision.requestAndroidPermission('android.permission.RECORD_AUDIO');
}
if (this.permisionState !== true && this.permisionState !== 1) {
uni.showToast({
title: '请先授权使用录音',
icon: 'none'
});
return;
}
// #endif
// #ifdef MP-WEIXIN
uni.authorize({
scope: 'scope.record',
success(e) {
that.permisionState = true;
},
fail() {
uni.showToast({
title: '请授权使用录音',
icon: 'none'
});
}
});
// #endif
}
}
}
</script>
<style lang="scss">
@import "./style.scss";
</style>