人工智能之web前端开发(deepSeek与文心一言结合版)

一.项目功能:

  1. 智能问答(实时聊天+流畅打字机效果+自动滚动)
  2. 停止生成(取消接口调用)、重新生成
  3. 复制功能、问答分页

二.效果展示:

三.技术分析:

  1. fetchEventSource:传统axios请求是等接口将所有数据一次性响应回来后再渲染到页面上,当数据量较大时,响应速度较慢,且无法做到实时输出。而fetchEventSource允许客户端接收来自服务器的实时更新,前端可以实时的将流式数据展示到页面上,类似于打字机的效果。

    xml 复制代码
    <pre>

    fetchEventSource(url, { method: "GET", headers: { "Content-type": "application/json", Accept: "text/event-stream" }, openWhenHidden: true, onopen: (e) => { //接口请求成功,但此时数据还未响应回来 }, onmessage: (event) => { //响应数据持续数据 }, onclose: () => { //请求关闭 }, onerror: () => { //请求错误 } })

    css 复制代码
    <p></p>
    </li>
    <li>
    <p>MarkdownIt :SSE响应的数据格式是markdown,无法直接展示,需要使用MarkdownIt第三方库转换成html,然后通过v-model展示到页面上。</p>
    
    <pre>

    // 1、新建实例md: const md = new MarkdownIt() // 2.将markdown转化为html const htmlStr= md.render(markdownStr)

    css 复制代码
    <p></p>
    </li>
    <li>
    <p>Clipboard+html-to-text:复制时,需要使用html-to-text第三方库将html转化为text,然后借助Clipboard复制到粘贴板上。</p>
    
    <pre>

    //1.在html中设置"copy"类名,并绑定data-clipboard-text <el-icon class="copy" @click="copyFn(copyHtmlStr)" :data-clipboard-text="copyText"> </el-icon>

    //2.先将html转化成text,然后复制到粘贴板 const copyFn = (copyHtmlStr) => { copyText.value=htmlToText(copyHtmlStr) const clipboard = new Clipboard(".copy") // 成功 clipboard.on("success", function (e) { ElMessage.success("复制成功") e.clearSelection() // 释放内存 clipboard.destroy() }) // 失败 clipboard.on("error", function (e) { ElMessage.error("复制失败") clipboard.destroy() }) }

    xml 复制代码
    <p></p>
    </li>
    <li>
    <p>scrollEvent:由于数据流式输出,页面内容持续增加,可能会溢出屏幕,因此需要在fetchEventSource接收消息onmessage的过程中,通过设置scrollTop =scrollHeight让页面实现自动滚动。</p>
    
    <pre>

    fetchEventSource(url, { ..., onmessage: (event) => { chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight }, ... })

    css 复制代码
    <p></p>
    </li>

四:疑难点及解决方案:

**1. 问题描述:**当页面请求fetchEventSource已发出时,切换url到其他网站再切换回来到这个页面时,fetchEventSource会重复请求,导致这两次请求的内容重复。

**解决方案:**设置openWhenHidden为true,表示当页面退至后台时仍保持连接,默认值为false

2. **问题描述:**前端调用AbortController的abort()方法取消请求时,只有第一次取消生效,当重新请求时,再次点击停止按钮不生效。

**解决方案:**每请求一次创建一个新的AbortController()实例,因为AbortController实例的abort()方法被设计为只能调用一次来取消请求,一旦调用了abort(),与AbortController相关的AbortSigal的aborted属性就会被设置成true,表示请求已取消,当再次调用abort()不会有任何效果。

**3. 问题描述:**当在fetchEventSource的onmessage中设置scrollTop =scrollHeight时,在生成问题的过程中无法向上滚动,但业务想要边生成边滚动查看。

**解决方案:**监听鼠标滚轮事件,在设置scrollTop =scrollHeight时添加判断,如果鼠标滚轮滑动且未到页面底部,则不自动滚动。

ini 复制代码
const isRolling = ref(false) //鼠标滚轮是否滚动
const isBottom = ref(false) //滚动参数

// 处理鼠标滚轮事件
const moveWheel1 = ref(true)
const moveWheel2 = ref(false)
const wheelClock = ref()
const stopWheel=()=> {
      if (moveWheel2.value == true) {
        moveWheel2.value = false
        moveWheel1.value = true
      }
    }
const moveWheel=()=> {
      if (moveWheel1.value == true) {
        isRolling.value = true
        moveWheel1.value = false
        moveWheel2.value = true
        //这里写开始滚动时调用的方法
        wheelClock.value = setTimeout(stopWheel, 200)
      } else {
        clearTimeout(wheelClock.value)
        wheelClock.value = setTimeout(stopWheel, 150)
      }
    }
const sendFn=()=>{
     fetchEventSource(url, {
        ...,
        onmessage: (event) => { 
            if (isRolling.value === false) {
              chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight
              isRolling.value = false
            }
            if (isBottom.value) {
              isRolling.value = false
              chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight
            }
        },
        ...
      })
}

**4. 问题描述:**SSE响应回来的数据中表格样式未生效。

**解决方案:**MarkdownIt第三方库将markdown转换成html时,部分样式会丢失,需使用github-markdown-css添加样式。

arduino 复制代码
npm i github-markdown-css
import "github-markdown-css"

五.完整代码示例:

index.vue:

ini 复制代码
<template>
  <div class="report-page">
    <div class="chat-container" ref="chatContainerRef" @scroll="scrollEvent">
      <div v-for="(item, index) in chatList" :key="index" class="chat-list-container">
        <div class="chat-item" :class="item.type === 'user' ? 'user' : 'ai'">
          <div v-if="item.type === 'user'" class="question">
            <div class="message" v-if="item.message && item.message.length > 0">{{ item.message[0] }}</div>
            <img class="avatar" src="../../assets/chat/userAvatar.png" alt="" />
          </div>
          <div v-else-if="item.type === 'ai'">
            <div v-if="item.message.length > 0 || item.isLoading" class="answer-container">
              <div class="answer">
                <div class="avatar-page">
                  <div><img class="avatar" src="../../assets/chat/aiAvatar.png" alt="" /></div>
                  <div class="page-container" v-if="item.message.length > 1">
                    <span class="pre-page" @click="preFn(index, item.answerIndex)">&lt;</span
                    >{{ item.answerIndex + 1 }} / {{ item.message.length
                    }}<span class="next-page" @click="nextFn(index, item.answerIndex)">&gt;</span>
                  </div>
                </div>

                <div class="answer-message">
                  <div v-if="item.isLoading">
                    <div v-html="currentHTML" class="markdown-body" />
                  </div>
                  <div v-else>
                    <div v-html="item.message[item.answerIndex]" class="markdown-body" />
                    <!-- <div v-if="item.reportFlag === 1">
                      <el-button @click="downloadReport(index, item.answerIndex)" class="download-btn">
                        <SvgIcon class="icon-img" name="download" />
                        下载尽调报告</el-button
                      >
                    </div> -->
                  </div>
                </div>
              </div>
              <div class="btn-container">
                <div class="opt-container">
                  <div v-if="index === chatList.length - 1">
                    <!-- <span v-if="item.isLoading" class="stop-btn" @click="stopFn">停止生成</span> -->
                    <span v-if="!item.isLoading && preInputValue" class="regenerate-btn" @click="regenerateFn"
                      >重新生成</span
                    >
                  </div>
                </div>
                <div class="tool-container" v-if="!item.isLoading">
                  <el-icon
                    class="copy"
                    :class="copyIndex === index ? 'copy-acive' : ''"
                    @click="copyFn(index, item.answerIndex)"
                    :data-clipboard-text="copyText"
                  >
                    <CopyDocument />
                  </el-icon>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
      <div class="loading-status" v-if="loadingStatus">
        <div><img class="avatar" src="../../assets/chat/aiAvatar.png" alt="" /></div>
        <div class="think">思考中...</div>
        <div><img class="loading-img" src="../../assets/chat/loading.gif" alt="" /></div>
      </div>
      <div v-if="!isLoading">
        <div class="scroll-container scroll-top" v-if="scrollTopShow">
          <el-icon style="vertical-align: middle" @click="scrollTopFn">
            <ArrowUp />
          </el-icon>
        </div>
        <div class="scroll-container scroll-bottom" v-else-if="scrollBottomShow">
          <el-icon style="vertical-align: middle" @click="scrollBottomFn">
            <ArrowDown />
          </el-icon>
        </div>
      </div>
    </div>
    <div class="input-container">
      <div class="stop-container" @click="stopFn" v-if="isLoading">
        <img class="stop-img" src="../../assets/chat/stop.png" alt="" />停止生成
      </div>
      <el-input
        class="input"
        type="textarea"
        v-model="inputValue"
        placeholder="你可以这样问:请写一份江苏省xxx有限公司的尽调报告"
        :autosize="{ minRows: 1, maxRows: 11 }"
        @keydown.enter.native="inputBlurFn($event)"
      />
      <div class="icon-container" :style="{ cursor: isLoading ? '' : 'pointer' }" @click="inputBlurFn">
        <img v-if="isLoading" class="loading-icon" src="../../assets/chat/loading.gif" alt="" />
        <img v-else-if="!isLoading && inputValue" src="../../assets/chat/send.svg" class="send-icon" alt="" />
        <img v-else src="../../assets/chat/unsend.svg" class="send-icon" alt="" />
      </div>
      <Agreement />
    </div>
    <ViewDialog ref="viewDialogRef" />
  </div>
</template>
<script lang="ts" src="./index.ts"></script>

index.ts:

ini 复制代码
import "./index.scss"
import { defineComponent, ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue"
import { fetchEventSource } from "@microsoft/fetch-event-source"
import { ElMessageBox, ElMessage } from "element-plus"
import MarkdownIt from "markdown-it"
import "github-markdown-css"
import Clipboard from "clipboard"
import { htmlToText } from "html-to-text"
import type { ChatItem } from "../../../types/chat"
export default defineComponent({
  components: { },
  setup() {
    const inputValue = ref("")
    const preInputValue = ref("") //上一次查询输入内容,用于重新生成使用
    const isLoading = ref(false) //节流loading
    const loadingStatus = ref(false) //加载状态显示loading
    const chatList = ref<ChatItem[]>([])
    const contentItems = ref("") //当前正在输出的数据流
    const chatContainerRef = ref()
    const isRegenerate = ref(false) //是否重新生成
    const controller = ref()
    const signal = ref()
    const copyText = ref("") //复制的文字
    const copyIndex = ref<number>()
    const scrollTopShow = ref(false)
    const scrollBottomShow = ref(false)
    const isRolling = ref(false) //鼠标滚轮是否滚动
    const isBottom = ref(false) //滚动参数
    onMounted(() => {
      initFn()
      chatContainerRef.value.addEventListener("wheel", moveWheel)
      window.addEventListener("message", function (event) {
        // 处理接收到的消息
        if (event.data && event.data.message) {
          inputValue.value = event.data.message
          sendFn()
        }
      })
    })
    // 处理鼠标滚轮事件
    const moveWheel1 = ref(true)
    const moveWheel2 = ref(false)
    const wheelClock = ref()
    function stopWheel() {
      if (moveWheel2.value == true) {
        // console.log("滚轮停止了")
        // isRolling.value = false
        moveWheel2.value = false
        moveWheel1.value = true
      }
    }
    function moveWheel() {
      if (moveWheel1.value == true) {
        // console.log("滚动了")
        isRolling.value = true
        moveWheel1.value = false
        moveWheel2.value = true
        //这里写开始滚动时调用的方法
        wheelClock.value = setTimeout(stopWheel, 200)
      } else {
        clearTimeout(wheelClock.value)
        wheelClock.value = setTimeout(stopWheel, 150)
      }
    }

    //初始化
    const initFn = () => {
      chatList.value = []
    }
    //上一页
    const preFn = (index: number, answerIndex: number) => {
      if (isLoading.value) return ElMessage.error("正在生成内容,请勿切换。")
      if (answerIndex === 0) {
        chatList.value[index].answerIndex = chatList.value[index].message.length - 1
      } else {
        chatList.value[index].answerIndex = chatList.value[index].answerIndex - 1
      }
    }
    //下一页
    const nextFn = (index: number, answerIndex: number) => {
      if (isLoading.value) return ElMessage.error("正在生成内容,请勿切换。")
      if (answerIndex === chatList.value[index].message.length - 1) {
        chatList.value[index].answerIndex = 0
      } else {
        chatList.value[index].answerIndex = chatList.value[index].answerIndex + 1
      }
    }
    // 1、新建实例md:
    const md = new MarkdownIt()
    const currentHTML = computed(() => {
      // 先判断存不存在,因为一开始currentPost有可能是undefined,在没有拿回数据的时候。
      if (contentItems.value) {
        if (contentItems.value.includes("</sy_think>")) {
          const arr = contentItems.value.split("</sy_think>")
          const thinkStr = `
          <h4> 师爷模型深度思考中...</h4>
          <div style="color: gray;font-size:14px;padding-left:10px;margin-bottom:10px;line-height:25px;border-left:1px solid #e5e5e5">${arr[0]}</div>
          <div><div>
         `
          return thinkStr + md.render(arr[1])
        } else {
          const thinkStr = `
          <h4> 师爷模型深度思考中...</h4>
          <div style="color: gray;font-size:14px;padding-left:10px;margin-bottom:10px;line-height:25px;border-left:1px solid #e5e5e5">${contentItems.value}</div>
          <div><div>
          `
          return thinkStr
        }
      }
    })
    //发送问题调用接口
    const sendFn = () => {
      showList.value = false
      controller.value = new AbortController()
      signal.value = controller.value.signal
      //先判断inputStr有没有值,isRegenerate表示是否重新生成
      const inputStr = isRegenerate.value ? preInputValue.value : inputValue.value
      if (!inputStr) return ElMessage.error("请输入要查询的问题。")
      if (isLoading.value) return ElMessage.error("正在生成内容,请稍后。")
      isLoading.value = true
      if (!isRegenerate.value) {
        //第一次生成
        chatList.value.push({ type: "user", message: [inputStr], answerIndex: 0, isLoading: false, reportFlag: null })
      }
      loadingStatus.value = true
      const url = `/recheck-web/open/shiye/chat?message=${inputStr}`
      fetchEventSource(url, {
        method: "GET",
        headers: {
          "Content-type": "application/json",
          Accept: "text/event-stream"
        },
        signal: signal.value,
        openWhenHidden: true,
        // params: JSON.stringify({ message: inputStr }),
        onopen: (e) => {
          if (e.status === 500) return ElMessage.error("服务器忙,请稍后再试。")
          if (isRegenerate.value) {
            //重新生成
            chatList.value[chatList.value.length - 1].message.push("")
            chatList.value[chatList.value.length - 1].answerIndex =
              chatList.value[chatList.value.length - 1].message.length - 1
          } else {
            chatList.value.push({ type: "ai", message: [], answerIndex: 0, isLoading: true })
            preInputValue.value = inputValue.value
          }
          chatList.value[chatList.value.length - 1].isLoading = true
          inputValue.value = ""
          isLoading.value = true
          loadingStatus.value = false
        },
        onmessage: (event) => {
          const data = JSON.parse(event.data)
          const newItem = data ? data.content : ""
          contentItems.value = contentItems.value + newItem
          if (data.status !== "end") {
          } else {
            if (isRegenerate.value) {
              //重新生成
              chatList.value[chatList.value.length - 1].message[
                chatList.value[chatList.value.length - 1].message.length - 1
              ] = currentHTML.value + ""
            } else {
              //第一次生成
              chatList.value[chatList.value.length - 1].message.push(currentHTML.value + "")
            }
            if (data.type) {
              chatList.value[chatList.value.length - 1].reportFlag = data.type
            }
          }
          nextTick(() => {
            if (isRolling.value === false) {
              chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight
              isRolling.value = false
            }
            if (isBottom.value) {
              isRolling.value = false
              chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight
            }
          })
        },
        onclose: () => {
          isLoading.value = false
          loadingStatus.value = false
          chatList.value[chatList.value.length - 1].isLoading = false
          contentItems.value = ""
        },
        onerror: () => {
          isLoading.value = false
          loadingStatus.value = false
          chatList.value[chatList.value.length - 1].isLoading = false
          contentItems.value = ""
        }
      })
    }
    //停止生成
    const stopFn = () => {
      isLoading.value = false
      loadingStatus.value = false
      controller.value.abort()
      //chatList最后一项
      const lastChatItem = chatList.value[chatList.value.length - 1]
      if (isRegenerate.value) {
        // lastChatItem.message[lastChatItem.message.length - 1] = md.render(contentItems.value + "\n" + "\n" + "停止生成")
        lastChatItem.message[lastChatItem.message.length - 1] =
          currentHTML.value + "<div style='font-size:16px;margin-top:10px'>停止生成</div>"
      } else {
        // lastChatItem.message.push(md.render(contentItems.value + "\n" + "\n" + "停止生成"))
        lastChatItem.message.push(currentHTML.value + "<div style='font-size:16px;margin-top:10px'>停止生成</div>")
      }
      contentItems.value = ""
      lastChatItem.isLoading = false
    }
    //重新生成
    const regenerateFn = () => {
      isRegenerate.value = true
      sendFn()
    }
    //发送
    const inputBlurFn = (event: any) => {
      if (!event.ctrlKey) {
        // 如果没有按下组合键ctrl,则会阻止默认事件
        event.preventDefault()
        isRegenerate.value = false
        sendFn()
      } else {
        // 如果同时按下ctrl+回车键,则会换行
        inputValue.value += "\n"
      }
    }
    //复制功能
    const copyFn = (index: number, answerIndex: number) => {
      copyIndex.value = index
      copyText.value = htmlToText(chatList.value[index].message[answerIndex])
      const clipboard = new Clipboard(".copy")
      // 成功
      clipboard.on("success", function (e) {
        ElMessage.success("复制成功")
        e.clearSelection()
        // 释放内存
        clipboard.destroy()
      })
      // 失败
      clipboard.on("error", function (e) {
        ElMessage.error("复制失败")
        clipboard.destroy()
      })
    }
    //试问
    const askFn = (question: string) => {
      inputValue.value = question
      isRegenerate.value = false
      sendFn()
    }
    //滚动事件
    const scrollEvent = (e: any) => {
      //如果滚动到底部,显示向上滚动按钮
      //如果滚动到顶部,显示向下滚动按钮
      const scrollTop = e.target.scrollTop
      const scrollHeight = e.target.scrollHeight
      const offsetHeight = Math.ceil(e.target.getBoundingClientRect().height)
      const currentHeight = scrollTop + offsetHeight

      if (currentHeight >= scrollHeight) {
        scrollTopShow.value = true
        isBottom.value = true
      } else {
        isBottom.value = false
        scrollTopShow.value = false
      }

      if (scrollHeight > offsetHeight) {
        scrollBottomShow.value = true
      } else {
        scrollBottomShow.value = false
      }
    }
    //向上滚动
    const scrollTopFn = () => {
      chatContainerRef.value.scrollTop = 0
    }
    //向下滚动
    const scrollBottomFn = () => {
      chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight + 250
    }
    //下载尽调报告
    const downloadReport = (index: number, answerIndex: number) => {
      const downStr = chatList.value[index].message[answerIndex]
      const arr = downStr.split("<div><div>")
      const blob = new Blob([arr[1]], { type: "text/plain" })
      const link = document.createElement("a")
      link.href = URL.createObjectURL(blob)
      link.download = "尽调报告.docx"
      link.click()
    }
    const generateReport = (question: string) => {
      inputValue.value = question
      isRegenerate.value = false
      sendFn()
    }
    const viewDialogRef = ref()
    const viewFn = () => {
      viewDialogRef.value.dialogVisible = true
    }
    watch(
      () => chatList.value,
      () => {
        if (chatList.value && chatList.value.length > 0) {
          chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight + 250
        }
      },
      { deep: true }
    )
    onUnmounted(() => {
      if (isLoading.value) {
        isLoading.value = false
        loadingStatus.value = false
        controller.value.abort()
      }
    })
    return {
      inputValue,
      isLoading,
      chatList,
      preFn,
      nextFn,
      sendFn,
      contentItems,
      stopFn,
      preInputValue,
      currentHTML,
      chatContainerRef,
      regenerateFn,
      inputBlurFn,
      copyFn,
      copyText,
      askFn,
      loadingStatus,
      copyIndex,
      downloadReport,
      generateReport,
      showList,
      scrollEvent,
      scrollTopFn,
      scrollBottomFn,
      scrollTopShow,
      scrollBottomShow,
      viewDialogRef,
      viewFn
    }
  }
})

index.scss:

css 复制代码
.report-page {
  height: 100%;
  width: 1201px;
  margin: 20px calc((100% - 1201px) / 2 - 35px) 20px calc((100% - 1201px) / 2 + 35px);
  .chat-container::-webkit-scrollbar {
    width: 0; /* 对于垂直滚动条 */
  }
  .chat-container {
    height: 85vh;
    overflow-y: auto;
    padding-bottom: 170px;
    margin-top: 0px;
    box-sizing: border-box;
    .chat-list-container {
      margin-top: 30px;
      .chat-item {
        display: flex;
        margin-bottom: 20px;
        .avatar {
          width: 40px;
          height: 40px;
        }
        .question {
          display: flex;
          margin-bottom: 30px;
          margin-left: 70px;
          margin-right: 10px;
          .message {
            padding: 12px 10px 10px;
            border-radius: 14px 0px 14px 14px;
            background: linear-gradient(128deg, #4672ff -1.27%, #7daafc 109.62%);
            color: #fff;
          }
          .avatar {
            margin-left: 20px;
          }
        }
        .answer-container {
          margin-right: 70px;
          margin-bottom: 30px;
          .answer {
            display: flex;
            .avatar-page {
              width: 70px;
              position: relative;
              .avatar {
                width: 60px;
                height: 60px;
                margin-right: 10px;
              }

              .page-container {
                position: absolute;
                top: 60px;
                left: 3px;
                color: #000;
                font-family: "PingFang SC";
                font-size: 14px;
                font-style: normal;
                font-weight: 400;
                line-height: 24px;
                .pre-page {
                  margin-right: 1px;
                  cursor: pointer;
                }
                .next-page {
                  margin-left: 1px;
                  cursor: pointer;
                }
              }
            }
            .answer-message {
              background-color: #fff;
              padding: 20px;
              border-radius: 0 14px 14px 14px;
              min-width: 500px;
              .download-btn {
                color: #333;
                text-align: center;
                font-family: "PingFang SC";
                font-size: 14px;
                font-style: normal;
                font-weight: 400;
                line-height: 14px;
                background-color: #f2f3f8;
                height: 34px;
                margin-top: 20px;
                border-radius: 6px;
                border-color: transparent;
                .icon-img {
                  width: 17px;
                  height: 17px;
                  margin-right: 5px;
                }
                &:hover {
                  background: linear-gradient(128deg, #4672ff -1.27%, #7daafc 109.62%);
                  color: #fff;
                }
              }
            }
          }
          .btn-container {
            margin-left: 80px;
            margin-top: 18px;
            text-align: left;
            display: flex;
            justify-content: space-between;
            .opt-container {
              .stop-btn,
              .regenerate-btn {
                cursor: pointer;
                color: #57f;
                font-family: "PingFang SC";
                font-size: 14px;
                font-style: normal;
                font-weight: 500;
                line-height: 24px;
              }
            }
            .tool-container {
              background-color: #fff;
              padding: 6px 10px 8px;
              height: 40px;
              border-radius: 20px;
              min-width: 70px;
              text-align: center;
              .copy {
                width: 28px;
                height: 28px;
                cursor: pointer;
              }
              .copy-acive {
                color: #5577ff;
              }
            }
          }
        }
      }
    }
    .user {
      flex-direction: row-reverse;
    }
    .loading-status {
      display: flex;
      .avatar {
        width: 60px;
        height: 60px;
        margin-right: 20px;
      }
      .think {
        height: 52px;
        line-height: 52px;
        background-color: #fff;
        text-align: center;
        border-radius: 0 14px 14px 14px;
        width: 100px;
        color: #999;
      }
      .loading-img {
        width: 40px;
        height: 40px;
      }
    }
    .scroll-container {
      width: 38px;
      height: 38px;
      background-color: #fff;
      border-radius: 19px;
      display: flex;
      justify-content: center;
      align-items: center;
      position: fixed;
      left: calc((100% - 1201px) / 2 + 175px + 1061px);
    }
    .scroll-top {
      bottom: 200px;
    }
    .scroll-bottom {
      top: 53px;
    }
  }
  .input-container {
    position: fixed;
    left: calc((100% - 1061px) / 2 + 35px);
    bottom: 5%;
    width: 1061px;
    .stop-container {
      cursor: pointer;
      width: 104px;
      height: 36px;
      background-color: #fff;
      color: #5863ff;
      line-height: 36px;
      text-align: center;
      border-radius: 18px;
      position: absolute;
      top: -50px;
      font-size: 14px;
      font-style: normal;
      font-weight: 400;
      .stop-img {
        width: 22px;
        height: 22px;
        vertical-align: middle;
        margin-right: 3px;
      }
    }
    .input {
      .el-textarea__inner {
        padding: 15px;
        border-radius: 14px;
        box-shadow: 14px 27px 45px 0px rgba(112, 144, 176, 0.2);
      }
      .el-textarea__inner::-webkit-scrollbar {
        width: 6px;
        height: 6px;
      }
      .el-textarea__inner::-webkit-scrollbar-thumb {
        border-radius: 3px;
        -moz-border-radius: 3px;
        -webkit-border-radius: 3px;
        background-color: #c3c3c3;
      }
      .el-textarea__inner::-webkit-scrollbar-track {
        background-color: transparent;
      }
    }
    .icon-container {
      position: absolute;
      right: 10px;
      bottom: 35px;
      z-index: 999;
      .loading-icon {
        width: 40px;
        height: 40px;
      }
      .send-icon {
        width: 35px;
        height: 35px;
      }
    }
  }
}
.markdown-body {
  box-sizing: border-box;
  max-width: 1021px !important;
  hr {
    display: none !important;
  }
}
相关推荐
中微子19 分钟前
React状态管理最佳实践
前端
烛阴29 分钟前
void 0 的奥秘:解锁 JavaScript 中 undefined 的正确打开方式
前端·javascript
中微子35 分钟前
JavaScript 事件与 React 合成事件完全指南:从入门到精通
前端
Hexene...44 分钟前
【前端Vue】如何实现echarts图表根据父元素宽度自适应大小
前端·vue.js·echarts
天天扭码1 小时前
《很全面的前端面试题》——HTML篇
前端·面试·html
xw51 小时前
我犯了错,我于是为我的uni-app项目引入环境标志
前端·uni-app
!win !1 小时前
被老板怼后,我为uni-app项目引入环境标志
前端·小程序·uni-app
Burt1 小时前
tsdown vs tsup, 豆包回答一坨屎,还是google AI厉害
前端
群联云防护小杜2 小时前
构建分布式高防架构实现业务零中断
前端·网络·分布式·tcp/ip·安全·游戏·架构
ohMyGod_1233 小时前
React16,17,18,19新特性更新对比
前端·javascript·react.js