Browse Source

feat: 专家问答

sunxiao 9 months ago
parent
commit
436ab4e0f4

+ 83 - 0
package-lock.json

@@ -8,8 +8,12 @@
       "name": "temp-vue-3",
       "version": "0.0.0",
       "dependencies": {
+        "@traptitech/markdown-it-katex": "^3.6.0",
         "axios": "^1.6.8",
+        "highlight.js": "^11.9.0",
         "load-awesome": "^1.1.0",
+        "markdown-it": "^14.1.0",
+        "markdown-it-link-attributes": "^4.0.1",
         "naive-ui": "^2.38.2",
         "pinia": "^2.1.7",
         "pinia-plugin-persistedstate": "^3.2.1",
@@ -1340,6 +1344,14 @@
         "win32"
       ]
     },
+    "node_modules/@traptitech/markdown-it-katex": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmmirror.com/@traptitech/markdown-it-katex/-/markdown-it-katex-3.6.0.tgz",
+      "integrity": "sha512-CnJzTWxsgLGXFdSrWRaGz7GZ1kUUi8g3E9HzJmeveX1YwVJavrKYqysktfHZQsujdnRqV5O7g8FPKEA/aeTkOQ==",
+      "dependencies": {
+        "katex": "^0.16.0"
+      }
+    },
     "node_modules/@trysound/sax": {
       "version": "0.2.0",
       "resolved": "https://registry.npmmirror.com/@trysound/sax/-/sax-0.2.0.tgz",
@@ -1732,6 +1744,11 @@
       "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
       "dev": true
     },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+    },
     "node_modules/arr-diff": {
       "version": "4.0.0",
       "resolved": "https://registry.npmmirror.com/arr-diff/-/arr-diff-4.0.0.tgz",
@@ -3981,6 +3998,25 @@
         "graceful-fs": "^4.1.6"
       }
     },
+    "node_modules/katex": {
+      "version": "0.16.10",
+      "resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.10.tgz",
+      "integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==",
+      "dependencies": {
+        "commander": "^8.3.0"
+      },
+      "bin": {
+        "katex": "cli.js"
+      }
+    },
+    "node_modules/katex/node_modules/commander": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz",
+      "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
     "node_modules/kind-of": {
       "version": "5.1.0",
       "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-5.1.0.tgz",
@@ -4011,6 +4047,14 @@
       "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
       "dev": true
     },
+    "node_modules/linkify-it": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.0.tgz",
+      "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
+      "dependencies": {
+        "uc.micro": "^2.0.0"
+      }
+    },
     "node_modules/load-awesome": {
       "version": "1.1.0",
       "resolved": "https://registry.npmmirror.com/load-awesome/-/load-awesome-1.1.0.tgz",
@@ -4090,12 +4134,38 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/markdown-it": {
+      "version": "14.1.0",
+      "resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.1.0.tgz",
+      "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
+      "dependencies": {
+        "argparse": "^2.0.1",
+        "entities": "^4.4.0",
+        "linkify-it": "^5.0.0",
+        "mdurl": "^2.0.0",
+        "punycode.js": "^2.3.1",
+        "uc.micro": "^2.1.0"
+      },
+      "bin": {
+        "markdown-it": "bin/markdown-it.mjs"
+      }
+    },
+    "node_modules/markdown-it-link-attributes": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmmirror.com/markdown-it-link-attributes/-/markdown-it-link-attributes-4.0.1.tgz",
+      "integrity": "sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ=="
+    },
     "node_modules/mdn-data": {
       "version": "2.0.14",
       "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.14.tgz",
       "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
       "dev": true
     },
+    "node_modules/mdurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-2.0.0.tgz",
+      "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="
+    },
     "node_modules/memorystream": {
       "version": "0.3.1",
       "resolved": "https://registry.npmmirror.com/memorystream/-/memorystream-0.3.1.tgz",
@@ -4983,6 +5053,14 @@
       "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
       "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
     },
