Browse Source

feat: 水务报警接口联调

whh 10 months ago
parent
commit
e6acc64dc7

+ 2 - 2
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'
 
@@ -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>

+ 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;

+ 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>

+ 0 - 1
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],

+ 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 - 2
src/components/Layout/TheMenu.vue

@@ -54,7 +54,7 @@ const menuOptions = [
       {
         label: '生化报警',
         icon: renderChildrenIcon({ name: 'menu-analyse-pymol' }),
-        key: 'key2',
+        key: '/pymol-warn',
       },
       {
         label: '预测报警',
@@ -64,7 +64,7 @@ const menuOptions = [
       {
         label: '智能工单',
         icon: renderChildrenIcon({ name: 'menu-analyse-order' }),
-        key: 'key4',
+        key: '/work-order',
       }
     ]
   },

+ 32 - 61
src/components/RecodeSquareCardItem/index.vue

@@ -32,19 +32,43 @@ const warningType = computed(() => {
       cls: 'tips_warning'
     }
   }
-  if (item.type == 0) {
-    return waterWhite[item.type + '']
+  const earlyWaring = {
+    '0': {
+      label: '预警中',
+      cls: 'tips_warning'
+    },
+    '0': {
+      label: '已完成',
+      cls: 'tips_warning'
+    },
+  }
+
+  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 },
+    ]
+  }
+  if (item.type == 1) {
     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: item.warningVal, type: 'wraning' },
+      { label: '报警次数', value: item.counts },
     ]
   }
 });
@@ -71,57 +95,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>

+ 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("cancel success");
       error.value = err;
     } finally {
       loading.value = false;

+ 2 - 1
src/composables/useInfinite.js

@@ -37,8 +37,9 @@ export const useInfinite = (path, props) => {
   }
 
   const onReset = async () => {
+    console.log( "recordList", recordList.value );
     const { rows, total } = await initRecordData({page:1, pageSize: (pageParams.value.page * pageParams.value.pageSize) + 1});
-
+    console.log( "row", rows );
     recordList.value = rows;
     counter.value = total;
     noMore.value = !unref(isMore);

+ 16 - 0
src/router/index.js

@@ -38,6 +38,22 @@ const constantRouterMap = [
           title: '水质报警'
         }
       },
+      {
+        path: 'pymol-warn',
+        name: 'PymolWarn',
+        component: () => import('@/views/analyse/ScreenView.vue'),
+        meta: {
+          title: '生化报警'
+        }
+      },
+      {
+        path: 'work-order',
+        name: 'WorKOrder',
+        component: () => import('@/views/analyse/ScreenView.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,
+  }
+})

+ 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 } 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}>{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;
+  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 }
+  ]
+
+  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?.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>
+      
+      <ChatAnswer 
+        :loading="false"
+        :delay-loading="false"
+        :toggleVisibleIcons="false"
+        :content="answerResult"
+        v-if="answerResult"
+      />
+
+    </TheChatView>
+
+  </section>
+</template>
+
+<style scoped lang="scss"></style>

+ 159 - 237
src/views/analyse/WaterView.vue

@@ -1,8 +1,10 @@
 <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";
 
@@ -16,11 +18,14 @@ 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: '',
@@ -28,13 +33,9 @@ const flowParams = {
   warningId: ''
 };
 
-const reportAnswer = ref('');
-const alertAnswer = ref([]);
-
-const checkedAlertData = ref([]);
+const answerLoading = ref(false);
 
 const textDataSources = ref(null);
-const answerAlertDataSources = ref([]);
 
 // 进出水数据
 const jsTableData = ref([]);
@@ -105,7 +106,6 @@ const columns = [
   }
 ]
 
-const inWaterTableData = ref([{ name: 1233, actions: "7.87" }]);
 
 // 新建对话
 const handleCreateDialog = () => {
@@ -123,13 +123,23 @@ const handleModelVisible = () => {
 /**
  * 报警详情
 */
-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();
+
+  answerResult.value = [];
 
   console.log( answerObjItem.biz );
 
@@ -144,20 +154,33 @@ const handleOpenContent = async ({id, category}) => {
   ]
 
   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();
+
+    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
+    })
   }
 
   textDataSources.value = format.textSorting(basic, textWhiteList);
@@ -165,52 +188,77 @@ const handleOpenContent = async ({id, category}) => {
   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 list = [ {biz: 'DECISION_REPORT', list: []} ]
+
+  const len = answerResult.value.length ? answerResult.value.length : 0;
+ 
+  const tempReport = { 
+    biz: 'DECISION_REPORT',
+    answer: '',
+    loading: true,
+    delayLoading: true,
+  };
+  const params = {
+    body: JSON.stringify(flowParams),
+    errorHandler: () => {
+      
+    },
+    successHandler: data => {
+
+      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
+        })
 
-  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)
+      if (item.biz === 'DECISION_SIMULATE') {
+        console.log( item );
+        alert("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;
+    }
   }
   catch(error) {
     console.log("exist error .....", error);
@@ -218,30 +266,39 @@ 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 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]">
@@ -272,6 +329,7 @@ const handlerAlertOptions = (item, index) => {
           '工作人员确保问题及时处理。报警时间为每小时警报,请大家及时处理。'
         ]" 
         :card-content="recommendList"
