我使用的是uniapp管网的uni-ai-chat插件,查看后续内容前,请先确认是否使用的也是该插件
uniapp官方插件介绍:https://uniapp.dcloud.net.cn/uniCloud/uni-ai-chat.html
插件地址:https://ext.dcloud.net.cn/plugin?name=uni-ai-chat
核心代码
主要就是重写了send方法中的部分逻辑,我这里是前端直接调用的百度的大模型知识库接口,需要将acess_token拼接在url上,这是不安全的,建议后台封装接口后,前端去调后端的接口
async send() {
let messages = []
// 复制一份,消息列表数据
let msgs = JSON.parse(JSON.stringify(this.msgList))
// - 获取上下文的代码【start】-
// 带总结的消息 index
let findIndex = [...msgs].reverse().findIndex(item => item.summarize)
// console.log('findIndex', findIndex)
if (findIndex != -1) {
let aiSummaryIndex = msgs.length - findIndex - 1
// console.log('aiSummaryIndex', aiSummaryIndex)
// 将带总结的消息的 内容 更换成 总结
msgs[aiSummaryIndex].content = msgs[aiSummaryIndex].summarize
// 拿最后一条带直接的消息作为与ai对话的msg body
msgs = msgs.splice(aiSummaryIndex)
} else {
// 如果未总结过就直接从末尾拿10条
msgs = msgs.splice(-10)
}
// 过滤涉敏问题
msgs = msgs.filter(msg => !msg.illegal)
// - 获取上下文的代码【end】-
// 如果:不希望带上上下文;请注释掉 上方:获取上下文的代码【start】-【end】。并添加,代码: msgs = [msgs.pop()]
// 根据数据内容设置角色
messages = msgs.map(item => {
// 角色默认为用户
let role = "user"
// 如果是ai再根据 是否有总结 来设置角色为 system 还是 assistant
if (item.isAi) {
role = item.summarize ? 'system' : 'assistant'
}
return {
content: item.content,
role
}
})
this.sliceMsgToLastMsg = new SliceMsgToLastMsg(this)
const requestTask = uni.request({
url: url, //这里我对接的是百度知识库的接口,例:url?access_token=xxxx
method: 'POST',
enableChunked: true,
data: {
"query": this.content,
"plugins": [
"uuid-zhishiku"
],
"verbose": true,
"stream": true
},
success: response => {},
fail: () => {},
complete: () => {}
});
requestTask.onChunkReceived(chunk => {
const arrayBuffer = chunk.data;
const uint8Array = new Uint8Array(arrayBuffer);
const str = new TextEncoding.TextDecoder('utf-8').decode(uint8Array);
var reciveMsg = '';
var msgArr = [];
var endMsg = ''
//百度的接口正常消息体会有一个is_end字段,其他的消息没有,这里我们不需要其他信息,所以做个过滤,is_end 为true 代表结束消息
if (str.indexOf('is_end') !== -1) {
if (str.indexOf('data') !== -1) {
//这里有个坑,百度返回的消息是含data:的字符串,冒号右边是json格式,也是我们需要的内容,需要将右边提取出来
msgArr = str.split('data: ')
reciveMsg = JSON.parse(msgArr[1])
//默认每一次将结束句存一次,我也不知道是百度抽风还是我代码问题,有时候结束句会和倒数第二句一起返回,所以默认都把当前的这句存一下
endMsg = JSON.parse(msgArr[1])
}
if (reciveMsg.id) {
if (this.sseIndex === 0) {
this.msgList.push({
isAi: true,
content: reciveMsg.result,
create_time: Date.now()
})
} else {
this.sliceMsgToLastMsg.addMsg(reciveMsg.result)
}
//开启流式
this.showLastMsg()
// 让流式响应计数值递增
this.sseIndex++
}
//这里就是结束句和倒数第二句一起返回的
if (msgArr[2]) {
endMsg = JSON.parse(msgArr[2])
}
//结束,立即停止流式
if (endMsg && endMsg.is_end) {
this.sliceMsgToLastMsg.t = 0
this.sseIndex = 0
this.content = ''
}
}
})
},
完整代码
<template>
<view class="container">
<view class="nav fixed">
<view class="mini-status" :style="{'height':statusBarHeight + 'px'}"></view>
<view class="mini-nav" :style="{'height':titleBarHeight + 'px', 'lineHeight':titleBarHeight + 'px'}">
<van-icon name="arrow-left" size="42rpx" @click="handleReturn" />
<text>AI智能问答</text>
</view>
</view>
<!-- #ifdef H5 -->
<view v-if="isWidescreen" class="header">uni-ai-chat</view>
<!-- #endif -->
<!-- <text class="noData" v-if="msgList.length === 0">你好,我是云网小智
点击下方输入框,试着跟我聊天吧~
</text> -->
<scroll-view :scroll-into-view="scrollIntoView" scroll-y="true" class="msg-list" :enable-flex="true">
<view class="default-question">
<text class="ai-name">你好,我是小智</text>
<text class="ai-desc">作为你的智能伙伴,我既能写文案、想点子,又能陪你聊天、答疑解惑。</text>
<text class="ai-desc">你可以试着问我:</text>
<view class="question-list">
<view @click="defaulSend(item)" :style="'background-image:url('+ques_first+')'"
class="question-item" v-for="(item,index) in question" :key="index">
{{item}}
</view>
</view>
<text class="ai-desc">特别说明:回答内容均为Al算法生成,不代表平台官方回复!</text>
</view>
<uni-ai-msg ref="msg" v-for="(msg,index) in msgList" :key="index" :msg="msg" @changeAnswer="changeAnswer(index)"
:show-cursor="index == msgList.length - 1 && msgList.length%2 === 0 && sseIndex"
:isLastMsg="index == msgList.length - 1" @removeMsg="removeMsg(index)"></uni-ai-msg>
<template v-if="msgList.length%2 !== 0">
<view v-if="requestState == -100" class="retries-box">
<text>消息发送失败</text>
<uni-icons @click="send" color="#d22" type="refresh-filled" class="retries-icon"></uni-icons>
</view>
<view class="tip-ai-ing" v-else-if="msgList.length">
<text>小智正在思考中...</text>
</view>
</template>
<view @click="closeSseChannel" class="stop-responding" v-if="sseIndex"> ▣ 停止回答</view>
<view id="last-msg-item" style="height: 1px;"></view>
</scroll-view>
<view class="foot-box" :style="{'padding-bottom':footBoxPaddingBottom}">
<!-- #ifdef H5 -->
<view class="pc-menu" v-if="isWidescreen">
<image class="menu-image" src="https://minio.ptd.dageek.cn/iip-dev/upload/static/message.png"
mode="heightFix"></image>
</view>
<!-- #endif -->
<view class="foot-box-content">
<view v-if="!isWidescreen" class="menu">
<image src="https://minio.ptd.dageek.cn/iip-dev/upload/static/message.png" class="menu-image"
mode="heightFix"></image>
</view>
<view class="textarea-box">
<textarea v-model="content" :cursor-spacing="15" class="textarea" :auto-height="!isWidescreen"
placeholder="请输入要发给小智的内容" :maxlength="-1" :adjust-position="false"
:disable-default-padding="false" placeholder-class="input-placeholder"></textarea>
</view>
<view class="send-btn-box" :title="(msgList.length && msgList.length%2 !== 0) ? '小智正在回复中不能发送':''">
<!-- #ifdef H5 -->
<text v-if="isWidescreen" class="send-btn-tip">↵ 发送 / shift + ↵ 换行</text>
<!-- #endif -->
<button @click="beforeSend" :disabled="inputBoxDisabled || !content" class="send"
type="primary">发送</button>
</view>
</view>
</view>
<uni-popup ref="popup" type="top">
<view class="box">
<text class="title">请选择llm的model</text>
<radio-group @change="radioChange" class="radio-group">
<label class="item" v-for="(item, index) in models" :key="item.value">
<radio :value="item.value" :checked="currentModel === item.value" class="radio" />
<view class="item-title">{{item.text}}</view>
</label>
</radio-group>
<view class="btn-box">
<view @click="cancel" class="btn cancel">取消</view>
<view @click="confirm" class="btn confirm">确认</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
let confirmCallback = () => {}
import * as TextEncoding from '@/uni_modules/text-encoding-shim/index.js';
import mixin from '@/mixins/index.js'
// 引入配置文件
import config from '@/config.js';
// 导入uniCloud云对象task模块
import uniCoTask from '@/common/unicloud-co-task.js';
// 导入 将多个字消息文本,分割成单个字 分批插入到最末尾的消息中 的类
import SliceMsgToLastMsg from './SliceMsgToLastMsg.js';
// 收集所有执行云对象的任务列表
let uniCoTaskList = []
// 定义终止并清空 云对象的任务列表中所有 任务的方法
uniCoTaskList.clear = function() {
// 执行数组内的所有任务
uniCoTaskList.forEach(task => task.abort())
// 清空数组
uniCoTaskList.slice(0, uniCoTaskList.length)
}
// 获取广告id
const {
adpid
} = config
// 初始化sse通道
let sseChannel = false;
// 键盘的shift键是否被按下
let shiftKeyPressed = false
export default {
mixins: [mixin],
data() {
return {
models: [{
text: "gpt-4",
value: "gpt-4"
},
{
text: "gpt-4-0314",
value: "gpt-4-0314"
},
{
text: "gpt-4-32k",
value: "gpt-4-32k"
},
{
text: "gpt-4-32k-0314",
value: "gpt-4-32k-0314"
},
{
text: "gpt-3.5-turbo",
value: "gpt-3.5-turbo"
},
{
text: "gpt-3.5-turbo-0301",
value: "gpt-3.5-turbo-0301"
},
{
text: "都不选",
value: ""
}
],
currentModel: '',
// 使聊天窗口滚动到指定元素id的值
scrollIntoView: "",
// 消息列表数据
msgList: [
],
// 通讯请求状态
requestState: 0, //0发送中 100发送成功 -100发送失败
// 本地对话是否因积分不足而终止
insufficientScore: false,
// 输入框的消息内容
content: "",
// 记录流式响应次数
sseIndex: 0,
// 是否启用流式响应模式
enableStream: true,
// 当前屏幕是否为宽屏
isWidescreen: false,
// 广告位id
adpid,
llmModel: false,
keyboardHeight: 0,
//预制问题
question: [
'工业互联网是什么?',
'网络安全管理办法是什么?',
'智改数转是什么?'
],
ques_first: "https://minio.ptd.dageek.cn/iip-dev/upload/static/ques_first.jpg",
ques_follow: "https://minio.ptd.dageek.cn/iip-dev/upload/static/ques_follow.jpg",
}
},
computed: {
// 输入框是否禁用
inputBoxDisabled() {
// 如果正在等待流式响应,则禁用输入框
if (this.sseIndex !== 0) {
return true
} else {
return false
}
// 如果消息列表长度为奇数,则禁用输入框
return !!(this.msgList.length && this.msgList.length % 2 !== 0)
},
// 获取当前环境
NODE_ENV() {
return process.env.NODE_ENV
},
footBoxPaddingBottom() {
return (this.keyboardHeight || 10) + 'px'
}
},
// 监听msgList变化,将其存储到本地缓存中
watch: {
msgList: {
handler(msgList) {
// 将msgList存储到本地缓存中
console.log('进行消息缓存');
uni.setStorage({
"key": "uni-ai-msg",
"data": msgList
})
},
// 深度监听msgList变化
deep: true
},
insufficientScore(insufficientScore) {
uni.setStorage({
"key": "uni-ai-chat-insufficientScore",
"data": insufficientScore
})
},
llmModel(llmModel) {
let title = 'uni-ai-chat'
if (llmModel) {
title += ` (${llmModel})`
}
// uni.setNavigationBarTitle({title})
// #ifdef H5
if (this.isWidescreen) {
document.querySelector('.header').innerText = title
}
// #endif
uni.setStorage({
key: 'uni-ai-chat-llmModel',
data: llmModel
})
}
},
beforeMount() {
// #ifdef H5
// 监听屏幕宽度变化,判断是否为宽屏 并设置isWidescreen的值
uni.createMediaQueryObserver(this).observe({
minWidth: 650,
}, matches => {
this.isWidescreen = matches;
})
// #endif
},
async mounted() {
// 获得历史对话记录
this.msgList = uni.getStorageSync('uni-ai-msg') || [];
if (this.msgList.length > 0) {
this.msgList[this.msgList.length - 1].isLast = true
}
// 获得之前设置的llmModel
this.llmModel = uni.getStorageSync('uni-ai-chat-llmModel')
// 获得之前设置的uni-ai-chat-insufficientScore
this.insufficientScore = uni.getStorageSync('uni-ai-chat-insufficientScore') || false
// 如果上一次对话中 最后一条消息ai未回复。则一启动就自动重发。
let length = this.msgList.length
if (length) {
let lastMsg = this.msgList[length - 1]
if (!lastMsg.isAi) {
this.send()
}
}
// 在dom渲染完毕后 使聊天窗口滚动到最后一条消息
this.$nextTick(() => {
this.showLastMsg()
})
// #ifdef H5
//获得消息输入框对象
let adjunctKeydown = false
const textareaDom = document.querySelector('.textarea-box textarea');
if (textareaDom) {
//键盘按下时
textareaDom.onkeydown = e => {
// console.log('onkeydown', e.keyCode)
if ([16, 17, 18, 93].includes(e.keyCode)) {
//按下了shift ctrl alt windows键
adjunctKeydown = true;
}
if (e.keyCode == 13 && !adjunctKeydown) {
e.preventDefault()
// 执行发送
setTimeout(() => {
this.beforeSend();
}, 300)
}
};
textareaDom.onkeyup = e => {
//松开adjunct键
if ([16, 17, 18, 93].includes(e.keyCode)) {
adjunctKeydown = false;
}
};
// 可视窗口高
let initialInnerHeight = window.innerHeight;
if (uni.getSystemInfoSync().platform == "ios") {
textareaDom.addEventListener('focus', () => {
let interval = setInterval(function() {
if (window.innerHeight !== initialInnerHeight) {
clearInterval(interval)
// 触发相应的回调函数
document.querySelector('.container').style.height = window
.innerHeight + 'px'
window.scrollTo(0, 0);
this.showLastMsg()
}
}, 1);
})
textareaDom.addEventListener('blur', () => {
document.querySelector('.container').style.height = initialInnerHeight + 'px'
})
} else {
window.addEventListener('resize', (e) => {
this.showLastMsg()
})
}
}
// #endif
// #ifndef H5
uni.onKeyboardHeightChange(e => {
this.keyboardHeight = e.height
// 在dom渲染完毕后 使聊天窗口滚动到最后一条消息
this.$nextTick(() => {
this.showLastMsg()
})
})
// #endif
},
methods: {
// 选择默认问题,并发送
defaulSend(text) {
this.content = text
this.beforeSend()
},
setLLMmodel() {
this.$refs['popup'].open(model => {
console.log('model', model);
this.llmModel = model
})
},
// 此(惰性)函数,检查是否开通uni-push;决定是否启用enableStream
async checkIsOpenPush() {
try {
// 获取推送客户端id
await uni.getPushClientId()
// 如果获取成功,则将checkIsOpenPush函数重写为一个空函数
this.checkIsOpenPush = () => {}
} catch (err) {
// 如果获取失败,则将enableStream设置为false
this.enableStream = false
}
},
// 更新最后一条消息
updateLastMsg(param) {
let length = this.msgList.length
if (length === 0) {
return
}
let lastMsg = this.msgList[length - 1]
// 如果param是函数,则将最后一条消息作为参数传入该函数
if (typeof param == 'function') {
let callback = param;
callback(lastMsg)
} else {
// 否则,将参数解构为data和cover两个变量
const [data, cover = false] = arguments
if (cover) {
lastMsg = data
} else {
lastMsg = Object.assign(lastMsg, data)
}
}
this.msgList.splice(length - 1, 1, lastMsg)
},
// 换一个答案
async changeAnswer(index) {
// 如果问题还在回答中需要先关闭
if (this.sseIndex) {
this.sseIndex = 0
}
//删除旧的回答
this.content = this.msgList[index-1].content
this.msgList.pop()
this.updateLastMsg({
// 防止 偶发答案涉及敏感,重复回答时。提问内容 被卡掉无法重新问
illegal: false
})
this.send()
},
removeMsg(index) {
// 成对删除,如果点中的是 ai 回答的内容,index -= 1
if (this.msgList[index].isAi) {
index -= 1
}
// 如果删除的就是正在问的,且问题还在回答中需要先关闭
if (this.sseIndex && index == this.msgList.length - 2) {
this.closeSseChannel()
}
this.msgList.splice(index, 2)
},
async beforeSend() {
if (this.inputBoxDisabled) {
return uni.showToast({
title: '云网小智正在回复中不能发送',
icon: 'none'
});
}
// 如果内容为空
if (!this.content) {
// 弹出提示框
return uni.showToast({
// 提示内容
title: '内容不能为空',
// 不显示图标
icon: 'none'
});
}
// 将用户输入的消息添加到消息列表中
this.msgList.push({
// 标记为非人工智能机器人,即:为用户发送的消息
isAi: false,
// 消息内容
content: this.content,
// 消息创建时间
create_time: Date.now()
})
// 展示最后一条消息
this.showLastMsg()
// dom加载完成后 清空文本内容
this.$nextTick(() => {
this.content = ''
})
this.send() // 发送消息
},
async send() {
let messages = []
// 复制一份,消息列表数据
let msgs = JSON.parse(JSON.stringify(this.msgList))
// - 获取上下文的代码【start】-
// 带总结的消息 index
let findIndex = [...msgs].reverse().findIndex(item => item.summarize)
// console.log('findIndex', findIndex)
if (findIndex != -1) {
let aiSummaryIndex = msgs.length - findIndex - 1
// console.log('aiSummaryIndex', aiSummaryIndex)
// 将带总结的消息的 内容 更换成 总结
msgs[aiSummaryIndex].content = msgs[aiSummaryIndex].summarize
// 拿最后一条带直接的消息作为与ai对话的msg body
msgs = msgs.splice(aiSummaryIndex)
} else {
// 如果未总结过就直接从末尾拿10条
msgs = msgs.splice(-10)
}
// 过滤涉敏问题
msgs = msgs.filter(msg => !msg.illegal)
// - 获取上下文的代码【end】-
// 如果:不希望带上上下文;请注释掉 上方:获取上下文的代码【start】-【end】。并添加,代码: msgs = [msgs.pop()]
// 根据数据内容设置角色
messages = msgs.map(item => {
// 角色默认为用户
let role = "user"
// 如果是ai再根据 是否有总结 来设置角色为 system 还是 assistant
if (item.isAi) {
role = item.summarize ? 'system' : 'assistant'
}
return {
content: item.content,
role
}
})
this.sliceMsgToLastMsg = new SliceMsgToLastMsg(this)
const requestTask = uni.request({
url: url,//这里我对接的是百度知识库的接口,例:url?access_token=xxxx
method: 'POST',
enableChunked: true,
data: {
"query": this.content,
"plugins": [
"uuid-zhishiku"
],
"verbose": true,
"stream": true
},
success: response => {},
fail: () => {},
complete: () => {}
});
requestTask.onChunkReceived(chunk => {
const arrayBuffer = chunk.data;
const uint8Array = new Uint8Array(arrayBuffer);
const str = new TextEncoding.TextDecoder('utf-8').decode(uint8Array);
var reciveMsg ='';
var msgArr=[];
var endMsg =''
// 看一下 打印出来的结果
if(str.indexOf('is_end') !==-1){
if(str.indexOf('data') !==-1){
msgArr = str.split('data: ')
reciveMsg = JSON.parse(msgArr[1])
endMsg = JSON.parse(msgArr[1])
}
if(reciveMsg.id){
if (this.sseIndex === 0) {
this.msgList.push({
isAi: true,
content: reciveMsg.result,
create_time: Date.now()
})
} else {
this.sliceMsgToLastMsg.addMsg(reciveMsg.result)
}
this.showLastMsg()
// 让流式响应计数值递增
this.sseIndex++
}
if(msgArr[2]){
endMsg = JSON.parse(msgArr[2])
}
if(endMsg&&endMsg.is_end){
this.sliceMsgToLastMsg.t = 0
this.sseIndex = 0
this.content =''
}
}
})
},
closeSseChannel() {
// 如果存在消息通道,就关闭消息通道
if (sseChannel) {
sseChannel.close()
// 设置为 false 防止重复调用closeSseChannel时出错
sseChannel = false
this.sliceMsgToLastMsg.end()
}
// 清空历史网络请求(调用云对象)任务
uniCoTaskList.clear()
// 将流式响应计数值归零
this.sseIndex = 0
},
// 滚动窗口以显示最新的一条消息
showLastMsg() {
// 等待DOM更新
this.$nextTick(() => {
// 将scrollIntoView属性设置为"last-msg-item",以便滚动窗口到最后一条消息
this.scrollIntoView = "last-msg-item"
// 等待DOM更新,即:滚动完成
this.$nextTick(() => {
// 将scrollIntoView属性设置为空,以便下次设置滚动条位置可被监听
this.scrollIntoView = ""
})
})
},
// 清空消息列表
clearAllMsg(e) {
// 弹出确认清空聊天记录的提示框
uni.showModal({
title: "确认要清空聊天记录?",
content: '本操作不可撤销',
complete: (e) => {
// 如果用户确认清空聊天记录
if (e.confirm) {
// 关闭ssh请求
this.closeSseChannel()
// 将消息列表清空
this.msgList.splice(0, this.msgList.length);
}
}
});
},
open(callback) {
this.currentModel = uni.getStorageSync('uni-ai-chat-llmModel')
confirmCallback = callback
console.log('打开选择模型窗口');
console.log('组件内', this.$refs);
this.$refs.popup.open('center')
},
radioChange(event) {
console.log('event', event.detail.value)
this.currentModel = event.detail.value
},
cancel() {
this.$refs.popup.close()
},
confirm() {
// console.log(this.models[this.current]);
confirmCallback(this.currentModel)
this.$refs.popup.close()
},
}
}
</script>
<style lang="scss">
.default-question {
width: 80%;
margin: 0 auto;
background: #fff;
border-radius: 8px;
box-shadow: 0 16px 20px 0 rgba(174, 167, 223, .06);
padding: 30rpx;
display: flex;
flex-direction: column;
.ai-name {
color: #05073b;
font-size: 35rpx;
font-weight: 600;
}
.ai-desc {
color: #676c90;
font-family: PingFangSC-Regular;
font-size: 23rpx;
font-weight: 400;
text-align: justify;
margin-top: 10rpx;
line-height: 40rpx;
}
.question-list {
display: flex;
flex-direction: column;
}
.question-item {
color: #120649;
font-family: PingFangSC-Medium;
font-size: 25rpx;
font-weight: 500;
letter-spacing: 0;
padding: 32rpx;
background: #f6f8fd;
background-size: 100% 100%;
margin-top: 10rpx;
}
}
/* #ifdef VUE3 && APP-PLUS */
@import "@/components/uni-ai-msg/uni-ai-msg.scss";
/* #endif */
/* #ifndef APP-NVUE */
page,
/* #ifdef H5 */
.container *,
/* #endif */
view,
textarea,
button {
display: flex;
box-sizing: border-box;
}
page {
height: 100%;
width: 100%;
}
/* #endif */
.nav {
display: flex;
flex-direction: column;
}
.menu-image {
width: 55rpx !important;
height: 55rpx !important;
}
.stop-responding {
font-size: 14px;
border-radius: 3px;
margin-bottom: 15px;
background-color: #f0b00a;
color: #FFF;
width: 90px;
height: 30px;
line-height: 30px;
margin: 0 auto;
justify-content: center;
margin-bottom: 15px;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.stop-responding:hover {
box-shadow: 0 0 10px #aaa;
}
.container {
height: 100%;
background: linear-gradient(180deg, #f5f4f6, #e6ebf7);
flex-direction: column;
align-items: center;
justify-content: center;
// border: 1px solid blue;
}
.foot-box {
width: 750rpx;
display: flex;
flex-direction: column;
padding: 10px 0px;
background-color: #FFF;
}
.foot-box-content {
justify-content: space-around;
align-items: center;
}
.textarea-box {
padding: 8px 10px;
background-color: #f9f9f9;
border-radius: 5px;
}
.textarea-box .textarea {
max-height: 120px;
font-size: 14px;
/* #ifndef APP-NVUE */
overflow: auto;
/* #endif */
width: 450rpx;
font-size: 14px;
}
/* #ifdef H5 */
/*隐藏滚动条*/
.textarea-box .textarea::-webkit-scrollbar {
width: 0;
}
/* #endif */
.input-placeholder {
color: #bbb;
line-height: 18px;
}
.trash,
.send {
width: 50px;
height: 30px;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.trash {
width: 30rpx;
margin-left: 10rpx;
}
.menu {
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.menu-item {
width: 30rpx;
margin: 0 10rpx;
}
.send {
color: #FFF;
border-radius: 4px;
display: flex;
margin: 0;
padding: 0;
font-size: 14px;
margin-right: 20rpx;
}
/* #ifndef APP-NVUE */
.send::after {
display: none;
}
/* #endif */
.msg-list {
height: 0; //不可省略,先设置为0 再由flex: 1;撑开才是一个滚动容器
flex: 1;
width: 750rpx;
padding-top: 200rpx;
// border: 1px solid red;
}
.noData {
margin-top: 204rpx;
text-align: center;
width: 750rpx;
color: #aaa;
font-size: 12px;
justify-content: center;
}
.open-ad-btn-box {
justify-content: center;
margin: 10px 0;
}
.tip-ai-ing {
align-items: center;
flex-direction: column;
font-size: 14px;
color: #919396;
padding: 15px 0;
}
.uni-link {
margin-left: 5px;
line-height: 20px;
}
/* #ifdef H5 */
@media screen and (min-width:650px) {
.foot-box {
border-top: solid 1px #dde0e2;
}
.container,
.container * {
max-width: 950px;
}
.container {
box-shadow: 0 0 5px #e0e1e7;
height: calc(100vh - 44px);
margin: 22px auto;
border-radius: 10px;
overflow: hidden;
background-color: #FAFAFA;
}
page {
background-color: #efefef;
}
.container .header {
height: 44px;
line-height: 44px;
border-bottom: 1px solid #F0F0F0;
width: 100vw;
justify-content: center;
font-weight: 500;
}
.content {
background-color: #f9f9f9;
position: relative;
max-width: 90%;
}
// .copy {
// color: #888888;
// position: absolute;
// right: 8px;
// top: 8px;
// font-size: 12px;
// cursor:pointer;
// }
// .copy :hover{
// color: #4b9e5f;
// }
.foot-box,
.foot-box-content,
.msg-list,
.msg-item,
// .create_time,
.noData,
.textarea-box,
.textarea,
textarea-box {
width: 100% !important;
}
.textarea-box,
.textarea,
textarea,
textarea-box {
height: 120px;
}
.foot-box,
.textarea-box {
background-color: #FFF;
}
.foot-box-content {
flex-direction: column;
justify-content: center;
align-items: flex-end;
padding-bottom: 0;
}
.pc-menu {
padding: 0 10px;
}
.pc-menu-item {
height: 20px;
justify-content: center;
align-items: center;
align-content: center;
display: flex;
margin-right: 10px;
cursor: pointer;
}
.pc-trash {
opacity: 0.8;
}
.pc-trash image {
height: 15px;
}
.textarea-box,
.textarea-box * {
// border: 1px solid #000;
}
.send-btn-box .send-btn-tip {
color: #919396;
margin-right: 8px;
font-size: 12px;
line-height: 28px;
}
}
/* #endif */
.retries-box {
justify-content: center;
align-items: center;
font-size: 12px;
color: #d2071b;
}
.retries-icon {
margin-top: 1px;
margin-left: 5px;
}
/* #ifndef APP-NVUE */
.box,
/* #ifdef H5 */
.box *,
/* #endif */
radio-group,
label {
display: flex;
box-sizing: border-box;
}
/* #endif */
.box,
.title,
.btn-box {
width: 250px;
}
.box {
background-color: #fff;
display: flex;
flex-direction: column;
align-items: flex-start;
padding-bottom: 0;
border-radius: 5px;
}
.title {
font-size: 16px;
padding: 10px 0;
padding-bottom: 5px;
font-weight: 400;
flex: 1;
text-align: center;
/* #ifndef APP-NVUE */
display: inline-block;
/* #endif */
}
.radio-group {
flex-direction: column;
padding: 0 15px;
}
.radio {
transform: scale(0.7);
}
.item {
flex-direction: row;
margin-bottom: 5px;
position: relative;
}
.item-title {
font-size: 14px;
color: #555;
}
.btn-box {
/* #ifdef APP-NVUE */
border-top: solid 1px #ccc;
/* #endif */
height: 48px;
position: relative;
}
/* #ifndef APP-NVUE */
.btn-box:after {
content: ' ';
position: absolute;
left: 0;
top: 0;
right: 0;
height: 1px;
border-top: 1px solid #d5d5d6;
color: #d5d5d6;
transform-origin: 0 0;
transform: scaleY(0.5);
}
/* #endif */
.btn {
justify-content: center;
align-items: center;
width: 150px;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.confirm {
color: #007aff;
position: relative;
/* #ifdef APP-NVUE */
border-left: solid 1px #ccc;
/* #endif */
}
/* #ifndef APP-NVUE */
.confirm::before {
content: '';
position: absolute;
left: 0;
top: 0;
right: 0;
background-color: #d5d5d6;
height: 48px;
width: 1px;
/* border-top: 1px solid #d5d5d6; */
/* color: #d5d5d6; */
/* transform-origin: 0 0; */
transform: scaleX(0.5);
}
/* #endif */
</style>