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

打印 上一主题 下一主题

主题 905|帖子 905|积分 2715

一.项目功能:


  • 智能问答(实时谈天+流通打字机效果+自动滚动)
  • 克制天生(取消接口调用)、重新天生
  • 复制功能、问答分页
二.效果展示:


三.技术分析:


  • fetchEventSource:传统axios哀求是等接口将所有数据一次性相应回来后再渲染到页面上,当数据量较大时,相应速度较慢,且无法做到实时输出。而fetchEventSource允许客户端吸收来自服务器的实时更新,前端可以实时的将流式数据展示到页面上,类似于打字机的效果。
    1. fetchEventSource(url, {
    2.         method: "GET",
    3.         headers: {
    4.           "Content-type": "application/json",
    5.           Accept: "text/event-stream"
    6.         },
    7.         openWhenHidden: true,
    8.         onopen: (e) => {
    9.           //接口请求成功,但此时数据还未响应回来
    10.         },
    11.         onmessage: (event) => {
    12.          //响应数据持续数据
    13.         },
    14.         onclose: () => {
    15.          //请求关闭
    16.         },
    17.         onerror: () => {
    18.           //请求错误
    19.         }
    20.       })
    复制代码

  • MarkdownIt :SSE相应的数据格式是markdown,无法直接展示,需要使用MarkdownIt第三方库转换成html,然后通过v-model展示到页面上。
    1.     // 1、新建实例md:
    2.     const md = new MarkdownIt()
    3.     // 2.将markdown转化为html
    4.     const htmlStr= md.render(markdownStr)
    复制代码

  • Clipboard+html-to-text:复制时,需要使用html-to-text第三方库将html转化为text,然后借助Clipboard复制到粘贴板上。
    1. //1.在html中设置“copy”类名,并绑定data-clipboard-text
    2. <el-icon class="copy" @click="copyFn(copyHtmlStr)" :data-clipboard-text="copyText"> </el-icon>
    3. //2.先将html转化成text,然后复制到粘贴板
    4. const copyFn = (copyHtmlStr) => {
    5.      copyText.value=htmlToText(copyHtmlStr)
    6.       const clipboard = new Clipboard(".copy")
    7.       // 成功
    8.       clipboard.on("success", function (e) {
    9.         ElMessage.success("复制成功")
    10.         e.clearSelection()
    11.         // 释放内存
    12.         clipboard.destroy()
    13.       })
    14.       // 失败
    15.       clipboard.on("error", function (e) {
    16.         ElMessage.error("复制失败")
    17.         clipboard.destroy()
    18.       })
    19.     }
    复制代码

  • scrollEvent:由于数据流式输出,页面内容持续增加,大概会溢出屏幕,因此需要在fetchEventSource吸收消息onmessage的过程中,通过设置scrollTop =scrollHeight让页面实现自动滚动。
    1. fetchEventSource(url, {
    2.         ...,
    3.         onmessage: (event) => {
    4.          chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight
    5.         },
    6.         ...
    7.       })
    复制代码

四:疑难点及办理方案:

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时添加判断,假如鼠标滚轮滑动且未到页面底部,则不自动滚动。
  1. const isRolling = ref(false) //鼠标滚轮是否滚动
  2. const isBottom = ref(false) //滚动参数
  3. // 处理鼠标滚轮事件
  4. const moveWheel1 = ref(true)
  5. const moveWheel2 = ref(false)
  6. const wheelClock = ref()
  7. const stopWheel=()=> {
  8.       if (moveWheel2.value == true) {
  9.         moveWheel2.value = false
  10.         moveWheel1.value = true
  11.       }
  12.     }
  13. const moveWheel=()=> {
  14.       if (moveWheel1.value == true) {
  15.         isRolling.value = true
  16.         moveWheel1.value = false
  17.         moveWheel2.value = true
  18.         //这里写开始滚动时调用的方法
  19.         wheelClock.value = setTimeout(stopWheel, 200)
  20.       } else {
  21.         clearTimeout(wheelClock.value)
  22.         wheelClock.value = setTimeout(stopWheel, 150)
  23.       }
  24.     }
  25. const sendFn=()=>{
  26.      fetchEventSource(url, {
  27.         ...,
  28.         onmessage: (event) => {
  29.             if (isRolling.value === false) {
  30.               chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight
  31.               isRolling.value = false
  32.             }
  33.             if (isBottom.value) {
  34.               isRolling.value = false
  35.               chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight
  36.             }
  37.         },
  38.         ...
  39.       })
  40. }