+    "node_modules/punycode.js": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz",
+      "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/query-string": {
       "version": "4.3.4",
       "resolved": "https://registry.npmmirror.com/query-string/-/query-string-4.3.4.tgz",
@@ -6449,6 +6527,11 @@
         "node": ">=14.17"
       }
     },
+    "node_modules/uc.micro": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz",
+      "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="
+    },
     "node_modules/unbox-primitive": {
       "version": "1.0.2",
       "resolved": "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz",

+ 4 - 0
package.json

@@ -11,8 +11,12 @@
     "type-check": "vue-tsc --build --force"
   },
   "dependencies": {
+    "@traptitech/markdown-it-katex": "^3.6.0",
     "axios": "^1.6.8",
+    "highlight.js": "^11.9.0",
     "load-awesome": "^1.1.0",
+    "markdown-it": "^14.1.0",
+    "markdown-it-link-attributes": "^4.0.1",
     "naive-ui": "^2.38.2",
     "pinia": "^2.1.7",
     "pinia-plugin-persistedstate": "^3.2.1",

+ 4 - 1
src/api/chat.js

@@ -9,7 +9,10 @@ export const chatApi = {
    * 2 智能体助手
    * 3 告警
    */
-  getAnswerHistoryList: params => http.get('/front/bigModel/qa/pageList', { params })
+  getAnswerHistoryList: params => http.get('/front/bigModel/qa/pageList', { params }),
+
+  getAnswerHistoryDetail: params => http.get('/front/bigModel/qa/qaListBySessionId', { params }),
+
 }
 
 

+ 121 - 0
src/components/Chat/ChatAnswer.vue

@@ -0,0 +1,121 @@
+<script setup>
+import { computed, ref } from 'vue';
+import MarkdownIt from 'markdown-it';
+import hljs from 'highlight.js';
+import mila from 'markdown-it-link-attributes';
+import mdKatex from '@traptitech/markdown-it-katex';
+import { SvgIcon } from '@/components';
+
+const props = defineProps({
+  content: {
+    type: String,
+    default: ''
+  },
+  loading: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const highlightBlock = (str, lang) => {
+  return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">${t('chat.copyCode')}</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`
+}
+
+const mdi = new MarkdownIt({
+  html: true,
+  linkify: true,     // 将类似 URL 的文本自动转换为链接。
+  // breaks: true,
+  typographer: true,
+  highlight(code, language) {
+    const validLang = !!(language && hljs.getLanguage(language))
+    if (validLang) {
+      const lang = language ?? ''
+      return highlightBlock(hljs.highlight(code, { language: lang }).value, lang)
+    }
+    return highlightBlock(hljs.highlightAuto(code).value, '')
+  },
+})
+
+mdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } })
+mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' })
+
+const text = computed(() => {
+  const value = props.content ?? ""
+  if (!props.asRawText)
+    return mdi.render(value)
+  return value
+})
+
+</script>
+
+<template>
+  <div class="answer-inner">
+    <div class="answer-card">
+      <div class="chat-answer_icon relative">
+        <SvgIcon name="common-logo" class="chat-logo" size="30" :style="{ scale: loading ? 0 : 1 }"/>
+        <div style="color: #2454FF" class="la-ball-circus la-dark la-sm" v-show="loading">
+          <div v-for="item in 5" :key="item"></div>
+        </div>
+      </div>
+      <div class="flex-1 pt-[4px] ml-[16px] text-[15px] leading-[24px]">
+        <p class="font-bold text-[#1A2029]" v-if="loading">内容生成中...</p>
+        <p class="flex-1 pt-[6px] ml-[16px] text-[15px] leading-[24px]" v-html="text" v-else></p>
+      </div>
+    </div>
+    <ul class="answer-btn-group">
+      <li class="btn">
+        <SvgIcon name="chat-icon-copy" size="16" />
+      </li>
+      <li class="line"></li>
+      <li class="btn">
+        <SvgIcon name="chat-icon-yes" size="16" />
+      </li>
+      <li class="line"></li>
+      <li class="btn">
+        <SvgIcon name="chat-icon-no" size="16" />
+      </li>
+    </ul>
+  </div>
+
+</template>
+
+
+<style lang="scss">
+.chat-logo {
+  position: absolute;
+  transition: all 1s;
+}
+.answer-inner {
+  margin-bottom: 20px;
+
+  .answer-card {
+    @include flex(x, start, start);
+    padding: 20px;
+    border-radius: 8px;
+    background: #fff;
+  }
+
+  .answer-btn-group {
+    @include flex(x, center, end);
+    padding-top: 6px;
+
+    .btn {
+      @include flex(x, center, center);
+      @include layout(28px, 28px, 4px);
+      color: #89909B;
+      cursor: pointer;
+
+      &:hover {
+        background: #DBEFFF;
+        color: #2454FF;
+      }
+    }
+
+    .line {
+      @include layout(1px, 12px, 0);
+      margin: 0 5px;
+      background: #D3D0E1;
+    }
+  }
+}
+</style>