+        @on-click="handleWelcomeRecommend"
         v-if="!textDataSources"
       />
   
@@ -302,69 +360,49 @@ const handlerAlertOptions = (item, index) => {
         </div>
       </ChatBaseCard>
 
-      <!-- report -->
+      {{  answerResult.length  }}
+
+      <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>
+      </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="
+      <!-- <button class="
         px-[30px] py-[10px] mb-[20px]
         rounded-[8px] 
         bg-white text-[13px] 
@@ -374,125 +412,9 @@ const handlerAlertOptions = (item, index) => {
         水质预测推演
       </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;
-  }
-
-  .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>

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

@@ -0,0 +1,172 @@
+<script setup>
+import { ref, unref, computed, onMounted } from 'vue';
+import { useRoute } from 'vue-router';
+import { useMessage, NDatePicker, NInput, NButton } from 'naive-ui'
+import { useChatStore } from '@/stores/modules/chatStore';
+import { BaseButton, RecodeCardItem, TheSubMenu, TheChatView, ChatWelcome, SvgIcon } 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';
+
+// TODO: 如果这里的key不一样,将会在拆一层组件出来 - list
+const { recordList, isFetching, onScrolltolower, onReset, addHistoryRecord } = useInfinite('/front/bigModel/qa/pageList', { model: 1 });
+const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll();
+const { chatDataSource, addChat, updateChat, clearChat, updateById } = useChat();
+const { recommendList } = useRecommend({ type: 0 });
+
+
+const oneText = ref('');
+const twoText = ref('');
+
+</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="[
+          '基于大语言模型的智能数据分析助手,可以为您实现数据分析及数据解读',
+          '选择日期并填写关键信息为您生成日报工单'
+        ]"
+        @on-click="handleWelcomeRecommend"
+        v-if="1"
+      />
+
+      <div class="order-container px-[60px] py-[30px] mt-[36px] rounded-[10px] bg-[#fff]">
+        <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">
+                <template #date-icon>
+                  <SvgIcon name="tool-arrow-bottom"></SvgIcon>
+                </template>
+              </NDatePicker>
+            </div>
+          </div>
+
+          <div class="flex items-end justify-start my-[20px] text-[16px] space-x-[16px]">
+            <span class="font-bold leading-[22px]">报告内容</span>
+            <span class="text-[12px] text-[#8F959C] leading-[16px]">可直接点击“立即生成”输出报告,也可以在输入框内调整您要分析的内容</span>
+          </div>
+
+          <ul class="flex flex-col text-[14px]">
+            <li class="list-item"><span class="title">1、水质数据、生化数据情况</span></li>
+            <li class="list-item">
+              <p class="item-top">
+                <span class="title">2、指标数据的分析</span>
+                <span class="num">{{ oneText.length }}/100</span>
+              </p>
+              <n-input
+                class="text-[14px] border-[1px] border-[#EFEFF0] bg-[#f8f8fa] rounded-[8px]"
+                clearable
+                maxlength="100"
+                placeholder="请输入您的建议,100字内"
+                type="textarea" 
+                size="large" 
+                :autosize="{ minRows: 1, maxRows: 2 }"
+                v-model:value="oneText"
+              />
+            </li>
+            <li class="list-item">
+              <p class="item-top">
+                <span class="title">3、根据分析法,输出分析报告</span>
+                <span class="num">{{ twoText.length }}/100</span>
+              </p>
+              <n-input
+                class="text-[14px] border-[1px] border-[#EFEFF0] bg-[#f8f8fa] rounded-[8px]"
+                clearable
+                maxlength="100"
+                placeholder="请输入您的建议,100字内"
+                type="textarea" 
+                size="large"
+                :autosize="{ minRows: 1, maxRows: 2 }"
+                v-model:value="oneText"
+              />
+            </li>
+          </ul>
+        </main>
+
+        <footer class="pt-[8px]">
+          <button class="btn-primary" @click="">立即生成</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>

+ 22 - 5
src/views/answer/AnswerView.vue

@@ -1,6 +1,8 @@
 <script setup>
-import { ref, unref, computed, onMounted } from 'vue';
+import { ref, unref, computed, onMounted, onUnmounted } from 'vue';
+import { useRoute } from 'vue-router';
 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';
@@ -14,6 +16,9 @@ const ANSWER_ID_KEY = '@@id@@';
 
 let controller = new AbortController();
 
+const route = useRoute();
+const chatStore = useChatStore();
+
 // TODO: 如果这里的key不一样,将会在拆一层组件出来 - list
 const { recordList, isFetching, onScrolltolower, onReset, addHistoryRecord } = useInfinite('/front/bigModel/qa/pageList', { model: 0 });
 const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll();
@@ -71,6 +76,7 @@ const onRegenerate = async ({ question, realQuestion }) => {
   const params = {
     data: {
       sessionId,
+      showVal: question,
       question: realQuestion || question,
       module: 0,
       isStrong: Number(unref(switchActive))
@@ -152,6 +158,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 +183,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"
@@ -186,7 +204,6 @@ 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>