复制代码


4.  题目形貌:SSE相应回来的数据中表格样式未见效。
     办理方案:MarkdownIt第三方库将markdown转换成html时,部门样式会丢失,需使用github-markdown-css添加样式。
  1. npm i github-markdown-css
  2. import "github-markdown-css"
复制代码

五.完整代码示例:

   index.vue:
  1. <template>
  2.   <div class="report-page">
  3.     <div class="chat-container" ref="chatContainerRef" @scroll="scrollEvent">
  4.       <div v-for="(item, index) in chatList" :key="index" class="chat-list-container">
  5.         <div class="chat-item" :class="item.type === 'user' ? 'user' : 'ai'">
  6.           <div v-if="item.type === 'user'" class="question">
  7.             <div class="message" v-if="item.message && item.message.length > 0">{{ item.message[0] }}</div>
  8.             <img class="avatar" src="../../assets/chat/userAvatar.png" alt="" />
  9.           </div>
  10.           <div v-else-if="item.type === 'ai'">
  11.             <div v-if="item.message.length > 0 || item.isLoading" class="answer-container">
  12.               <div class="answer">
  13.                 <div class="avatar-page">
  14.                   <div><img class="avatar" src="../../assets/chat/aiAvatar.png" alt="" /></div>
  15.                   <div class="page-container" v-if="item.message.length > 1">
  16.                     <span class="pre-page" @click="preFn(index, item.answerIndex)">&lt;</span
  17.                     >{{ item.answerIndex + 1 }} / {{ item.message.length
  18.                     }}<span class="next-page" @click="nextFn(index, item.answerIndex)">&gt;</span>
  19.                   </div>
  20.                 </div>
  21.                 <div class="answer-message">
  22.                   <div v-if="item.isLoading">
  23.                     <div v-html="currentHTML" class="markdown-body" />
  24.                   </div>
  25.                   <div v-else>
  26.                     <div v-html="item.message[item.answerIndex]" class="markdown-body" />
  27.                     <!-- <div v-if="item.reportFlag === 1">
  28.                       <el-button @click="downloadReport(index, item.answerIndex)" class="download-btn">
  29.                         <SvgIcon class="icon-img" name="download" />
  30.                         下载尽调报告</el-button
  31.                       >
  32.                     </div> -->
  33.                   </div>
  34.                 </div>
  35.               </div>
  36.               <div class="btn-container">
  37.                 <div class="opt-container">
  38.                   <div v-if="index === chatList.length - 1">
  39.                     <!-- <span v-if="item.isLoading" class="stop-btn" @click="stopFn">停止生成</span> -->
  40.                     <span v-if="!item.isLoading && preInputValue" class="regenerate-btn" @click="regenerateFn"
  41.                       >重新生成</span
  42.                     >
  43.                   </div>
  44.                 </div>
  45.                 <div class="tool-container" v-if="!item.isLoading">
  46.                   <el-icon
  47.                     class="copy"
  48.                     :class="copyIndex === index ? 'copy-acive' : ''"
  49.                     @click="copyFn(index, item.answerIndex)"
  50.                     :data-clipboard-text="copyText"
  51.                   >
  52.                     <CopyDocument />
  53.                   </el-icon>
  54.                 </div>
  55.               </div>
  56.             </div>
  57.           </div>
  58.         </div>
  59.       </div>
  60.       <div class="loading-status" v-if="loadingStatus">
  61.         <div><img class="avatar" src="../../assets/chat/aiAvatar.png" alt="" /></div>
  62.         <div class="think">思考中…</div>
  63.         <div><img class="loading-img" src="../../assets/chat/loading.gif" alt="" /></div>
  64.       </div>
  65.       <div v-if="!isLoading">
  66.         <div class="scroll-container scroll-top" v-if="scrollTopShow">
  67.           <el-icon style="vertical-align: middle" @click="scrollTopFn">
  68.             <ArrowUp />
  69.           </el-icon>
  70.         </div>
  71.         <div class="scroll-container scroll-bottom" v-else-if="scrollBottomShow">
  72.           <el-icon style="vertical-align: middle" @click="scrollBottomFn">
  73.             <ArrowDown />
  74.           </el-icon>
  75.         </div>
  76.       </div>
  77.     </div>
  78.     <div class="input-container">
  79.       <div class="stop-container" @click="stopFn" v-if="isLoading">
  80.         <img class="stop-img" src="../../assets/chat/stop.png" alt="" />停止生成
  81.       </div>
  82.       <el-input
  83.         class="input"
  84.         type="textarea"
  85.         v-model="inputValue"
  86.         placeholder="你可以这样问:请写一份江苏省xxx有限公司的尽调报告"
  87.         :autosize="{ minRows: 1, maxRows: 11 }"
  88.         @keydown.enter.native="inputBlurFn($event)"
  89.       />
  90.       <div class="icon-container" :style="{ cursor: isLoading ? '' : 'pointer' }" @click="inputBlurFn">
  91.         <img v-if="isLoading" class="loading-icon" src="../../assets/chat/loading.gif" alt="" />
  92.         <img v-else-if="!isLoading && inputValue" src="../../assets/chat/send.svg" class="send-icon" alt="" />
  93.         <img v-else src="../../assets/chat/unsend.svg" class="send-icon" alt="" />
  94.       </div>
  95.       <Agreement />
  96.     </div>
  97.     <ViewDialog ref="viewDialogRef" />
  98.   </div>
  99. </template>
  100. <script lang="ts" src="./index.ts"></script>
