You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
apply-assistant-v3/root/chat/index.vue

2553 lines
79 KiB
Vue

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 class="p-home-chat g_w_all g_bg_f_5">
<view class="chat-content g_clear_scroll">
<scroll-view :scroll-y="true" :scroll-top="stesttest" :scroll-with-animation="true"
:style="{
height: wh,
}"
:upper-threshold="200" @scrolltoupper ='getMore' @scroll="getScrollInfo"
>
<view class="g_flex_row_center g_fs_12 g_pt_12 g_pb_12 obj_1" v-show="list && list.length >= 10"
style="color: #b3b7bc;"
>
{{ isFinish == 1 ? "查看更多" : "已全部加载" }}
</view>
<view style="padding-top: 12px" class="chatBox obj_2">
<view class="g_mb_16 chat-item"
v-for="(item, index) in list" :key="index"
:class="item.robotTag == 1 ? '' : (item.robotTag == 0 ? 'g_flex_row_end' : 'g_flex_row_center')"
:id="'id_' + item.id"
>
<view class="m-item chat-left g_flex_row_start" v-if="item.robotTag == 1">
<view>
<view v-if="index + 1 == list.length && index > 0 && isShowLeftLoading" class="left_animate g_flex_row_start">
<view class="g_flex_row_start g_bg_f g_pl_16 g_pr_16 g_pt_10 g_pb_10"
style="border-radius: 12px;"
>
<view class="g_c_6 g_flex_column_center" style="font-size: 17px;" v-if="leftAnimate1">
搜索中
</view>
<view class="g_c_6 g_flex_column_center" style="font-size: 17px;" v-if="leftAnimate2">
已搜索{{jobnum}}个职位
</view>
<view class="g_c_6 g_flex_column_center" style="font-size: 17px;" v-if="leftAnimate3">
智能匹配中
</view>
<view class="g_flex_column_center" hover-class="none" hover-stop-propagation="false"
style="height: 25px;"
v-if="!leftAnimate2"
>
<view class="loader">
<text class="dot"></text>
<text class="dot"></text>
<text class="dot"></text>
</view>
</view>
</view>
</view>
<view v-else>
<view class="msg g_c_0"
:class="item.chat_type == 'markdown' ? '' : (item.chat_type ? 'g_bg_f g_pl_10 g_pr_10 g_pt_10 g_pb_10' : '')"
@longpress="handleLongPress(item)"
:style="{
'max-width':item.chat_type == 'text' ? (index == 0 && item.messageType == 98 && !helloText ? 'calc(100vw - 20px)' : '80vw') : 'calc(100vw - 20px)',
'min-width': item.chat_type == 'text' ? 'auto' : 'calc(100vw - 20px)',
}"
>
<view v-if="gptType == 'ai' || gptType == 'ai-text'">
<view class="" v-if="item.chat_type == 'text' && index == 0 && item.messageType == 98">
<g-chat-gree v-if="!helloText" />
<view v-if="helloText" style="font-size: 17px;">
你好,我是{{ helloText }}机器人
</view>
</view>
<view class="" v-if="item.chat_type == 'text' && index == 0 && item.messageType != 98">
<g-chat-message :cusMessage="item.msg" :isRender="0" />
</view>
<view class="" v-if="item.chat_type == 'text' && index > 0">
<g-chat-message :cusMessage="item.msg" v-if="index > 0" :isRender="isRender && index == list.length - 1 ? 1 : 0" />
</view>
<view class="" v-if="item.chat_type == 'markdown'">
<g-chat-markdown :cusList="item.comList"
:isbor="item.isRecordJobEnd ? 1 : 0"
@exportMore.stop="goHotMore(item)"
/>
<div v-if="item.isRecordJobEnd">
<view class="markgroup g_bg_f">
<div class="g_flex_row_between g_h_all"
style="margin: 0 10px;border-top: 1px solid #eee;width: calc(100% - 20px);">
<div class="g_flex_row_start" @click.stop="getReload(item)" style="width: 50%;">
<div class="g_flex_column_center g_pl_6 g_mr_4"
>
<i class="iconfont icon-sync" style="color: #666666;font-size: 12px;position: relative;top: 1px;"></i>
</div>
<div class="g_flex_column_center" style="color: #666666;font-size: 14px;">换一批</div>
</div>
<div class="g_flex_row_end" @click.stop="goHotMore(item)" style="width: 50%;">
<div class="g_flex_column_center" style="color: #666666;font-size: 14px;">查看更多</div>
<div class="g_flex_column_center g_pr_6 g_ml_4">
<i class="iconfont icon-gengduo11" style="color: #666666;font-size: 12px;"></i>
</div>
</div>
</div>
</view>
<view class="g_h_16" v-if="index == list.length - 1"></view>
</div>
</view>
</view>
</view>
</view>
<view class="g_h_16" v-if="index == list.length - 1"></view>
</view>
</view>
<view class="m-time g_text_c g_fs_12" v-else-if="item.robotTag == 2"
style="color: #b3b7bc;"
>
{{ formatMessageTime(item.message) }}
</view>
<view class="m-item chat-right" v-else style="max-width: 80vw">
<view class="msg g_bg_main g_c_f g_fs_17 g_pl_16 g_pr_16 g_pt_10 g_pb_10"
@longpress="handleLongPress(item)" style="min-height: 25px;"
v-if="!item.isHidden"
>
<g-chat-message :cusMessage="item.msg" style="max-width: 80%" v-if="item.msg" :data-msg='item.msg' />
<view v-if="index == (list.length - 1) && isShowRightLoading && !item.msg" class="right_animate g_flex_row_end g_mr_10 obj_4">
<view class="g_flex_row_end" style="border-radius: 12px;">
<view class="g_c_f g_flex_column_center" style="font-size: 17px;margin-right: 4px;" v-if="rightAnimate1">
识别中
</view>
<view class="g_c_f g_flex_column_center" style="font-size: 17px;margin-right: 4px;" v-if="rightAnimate2">
转写中
</view>
<view class="g_flex_column_center" style="height: 25px;">
<view class="loades">
<text class="dos"></text>
<text class="dos"></text>
<text class="dos"></text>
</view>
</view>
</view>
</view>
</view>
<view class="g_h_16" v-if="index == list.length - 1"></view>
</view>
</view>
</view>
</scroll-view>
</view>
<view class="chat-operate g_flex_c g_bg_f_5" :style="writeStyle">
<!-- {{wh}} === {{writeStyle}} -->
<view class="g_bg_f m-input g_flex_row_center g_position_rela"
:class="hasTopPadding && msgType == 'text' ? 'hasTopPadding' : ''" :style="{
height: msgType == 'voice' ? '56px' : `${textareaHeight + 16}px`,
maxHeight: '240px',
overflow: 'auto',
}" style="align-items: flex-end; min-height: 56px">
<!-- 左侧 -->
<view class="g_flex_none g_flex_c g_h_56 g_w_56 g_posi_relative" @click.stop="handleUpdateMsgType()"
:class="voiceStatus == 0 && spec ? 'g_bg_main' : voiceStatus == 1 ? 'g_bg_f0a' : voiceStatus == -2 ? 'g_c_t g_bg_main' : ''"
style="border-radius: 40px 0px 0px 40px;position: relative;" v-if="gptType != 'ai-text'">
<i class="iconfont"
:class="msgType == 'text' ? 'icon-huatongyuyin g_fs_22' : 'icon-weixinjianpan2 g_fs_26'"
style="position: absolute;"
:style="{
'left':msgType == 'text' ? '19px' :'17px',
'top':msgType == 'text' ? '17px' :'15px'
}"
v-if="voiceStatus == -1"></i>
</view>
<!-- 中部 -->
<view class="g_flex_1 g_flex_column_end">
<view v-if="msgType == 'text'" class="m-input-point g_w_all g_h_56 g_flex_column_end">
<div class="container g_flex_column_end g_h_32" ref="container">
<textarea auto-height cursor-spacing="50" class="g_fs_17 g_c_0"
:class="hasTopPadding ? 'g_mt_0' : ''" id="textarea" placeholder="请输入内容"
v-model="sendMsg"
style="position: absolute; width: calc(100vw - 140px); top: 50%; transform: translateY(-50%); padding: 0 0 0 0px"
@input="onInput" @focus="onFocus" @blur="onBlur"
:adjust-position="isIosWxapp ? false : true"
@keyboardheightchange="keyboardheightchange" @linechange="linechange" />
</div>
</view>
<view v-else>
<view v-if="voiceStatus == 0"
class="voice-tip g_flex_c"
style="color: #666666;
font-size: 12px;
position: fixed;
left: 50%;
transform: translateX(-50%);
bottom: calc(150rpx + constant(safe-area-inset-bottom));
bottom: calc(150rpx + env(safe-area-inset-bottom));
width: 100vw;
height: 30px;
background-color: rgba(237,237,237,1);
"
>
松手发送,上移取消
</view>
<view v-if="voiceStatus == 1"
class="voice-tip g_flex_c"
style="color: #fe0000;
font-size: 12px;
position: fixed;
left: 50%;
transform: translateX(-50%);
bottom: calc(150rpx + constant(safe-area-inset-bottom));
bottom: calc(150rpx + env(safe-area-inset-bottom));
width: 100vw;
height: 30px;
background-color: rgba(237,237,237,1);
"
>
松手取消
</view>
<view class="g_w_all g_h_56 m-voice-point g_flex_c g_fw_700 g_fs_17"
@touchstart="onTouchStart" @touchend="onTouchEnd" @touchmove="onTouchMove"
:class="voiceStatus == 1 ? 'g_c_f g_bg_f0a' : voiceStatus == 0 ? 'g_bg_main' : voiceStatus == -2 ? 'g_bg_main' : ''">
{{ voiceStatus == 1 ? "" : voiceStatus == -1 ? "按住 说话" : "" }}
</view>
</view>
</view>
<!-- 发送 -->
<view class="g_flex_none g_flex_c g_h_56 g_w_56 g_position_rela" @click.stop="handleSendMsg"
:class="voiceStatus == 0 && spec ? 'g_bg_main' : voiceStatus == 1 ? 'g_bg_f0a' : voiceStatus == -2 ? 'g_c_t g_bg_main' : ''"
style="border-radius: 0 40px 40px 0;position: relative;">
<i class="iconfont icon-fasong g_fs_32"
:class="sendIconStatus ? 'g_c_main' : voiceStatus == -2 ? 'g_c_t g_bg_main' : 'g_c_b'"
style="position: absolute;right: 13px;top: 12px;"
v-if="voiceStatus == -1"></i>
</view>
<!-- 清屏 -->
<i class="iconfont icon-close-circle g_fs_26 g_c_9"
style="position: absolute; right: 22px; top: 50%; transform: translateY(-80%)"
v-if="textareaHeight > 80 && msgType == 'text'" @click.stop="clearMsg"></i>
<!-- 录音交互动画 -->
<view class="longpress-mask" :data-status="voiceStatus" @touchend="onTouchEnd">
<view class="longpress-top-mask g_h_56 g_flex_c"
style="bottom: calc(12px + constant(safe-area-inset-bottom)); bottom: calc(12px + env(safe-area-inset-bottom))"
v-if="voiceStatus == 0 || voiceStatus == 1 || voiceStatus == -2">
<div class="column-voice g_flex_row_center">
<div v-for="(item, index) in 20" :key="index" class="g_flex_column_center">
<div class="column-item g_bg_f" :style="{
animation: 'voi_animate 1.5s infinite ' + 0.1 * index + 's',
'-webkit-animation': 'voi_animate 1.5s infinite ' + 0.1 * index + 's',
}"></div>
</div>
</div>
</view>
<!-- <view class="longpress-bottom-mask"
style="height: calc(12px + constant(safe-area-inset-bottom)); height: calc(12px + env(safe-area-inset-bottom))"
v-if="voiceStatus == 0 || voiceStatus == -2"></view> -->
</view>
</view>
</view>
<!-- <view class="g_w_50 g_h_50 g_flex_c"
style="position: fixed;right: 20px;bottom: 140px;border-radius: 50%;background-color: red;"
@click="delAll"
>
<i class="iconfont icon-clear g_c_f" style="font-size: 20px;"></i>
</view> -->
<gao-ChatSSEClient
ref="chatSSEClientRef"
@onOpen="openCore"
@onError="errorCore"
@onMessage="messageCore"
@onFinish="finishCore"
/>
</view>
</template>
<!-- #ifdef APP -->
<script module="yourModuleName" lang="renderjs">
//此模块内部只能用选项式API风格vue2、vue3均可用请照抄这段代码不可改成setup组合式API风格否则可能不能import vue导致编译失败
/**需要编译成App时你需要添加一个renderjs模块然后一模一样的import上面那些js微信的js除外
因为App中默认是在renderjsWebView中进行录音和音频编码
。如果配置了 RecordApp.UniWithoutAppRenderjs=true 且未调用依赖renderjs的功能时如nvue、可视化、仅H5中可用的插件
可不提供此renderjs模块同时逻辑层中需要将相关import的条件编译去掉**/
import 'recorder-core'
import RecordApp from 'recorder-core/src/app-support/app'
import './Recorder-UniCore/app-uni-support.js' //renderjs中似乎不支持"@/"打头的路径,如果编译路径错误请改正路径即可
//按需引入你需要的录音格式支持文件,和插件
import 'recorder-core/src/engine/mp3'
import 'recorder-core/src/engine/mp3-engine'
//按需引入你需要的录音格式支持文件如果需要多个格式支持把这些格式的编码引擎js文件统统引入进来即可
import "recorder-core/src/engine/wav";
import 'recorder-core/src/extensions/waveview'
export default {
mounted() {
//App的renderjs必须调用的函数传入当前模块this
RecordApp.UniRenderjsRegister(this);
},
methods: {
//这里定义的方法,在逻辑层中可通过 RecordApp.UniWebViewVueCall(this,'this.xxxFunc()') 直接调用
//调用逻辑层的方法,请直接用 this.$ownerInstance.callMethod("xxxFunc",{args}) 调用二进制数据需转成base64来传递
}
}
</script>
<!-- #endif -->
<script>
import Ajax from "../../utils/ajax.js";
import coziAjax from "../utils/cozi.js";
import gChatMessage from "../components/chatEvent/message.vue";
import gChatGree from "../components/chatEvent/greeting.vue";
import gChatMarkdown from "../components/chatEvent/markdown.vue";
import {
AsrClient
} from "../utils/AsrClient.js";
//必须引入的Recorder核心文件路径是 /src/recorder-core.js 下同使用import、require都行
import Recorder from "recorder-core"; //注意如果未引用Recorder变量可能编译时会被优化删除如vue3 tree-shaking请改成 import 'recorder-core',或随便调用一下 Recorder.a=1 保证强引用
//必须引入的RecordApp核心文件文件路径是 /src/app-support/app.js
import RecordApp from "recorder-core/src/app-support/app";
//所有平台必须引入的uni-app支持文件如果编译出现路径错误请把@换成 ../../ 这种)
import "./Recorder-UniCore/app-uni-support.js";
/** 需要编译成微信小程序时,引入微信小程序支持文件 **/
// #ifdef MP-WEIXIN
import "recorder-core/src/app-support/app-miniProgram-wx-support.js";
// #endif
/** H5、小程序环境中引入需要的格式编码器、可视化插件App环境中在renderjs中引入 **/
// 注意如果App中需要在逻辑层中调用Recorder的编码/转码功能,需要去掉此条件编译,否则会报未加载编码器的错误
// #ifdef H5 || MP-WEIXIN
//按需引入你需要的录音格式支持文件如果需要多个格式支持把这些格式的编码引擎js文件统统引入进来即可
import "recorder-core/src/engine/wav";
//可选的插件支持项,把需要的插件按需引入进来即可
import "recorder-core/src/extensions/waveview";
// #endif
var _wsTimer = null;
let defaultMsg = '你好我是伯才智能匹配AI大鹏可以帮老乡快速匹配工作支持语音输入。为了匹配更准确需多提供老乡需求信息例如:1. 性别2. 年龄3. 意向城市4. 工作要求(如吃住、班次等)示例有位32岁大姐想去常州找个长白班的工作。快告诉我老乡需求开始匹配吧!'
export default {
components: {
gChatMessage,
gChatGree,
gChatMarkdown,
},
onShareAppMessage() {
return this.G.shareFun();
},
data() {
return {
isHidden:false,
jobnum:0,
rightAnimate1:false,
rightAnimate2:false,
defaultChat:[
{
msg:defaultMsg,
chat_type:'text',
robotTag:1,
messageType:98
}
],
previousHeight:0,
stesttest:0,
writeStyle: 'bottom:0',
isIosWxapp: false,
scrollIntoView: '',
wh: uni.getSystemInfoSync().screenHeight + 'px',
isFinish: 1,
isFluency: false,
isAside: {
isShow: false,
},
content: "",
textareaHeight: 30,
initialHeight: 0,
isShowLeftLoading:false,
leftAnimate1:false,
leftAnimate2:false,
leftAnimate3:false,
localBaseImg: this.G.store().localBaseImg,
scrollTop: 0,
list: [],
dzj: "",
msgType: "voice",
sendMsg: "",
sendIconStatus: false,
voiceMsg: "按住 说话",
voiceStatus: -1, // -1 录音前 & 录音结束 0 录音时且在指定范围 1录音时但不在指定范围 -2点击
longPressDelay: 230, // 设定长按所需时间,单位毫秒
spec: true,
isAuth: false,
content: "", // 内容
hasTopPadding: false,
allJob: [],
talkId: 0,
gptType: "",
bottomHeight: 100,
longPressTimer: null,
isStartRecord: false,
sendMsgGroup: [],
socketTask: null,
sequenceCounter: 1,
innerAudioContext: null,
cid: -1,
hisPage: 1,
ws_send_ready: false,
reconnectCount: 0,
isSending: false,
form: 'chat',
codeGroup: [],
stip: 'text',
isShowRightLoading: false,
isReturn: false,
isShowWelcome:false,
lastClickTime: 0, // 记录上一次点击时间
isFastClick: false, // 是否是快速点击
hotObj:{},
isjob:false,
aiPage:0,
ishide:false,
isShowFirst:false,
helloText:'',
isShowDel:false,
jobRendered:false,
endType:'text',
sendInfo:{
chat_id:'',
conversation_id:''
},
isLiuSend:false,
isCancel:false,
isRender:false,
};
},
onLoad(options) {
let that = this;
console.log('聊天页获取参数',options)
that.cid = options.cid;
that.form = options.form;
that.stip = options.stip;
that.helloText = options.title;
if (options.type) {
that.gptType = options.type;
} else {
that.gptType = "ai";
}
// 初始化音频上下文
that.innerAudioContext = uni.createInnerAudioContext();
that.innerAudioContext.autoplay = false; // 设置为 false手动控制播放
that.innerAudioContext.onPlay(() => {
});
that.innerAudioContext.onError((res) => {});
this.isMounted = true;
//页面onShow时【必须调用】的函数传入当前组件this
RecordApp.UniPageOnShow(this);
},
onShow() {
let that = this;
that.isHidden = false;
uni.removeStorageSync('ls_obj')
that.checkRecordingPermission();
if (this.isMounted) RecordApp.UniPageOnShow(this);
that.initASRClient();
// 监听ws状态
uni.$on("ws_send_ready", function(data) {
that.ws_send_ready = data.data;
});
// 异常监听
uni.$on("asrEvent_close", function(data) {
that.isShowRightLoading = false;
that.rightAnimate1 = false;
that.rightAnimate2 = false;
if (data.code == 400) {
// 链接超限,重连
} else {
that.endType = 'text'
that.isHidden = true;
that.list[that.list.length - 1].isHidden = true;
uni.showToast({
icon: "error",
title: data.data,
});
}
});
// 监听ws事件
uni.$on("asrEvent_message", function(data) {
let _asrData = {};
if (data.data) {
_asrData = JSON.parse(data.data);
if (_asrData.sequence == 1) {
// 开始录音
} else if (_asrData.sequence < 0) {
// 将 获取的_asrData.sequence 推进数组并去重,然后遍历该数组,判断哪个元素是负值,在负值中执行结束录音的操作
// 将当前 sequence 推入数组
that.codeGroup.push(_asrData.sequence);
// 去重
that.codeGroup = [...new Set(that.codeGroup)];
// 遍历数组,检查是否有负值
for (let i = 0; i < that.codeGroup.length; i++) {
if (that.codeGroup[i] < 0) {
// 结束录音
that.asrClient.asr_close();
if (_asrData.result && _asrData.result.length > 0) {
let asrStr = _asrData.result[0].text;
that.$nextTick(()=>{
that.scrollToBottom(()=>{
that.$nextTick(()=>{
that.animate();
that.sendAudioAI(asrStr);
})
},'拿到一句话识别',100)
})
}
break;
}
}
} else {
// 发送中
}
}
});
setTimeout(() => {
that.hisPage = 1;
}, 10);
// #ifdef MP-WEIXIN
const accountInfo = uni.getAccountInfoSync();
const env = accountInfo.miniProgram.envVersion;
if (env === 'develop') {
that.isShowDel = true;
} else if (env === 'trial') {
that.isShowDel = true;
} else if (env === 'release') {
that.isShowDel = false;
}
// #endif
// #ifdef APP-PLUS
that.isShowDel = false;
// #endif
if(uni.getStorageSync('isExecute') == 0){
return false;
}
uni.showLoading({
title: '加载中',
})
// that.list = [];
that.G.Post(that.api.chat_getConversationld, {
robotId:uni.getStorageSync('robot_id'),
agencyId:uni.getStorageSync("apply-agencyId")
}, (res) => {
uni.hideLoading();
that.talkId = res;
that.cid = res;
that.invokeGetCoziInfoIfAllowed((coziRes)=>{
if (that.cid && String(that.cid).length > 4) {
that.talkId = that.cid;
that.list = [];
that.isShowWelcome = false;
that.isShowFirst = false
that.getHistory();
} else {
that.isShowWelcome = true;
that.list = that.defaultChat;
that.initSocket();
that.isShowFirst = true;
}
if(that.ishide){
that.ishide = false;
that.scrollToBottom()
}
that.previousHeight = 0;
that.codeGroup = [];
const isWxApp = uni.getSystemInfoSync().uniPlatform == 'mp-weixin'
const isIosWxapp = uni.getSystemInfoSync().platform == 'ios' && isWxApp
that.isIosWxapp = isIosWxapp;
that.isFinish = 1;
that.voiceStatus = -1;
that.isStartRecord = false;
// 创建查询对象
const query = wx.createSelectorQuery();
// 选择指定的 DOM 元素
query.select(".chat-operate").boundingClientRect();
// 执行查询
query.exec((res) => {
if (res && res[0]) {
const height = res[0].height;
that.bottomHeight = height;
that.wh = `calc(${uni.getSystemInfoSync().windowHeight}px - ${that.bottomHeight}px)`;
}
});
})
});
},
onHide(){
this.ishide = true;
this.leaveEvent()
},
onUnload() {
this.leaveEvent()
},
watch: {
sendMsg(val) {
if (val) {
this.sendIconStatus = true;
} else {
this.sendIconStatus = false;
}
},
isShowRightLoading(val) {
let that = this;
if(val == true){
setTimeout(() => {
that.$nextTick(()=>{
that.scrollToBottom(()=>{},'watch 监听 右侧loading',300)
})
}, 100);
}
},
isShowLeftLoading(val){
let that = this;
if(val == true){
setTimeout(() => {
that.$nextTick(()=>{
that.scrollToBottom(()=>{},'watch 监听 左侧loading',300)
})
}, 101);
}
},
},
methods: {
shouldCallGetCoziInfo() {
const lastCalledTime = uni.getStorageSync('last_called_getCoziInfo');
const now = new Date().getTime();
if (!lastCalledTime || now - lastCalledTime >= 60 * 60 * 1000) {
return true;
}
return false;
},
invokeGetCoziInfoIfAllowed(callback=()=>{}) {
if (this.shouldCallGetCoziInfo()) {
this.getCoziInfo((coziRes) => {
callback();
});
uni.setStorageSync('last_called_getCoziInfo', new Date().getTime());
} else{
callback();
}
},
delAll(){
let that = this;
uni.showModal({
title: '提示',
content: '确定要删除该机器人的全部记录吗?',
success: (res) => {
if (res.confirm) {
that.G.Post(
that.api.chat_delMsg, {
deleteAll:1,
conversationId: that.cid,
},() => {
uni.showToast({
icon:'success',
title:'删除成功'
})
uni.navigateBack();
}
)
}else{}
}
});
},
leaveEvent(){
let that = this;
that.spec = false;
that.hisPage = 1;
this.previousHeight = 0;
// this.stesttest = 0;
this.clearHeartbeat();
this.asrClient.asr_close();
_wsTimer = null;
uni.$off('asrEvent_message')
uni.hideLoading();
uni.removeStorageSync('test_file')
this.isShowRightLoading = false;
that.rightAnimate1 = false;
that.rightAnimate2 = false;
this.isShowLeftLoading = false;
this.leftAnimate1 = false;
this.leftAnimate2 = false;
this.leftAnimate3 = false;
this.G.startAbort(1)
},
initFormPageInfo() {
let that = this;
that.isShowLeftLoading = false;
that.leftAnimate1 = false;
that.leftAnimate2 = false;
that.leftAnimate3 = false;
that.isShowRightLoading = true;
that.rightAnimate1 = false;
that.rightAnimate2 = false;
that.list.push({
msg: "",
isGood: false,
isDown: false,
chat_type: "",
robotTag: 0,
})
that.startRightAnimate();
that.asrClient.asr_sync_connect('main');
let _wsTimer = setInterval(() => {
if (that.ws_send_ready) {
var audioChunks_copy = uni.getStorageSync('test_file').split(',').map(item => {
return item * 1
});
var audioChunks = new Int8Array(audioChunks_copy)
const CHUNK_SIZE = 1024 * 20;
for (let i = 0; i < audioChunks.length; i += CHUNK_SIZE) {
const end = Math.min(i + CHUNK_SIZE, audioChunks.length);
const chunk = audioChunks.subarray(i, end); // 关键修改
that.asrClient.asr_send(chunk, end === audioChunks.length);
}
that.$nextTick(()=>{
that.scrollToBottom(()=>{
that.$nextTick(()=>{
clearInterval(_wsTimer); // 停止值的更新
})
},'解析文件结束',100)
})
} else {
if (that.reconnectCount < 10) {
that.reconnectCount++;
that.asrClient.asr_close();
uni.removeStorageSync('asytip');
that.asrClient.asr_sync_connect('main');
} else {
clearInterval(_wsTimer);
_wsTimer = null;
that.asrClient.asr_close();
setTimeout(() => {
uni.hideToast();
}, 800);
}
}
}, 1000);
},
//请求录音权限
recReq() {
var that = this;
//编译成App时提供的授权许可编译成H5、小程序为免费授权可不填写如果未填写授权许可将会在App打开后第一次调用请求录音权限时弹出“未获得商用授权时App上仅供测试”提示框
//RecordApp.UniAppUseLicense='我已获得UniAppID=*****的商用授权';
RecordApp.RequestPermission_H5OpenSet = {
audioTrackSet: {
noiseSuppression: true,
echoCancellation: true,
autoGainControl: true
}
}; //这个是Start中的audioTrackSet配置在h5H5、App+renderjs中必须提前配置因为h5中RequestPermission会直接打开录音
RecordApp.UniWebViewActivate(this); //App环境下必须先切换成当前页面WebView
RecordApp.RequestPermission(
() => {
console.log("已获得录音权限,可以开始录音了");
that.isAuth = true;
},
(msg, isUserNotAllow) => {
if (isUserNotAllow) {
//用户拒绝了录音权限
//这里你应当编写代码进行引导用户给录音权限,不同平台分别进行编写
uni.showModal({
title: '提示',
content: '需要录音权限才能使用语音功能,请前往设置开启权限',
success: function (res) {
if (res.confirm) {
uni.openSetting({
success: (settingData) => {
if (settingData.authSetting['scope.record']) {
uni.showToast({ title: '授权成功' });
that.msgType = "voice";
that.isAuth = true;
that.checkRecordingPermission(); // 重新尝试请求权限
} else {
uni.showToast({ icon: 'none', title: '授权失败' });
}
},
});
}
},
});
}
console.error("请求录音权限失败:" + msg);
that.msgType = 'text'
}
);
},
//开始录音
recStart() {
this.codeGroup = [];
//Android App如果要后台录音需要启用后台录音保活服务iOS不需要需使用配套原生插件、或使用第三方保活插件
//RecordApp.UniNativeUtsPluginCallAsync("androidNotifyService",{ title:"正在录音" ,content:"正在录音中请勿关闭App运行" }).then(()=>{...}).catch((e)=>{...}) 注意必须RecordApp.RequestPermission得到权限后调用
// const params_msg = this.asrClient.asr_send_full_client_req();
//录音配置信息
var set = {
type: "wav",
sampleRate: 16000,
bitRate: 16, //mp3格式指定采样率hz、比特率kbps其他参数使用默认配置注意是数字的参数必须提供数字不要用字符串需要使用的type类型需提前把格式支持文件加载进来比如使用wav格式需要提前加载wav.js编码引擎
/*,audioTrackSet:{ //可选如果需要同时播放声音比如语音通话需要打开回声消除并不一定会生效打开后声音可能会从听筒播放部分环境下如小程序、App原生插件可调用接口切换成扬声器外放
//注意H5、App+renderjs中需要在请求录音权限前进行相同配置RecordApp.RequestPermission_H5OpenSet后此配置才会生效
echoCancellation:true,noiseSuppression:true,autoGainControl:true} */
onProcess: (buffers, powerLevel, duration, sampleRate, newBufferIdx, asyncEnd) => {
//全平台通用可实时上传发送数据配合Recorder.SampleData方法将buffers中的新数据连续的转换成pcm上传或使用mock方法将新数据连续的转码成其他格式上传可以参考Recorder文档里面的Demo片段列表 -> 实时转码并上传-通用版基于本功能可以做到实时转发数据、实时保存数据、实时语音识别ASR
//注意App里面是在renderjs中进行实际的音频格式编码操作此处的buffers数据是renderjs实时转发过来的修改此处的buffers数据不会改变renderjs中buffers所以不会改变生成的音频文件可在onProcess_renderjs中进行修改操作就没有此问题了如需清理buffers内存此处和onProcess_renderjs中均需要进行清理H5、小程序中无此限制
//注意如果你要用只支持在浏览器中使用的Recorder扩展插件App里面请在renderjs中引入此扩展插件然后在onProcess_renderjs中调用这个插件H5可直接在这里进行调用小程序不支持这类插件如果调用插件的逻辑比较复杂建议封装成js文件这样逻辑层、renderjs中直接import不需要重复编写
//H5、小程序等可视化图形绘制直接运行在逻辑层App里面需要在onProcess_renderjs中进行这些操作
// #ifdef H5 || MP-WEIXIN
if (this.waveView) this.waveView.input(buffers[buffers.length - 1], powerLevel,
sampleRate);
// #endif
/*实时释放清理内存用于支持长时间录音在指定了有效的type时编码器内部可能还会有其他缓冲必须同时提供takeoffEncodeChunk才能清理内存否则type需要提供unknown格式来阻止编码器内部缓冲App的onProcess_renderjs中需要进行相同操作
if(this.clearBufferIdx>newBufferIdx){ this.clearBufferIdx=0 } //重新录音了就重置
for(var i=this.clearBufferIdx||0;i<newBufferIdx;i++) buffers[i]=null;
this.clearBufferIdx=newBufferIdx; */
},
onProcess_renderjs: `function(buffers,powerLevel,duration,sampleRate,newBufferIdx,asyncEnd){
//App中在这里修改buffers会改变生成的音频文件但注意buffers会先转发到逻辑层onProcess后才会调用本方法因此在逻辑层的onProcess中需要重新修改一遍
//本方法可以返回truerenderjs中的onProcess将开启异步模式处理完后调用asyncEnd结束异步注意这里异步修改的buffers一样的不会在逻辑层的onProcess中生效
//App中是在renderjs中进行的可视化图形绘制因此需要写在这里this是renderjs模块的this也可以用This变量如果代码比较复杂请直接在renderjs的methods里面放个方法xxxFunc这里直接使用this.xxxFunc(args)进行调用
if(this.waveView) this.waveView.input(buffers[buffers.length-1],powerLevel,sampleRate);
/*和onProcess中一样进行释放清理内存用于支持长时间录音
if(this.clearBufferIdx>newBufferIdx){ this.clearBufferIdx=0 } //重新录音了就重置
for(var i=this.clearBufferIdx||0;i<newBufferIdx;i++) buffers[i]=null;
this.clearBufferIdx=newBufferIdx; */
}`,
onProcessBefore_renderjs: `function(buffers,powerLevel,duration,sampleRate,newBufferIdx){
//App中本方法会在逻辑层onProcess之前调用因此修改的buffers会转发给逻辑层onProcess本方法没有asyncEnd参数不支持异步处理
//一般无需提供本方法只用onProcess_renderjs就行renderjs的onProcess内部调用过程onProcessBefore_renderjs -> 转发给逻辑层onProcess -> onProcess_renderjs
}`,
takeoffEncodeChunk: true ?
null :
(chunkBytes) => {
//全平台通用实时接收到编码器编码出来的音频片段数据chunkBytes是Uint8Array二进制数据可以实时上传发送出去
//App中如果未配置RecordApp.UniWithoutAppRenderjs时建议提供此回调因为录音结束后会将整个录音文件从renderjs传回逻辑层由于uni-app的逻辑层和renderjs层数据交互性能实在太拉跨了大点的文件传输会比较慢提供此回调后可避免Stop时产生超大数据回传
//App中使用原生插件时可方便的将数据实时保存到同一文件第一帧时append:false新建文件后面的append:true追加到文件
//RecordApp.UniNativeUtsPluginCallAsync("writeFile",{path:"xxx.mp3",append:回调次数!=1, dataBase64:RecordApp.UniBtoa(chunkBytes.buffer)}).then(...).catch(...)
},
takeoffEncodeChunk_renderjs: true ?
null :
`function(chunkBytes){
//App中这里可以做一些仅在renderjs中才生效的事情不提供也行this是renderjs模块的this也可以用This变量
}`,
start_renderjs: `function(){
//App中可以放一个函数在Start成功时renderjs中会先调用这里的代码this是renderjs模块的this也可以用This变量
//放一些仅在renderjs中才生效的事情比如初始化不提供也行
}`,
stop_renderjs: `function(arrayBuffer,duration,mime){
//App中可以放一个函数在Stop成功时renderjs中会先调用这里的代码this是renderjs模块的this也可以用This变量
//放一些仅在renderjs中才生效的事情不提供也行
}`,
};
RecordApp.UniWebViewActivate(this); //App环境下必须先切换成当前页面WebView
RecordApp.Start(
set,
() => {
// that.codeGroup = [];
console.log("已开始录音");
//【稳如老狗WDT】可选的监控是否在正常录音有onProcess回调如果长时间没有回调就代表录音不正常
//var wdt=this.watchDogTimer=setInterval ... 请参考示例Demo的main_recTest.vue中的watchDogTimer实现
//创建音频可视化图形绘制App环境下是在renderjs中绘制H5、小程序等是在逻辑层中绘制因此需要提供两段相同的代码
//view里面放一个canvascanvas需要指定宽高下面style里指定了300*100
//<canvas type="2d" class="recwave-WaveView" style="width:300px;height:100px"></canvas>
// RecordApp.UniFindCanvas(this,[".recwave-WaveView"],`
// this.waveView=Recorder.WaveView({compatibleCanvas:canvas1, width:300, height:100});
// `,(canvas1)=>{
// this.waveView=Recorder.WaveView({compatibleCanvas:canvas1, width:300, height:100});
// });
},
(msg) => {
console.error("开始录音失败:" + msg);
}
);
},
//暂停录音
recPause() {
if (RecordApp.GetCurrentRecOrNull()) {
RecordApp.Pause();
}
},
//继续录音
recResume() {
if (RecordApp.GetCurrentRecOrNull()) {
RecordApp.Resume();
}
},
//停止录音
recStop() {
//RecordApp.UniNativeUtsPluginCallAsync("androidNotifyService",{ close:true }) //关闭Android App后台录音保活服务
let that = this;
RecordApp.Stop(
(arrayBuffer, duration, mime) => {
if (that.spec) {
that.list.push({
msg: "",
isGood: false,
isDown: false,
chat_type: "",
robotTag: 0,
})
that.startRightAnimate(()=>{});
that.$nextTick(()=>{
that.scrollToBottom(()=>{
//全平台通用arrayBuffer是音频文件二进制数据可以保存成文件或者发送给服务器
//App中如果在Start参数中提供了stop_renderjsrenderjs中的函数会比这个函数先执行
//注意当Start时提供了takeoffEncodeChunk后你需要自行实时保存录音文件数据因此Stop时返回的arrayBuffer的长度将为0字节
//如果是H5环境也可以直接构造成Blob/File文件对象和Recorder使用一致
// #ifdef H5
var blob = new Blob([arrayBuffer], {
type: mime
});
var file = new File([arrayBuffer], "recorder.wav");
//uni.uploadFile({file:file, ...}) //参考demo中的test_upload_saveFile.vue
// #endif
//如果是App、小程序环境可以直接保存到本地文件然后调用相关网络接口上传
// #ifdef APP || MP-WEIXIN
var audioChunks = new Int8Array(arrayBuffer);
_wsTimer = setInterval(() => {
if (that.ws_send_ready) {
const CHUNK_SIZE = 1024 * 20;
for (let i = 0; i < audioChunks.length; i += CHUNK_SIZE) {
const end = Math.min(i + CHUNK_SIZE, audioChunks.length);
const chunk = audioChunks.subarray(i, end); // 关键修改
that.asrClient.asr_send(chunk, end === audioChunks.length);
}
that.$nextTick(()=>{
that.scrollToBottom(()=>{
that.$nextTick(()=>{
clearInterval(_wsTimer); // 停止值的更新
})
},'当前页解析结束',100)
})
} else {
if (that.reconnectCount < 10) {
that.reconnectCount++;
that.asrClient.asr_close();
uni.removeStorageSync('asytip');
that.asrClient.asr_sync_connect();
} else {
clearInterval(_wsTimer);
_wsTimer = null;
that.asrClient.asr_close();
// uni.showToast({
// title: "识别异常,请重试",
// icon: "none",
// });
setTimeout(() => {
uni.hideToast();
}, 800);
}
}
}, 1000);
},100,100)
})
}
// #endif
},
(msg) => {
console.error("结束录音失败:" + msg);
}
);
},
getMore() {
let that = this;
if (that.isFinish == 1) {
that.hisPage++;
uni.setStorageSync('isExecute',1)
that.getHistory("concat");
}
},
formatMessageTime(dateStr) {
const now = new Date();
const date = new Date(dateStr);
// 判断是否为有效日期
if (isNaN(date.getTime())) return "";
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hour = date.getHours().toString().padStart(2, "0");
const minute = date.getMinutes().toString().padStart(2, "0");
// 计算时间差(单位:毫秒)
const diffMs = now - date;
// 1. 刚刚1分钟以内
if (diffMs < 60000) {
return "刚刚";
}
// 2. 今天的消息(显示时间 HH:mm
if (
now.getFullYear() === year &&
now.getMonth() + 1 === month &&
now.getDate() === day
) {
return `${hour}:${minute}`;
}
// 3. 昨天的消息
const yesterday = new Date(now);
yesterday.setDate(yesterday.getDate() - 1);
if (
yesterday.getFullYear() === year &&
yesterday.getMonth() + 1 === month &&
yesterday.getDate() === day
) {
return `昨天 ${hour}:${minute}`;
}
// 4. 今年的消息(不跨年)
if (now.getFullYear() === year) {
return `${month}月${day}日 ${hour}:${minute}`;
}
// 5. 去年的消息或更早(跨年)
return `${year}年${month}月${day}日 ${hour}:${minute}`;
},
getHistory($type = "init") {
let that = this;
const query = uni.createSelectorQuery().in(this);
if(uni.getStorageSync('isExecute') == 0){
return false;
}
// 获取当前滚动位置
query.select(".chat-content .chatBox").boundingClientRect(res => {
let currentScrollTop = res ? res.height : 0;
if ($type == "concat") {
uni.showToast({
title: "加载中...",
icon: "loading",
duration: 1000,
});
}
that.G.Post(
that.api.chat_getHistory, {
classify: 0,
conversationId: that.cid,
pageNum: that.hisPage,
pageSize: 10,
},
(historyRes) => {
uni.setStorageSync('isExecute',0)
if ($type == "concat") {
uni.hideToast();
}
console.log("获取历史记录:", historyRes,' that.hisPage:',that.hisPage);
if (historyRes && historyRes.length > 0) {
let _array = historyRes.map((item) => {
item.chatType = "text";
item.ids = '';
item.comList = [];
try {
console.log('0811 try')
item.chatType = "markdown";
item.message = JSON.parse(item.message);
item.ids = item.message.map(itm => itm['职位ID']).join(',');
} catch (e) {
console.log('0811 catch')
if(item.messageType == 99){
item.chatType = "markdown";
item.message = item.message;
item.ids = item.attr2;
}else{
item.chatType = "text";
if(item.message){
if(item.robotTag == 2){
item.message = String(item.message).replace(/.{3}$/, '');
}else{
item.message = item.message;
}
}
item.ids = '';
}
}
return {
classify: item.classify,
ids: item.ids,
msg: item.message,
id: item.id,
message: item.message,
isGood: false,
isDown: false,
chat_type: item.chatType,
robotTag: item.robotTag,
comList: item.comList,
createTime: item.createTime,
messageType:item.messageType || 0,
attr1:item.attr1 || '',
attr2:item.attr2 || '',
};
});
console.log('0811 001')
if ($type != "concat") {
uni.showLoading({ title: '加载中' });
setTimeout(()=>{
uni.hideLoading();
},300)
that.isShowWelcome = false;
}
console.log('0811 002')
const promises = _array.map(item => {
if (item.ids && item.ids.replace(/,/g, "")) {
return new Promise((resolve, reject) => {
that.getHomeList(item.ids, (hotList) => {
item.comList = hotList;
resolve();
}, 1, 'hostory');
});
} else {
return Promise.resolve(); // 如果没有 ids直接返回已解决的 Promise
}
});
console.log('0811 003')
Promise.all(promises).then(() => {
console.log('0811 004')
if ($type != "concat") {
uni.hideLoading();
}
if ($type == "concat") {
if (historyRes && historyRes.length < 10) {
that.isFinish = 2;
} else {
that.isFinish = 1;
}
// 向上拼接
that.list = [..._array, ...that.list];
that.$nextTick(() => {
const queryRestore = uni.createSelectorQuery().in(this);
queryRestore.select(".chatBox").boundingClientRect(resNew => {
if (resNew) {
const heightDiff = resNew.height - currentScrollTop;
that.stesttest = heightDiff > 0 ? heightDiff : 0;
}
}).exec();
});
} else {
that.isFinish = 1;
that.list = _array;
if(that.aiPage <= 1){
that.hotObj = {};
that.isjob = false;
function parseStringToObject(str) {
const obj = {};
const pairs = str.split(',').map(pair => pair.trim());
pairs.forEach(pair => {
const [key, value] = pair.split(':').map(part => part.trim());
obj[key] = value.replace(/^['"]|['"]$/g, '');
});
return obj;
}
that.handleRecordJobEnd(()=>{
that.list.forEach((item,index) => {
if(item.messageType == 99 && item.isRecordJobEnd){
let _objs = parseStringToObject(item.attr1);
that.isjob = true;
that.hotObj['sex'] = _objs['sex'];
that.hotObj['ageRangeStr'] = _objs['ageRangeStr'];
that.hotObj['cityName'] = _objs['cityName'];
that.hotObj['labelNames'] = _objs['labelNames'];
let _oa = {};
_oa['sex'] = _objs['sex'];
_oa['ageRangeStr'] = _objs['ageRangeStr'];
_oa['cityName'] = _objs['cityName'];
_oa['labelNames'] = _objs['labelNames'];
uni.setStorageSync('ls_obj', JSON.stringify(_oa))
item.ids = item.attr2;
}
});
});
}
};
if(uni.getStorageSync('btn_form') == 1){
that.startRightAnimate(()=>{});
uni.removeStorageSync('btn_form')
}
that.$nextTick(() => {
that.initFormPage()
});
}).catch((rea)=>{
console.log('reaaaa',rea)
});
} else {
that.isFinish = 2;
that.$nextTick(()=>{
that.initFormPage()
if ($type != "concat") {
that.scrollToBottom(()=>{},'当前页-无记录',300);
}
if($type == "init"){
that.list = that.defaultChat;
}
})
}
}
);
}).exec();
},
initFormPage(){
let that = this;
// 页面-获取文字
if (that.form == 'msgdata') {
that.sendAI('msgdata');
}
// 页面-获取音频
if (that.form == 'audiodata') {
that.initFormPageInfo()
}
// 页面-查看会话
if (that.form == 'viwdata') {
that.scrollToBottom(()=>{},'查看会话',300);
}
},
getHomeList(ids, callback = () => {}, $pageNum = 1,$form='') {
let that = this;
let _url = that.api.new_job_list;
let params = {}
if(that.aiPage <= 1){
params = {
shareJobIds: ids,
recruitment: 1,
pageNum: that.aiPage
}
}else{
params = Object.assign(that.hotObj,{
recruitment: 1,
pageNum: that.aiPage,
pageSize:5
})
}
if(params.pageNum == 0){
params.pageNum = 1;
}
that.G.Post(_url, params, (res) => {
let resData = {};
resData = res;
if (resData.recordList && resData.recordList.length > 0) {
resData.recordList = that.G.toGetAddressv3(resData.recordList);
resData.recordList = that.G.toGetAge(resData.recordList);
that.G.Post("/yishoudan/labels/type/app/80", {}, (res) => {
uni.setStorageSync("JOB_REQUIRE_LIST", {
data: res.labels,
time: Date.now()
});
resData.recordList = that.G.yijobCopy(resData.recordList);
});
}
resData.recordList.forEach(item => {
item.gender = that.G.getGenderByMinAge(item);
item.fuWuFei = that.G.setReturnFee(item.returnFee, item.returnFeeType);
item.baoNum = uni.getStorageSync("lin-bao") ? uni.getStorageSync("lin-bao") / 10 :
Math.ceil(item.liuNum / 10);
item.cus_price = item.salaryClassify != 7 ? that.G.getSalaryClassifyValueHtml(item
.salaryClassify, item.salaryClassifyValue) : "月薪";
})
callback(resData.recordList)
});
},
handlePlayAudio() {
let that = this;
if (that.audio_url) {
that.innerAudioContext.src = that.audio_url; // 设置音频文件路径
that.innerAudioContext.play(); // 开始播放
that.innerAudioContext.onPlay(() => {
});
that.innerAudioContext.onError((res) => {});
} else {}
},
initASRClient() {
let that = this;
try {
// 初始化 ASR 客户端
this.asrClient = new AsrClient("wss://openspeech.bytedance.com/api/v2/asr");
} catch (error) {
console.error("Error initializing ASR client:", error);
}
},
readAudioFileAsArrayBuffer(filePath) {
var that = this;
return new Promise((resolve, reject) => {
uni.getFileSystemManager().readFile({
filePath: filePath,
responseType: "arraybuffer", // 指定响应类型为数组缓冲区
success: (res) => {
resolve(new Int8Array(res.data));
},
fail: (err) => {
reject(err);
},
});
});
},
clearHeartbeat() {
clearInterval(this.heartbeatInterval);
},
// 获取扣子信息
getCoziInfo(callback=()=>{}) {
let that = this;
// 获取扣子 token
that.G.Get("/yishoudan/common/structure/getConfig", {}, (res) => {
uni.setStorageSync("cozi_token", res.token);
callback(res.token);
});
},
// 创建会话
coziCreate(callback=()=>{}){
let that = this;
coziAjax.coziPost('/v1/conversation/create',{
"bot_id": uni.getStorageSync('robot_id'),
"messages": [
{
"content_type": "text",
"role": "user",
"type": "question"
}
]
},(sdkRes) =>{
callback(sdkRes)
});
},
// 发起会话
coziChat($sendMessage,callback=()=>{}){
let that = this;
that.jobRendered = false;
that.$refs.chatSSEClientRef.startChat({
url: "https://api.coze.cn/v3/chat?conversation_id=" + that.cid,
// 请求头
headers: {
Authorization: "Bearer " + uni.getStorageSync("cozi_token"),
"Content-Type": "application/json;charset=UTF-8",
},
// 默认为 post
method: "post",
body: {
"bot_id": uni.getStorageSync('robot_id'),
"user_id": "user",
"stream": true,
"additional_messages": [
{
"content": $sendMessage,
"content_type": "text",
"role": "user",
"type": "question"
}
]
},
});
callback()
},
// 取消会话
coziCancelChat(callback=()=>{}){
let that = this;
that.isRender = false;
if(that.sendInfo.conversation_id && that.sendInfo.chat_id){
that.isCancel = true;
coziAjax.coziPost('/v3/chat/cancel',{
"conversation_id":that.sendInfo.conversation_id,
"chat_id":that.sendInfo.chat_id
},(sdkRes) => {
that.isCancel = false;
that.sendInfo = {};
callback(sdkRes)
});
}else{
that.sendInfo = {};
callback({
"code": 400,
"message": "会话不存在"
})
}
},
stopCore($form='default'){
let that = this;
that.list = that.list.slice(0, -1);
that.$refs.chatSSEClientRef.stopChat();
// 调用发送消息
that.handleSendMsg()
},
openCore(err){
},
errorCore(err){
},
messageCore(msg){
let that = this;
if(that.isCancel){
that.isRender = false;
that.stopCore('停止流式输出');
return false;
}
if(JSON.parse(msg.data) == '[DONE]'){
that.isRender = false;
that.isLiuSend = false;
that.handleEnd();
}else{
that.isRender = true;
that.isLiuSend = true;
console.log('000000000000000000000',JSON.parse(msg.data))
if(JSON.parse(msg.data) && JSON.parse(msg.data).type == 'answer'){
that.sendInfo = {
chat_id:JSON.parse(msg.data).chat_id,
conversation_id:JSON.parse(msg.data).conversation_id
}
if(JSON.parse(msg.data).content && JSON.parse(msg.data).content != ' ' && !JSON.parse(msg.data).updated_at && JSON.parse(msg.data).content.length < 5){
// console.log('文本类型**************************************回复:',JSON.parse(msg.data).content)
// 流式输出增量渲染
that.endType = 'text';
that.handleTextRender(JSON.parse(msg.data))
}else{
that.$nextTick(()=>{
that.scrollToBottom(()=>{},'类型异常',3);
})
try {
const parsedData = JSON.parse(msg.data);
try {
const contentData = JSON.parse(parsedData.content);
if (Array.isArray(contentData)) {
// 处理数组内容,如渲染到页面或更新状态
// console.log('推荐职位类型**************************************回复:',contentData)
that.endType = 'markdown';
that.handleJobRender(contentData)
}else {}
} catch (contentError) {}
} catch (dataError) {}
}
}else if(JSON.parse(msg.data) && JSON.parse(msg.data).type == 'verbose'){
that.$nextTick(()=>{
that.scrollToBottom(()=>{},'errr',10);
})
}else if(JSON.parse(msg.data) && JSON.parse(msg.data).type == 'tool_response' && JSON.parse(msg.data).content == '暂未找到匹配的岗位'){
// 异常
that.isShowLeftLoading = false;
uni.showToast({
title: '暂未找到匹配的岗位',
icon: 'none',
duration:2000
})
that.list.splice(that.list.length - 1, 1);
}
}
},
finishCore(err){
},
handleTextRender($smg){
let that = this;
const targetIndex = that.list.length - 1;
const parsed = $smg;
that.list[targetIndex].chat_type = "text";
that.list[targetIndex].robotTag = 1;
that.list[targetIndex].ids = '';
const newContent = parsed.content;
// 增量追加内容
that.$set(that.list[targetIndex], 'msg', (that.list[targetIndex].msg || '') + newContent);
that.isSending = false;
that.isShowLeftLoading = false;
that.leftAnimate1 = false;
that.leftAnimate2 = false;
that.leftAnimate3 = false;
that.$nextTick(()=>{
that.scrollToBottom(()=>{},'流式输出中',10);
})
},
handleJobRender($array){
let that = this;
if (that.jobRendered) return; // 已执行过则返回
that.jobRendered = true;
that.hotObj = {};
that.isjob = false;
try {
const parsedContent = $array;
that.aiPage = 1;
that.list.forEach((ita,ina) => {
ita.opbtn = 0
})
that.list[that.list.length - 1].opbtn = 1;
that.list[that.list.length - 1].msg = parsedContent;
that.list[that.list.length - 1].chat_type = "markdown";
that.list[that.list.length - 1].robotTag = 1;
that.list[that.list.length - 1].ids = parsedContent.map((itm,inx) => {
if(inx == 0){
that.isjob = true;
that.hotObj['sex'] = itm['sex'];
that.hotObj['ageRangeStr'] = itm['ageRangeStr'];
that.hotObj['cityName'] = itm['cityName'];
that.hotObj['labelNames'] = itm['labelNames'];
let _oa = {};
_oa['sex'] = itm['sex'];
_oa['ageRangeStr'] = itm['ageRangeStr'];
_oa['cityName'] = itm['cityName'];
_oa['labelNames'] = itm['labelNames'];
uni.setStorageSync('ls_obj', JSON.stringify(_oa))
}
return itm['职位ID']
}).join(',');
that.getHomeList(that.list[that.list.length - 1].ids, (hotList) => {
that.list[that.list.length - 1].comList = [];
that.list[that.list.length - 1].comList = hotList;
},1,'send');
} catch (e) {}
},
handleEnd(){
let that = this;
that.saveHistory(1, that.list[that.list.length - 1].msg, (historyData) => {
console.log('最后一条消息保存成功',that.endType)
that.list[that.list.length - 1].id = historyData.id;
that.isSending = false;
that.handleSet(that.list[that.list.length - 1].msg, "", 0, 300);
},'messageEnd');
},
initSocket() {
let that = this;
that.coziCreate((sdkRes)=>{
that.talkId = sdkRes.id;
that.cid = sdkRes.id;
that.saveHistory(1, that.helloText ? '你好,我是' + that.helloText + '机器人' : '你好我是伯才智能匹配AI大鹏可以帮老乡快速匹配工作支持语音输入。为了匹配更准确需多提供老乡需求信息例如:1. 性别2. 年龄3. 意向城市4. 工作要求(如吃住、班次等)示例有位32岁大姐想去常州找个长白班的工作。快告诉我老乡需求开始匹配吧!', (historyData) => {
that.defaultChat.id = historyData.id;
that.list = that.defaultChat;
},'welcome');
})
},
keyboardheightchange(e) {
var that = this;
if (e.target.dataset.height == 0 && this.sendMsg == "") {
this.textareaHeight = 30;
}
if (this.isIosWxapp) {
if (e.detail.height == 0) {
this.writeStyle = `bottom:0`
this.wh = `calc(${uni.getSystemInfoSync().windowHeight}px - ${this.bottomHeight}px)`
// this.ghHeight = 'height:84px'
} else {
this.writeStyle = `bottom: calc(${e.detail.height}px - env(safe-area-inset-bottom) + 12rpx)`
// this.ghHeight = `height:calc(84px + ${e.detail.height}px)`
this.wh =
`calc(${uni.getSystemInfoSync().windowHeight}px - ${this.bottomHeight}px - ${e.detail.height}px)`
}
that.scrollToBottom(()=>{},'键盘',100);
}
},
onInput(e) {
this.content = e.target.value;
this.updateTextareaHeight();
},
onFocus() {
this.initialHeight = this.textareaHeight;
},
onBlur() {
this.updateTextareaHeight();
},
updateTextareaHeight() {},
linechange(e) {
if (e.detail.lineCount > 1) {
this.hasTopPadding = true;
} else {
this.hasTopPadding = false;
}
},
getScrollInfo(e){
// console.log('滚动信息:',e.detail)
},
scrollToBottom(callback=()=>{},$from='',$time=300) {
let that = this;
const query = uni.createSelectorQuery().in(this);
setTimeout(() => {
query.select(".chatBox").boundingClientRect(res => {
that.$nextTick(()=>{
if (res) {
let newHeight = res.height;
if (newHeight === that.previousHeight) {
newHeight += 1;
}
that.previousHeight = newHeight;
if ($from == 100) {
that.stesttest = newHeight + 41;
} else if ($from == 100) {
that.stesttest = newHeight + 42;
} else {
that.stesttest = newHeight + 40;
}
} else {
that.previousHeight = 0;
}
// console.log('滚动动画:',that.stesttest, '备注:',$from,' 延迟:',$time)
callback();
})
}).exec();
}, $time);
},
handleUpdateGood($item, $tip) {
if ($tip == "good") {
if ($item.isGood) {
$item.isGood = false;
} else {
$item.isGood = true;
}
} else {
if ($item.isDown) {
$item.isDown = false;
} else {
$item.isDown = true;
}
}
this.animate();
},
handleClickCopy($content = "") {
let that = this;
uni.setClipboardData({
data: $content,
success: () => {},
fail: (err) => {},
});
that.animate();
},
handleUpdateMsgType() {
let that = this;
that.form = '';
this.animate();
if (this.msgType == "text") {
this.msgType = "voice";
this.checkRecordingPermission();
} else {
this.msgType = "text";
}
that.writeStyle = `bottom:0`
that.wh = `calc(${uni.getSystemInfoSync().windowHeight}px - ${this.bottomHeight}px)`
},
handleNextMsg($type=0){
let that = this;
if($type == 0){
that.stopCore(' 流式 取消会话');
}
// 取消会话
that.coziCancelChat((res)=>{
if(res.code == 400){
return false
}else{
// 再次发送
// that.coziChat(that.sendMsg,()=>{
// that.sendMsg = '';
// })
}
})
},
handleSendMsg() {
let that = this;
if (that.msgType == "voice") {
return false;
}
if(that.isShowLeftLoading){
uni.showToast({
title:'匹配中,请稍候',
icon:'none'
})
return false;
}
if (that.isLiuSend) {
that.isLiuSend = false;
that.handleNextMsg();
return false;
}
if (that.sendMsg == "") {
uni.showToast({
icon: "none",
title: "请输入内容",
});
return false;
}
that.animate();
that.sendAI();
that.writeStyle = `bottom:0`
that.wh = `calc(${uni.getSystemInfoSync().windowHeight}px - ${this.bottomHeight}px)`
that.$nextTick(()=>{
that.sendMsg = "";
that.updateTextareaHeight();
that.textareaHeight = 30;
that.$nextTick(()=>{
that.textareaHeight = 30;
})
})
},
checkRecordingPermission() {
this.recReq();
},
onTouchMove(e) {
let that = this;
if (!that.isAuth) {
that.voiceStatus = -1;
return false;
}
const touch = e.touches[0];
let _x = touch.clientX,
_y = touch.clientY;
if (_y > uni.getSystemInfoSync().windowHeight - that.bottomHeight) {
this.voiceStatus = 0;
this.spec = true;
} else {
this.voiceStatus = 1;
this.spec = false;
}
},
onTouchStart() {
let that = this;
that.isHidden = false;
if (!that.isAuth) {
uni.showToast({
icon: "none",
title: "授权中,请稍候",
});
return false;
}
if(that.isShowLeftLoading){
uni.showToast({
title:'匹配中,请稍候',
icon:'none'
})
return false;
}
if (that.isLiuSend) {
that.isLiuSend = false;
that.handleNextMsg();
return false;
}
that.animate("heavy");
uni.removeStorageSync('asytip');
that.reconnectCount = 0;
that.voiceStatus = -2;
that.isStartRecord = false;
that.longPressTimer = setTimeout(() => {
that.asrClient.asr_sync_connect();
that.isStartRecord = true;
that.spec = true;
that.startRecording();
}, that.longPressDelay);
},
onTouchEnd() {
let that = this;
if (!that.isAuth) {
that.voiceStatus = -1;
return false;
}
clearTimeout(that.longPressTimer);
if (that.isAuth) {
const currentTime = new Date().getTime();
const timeDiff = currentTime - that.lastClickTime;
that.isShowRightLoading = false;
that.rightAnimate1 = false;
that.rightAnimate2 = false;
if (timeDiff > 0 && timeDiff < 300) {
that.isFastClick = true;
} else {
that.isFastClick = false;
}
that.lastClickTime = currentTime;
that.scrollToBottom(()=>{
that.stopRecording();
},'触摸离开',100)
} else {
uni.showToast({
icon: "none",
title: "授权中,请稍候",
});
return false;
}
},
startRecording() {
let that = this;
setTimeout(() => {
that.voiceStatus = 0; // 标记为正在录音
if (!that.isFluency) {
that.isSending = false;
// 语音识别开始
that.recStart();
}
}, 100);
},
stopRecording() {
let that = this;
that.baseEnd();
setTimeout(
() => {
that.baseEnd();
},
that.isFluency ? 500 : 0
);
},
baseEnd() {
let that = this;
that.voiceStatus = -1;
if (that.isStartRecord) {
that.recStop();
that.isStartRecord = false;
that.isReturn = true;
} else {
that.isReturn = false;
}
that.$forceUpdate();
},
sendAudioAI($msg) {
let that = this;
that.isHidden = false;
if (that.isSending) {
return;
}
that.isSending = true;
that.$nextTick(()=>{
that.scrollToBottom(()=>{
that.voiceStatus = -1;
that.sendBaseData($msg,'','audio');
},'发送语音',300)
})
},
sendAI($form = "", $value = "") {
let that = this;
that.isHidden = false;
that.textareaHeight = 30;
if ($form == 'msgdata') {
that.sendBaseData(uni.getStorageSync('ls-chat-text'), $form);
} else {
that.sendBaseData(that.sendMsg,'','text');
}
},
sendBaseData($sendMessage = "", $form = '',$msgtype='') {
let that = this;
uni.removeStorageSync('ls_obj')
if ($form == 'msgdata') {
uni.removeStorageSync('ls-chat-text')
}
that.isShowRightLoading = false;
that.rightAnimate1 = false;
that.rightAnimate2 = false;
let currentTime = new Date();
let shouldInsertNewRecord = true;
if (that.list.length > 0) {
let lastItem = that.list[that.list.length - 1];
if (lastItem.createTime) {
let lastCreateTime = new Date(lastItem.createTime);
let timeDifference = (currentTime - lastCreateTime) / (1000 * 60);
if (timeDifference <= 5) {
shouldInsertNewRecord = false;
}else{
shouldInsertNewRecord = true;
}
}else{
shouldInsertNewRecord = false;
}
}
if($msgtype == 'audio'){
that.rightPush(shouldInsertNewRecord,'','audio');
const targetIndex = shouldInsertNewRecord ? that.list.length - 1 : that.list.length - 1;
let index = 0;
const typingSpeed = 88;
const typingInterval = setInterval(() => {
if (index < $sendMessage.length) {
this.$set(that.list[targetIndex], 'msg', that.list[targetIndex].msg + $sendMessage[index]);
index++;
this.scrollToBottom();
} else {
clearInterval(typingInterval);
that.thenResult($sendMessage);
}
}, typingSpeed);
}else{
that.rightPush(shouldInsertNewRecord,$sendMessage,'text');
that.thenResult($sendMessage);
}
},
rightPush(shouldInsertNewRecord,$sendMessage,$type=''){
let that = this;
if($type == 'audio'){
that.list[that.list.length - 1].msg = $sendMessage;
that.list[that.list.length - 1].chat_type = "job";
that.list[that.list.length - 1].robotTag = 0;
}else{
if (shouldInsertNewRecord) {
that.list.push(
{
msg: new Date().getTime(),
message: new Date().getTime(),
chat_type: "",
robotTag: 2,
},
{
msg: $sendMessage,
chat_type: "job",
robotTag: 0,
}
);
}else{
that.list.push({
msg: $sendMessage,
chat_type: "job",
robotTag: 0,
});
}
}
},
thenResult($sendMessage=''){
let that = this;
that.scrollToBottom(()=>{
that.list.push({
msg: "",
isGood: false,
isDown: false,
chat_type: "",
robotTag: 1,
});
that.startLeftAnimate(()=>{});
that.scrollToBottom(()=>{
if (!$sendMessage) {
return false;
}
that.saveHistory(0, $sendMessage, (historyData) => {
that.list[that.list.length - 2].id = historyData.id;
// sdk-聊天
that.coziChat($sendMessage,()=>{
$sendMessage = "";
});
},'sendmsg')
},'发送消息',300);
},'显示loading',100)
},
saveHistory(robotTag = 0, $message = "", callabck = () => {},$type='') {
let that = this,
params = {
classify: 0,
conversationId: that.cid,
userId: uni.getStorageSync("apply-uid"),
agencyId: uni.getStorageSync("apply-agencyId"),
robotTag: robotTag,
message: $message,
form: 'root'
};
if ($message) {
that.form = '';
if(that.endType == 'markdown'){
params['messageType'] = 99;
params['attr1'] = Object.entries(that.hotObj).map(([key, value]) => `${key}: '${value}'`).join(',')
params['attr2'] = that.list[that.list.length - 1].ids;// ids
params['attr3'] = 'jobcard'
params['message'] = '推荐职位';
}else{
params['messageType'] = 97;
params['attr3'] = '';
}
if($type == 'welcome'){
params['messageType'] = 98;
}
params.robotId = uni.getStorageSync('robot_id')
params.agencyId = uni.getStorageSync('apply-agencyId')
that.G.Post(
that.api.chat_addHistory, params,(res) => {
that.handleRecordJobEnd();
that.isjob = false;
callabck(res);
}
);
}
},
// 是否是推荐职位的最后一个
handleRecordJobEnd(callback= ()=>{}) {
let that = this;
if (that.list.length > 0) {
// 初始化所有项为 false
that.list.forEach((item) => {
item.isRecordJobEnd = false;
});
// 找到最后一个符合条件的项chat_type 为 markdown
for (let i = that.list.length - 1; i >= 0; i--) {
if (that.list[i].chat_type === 'markdown') {
that.list[i].isRecordJobEnd = true;
break; // 找到后退出循环
}
}
callback();
}
},
handleSet(str, result = "", index, time) {
let that = this;
that.$nextTick(()=>{
that.isShowLeftLoading = false;
that.animate()
that.leftAnimate1 = false;
that.leftAnimate2 = false;
that.leftAnimate3 = false;
that.sendMsg = '';
that.$nextTick(()=>{
that.scrollToBottom(()=>{},'handleSet方法',300);
})
})
},
getRandomElements(array, count) {
let shuffled = array.slice();
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled.slice(0, count);
},
animate($type = "heavy") {
uni.vibrateShort({
type: $type,
fail(err){
console.log('震动失败:',err)
}
});
},
clearMsg() {
this.sendMsg = "";
setTimeout(() => {
this.textareaHeight = 30;
}, 100);
},
goHotMore($item) {
let that = this;
if(this.hotObj && this.hotObj['sex']){
console.log('流程1')
uni.navigateTo({
url: `/root/other/channelPage?jobparams=${JSON.stringify(this.hotObj)}`,
});
}else{
console.log('流程2')
if(uni.getStorageSync('ls_obj') && JSON.parse(uni.getStorageSync('ls_obj'))){
that.isjob = true;
let itm = JSON.parse(uni.getStorageSync('ls_obj'));
that.hotObj['sex'] = itm['sex'];
that.hotObj['ageRangeStr'] = itm['ageRangeStr'];
that.hotObj['cityName'] = itm['cityName'];
that.hotObj['labelNames'] = itm['labelNames'];
}else{
console.log('流程3',$item)
uni.navigateTo({
url: `/root/other/channelPage`,
});
}
}
},
getReload($item) {
let that = this;
that.aiPage++;
that.list.push({
msg: "",
isGood: false,
isDown: false,
chat_type: "",
robotTag: 1,
});
that.$nextTick(()=>{
that.startLeftAnimate(()=>{
that.scrollToBottom(()=>{
that.list.forEach((ita,ina) => {
ita.opbtn = 0
})
that.list[that.list.length - 1].msg = $item.msg;
that.list[that.list.length - 1].chat_type = "markdown";
that.list[that.list.length - 1].robotTag = 1;
that.list[that.list.length - 1].ids = $item.ids;
that.list[that.list.length - 1].opbtn = 1;
that.getHomeList(that.list[that.list.length - 1].ids, (hotList) => {
that.list[that.list.length - 1].comList = [];
that.list[that.list.length - 1].comList = hotList;
let _art = [];
if (hotList && hotList.length > 0) {
_art = hotList.map(item => {
return {
'职位ID': item.id
}
})
} else {
_art = []
}
that.saveHistory(1, JSON.stringify(_art), (historyData) => {
that.list[that.list.length - 1].id = $item.id;
that.isSending = false;
that.handleSet(JSON.stringify(_art), "", 0, 300);
},'update');
}, $item.page,'more')
},'换一批',300);
});
})
},
startLeftAnimate(callback=()=>{}){
let that = this;
that.animate()
that.jobnum = 0;
that.isShowLeftLoading = true;
setTimeout(()=>{
that.animate()
that.leftAnimate1 = true;
that.scrollToBottom(()=>{
setTimeout(()=>{
that.animate()
that.leftAnimate1 = false;
console.log('endType',that.endType)
that.jobnum = that.getRandomNumber(100,500);
that.leftAnimate2 = true;
that.scrollToBottom(()=>{
setTimeout(()=>{
that.jobnum = that.getRandomNumber(500,2000);
setTimeout(()=>{
that.jobnum = that.getRandomNumber(2000,4000);
setTimeout(()=>{
that.jobnum = that.getRandomNumber(4000,8000);
setTimeout(()=>{
that.animate()
that.leftAnimate2 = false;
that.leftAnimate3 = true;
that.scrollToBottom(()=>{
setTimeout(()=>{
callback();
},800)
},'左3',10)
},800)
},400)
},400)
},400)
},'左2',10)
},800)
},'左1',10);
},800)
},
startRightAnimate(callback=()=>{}){
let that = this;
that.animate()
that.isShowRightLoading = true;
setTimeout(()=>{
that.animate()
that.rightAnimate1 = true;
that.scrollToBottom(()=>{
setTimeout(()=>{
that.animate()
that.rightAnimate1 = false;
that.rightAnimate2 = true;
that.scrollToBottom(()=>{
setTimeout(()=>{
callback();
},800)
},'右2',10)
},800)
},'右1',10);
},800)
},
getRandomNumber(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
},
handleLongPress($item) {
let that = this,
_array = [];
if($item.messageType == 99){
_array = ['删除'];
}else{
_array = ['删除','复制']
}
uni.showActionSheet({
itemList: _array,
success: function (res) {
if(res.tapIndex == 0){
that.handleDelete($item);
}else{
that.handleCopy($item);
}
},
fail: function (res) {
}
});
},
handleCopy($item){
uni.setClipboardData({
data: $item.msg,
success: () => {
uni.showToast({ title: '复制成功' });
},
fail: () => {
uni.showToast({ title: '复制失败', icon: 'none' });
}
});
},
handleDelete($item){
let that = this;
uni.showModal({
title: '提示',
content: '确定要删除此条消息吗?',
success: (res) => {
if (res.confirm) {
that.handleDelMsg($item);
}else{}
}
});
},
handleDelMsg($item){
let that = this;
if($item.id){
that.G.Post(
that.api.chat_delMsg, {
deleteAll:0,
ids: $item.id,
classify:$item.classify,
conversationId: that.cid,
},() => {
that.list.splice(that.list.findIndex(item => item.id === $item.id), 1);
// that.$nextTick(()=>{
// that.scrollToBottom(()=>{},'删除消息',300);
// })
}
)
}else{
uni.showToast({
icon:'none',
title:'请重试'
})
}
}
},
};
</script>
<style lang="scss">
.g_c_b {
color: #bbb;
}
.p-home-chat {
// background-color: #f5f5f5;
min-height: 100vh;
.g_bg_f0a {
background-color: #fe0000;
}
.chat-content {
width: calc(100% - 0px);
margin: 0 auto;
padding-bottom: 0px;
.chat-left {
padding: 0 10px;
.msg {
border-radius: 12px;
line-height: 1.5;
// letter-spacing: 1.5px;
word-break: break-all;
min-height: 25px;
}
}
.chat-right {
padding: 0 10px;
.msg {
border-radius: 12px;
line-height: 1.5;
// letter-spacing: 1.5px;
word-break: break-all;
min-height: 25px;
}
}
}
.chat-operate {
position: fixed;
width: 100%;
left: 0;
bottom: 0;
z-index: 1;
padding-bottom: calc(24rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
padding-top: 12px;
// background-color: #f5f5f5;
.m-input {
width: calc(100% - 20px);
margin: 0 auto;
border-radius: 40px;
height: 56px;
font-size: 16px;
}
input {
height: 56px;
}
}
.btn-share {
position: absolute;
right: 0;
top: 0;
z-index: 1;
width: 100%;
height: 100%;
opacity: 0;
}
.longpress-top-mask {
position: fixed;
left: 0;
// bottom: 120px;
width: 100vw;
// height: 170px;
z-index: 99;
background-color: transparent;
box-shadow: 0 -5px 10px rgba(245, 245, 245, 0.5);
}
.longpress-bottom-mask {
position: fixed;
left: 0;
bottom: 0;
width: 100vw;
// height: 170px;
z-index: 99;
background-color: rgba(245, 245, 245, 0.5);
box-shadow: 0 -5px 10px rgba(245, 245, 245, 0.5);
}
.longpress-bottom-down {
position: fixed;
left: 0;
bottom: 0;
width: 100vw;
// height: 170px;
z-index: 99;
background-color: #3578f6;
box-shadow: 0 -5px 10px rgba(245, 245, 245, 0.5);
}
@keyframes voi_animate {
0% {
height: 50%;
background-color: #ffffff;
}
20% {
height: 50%;
background-color: #ffffff;
}
50% {
height: 100%;
background-color: #ffffff;
}
80% {
height: 50%;
background-color: #ffffff;
}
100% {
height: 50%;
background-color: #ffffff;
}
}
.column-voice {
width: 100%;
height: 22px;
overflow: hidden;
// max-width: calc(100% - 140px);
max-width: calc(100% - 140px);
margin: 0 auto;
.column-item {
width: 3px;
height: 100%;
margin-left: 6px;
border-radius: 10px;
background-color: #ffffff;
vertical-align: middle;
display: inline-block;
}
}
}
/* */
.loader {
display: flex;
}
/* */
.dot {
width: 10px;
height: 10px;
margin: 0 2px;
border-radius: 50%;
background-color: #666;
animation: dotPulse 1s infinite ease-in-out;
}
/* */
.loades {
display: flex;
}
/* */
.dos {
width: 10px;
height: 10px;
margin: 0 2px;
border-radius: 50%;
background-color: #fff;
animation: dotPulse 1s infinite ease-in-out;
}
/* */
@keyframes dotPulse {
0%,
80%,
100% {
transform: scale(0.9);
}
40% {
transform: scale(1.1);
}
}
/* */
.dot:nth-child(2) {
animation-delay: -0.33s;
}
/* */
.dot:nth-child(3) {
animation-delay: -0.66s;
}
/* */
.dos:nth-child(2) {
animation-delay: -0.33s;
}
/* */
.dos:nth-child(3) {
animation-delay: -0.66s;
}
.container {
// position: relative;
// overflow-y: auto; /* */
// height: 100%; /* */
}
textarea {
resize: none;
/* */
overflow: hidden;
/* */
width: 100%;
/* */
}
.hasTopPadding {
padding-top: 8px !important;
box-sizing: content-box !important;
}
.biggerSize::after {
content: "";
/* display: inline-block; */
width: 60px;
height: 60px;
position: absolute;
left: 50%;
top: 50%;
z-index: 99;
transform: translate(-50%, -50%);
}
.markgroup {
height: 56px;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
.g_fs_32 {
font-size: 32px;
}
</style>