当前位置:首页 » 《关注互联网》 » 正文

《通义千问AI落地—中》:前端实现

6 人参与  2024年09月15日 08:41  分类 : 《关注互联网》  评论

点击全文阅读


一、前言

本文源自微博客且已获授权,请尊重版权.

     书接上文,上文中,我们介绍了通义千问AI落地的后端接口。那么,接下来我们将继续介绍前端如何调用接口以及最后的效果;首先看效果:

result.gif

     上述就是落地到本微博客以后的页面效果,由于是基于落在现有项目之上,因此什么登录注册等基本功能都省去了,言归正传,下面我们将正式介绍通义千问AI落地的前端实现。

二、前端实现

2.1、前端依赖

     前端所需依赖基本如下(本项目前端是基于Nuxtjs的,这样又益与SSE,所以有nuxt相关依赖):

    "dependencies": {        "@nuxtjs/axios": "^5.13.6",        "dayjs": "^1.11.12",        "element-ui": "^2.15.1",        "highlight.js": "^11.9.0", //代码高亮组件        "mavon-editor": "^2.10.4",  //富文本展示        "nuxt": "^2.0.0",        "@stomp/stompjs": "^6.0.0",  //         "ws": "^7.0.0"  //websocket    }

2.2、页面布局

      如上动图所示,前端项目主要是左右分布。其中,左侧负责管理各个session会话,包括激活会话、展示会话、删除会话等;
      右侧则主要负责消息处理,包括消息收发、处理GPT生成的消息等。由于使用组件化开发,因此各个组件比较多,接下来的内容将采用 总-分 结构介绍。

2.2.1、主聊天页面

     主聊天页面聚合了左侧的session管理和右侧的消息管理,内容如下:

<template>    <!-- 最外层页面于窗口同宽,使聊天面板居中 -->    <div class="home-view">        <!-- 整个聊天面板 -->        <div class="chat-panel">            <!-- 左侧的会话列表 -->            <div class="session-panel hidden-sm-and-down">                <div class="title">ChatGPT助手</div>                <div class="description">构建你的AI助手</div>                <div class="session-list">                    <SessionItem                        v-for="(session, index) in sessionList"                        :key="session.id+index"                        :active="session.id === activeSession.id"                        :session="sessionList[index]"                        class="session"                        @click.native="sessionSwitch(session,index)"                        @delete="deleteSession"                    >                    </SessionItem>                </div>                <div class="button-wrapper">                    <div class="new-session">                        <el-button @click="createSession">                            <el-icon :size="15" class="el-icon-circle-plus-outline"></el-icon>                            新的聊天                        </el-button>                    </div>                </div>            </div>            <!-- 右侧的消息记录 -->            <div class="message-panel">                <!-- 会话名称 -->                <div class="header">                    <div class="front">                        <div v-if="!isEdit" class="title">                            <el-input style="font-size: 20px"                                      v-model="activeSession.topic"                                      @keyup.enter.native="editTopic()"                            ></el-input>                        </div>                        <div v-else class="title" style="margin-top: 6px;" @dblclick="editTopic()">                            {{ activeSession.topic }}                        </div>                        <div class="description">与ChatGPT的 {{ activeSession?.messageSize ?? 0 }} 条对话</div>                    </div>                    <!-- 尾部的编辑按钮 -->                    <div class="rear">                        <i v-if="isEdit" @click="editTopic" class="el-icon-edit rear-icon"></i>                        <i v-else @click="editTopic" class="el-icon-check rear-icon"></i>                    </div>                </div>                <el-divider></el-divider>                <div class="message-list" id="messageListId">                    <!-- 过渡效果 -->                    <transition-group name="list">                        <message-row                            v-for="(message, index) in activeSession.messages"                            :key="message.id+`${index}`"                            :message="message"                        ></message-row>                    </transition-group>                </div>                <div class="toBottom" v-if="!this.isScrolledToBottom">                    <el-tooltip class="item" effect="light" content="直达最新" placement="top-center">                        <el-button class="el-icon-bottom bottom-icon" @click="toBottom"></el-button>                    </el-tooltip>                </div>                <!-- 监听发送事件 -->                <MessageInput @send="sendMessage" :isSend="isSend"></MessageInput>            </div>        </div>    </div></template><script>import MessageInput from '@/components/gpt/MessageInput'import MessageRow from '@/components/gpt/MessageRow'import SessionItem from "@/components/gpt/SessionItem";import {Client} from "@stomp/stompjs";import dayjs from "dayjs";import {scrollToBottom} from '@/utils/CommonUtil'export default {    name: 'gpt',    layout: 'gpt',    middleware: 'auth', //权限中间件,要求用户登录以后才能使用    components: {        MessageInput, MessageRow, SessionItem    },    created() {        this.loadChart();    },    mounted() {        this.handShake()        this.$nextTick(() => {            this.messageListEl = document.getElementById('messageListId');            if (this.messageListEl) {                this.messageListEl.addEventListener('scroll', this.onScroll);            }        });    },    beforeUnmount() {        this.closeClient();    },    beforeDestroy() {        if (this.messageListEl) {            this.messageListEl.removeEventListener('scroll', this.onScroll);        }    },    watch: {        activeSession(newVal) {            if (newVal) {                //确保dom加载完毕                this.$nextTick(() => {                    this.toBottom();                });            }        },    },    data() {        return {            sessionList: [],            activeSession: {                topic: '',                messageSize:0            },            isEdit: true,            isSend: false,            client: null,            gptRes: {                content:''            },            userInfo: null,            activeTopic:null,            //消息计数            msgCount:false,            isScrolledToBottom: true,            messageListEl: null,            msgQueue:[], //收到的消息队列;先将后端返回的消息收集起来放在一个队列里面,一点点追加到页面显示,这样可以使消息显示更加平滑            interval:null,            lineCount:5        }    },    methods: {        async loadChart() {            //查询历史对话            const queryArr = {                query: {                    userId: this.userInfo.uid                },                pageNum: 1,                pageSize: 7            };            let res = await this.$querySession(queryArr);            if (res.code === 20000) {                if (res.data.length > 0) {                    this.activeSession = res.data[0]                    res.data.forEach(item => this.sessionList.push(item))                    this.activeTopic = this.activeSession.topic                    return                }            }            let session = {                topic: "新建的聊天",                userId: this.userInfo.uid,            }            let resp = await this.$createSession(session)            if (resp.code === 20000) {                session.id = resp.data.id            }            session.updateDate = this.now()            session.createDate = this.now()            session.messages = []            this.sessionList.push(session)            this.activeSession = this.sessionList[0]            this.activeTopic = this.activeSession.topic        },        editTopic() {            this.isEdit = !this.isEdit            if (this.isEdit) {                if (this.activeTopic===this.activeSession.topic)                    return                this.$updateSession(this.activeSession).then(() => {                    this.activeSession.updateDate = this.now()                    this.activeTopic = this.activeSession.topic                })            }        },        deleteSession(session) {            let index = this.sessionList.findIndex((value) => {                return value.id === session.id            })            this.sessionList.splice(index, 1)            if (this.sessionList.length > 0) {                this.activeSession = this.sessionList[0]                return            }            this.createSession()        },        sessionSwitch(session,index) {            if (!session) return            if (session.messages && session.messages.length > 0) {                this.activeSession = null                this.activeSession = session                this.toBottom()                return;            }            this.$getSessionById(session.id).then(resp => {                if (resp.code === 20000) {                    this.activeSession = null                    this.activeSession = resp.data                    this.toBottom()                    this.sessionList[index] = resp.data                    this.sessionList[index].messageSize = session.messageSize                }            })        },        createSession() {            let time = this.now()            let chat = {                id: time.replaceAll(" ", ""),                createDate: time,                updateDate: time,                messageSize:0,                topic: "新建的聊天",                messages: []            }            this.activeSession = chat            //从聊天列表头部插入新建的元素            this.sessionList.unshift(chat)            this.createChatMessage(chat)        },        async createChatMessage(chat) {            let resp = await this.$createSession(chat)            if (resp.code === 20000) {                this.activeSession.id = resp.data.id            }        },        //socket握手        handShake() {            this.client = new Client({                //连接地址要加上项目跟地址                brokerURL: `${process.env.socketURI}`,                onConnect: () => {                    this.isSend = true                    // 连接成功后订阅ChatGPT回复地址                    this.client.subscribe('/user/queue/gpt', (message) => {                        let msg = message.body                        this.handleGPTMsg(msg)                    })                }            })            // 发起连接            this.client.activate()        },        /**         * 处理GPT返回的消息         * @param msg         */        handleGPTMsg(msg){            if (msg && msg !== '!$$---END---$$!'){                this.msgQueue.push(msg)//设置定时器,每40毫秒取出一个队列元素,追加到页面显示,不要一下子把后端返回的内容全部追加到页面;使消息平滑显示                if (!this.interval){                    this.interval = setInterval(()=>{                        this.appendQueueToContent()                    },40)                }                if (this.msgCount){                    this.activeSession.messageSize+=1                    this.msgCount = false                }                return;            }            if (msg === '!$$---END---$$!') {                clearTimeout(this.interval)                this.interval = null//清理掉定时器以后,需要处理队列里面剩余的消息内容                this.handleLastMsgQueue()            }        },        /**         * 处理队列里面剩余的消息         */        handleLastMsgQueue(){          while (this.msgQueue.length>0){              this.appendQueueToContent()          }          this.isSend = true        },        /**         * 将消息队列里面的消息取出一个字符追加到显示content         */        appendQueueToContent() {            if (this.msgQueue.length <= 0) {                return            }            // 如果当前字符串还有字符未处理            const currentItem = this.msgQueue[0];            if (currentItem) {                // 取出当前字符串的第一个字符                const char = currentItem[0];                //不能频繁调用 到底部 函数                if (this.lineCount % 5 === 0) {                    this.toBottom()                }                this.lineCount++                this.gptRes.content += char;                // 移除已处理的字符                this.msgQueue[0] = currentItem.slice(1);                // 如果当前字符串为空,则从队列中移除                if (this.msgQueue[0].length === 0) {                    this.msgQueue.shift();                }            }        },        sendMessage(msg) {            this.buildMsg('user', msg)            let chatMessage = {                content: msg,                role: 'user',                sessionId: this.activeSession.id            }            try {                this.client.publish({                    destination: '/ws/chat/send',                    body: JSON.stringify(chatMessage)                })            } catch (e) {                console.log("socket connection error:{}", e)                this.handShake()                return            }            this.isSend = false            this.gptRes = {                role: 'assistant', content: '', createDate: this.now()            }            this.activeSession.messages.push(this.gptRes)            this.toBottom()            this.msgCount = true            this.activeSession.messageSize+=1        },        toBottom(){            scrollToBottom('messageListId')        },        buildMsg(_role, msg) {            let message = {role: _role, content: msg, createDate: this.now()}            this.activeSession.messages.push(message)        },        closeClient() {            try {                this.client.deactivate()                this.client = null            } catch (e) {                console.log(e)            }        },        now() {            return dayjs().format('YYYY-MM-DD HH:mm:ss');        },        onScroll(event) {            this.isScrolledToBottom = event.target.scrollHeight - event.target.scrollTop <= (event.target.clientHeight + 305);        },    },    async asyncData({store, redirect}) {        const userId = store.state.userInfo && store.state.userInfo.uid        if (typeof userId == 'undefined' || userId == null || Object.is(userId, 'null')) {            return redirect("/");        }        return {            userInfo: store.state.userInfo        }    },}</script><style lang="scss" scoped>.home-view {    display: flex;    justify-content:center;    margin-top: -80px;    .chat-panel {        display: flex;        border-radius: 20px;        background-color: white;        box-shadow: 0 0 20px 20px rgba(black, 0.05);        margin-top: 70px;        margin-right: 75px;        .session-panel {            width: 300px;            border-top-left-radius: 20px;            border-bottom-left-radius: 20px;            padding: 5px 10px 20px 10px;            position: relative;            border-right: 1px solid rgba(black, 0.07);            background-color: rgb(231, 248, 255);            /* 标题 */            .title {                margin-top: 20px;                font-size: 20px;            }            /* 描述*/            .description {                color: rgba(black, 0.7);                font-size: 14px;                margin-top: 10px;            }            .session-list {                .session {                    /* 每个会话之间留一些间距 */                    margin-top: 20px;                }            }            .button-wrapper {                /* session-panel是相对布局,这边的button-wrapper是相对它绝对布局 */                position: absolute;                bottom: 20px;                left: 0;                display: flex;                /* 让内部的按钮显示在右侧 */                justify-content: flex-end;                /* 宽度和session-panel一样宽*/                width: 100%;                /* 按钮于右侧边界留一些距离 */                .new-session {                    margin-right: 20px;                }            }        }        /* 右侧消息记录面板*/        .message-panel {            width: 750px;            position: relative;            .header {                text-align: left;                padding: 5px 20px 0 20px;                display: flex;                /* 会话名称和编辑按钮在水平方向上分布左右两边 */                justify-content: space-between;                /* 前部的标题和消息条数 */                .front {                    .title {                        color: rgba(black, 0.7);                        font-size: 20px;                        ::v-deep {                            .el-input__inner {                                padding: 0 !important;                            }                        }                    }                    .description {                        margin-top: 10px;                        color: rgba(black, 0.5);                    }                }                /* 尾部的编辑和取消编辑按钮 */                .rear {                    display: flex;                    align-items: center;                    .rear-icon {                        font-size: 20px;                        font-weight: bold;                    }                }            }            .message-list {                height: 560px;                padding: 15px;                // 消息条数太多时,溢出部分滚动                overflow-y: scroll;                // 当切换聊天会话时,消息记录也随之切换的过渡效果                .list-enter-active,                .list-leave-active {                    transition: all 0.5s ease;                }                .list-enter-from,                .list-leave-to {                    opacity: 0;                    transform: translateX(30px);                }            }            ::v-deep{                .el-divider--horizontal {                    margin: 14px 0;                }            }        }    }}::v-deep {    .mcb-main {        padding-top: 10px;    }    .mcb-footer{        display: none;    }}.message-input {    padding: 20px;    border-top: 1px solid rgba(black, 0.07);    border-left: 1px solid rgba(black, 0.07);    border-right: 1px solid rgba(black, 0.07);    border-top-right-radius: 5px;    border-top-left-radius: 5px;}.button-wrapper {    display: flex;    justify-content: flex-end;    margin-top: 20px;}.toBottom{    display: inline;    background-color: transparent;    position: absolute;    z-index: 999;    text-align: center;    width: 100%;    bottom: 175px;}.bottom-icon{    align-items: center;    background: #fff;    border: 1px solid rgba(0,0,0,.08);    border-radius: 50%;    bottom: 0;    box-shadow: 0 2px 8px 0 rgb(0 0 0 / 10%);    box-sizing: border-box;    cursor: pointer;    display: flex;    font-size: 20px;    height: 40px;    justify-content: center;    position: absolute;    right: 50%;    width: 40px;    z-index: 999;}.bottom-icon:hover {    color: #5dbdf5;    cursor: pointer;    border: 1px solid #5dbdf5;}</style>

     我们来着重介绍一下以下三个函数:

 /**         * 处理GPT返回的消息         * @param msg         */        handleGPTMsg(msg){            if (msg && msg !== '!$$---END---$$!'){                this.msgQueue.push(msg)//设置定时器,每40毫秒取出一个队列元素,追加到页面显示,不要一下子把后端返回的内容全部追加到页面;使消息平滑显示                if (!this.interval){                    this.interval = setInterval(()=>{                        this.appendQueueToContent()                    },40)                }                if (this.msgCount){                    this.activeSession.messageSize+=1                    this.msgCount = false                }                return;            }            if (msg === '!$$---END---$$!') {                clearTimeout(this.interval)                this.interval = null//清理掉定时器以后,需要处理队列里面剩余的消息内容                this.handleLastMsgQueue()            }        },        /**         * 处理队列里面剩余的消息         */        handleLastMsgQueue(){          while (this.msgQueue.length>0){              this.appendQueueToContent()          }          this.isSend = true        },        /**         * 将消息队列里面的消息取出一个字符追加到显示content         */        appendQueueToContent() {            if (this.msgQueue.length <= 0) {                return            }            // 如果当前字符串还有字符未处理            const currentItem = this.msgQueue[0];            if (currentItem) {                // 取出当前字符串的第一个字符                const char = currentItem[0];                //不能频繁调用 到底部 函数                if (this.lineCount % 5 === 0) {                    this.toBottom()                }                this.lineCount++                this.gptRes.content += char;                // 移除已处理的字符                this.msgQueue[0] = currentItem.slice(1);                // 如果当前字符串为空,则从队列中移除                if (this.msgQueue[0].length === 0) {                    this.msgQueue.shift();                }            }        }
handleGPTMsg 这个函数就是处理后端websocket传递过来的消息,其中,最主要的就是这里。在这里,我们设置了一个定时器,每40ms调用一次 appendQueueToContent函数,这个函数从消息队列的队头取出一个字符,把这个字符追加到页面组件上面显示,这样处理使得消息的显示更加平滑,而不是后端生成多少就一次性展示多少。
if (!this.interval){    this.interval = setInterval(()=>{        this.appendQueueToContent()    },40)}
appendQueueToContent 这个函数就是负责从queue里面获取内容,然后追加到gptRes这个变量里面;并且将已经追加的内容从队列里面移除掉。handleLastMsgQueue 由于前后端处理消息的速度是不一样的,当后端发送生成结束标记(即 !$$---END---$$! )后,前端就需要以此为根据,删除定时器,否则我们没办法知道queue消息队列什么时候为空、什么时候该清楚定时器。那么,此时清除定时器,我们就需要用一个while函数来处理queue里面剩下的内容,handleLastMsgQueue 函数就是干这个的。

2.2.2、session管理组件

     这个组件没有什么隐晦难懂的知识,直接贴代码:

<template>    <div :class="['session-item', active ? 'active' : '']">        <div class="name">{{ session.topic }}</div>        <div class="count-time">            <div class="count">{{ session?.messageSize ?? 0 }}条对话</div>            <div class="time">{{ session.updateDate }}</div>        </div>        <!-- 当鼠标放在会话上时会弹出遮罩 -->        <div class="mask"></div>        <div class="btn-wrapper" @click.stop="$emit('click')">            <el-popconfirm                confirm-button-text='好的'                cancel-button-text='不用了'                icon="el-icon-circle-close"                icon-color="red"                @click.prevent="deleteSession(session)"                title="是否确认永久删除该聊天会话?"                @confirm="deleteSession(session)"            >                <el-icon slot="reference" :size="15" class="el-icon-circle-close"></el-icon>            </el-popconfirm>        </div>    </div></template><script>export default {    props: {        session: {            type: Object,            required: true        },        active: {            type: Boolean,            default: false        }    },    data() {        return {            ChatSession: {}        }    },    methods: {        deleteSession(session) {            //请求后台删除接口            this.$deleteSession(session.id)            //通知父组件删除session            this.$emit('delete', session)        }    }}</script><style lang="scss" scoped>.session-item {    padding: 12px;    background-color: white;    border-radius: 10px;    width: 91%;    /* 当鼠标放在会话上时改变鼠标的样式,暗示用户可以点击。目前还没做拖动的效果,以后会做。 */    cursor: grab;    position: relative;    overflow: hidden;    .name {        font-size: 14px;        font-weight: 700;        width: 200px;        color: rgba(black, 0.8);        text-align: left;    }    .count-time {        margin-top: 10px;        font-size: 10px;        color: rgba(black, 0.5);        /* 让消息数量和最近更新时间显示水平显示 */        display: flex;        /* 让消息数量和最近更新时间分布在水平方向的两端 */        justify-content: space-between;    }    /* 当处于激活状态时增加蓝色描边 */    &.active {        transition: all 0.12s linear;        border: 2px solid #1d93ab;    }    &:hover {        /* 遮罩入场,从最左侧滑进去,渐渐变得不透明 */        .mask {            opacity: 1;            left: 0;        }        .btn-wrapper {            &:hover {                cursor: pointer;            }            /* 按钮入场,从最右侧滑进去,渐渐变得不透明 */            opacity: 1;            right: 20px;        }    }    .mask {        transition: all 0.2s ease-out;        position: absolute;        background-color: rgba(black, 0.05);        width: 100%;        height: 100%;        top: 0;        left: -100%;        opacity: 0;    }    /* 删除按钮样式的逻辑和mask类似 */    .btn-wrapper {        color: rgba(black, 0.5);        transition: all 0.2s ease-out;        position: absolute;        top: 10px;        right: -20px;        z-index: 10;        opacity: 0;        .edit {            margin-right: 5px;        }    ;        .el-icon-circle-close {            display: inline-block;            width: 25px;            height: 25px;            color: red;        }    }}</style>

     上述代码只有一个地方稍稍注意,那就是 <div class="btn-wrapper" @click.stop="$emit('click')"> 这里, 在这个div中,我们必须阻止 click 点击事件,防止我们点击div里面的删除元素时,把点击事件传递给div,激活当前session。效果如下:

click.gif

2.2.3、聊天组件

各个聊天组件如下所示,其中:

2.2.3.1、MessageInput组件
<template>  <div class="message-input">    <div class="input-wrapper">      <el-input          v-model="message"          :autosize="false"          :rows="3"          class="input"          resize="none"          type="textarea"          @keydown.native="sendMessage"          autofocus="autofocus"      >      </el-input>      <div class="button-wrapper">        <el-button icon="el-icon-position" type="primary" @click="send" :disabled="!isSend">          发送        </el-button>      </div>    </div>  </div></template><script>export default {  props: {    isSend: {      type: Boolean,      default: false    }  },  data() {    return {      message: ""    };  },  methods: {      sendMessage(e) {          //shift + enter 换行          if (!e.shiftKey && e.keyCode === 13) {              if ((this.message + "").trim() === '' || this.message.length <= 0) {                  return;              }              // 阻止默认行为,避免换行              e.preventDefault();             this.send();          }      },      send(){          if (this.isSend) {              this.$emit('send', this.message);              this.message = '';          }      }  }}</script><style lang="scss" scoped>.message-input {  padding: 20px;  border-top: 1px solid rgba(black, 0.07);  border-left: 1px solid rgba(black, 0.07);  border-right: 1px solid rgba(black, 0.07);  border-top-right-radius: 5px;  border-top-left-radius: 5px;}.button-wrapper {  display: flex;  justify-content: flex-end;  margin-top: 20px;}</style>
2.2.3.2、MessageRow组件
<!-- 整个div是用来调整内部消息的位置,每条消息占的空间都是一整行,然后根据right还是left来调整内部的消息是靠右边还是靠左边 --><template>    <div :class="['message-row', message.role === 'user' ? 'right' : 'left']">        <!-- 消息展示,分为上下,上面是头像,下面是消息 -->        <div class="row">            <!-- 头像, -->            <div class="avatar-wrapper">                <el-avatar v-if="message.role === 'user'" :src="$store.state.userInfo?$store.state.userInfo.imageUrl:require('@/assets/gpt.png')" class="avatar"                           shape="square"/>                <el-avatar v-else :src="require('@/assets/logo.png')" class="avatar" shape="square"/>            </div>            <!-- 发送的消息或者回复的消息 -->            <div class="message">                <!-- 预览模式,用来展示markdown格式的消息 -->                <client-only>                    <mavon-editor v-if="message.content" :class="message.role"                                  :style="{                                    backgroundColor: message.role === 'user' ? 'rgb(231, 248, 255)' : '#f7e8f1e3',                                    zIndex: 1,                                    minWidth: '5px',                                    fontSize:'15px',                                }"                                  default-open="preview" :subfield='false' :toolbarsFlag="false" :ishljs="true" ref="md"                                  v-model="message.content" :editable="false"/>                    <TextLoading v-else></TextLoading>                    <!-- 如果消息的内容为空则显示加载动画 -->                </client-only>            </div>        </div>    </div></template><script>import '@/assets/css/md/github-markdown.css'import TextLoading from './TextLoading'export default {    components: {        TextLoading    },    props: {        message: {            type: Object,            default: null        }    },    data() {        return {            Editor: "",        }    },    created(){    }}</script><style lang="scss" scoped>.message-row {    display: flex;    &.right {        // 消息显示在右侧        justify-content: flex-end;        .row {            // 头像也要靠右侧            .avatar-wrapper {                display: flex;                justify-content: flex-end;            }            // 用户回复的消息和ChatGPT回复的消息背景颜色做区分            .message {                background-color: rgb(231, 248, 255);            }        }    }    // 默认靠左边显示    .row {        .avatar-wrapper {            .avatar {                box-shadow: 20px 20px 20px 3px rgba(0, 0, 0, 0.03);                margin-bottom: 10px;                max-width: 40px;                max-height: 40px;                background: #d4d6dcdb !important;            }        }        .message {            font-size: 15px;            padding: 1.5px;            // 限制消息展示的最大宽度            max-width: 500px;            // 圆润一点            border-radius: 7px;            // 给消息框加一些描边,看起来更加实一些,要不然太扁了轻飘飘的。            border: 1px solid rgba(black, 0.1);            // 增加一些阴影看起来更加立体            box-shadow: 20px 20px 20px 1px rgba(0, 0, 0, 0.01);            margin-bottom: 5px;        }    }}.left {    text-align: left;    .message {        background-color: rgba(247, 232, 241, 0.89);    }}// 调整markdown组件的一些样式,deep可以修改组件内的样式,正常情况是scoped只能修改本组件的样式。::v-deep {    .v-note-wrapper .v-note-panel .v-note-show .v-show-content, .v-note-wrapper .v-note-panel .v-note-show .v-show-content-html {        padding: 9px 10px 0 15px;    }    .markdown-body {        min-height: 0;        flex-grow: 1;        .v-show-content {            background-color: transparent !important;        }    }}</style>
2.2.3.3、TextLoading组件
<template>  <div class="loading">    <!--  三个 div 三个黑点 -->    <div></div>    <div></div>    <div></div>  </div></template><style lang="scss" scoped>.loading {  // 三个黑点水平展示  display: flex;  // 三个黑点均匀分布在54px中  justify-content: space-around;  color: #000;  width: 54px;  padding: 15px;  div {    background-color: currentColor;    border: 0 solid currentColor;    width: 5px;    height: 5px;    // 变成黑色圆点    border-radius: 100%;    // 播放我们下面定义的动画,每次动画持续0.7s且循环播放。    animation: ball-beat 0.7s -0.15s infinite linear;  }  div:nth-child(2n-1) {    // 慢0.5秒    animation-delay: -0.5s;  }}// 动画定义@keyframes ball-beat {  // 关键帧定义,在50%的时候是颜色变透明,且缩小。  50% {    opacity: 0.2;    transform: scale(0.75);  }  // 在100%时是回到正常状态,浏览器会自动在这两个关键帧间平滑过渡。  100% {    opacity: 1;    transform: scale(1);  }}</style>
2.2.3.4、scrollToBottom 函数
export function scrollToBottom(elementId) {    const container = document.getElementById(elementId);    if (!container) {        return    }    // 头部    const start = container.scrollTop;    //底部-头部    const change = container.scrollHeight - start;    const duration = 1000; // 动画持续时间,单位毫秒    let startTime = null;    const animateScroll = (timestamp) => {        if (!startTime) startTime = timestamp;        const progress = timestamp - startTime;        const run = easeInOutQuad(progress, start, change, duration);        container.scrollTop = Math.floor(run);        if (progress < duration) {            requestAnimationFrame(animateScroll);        }    };    // 二次贝塞尔曲线缓动函数    function easeInOutQuad(t, b, c, d) {        t /= d / 2;        if (t < 1) return c / 2 * t * t + b;        t--;        return -c / 2 * (t * (t - 2) - 1) + b;    }    requestAnimationFrame(animateScroll);}

三、总结

     通义千问AI落地前端实现大致如上,在下篇中,我们将继续介绍Websocket是如何实现前后端的消息接受与发送的,敬请期待。。。。


点击全文阅读


本文链接:http://m.zhangshiyu.com/post/160122.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1