复制代码
   index.ts:
  1. import "./index.scss"
  2. import { defineComponent, ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue"
  3. import { fetchEventSource } from "@microsoft/fetch-event-source"
  4. import { ElMessageBox, ElMessage } from "element-plus"
  5. import MarkdownIt from "markdown-it"
  6. import "github-markdown-css"
  7. import Clipboard from "clipboard"
  8. import { htmlToText } from "html-to-text"
  9. import type { ChatItem } from "../../../types/chat"
  10. export default defineComponent({
  11.   components: { },
  12.   setup() {
  13.     const inputValue = ref("")
  14.     const preInputValue = ref("") //上一次查询输入内容,用于重新生成使用
  15.     const isLoading = ref(false) //节流loading
  16.     const loadingStatus = ref(false) //加载状态显示loading
  17.     const chatList = ref<ChatItem[]>([])
  18.     const contentItems = ref("") //当前正在输出的数据流
  19.     const chatContainerRef = ref()
  20.     const isRegenerate = ref(false) //是否重新生成
  21.     const controller = ref()
  22.     const signal = ref()
  23.     const copyText = ref("") //复制的文字
  24.     const copyIndex = ref<number>()
  25.     const scrollTopShow = ref(false)
  26.     const scrollBottomShow = ref(false)
  27.     const isRolling = ref(false) //鼠标滚轮是否滚动
  28.     const isBottom = ref(false) //滚动参数
  29.     onMounted(() => {
  30.       initFn()
  31.       chatContainerRef.value.addEventListener("wheel", moveWheel)
  32.       window.addEventListener("message", function (event) {
  33.         // 处理接收到的消息
  34.         if (event.data && event.data.message) {
  35.           inputValue.value = event.data.message
  36.           sendFn()
  37.         }
  38.       })
  39.     })
  40.     // 处理鼠标滚轮事件
  41.     const moveWheel1 = ref(true)
  42.     const moveWheel2 = ref(false)
  43.     const wheelClock = ref()
  44.     function stopWheel() {
  45.       if (moveWheel2.value == true) {
  46.         // console.log("滚轮停止了")
  47.         // isRolling.value = false
  48.         moveWheel2.value = false
  49.         moveWheel1.value = true
  50.       }
  51.     }
  52.     function moveWheel() {
  53.       if (moveWheel1.value == true) {
  54.         // console.log("滚动了")
  55.         isRolling.value = true
  56.         moveWheel1.value = false
  57.         moveWheel2.value = true
  58.         //这里写开始滚动时调用的方法
  59.         wheelClock.value = setTimeout(stopWheel, 200)
  60.       } else {
  61.         clearTimeout(wheelClock.value)
  62.         wheelClock.value = setTimeout(stopWheel, 150)
  63.       }
  64.     }
  65.     //初始化
  66.     const initFn = () => {
  67.       chatList.value = []
  68.     }
  69.     //上一页
  70.     const preFn = (index: number, answerIndex: number) => {
  71.       if (isLoading.value) return ElMessage.error("正在生成内容,请勿切换。")
  72.       if (answerIndex === 0) {
  73.         chatList.value[index].answerIndex = chatList.value[index].message.length - 1
  74.       } else {
  75.         chatList.value[index].answerIndex = chatList.value[index].answerIndex - 1
  76.       }
  77.     }
  78.     //下一页
  79.     const nextFn = (index: number, answerIndex: number) => {
  80.       if (isLoading.value) return ElMessage.error("正在生成内容,请勿切换。")
  81.       if (answerIndex === chatList.value[index].message.length - 1) {
  82.         chatList.value[index].answerIndex = 0
  83.       } else {
  84.         chatList.value[index].answerIndex = chatList.value[index].answerIndex + 1
  85.       }
  86.     }
  87.     // 1、新建实例md:
  88.     const md = new MarkdownIt()
  89.     const currentHTML = computed(() => {
  90.       // 先判断存不存在,因为一开始currentPost有可能是undefined,在没有拿回数据的时候。
  91.       if (contentItems.value) {
  92.         if (contentItems.value.includes("</sy_think>")) {
  93.           const arr = contentItems.value.split("</sy_think>")
  94.           const thinkStr = `
  95.           <h4> 师爷模型深度思考中...</h4>
  96.           <div style="color: gray;font-size:14px;padding-left:10px;margin-bottom:10px;line-height:25px;border-left:1px solid #e5e5e5">${arr[0]}</div>
  97.           <div><div>
  98.          `
  99.           return thinkStr + md.render(arr[1])
  100.         } else {
  101.           const thinkStr = `
  102.           <h4> 师爷模型深度思考中...</h4>
  103.           <div style="color: gray;font-size:14px;padding-left:10px;margin-bottom:10px;line-height:25px;border-left:1px solid #e5e5e5">${contentItems.value}</div>
  104.           <div><div>
  105.           `
  106.           return thinkStr
  107.         }
  108.       }
  109.     })
  110.     //发送问题调用接口
  111.     const sendFn = () => {
  112.       showList.value = false
  113.       controller.value = new AbortController()
  114.       signal.value = controller.value.signal
  115.       //先判断inputStr有没有值,isRegenerate表示是否重新生成
  116.       const inputStr = isRegenerate.value ? preInputValue.value : inputValue.value
  117.       if (!inputStr) return ElMessage.error("请输入要查询的问题。")
  118.       if (isLoading.value) return ElMessage.error("正在生成内容,请稍后。")
  119.       isLoading.value = true
  120.       if (!isRegenerate.value) {
  121.         //第一次生成
  122.         chatList.value.push({ type: "user", message: [inputStr], answerIndex: 0, isLoading: false, reportFlag: null })
  123.       }
  124.       loadingStatus.value = true
  125.       const url = `/recheck-web/open/shiye/chat?message=${inputStr}`
  126.       fetchEventSource(url, {
  127.         method: "GET",
  128.         headers: {
  129.           "Content-type": "application/json",
  130.           Accept: "text/event-stream"
  131.         },
  132.         signal: signal.value,
  133.         openWhenHidden: true,
  134.         // params: JSON.stringify({ message: inputStr }),
  135.         onopen: (e) => {
  136.           if (e.status === 500) return ElMessage.error("服务器忙,请稍后再试。")
  137.           if (isRegenerate.value) {
  138.             //重新生成
  139.             chatList.value[chatList.value.length - 1].message.push("")
  140.             chatList.value[chatList.value.length - 1].answerIndex =
  141.               chatList.value[chatList.value.length - 1].message.length - 1
  142.           } else {
  143.             chatList.value.push({ type: "ai", message: [], answerIndex: 0, isLoading: true })
  144.             preInputValue.value = inputValue.value
  145.           }
  146.           chatList.value[chatList.value.length - 1].isLoading = true
  147.           inputValue.value = ""
  148.           isLoading.value = true
  149.           loadingStatus.value = false
  150.         },
  151.         onmessage: (event) => {
  152.           const data = JSON.parse(event.data)
  153.           const newItem = data ? data.content : ""
  154.           contentItems.value = contentItems.value + newItem
  155.           if (data.status !== "end") {
  156.           } else {
  157.             if (isRegenerate.value) {
  158.               //重新生成
  159.               chatList.value[chatList.value.length - 1].message[
  160.                 chatList.value[chatList.value.length - 1].message.length - 1
  161.               ] = currentHTML.value + ""
  162.             } else {
  163.               //第一次生成
  164.               chatList.value[chatList.value.length - 1].message.push(currentHTML.value + "")
  165.             }
  166.             if (data.type) {
  167.               chatList.value[chatList.value.length - 1].reportFlag = data.type
  168.             }
  169.           }
  170.           nextTick(() => {
  171.             if (isRolling.value === false) {
  172.               chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight
  173.               isRolling.value = false
  174.             }
  175.             if (isBottom.value) {
  176.               isRolling.value = false
  177.               chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight
  178.             }
  179.           })
  180.         },
  181.         onclose: () => {
  182.           isLoading.value = false
  183.           loadingStatus.value = false
  184.           chatList.value[chatList.value.length - 1].isLoading = false
  185.           contentItems.value = ""
  186.         },
  187.         onerror: () => {
  188.           isLoading.value = false
  189.           loadingStatus.value = false
  190.           chatList.value[chatList.value.length - 1].isLoading = false
  191.           contentItems.value = ""
  192.         }
  193.       })
  194.     }
  195.     //停止生成
  196.     const stopFn = () => {
  197.       isLoading.value = false
  198.       loadingStatus.value = false
  199.       controller.value.abort()
  200.       //chatList最后一项
  201.       const lastChatItem = chatList.value[chatList.value.length - 1]
  202.       if (isRegenerate.value) {
  203.         // lastChatItem.message[lastChatItem.message.length - 1] = md.render(contentItems.value + "\n" + "\n" + "停止生成")
  204.         lastChatItem.message[lastChatItem.message.length - 1] =
  205.           currentHTML.value + "<div style='font-size:16px;margin-top:10px'>停止生成</div>"
  206.       } else {
  207.         // lastChatItem.message.push(md.render(contentItems.value + "\n" + "\n" + "停止生成"))
  208.         lastChatItem.message.push(currentHTML.value + "<div style='font-size:16px;margin-top:10px'>停止生成</div>")
  209.       }
  210.       contentItems.value = ""
  211.       lastChatItem.isLoading = false
  212.     }
  213.     //重新生成
  214.     const regenerateFn = () => {
  215.       isRegenerate.value = true
  216.       sendFn()
  217.     }
  218.     //发送
  219.     const inputBlurFn = (event: any) => {
  220.       if (!event.ctrlKey) {
  221.         // 如果没有按下组合键ctrl,则会阻止默认事件
  222.         event.preventDefault()
  223.         isRegenerate.value = false
  224.         sendFn()
  225.       } else {
  226.         // 如果同时按下ctrl+回车键,则会换行
  227.         inputValue.value += "\n"
  228.       }
  229.     }
  230.     //复制功能
  231.     const copyFn = (index: number, answerIndex: number) => {
  232.       copyIndex.value = index
  233.       copyText.value = htmlToText(chatList.value[index].message[answerIndex])
  234.       const clipboard = new Clipboard(".copy")
  235.       // 成功
  236.       clipboard.on("success", function (e) {
  237.         ElMessage.success("复制成功")
  238.         e.clearSelection()
  239.         // 释放内存
  240.         clipboard.destroy()
  241.       })
  242.       // 失败
  243.       clipboard.on("error", function (e) {
  244.         ElMessage.error("复制失败")
  245.         clipboard.destroy()
  246.       })
  247.     }
  248.     //试问
  249.     const askFn = (question: string) => {
  250.       inputValue.value = question
  251.       isRegenerate.value = false
  252.       sendFn()
  253.     }
  254.     //滚动事件
  255.     const scrollEvent = (e: any) => {
  256.       //如果滚动到底部,显示向上滚动按钮
  257.       //如果滚动到顶部,显示向下滚动按钮
  258.       const scrollTop = e.target.scrollTop
  259.       const scrollHeight = e.target.scrollHeight
  260.       const offsetHeight = Math.ceil(e.target.getBoundingClientRect().height)
  261.       const currentHeight = scrollTop + offsetHeight
  262.       if (currentHeight >= scrollHeight) {
  263.         scrollTopShow.value = true
  264.         isBottom.value = true
  265.       } else {
  266.         isBottom.value = false
  267.         scrollTopShow.value = false
  268.       }
  269.       if (scrollHeight > offsetHeight) {
  270.         scrollBottomShow.value = true
  271.       } else {
  272.         scrollBottomShow.value = false
  273.       }
  274.     }
  275.     //向上滚动
  276.     const scrollTopFn = () => {
  277.       chatContainerRef.value.scrollTop = 0
  278.     }
  279.     //向下滚动
  280.     const scrollBottomFn = () => {
  281.       chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight + 250
  282.     }
  283.     //下载尽调报告
  284.     const downloadReport = (index: number, answerIndex: number) => {
  285.       const downStr = chatList.value[index].message[answerIndex]
  286.       const arr = downStr.split("<div><div>")
  287.       const blob = new Blob([arr[1]], { type: "text/plain" })
  288.       const link = document.createElement("a")
  289.       link.href = URL.createObjectURL(blob)
  290.       link.download = "尽调报告.docx"
  291.       link.click()
  292.     }
  293.     const generateReport = (question: string) => {
  294.       inputValue.value = question
  295.       isRegenerate.value = false
  296.       sendFn()
  297.     }
  298.     const viewDialogRef = ref()
  299.     const viewFn = () => {
  300.       viewDialogRef.value.dialogVisible = true
  301.     }
  302.     watch(
  303.       () => chatList.value,
  304.       () => {
  305.         if (chatList.value && chatList.value.length > 0) {
  306.           chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight + 250
  307.         }
  308.       },
  309.       { deep: true }
  310.     )
  311.     onUnmounted(() => {
  312.       if (isLoading.value) {
  313.         isLoading.value = false
  314.         loadingStatus.value = false
  315.         controller.value.abort()
  316.       }
  317.     })
  318.     return {
  319.       inputValue,
  320.       isLoading,
  321.       chatList,
  322.       preFn,
  323.       nextFn,
  324.       sendFn,
  325.       contentItems,
  326.       stopFn,
  327.       preInputValue,
  328.       currentHTML,
  329.       chatContainerRef,
  330.       regenerateFn,
  331.       inputBlurFn,
  332.       copyFn,
  333.       copyText,
  334.       askFn,
  335.       loadingStatus,
  336.       copyIndex,
  337.       downloadReport,
  338.       generateReport,
  339.       showList,
  340.       scrollEvent,
  341.       scrollTopFn,
  342.       scrollBottomFn,
  343.       scrollTopShow,
  344.       scrollBottomShow,
  345.       viewDialogRef,
  346.       viewFn
  347.     }
  348.   }
  349. })
复制代码
   index.scss:
  1. .report-page {
  2.   height: 100%;
  3.   width: 1201px;
  4.   margin: 20px calc((100% - 1201px) / 2 - 35px) 20px calc((100% - 1201px) / 2 + 35px);
  5.   .chat-container::-webkit-scrollbar {
  6.     width: 0; /* 对于垂直滚动条 */
  7.   }
  8.   .chat-container {
  9.     height: 85vh;
  10.     overflow-y: auto;
  11.     padding-bottom: 170px;
  12.     margin-top: 0px;
  13.     box-sizing: border-box;
  14.     .chat-list-container {
  15.       margin-top: 30px;
  16.       .chat-item {
  17.         display: flex;
  18.         margin-bottom: 20px;
  19.         .avatar {
  20.           width: 40px;
  21.           height: 40px;
  22.         }
  23.         .question {
  24.           display: flex;
  25.           margin-bottom: 30px;
  26.           margin-left: 70px;
  27.           margin-right: 10px;
  28.           .message {
  29.             padding: 12px 10px 10px;
  30.             border-radius: 14px 0px 14px 14px;
  31.             background: linear-gradient(128deg, #4672ff -1.27%, #7daafc 109.62%);
  32.             color: #fff;
  33.           }
  34.           .avatar {
  35.             margin-left: 20px;
  36.           }
  37.         }
  38.         .answer-container {
  39.           margin-right: 70px;
  40.           margin-bottom: 30px;
  41.           .answer {
  42.             display: flex;
  43.             .avatar-page {
  44.               width: 70px;
  45.               position: relative;
  46.               .avatar {
  47.                 width: 60px;
  48.                 height: 60px;
  49.                 margin-right: 10px;
  50.               }
  51.               .page-container {
  52.                 position: absolute;
  53.                 top: 60px;
  54.                 left: 3px;
  55.                 color: #000;
  56.                 font-family: "PingFang SC";
  57.                 font-size: 14px;
  58.                 font-style: normal;
  59.                 font-weight: 400;
  60.                 line-height: 24px;
  61.                 .pre-page {
  62.                   margin-right: 1px;
  63.                   cursor: pointer;
  64.                 }
  65.                 .next-page {
  66.                   margin-left: 1px;
  67.                   cursor: pointer;
  68.                 }
  69.               }
  70.             }
  71.             .answer-message {
  72.               background-color: #fff;
  73.               padding: 20px;
  74.               border-radius: 0 14px 14px 14px;
  75.               min-width: 500px;
  76.               .download-btn {
  77.                 color: #333;
  78.                 text-align: center;
  79.                 font-family: "PingFang SC";
  80.                 font-size: 14px;
  81.                 font-style: normal;
  82.                 font-weight: 400;
  83.                 line-height: 14px;
  84.                 background-color: #f2f3f8;
  85.                 height: 34px;
  86.                 margin-top: 20px;
  87.                 border-radius: 6px;
  88.                 border-color: transparent;
  89.                 .icon-img {
  90.                   width: 17px;
  91.                   height: 17px;
  92.                   margin-right: 5px;
  93.                 }
  94.                 &:hover {
  95.                   background: linear-gradient(128deg, #4672ff -1.27%, #7daafc 109.62%);
  96.                   color: #fff;
  97.                 }
  98.               }
  99.             }
  100.           }
  101.           .btn-container {
  102.             margin-left: 80px;
  103.             margin-top: 18px;
  104.             text-align: left;
  105.             display: flex;
  106.             justify-content: space-between;
  107.             .opt-container {
  108.               .stop-btn,
  109.               .regenerate-btn {
  110.                 cursor: pointer;
  111.                 color: #57f;
  112.                 font-family: "PingFang SC";
  113.                 font-size: 14px;
  114.                 font-style: normal;
  115.                 font-weight: 500;
  116.                 line-height: 24px;
  117.               }
  118.             }
  119.             .tool-container {
  120.               background-color: #fff;
  121.               padding: 6px 10px 8px;
  122.               height: 40px;
  123.               border-radius: 20px;
  124.               min-width: 70px;
  125.               text-align: center;
  126.               .copy {
  127.                 width: 28px;
  128.                 height: 28px;
  129.                 cursor: pointer;
  130.               }
  131.               .copy-acive {
  132.                 color: #5577ff;
  133.               }
  134.             }
  135.           }
  136.         }
  137.       }
  138.     }
  139.     .user {
  140.       flex-direction: row-reverse;
  141.     }
  142.     .loading-status {
  143.       display: flex;
  144.       .avatar {
  145.         width: 60px;
  146.         height: 60px;
  147.         margin-right: 20px;
  148.       }
  149.       .think {
  150.         height: 52px;
  151.         line-height: 52px;
  152.         background-color: #fff;
  153.         text-align: center;
  154.         border-radius: 0 14px 14px 14px;
  155.         width: 100px;
  156.         color: #999;
  157.       }
  158.       .loading-img {
  159.         width: 40px;
  160.         height: 40px;
  161.       }
  162.     }
  163.     .scroll-container {
  164.       width: 38px;
  165.       height: 38px;
  166.       background-color: #fff;
  167.       border-radius: 19px;
  168.       display: flex;
  169.       justify-content: center;
  170.       align-items: center;
  171.       position: fixed;
  172.       left: calc((100% - 1201px) / 2 + 175px + 1061px);
  173.     }
  174.     .scroll-top {
  175.       bottom: 200px;
  176.     }
  177.     .scroll-bottom {
  178.       top: 53px;
  179.     }
  180.   }
  181.   .input-container {
  182.     position: fixed;
  183.     left: calc((100% - 1061px) / 2 + 35px);
  184.     bottom: 5%;
  185.     width: 1061px;
  186.     .stop-container {
  187.       cursor: pointer;
  188.       width: 104px;
  189.       height: 36px;
  190.       background-color: #fff;
  191.       color: #5863ff;
  192.       line-height: 36px;
  193.       text-align: center;
  194.       border-radius: 18px;
  195.       position: absolute;
  196.       top: -50px;
  197.       font-size: 14px;
  198.       font-style: normal;
  199.       font-weight: 400;
  200.       .stop-img {
  201.         width: 22px;
  202.         height: 22px;
  203.         vertical-align: middle;
  204.         margin-right: 3px;
  205.       }
  206.     }
  207.     .input {
  208.       .el-textarea__inner {
  209.         padding: 15px;
  210.         border-radius: 14px;
  211.         box-shadow: 14px 27px 45px 0px rgba(112, 144, 176, 0.2);
  212.       }
  213.       .el-textarea__inner::-webkit-scrollbar {
  214.         width: 6px;
  215.         height: 6px;
  216.       }
  217.       .el-textarea__inner::-webkit-scrollbar-thumb {
  218.         border-radius: 3px;
  219.         -moz-border-radius: 3px;
  220.         -webkit-border-radius: 3px;
  221.         background-color: #c3c3c3;
  222.       }
  223.       .el-textarea__inner::-webkit-scrollbar-track {
  224.         background-color: transparent;
  225.       }
  226.     }
  227.     .icon-container {
  228.       position: absolute;
  229.       right: 10px;
  230.       bottom: 35px;
  231.       z-index: 999;
  232.       .loading-icon {
  233.         width: 40px;
  234.         height: 40px;
  235.       }
  236.       .send-icon {
  237.         width: 35px;
  238.         height: 35px;
  239.       }
  240.     }
  241.   }
  242. }
  243. .markdown-body {
  244.   box-sizing: border-box;
  245.   max-width: 1021px !important;
  246.   hr {
  247.     display: none !important;
  248.   }
  249. }
复制代码




免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

去皮卡多

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表