소스 검색

feat:init

余尚辉 10 달 전
부모
커밋
4b98ba3602

+ 2 - 1
package.json

@@ -5,7 +5,8 @@
   "type": "module",
   "scripts": {
     "dev": "vite",
-    "build": "run-p type-check \"build-only {@}\" --",
+    "build": "vite build",
+    "build-lint": "run-p type-check \"build-only {@}\" --",
     "preview": "vite preview",
     "build-only": "vite build",
     "type-check": "vue-tsc --build --force"

+ 10 - 10
src/App.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import { RouterView } from 'vue-router';
-import { NConfigProvider, NMessageProvider } from 'naive-ui';
+import { NConfigProvider, NMessageProvider, zhCN, dateZhCN } from 'naive-ui';
 import type { GlobalThemeOverrides } from 'naive-ui';
 import { SelectProps, InputProps, TableProps } from 'naive-ui'
 
@@ -20,14 +20,14 @@ const themeOverrides: GlobalThemeOverrides = {
 
   },
   Menu: {
-    // itemTextColor: primaryColor,
-    // itemTextColorActive: primaryColor,
-    // itemTextColorActiveHover: primaryColor,
-    // itemTextColorChildActiveHover: primaryColor,
-    // itemTextColorChildActive: primaryColor,
-    // itemColorActive: '#FCFDFE',
-    // itemColorActiveHover: '#FCFDFE',
-    // arrowColorChildActive: primaryColor
+    itemTextColor: primaryColor,
+    itemTextColorActive: primaryColor,
+    itemTextColorActiveHover: primaryColor,
+    itemTextColorChildActiveHover: primaryColor,
+    itemTextColorChildActive: primaryColor,
+    itemColorActive: '#FCFDFE',
+    itemColorActiveHover: '#FCFDFE',
+    arrowColorChildActive: primaryColor
   },
   Scrollbar: {
     width: '0px',
@@ -96,7 +96,7 @@ const themeOverrides: GlobalThemeOverrides = {
 </script>
 
 <template>
-  <NConfigProvider :theme-overrides="themeOverrides">
+  <NConfigProvider :theme-overrides="themeOverrides" :locale="zhCN" :date-locale="dateZhCN">
     <NMessageProvider>
       <RouterView />
     </NMessageProvider>

+ 9 - 0
src/api/helper.js

@@ -0,0 +1,9 @@
+import http from "@/utils/request";
+
+export const helperApi = {
+  /**
+   * 获取助手列表
+   */
+  getHelperList: params => http.get('/front/bigModel/agentAssistant/list', { params }),
+
+}

+ 122 - 3
src/assets/styles/common.scss

@@ -2,14 +2,15 @@
   font-display: swap;
   font-family: 'AlimamaShuHeiTi';
   src: url('@/assets/font/AlimamaShuHeiTi-Bold.woff2') format('woff2'),
-  url('@/assets/font/AlimamaShuHeiTi-Bold.woff') format('woff'),
-  url('@/assets/font/AlimamaShuHeiTi-Bold.ttf') format('ttf');
+    url('@/assets/font/AlimamaShuHeiTi-Bold.woff') format('woff'),
+    url('@/assets/font/AlimamaShuHeiTi-Bold.ttf') format('ttf');
   font-weight: normal;
   font-style: normal;
 }
 
 // chat 布局相关
-.chat-ask_icon, .chat-answer_icon {
+.chat-ask_icon,
+.chat-answer_icon {
   display: flex;
   align-items: center;
   justify-content: center;
@@ -29,6 +30,124 @@
   padding: 10px 0;
 }
 
+
+#warning {
+
+  .base-card-container {
+    margin-bottom: 20px;
+  }
+
+  .warning-item-inner {
+    position: relative;
+    padding: 20px 8px 8px 8px;
+    border-radius: 4px;
+    background: #DDE5EF;
+
+    .tips {
+      position: absolute;
+      width: 36px;
+      height: 14px;
+      top: 0;
+      right: 0px;
+      border-radius: 0px 4px 0px 4px;
+      font-size: 8px;
+      text-align: center;
+      line-height: 14px;
+
+      &_warning,
+      &_being {
+        color: #F44C49;
+        background: #FFF0ED;
+      }
+
+      &_success {
+        color: #51BF8E;
+        background: #E9FAF2;
+      }
+
+      &_close {
+        color: #999999;
+        background: #D5D5D5;
+      }
+    }
+  }
+
+  .warning-info {
+    line-height: 16px;
+    font-size: 11px;
+    color: #5E5E5E;
+
+    dd {
+      margin-top: 4px;
+    }
+
+    &_medium {
+      line-height: 26px;
+      font-size: 14px;
+      color: #1A2029;
+    }
+  }
+
+  // 回答区域卡片
+  .waring-answer-wrapper {
+    @include flex(x, start, between);
+
+    .message-inner {
+      width: 194px;
+      flex-shrink: 0;
+    }
+
+    .table-inner {
+      @include flex(y, end, center);
+      padding-left: 20px;
+      border-left: 1px solid #F1F1F1;
+
+      .warning-table {
+        .title {
+          margin-bottom: 8px;
+          line-height: 16px;
+          font-size: 12px;
+          font-weight: bold;
+          color: #1A2029;
+        }
+      }
+    }
+  }
+
+  .radio-wrapper {
+
+    .radio-btn-group {
+      @include flex(x, center, center);
+      font-size: 14px;
+      text-align: center;
+      color: #5E5E5E;
+
+      .radio-btn {
+        width: 62px;
+        height: 28px;
+        border-radius: 4px;
+        background: #F4F6F8;
+        font-size: 14px;
+        line-height: 26px;
+        cursor: pointer;
+
+        &.active,
+        &:hover {
+          color: #2454FF;
+          background: #E2F1FF;
+        }
+
+        &.active {
+          background: #E2F1FF url('@/assets/images/chat/bg-raido-check.png') right bottom no-repeat;
+
+        }
+      }
+    }
+
+  }
+
+}
+
 // pre code,
 // pre tt {
 //   line-height: 1.65;

+ 12 - 6
src/assets/styles/github-markdown.scss

@@ -289,12 +289,18 @@ html {
 }
 
 .markdown-body table {
-  border-spacing: 0;
-  border-collapse: collapse;
-  display: block;
-  width: max-content;
-  max-width: 100%;
-  overflow: auto;
+  table-layout: fixed;
+  width: 100%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: normal;
+  text-align: center;
+  font-size: 12px;
+  // border-spacing: 0;
+  // border-collapse: collapse;
+  // display: block;
+  // max-width: 100%;
+  // overflow: auto;
 }
 
 .markdown-body td,

+ 3 - 0
src/assets/svgs/tool/arrow-bottom.svg

@@ -0,0 +1,3 @@
+<svg t="1716796473166" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1445" width="200" height="200" data-spm-anchor-id="a313x.search_index.0.i4.18f63a81cfsMIr">
+    <path d="M512 579.669333L286.165333 353.834667a42.624 42.624 0 1 0-60.330666 60.330666l256 256a42.624 42.624 0 0 0 60.330666 0l256-256a42.624 42.624 0 1 0-60.330666-60.330666L512 579.669333z" fill="#999999" p-id="1446"></path>
+</svg>

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 6 - 0
src/assets/svgs/tool/report.svg


+ 3 - 34
src/components/Chat/ChatAnswer.vue

@@ -7,7 +7,6 @@ import { chatApi } from "@/api/chat"
 import ChatBaseCard from './ChatBaseCard';
 import ChatText from './ChatText';
 
-
 const props = defineProps({
   id: {
     type: [String, Number],
@@ -63,10 +62,13 @@ const handleCopy = () => {
 <template>
   <ChatBaseCard class="answer-inner" :loading="loading" :delayLoading="delayLoading">
 
+    <slot></slot>
+    
     <template #text>
       <ChatText :content="content"></ChatText>
     </template>
 
+
     <template #button>
       <ul class="answer-btn-group" v-if="!loading && toggleVisibleIcons">
         <li class="btn" @click="handleCopy">
@@ -85,39 +87,6 @@ const handleCopy = () => {
 
   </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 }" />
-        <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>
-          <slot></slot>
-      </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">

+ 2 - 2
src/components/Chat/ChatBaseCard.vue

@@ -57,10 +57,10 @@ const props = defineProps({
           <p class="font-bold text-[#1A2029] leading-[24px]">内容生成中...</p>
         </template>
 
-        <slot name="text"></slot>
-
         <slot></slot>
 
+        <slot name="text"></slot>
+
       </div>
     </div>
 

+ 2 - 1
src/components/Chat/ChatInput.vue

@@ -62,7 +62,8 @@ const clearInpVal = () => {
 }
 
 defineExpose({
-  clearInpVal
+  clearInpVal,
+  inpVal,
 })
 
 </script>

+ 7 - 0
src/components/Chat/ChatSite.vue

@@ -0,0 +1,7 @@
+<template>
+
+
+  后期 = 变成问答大组件
+
+
+</template>

+ 7 - 1
src/components/Chat/ChatText.vue

@@ -71,7 +71,13 @@ const text = computed(() => {
   }
 
   table {
-    font-size: 12px;
+    /* font-size: 10px; */
+
+    thead {
+      strong {
+        font-size: 12px;
+      }
+    }
   }
 }
 </style>

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

@@ -38,7 +38,7 @@ const handleEmit = ({ content: realQuestion, question }) => {
       <p class="py-[10px] text-[#1A2029] text-[36px] font-bold leading-[50px]">{{ title }}</p>
       <p class="text-[#333333] leading-[20px]" v-for="item, index in subTitle" :key="index">{{ item }}</p>
     </div>
-    <dl class="answer-list rounded-[8px] bg-white py-[30px] pl-[82px] mt-[36px]">
+    <dl class="answer-list rounded-[8px] bg-white py-[30px] pl-[82px] mt-[36px]" v-if="cardContent.length">
       <dt class="mb-[18px] text-[20px] text-[#1A2029] leading-[28px] font-bold">{{ cardTitle }}</dt>
       <dd
         :class="[

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

@@ -59,7 +59,8 @@ defineExpose({ targetScrollDom });
       <main class="flex flex-col justify-between h-full m-auto chat-main"
         :style="{ height: isFooter ? 'calc(100% - 212px)' : 'calc(100% - 100px)' }">
         <div class="chat-scroll" ref="targetScrollDom">
-          <div class="w-[800px] m-auto pb-[20px]">
+          <!-- pb-[20px] -->
+          <div class="w-[800px] m-auto ">
             <slot></slot>
           </div>
         </div>

+ 21 - 27
src/components/Layout/TheMenu.vue

@@ -9,36 +9,36 @@ const router = useRouter();
 
 const activeMenuKey = ref(route.path);
 
-watch(() => route.path, currentPath => { 
+watch(() => route.path, currentPath => {
   activeMenuKey.value = currentPath;
 })
 
-function renderIcon(props) {
+function renderIcon (props) {
   return () => h(SvgIcon, { ...props })
 }
 
-function renderChildrenIcon({ name, size = '20' }) {
+function renderChildrenIcon ({ name, size = '20' }) {
   return () => (
     <div class="flex items-center justify-center">
-      { renderIcon({ name, size, class: ['icon-default'] })() }
-      { renderIcon({ name: name + '-active', size, class: ['icon-active', 'hidden'] })() }
+      {renderIcon({ name, size, class: ['icon-default'] })()}
+      {renderIcon({ name: name + '-active', size, class: ['icon-active', 'hidden'] })()}
     </div>
   )
 }
 
 function renderLabel (val, url) {
-  return url ? (<a href={ url } target="_blank" class="pl-1">{ val }</a>) : (<span class="pl-1">{ val }</span>) ;
+  return url ? (<a href={url} target="_blank" class="pl-1">{val}</a>) : (<span class="pl-1">{val}</span>);
 }
 
 const menuOptions = [
   {
     label: () => renderLabel('智慧总控'),
-    icon: renderIcon({name: 'menu-control'}),
+    icon: renderIcon({ name: 'menu-control' }),
     key: '/'
   },
   {
     label: () => renderLabel('专家问答'),
-    icon: renderIcon({name: 'menu-answers'}),
+    icon: renderIcon({ name: 'menu-answers' }),
     key: '/answer'
   },
   {
@@ -47,14 +47,14 @@ const menuOptions = [
     key: '/',
     children: [
       {
-        label:() =>  renderLabel('水质报警'),
+        label: () => renderLabel('水质报警'),
         icon: renderChildrenIcon({ name: 'menu-analyse-water' }),
         key: '/water-warn',
       },
       {
         label: '生化报警',
         icon: renderChildrenIcon({ name: 'menu-analyse-pymol' }),
-        key: 'key2',
+        key: '/pymol-warn',
       },
       {
         label: '预测报警',
@@ -64,18 +64,18 @@ const menuOptions = [
       {
         label: '智能工单',
         icon: renderChildrenIcon({ name: 'menu-analyse-order' }),
-        key: 'key4',
+        key: '/work-order',
       }
     ]
   },
   {
     label: () => renderLabel('智能助手'),
-    icon: renderIcon({ name: 'menu-user' }),
-    key: '/'
+    icon: renderIcon({ name: 'menu-help' }),
+    key: '/helper'
   },
   {
     label: () => renderLabel('用户中心'),
-    icon: renderIcon({ name: 'menu-help' }),
+    icon: renderIcon({ name: 'menu-user' }),
     key: '/user',
   },
 ]
@@ -89,32 +89,25 @@ const handleUpdateValue = (key, { url }) => {
 
 <template>
   <n-scrollbar style="height: 100%">
-    <n-menu
-      :options="menuOptions"
-      :icon-size="30"
-      v-model:value="activeMenuKey"
-      @update:value="handleUpdateValue"
-    >
+    <n-menu :options="menuOptions" :icon-size="30" v-model:value="activeMenuKey" @update:value="handleUpdateValue">
     </n-menu>
   </n-scrollbar>
 </template>
 
-<style scoped lang="scss">
-
-</style>
+<style scoped lang="scss"></style>
 
 <style lang="scss">
-
 @mixin sub-menu-active {
   .n-menu-item-content__icon {
     .icon-default {
       display: none;
     }
+
     .icon-active {
       display: block;
     }
   }
-  
+
   .n-menu-item-content-header {
     font-weight: bold;
   }
@@ -132,9 +125,11 @@ const handleUpdateValue = (key, { url }) => {
     @include sub-menu-active();
   }
 }
+
 // reset - 选中状态
 .n-menu-item-content--selected {
   @include sub-menu-active();
+
   &:not(.n-menu-item-content--disabled)::before {
     border: 1px solid #fff;
     background: #FCFDFE;
@@ -150,5 +145,4 @@ const handleUpdateValue = (key, { url }) => {
 //   svg {
 //     display: none;
 //   }
-// }
-</style>
+// }</style>

+ 1 - 1
src/components/Layout/TheUserAvatar.vue

@@ -75,7 +75,7 @@ const RenderUserAvatar = ({ store }) => {
   return (
     <NPopover
       v-slots={slots}
-      trigger="click"
+      trigger="hover"
       raw={true}
       show-arrow={false}
       style="box-shadow: 0px 10px 31px 0px #B1B6B933"

+ 36 - 64
src/components/RecodeSquareCardItem/index.vue

@@ -2,6 +2,7 @@
 import { ref, computed } from 'vue';
 import { NEllipsis } from "naive-ui";
 import { BaseButton, SvgIcon } from '@/components';
+import { truncateDecimals } from '@/utils/format';
 
 const props = defineProps({
   item: {
@@ -24,27 +25,51 @@ const warningType = computed(() => {
       cls: 'tips_success'
     },
     '2': {
-      label: '系统自动关闭',
-      cls: 'tips_success'
+      label: '系统关闭',
+      cls: 'tips_close'
     },
     '3': {
-      label: '用户转为应急处理中',
+      label: '应急处理中',
       cls: 'tips_warning'
     }
   }
-  if (item.type == 0) {
-    return waterWhite[item.type + '']
+  const earlyWaring = {
+    '0': {
+      label: '预警中',
+      cls: 'tips_warning'
+    },
+    '0': {
+      label: '已完成',
+      cls: 'tips_success'
+    },
+  }
+
+  if ( item.type == 0 ) {
+    return waterWhite[item.status + '']
+  }
+  if ( item.type === 1 ) {
+    return waterWhite[item.status + '']
+  }
+  if ( item.type === 2 ) {
+    return earlyWaring[item.status + '']
   }
 })
 
 const dataSources = computed(() => {
   const item = props.item;
-  if ( item.type == 0 ) {
+  if (item.type == 0) {
     return [
-      {label: '报警时间',   value: item.time},
-      {label: '报警值',     value: item.warningVal, type: 'wraning'},
-      {label: '报警级别',   value: item.level},
-      {label: '报警次数',   value: item.counts},
+      { label: '报警时间', value: item.time },
+      { label: '报警值',   value: truncateDecimals(item.warningVal), type: 'wraning' },
+      { label: '报警级别', value: item.level },
+      { label: '报警次数', value: item.counts },
+    ]
+  }
+  if (item.type == 1) {
+    return [
+      { label: '报警时间', value: item.time },
+      { label: '报警值',   value: item.warningVal, type: 'wraning' },
+      { label: '报警次数', value: item.counts },
     ]
   }
 });
@@ -71,57 +96,4 @@ const handleEmitParent = () => {
     </dl>
     <BaseButton type="gray" class="mt-[8px]" @click="handleEmitParent">操作</BaseButton>
   </div>
-</template>
-
-<style scoped lang="scss">
-.warning-item-inner {
-  position: relative;
-  padding: 20px 8px 8px 8px;
-  border-radius: 4px;
-  background: #DDE5EF;
-
-  .tips {
-    position: absolute;
-    width: 36px;
-    height: 14px;
-    top: 0;
-    right: 0px;
-    border-radius: 0px 4px 0px 4px;
-    font-size: 8px;
-    text-align: center;
-    line-height: 14px;
-
-    &_warning,
-    &_being {
-      color: #F44C49;
-      background: #FFF0ED;
-    }
-  
-    &_success {
-      color: #51BF8E;
-      background: #E9FAF2;
-    }
-
-    &_close {
-      color: #999999;
-      background: #D5D5D5;
-    }
-  }
-}
-
-.warning-info {
-  line-height: 16px;
-  font-size: 11px;
-  color: #5E5E5E;
-
-  dd {
-    margin-top: 4px;
-  }
-
-  &_medium {
-    line-height: 26px;
-    font-size: 14px;
-    color: #1A2029;
-  }
-}
-</style>
+</template>

+ 2 - 3
src/components/User/userEdit.vue

@@ -61,14 +61,13 @@ const generalOptions = ['信义污水厂'].map(
         </n-form-item>
         <n-form-item label="水厂" v-if="user.dept">
           <div class="content">
-            <n-select v-model:value="user.dept['deptName']" style="width: 164px;" disabled placeholder="Select"
-              :options="generalOptions" />
+            {{ user.dept['deptName'] }}
             <span class="des">设置为默认登录</span>
           </div>
         </n-form-item>
         <n-form-item label="手机号">
           <div class="content">
-            <span>xxx</span>
+            <span>{{ user.emergencyPhone }}</span>
           </div>
         </n-form-item>
       </n-form>

+ 14 - 0
src/composables/index.js

@@ -0,0 +1,14 @@
+import { useChat } from './useChat';
+import { useFetchStream } from './useFetchStream';
+import { useInfinite } from './useInfinite';
+import { useRecommend } from './useRecommend';
+import { useScroll } from './useScroll';
+
+
+export {
+  useChat,
+  useFetchStream,
+  useInfinite,
+  useRecommend,
+  useScroll
+}

+ 6 - 3
src/composables/useFetchStream.js

@@ -23,8 +23,8 @@ export function useFetchStream(url, options = {}, immediate = true) {
     Authorization: 'Bearer ' + token
   }
 
-  async function fetchData({ body, successHandler, doneHandler }) {
-    console.log(body, {body});
+  async function fetchData({ body, successHandler, doneHandler, errorHandler }) {
+  
     if (abortController) {
       abortController.abort();
     }
@@ -63,9 +63,12 @@ export function useFetchStream(url, options = {}, immediate = true) {
         completeData.value.push(chunkText);
         streamData.value = chunkText;
         successHandler && successHandler(chunkText);
-        console.log('Received chunk:', chunkText);
       }
+
+      return completeData;
     } catch (err) {
+      errorHandler && errorHandler();
+      console.log("fetch err:", err);
       error.value = err;
     } finally {
       loading.value = false;

+ 24 - 0
src/router/index.js

@@ -38,6 +38,30 @@ const constantRouterMap = [
           title: '水质报警'
         }
       },
+      {
+        path: 'pymol-warn',
+        name: 'PymolWarn',
+        component: () => import('@/views/analyse/PymolView.vue'),
+        meta: {
+          title: '生化报警'
+        }
+      },
+      {
+        path: 'work-order',
+        name: 'WorKOrder',
+        component: () => import('@/views/analyse/WorkOrder.vue'),
+        meta: {
+          title: '智慧工单'
+        }
+      },
+      {
+        path: 'helper',
+        name: 'HelperView',
+        component: () => import('@/views/helper/HelperView.vue'),
+        meta: {
+          title: '智能助手'
+        }
+      }
     ]
   },
   {

+ 20 - 0
src/stores/modules/chatStore.js

@@ -0,0 +1,20 @@
+import { ref, unref, computed } from 'vue'
+import { defineStore } from 'pinia'
+
+export const useChatStore = defineStore('chat', () => {
+  const chatQuestion = ref({});
+
+  const setChatQuestion = question => {
+    chatQuestion.value = question;
+  }
+
+  const clearChatQuestion = () => {
+    chatQuestion.value = {};
+  }
+
+  return {
+    chatQuestion,
+    setChatQuestion,
+    clearChatQuestion,
+  }
+})

+ 14 - 8
src/utils/format.js

@@ -1,22 +1,28 @@
 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
+        if (item.realKey === key ) {
+          if ( dataSource[key] !== null ) {
+            item.value = (isNaN(dataSource[key]) ? dataSource[key]  : truncateDecimals(dataSource[key])) + item.value
+          } else{
+            item.value = ''
+          }
+          
         }
       })
       return item;
-    })
-
+    }).filter(({ value }) => value)
     return { title, list };
-  },
+  }
+}
 
-}
+export const truncateDecimals = num => {
+  return Number((Math.floor(num * 100) / 100).toFixed(2));
+}

+ 1 - 0
src/utils/request.ts

@@ -71,6 +71,7 @@ export class Request {
       if ( res.config.onDownloadProgress ) return res;
    
       const { code } = res.data;
+      // console.log(code)
       // !success && showNotification("error", message);
       return code === 200 ? res.data : Promise.reject(res.data);
 

+ 199 - 0
src/views/analyse/PymolView.vue

@@ -0,0 +1,199 @@
+<script setup lang="jsx">
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+import { NTabs, NTab } from 'naive-ui';
+import { useChatStore } from '@/stores/modules/chatStore';
+import { BaseTable, ChatWelcome, RecodeSquareCardItem, TheSubMenu, TheChatView } from "@/components";
+import { ChatBaseCard, ChatAnswer } from '@/components/Chat';
+import { format, truncateDecimals } from "@/utils/format";
+import { waterApi } from '@/api/water';
+import {useInfinite, useRecommend, useScroll} from '@/composables'
+
+const { recommendList } = useRecommend({ type: 2 });
+const { scrollRef, scrollToTop } = useScroll();
+const { recordList, isFetching, onScrolltolower, onRestore } = useInfinite('/front/bigModel/warning/pageList', { type: 1, warningStatus: 0 });
+
+const router = useRouter();
+const chatStore = useChatStore();
+
+const answerResult = ref("");
+const textDataSources = ref(null);
+
+// 进出水数据
+const jsTableData = ref([]);
+const csTableData = ref([]);
+
+const renderRowDom = ({ row, key }) => {
+  const { exceed, value } = row[key] || {};
+  const cls = exceed ? 'text-[#F44C49] font-bold' : 'text-[1A2029]'
+  return (<span class={cls}>{truncateDecimals(value)} {exceed && <i>↑</i>}</span>)
+}
+
+const columns = [
+  {
+    title: '流量(m³/h)',
+    key: 'name',
+    titleAlign: 'center',
+    align: 'center',
+    className: 'small',
+    width: '80px',
+    render: (row) => renderRowDom({ row, key: '流量' })
+  },
+  {
+    title: 'COD(mg/L)',
+    key: 'small',
+    titleAlign: 'center',
+    align: 'center',
+    className: 'small',
+    width: '80px',
+    render: (row) => renderRowDom({ row, key: 'COD' })
+  },
+  {
+    title: 'TN(mg/L)',
+    key: 'address',
+    titleAlign: 'center',
+    align: 'center',
+    className: 'small',
+    width: '80px',
+    render: (row) => renderRowDom({ row, key: 'TN' })
+  },
+  {
+    title: 'NH3-N(mg/L)',
+    key: 'tags',
+    titleAlign: 'center',
+    align: 'center',
+    className: 'small',
+    width: '80px',
+    render: (row) => renderRowDom({ row, key: 'NH3-N' })
+  },
+  {
+    title: 'TP(mg/L)',
+    key: 'COD',
+    titleAlign: 'center',
+    align: 'center',
+    className: 'small',
+    width: '80px',
+    render: (row) => renderRowDom({ row, key: 'TP' })
+  },
+  {
+    title: 'SS(mg/L)',
+    key: '流量',
+    titleAlign: 'center',
+    align: 'center',
+    className: 'age',
+    width: '78px',
+    render: (row) => renderRowDom({ row, key: 'SS' })
+  }
+]
+
+// 切换Tabs
+const onChangeTabs = warningStatus => {
+  answerResult.value = '';
+  textDataSources.value = '';
+  onRestore({ warningStatus })
+}
+
+// 打开详情
+const handleOpenContent = async ({ id, category }) => {
+  const { data } = await waterApi.getWaringDetails(id);
+  const showVal = JSON.parse(data.showVal);
+  const { basic, jsData, csData } = showVal;
+  console.log( jsData );
+  const textWhiteList = [
+    { label: '报警时间', realKey: '报警时间', value: '', isWarning: false },
+    { label: '报警值',   realKey: '报警值', value: 'mg/L', isWarning: true },
+    { label: '管控值',   realKey: '管控值', value: 'mg/L', isWarning: false },
+    { label: '标准值',   realKey: '标准值', value: 'mg/L', isWarning: false },
+    // { label: '报警级别', realKey: '告警级别', value: '', isWarning: false },
+    { label: '报警次数', realKey: '报警次数', value: '', isWarning: false },
+    { label: '状态',     realKey: '状态', value: '', isWarning: false }
+  ]
+
+  answerResult.value = data.answer;
+
+  textDataSources.value = format.textSorting(basic, textWhiteList);
+
+  jsTableData.value = [jsData];
+  csTableData.value = [csData];
+
+  scrollToTop();
+}
+
+// 欢迎 - 问答
+const handleWelcomeRecommend = question => {
+  chatStore.setChatQuestion(question);
+  router.push('/answer');
+}
+</script>
+
+<template>
+  <section class="flex items-start h-full" id="warning">
+    <TheSubMenu title="生化报警" @scrollToLower="onScrolltolower" :loading="isFetching">
+      <template #top>
+        <div class="border-[#DAE5ED]">
+          <n-tabs type="line" justify-content="space-evenly">
+            <n-tab name="oasis" tab="正在报警" @click="onChangeTabs(0)"></n-tab>
+            <n-tab name="thebeatles" tab="历史报警" @click="onChangeTabs(1)"></n-tab>
+          </n-tabs>
+        </div>
+      </template>
+
+      <div class="px-[12px] py-[14px] text-[#5e5e5e]">
+        <div class="grid grid-cols-1 gap-[12px]">
+          <RecodeSquareCardItem v-for="item in recordList" :key="item.id" :item="item" @on-click="handleOpenContent" />
+        </div>
+      </div>
+    </TheSubMenu>
+
+    <TheChatView ref="scrollRef" :is-footer="false">
+
+      <ChatWelcome title="您好,我是LibraAI工艺管控助手" card-title="常见处理方案:" :sub-title="[
+        '报警分析功能具备实时监测与预警机制,检测到异常情况推送相关工作人员确保问题及时处理。',
+        '报警时间为每小时警报,请大家及时处理。'
+      ]"
+        v-if="!textDataSources"
+        :card-content="recommendList"
+        @on-click="handleWelcomeRecommend"
+      />
+
+      <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?.title }}</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>
+      
+      <ChatAnswer 
+        :loading="false"
+        :delay-loading="false"
+        :toggleVisibleIcons="false"
+        :content="answerResult"
+        v-if="answerResult"
+      />
+
+    </TheChatView>
+
+  </section>
+</template>
+
+<style scoped lang="scss"></style>

+ 268 - 263
src/views/analyse/WaterView.vue

@@ -1,10 +1,12 @@
 <script setup lang="jsx">
-import { ref, unref, watch } from 'vue';
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
 import { NTabs, NTab } from 'naive-ui';
-import { BaseCard, BaseTable, ChatWelcome, SvgIcon, RecodeSquareCardItem, TheSubMenu, TheChatView } from "@/components";
-import { ChatAsk, ChatBaseCard, ChatAnswer } from '@/components/Chat';
+import { useChatStore } from '@/stores/modules/chatStore';
+import { BaseTable, ChatWelcome, RecodeSquareCardItem, TheSubMenu, TheChatView } from "@/components";
+import { ChatBaseCard, ChatAnswer } from '@/components/Chat';
 
-import { format } from "@/utils/format";
+import { format, truncateDecimals } from "@/utils/format";
 
 import { waterApi } from '@/api/water';
 import { CustomModal } from "./components";
@@ -16,25 +18,25 @@ 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 { refetch, cancelFetch } = 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 router = useRouter();
+const chatStore = useChatStore();
 
+// 回答列表
+const answerResult = ref([]);
 // 获取最终回答流数据参数
 const flowParams = {
   feedback: '',
   category: '',
-  warningId: ''
+  warningId: '',
+  simulate: null
 };
 
-const reportAnswer = ref('');
-const alertAnswer = ref([]);
-
-const checkedAlertData = ref([]);
+const answerLoading = ref(false);
 
 const textDataSources = ref(null);
-const answerAlertDataSources = ref([]);
 
 // 进出水数据
 const jsTableData = ref([]);
@@ -42,10 +44,12 @@ const csTableData = ref([]);
 
 const visible = ref(false);
 
+const modalData = ref({});
+
 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>)
+  return (<span class={ cls }>{truncateDecimals(value)} {exceed && <i>↑</i>}</span>)
 } 
 
 const columns = [
@@ -86,7 +90,7 @@ const columns = [
     render: (row) => renderRowDom({ row, key: 'NH3-N' })
   },
   {
-    title: '总磷TP(mg/L)',
+    title: 'TP(mg/L)',
     key: 'COD',
     titleAlign: 'center',
     align: 'center',
@@ -105,7 +109,6 @@ const columns = [
   }
 ]
 
-const inWaterTableData = ref([{ name: 1233, actions: "7.87" }]);
 
 // 新建对话
 const handleCreateDialog = () => {
@@ -117,100 +120,168 @@ const handleLoad = () => {
 }
 
 const handleModelVisible = () => {
-  visible.value = true
+  visible.value = true;
 } 
 
 /**
  * 报警详情
 */
-const handleOpenContent = async ({id, category}) => {
+const handleOpenContent = async ({ id, category }) => {
+
+  if ( id == flowParams.warningId ) return;
+
+  flowParams.category = category;
+  flowParams.warningId = id;
+
   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 );
+  
+  cancelFetch();
 
-  console.log( answerObjItem.biz );
+  answerResult.value = [];
 
   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 }
+    { label: '报警时间', realKey: '报警时间', value: '', isWarning: false },
+    { label: '报警值',   realKey: '报警值', value: 'mg/L', isWarning: true },
+    { label: '管控值',   realKey: '管控值', value: 'mg/L', isWarning: false },
+    { label: '标准值',   realKey: '标准值', value: 'mg/L', isWarning: false },
+    { label: '报警级别', realKey: '告警级别', value: '', isWarning: false },
+    { label: '报警次数', realKey: '报警次数', value: '', isWarning: false },
+    { label: '状态',     realKey: '状态', value: '', isWarning: false }
   ]
 
   if ( answerObjItem.biz === "DECISION_REPORT" ) {
-    alertAnswer.value = [];
-    reportAnswer.value = answer.map(item => {
+
+    const answerContent = answer.map(item => {
       const itemParse = JSON.parse(item); 
       return itemParse.message;
-    }).join();
+    }).join("");
+
+    answerResult.value.push({
+      biz: 'DECISION_REPORT',
+      answer: answerContent,
+      loading: false,
+      delayLoading: false
+    })
+
   } 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;
+    answerResult.value.push({
+      biz: 'DECISION_ALERT',
+      loading: false,
+      delayLoading: false,
+      isAllSelect: false,
+      list: parseAnswer?.message
+    })
   }
 
+  console.log( basic );
   textDataSources.value = format.textSorting(basic, textWhiteList);
 
   jsTableData.value = [jsData];
   csTableData.value = [csData];
 
-  flowParams.category = category;
-  flowParams.warningId = id;
-
   scrollToTop();
 }
 
 const onChangeTabs = warningStatus => {
+  textDataSources.value = '';
+  answerLoading.value = false;
+  answerResult.value = [];
+  cancelFetch();
   onRestore({ warningStatus })
 }
 
 const onRegenerate = async () => {
+  
+  answerLoading.value = true;
+
+  const len = answerResult.value.length ? answerResult.value.length : 0;
 
-  let counter = 0;
-  let str = 0;
+  const tempReport = { 
+    biz: 'DECISION_REPORT',
+    answer: '',
+    loading: true,
+    delayLoading: true,
+  };
 
-  // const timer = setInterval(item => {
-  //   if( timer === 10 ) {
-  //     console.log("str", str);
-  //     clearInterval(timer);
-  //   }
-  //   counter++;
-  //   const data = {biz: "DECISION_REPORT", message: counter};
+  let tempSimulate = null;
 
-  //   str += data.message + "||"
+  answerLoading.value = true;
 
-  //   reportAnswer.value = str;
+  const params = {
+    body: JSON.stringify(flowParams),
+    errorHandler: () => {
+      
+    },
+    successHandler: data => {
 
-  // }, 1000)
+      const item = JSON.parse(data);
 
+      answerLoading.value = false;
+
+      if (item.biz === 'DECISION_REPORT') {
+        tempReport.answer += item.message;
+        answerResult.value[len] = { ...tempReport };
+      }
+      
+      if (item.biz === 'DECISION_ALERT') {
+        const list = Object.keys(item.message).map(key => ({ ...item.message[key], isActive: null }));
+   
+        answerResult.value.push({
+          biz: 'DECISION_ALERT',
+          loading: true,
+          delayLoading: true,
+          isAllSelect: false,
+          list
+        })
+      }
+
+      if (item.biz === 'DECISION_SIMULATE') {
+        const lastAnswerItem = answerResult.value[len - 1];
+
+        if ( lastAnswerItem.biz === 'DECISION_TABLE' ) {
+          answerResult.value[len - 1] = {
+            ...lastAnswerItem,
+            content: JSON.parse(item.message).pred.join(",")
+          }
+        } else {
+          const { off, on, pred } = JSON.parse(item.message);
+          tempSimulate = {
+            biz: 'DECISION_SIMULATE',
+            off,
+            on,
+            pred,
+            isDisable: false
+          }
+          modalData.value = tempSimulate;
+        }
+        console.log("DECISION_SIMULATE")
+      }
+
+      scrollToBottomIfAtBottom();
+    }
+  }
 
   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("结束了")
-    //   }
-    // })
+    const res = await refetch(params);
+    console.log( "最终", res );
+    const answerItem = answerResult.value[answerResult.value.length - 1];
+    if (answerItem?.biz) {
+      answerItem.loading = false;
+      answerItem.delayLoading = false;
+    }
+    if (tempSimulate) {
+      answerResult.value.push(tempSimulate);
+    }
   }
   catch(error) {
     console.log("exist error .....", error);
@@ -218,30 +289,58 @@ const onRegenerate = async () => {
 }
 
 // 回答选项点击
-const handlerAlertOptions = (item, index) => {
+const handlerAlertOptions = (item, val, index) => {
+  const { list, isAllSelect } = item;
 
-  const isExists = checkedAlertData.value.find(d => d.id === item.id);
+  if ( isAllSelect ) return;
 
-  item.isActive = index;
+  val.isActive = index;
 
-  isExists ?? checkedAlertData.value.push( item );
+  const isExists = list.find(({ isActive }) => isActive === null);
 
-  if ( unref(checkedAlertData).length === unref(alertAnswer).length ) {
-    
-    const tempArr = alertAnswer.value.map(({ id, options, isActive }) => ({ [id]: options[isActive] }));
-    const tempArrToStr = JSON.stringify(tempArr);
+  if ( !isExists ) {
+    item.isAllSelect = true; 
 
-    flowParams.feedback = JSON.stringify(tempArr).substring(1, tempArrToStr.length - 1);
-    
-    onRegenerate();
+    const result = item.list
+    .map(({ id, options, isActive }) => ({ [id]: options[isActive] }))
+    .reduce((accumulator, currentValue) => {
+      Object.keys(currentValue).forEach(key => accumulator[key] = currentValue[key]);
+      return accumulator;
+    }, {});
+    flowParams.feedback = JSON.stringify(result);
 
+    onRegenerate();
   }
+}
+
+// 开始预测
+const handleSendSimulate = ({ simulate, table }) => {
+  const len = answerResult.value.length;
+
+  flowParams.simulate = simulate;
+  answerResult.value[len - 1].isDisable = false;
+
+  answerResult.value.push({
+    biz: 'DECISION_TABLE',
+    loading: true,
+    delayLoading: true,
+    table,
+    isDisable: false
+  })
 
+  onRegenerate();
 }
+
+// 欢迎页提交
+const handleWelcomeRecommend = question => {
+  chatStore.setChatQuestion(question);
+  router.push('/answer');
+}
+
 </script>
 
 <template>
-  <section class="flex items-start h-full">
+  <section class="flex items-start h-full" id="warning">
     <TheSubMenu title="水质报警" @scrollToLower="onScrolltolower" :loading="isFetching">
       <template #top>
         <div class="border-[#DAE5ED]">
@@ -268,17 +367,18 @@ const handlerAlertOptions = (item, index) => {
 
       <ChatWelcome title="您好,我是LibraAI工艺管控助手" card-title="常见处理方案:"
         :sub-title="[
-          '报警分析功能具备实时监测与预警机制,检测到异常情况立即触发多种报警方式,推送相关',
-          '工作人员确保问题及时处理。报警时间为每小时警报,请大家及时处理。'
+          '报警分析功能具备实时监测与预警机制,检测到异常情况推送相关工作人员确保问题及时处理。',
+          '报警时间为每小时警报,请大家及时处理。'
         ]" 
         :card-content="recommendList"
+        @on-click="handleWelcomeRecommend"
         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>
+            <dt class="mb-[2px] font-bold text-[#1A2029]">{{ textDataSources?.title }}</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">
@@ -302,197 +402,102 @@ const handlerAlertOptions = (item, index) => {
         </div>
       </ChatBaseCard>
 
-      <!-- report -->
+      <section v-for="item,index in answerResult" :key="index">
+        <template v-if="item.biz === 'DECISION_REPORT'">
+          <ChatAnswer
+            :loading="item.loading"
+            :delay-loading="item.delayLoading"
+            :toggleVisibleIcons="false"
+            :content="item.answer"
+          ></ChatAnswer>
+        </template>
+        <!-- {{ item }} -->
+        <template v-if="item.biz === 'DECISION_ALERT'">
+          <ChatBaseCard
+            :loading="item.loading"
+            :delay-loading="item.delayLoading"
+            :toggleVisibleIcons="false"
+          >
+            <p class="mb-[15px] font-bold text-[#1A2029]">需要确定以下问题,完成决策方案:</p>
+            <ul class="radio-wrapper space-y-[14px]">
+              <li class="flex items-center" v-for="val,i in item.list" :key="i">
+                <p class="mr-[14px]">{{ val.mainContent }}</p>
+                <p class="radio-btn-group space-x-[14px]">
+                  <span
+                    v-for="option,index in val.options"
+                    :class="['radio-btn', { active: val.isActive === index }]"
+                    @click="handlerAlertOptions(item, val, index)"
+                  >{{ option }}</span>
+                </p>
+              </li>
+            </ul>
+          </ChatBaseCard>
+        </template>
+
+        <template v-if="item.biz === 'DECISION_SIMULATE'">
+          <button class="
+            px-[30px] py-[10px] mb-[20px]
+            rounded-[8px] 
+            bg-white text-[13px] 
+            text-[#5E5E5E] hover:text-[#2454FF]"
+            :disabled="item.isDisable"
+            @click="handleModelVisible"
+          >
+            水质预测推演
+          </button>
+        </template>
+        
+        <template v-if="item.biz === 'DECISION_TABLE'">
+          <ChatAnswer
+            :loading="item.loading"
+            :delay-loading="item.delayLoading"
+            :toggleVisibleIcons="false"
+          >
+            <div class="markdown-body text-[15px] break-all">
+              <strong class="block mb-[16px]">推荐指标调整:</strong>
+              <table>
+                <thead>
+                  <tr>
+                    <th v-for="text in item.table.header" :key="text">{{ text }}</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <tr>
+                    <td v-for="text in item.table.body" :key="text">{{ text }}</td>
+                  </tr>
+                </tbody>
+              </table>
+              <strong class="block mb-[16px]">预测推演结果:</strong>
+              <span>出水硝酸盐:{{ item.content }}</span>
+            </div>
+          </ChatAnswer>
+          <button class="
+            px-[30px] py-[10px] mb-[20px]
+            rounded-[8px] 
+            bg-white text-[13px] 
+            text-[#5E5E5E] hover:text-[#2454FF]"
+            :disabled="item.isDisable"
+            @click="handleModelVisible"
+          >
+            水质预测推演
+          </button>
+        </template>
+      </section>
+
       <ChatAnswer
-        :loading="true"
+        :loading="answerLoading"
+        :delay-loading="answerLoading"
         :toggleVisibleIcons="false"
-        :content="reportAnswer"
-        v-show="reportAnswer"
+        v-show="answerLoading"
       ></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">
-        <div class="waring-answer-wrapper">
-          <dl class="message-inner warning-info_medium ">
-            <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>
-            <dd><span>报警级别:二级</span></dd>
-            <dd><span>报警次数:33</span></dd>
-            <dd><span>状态:报警中</span></dd>
-          </dl>
-          <div class="table-inner">
-            <div class="warning-table mb-[8px]">
-              <div class="title">
-                <span>当前进水数据:</span>
-              </div>
-              <div class="main">
-              </div>
-            </div>
-            <div class="warning-table">
-              <div class="title">
-                <span>当前出水数据:</span>
-              </div>
-              <div class="main">
-              </div>
-            </div>
-          </div>
-        </div>
-      </BaseCard> -->
-
-      <!-- <BaseCard>
-        <p class="flex-1 text-[15px] leading-[24px]">
-          COD,即化学需氧量,是衡量水中有机物质含量的重要指标。它反映了水中可氧化有机物的量,通常用来评估水体的污染程度。水中的有机物主要来源于工业废水、生活污水、农药残留等,这些有机物不仅会导致水质变差,还会对生物和人类健康产生负面影响。因此,通过测定COD值,可以了解水中有机污染物的含量,进而评估水体的污染程度。这对于制定环境保护政策、控制污染源、保障水资源安全等方面都具有重要的指导意义
-        </p>
-      </BaseCard>
-
-      <button class="
-        px-[30px] py-[10px] mb-[20px]
-        rounded-[8px] 
-        bg-white text-[13px] 
-        text-[#5E5E5E] hover:text-[#2454FF]"
-        @click="handleModelVisible"
-      >
-        水质预测推演
-      </button> -->
-
-
-
     </TheChatView>
   </section>
 
-  <CustomModal v-model:visible="visible"></CustomModal>
-
-</template>
-
-<style scoped lang="scss">
-
-
-.base-card-container {
-  margin-bottom: 20px;
-}
-
-.warning-item-inner {
-  position: relative;
-  padding: 20px 8px 8px 8px;
-  border-radius: 4px;
-  background: #DDE5EF;
-
-  .tips {
-    position: absolute;
-    width: 36px;
-    height: 14px;
-    top: 0;
-    right: 0px;
-    border-radius: 0px 4px 0px 4px;
-    font-size: 8px;
-    text-align: center;
-    line-height: 14px;
-
-    &_warning,
-    &_being {
-      color: #F44C49;
-      background: #FFF0ED;
-    }
-
-    &_success {
-      color: #51BF8E;
-      background: #E9FAF2;
-    }
-
-    &_close {
-      color: #999999;
-      background: #D5D5D5;
-    }
-  }
-}
-
-.warning-info {
-  line-height: 16px;
-  font-size: 11px;
-  color: #5E5E5E;
-
-  dd {
-    margin-top: 4px;
-  }
-
-  &_medium {
-    line-height: 26px;
-    font-size: 14px;
-    color: #1A2029;
-  }
-}
-
-// 回答区域卡片
-.waring-answer-wrapper {
-  @include flex(x, start, between);
-
-  .message-inner {
-    width: 194px;
-    flex-shrink: 0;
-  }
+  <CustomModal
+    v-model:visible="visible"
+    :current-data="modalData"
+    @on-submit="handleSendSimulate"
+    ></CustomModal>
 
-  .table-inner {
-    @include flex(y, end, center);
-    padding-left: 20px;
-    border-left: 1px solid #F1F1F1;
-    .warning-table {
-      .title {
-        margin-bottom: 8px;
-        line-height: 16px;
-        font-size: 12px;
-        font-weight: bold;
-        color: #1A2029;
-      }
-    }
-  }
-}
-
-.radio-wrapper {
-
-  .radio-btn-group {
-    @include flex(x, center, center);
-    font-size: 14px;
-    text-align: center;
-    color: #5E5E5E;
-
-    .radio-btn {
-      width: 62px;
-      height: 28px;
-      border-radius: 4px;
-      background: #F4F6F8;
-      font-size: 14px;
-      line-height: 26px;
-      cursor: pointer;
-
-      &.active, &:hover {
-        color: #2454FF;
-        background: #E2F1FF;
-      }
-      &.active {
-        background: #E2F1FF url('@/assets/images/chat/bg-raido-check.png') right bottom no-repeat;
-        
-      }
-    }
-  }
-
-}
-</style>
+</template>

+ 287 - 0
src/views/analyse/WorkOrder.vue

@@ -0,0 +1,287 @@
+<script setup>
+import { ref, unref, computed, onUnmounted } from 'vue';
+import { useMessage, NDatePicker } from 'naive-ui';
+import { BaseButton, RecodeCardItem, TheSubMenu, TheChatView, ChatWelcome, SvgIcon } from '@/components';
+import { ChatAsk, ChatAnswer } from '@/components/Chat';
+import { chatApi } from '@/api/chat';
+
+import {useInfinite, useScroll, useChat, useRecommend} from '@/composables';
+
+const ANSWER_ID_KEY = '@@id@@';
+
+let controller = new AbortController();
+
+const { recordList, isFetching, onScrolltolower, onReset, addHistoryRecord } = useInfinite('/front/bigModel/qa/pageList', { module: 1 });
+const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll();
+const { chatDataSource, addChat, updateChat, clearChat, updateById } = useChat();
+
+const message = useMessage();
+const reportDate = ref();
+const switchActive = ref(false);
+
+const isLoading = ref(false);
+const inputRef = ref(null);
+
+const currenSessionId = ref(null);
+
+const isExistInHistory = computed(() => (recordList.value.findIndex(({ sessionId: sId }) => sId === unref(currenSessionId)) === -1));
+
+// 新建对话
+const handleCreateDialog = async () => {
+  message.destroyAll();
+
+  if (unref(isLoading)) {
+    return message.warning('当前对话生成中');
+  }
+
+  if (!unref(chatDataSource).length) {
+    return message.info('已切换最新会话');
+  }
+
+  currenSessionId.value = null;
+
+  clearChat();
+}
+
+// 查询对话详情
+const handleChatDetail = async ({ sessionId }) => {
+  isLoading.value = false;
+
+  controller.abort();
+
+  const { data } = await chatApi.getAnswerHistoryDetail({ sessionId });
+
+  chatDataSource.value = data.map(item => ({ ...item, loading: false,  }));
+  currenSessionId.value = sessionId;
+
+  scrollToBottom();
+}
+
+const onRegenerate = async ({ question, realQuestion }) => {
+  controller = new AbortController();
+
+  const sessionId = unref(currenSessionId);
+  const params = {
+    data: {
+      sessionId,
+      showVal: question,
+      question: realQuestion || question,
+      module: 1,
+      isStrong: Number(unref(switchActive)),
+      reportDate: reportDate.value
+    },
+    signal: controller.signal,
+    onDownloadProgress: ({ event }) => {
+      const xhr = event.target;
+      const { responseText } = xhr;
+      const [ answer ] = responseText.split(ANSWER_ID_KEY);
+
+      updateChat({
+        sessionId,
+        showVal:question,
+        answer,
+        loading: true,
+        delayLoading: false,
+      })
+
+      scrollToBottomIfAtBottom();
+    }
+  }
+
+  try {
+    const { data } = await chatApi.getChatStream(params);
+ 
+    const [ answer, id ] = data.split(ANSWER_ID_KEY);
+
+    updateChat({
+      id,
+      sessionId,
+      showVal: question,
+      answer,
+      loading: false,
+      delayLoading: false
+    })
+    
+    scrollToBottomIfAtBottom();
+  }
+  catch (error){
+    console.log("取消了请求 - catch", error);
+  }
+  finally {
+    isLoading.value = false;
+    onReset();
+  }
+}
+// 提交问题
+const handleSubmit = async (question, realQuestion = '') => {
+  // 用于模拟 - 内容生成前置等待状态
+  
+  if (unref(isExistInHistory)) {
+    const { data: sessionId } = await chatApi.getChatSessionTag();
+    currenSessionId.value = sessionId;
+  }
+  
+  isLoading.value = true;
+
+  addChat({
+    sessionId: unref(currenSessionId),
+    showVal: question,
+    realQuestion,
+    answer: '',
+    loading: true,
+    delayLoading: true,
+  })
+
+  scrollToBottom();
+
+  setTimeout(() => onRegenerate({ question, realQuestion }), 2 * 1000);
+}
+
+// 处理推荐问题
+const handleCreateOrder = () => {
+  
+  if ( !reportDate.value ) {
+    return message.warning('请选择日期');
+  }
+
+  handleSubmit(`请生成${reportDate.value}智能工单分析报告`);
+}
+
+// 删除历史对话
+const handeChatDelete = async (id) => {
+  await chatApi.deleteHistory(id);
+  onReset();
+  clearChat();
+  message.success('删除成功');
+}
+
+onUnmounted(() => {
+  controller.abort();
+})
+</script>
+
+<template>
+  <section class="flex items-start h-full">
+
+    <TheSubMenu title="智能工单" @scrollToLower="onScrolltolower" :loading="isFetching">
+      <template #top>
+        <div class="create-btn px-[11px] pb-[22px]">
+          <BaseButton @click="handleCreateDialog">新建工单</BaseButton>
+        </div>
+      </template>
+
+      <div class="pr-[4px] text-[#5e5e5e]">
+        <RecodeCardItem v-for="item, index in recordList" :key="item.sessionId + index" :title="item.showVal"
+          :time="item.createTime" :data-item="item" @on-click="handleChatDetail" @on-delete="handeChatDelete" />
+      </div>
+    </TheSubMenu>
+
+    <TheChatView ref="scrollRef" :is-footer="false">
+      <ChatWelcome 
+        title="您好,我是LibraAI智慧工单助手" 
+        card-title="您可以试着问我:"
+        :sub-title="[
+          '基于大语言模型的智能数据分析助手,可以为您实现数据分析及数据解读',
+          '选择日期并填写关键信息为您生成日报工单'
+        ]"
+        v-if="!chatDataSource.length"
+      />
+
+      <div class="conversation-item" v-if="chatDataSource.length">
+        <template v-for="item in chatDataSource" :key="item.id">
+          <ChatAsk :content="item.showVal" :sessionId="item.sessionId"></ChatAsk>
+          <ChatAnswer
+            :id="item.id"
+            :content="item.answer"
+            :loading="item.loading"
+            :delay-loading="item.delayLoading"
+            :isSatisfied="item.isSatisfied"
+            @on-click-icon=" params => updateById(params)"
+          ></ChatAnswer>
+        </template>
+      </div>
+
+      <div class="order-container px-[60px] py-[30px] mt-[36px] rounded-[10px] bg-[#fff]" v-if="!chatDataSource.length">
+        <div class="flex items-end justify-start space-x-[16px] pb-[20px] border-b-[1px] border-solid border-[#F1F1F1]">
+          <span class="text-[20px] leading-[28px] font-bold">智慧工单报告</span>
+          <span class="text-[12px] text-[#8F959C]">选择日期后为您生成日报工单</span>
+        </div>
+
+        <main class="order pt-[20px] text-[#1A2029]">
+          <div class="flex items-center justify-start text-[16px] space-x-[16px]">
+            <span class="font-bold">选择时间</span>
+            <div class="w-[164px] border-[1px] border-[#EFEFF0] rounded-[8px] overflow-hidden">
+              <NDatePicker placeholder="选择日期" :readonly="true" v-model:formatted-value="reportDate" value-format="yyyy-MM-dd">
+                <template #date-icon>
+                  <SvgIcon name="tool-arrow-bottom"></SvgIcon>
+                </template>
+              </NDatePicker>
+            </div>
+          </div>
+
+          <dl class="pt-[26px] text-[#1A2029] space-y-[16px]">
+            <dt class="text-[16px] font-bold leading-[22px]">报告内容</dt>
+            <dd>1、水质数据、生化数据情况</dd>
+            <dd>2、各项指标数据分析</dd>
+            <dd>3、对于工艺调整方面、数据趋势方面的措施与建议</dd>
+          </dl>
+        </main>
+
+        <footer class="pt-[24px]">
+          <button class="btn-primary" @click="handleCreateOrder">立即生成</button>
+        </footer>
+      </div>
+    </TheChatView>
+  </section>
+</template>
+
+<style scoped lang="scss">
+.n-input__input-el {
+  font-size: 12px !important;
+}
+
+.list-item {
+  margin-bottom: 16px;
+
+  .title {
+    font-size: 14px;
+    font-weight: bold;
+    line-height: 20px;
+  }
+
+  .item-top {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 10px;
+
+    .num {
+      font-size: 12px;
+      font-weight: 400;
+      color: #B0B7C0;
+    }
+  }
+}
+
+
+.btn-primary {
+  width: 88px;
+  height: 32px;
+  border-radius: 6px;
+  background-color: #2454FF;
+  font-size: 14px;
+  line-height: 32px;
+  color: #fff;
+
+  &:hover {
+    background: #1D43CC;
+  }
+}
+</style>
+
+<style lang="scss">
+.order {
+  .n-input__input-el, .n-input__placeholder {
+    font-size: 14px;
+  }
+}
+</style>

+ 76 - 41
src/views/analyse/components/CustomModal.vue

@@ -1,13 +1,71 @@
 <script setup>
-import { ref, defineModel } from 'vue';
+import { ref, defineModel, computed, unref } from 'vue';
 import { NModal, NInput } from 'naive-ui';
+import { SIMULATE_ENUM } from './config';
+import { truncateDecimals } from "@/utils/format";
+
+const props = defineProps({
+  currentData: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+const emit = defineEmits(['on-submit']);
 
 const modelVisible = defineModel('visible');
-const showModal = ref(true);
 
+const simulateData = computed(() => {
+  const data = props.currentData;
+  const usefulkeys = ['on', 'off'];
+  const resultObj = {};
+
+  usefulkeys.forEach(key => {
+    const tempArr = data[key];
+
+    resultObj[key] = tempArr.map(item => {
+      return {
+        ...item,
+        label: SIMULATE_ENUM[item.name],
+        inpVal: Array.isArray( item.value ) ? item.value.join() : item.value,
+        errMsg: ''
+      }
+    })
+  })
+  console.log("resultObj", resultObj);
+  return resultObj;
+})
+
+// 关闭弹窗
 const handleCancel = () => {
   modelVisible.value = false;
 }
+
+// 发起预测
+const handleStartReport = () => {
+  const usefulkeys = ['on', 'off'];
+  const simulate = {};
+  const table = {
+    header: [],
+    body: []
+  };
+  usefulkeys.forEach(key => {
+    simulateData.value[key].forEach(item => {
+      
+      simulate[item.name] = Array.isArray(item.value) ? item.inpVal.split(",") : item.inpVal;
+
+      table.header.push(item.label);
+      table.body.push(item.inpVal);
+    })
+  })
+  
+  console.log( simulateData.value );
+  console.log( simulate );
+  handleCancel();
+  emit('on-submit', { simulate: JSON.stringify(simulate), table })
+  console.log( "result", JSON.stringify(simulate), table );
+}
+
 </script>
 
 <template>
@@ -19,25 +77,16 @@ const handleCancel = () => {
   >
     <div class="modal-wrapper">
       <p class="header mb-[16px] font-bold text-[16px] leading-[22px]">水质预测推演 - 出水硝酸盐</p>
-
       <div class="content-card mb-[8px]">
         <p class="mb-[10px] font-bold text-[14px] leading-[20px]">可调参数</p>
         <ul class="inp-group grid grid-cols-2 gap-x-[24px]  gap-y-[8px]">
-          <li>
-            <span>碳源投加量(L/h)</span>
-            <input type="text">
-          </li>
-          <li>
-            <span>碳源投加量(L/h)</span>
-            <input type="text">
-          </li>
-          <li>
-            <span>碳源投加量(L/h)</span>
-            <input type="text">
-          </li>
-          <li>
-            <span>碳源投加量(L/h)</span>
-            <input type="text">
+          <li
+            class="flex items-center justify-start mt-[8px]" 
+            v-for="item,index in simulateData.on"
+            :key="index"
+          >
+            <span class="w-[120px] flex-shrink-0">{{ item.label }}</span>
+            <input type="text" v-model="item.inpVal">
           </li>
         </ul>
       </div>
@@ -45,25 +94,11 @@ const handleCancel = () => {
       <div class="content-card mb-[20px]">
         <p class="mb-[10px] font-bold text-[14px] leading-[20px]">相关参数</p>
         <ul class="grid grid-cols-2 gap-x-[24px]  gap-y-[10px]">
-          <li class="space-x-[8px]">
-            <span>好氧池水温</span>
-            <span>11.9</span>
-          </li>
-          <li class="space-x-[8px]">
-            <span>好氧池PH</span>
-            <span>11.9</span>
-          </li>
-          <li class="space-x-[8px]">
-            <span>好氧池硝酸盐</span>
-            <span>11.9</span>
-          </li>
-          <li class="space-x-[8px]">
-            <span>缺氧池氨氮</span>
-            <span>11.9</span>
-          </li>
-          <li class="space-x-[8px]">
-            <span>缺氧池硝酸盐</span>
-            <span>11.9</span>
+          <li class="space-x-[8px]" v-for="item,index in simulateData.off" :key="index">
+            <span class="text-[#5E5E5E]">{{ item.label }}</span>
+            <span class="text-[#1A2029] font-bold">
+              {{ item.inpVal }}
+            </span>
           </li>
         </ul>
       </div>
@@ -72,7 +107,7 @@ const handleCancel = () => {
         <p>*红色数字为建议调整数值</p>
         <div class="btn-group space-x-[16px]">
           <button class="btn btn_default" @click="handleCancel">取消</button>
-          <button class="btn btn_primary">发起预测</button>
+          <button class="btn btn_primary" @click="handleStartReport">发起预测</button>
         </div>
       </div>
     </div>
@@ -81,7 +116,7 @@ const handleCancel = () => {
 
 <style scoped lang="scss">
 .modal-wrapper {
-  width: 432px;
+  // width: 432px;
   padding: 30px 32px 20px 32px;
   border: 1px solid #fff;
   border-radius: 8px;
@@ -94,12 +129,12 @@ const handleCancel = () => {
 
     span {
       font-size: 12px;
-      color: #5E5E5E;
+      // color: #5E5E5E;
     }
 
     input {
       padding: 7px 13px;
-      margin-top: 8px;
+      // margin-top: 8px;
       border: 1px solid #D7D7D7;
       border-radius: 4px;
       background: #F8F8FA;

+ 15 - 0
src/views/analyse/components/config.js

@@ -0,0 +1,15 @@
+export const SIMULATE_ENUM = {
+  COD_in: '进水COD',
+  DO_O:         '好氧池DO (多池)',
+  MLSS:         '好氧池MLSS (多池)',
+  Q_in:         '进水流量',
+  tyjyl:        '碳源药剂投加量',
+  r:            '内回流比(多池)',
+  cltjl:        '除磷药剂投加量',
+  gwnl:         '出泥量(千泥)',
+  hycxsy_all:   '好氧硝酸盐(多池)',
+  qyan_all:     '缺氧氨氮(多池)',
+  qyckxsy_all:  '缺氧硝酸盐',
+  T:            '好氧池水温',
+  pH:           '好氧池'
+}

+ 24 - 10
src/views/answer/AnswerView.vue

@@ -1,21 +1,20 @@
 <script setup>
-import { ref, unref, computed, onMounted } from 'vue';
+import { ref, unref, computed, onMounted, onUnmounted } from 'vue';
 import { useMessage } from 'naive-ui';
+import { useChatStore } from '@/stores/modules/chatStore';
 import { 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';
-import { useRecommend } from '@/composables/useRecommend';
+import {useInfinite, useScroll, useChat, useRecommend} from '@/composables';
 
 const ANSWER_ID_KEY = '@@id@@';
 
 let controller = new AbortController();
 
-// TODO: 如果这里的key不一样,将会在拆一层组件出来 - list
-const { recordList, isFetching, onScrolltolower, onReset, addHistoryRecord } = useInfinite('/front/bigModel/qa/pageList', { model: 0 });
+const chatStore = useChatStore();
+
+const { recordList, isFetching, onScrolltolower, onReset } = useInfinite('/front/bigModel/qa/pageList', { module: 0 });
 const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll();
 const { chatDataSource, addChat, updateChat, clearChat, updateById } = useChat();
 const { recommendList } = useRecommend({type: 0});
@@ -71,6 +70,7 @@ const onRegenerate = async ({ question, realQuestion }) => {
   const params = {
     data: {
       sessionId,
+      showVal: question,
       question: realQuestion || question,
       module: 0,
       isStrong: Number(unref(switchActive))
@@ -106,6 +106,8 @@ const onRegenerate = async ({ question, realQuestion }) => {
       loading: false,
       delayLoading: false
     })
+    
+    scrollToBottomIfAtBottom();
   }
   catch (error){
     console.log("取消了请求 - catch", error);
@@ -152,6 +154,18 @@ const handeChatDelete = async (id) => {
   clearChat();
   message.success('删除成功');
 }
+
+onMounted(() => {
+  const question = chatStore.chatQuestion;
+  if ( Object.keys(question).length ) {
+    handleWelcomeRecommend(chatStore.chatQuestion);
+    chatStore.clearChatQuestion();
+  }
+})
+
+onUnmounted(() => {
+  controller.abort();
+})
 </script>
 
 <template>
@@ -165,9 +179,9 @@ const handeChatDelete = async (id) => {
 
       <div class="pr-[4px] text-[#5e5e5e]">
         <RecodeCardItem
-          v-for="item in recordList"
-          :key="item.sessionId"
-          :title="item.question"
+          v-for="item, index in recordList"
+          :key="item.sessionId + index"
+          :title="item.showVal"
           :time="item.createTime"
           :data-item="item"
           @on-click="handleChatDetail"

+ 302 - 0
src/views/helper/HelperView.vue

@@ -0,0 +1,302 @@
+<script setup>
+import { ref, unref, computed, onMounted, onUnmounted } from 'vue';
+import { useMessage } from 'naive-ui';
+import { BaseButton, RecodeCardItem, TheSubMenu, TheChatView, ChatWelcome, SvgIcon } from '@/components';
+import { ChatAsk, ChatAnswer, ChatInput } from '@/components/Chat';
+import { chatApi } from '@/api/chat';
+import { helperApi } from '@/api/helper';
+
+import { useInfinite, useScroll, useChat } from '@/composables';
+
+const ANSWER_ID_KEY = '@@id@@';
+
+let controller = new AbortController();
+
+// TODO: 如果这里的key不一样,将会在拆一层组件出来 - list
+const { recordList, isFetching, onScrolltolower, onReset, addHistoryRecord } = useInfinite('/front/bigModel/qa/pageList', { module: 2 });
+const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll();
+const { chatDataSource, addChat, updateChat, clearChat, updateById } = useChat();
+
+const helperList = ref([]);
+const message = useMessage();
+
+const switchActive = ref(false);
+
+const isLoading = ref(false);
+const inputRef = ref(null);
+
+const currenSessionId = ref(null);
+
+const isExistInHistory = computed(() => (recordList.value.findIndex(({ sessionId: sId }) => sId === unref(currenSessionId)) === -1));
+
+// 新建对话
+const handleCreateDialog = async () => {
+  message.destroyAll();
+
+  if (unref(isLoading)) {
+    return message.warning('当前对话生成中');
+  }
+
+  if (!unref(chatDataSource).length) {
+    return message.info('已切换最新会话');
+  }
+
+  inputRef.value.clearInpVal();
+
+  currenSessionId.value = null;
+
+  clearChat();
+}
+
+// 查询对话详情
+const handleChatDetail = async ({ sessionId }) => {
+  isLoading.value = false;
+
+  controller.abort();
+
+  const { data } = await chatApi.getAnswerHistoryDetail({ sessionId });
+
+  chatDataSource.value = data.map(item => ({ ...item, loading: false,  }));
+  currenSessionId.value = sessionId;
+
+  scrollToBottom();
+}
+
+const onRegenerate = async ({ question, realQuestion }) => {
+  controller = new AbortController();
+
+  const sessionId = unref(currenSessionId);
+  const params = {
+    data: {
+      sessionId,
+      showVal: question,
+      question: realQuestion || question,
+      module: 2,
+      isStrong: Number(unref(switchActive))
+    },
+    signal: controller.signal,
+    onDownloadProgress: ({ event }) => {
+      const xhr = event.target;
+      const { responseText } = xhr;
+      const [ answer ] = responseText.split(ANSWER_ID_KEY);
+
+      updateChat({
+        sessionId,
+        question,
+        answer,
+        loading: true,
+        delayLoading: false
+      })
+
+      scrollToBottomIfAtBottom();
+    }
+  }
+
+  try {
+    const { data  } = await chatApi.getChatStream(params);
+ 
+    const [ answer, id ] = data.split(ANSWER_ID_KEY);
+
+    updateChat({
+      id,
+      sessionId,
+      question,
+      answer,
+      loading: false,
+      delayLoading: false
+    })
+    
+    scrollToBottomIfAtBottom();
+  }
+  catch (error){
+    console.log("取消了请求 - catch", error);
+  }
+  finally {
+    isLoading.value = false;
+    onReset();
+  }
+}
+// 提交问题
+const handleSubmit = async (question, realQuestion = '') => {
+  // 用于模拟 - 内容生成前置等待状态
+  
+  if (unref(isExistInHistory)) {
+    const { data: sessionId } = await chatApi.getChatSessionTag();
+    currenSessionId.value = sessionId;
+  }
+  
+  isLoading.value = true;
+
+  addChat({
+    sessionId: unref(currenSessionId),
+    question,
+    realQuestion,
+    answer: '',
+    loading: true,
+    delayLoading: true
+  })
+
+  scrollToBottom();
+
+  setTimeout(() => onRegenerate({ question, realQuestion }), 2 * 1000);
+}
+
+// 处理推荐问题
+const handleWelcomeRecommend = ({ content }) => {
+  inputRef.value.inpVal = content;
+}
+
+// 删除历史对话
+const handeChatDelete = async (id) => {
+  await chatApi.deleteHistory(id);
+  onReset();
+  clearChat();
+  message.success('删除成功');
+}
+
+onMounted(async () => {
+  const { data } = await helperApi.getHelperList();
+  helperList.value = data;
+})
+
+onUnmounted(() => {
+  controller.abort();
+})
+</script>
+<template>
+  <section class="flex items-start h-full">
+    <TheSubMenu title="历史记录" @scrollToLower="onScrolltolower" :loading="isFetching">
+      <template #top>
+        <div class="create-btn px-[11px] pb-[22px]">
+          <BaseButton @click="handleCreateDialog">新建指令</BaseButton>
+        </div>
+      </template>
+
+      <div class="pr-[4px] text-[#5e5e5e]">
+        <RecodeCardItem
+          v-for="item, index in recordList"
+          :key="item.sessionId + index"
+          :title="item.showVal"
+          :time="item.createTime"
+          :data-item="item"
+          @on-click="handleChatDetail"
+          @on-delete="handeChatDelete"
+        />
+      </div>
+    </TheSubMenu>
+
+    <TheChatView ref="scrollRef">
+      <div v-if="!chatDataSource.length">
+        <ChatWelcome title="您好,我是LibraAI专家问答" card-title="您可以试着问我:"
+        :sub-title="[
+          'LibarAI智能助手模块,具有强大的文本理解和生成能力,可快速响应用户需求',
+          '提供撰写文章、生成报告等服务。'
+        ]"
+        />
+        <div class="grid-container">
+          <div class="grid-content">
+            <div
+              class="grid-item"
+              v-for="item in helperList"
+              :key="item.id"
+              @click="handleWelcomeRecommend(item)"
+            >
+              <div class="grid-item-icon space-x-[8px]">
+                <SvgIcon name="tool-report" size="24"></SvgIcon>
+                <h3 class="grid-item-title">{{item.title}}</h3>
+              </div>
+              <div class="text-[#5E5E5E] mt-[8px] text-justify">
+                <template v-if="item.content.indexOf('#') !== -1">
+                  <span v-for="word, i in item.content.split('#')" :key="word">
+                    {{ word }}<i v-if="i !== item.content.split('#').length - 1" class="text-[#2454FF]">#</i>
+                  </span>
+                </template>
+                <template v-else>
+                  <p>{{item.content}}</p>
+                </template>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+      
+      <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"
+            :loading="item.loading"
+            :delay-loading="item.delayLoading"
+            :isSatisfied="item.isSatisfied"
+            @on-click-icon=" params => updateById(params)"
+          ></ChatAnswer>
+        </template>
+      </div>
+
+      <template #footer>
+        <ChatInput
+          ref="inputRef"
+          v-model:loading="isLoading"
+          v-model:switch="switchActive"
+          @on-click="handleSubmit"
+          @on-enter="handleSubmit"
+        ></ChatInput>
+      </template>
+    </TheChatView>
+  </section>  
+</template>
+
+<style scoped lang="scss">
+.grid-container{
+	position: relative;
+	height: calc(100vh - 460px);
+  padding-bottom: 20px;
+  margin-top: 36px;
+	overflow: hidden;
+	overflow-y: scroll;
+  
+  &::-webkit-scrollbar {
+    width: 0px;
+  }
+
+	.grid-content{
+		column-count: 3; 
+		column-gap: 16px;
+		-moz-column-count: 3;
+		-webkit-column-count: 3;
+		-moz-column-gap: 16px;
+		-webkit-column-gap: 16px;
+
+		.grid-item{
+			height: auto;
+			padding: 16px;
+			margin-bottom: 16px;
+			border-radius: 10px;
+			box-shadow: 0 2px 4px rgba(0,0,0,0.08);
+			background-color: #fff;
+			-webkit-column-break-inside: avoid;
+			break-inside: avoid;
+      border:1px solid #fff;
+      cursor: pointer;
+
+      &:hover {
+        border:1px solid #2454FF;
+      }
+
+			.grid-item-icon{
+				display: flex;
+				align-items: center;
+				justify-content: start;
+
+				.grid-item-title{
+					font-size: 14px;
+					font-weight: 600;
+					color: #1A2029;
+				}
+			}
+		}
+	}
+}
+
+</style>

+ 3 - 3
src/views/login/LoginView.vue

@@ -13,8 +13,8 @@ const errorMsg = ref('');
 const loading = ref(false);
 
 const loginFormData = ref({
-  username: 'admin',
-  password: 'admin123'
+  username: '',
+  password: ''
 })
 
 const handleSubmit = async () => {
@@ -37,7 +37,7 @@ const handleSubmit = async () => {
     router.push("/");
   }
   catch (error) {
-    errorMsg.value = error.message;
+    errorMsg.value = error.msg;
   }
   finally {
     loading.value = false;

+ 2 - 2
src/views/screen/ScreenView.vue

@@ -98,9 +98,9 @@ onBeforeUnmount(() => {
     <div class="screen-container">
       <div class="menu">
         <RouterLink to="/" class="item item1">智慧总控</RouterLink>
-        <RouterLink to="/answer" class="item item2">专家问答a</RouterLink>
+        <RouterLink to="/answer" class="item item2">专家问答</RouterLink>
         <RouterLink to="/water-warn" class="item item3">智能分析</RouterLink>
-        <RouterLink to="/answer" class="item item4">智能助手</RouterLink>
+        <RouterLink to="/helper" class="item item4">智能助手</RouterLink>
       </div>
       <div class="screen-container-main">
         <div class="left">

+ 1 - 1
src/views/screen/components/dataBox.vue

@@ -19,7 +19,7 @@ const content = '① 因房地产市场并不十分活跃和顺利运转,因
         <h5 class="title">{{ reportData.showVal }}</h5>
         <ChatText :content="reportData.answer" class="html-box"></ChatText>
       </div>
-      <RouterLink to="/answer" class="btn">更多数据分析</RouterLink>
+      <RouterLink to="/work-order" class="btn">更多数据分析</RouterLink>
     </div>
   </div>
 </template>

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.