+ 20 - 0
src/components/Chat/ChatAsk.vue

@@ -0,0 +1,20 @@
+<script setup>
+import { SvgIcon } from "@/components";
+
+defineProps({
+  content: {
+    type: String,
+    default: ''
+  }
+})
+
+</script>
+
+<template>
+  <div class="ask-inner flex items-start justify-start pl-[20px] mb-[20px]">
+    <div class="chat-ask_icon">
+      <SvgIcon name="chat-avatar" size="20" />
+    </div>
+    <p class="flex-1 pt-[6px] ml-[16px] text-[15px] font-bold leading-[24px]" v-html="content"></p>
+  </div>
+</template>

+ 115 - 0
src/components/Chat/ChatInput.vue

@@ -0,0 +1,115 @@
+<script setup>
+import { ref, unref } from 'vue';
+import { useMessage, NInput, NSwitch } from 'naive-ui';
+import SvgIcon from '@/components/SvgIcon';
+
+import 'load-awesome/css/ball-running-dots.min.css';
+
+const emit = defineEmits(['onClick', 'onEnter']);
+const modelLoading = defineModel('loading');
+
+const message = useMessage();
+
+const inpVal = ref(null);
+const inpRef = ref(null);
+
+const isFocusState = ref(false);
+
+const focusInput = _ => isFocusState.value = true;
+
+const blurInput = _ => isFocusState.value = false;
+
+const handleInpFocus = () => {
+  inpRef.value?.focus();
+}
+
+const commonEmitEvent = (eventName) => {
+  const val = unref(inpVal);
+  const len = val.trim().length;
+
+  if ( !len ) {
+    return message.warning('请输入您的问题或需求');
+  }
+
+  if ( len > 500 ) {
+    return message.warning('问题限制500个字以内');
+  }
+
+  if ( modelLoading.value ) {
+    return message.warning('当前对话进行中');
+  }
+
+  emit(eventName, val);
+
+  inpVal.value = '';
+}
+
+const handleInpEnter = (event) => {
+  if (event.key === 'Enter' && !event.shiftKey) {
+    event.preventDefault()
+    commonEmitEvent('onEnter')
+  }
+}
+
+const handleBtnClick = () => {
+  commonEmitEvent("onClick")
+} 
+
+</script>
+<template>
+  <div class="chat-inp-inner border-[1px]" :class="[{ 'border-[#2454FF]': isFocusState }]">
+    <div class="inp-wrapper flex-1" @click="handleInpFocus">
+      <NInput 
+        class="flex-1" 
+        ref="inpRef" 
+        type="textarea" 
+        size="medium"
+        placeholder="输入您的问题或需求,Enter发送,Shift+Enter换行"
+        v-model:value="inpVal" 
+        :autosize="{ minRows: 1, maxRows: 5 }"
+        @focus="focusInput"
+        @blur="blurInput"
+        @keypress="handleInpEnter"
+      />
+    </div>
+    <div class="submit-btn">
+      <button class="btn bg-[#1A2029] hover:bg-[#3C4148]" @click="handleBtnClick">
+        <SvgIcon name="tool-send-plane" size="22" v-show="!modelLoading"></SvgIcon>
+        <div style="color: #fff" class="la-ball-running-dots la-sm" v-show="modelLoading">
+          <div v-for="item in 5" :key="item"></div>
+        </div>
+      </button>
+    </div>
+  </div>
+  <div class="switch-inner pt-[8px] space-x-[6px]">
+    <NSwitch size="small"></NSwitch>
+    <span class="text-[12px] text-[#9E9E9E]">使用搜索增强</span>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.chat-inp-inner {
+  position: relative;
+  @include flex(x, center, between);
+  border-radius: 8px;
+  background: #fff;
+  box-shadow: 0px 3px 12px 0px #97D3FF40;
+
+  .inp-wrapper {
+    padding: 17px 0px 17px 34px;
+  }
+
+  .submit-btn {
+    @include flex(x, center, center);
+    width: 84px;
+
+    .btn {
+      @include flex(x, center, center);
+      width: 50px;
+      height: 32px;
+      border-radius: 32px;
+      transition: all .3s;
+    }
+  }
+}
+</style>

