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.

947 lines
25 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">
<!-- <img src="../../static/image/1.jpg" alt="" style="position: fixed;
width: 100vw;
height: 100vh;
z-index: 999;
opacity: 0.7;"> -->
<!-- aaa{{textareaHeight}}bbb -->
<view class="chat-content g_flex_column_end g_clear_scroll" :style="{ paddingBottom: `${textareaHeight + 80}px` }">
<scroll-view scroll-y="true" :scroll-top="scrollTop" class="chat-link g_clear_scroll m-scroll "
scroll-with-animation :show-scrollbar='false' style="padding: 24px 0;">
<view class="g_flex_column_end g_clear_scroll" style="overflow-y: auto;min-height: 100%;">
<view class="g_mb_10" v-for="(item,index) in list" :key="index"
:class="index % 2 === 0 ? '' : 'g_flex_row_end'"
>
<view class="m-item chat-left g_flex_row_start" v-if="index % 2 === 0">
<view>
<view class="g_flex_row_start g_mb_10" v-if="gptType != 'ai-text' && gptType != 'ai'">
<view class="g_flex_column_center g_h_20" style="overflow: hidden;">
<image :src="item.avatar" mode="widthFix" class="g_w_20"></image>
</view>
<view class="g_flex_column_center g_ml_6 g_c_3 g_fs_15 g_fw_700">
{{item.username}}
</view>
</view>
<view class="loader" v-if="(index+1 == list.length) && index > 0 && showLoad">
<text class="dot"></text>
<text class="dot"></text>
<text class="dot"></text>
</view>
<view v-else>
<view class="msg g_bg_f g_c_0 g_pl_10 g_pr_10 g_pt_10 g_pb_10" style="min-width:calc(100vw - 20px);">
<view v-if="gptType == 'demo'">
<view class="" v-if="item.chat_type == 'text' && index == 0">
<g-chat-message :cusMessage='item.msg' />
</view>
<view class="" v-if="index == 2">
<g-chat-message cusMessage='住宿有要求吗?' />
</view>
<view v-if="item.chat_type == 'job' && index > 3">
<g-chat-job :cusAllJob='allJob'
:cusItem='item'
@exportToggle='toggleChange'
/>
</view>
</view>
<view v-if="gptType == 'ai' || gptType == 'ai-text'">
<view class="" v-if="item.chat_type == 'text'">
<g-chat-message :cusMessage='item.msg' />
</view>
<view class="" v-if="item.chat_type == 'rich-text'">
<g-chat-rich :cusMessage='item.msg' />
</view>
<view class="" v-if="item.chat_type == 'job'">
<g-chat-job :cusAllJob='allJob'
:cusItem='item'
@exportToggle='toggleChange'
/>
</view>
</view>
</view>
<!-- 底部操作 -->
<view class="g_flex_row_between g_mt_10 g_h_22 g_pl_10 g_pr_10">
<view class="g_flex_1 g_flex_row_start" v-if="item.isShowIcon">
<view class="g_flex_column_center" @click="handleUpdateGood(item,'good')">
<i class="iconfont icon-useful g_fs_20 g_c_8" :class="item.isGood ? 'g_c_main' : ''"></i>
</view>
<view class="g_flex_column_center g_ml_12" @click="handleUpdateGood(item,'down')">
<i class="iconfont icon-un_useful g_fs_20 g_c_8" :class="item.isDown ? 'g_c_main' : ''"></i>
</view>
</view>
<view class="g_flex_none g_flex_row_end" v-if="item.isShowIcon">
<view class="g_flex_column_center g_mr_12" @click="handleClickCopy(item.msg)">
<i class="iconfont icon-fuzhi1 g_fs_20 g_c_8"></i>
</view>
<view class="g_flex_column_center g_position_rela" @click="animate()">
<i class="iconfont icon-fenxiangfenxiangshare g_fs_20 g_c_8"></i>
<button class="btn-share" open-type="share"></button>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="m-item chat-right g_flex_row_end" 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">
<g-chat-message :cusMessage='item.msg' style="max-width: 80%;" />
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<view class="chat-operate g_flex_c">
<view class="g_bg_f m-input g_flex_row_center g_position_rela"
:class="hasTopPadding ? 'hasTopPadding' : ''"
:style="{
height: msgType == 'voice' ? '56px' : `${textareaHeight + 16}px`
}"
style="align-items: flex-end;min-height: 56px;"
>
<view class="g_flex_none g_flex_c g_h_56 g_w_48"
@click="handleUpdateMsgType();"
:class="voiceStatus == 0 && spec ? 'g_bg_main' : (voiceStatus == 1 ? 'g_bg_f0a' : '')"
style="border-radius: 40px 0px 0px 40px;"
v-if="gptType != 'ai-text'"
>
<i class="iconfont g_ml_20"
:class="msgType == 'text' ? 'icon-huatongyuyin g_fs_22' : 'icon-weixinjianpan2 g_fs_26'"
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 12px;"
:style="{ height: `${textareaHeight + 20}px` }"
@input="onInput"
@focus="onFocus"
@blur="onBlur"
@keyboardheightchange="keyboardheightchange"
@linechange="linechange"
/>
</div>
</view>
<view v-else>
<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 == 1 ? '松手 取消' : '按住 说话'}}
</view>
</view>
</view>
<view class="g_flex_none g_flex_c g_h_56 g_w_48 g_position_rela"
@click="handleSendMsg"
:class="voiceStatus == 0 && spec ? 'g_bg_main' : (voiceStatus == 1 ? 'g_bg_f0a' : '')"
style="border-radius: 0 40px 40px 0;"
>
<i class="iconfont icon-fasong g_fs_32 g_mr_20"
:class="sendIconStatus ? 'g_c_main' : 'g_c_b'"
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="sendMsg = '';textareaHeight = 22"
></i>
<view class="longpress-mask">
<view class="longpress-top-mask g_h_56 g_flex_c"
style="bottom:calc(70px + constant(safe-area-inset-bottom)); bottom: calc(70px + env(safe-area-inset-bottom));"
v-if="voiceStatus == 0 || voiceStatus == 1">
<div class="column-voice g_flex_row_center">
<div v-for="(item,index) in 26" :key="index" class="g_flex_column_center">
<div class="column-item" :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(70px + constant(safe-area-inset-bottom)); height: calc(70px + env(safe-area-inset-bottom));"
v-if="voiceStatus == 0"></view>
</view>
</view>
</view>
</view>
</template>
<script>
import {
chatMock
} from '../../../utils/chat.js';
var plugin = requirePlugin("WechatSI")
let manager = plugin.getRecordRecognitionManager()
export default {
onShareAppMessage() {
return this.G.shareFun();
},
data() {
return {
content: '',
textareaHeight: 30,
initialHeight: 0,
showLoad: false,
localBaseImg: this.G.store().localBaseImg,
scrollTop: 0,
windowHeight: 0,
list: chatMock,
dzj: "",
msgType: 'text',
sendMsg: '',
sendIconStatus: false,
voiceMsg: '按住 说话',
voiceStatus: -1, // -1 录音前 & 录音结束 0 录音时且在指定范围 1录音时但不在指定范围
recorderManager: null, // 录音管理对象
touchStartTime: 0, // 记录按下按钮的时间
longPressDelay: 0, // 设定长按所需时间,单位毫秒
spec: true,
isAuth: false,
content: '', // 内容
hasTopPadding: false,
allJob: [],
talkId:0,
gptType:'',
bottomHeight:100,
};
},
onLoad(options) {
let that = this;
console.log('聊天页参数:',options)
if(options.type){
that.gptType = options.type;
}else{
that.gptType = 'ai';
}
that.initSI();
},
onShow() {
let that = this;
that.voiceStatus = -1;
// 创建查询对象
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;
} else {
console.error('未找到元素');
}
});
// that.list = [
// {
// avatar:uni.getStorageSync("miniApp-info").logo,
// username:uni.getStorageSync("miniApp-info").fullName,
// msg:'Hi,我是' + uni.getStorageSync("miniApp-info").fullName +',很高兴见到你',
// isGood:false,
// isDown:false,
// chat_type:'text',
// isToggle:false,
// isShowIcon:true,
// }
// ];
that.initSocket();
},
watch: {
'sendMsg'(val) {
if (val) {
this.sendIconStatus = true;
} else {
this.sendIconStatus = false;
}
}
},
methods: {
initSocket(){
let that = this;
// 创建会话
if(that.gptType == 'ai' || that.gptType == 'ai-text'){
that.G.Get(that.api.chat_create,{},(res)=>{
console.log('获取 会话ID',res)
that.talkId = res;
});
}else{
that.getList();
that.talkId = 0;
}
},
getList(callback=()=>{}) {
let that = this;
that.windowHeight = uni.getSystemInfoSync().windowHeight - 150;
if(!that.api.chat_daotian_job){
return false;
}
that.G.Post(that.api.chat_daotian_job, {
pageNum: 1,
pageSize: 8,
classify: 1,
sex: -1,
workTypeStr:"",
lat: "",
lng: "",
jobClassify:"",
sortTag: 0,
jobSpecialLabelIds:"",
cityName: '全国',
brandIds: '',
jobCategoryLabelIds:'',
ucj: 0,
provinceName:'',
keys: ''
}, (res) => {
console.log('稻田职位列表',res)
if (res.pageBean.recordCount > 0) {
res.pageBean.recordList = that.G.toGetAddress(res.pageBean.recordList);
res.pageBean.recordList = that.G.toGetAge(res.pageBean.recordList);
res.pageBean.recordList = that.G.yijobCopy(res.pageBean.recordList);
that.allJob = res.pageBean.recordList.map(item => {
return {
...item,
isToggle:false,
_time:that.G.setDeadLine(item.createTime,'jiaofu'),
liuNum: item.liuNum,
baoNum: uni.getStorageSync("lin-bao") ? uni.getStorageSync("lin-bao") / 10 : Math.ceil(item.liuNum / 10),
collected: item.collected ? true : false,
cus_price: item.salaryClassify != 7 ? that.G.getSalaryClassifyValueHtml(item.salaryClassify, item.salaryClassifyValue) : "月薪",
isDown:false
}
});
} else {
that.allJob = [];
};
setTimeout(()=>{
that.scrollToBottom();
callback();
},300);
})
},
toggleChange(){
let that = this;
setTimeout(()=>{
that.scrollToBottom();
},600);
},
keyboardheightchange(e) {
if (e.target.dataset.height == 0 && this.sendMsg == "") {
this.textareaHeight = 30
}
},
onInput(e) {
this.content = e.target.value;
this.updateTextareaHeight();
this.scrollToBottom();
},
onFocus() {
this.initialHeight = this.textareaHeight;
},
onBlur() {
this.updateTextareaHeight();
},
updateTextareaHeight() {
uni.createSelectorQuery()
.in(this)
.select('#textarea')
.boundingClientRect((rect) => {
this.textareaHeight = rect.height;
})
.exec();
},
linechange(e) {
if (e.detail.lineCount > 1) {
this.hasTopPadding = true
} else {
this.hasTopPadding = false
}
},
scrollToBottom() {
let that = this;
wx.pageScrollTo({
scrollTop: 999999,
duration: 100
})
},
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() {
this.animate()
if (this.msgType == 'text') {
this.msgType = 'voice'
this.checkRecordingPermission().then(() => {}).catch((error) => {});
} else {
this.msgType = 'text'
}
},
handleSendMsg() {
let that = this;
if (that.msgType == 'voice') {
return false;
}
if (that.sendMsg == '') {
uni.showToast({
icon: 'none',
title: '请输入内容'
})
return false;
};
that.animate()
that.sendAI();
setTimeout(() => {
that.sendMsg = '';
that.updateTextareaHeight();
that.scrollToBottom();
that.textareaHeight = 30
}, 10);
setTimeout(() => {
that.textareaHeight = 30
}, 80);
},
checkRecordingPermission() {
var that = this;
return new Promise((resolve, reject) => {
wx.getSetting({
success: (res) => {
if (res.authSetting['scope.record']) {
that.voiceStatus = -1
that.isAuth = true;
resolve(true);
} else {
that.voiceStatus = -1
wx.authorize({
scope: 'scope.record',
success: () => {
that.voiceStatus = -1
that.isAuth = true;
resolve(true);
},
fail: () => {
that.isAuth = false;
reject(new Error('录音权限未授权'));
},
});
}
},
fail: () => {
that.voiceStatus = -1
reject(new Error('无法获取权限设置'));
},
});
});
},
onTouchStart() {
let that = this;
if (!this.isAuth) {
return false;
}
this.touchStartTime = Date.now();
this.longPressTimer = setTimeout(() => {
this.checkRecordingPermission().then(() => {
this.voiceStatus = -1;
this.spec = true;
that.animate();
this.startRecording();
}).catch((error) => {});
}, this.longPressDelay);
},
onTouchMove(e) {
let that = this;
const touch = e.touches[0];
let _x = touch.clientX,
_y = touch.clientY;
if (_y > (uni.getSystemInfoSync().windowHeight - that.bottomHeight) &&
_x > 30 &&
_x < (uni.getSystemInfoSync().windowWidth - 30)
) {
this.voiceStatus = 0;
this.spec = true;
} else {
this.voiceStatus = 1;
this.spec = false;
}
},
onTouchEnd() {
let that = this;
const touchEndTime = Date.now();
clearTimeout(this.longPressTimer);
// debugger
if (this.voiceStatus == 0 || this.voiceStatus == 1) {
this.stopRecording();
} else if (touchEndTime - this.touchStartTime < this.longPressDelay) {
this.voiceStatus = -1
} else {
setTimeout(() => {
that.voiceStatus = -1
manager.stop();
}, 300)
}
},
startRecording() {
this.voiceStatus = 0; // 标记为正在录音
// 语音识别开始
manager.start({
duration: 30000,
lang: 'zh_CN',
});
},
stopRecording() {
let that = this;
// 语音识别结束
manager.stop();
that.voiceStatus = -1;
},
// 插件初始化
initSI() {
const that = this;
manager.onRecognize = function(res) {
};
manager.onStart = function(res) {
// 开始录音时-抖动一下手机
// that.animate();
};
// 识别错误事件
manager.onError = function(res) {
console.error('error msg', res);
const tips = {
'-30003': '说话时间间隔太短,无法识别语音',
'-30004': '没有听清,请再说一次~',
'-30011': '上个录音正在识别中,请稍后尝试',
};
const retcode = res?.retcode.toString();
retcode &&
wx.showToast({
title: tips[`${retcode}`],
icon: 'none',
duration: 2000,
});
};
manager.onStop = function(res) {
if (res.result === '') {
wx.showModal({
title: '提示',
content: '没有听清,请再说一次~',
showCancel: false,
});
return;
}
that.content = '';
var text = that.content + res.result;
that.content = text;
that.sendAI('stop');
};
},
sendAI($form = '') {
let that = this;
if ($form == 'stop') {
if (that.voiceStatus == 1) {
that.voiceStatus = -1;
that.scrollToBottom();
return false;
}
}
that.voiceStatus = -1;
that.list.push({
avatar: '',
username: '',
msg: that.msgType == 'text' ? that.sendMsg : that.content,
isGood: false,
isDown:false,
chat_type: that.sendMsg.indexOf('职位') > -1 ? 'job' : 'job',
isToggle: false,
isShowIcon:false,
}, );
var str = "根据输入框文本返回的内容根据输入框文本返回的内容根据输入框文本返回的内容"
var result = "";
let index = 0;
if(that.gptType == 'ai' || that.gptType == 'ai-text'){
that.list.push({
avatar: uni.getStorageSync("miniApp-info").logo,
username: uni.getStorageSync("miniApp-info").fullName,
msg: '',
isGood: false,
isDown:false,
chat_type:'',
isToggle: false,
isShowIcon:false,
});
that.textareaHeight = 30
that.showLoad = true;
that.G.Post(that.api.chat_send,{
content:that.msgType == 'text' ? that.sendMsg : that.content,
talkId:that.talkId
},(res)=>{
if(res.jobIds){
// 调用新职位接口
console.log('获取职位参数:',res.jobIds)
let mockJobIdGroup = [
10938,10977,10973,10605,10604,10355,10971,11025,9467,9909,10584,10562,10242,73,38,77,1,29,44
];
// 随机8个逗号拼接
that.getNewList(res.jobIds + ',' + that.getRandomElements(mockJobIdGroup, Number(8 - res.jobIds.split(',').length)).join(','),()=>{
base()
});
}else{
// 调用稻田职位接口
that.getList(()=>{
base()
});
}
function base(){
if(res.talk){
that.list[that.list.length-1].msg = res.content;
that.list[that.list.length-1].chat_type = 'text';
}else{
if(res.classify == 'text'){
that.list[that.list.length-1].chat_type = 'rich-text';
// 去除 **
res.content = res.content.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
that.list[that.list.length-1].msg = res.content;
}else{
that.list[that.list.length-1].chat_type = 'job';
}
}
setTimeout(()=>{
that.handleSet(res.content,result,index,300);
},100);
}
})
}else{
that.list.push({
avatar: uni.getStorageSync("miniApp-info").logo,
username: uni.getStorageSync("miniApp-info").fullName,
msg: that.msgType == 'text' ? "" : '根据语音文件返回的ai内容',
isGood: false,
isDown:false,
chat_type: that.sendMsg.indexOf('职位') > -1 ? 'job' : 'job',
isToggle: false,
isShowIcon:false,
});
that.textareaHeight = 30
that.showLoad = true;
that.handleSet(str,result,index,1500);
}
},
handleSet(str,result,index,time){
let that = this;
setTimeout(() => {
that.updateTextareaHeight();
that.scrollToBottom();
}, 10);
setTimeout(() => {
that.showLoad = false;
// const typingInterval = setInterval(() => {
// if (index < str.length) {
// result += str[index];
// that.list[that.list.length - 1].msg = result
// index++;
// that.scrollToBottom();
// } else {
that.list[that.list.length - 1].isShowIcon = true;
// clearInterval(typingInterval);
// }
// }, 100); // 每100ms显示一个字符可以根据需要调整速度
}, time);
},
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);
},
getNewList($str,callback=()=>{}){
let that = this;
that.G.Get(that.api.chat_ai_job + '?storeJobIds=' + $str,{},(res)=>{
console.log('获取聊天职位列表:',res)
if (res.list && res.list.length > 0) {
res.list = that.G.toGetAddress(res.list);
res.list = that.G.toGetAge(res.list);
res.list = that.G.yijobCopy(res.list);
that.allJob = res.list.map(item => {
return {
...item,
isToggle:false,
_time:that.G.setDeadLine(item.updateTime,'jiaofu'),
liuNum: item.liuNum,
baoNum: uni.getStorageSync("lin-bao") ? uni.getStorageSync("lin-bao") / 10 : Math.ceil(item.liuNum / 10),
collected: item.collected ? true : false,
cus_price: item.salaryClassify != 7 ? that.G.getSalaryClassifyValueHtml(item.salaryClassify, item.salaryClassifyValue) : "月薪",
}
});
} else {
that.allJob = [];
};
setTimeout(()=>{
that.scrollToBottom();
callback();
},300);
callback();
})
},
animate() {
uni.vibrateShort({
success: function() {
}
});
}
}
};
</script>
<style lang="scss">
.g_c_b {
color: #bbb;
}
.p-home-chat {
background-color: #efefef;
.g_bg_f0a{
background-color: #CC463A;
}
.chat-content {
width: calc(100% - 0px);
margin: 0 auto;
min-height: 100vh;
padding-bottom: 100px;
.chat-left {
padding: 0 10px;
.msg {
border-radius: 12px;
line-height: 1.5;
// letter-spacing: 1.5px;
word-break: break-all;
}
}
.chat-right {
padding: 0 10px;
.msg {
border-radius: 12px;
line-height: 1.5;
// letter-spacing: 1.5px;
word-break: break-all;
}
}
}
.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: #efefef;
.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: rgba(245, 245, 245, 0.5);
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);
}
@keyframes voi_animate {
0% {
height: 50%;
background-color: #1677ff;
}
20% {
height: 50%;
background-color: #92F7FE;
}
50% {
height: 100%;
background-color: #1989fa;
}
80% {
height: 50%;
background-color: #92F7FE;
}
100% {
height: 50%;
background-color: #1677ff;
}
}
.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: #1677ff;
vertical-align: middle;
display: inline-block;
}
}
}
/* */
.loader {
display: flex;
// justify-content: center;
// align-items: center;
// height: 100vh; /* 使 */
}
/* */
.dot {
width: 10px;
height: 10px;
margin: 0 2px;
border-radius: 50%;
background-color: #666;
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;
}
.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;
}
</style>