赞
踩
我使用的是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>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。