+ 9 - 0
src/components/Chat/index.js

@@ -0,0 +1,9 @@
+import ChatAsk from './ChatAsk';
+import ChatAnswer from './ChatAnswer';
+import ChatInput from './ChatInput';
+
+export { 
+  ChatAsk,
+  ChatAnswer,
+  ChatInput
+};

+ 1 - 1
src/components/ChatWelcome/index.vue

@@ -1,4 +1,5 @@
 <script setup>
+import { SvgIcon } from '@/components'
 
 defineProps({
   title: {
@@ -18,7 +19,6 @@ defineProps({
     default: []
   }
 });
-
 </script>
 
 <template>

+ 3 - 1
src/components/Layout/TheChatView.vue

@@ -4,6 +4,7 @@ import { NSelect } from 'naive-ui';
 import SvgIcon from '@/components/SvgIcon';
 import BasePopover from "@/components/BasePopover";
 
+const targetScrollDom = ref(null);
 const selectValue = ref('water');
 const voiceSwitchStatus = ref(false);
 
@@ -20,6 +21,7 @@ const changeVoiceStatus = () => {
   voiceSwitchStatus.value = !voiceSwitchStatus.value;
 }
 
+defineExpose({ targetScrollDom });
 </script>
 
 <template>
@@ -46,7 +48,7 @@ const changeVoiceStatus = () => {
         </div>
       </div>
       <main class="chat-main w-[800px] h-full m-auto flex flex-col justify-between">
-        <div class="chat-scroll">
+        <div class="chat-scroll" ref="targetScrollDom">
           <slot></slot>
         </div>
 

+ 30 - 8
src/components/RecodeCardItem/index.vue

@@ -3,7 +3,22 @@ import { ref } from 'vue';
 import { NPopconfirm, useMessage  } from 'naive-ui';
 import { SvgIcon } from '@/components';
 
-defineProps(['question', 'createTime']);
+const props = defineProps({
+  title: {
+    type: String,
+    default: ''
+  },
+  time: {
+    type: String,
+    default: ''
+  },
+  dataItem: {
+    type: Object,
+    default: {}
+  }
+});
+
+const emit = defineEmits(['on-delete', 'on-click']);
 
 const message = useMessage();
 
@@ -11,24 +26,31 @@ const handlePositiveClick = () => {
   message.success('删除成功');
 }
 
+const handleCardClick = () => {
+  emit('on-click', props.dataItem);
+}
+
+const handleDelete = () => {
+  console.log('点击了 del');
+}
 </script>
 
 <template>
-  <div class="recode-card-item">
-    <p class="content">{{ question }}</p>
+  <div class="recode-card-item" @click="handleCardClick">
+    <p class="content">{{ title }}</p>
     <p class="time flex item-center justify-between w-full mt-[2px]">
-      <span>{{ createTime }}</span>
-      <NPopconfirm
+      <span>{{ time }}</span>
+      <SvgIcon name="tool-bucket-del" size="16" class="del-icon cursor-pointer hidden" @click.stop="handleDelete"></SvgIcon>
+      <!-- <NPopconfirm
         negative-text="取消"
         positive-text="删除"
         @positive-click="handlePositiveClick"
       >
-      <!-- :on-update:show="" -->
         <template #trigger>
-          <SvgIcon name="tool-bucket-del" size="16" class="del-icon cursor-pointer hidden"></SvgIcon>
+          
         </template>
         删除后无法恢复,是否继续删除?
-      </NPopconfirm>
+      </NPopconfirm> -->
     </p>
   </div>
 </template>

+ 4 - 2
src/components/index.js

@@ -11,9 +11,11 @@ import ThePublicLayout from './Layout/ThePublicLayout';
 import TheSubMenu from './Layout/TheSubMenu';
 
 import RecodeCardItem from './RecodeCardItem';
-import ChatWelcome from "./ChatWelcome";
+
 import SvgIcon from './SvgIcon';
 
+import ChatWelcome from "./ChatWelcome";
+
 export {
 
   BaseButton,
@@ -29,7 +31,7 @@ export {
   TheSubMenu,
 
   RecodeCardItem,
-  ChatWelcome,
   SvgIcon,
+  ChatWelcome,
 
 }

+ 16 - 0
src/composables/useChat.js

@@ -0,0 +1,16 @@
+import { ref } from 'vue';
+
+export const useChat = () => {
+  const chatDataSource = ref([]);
+  const isLoading = ref(false);
+  const activeSection = ref(null);
+
+  const addChat = params => {
+    chatDataSource.value.push(params);
+  }
+
+  return {
+    chatDataSource,
+    addChat
+  }
+}

+ 4 - 11
src/composables/useInfinite.js

@@ -1,11 +1,10 @@
 import { ref, unref, onMounted } from "vue";
 import { chatApi } from '@/api/chat';
 
-export const useInfinite = (props) => {
+export const useInfinite = props => {
   const pageParams = { page: 1, pageSize: 10 };
 
   const recordList = ref([]);
-  const counter = ref(0);
   const isFetching = ref(false);
   const noMore = ref(false);
 
@@ -16,24 +15,18 @@ export const useInfinite = (props) => {
 
     const { rows, total } = await chatApi.getAnswerHistoryList({ ...props, ...pageParams });
     
-    recordList.value.push(rows);
+    recordList.value.push(...rows);
 
     if (pageParams.page * pageParams.pageSize < total) {
       pageParams.page ++;
     } else {
-      noMore = true;
+      noMore.value = true;
     }
 
     isFetching.value = false;
   }
 
-  const getMore = async() => {
-    const { rows, total } = await chatApi.getAnswerHistoryList({ ...props, ...pageParams });
-    counter.value = total;
-    recordList.value.push(rows);
-  }
-
-  onMounted(() => getMore());
+  onMounted(() => onScrolltolower());
 
   return {
     recordList,

+ 23 - 0
src/composables/useScroll.js

@@ -0,0 +1,23 @@
+import { ref, nextTick } from "vue";
+
+export const useScroll = () => {
+  const scrollRef = ref(null);
+
+  const scrollToBottom = async () => {
+    await nextTick();
+    if (scrollRef.value) {
+      const scrollDom = scrollRef.value.targetScrollDom;
+      scrollDom.scrollTo({
+        top: scrollDom.scrollHeight,
+        left: 0,
+        behavior: 'smooth'
+      });
+    }
+  }
+
+  return {
+    scrollRef,
+    scrollToBottom
+  }
+
+}

+ 0 - 2
src/utils/request.ts

@@ -68,12 +68,10 @@ export class Request {
     });
 
     this.instance.interceptors.response.use(res => {
-      console.log("res", res);
       const { code } = res.data;
       // !success && showNotification("error", message);
       return code === 200 ? res.data : Promise.reject(res.data);
     }, (error: AxiosError) => {
-      console.log(error);
       const errorMessage = errorCode[error.response?.status as number] || error.message ||'未知错误';
       showNotification("error", errorMessage);
       return Promise.reject(error);

+ 0 - 1
src/views/analyse/WaterView.vue

@@ -3,7 +3,6 @@ import { ref, h } from 'vue';
 import { NTabs, NTab, NEllipsis, NModal, NInput  } from 'naive-ui';
 import {
   BaseButton,
-  BaseInput,
   BaseCard,
   BaseTable,
   ChatWelcome,

+ 69 - 99
src/views/answer/AnswerView.vue

@@ -1,22 +1,61 @@
 <script setup>
 import { ref, reactive,onMounted } from 'vue';
 import { NPopconfirm } from 'naive-ui';
-import { SvgIcon, BaseButton, BaseInput, RecodeCardItem, TheSubMenu, TheChatView } from '@/components';
-import { useInfinite } from '@/composables/useInfinite';
+import { SvgIcon, BaseButton, RecodeCardItem, TheSubMenu, TheChatView, ChatWelcome } from '@/components';
+import { ChatAsk, ChatAnswer, ChatInput } from '@/components/Chat';
 import { chatApi } from '@/api/chat';
 
+import { useInfinite } from '@/composables/useInfinite';
+import { useScroll } from '@/composables/useScroll';
+import { useChat } from '@/composables/useChat';
+
 // TODO: 如果这里的key不一样,将会在拆一层组件出来 - list
 const { recordList, isFetching, onScrolltolower } = useInfinite({model: 0});
+const { scrollRef, scrollToBottom } = useScroll();
+const { chatDataSource, addChat } = useChat();
+
+// chat数据源
+// const chatDataSource = ref([]);
+const isLoading = ref(false);
+const activeSection = ref(null);
 
 const switchModelState = ref(true);
 
 onMounted(async () => {
+  // scrollToBottom();
 })
 
 // 新建对话
 const handleCreateDialog = () => {
   console.log("handleCreateDialog");
 }
+
+// 查询对话详情
+const handleChatDetail = async ({ sessionId:sId }) => {
+  const { data } = await chatApi.getAnswerHistoryDetail({ sessionId:sId });
+  chatDataSource.value = data.map(({ createTime, sessionId, question, answer }) => ({ createTime, sessionId, question, answer }));
+  scrollToBottom();
+}
+
+// 提交问题
+const handleSubmit = inpVal => {
+
+  isLoading.value = true;
+
+  addChat({
+    sessionId: null,
+    question: inpVal,
+    answer: '',
+  })
+
+  setTimeout(() => {
+    isLoading.value = false;
+  }, 2000);
+
+  console.log("tempParams", tempParams);
+}
+
+
 </script>
 
 <template>
@@ -30,110 +69,41 @@ const handleCreateDialog = () => {
       </template>
 
       <div class="pr-[4px] text-[#5e5e5e]">
-        <RecodeCardItem v-for="item in recordList" :key="item.sessionId" v-bind="item" />
+        <RecodeCardItem
+          v-for="item in recordList"
+          :key="item.sessionId"
+          :title="item.question"
+          :time="item.createTime"
+          :data-item="item"
+          @on-click="handleChatDetail"
+        />
       </div>
 
     </TheSubMenu>
 
-    <TheChatView>
-      <div class="chat-welcome" v-show="switchModelState">
-        <div class="
-          welcome
-          flex flex-col items-center justify-between
-          text-center
-        ">
-          <SvgIcon name="common-logo" size="56"></SvgIcon>
-          <p class="py-[10px] text-[#1A2029] text-[36px] font-bold leading-[50px]">您好,我是LibraAI专家问答</p>
-          <p class="text-[#333333] leading-[20px]">期待与您一同规划和完成未来的工作。有任何重点或需讨论的事项,随时告诉我。</p>
-        </div>
-        <dl class="answer-list rounded-[8px] bg-white py-[30px] pl-[82px] mt-[36px]">
-          <dt class="mb-[18px] text-[20px] text-[#1A2029] leading-[28px] font-bold">您可以试着问我:</dt>
-          <dd class="mb-[19px] text-[15px] text-[#2E5CFF] leading-[21px] cursor-pointer hover:text-[#2E5CFF]/90">
-            帮我做一份如何快速入手污水处理厂的相关工作的学习计划?
-          </dd>
-          <dd class="mb-[19px] text-[15px] text-[#2E5CFF] leading-[21px] cursor-pointer hover:text-[#2E5CFF]/90">
-            硝化作用的速度快慢与哪些因素有关?</dd>
-          <dd class="text-[15px] text-[#2E5CFF] leading-[21px] cursor-pointer hover:text-[#2E5CFF]/90">污泥回流比如何计算?</dd>
-        </dl>
-      </div>
-
-      <div class="ask-inner" v-show="!switchModelState">
-        <div class="chat-ask_icon">
-          <SvgIcon name="chat-avatar" size="20" />
-        </div>
-        <p class="flex-1 pt-[6px] ml-[16px] text-[15px] font-bold leading-[24px]">帮我看一下COD现在的指标是什么,COD是什么意思?</p>
-      </div>
-
-      <div class="answer-inner">
-        <div class="answer-card">
-          <div class="chat-answer_icon">
-            <SvgIcon name="common-logo" size="30" />
-          </div>
-          <p class="flex-1 pt-[6px] ml-[16px] text-[15px] leading-[24px]">
-            COD,即化学需氧量,是衡量水中有机物质含量的重要指标。它反映了水中可氧化有机物的量,通常用来评估水体的污染程度。水中的有机物主要来源于工业废水、生活污水、农药残留等,这些有机物不仅会导致水质变差,还会对生物和人类健康产生负面影响。因此,通过测定COD值,可以了解水中有机污染物的含量,进而评估水体的污染程度。这对于制定环境保护政策、控制污染源、保障水资源安全等方面都具有重要的指导意义
-          </p>
-        </div>
-        <ul class="answer-btn-group">
-          <li class="btn">
-            <SvgIcon name="chat-icon-copy" size="16" />
-          </li>
-          <li class="line"></li>
-          <li class="btn">
-            <SvgIcon name="chat-icon-yes" size="16" />
-          </li>
-          <li class="line"></li>
-          <li class="btn">
-            <SvgIcon name="chat-icon-no" size="16" />
-          </li>
-        </ul>
+    <TheChatView ref="scrollRef">
+      <ChatWelcome title="您好,我是LibraAI专家问答" card-title="您可以试着问我:"
+        :sub-title="[
+          '期待与您一同规划和完成未来的工作。有任何重点或需讨论的事项,随时告诉我。'
+        ]" 
+        :card-content="[
+          '帮我做一份如何快速入手污水处理厂的相关工作的学习计划?',
+          '硝化作用的速度快慢与哪些因素有关?',
+          '污泥回流比如何计算?'
+        ]"
+        v-if="!chatDataSource.length"
+      />
+
+      <div class="conversation-item" v-if="chatDataSource.length">
+        <template v-for="item in chatDataSource" :key="item.sessionId">
+          <ChatAsk :content="item.question"></ChatAsk>
+          <ChatAnswer :content="item.answer" :loading="isLoading"></ChatAnswer>
+        </template>
       </div>
 
       <template #footer>
-        <BaseInput></BaseInput>
+        <ChatInput @on-click="handleSubmit" @on-enter="handleSubmit" v-model:loading="isLoading"></ChatInput>
       </template>
     </TheChatView>
-
   </section>
-</template>
-
-<style scoped lang="scss">
-
-.ask-inner {
-  @include flex(x, start, start);
-  padding-left: 20px;
-  margin-bottom: 20px;
-}
-
-.answer-inner {
-  margin-bottom: 20px;
-  .answer-card {
-    @include flex(x, start, start);
-    padding: 20px;
-    border-radius: 8px;
-    background: #fff;
-  }
-
-  .answer-btn-group {
-    @include flex(x, center, end);
-    padding-top: 6px;
-
-    .btn {
-      @include flex(x, center, center);
-      @include layout(28px, 28px, 4px);
-      color: #89909B;
-      cursor: pointer;
-
-      &:hover {
-        background: #DBEFFF;
-        color: #2454FF;
-      }
-    }
-
-    .line {
-      @include layout(1px, 12px, 0);
-      margin: 0 5px;
-      background: #D3D0E1;
-    }
-  }
-}
-</style>
+</template>