소스 검색

feat: 水质报警

whh 10 달 전
부모
커밋
21ff40d0ed

+ 10 - 1
src/api/water.js

@@ -10,7 +10,16 @@ export const waterApi = {
   */
   getWaringList: params => http.get('/front/bigModel/warning/pageList', params),
   
-    
+  /**
+   * 预警详情
+  */
+  getWaringDetails: params => http.get('/front/bigModel/warning/qaDetailByWarningId/' + params),
+  
+  /**
+   * 回答 - flow
+  */
+  getWaterStream: ({ data, onDownloadProgress, signal }) => http.post('/grpc/decisionStream', data, { onDownloadProgress, signal, }),
+
   // getAnswerHistoryList: params => http.get('/front/bigModel/qa/pageList', { params }),
 
   // getAnswerHistoryDetail: params => http.get('/front/bigModel/qa/qaListBySessionId', { params }),

+ 1 - 1
src/assets/styles/github-markdown.scss

@@ -659,7 +659,7 @@ html {
 
 .markdown-body table th,
 .markdown-body table td {
-  padding: 6px 13px;
+  padding: 6px 6px;
   border: 1px solid var(--color-border-default);
 }
 

+ 2 - 2
src/components/BaseTable/index.vue

@@ -22,7 +22,7 @@ const dataTableThemeOverrides: DataTableThemeOverrides = {
   borderRadius: '4px',
   borderColor: '#D7D7D7',
   thPaddingMedium: '8px 0px',
-  // tdPaddingMedium: '0px 10px',
+  tdPaddingMedium: '10px 0px',
   fontSizeMedium: '11px',
   emptyPadding: '0px'
 }
@@ -45,7 +45,7 @@ const rowClassName = (row) => {
 
 <style scoped lang="scss">
 :deep(.custom-row .small) {
-  color: red !important;
+  // color: red !important;
   line-height: 32px;
   padding-top: 0;
   padding-bottom: 0;

+ 184 - 0
src/components/Chat/ChatAnswer - source.vue

@@ -0,0 +1,184 @@
+<script setup>
+import { computed, unref } from 'vue';
+import { useMessage } from 'naive-ui';
+import { useClipboard } from '@vueuse/core'
+import MarkdownIt from 'markdown-it';
+import hljs from 'highlight.js';
+import mila from 'markdown-it-link-attributes';
+// import markdownItLatex from 'markdown-it-latex'
+// import mdKatex from '@traptitech/markdown-it-katex';
+import mdKatex from 'markdown-it-katex';
+import { SvgIcon } from '@/components';
+import { chatApi } from "@/api/chat"
+
+// import 'markdown-it-latex/dist/index.css'
+
+const props = defineProps({
+  id: {
+    type: [String, Number],
+    default: ''
+  },
+  content: {
+    type: String,
+    default: ''
+  },
+  loading: {
+    type: Boolean,
+    default: false
+  },
+  delayLoading: {
+    type: Boolean,
+    default: false
+  },
+  isSatisfied: {
+    type: Number,
+    default: 2
+  }
+})
+
+const emit = defineEmits(['on-click-icon']);
+
+const { copy } = useClipboard();
+const message = useMessage();
+
+function highlightBlock(str, lang) {
+  return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__copy"></span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`
+}
+
+const mdi = new MarkdownIt({
+  html: true,
+  linkify: true,
+  // 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' })
+// mdi.use(markdownItLatex)
+
+const text = computed(() => {
+  const value = props.content ?? ""
+  if (!props.asRawText)
+    return mdi.render(value)
+  return value
+})
+
+const handlLeToggleLike = async (state) => {
+  const { id } = unref(props);
+  const isSatisfied = props.isSatisfied === state ? 2 : state;
+  const params = { id, isSatisfied };
+  
+  await chatApi.putIsSatisfiedAnswer(params);
+
+  isSatisfied < 2 ? message.success('感谢您的反馈') : message.success('已取消反馈');
+
+  emit('on-click-icon', params)
+}
+
+const handleCopy = () => {
+  copy(props.content).then(() => {
+    message.success('复制成功');
+  })
+}
+</script>
+
+<template>
+  <div class="answer-inner">
+    <div :class="[ 'answer-card', 'px-[20px]', 'py-[20px]']">
+      <div class="chat-answer_icon relative flex-shrink-0">
+        <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 flex-shrink-0" v-show="loading">
+          <div v-for="item in 5" :key="item"></div>
+        </div>
+      </div>
+      <div class="flex-1 pt-[4px] ml-[16px] text-[15px]">
+        <template v-if="loading && delayLoading">
+          <p class="font-bold text-[#1A2029] leading-[24px]">内容生成中...</p>
+        </template>
+        <div class="markdown-body text-[15px]" v-if="content">
+          <div v-html="text"></div>
+        </div>
+        <!-- <template> -->
+          <slot></slot>
+        <!-- </template> -->
+      </div>
+    </div>
+    <ul class="answer-btn-group" v-if="!loading">
+      <li class="btn" @click="handleCopy">
+        <SvgIcon name="chat-icon-copy" size="16" />
+      </li>
+      <li class="line"></li>
+      <li :class="['btn', { btn_active: isSatisfied == 1 }]">
+        <SvgIcon name="chat-icon-yes" size="16" @click="handlLeToggleLike(1)" />
+      </li>
+      <li class="line"></li>
+      <li :class="['btn', { btn_active: isSatisfied == 0 }]">
+        <SvgIcon name="chat-icon-no" size="16" @click="handlLeToggleLike(0)" />
+      </li>
+    </ul>
+  </div>
+
+</template>
+
+<style lang="scss">
+.markdown-body p:last-child {
+  margin-bottom: 0;
+}
+
+.markdown-body  {
+  p:last-child {
+    margin-bottom: 0;
+  }
+  img {
+    margin-top: 20px;
+  }
+}
+
+.chat-logo {
+  position: absolute;
+  transition: all 1s;
+}
+
+.answer-inner {
+  margin-bottom: 20px;
+
+  .answer-card {
+    @include flex(x, start, start);
+    // padding: 20px 20px 4px 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, &_active {
+        background: #DBEFFF;
+        color: #2454FF;
+      }
+
+    }
+
+    .line {
+      @include layout(1px, 12px, 0);
+      margin: 0 5px;
+      background: #D3D0E1;
+    }
+  }
+}
+</style>

+ 42 - 48
src/components/Chat/ChatAnswer.vue

@@ -1,17 +1,12 @@
 <script setup>
-import { computed, unref } from 'vue';
+import { unref } from 'vue';
 import { useMessage } from 'naive-ui';
 import { useClipboard } from '@vueuse/core'
-import MarkdownIt from 'markdown-it';
-import hljs from 'highlight.js';
-import mila from 'markdown-it-link-attributes';
-// import markdownItLatex from 'markdown-it-latex'
-// import mdKatex from '@traptitech/markdown-it-katex';
-import mdKatex from 'markdown-it-katex';
 import { SvgIcon } from '@/components';
 import { chatApi } from "@/api/chat"
+import ChatBaseCard from './ChatBaseCard';
+import ChatText from './ChatText';
 
-// import 'markdown-it-latex/dist/index.css'
 
 const props = defineProps({
   id: {
@@ -33,49 +28,24 @@ const props = defineProps({
   isSatisfied: {
     type: Number,
     default: 2
+  },
+  toggleVisibleIcons: {
+    type: Boolean,
+    default: true
   }
 })
 
 const emit = defineEmits(['on-click-icon']);
 
-const { copy } = useClipboard();
 const message = useMessage();
 
-function highlightBlock(str, lang) {
-  return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__copy"></span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`
-}
-
-const mdi = new MarkdownIt({
-  html: true,
-  linkify: true,
-  // 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' })
-// mdi.use(markdownItLatex)
-
-const text = computed(() => {
-  const value = props.content ?? ""
-  if (!props.asRawText)
-    return mdi.render(value)
-  return value
-})
+const { copy } = useClipboard();
 
 const handlLeToggleLike = async (state) => {
   const { id } = unref(props);
   const isSatisfied = props.isSatisfied === state ? 2 : state;
   const params = { id, isSatisfied };
-  
+
   await chatApi.putIsSatisfiedAnswer(params);
 
   isSatisfied < 2 ? message.success('感谢您的反馈') : message.success('已取消反馈');
@@ -91,7 +61,31 @@ const handleCopy = () => {
 </script>
 
 <template>
-  <div class="answer-inner">
+  <ChatBaseCard class="answer-inner" :loading="loading" :delayLoading="delayLoading">
+
+    <template #text>
+      <ChatText :content="content"></ChatText>
+    </template>
+
+    <template #button>
+      <ul class="answer-btn-group" v-if="!loading && toggleVisibleIcons">
+        <li class="btn" @click="handleCopy">
+          <SvgIcon name="chat-icon-copy" size="16" />
+        </li>
+        <li class="line"></li>
+        <li :class="['btn', { btn_active: isSatisfied == 1 }]">
+          <SvgIcon name="chat-icon-yes" size="16" @click="handlLeToggleLike(1)" />
+        </li>
+        <li class="line"></li>
+        <li :class="['btn', { btn_active: isSatisfied == 0 }]">
+          <SvgIcon name="chat-icon-no" size="16" @click="handlLeToggleLike(0)" />
+        </li>
+      </ul>
+    </template>
+
+  </ChatBaseCard>
+
+  <!-- <div class="answer-inner">
     <div :class="[ 'answer-card', 'px-[20px]', 'py-[20px]']">
       <div class="chat-answer_icon relative flex-shrink-0">
         <SvgIcon name="common-logo" class="chat-logo " size="30" :style="{ scale: loading ? 0 : 1 }" />
@@ -103,12 +97,10 @@ const handleCopy = () => {
         <template v-if="loading && delayLoading">
           <p class="font-bold text-[#1A2029] leading-[24px]">内容生成中...</p>
         </template>
-          <div class="markdown-body text-[15px]" v-if="content">
-            <div v-html="text"></div>
-          </div>
-        <template>
+        <div class="markdown-body text-[15px]" v-if="content">
+          <div v-html="text"></div>
+        </div>
           <slot></slot>
-        </template>
       </div>
     </div>
     <ul class="answer-btn-group" v-if="!loading">
@@ -124,7 +116,7 @@ const handleCopy = () => {
         <SvgIcon name="chat-icon-no" size="16" @click="handlLeToggleLike(0)" />
       </li>
     </ul>
-  </div>
+  </div> -->
 
 </template>
 
@@ -133,10 +125,11 @@ const handleCopy = () => {
   margin-bottom: 0;
 }
 
-.markdown-body  {
+.markdown-body {
   p:last-child {
     margin-bottom: 0;
   }
+
   img {
     margin-top: 20px;
   }
@@ -167,7 +160,8 @@ const handleCopy = () => {
       color: #89909B;
       cursor: pointer;
 
-      &:hover, &_active {
+      &:hover,
+      &_active {
         background: #DBEFFF;
         color: #2454FF;
       }

+ 110 - 0
src/components/Chat/ChatBaseCard.vue

@@ -0,0 +1,110 @@
+<script setup>
+import { computed, unref } from 'vue';
+import { useMessage } from 'naive-ui';
+import { useClipboard } from '@vueuse/core'
+import MarkdownIt from 'markdown-it';
+import hljs from 'highlight.js';
+import mila from 'markdown-it-link-attributes';
+import mdKatex from 'markdown-it-katex';
+
+import { SvgIcon } from '@/components';
+import { chatApi } from "@/api/chat"
+
+
+const props = defineProps({
+  id: {
+    type: [String, Number],
+    default: ''
+  },
+  content: {
+    type: String,
+    default: ''
+  },
+  loading: {
+    type: Boolean,
+    default: false
+  },
+  delayLoading: {
+    type: Boolean,
+    default: false
+  },
+  isSatisfied: {
+    type: Number,
+    default: 2
+  },
+  toggleVisibleIcons: {
+    type: Boolean,
+    default: false
+  },
+})
+
+</script>
+
+<template>
+  <div class="answer-inner">
+    <div :class="[ 'answer-card', 'px-[20px]', 'py-[20px]']">
+
+      <div class="chat-answer_icon relative flex-shrink-0">
+        <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 flex-shrink-0" v-show="loading">
+          <div v-for="item in 5" :key="item"></div>
+        </div>
+      </div>
+
+      <div class="flex-1 pt-[4px] ml-[16px] text-[15px]">
+
+        <template v-if="loading && delayLoading">
+          <p class="font-bold text-[#1A2029] leading-[24px]">内容生成中...</p>
+        </template>
+
+        <slot name="text"></slot>
+
+        <slot></slot>
+
+      </div>
+    </div>
+
+    <slot name="button"/>
+  </div>
+
+</template>
+
+<style lang="scss">
+.chat-logo {
+  position: absolute;
+  transition: all 1s;
+}
+
+.answer-inner {
+  margin-bottom: 20px;
+
+  .answer-card {
+    @include flex(x, start, start);
+    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, &_active {
+        background: #DBEFFF;
+        color: #2454FF;
+      }
+    }
+
+    .line {
+      @include layout(1px, 12px, 0);
+      margin: 0 5px;
+      background: #D3D0E1;
+    }
+  }
+}
+</style>

+ 77 - 0
src/components/Chat/ChatText.vue

@@ -0,0 +1,77 @@
+<script setup>
+import { computed } from 'vue';
+import MarkdownIt from 'markdown-it';
+import hljs from 'highlight.js';
+import mila from 'markdown-it-link-attributes';
+import mdKatex from 'markdown-it-katex';
+
+const props = defineProps({
+  content: {
+    type: String,
+    default: ''
+  }
+})
+
+const highlightBlock = (str, lang) => {
+  return `
+  <pre class="code-block-wrapper">
+    <div class="code-block-header">
+      <span class="code-block-header__copy"></span>
+    </div>
+    <code class="hljs code-block-body ${lang}">${str}</code>
+  </pre>`
+}
+
+const mdi = new MarkdownIt({
+  html: true,
+  linkify: true,
+  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="markdown-body text-[15px] break-all" v-if="content">
+    <div v-html="text"></div>
+  </div>
+</template>
+
+<style>
+.markdown-body p:last-child {
+  margin-bottom: 0;
+}
+
+.markdown-body  {
+
+  p:last-child {
+    margin-bottom: 0;
+  }
+
+  img {
+    margin-top: 20px;
+  }
+
+  table {
+    font-size: 12px;
+  }
+}
+</style>

+ 4 - 1
src/components/Chat/index.js

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

+ 8 - 49
src/components/Layout/TheChatView.vue

@@ -2,10 +2,14 @@
 import { ref, unref, computed, onMounted } from 'vue';
 import { NSelect, NDropdown, NPopover } from 'naive-ui';
 import { SvgIcon, BasePopover } from '@/components';
-import { userApi } from '@/api/user';
-
 import TheUserAvatar from './TheUserAvatar.vue';
 
+defineProps({
+  isFooter: {
+    type: Boolean,
+    default: true
+  }
+})
 
 const userInfo = ref({});
 
@@ -41,29 +45,6 @@ const changeVoiceStatus = () => {
   voiceSwitchStatus.value = !voiceSwitchStatus.value;
 }
 
-const handleUserMenuOptions = (key) => {
-  // switch (key) {
-  //   case 'copyText':
-  //     copyText({ text: props.text ?? '' })
-  //     return
-  //   case 'toggleRenderType':
-  //     asRawText.value = !asRawText.value
-  //     return
-  //   case 'delete':
-  //     emit('delete')
-  // }
-}
-
-// onMounted(async () => {
-//   try {
-//     const { user } = await userApi.getUserInfo()
-//     userInfo.value = user;
-//   }
-//   catch (error) {
-//     console.log("err", error);
-//   }
-// })
-
 defineExpose({ targetScrollDom });
 </script>
 
@@ -86,37 +67,15 @@ defineExpose({ targetScrollDom });
           :consistent-menu-width="false" />
         <!-- 用户头像 -->
         <TheUserAvatar></TheUserAvatar>
-        <!-- <NPopover trigger="click" raw :show-arrow="false" style="box-shadow: 0px 10px 31px 0px #B1B6B933;">
-          <template #trigger>
-            <div class="flex items-center cursor-pointer">
-              <img src="@/assets/images/chat/img-user-avatar.png" alt="" class="w-[32px] mr-[10px]" />
-              <span class="text-[#272D35] text-[12px]">{{ userInfo.nickName }}</span>
-            </div>
-          </template>
-          <ul class="
-            w-[120px] px-[10px] py-[10px]
-            rounded-[4px] bg-white
-            text-[12px] text-[#272D35] leading-[30px] text-center
-          ">
-            <li 
-              class="h-[30px] rounded-[4px] hover:bg-[#F4F6F8] hover:text-[#2454FF] cursor-pointer"
-              v-for="item in userMenuOptions" 
-              :key="item.key"
-              @click="handleUserMenuOptions"
-            >
-              {{ item.label }}
-            </li>
-          </ul>
-        </NPopover> -->
       </div>
-      <main class="chat-main h-full m-auto flex flex-col justify-between">
+      <main class="chat-main h-full m-auto flex flex-col justify-between" :style="{height: isFooter ? 'calc(100% - 212px)' : 'calc(100% - 100px)'}">
         <div class="chat-scroll" ref="targetScrollDom">
           <div class="w-[800px] m-auto pb-[20px]">
             <slot></slot>
           </div>
         </div>
       </main>
-      <footer class="chat-footer relative w-[800px] m-auto pb-[30px]">
+      <footer class="chat-footer relative w-[800px] m-auto pb-[30px]" v-if="isFooter">
         <slot name="footer" />
       </footer>
     </div>

+ 2 - 5
src/components/RecodeSquareCardItem/index.vue

@@ -1,5 +1,6 @@
 <script setup>
 import { ref, computed } from 'vue';
+import { NEllipsis } from "naive-ui";
 import { BaseButton, SvgIcon } from '@/components';
 
 const props = defineProps({
@@ -45,10 +46,6 @@ const dataSources = computed(() => {
       {label: '报警级别',   value: item.level},
       {label: '报警次数',   value: item.counts},
     ]
-    return {
-      ...props.item,
-      customWraningVal: props.item.warningVal,
-    }
   }
 });
 
@@ -65,7 +62,7 @@ const handleEmitParent = () => {
     </div>
     <dl class="warning-info">
       <dt>
-        <n-ellipsis class="font-bold text-[#1A2029] leading-[20px]">{{ item.reason }}</n-ellipsis>
+        <NEllipsis class="font-bold text-[#1A2029] leading-[20px]">{{ item.reason }}</NEllipsis>
       </dt>
       <dd class="flex items-center" v-for="(item, index) in dataSources" :key="index">
         <span>{{ item.label }}: {{ item.value }}</span>

+ 98 - 0
src/composables/useFetchStream.js

@@ -0,0 +1,98 @@
+import { ref, onUnmounted, watch } from 'vue';
+import { useUserStore } from '@/stores/modules/userStore';
+
+const url = import.meta.env.VITE_BASE_URL;
+const prefix = import.meta.env.VITE_BASE_PREFIX;
+const baseURL = url + prefix;
+
+export function useFetchStream(url, options = {}, immediate = true) {
+
+  const useStore = useUserStore();
+
+  const { token } = useStore.userInfo;
+
+  const streamData = ref("");
+  const completeData = ref([]);
+  const error = ref(null);
+  const loading = ref(false);
+
+  let abortController = null;
+ 
+  const headers = {
+    'Content-Type': 'application/json',
+    Authorization: 'Bearer ' + token
+  }
+
+  async function fetchData({ body, successHandler, doneHandler }) {
+    console.log(body, {body});
+    if (abortController) {
+      abortController.abort();
+    }
+
+    abortController = new AbortController();
+    options.signal = abortController.signal;
+    options.headers = headers
+
+    streamData.value = ''
+    completeData.value = []
+
+    try {
+      loading.value = true;
+
+      const response = await fetch(baseURL + url, {...options, method: 'POST', body });
+
+      if (!response.ok) {
+        throw new Error(`HTTP error! Status: ${response.status}`);
+      }
+
+      const reader = response.body.getReader();
+      const textDecoder = new TextDecoder();
+      let result = true;
+
+      while (result) {
+        const { done, value } = await reader.read();
+
+        if (done) {
+          console.log('Stream ended');
+          doneHandler && doneHandler(done);
+          result = false;
+          break;
+        }
+
+        const chunkText = textDecoder.decode(value);
+        completeData.value.push(chunkText);
+        streamData.value = chunkText;
+        successHandler && successHandler(chunkText);
+        console.log('Received chunk:', chunkText);
+      }
+    } catch (err) {
+      error.value = err;
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  if (immediate) {
+    fetchData();
+  }
+
+  onUnmounted(() => {
+    if (abortController) {
+      abortController.abort();
+    }
+  });
+
+  return {
+    streamData,
+    completeData,
+    error,
+    loading,
+    refetch: fetchData,
+    cancelFetch: () => {
+      if (abortController) {
+        abortController.abort();
+        abortController = null;
+      }
+    },
+  };
+}

+ 4 - 3
src/composables/useInfinite.js

@@ -44,10 +44,11 @@ export const useInfinite = (path, props) => {
     noMore.value = !unref(isMore);
   }
 
-  const onRestore = (params) => {
-    console.log( "params", params );
+  const onRestore = async (params) => {
     pageParams.value = { page: 1, pageSize: 20 };
-    initRecordData(params);
+    const { rows, total } = await initRecordData(params);
+    recordList.value = rows;
+    counter.value = total;
   }
 
   const initRecordData = async (params = {}) => {

+ 10 - 0
src/composables/useScroll.js

@@ -5,6 +5,15 @@ export const useScroll = () => {
 
   const targeScrolltDom = computed(() => scrollRef.value.targetScrollDom);
 
+  const scrollToTop = async () => {
+    await nextTick();
+    targeScrolltDom.value.scrollTo({
+      top: 0,
+      left: 0,
+      behavior: 'smooth'
+    });
+  };
+
   const scrollToBottom = async () => {
     await nextTick();
     if (scrollRef.value) {
@@ -29,6 +38,7 @@ export const useScroll = () => {
 
   return {
     scrollRef,
+    scrollToTop,
     scrollToBottom,
     scrollToBottomIfAtBottom
   }

+ 22 - 0
src/utils/format.js

@@ -0,0 +1,22 @@
+const formatTextData = (dataSource, whileList) => {
+
+
+
+}
+
+export const format = {
+  textSorting(dataSource, rule) {
+    const title = dataSource.title || dataSource['进水SS超标报警'];
+    const list = rule.map(item => {
+      Object.keys(dataSource).forEach(key => {
+        if (item.label === key) {
+          item.value = dataSource[key] + item.value
+        }
+      })
+      return item;
+    })
+
+    return { title, list };
+  },
+
+}

+ 2 - 1
src/utils/tools.ts

@@ -94,4 +94,5 @@ export function copyText(options: { text: string; origin?: boolean }) {
   if (document.execCommand('copy'))
     document.execCommand('copy')
   document.body.removeChild(input)
-}
+}
+

+ 231 - 52
src/views/analyse/WaterView.vue

@@ -1,23 +1,62 @@
 <script setup lang="jsx">
-import { ref, h } from 'vue';
-import { NTabs, NTab, NEllipsis, NModal, NInput  } from 'naive-ui';
+import { ref, unref, watch } from 'vue';
+import { NTabs, NTab } from 'naive-ui';
 import { BaseCard, BaseTable, ChatWelcome, SvgIcon, RecodeSquareCardItem, TheSubMenu, TheChatView } from "@/components";
+import { ChatAsk, ChatBaseCard, ChatAnswer } from '@/components/Chat';
 
-import { useInfinite } from '@/composables/useInfinite';
+import { format } from "@/utils/format";
+
+import { waterApi } from '@/api/water';
 import { CustomModal } from "./components";
 
+import { useInfinite } from '@/composables/useInfinite';
+import { useRecommend } from '@/composables/useRecommend';
+import { useFetchStream } from '@/composables/useFetchStream';
+import { useScroll } from '@/composables/useScroll';
+
+const { recommendList } = useRecommend({type: 1});
+const { scrollRef, scrollToTop, scrollToBottomIfAtBottom } = useScroll();
+const { streamData, refetch } = useFetchStream("/grpc/decisionStream", { methdos: 'POST' }, false);
 const { recordList, isFetching, onScrolltolower, onRestore, addHistoryRecord } = useInfinite('/front/bigModel/warning/pageList', { type: 0, warningStatus: 0 });
 
+let controller = new AbortController();
+
+// 获取最终回答流数据参数
+const flowParams = {
+  feedback: '',
+  category: '',
+  warningId: ''
+};
+
+const reportAnswer = ref('');
+const alertAnswer = ref([]);
+
+const checkedAlertData = ref([]);
+
+const textDataSources = ref(null);
+const answerAlertDataSources = ref([]);
+
+// 进出水数据
+const jsTableData = ref([]);
+const csTableData = ref([]);
+
 const visible = ref(false);
 
+const renderRowDom = ({ row, key }) => {
+  const { exceed, value } = row[key] || {};
+  const cls = exceed ? 'text-[#F44C49] font-bold' : 'text-[1A2029]'
+  return (<span class={ cls }>{value} {exceed && <i>↑</i>}</span>)
+} 
+
 const columns = [
   {
-    title: (<span class="text-[11px]">COD</span>),
+    title: '流量(m³/h)',
     key: 'name',
     titleAlign: 'center',
     align: 'center',
     className: 'small',
-    width: '80px'
+    width: '80px',
+    render: (row) => renderRowDom({ row, key: '流量' })
   },
   {
     title: 'COD(mg/L)',
@@ -25,7 +64,8 @@ const columns = [
     titleAlign: 'center',
     align: 'center',
     className: 'small',
-    width: '80px'
+    width: '80px',
+    render: (row) => renderRowDom({ row, key: 'COD' })
   },
   {
     title: 'TN(mg/L)',
@@ -33,39 +73,38 @@ const columns = [
     titleAlign: 'center',
     align: 'center',
     className: 'small',
-    width: '80px'
+    width: '80px',
+    render: (row) => renderRowDom({ row, key: 'TN' })
   },
   {
-    title: 'NH3 -N(mg/L)',
+    title: 'NH3-N(mg/L)',
     key: 'tags',
     titleAlign: 'center',
     align: 'center',
     className: 'small',
-    width: '80px'
+    width: '80px',
+    render: (row) => renderRowDom({ row, key: 'NH3-N' })
   },
   {
     title: '总磷TP(mg/L)',
-    key: 'actions',
+    key: 'COD',
     titleAlign: 'center',
     align: 'center',
     className: 'small',
     width: '80px',
-    render(row) {
-      // TODO: 需要调整,待后续请求参数确定
-      return (<span class={row.actions > 7 ? 'text-[#F44C49] font-bold' : 'text-[1A2029]'}>{row.actions} ↑</span>)
-    }
+    render: (row) => renderRowDom({ row, key: 'TP' })
   },
   {
     title: 'SS(mg/L)',
-    key: 'actions1',
+    key: '流量',
     titleAlign: 'center',
     align: 'center',
     className: 'age',
-    width: '78px'
+    width: '78px',
+    render: (row) => renderRowDom({ row, key: 'SS' })
   }
 ]
 
-
 const inWaterTableData = ref([{ name: 1233, actions: "7.87" }]);
 
 // 新建对话
@@ -81,14 +120,124 @@ const handleModelVisible = () => {
   visible.value = true
 } 
 
-const handleOpenContent = () => {
-  alert(1)
+/**
+ * 报警详情
+*/
+const handleOpenContent = async ({id, category}) => {
+  const { data } = await waterApi.getWaringDetails(id);
+  const showVal = JSON.parse(data.showVal);
+  const { basic, jsData, csData } = showVal;
+  const answer = JSON.parse(data.answer);
+  const [ answerStrItem ] = answer;
+  const answerObjItem = JSON.parse( answerStrItem );
+
+  console.log( answerObjItem.biz );
+
+  const textWhiteList = [
+    { label: '报警时间', value: '', isWarning: false },
+    { label: '报警值',   value: 'mg/L', isWarning: true },
+    { label: '管控值',   value: 'mg/L', isWarning: false },
+    { label: '标准值',   value: 'mg/L', isWarning: false },
+    { label: '报警级别', value: '', isWarning: false },
+    { label: '报警次数', value: '', isWarning: false },
+    { label: '状态',     value: '', isWarning: false }
+  ]
+
+  if ( answerObjItem.biz === "DECISION_REPORT" ) {
+    alertAnswer.value = [];
+    reportAnswer.value = answer.map(item => {
+      const itemParse = JSON.parse(item); 
+      return itemParse.message;
+    }).join();
+  } else {
+    reportAnswer.value = '';
+    const [ parseAnswer ] = answer.map(item => {
+      const result = JSON.parse( item );
+      result.message = Object.keys(result.message).map(key => ({ ...result.message[key], isActive: null }));
+      return result;
+    })
+    console.log( parseAnswer?.message );
+    alertAnswer.value = parseAnswer?.message;
+  }
+
+  textDataSources.value = format.textSorting(basic, textWhiteList);
+
+  jsTableData.value = [jsData];
+  csTableData.value = [csData];
+
+  flowParams.category = category;
+  flowParams.warningId = id;
+
+  scrollToTop();
 }
 
 const onChangeTabs = warningStatus => {
-  console.log( "warningStatus", warningStatus );
   onRestore({ warningStatus })
 }
+
+const onRegenerate = async () => {
+
+  let counter = 0;
+  let str = 0;
+
+  // const timer = setInterval(item => {
+  //   if( timer === 10 ) {
+  //     console.log("str", str);
+  //     clearInterval(timer);
+  //   }
+  //   counter++;
+  //   const data = {biz: "DECISION_REPORT", message: counter};
+
+  //   str += data.message + "||"
+
+  //   reportAnswer.value = str;
+
+  // }, 1000)
+
+
+  try {
+    const obj = {"biz": "DECISION_ALERT", "message": {"2_30": {"id": "2_30", "mainType": "alert", "mainContent": "设备与电气类是否有故障发生", "options": ["否", "是"], "next": "", "checked": false}}}
+    const result = Object.keys(obj.message).map(key => ({ ...obj.message[key], isActive: null }));
+    console.log( "result", result );
+    // refetch({
+    //   body: JSON.stringify(flowParams),
+    //   successHandler: data => {
+    //     const item = JSON.parse(data);
+    //     str += item.message;
+    //     reportAnswer.value = str;
+    //     scrollToBottomIfAtBottom()
+    //   },
+    //   doneHandler: () => {
+    //     alert("结束了")
+    //   }
+    // })
+  }
+  catch(error) {
+    console.log("exist error .....", error);
+  }
+}
+
+// 回答选项点击
+const handlerAlertOptions = (item, index) => {
+
+  const isExists = checkedAlertData.value.find(d => d.id === item.id);
+
+  item.isActive = index;
+
+  isExists ?? checkedAlertData.value.push( item );
+
+  if ( unref(checkedAlertData).length === unref(alertAnswer).length ) {
+    
+    const tempArr = alertAnswer.value.map(({ id, options, isActive }) => ({ [id]: options[isActive] }));
+    const tempArrToStr = JSON.stringify(tempArr);
+
+    flowParams.feedback = JSON.stringify(tempArr).substring(1, tempArrToStr.length - 1);
+    
+    onRegenerate();
+
+  }
+
+}
 </script>
 
 <template>
@@ -115,24 +264,74 @@ const onChangeTabs = warningStatus => {
       </div>
     </TheSubMenu>
 
-    <TheChatView>
+    <TheChatView ref="scrollRef" :is-footer="false">
 
       <ChatWelcome title="您好,我是LibraAI工艺管控助手" card-title="常见处理方案:"
         :sub-title="[
           '报警分析功能具备实时监测与预警机制,检测到异常情况立即触发多种报警方式,推送相关',
           '工作人员确保问题及时处理。报警时间为每小时警报,请大家及时处理。'
         ]" 
-        :card-content="[
-          '进水COD超标原因分析及常见的解决方案',
-          '进水TP超标原因分析及常见的解决方案',
-          '出水TN超标原因分析及常见的解决方案'
-        ]" v-if="false"
+        :card-content="recommendList"
+        v-if="!textDataSources"
       />
+  
+      <ChatBaseCard v-if="textDataSources">
+        <div class="waring-answer-wrapper">
+          <dl class="message-inner warning-info_medium ">
+            <dt class="mb-[2px] font-bold text-[#1A2029]">{{ textDataSources?.value }}</dt>
+            <dd v-for="item, index in textDataSources?.list" :key="index"><span :class="{'text-[#F44C49]': item.isWarning}">{{ item.label }}: {{ item.value }}</span></dd>
+          </dl>
+          <div class="table-inner">
+            <div class="warning-table mb-[8px]">
+              <div class="title">
+                <span>当前进水数据:</span>
+              </div>
+              <div class="main">
+                <BaseTable :columns="columns" :data="jsTableData"></BaseTable>
+              </div>
+            </div>
+            <div class="warning-table">
+              <div class="title">
+                <span>当前出水数据:</span>
+              </div>
+              <div class="main">
+                <BaseTable :columns="columns" :data="csTableData"></BaseTable>
+              </div>
+            </div>
+          </div>
+        </div>
+      </ChatBaseCard>
+
+      <!-- report -->
+      <ChatAnswer
+        :loading="true"
+        :toggleVisibleIcons="false"
+        :content="reportAnswer"
+        v-show="reportAnswer"
+      ></ChatAnswer>
+
+      <!-- alert -->
+      <ChatBaseCard v-show="alertAnswer.length">
+        <p class="mb-[15px] font-bold text-[#1A2029]">需要确定以下问题,完成决策方案:</p>
+        <ul class="radio-wrapper space-y-[14px]">
+          <li class="flex items-center" v-for="item in alertAnswer" :key="item.id">
+            <p class="mr-[14px]">{{ item.mainContent }}</p>
+            <p class="radio-btn-group space-x-[14px]">
+              <span
+                :key="index"
+                :class="['radio-btn', { active: item.isActive === index }]"
+                v-for="val,index in item.options"
+                @click="handlerAlertOptions(item, index)"
+              >{{ val }}</span>
+            </p>
+          </li>
+        </ul>
+      </ChatBaseCard>
 
-      <BaseCard :loading="true">
+      <!-- <BaseCard :loading="true">
         <div class="waring-answer-wrapper">
           <dl class="message-inner warning-info_medium ">
-            <dt class="mb-[2px] font-bold text-[#1A2029]">进水总磷报警</dt>
+            <dt class="mb-[2px] font-bold text-[#1A2029]">{{ textDataSources?.title }}</dt>
             <dd><span>报警时间:2024-4-25 21:00</span></dd>
             <dd><span class="text-[#F44C49]">报警值:7.87mg/L</span></dd>
             <dd><span>标准值:7.1mg/L</span></dd>
@@ -146,7 +345,6 @@ const onChangeTabs = warningStatus => {
                 <span>当前进水数据:</span>
               </div>
               <div class="main">
-                <BaseTable :columns="columns" :data="inWaterTableData"></BaseTable>
               </div>
             </div>
             <div class="warning-table">
@@ -154,14 +352,13 @@ const onChangeTabs = warningStatus => {
                 <span>当前出水数据:</span>
               </div>
               <div class="main">
-                <BaseTable :columns="columns" :data="[]"></BaseTable>
               </div>
             </div>
           </div>
         </div>
-      </BaseCard>
+      </BaseCard> -->
 
-      <BaseCard>
+      <!-- <BaseCard>
         <p class="flex-1 text-[15px] leading-[24px]">
           COD,即化学需氧量,是衡量水中有机物质含量的重要指标。它反映了水中可氧化有机物的量,通常用来评估水体的污染程度。水中的有机物主要来源于工业废水、生活污水、农药残留等,这些有机物不仅会导致水质变差,还会对生物和人类健康产生负面影响。因此,通过测定COD值,可以了解水中有机污染物的含量,进而评估水体的污染程度。这对于制定环境保护政策、控制污染源、保障水资源安全等方面都具有重要的指导意义
         </p>
@@ -175,27 +372,9 @@ const onChangeTabs = warningStatus => {
         @click="handleModelVisible"
       >
         水质预测推演
-      </button>
+      </button> -->
+
 
-      <BaseCard>
-        <p class="mb-[15px] font-bold text-[#1A2029]">需要确定以下问题,完成决策方案:</p>
-        <ul class="radio-wrapper space-y-[14px]">
-          <li class="flex items-center ">
-            <p class="mr-[14px]">在线仪表是否正常?</p>
-            <p class="radio-btn-group space-x-[14px]">
-              <span class="radio-btn active">是</span>
-              <span class="radio-btn">否</span>
-            </p>
-          </li>
-          <li class="flex items-center ">
-            <p class="mr-[14px]">在线仪表是否正常?</p>
-            <p class="radio-btn-group space-x-[14px]">
-              <span class="radio-btn">是</span>
-              <span class="radio-btn">否</span>
-            </p>
-          </li>
-        </ul>
-      </BaseCard>
 
     </TheChatView>
   </section>

+ 7 - 9
src/views/answer/AnswerView.vue

@@ -22,7 +22,7 @@ const { recommendList } = useRecommend({type: 0});
 
 const message = useMessage();
 
-const switchActive = ref(true);
+const switchActive = ref(false);
 
 const isLoading = ref(false);
 const inputRef = ref(null);
@@ -63,7 +63,7 @@ const handleChatDetail = async ({ sessionId }) => {
 
   scrollToBottom();
 }
-
+const log = ref('');
 const onRegenerate = async ({ question, realQuestion }) => {
   controller = new AbortController();
 
@@ -94,8 +94,8 @@ const onRegenerate = async ({ question, realQuestion }) => {
   }
 
   try {
-    const { data } = await chatApi.getChatStream(params);
-
+    const { data  } = await chatApi.getChatStream(params);
+ 
     const [ answer, id ] = data.split(ANSWER_ID_KEY);
 
     updateChat({
@@ -107,15 +107,14 @@ const onRegenerate = async ({ question, realQuestion }) => {
       delayLoading: false
     })
   }
-  catch {
-    console.log("取消了请求 - catch");
+  catch (error){
+    console.log("取消了请求 - catch", error);
   }
   finally {
     isLoading.value = false;
     onReset();
   }
 }
-
 // 提交问题
 const handleSubmit = async (question, realQuestion = '') => {
   // 用于模拟 - 内容生成前置等待状态
@@ -187,11 +186,10 @@ const handeChatDelete = async (id) => {
         v-if="!chatDataSource.length"
         @on-click="handleWelcomeRecommend"
       />
-
+      
       <div class="conversation-item" v-if="chatDataSource.length">
         <template v-for="item in chatDataSource" :key="item.id">
           <ChatAsk :content="item.question" :sessionId="item.sessionId"></ChatAsk>
-      
           <ChatAnswer
             :id="item.id"
             :content="item.answer"