WaterView.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  1. <script setup lang="jsx">
  2. import { ref } from 'vue';
  3. import { useRouter } from 'vue-router';
  4. import { NTabs, NTab } from 'naive-ui';
  5. import { useChatStore } from '@/stores/modules/chatStore';
  6. import { BaseTable, ChatWelcome, RecodeSquareCardItem, TheSubMenu, TheChatView } from "@/components";
  7. import { ChatBaseCard, ChatAnswer } from '@/components/Chat';
  8. import { format, truncateDecimals } from "@/utils/format";
  9. import { waterApi } from '@/api/water';
  10. import { CustomModal } from "./components";
  11. import { useInfinite } from '@/composables/useInfinite';
  12. import { useRecommend } from '@/composables/useRecommend';
  13. import { useFetchStream } from '@/composables/useFetchStream';
  14. import { useScroll } from '@/composables/useScroll';
  15. import { SIMULATE_ENUM } from './components/config';
  16. const { recommendList } = useRecommend({type: 1});
  17. const { scrollRef, scrollToTop, scrollToBottom, scrollToBottomIfAtBottom } = useScroll();
  18. const { refetch, cancelFetch } = useFetchStream("/grpc/decisionStream", { methdos: 'POST' }, false);
  19. const { recordList, isFetching, onScrolltolower, onRestore } = useInfinite('/front/bigModel/warning/pageList', { type: 0, warningStatus: 0 });
  20. const router = useRouter();
  21. const chatStore = useChatStore();
  22. // 回答列表
  23. const answerResult = ref([]);
  24. // 获取最终回答流数据参数
  25. const flowParams = {
  26. feedback: '',
  27. category: '',
  28. warningId: '',
  29. simulate: '{}'
  30. };
  31. const answerLoading = ref(false);
  32. const textDataSources = ref(null);
  33. // 进出水数据
  34. const jsTableData = ref([]);
  35. const csTableData = ref([]);
  36. const visible = ref(false);
  37. const modalData = ref({});
  38. const renderRowDom = ({ row, key }) => {
  39. const { exceed, value } = row[key] || {};
  40. const cls = exceed ? 'text-[#F44C49] font-bold' : 'text-[1A2029]'
  41. return (<span class={ cls }>{truncateDecimals(value)} {exceed && <i>↑</i>}</span>)
  42. }
  43. const columns = [
  44. {
  45. title: '流量(m³/h)',
  46. key: 'name',
  47. titleAlign: 'center',
  48. align: 'center',
  49. className: 'small',
  50. width: '80px',
  51. render: (row) => renderRowDom({ row, key: '流量' })
  52. },
  53. {
  54. title: 'COD(mg/L)',
  55. key: 'small',
  56. titleAlign: 'center',
  57. align: 'center',
  58. className: 'small',
  59. width: '80px',
  60. render: (row) => renderRowDom({ row, key: 'COD' })
  61. },
  62. {
  63. title: 'TN(mg/L)',
  64. key: 'address',
  65. titleAlign: 'center',
  66. align: 'center',
  67. className: 'small',
  68. width: '80px',
  69. render: (row) => renderRowDom({ row, key: 'TN' })
  70. },
  71. {
  72. title: 'NH3-N(mg/L)',
  73. key: 'tags',
  74. titleAlign: 'center',
  75. align: 'center',
  76. className: 'small',
  77. width: '80px',
  78. render: (row) => renderRowDom({ row, key: 'NH3-N' })
  79. },
  80. {
  81. title: 'TP(mg/L)',
  82. key: 'COD',
  83. titleAlign: 'center',
  84. align: 'center',
  85. className: 'small',
  86. width: '80px',
  87. render: (row) => renderRowDom({ row, key: 'TP' })
  88. },
  89. {
  90. title: 'SS(mg/L)',
  91. key: '流量',
  92. titleAlign: 'center',
  93. align: 'center',
  94. className: 'age',
  95. width: '78px',
  96. render: (row) => renderRowDom({ row, key: 'SS' })
  97. }
  98. ]
  99. const handleModelVisible = () => {
  100. visible.value = true;
  101. }
  102. const resetConfiguration = () => {
  103. /**
  104. * 临时这样,后续统一处理
  105. * */
  106. textDataSources.value = '';
  107. answerLoading.value = false;
  108. answerResult.value = [];
  109. flowParams.feedback = '';
  110. flowParams.category = '';
  111. flowParams.warningId = '';
  112. flowParams.simulate = '{}';
  113. cancelFetch();
  114. }
  115. /**
  116. * 报警详情
  117. */
  118. const handleOpenContent = async ({ id, category }) => {
  119. if ( id == flowParams.warningId ) return;
  120. flowParams.category = category;
  121. flowParams.warningId = id;
  122. flowParams.feedback = '';
  123. flowParams.simulate = '{}';
  124. answerLoading.value = false;
  125. cancelFetch();
  126. const { data } = await waterApi.getWaringDetails(id);
  127. const res = await waterApi.getWaringForecast(id);
  128. const showVal = JSON.parse(data.showVal);
  129. const { basic, jsData, csData } = showVal;
  130. const answer = JSON.parse(data.answer);
  131. cancelFetch();
  132. answerResult.value = [];
  133. const reportList = [];
  134. const alertList = [];
  135. answer.map(item => {
  136. const answerObjItem = JSON.parse( item );
  137. if ( answerObjItem.biz === "DECISION_REPORT" ) {
  138. reportList.push(answerObjItem.message);
  139. // const answerContent = answer.map(item => {
  140. // const itemParse = JSON.parse(item);
  141. // return itemParse.message;
  142. // }).join("");
  143. // answerResult.value.push({
  144. // biz: 'DECISION_REPORT',
  145. // answer: answerContent,
  146. // loading: false,
  147. // delayLoading: false
  148. // })
  149. }
  150. if( answerObjItem.biz === "DECISION_ALERT" ) {
  151. alertList.push(item);
  152. // const [ parseAnswer ] = answer.map(item => {
  153. // const result = JSON.parse( item );
  154. // result.message = Object.keys(result.message).map(key => ({ ...result.message[key], isActive: null }));
  155. // return result;
  156. // })
  157. // answerResult.value.push({
  158. // biz: 'DECISION_ALERT',
  159. // loading: false,
  160. // delayLoading: false,
  161. // isAllSelect: false,
  162. // list: parseAnswer?.message
  163. // })
  164. }
  165. if (answerObjItem.biz === "DECISION_SIMULATE") {
  166. // const usefulkeys = ['on', 'off'];
  167. // const resultObj = {};
  168. // usefulkeys.forEach(key => {
  169. // const tempArr = data[key];
  170. // resultObj[key] = tempArr.map(item => {
  171. // return {
  172. // ...item,
  173. // label: SIMULATE_ENUM[item.name],
  174. // inpVal: Array.isArray( item.value ) ? item.value.join() : item.value,
  175. // errMsg: ''
  176. // }
  177. // })
  178. // })
  179. }
  180. })
  181. if ( reportList.length ) {
  182. const answerContent = reportList.join("");
  183. answerResult.value.push({
  184. biz: 'DECISION_REPORT',
  185. answer: answerContent,
  186. loading: false,
  187. delayLoading: false
  188. })
  189. }
  190. if ( alertList.length ) {
  191. const [ parseAnswer ] = alertList.map(item => {
  192. const result = JSON.parse( item );
  193. result.message = Object.keys(result.message).map(key => ({ ...result.message[key], isActive: null }));
  194. return result;
  195. })
  196. answerResult.value.push({
  197. biz: 'DECISION_ALERT',
  198. loading: false,
  199. delayLoading: false,
  200. isAllSelect: false,
  201. list: parseAnswer?.message
  202. })
  203. }
  204. // console.log( "reportList", reportList );
  205. // console.log( "alertList", alertList );
  206. // const [ answerStrItem ] = answer;
  207. // const answerObjItem = JSON.parse( answerStrItem );
  208. // console.log( "answerObjItem", answer, JSON.parse(answerObjItem.message) );
  209. const textWhiteList = [
  210. { label: '报警时间', realKey: '报警时间', value: '', isWarning: false },
  211. { label: '报警值', realKey: '报警值', value: 'mg/L', isWarning: true },
  212. { label: '管控值', realKey: '管控值', value: 'mg/L', isWarning: false },
  213. { label: '标准值', realKey: '标准值', value: 'mg/L', isWarning: false },
  214. { label: '报警级别', realKey: '告警级别', value: '', isWarning: false },
  215. { label: '报警次数', realKey: '报警次数', value: '', isWarning: false },
  216. { label: '数据来源', realKey: '数据来源', value: '', isWarning: false },
  217. { label: '状态', realKey: '状态', value: '', isWarning: false }
  218. ]
  219. basic['数据来源'] = '在线仪表';
  220. textDataSources.value = format.textSorting(basic, textWhiteList);
  221. jsTableData.value = [jsData];
  222. csTableData.value = [csData];
  223. scrollToTop();
  224. }
  225. const onChangeTabs = warningStatus => {
  226. resetConfiguration();
  227. onRestore({ warningStatus });
  228. }
  229. // 生成流数据
  230. const onRegenerate = async () => {
  231. answerLoading.value = true;
  232. const len = answerResult.value.length ? answerResult.value.length : 0;
  233. const tempReport = {
  234. biz: 'DECISION_REPORT',
  235. answer: '',
  236. loading: true,
  237. delayLoading: true,
  238. };
  239. let tempSimulate = null;
  240. answerLoading.value = answerResult.value[len -1 ].biz !== 'DECISION_TABLE';
  241. const params = {
  242. body: JSON.stringify(flowParams),
  243. errorHandler: () => {
  244. },
  245. successHandler: data => {
  246. const item = JSON.parse(data);
  247. answerLoading.value = false;
  248. if (item.biz === 'DECISION_REPORT') {
  249. tempReport.answer += item.message;
  250. tempReport.delayLoading = false;
  251. answerResult.value[len] = { ...tempReport };
  252. }
  253. if (item.biz === 'DECISION_ALERT') {
  254. const list = Object.keys(item.message).map(key => ({ ...item.message[key], isActive: null }));
  255. answerResult.value.push({
  256. biz: 'DECISION_ALERT',
  257. loading: true,
  258. delayLoading: false,
  259. isAllSelect: false,
  260. list
  261. })
  262. }
  263. if (item.biz === 'DECISION_SIMULATE') {
  264. const lastAnswerItem = answerResult.value[len - 1];
  265. if ( lastAnswerItem.biz === 'DECISION_TABLE' ) {
  266. answerResult.value[len - 1] = {
  267. ...lastAnswerItem,
  268. content: JSON.parse(item.message).pred.join(", ")
  269. }
  270. } else {
  271. const { off, on, pred } = JSON.parse(item.message);
  272. tempSimulate = {
  273. biz: 'DECISION_SIMULATE',
  274. off,
  275. on,
  276. pred,
  277. isDisable: false
  278. }
  279. modalData.value = tempSimulate;
  280. }
  281. }
  282. scrollToBottomIfAtBottom();
  283. }
  284. }
  285. try {
  286. await refetch(params);
  287. const answerItem = answerResult.value[answerResult.value.length - 1];
  288. if (answerItem?.biz) {
  289. answerItem.loading = false;
  290. answerItem.delayLoading = false;
  291. if (answerItem.biz === 'DECISION_TABLE') {
  292. scrollToBottom()
  293. }
  294. }
  295. if (tempSimulate) {
  296. answerResult.value.push(tempSimulate);
  297. }
  298. setTimeout(() => {
  299. scrollToBottomIfAtBottom();
  300. }, 500)
  301. }
  302. catch(error) {
  303. console.log("exist error .....", error);
  304. }
  305. }
  306. // 回答选项点击
  307. const handlerAlertOptions = (item, val, index) => {
  308. const { list, isAllSelect } = item;
  309. if ( isAllSelect ) return;
  310. val.isActive = index;
  311. const isExists = list.find(({ isActive }) => isActive === null);
  312. if ( !isExists ) {
  313. item.isAllSelect = true;
  314. const result = item.list
  315. .map(({ id, options, isActive }) => ({ [id]: options[isActive] }))
  316. .reduce((accumulator, currentValue) => {
  317. Object.keys(currentValue).forEach(key => accumulator[key] = currentValue[key]);
  318. return accumulator;
  319. }, {});
  320. flowParams.feedback = JSON.stringify(result);
  321. onRegenerate();
  322. }
  323. }
  324. // 开始预测
  325. const handleSendSimulate = ({ simulate, table }) => {
  326. const len = answerResult.value.length;
  327. flowParams.simulate = simulate;
  328. answerResult.value[len - 1].isDisable = true;
  329. answerResult.value.push({
  330. biz: 'DECISION_TABLE',
  331. loading: true,
  332. delayLoading: false,
  333. table,
  334. isDisable: false
  335. })
  336. onRegenerate();
  337. }
  338. // 欢迎页提交
  339. const handleWelcomeRecommend = question => {
  340. chatStore.setChatQuestion(question);
  341. router.push('/answer');
  342. }
  343. </script>
  344. <template>
  345. <section class="flex items-start h-full" id="warning">
  346. <TheSubMenu title="水质报警" @scrollToLower="onScrolltolower" :loading="isFetching">
  347. <template #top>
  348. <div class="border-[#DAE5ED]">
  349. <n-tabs type="line" justify-content="space-evenly">
  350. <n-tab name="oasis" tab="正在报警" @click="onChangeTabs(0)"></n-tab>
  351. <n-tab name="thebeatles" tab="历史报警" @click="onChangeTabs(1)"></n-tab>
  352. </n-tabs>
  353. </div>
  354. </template>
  355. <div class="px-[12px] py-[14px] text-[#5e5e5e]">
  356. <div class="grid grid-cols-1 gap-[12px]">
  357. <RecodeSquareCardItem
  358. v-for="item in recordList"
  359. :key="item.id"
  360. :item="item"
  361. @on-click="handleOpenContent"
  362. />
  363. </div>
  364. </div>
  365. </TheSubMenu>
  366. <TheChatView ref="scrollRef" :is-footer="false">
  367. <ChatWelcome title="您好,我是LibraAI工艺管控助手" card-title="常见处理方案:"
  368. :sub-title="[
  369. '水质报警功能针对五大核心指标实时监测,发现异常后将推送给相关人员决策方案',
  370. '报警时间为每小时警报,请大家及时处理'
  371. ]"
  372. :card-content="recommendList"
  373. @on-click="handleWelcomeRecommend"
  374. v-if="!textDataSources"
  375. />
  376. <ChatBaseCard v-if="textDataSources">
  377. <div class="waring-answer-wrapper">
  378. <dl class="message-inner warning-info_medium ">
  379. <dt class="mb-[2px] font-bold text-[#1A2029]">{{ textDataSources?.title }}</dt>
  380. <dd v-for="item, index in textDataSources?.list" :key="index"><span :class="{'text-[#F44C49]': item.isWarning}">{{ item.label }}: {{ item.value }}</span></dd>
  381. </dl>
  382. <div class="table-inner">
  383. <div class="warning-table mb-[8px]">
  384. <div class="title">
  385. <span>当前进水数据:</span>
  386. </div>
  387. <div class="main">
  388. <BaseTable :columns="columns" :data="jsTableData"></BaseTable>
  389. </div>
  390. </div>
  391. <div class="warning-table">
  392. <div class="title">
  393. <span>当前出水数据:</span>
  394. </div>
  395. <div class="main">
  396. <BaseTable :columns="columns" :data="csTableData"></BaseTable>
  397. </div>
  398. </div>
  399. </div>
  400. </div>
  401. </ChatBaseCard>
  402. <section v-for="item,index in answerResult" :key="index">
  403. <template v-if="item.biz === 'DECISION_REPORT'">
  404. <ChatAnswer
  405. :loading="item.loading"
  406. :delay-loading="item.delayLoading"
  407. :toggleVisibleIcons="false"
  408. :content="item.answer"
  409. ></ChatAnswer>
  410. </template>
  411. <template v-if="item.biz === 'DECISION_ALERT'">
  412. <ChatBaseCard
  413. :loading="item.loading"
  414. :delay-loading="item.delayLoading"
  415. :toggleVisibleIcons="false"
  416. >
  417. <p class="mb-[15px] font-bold text-[#1A2029]">需要确定以下问题,完成决策方案:</p>
  418. <ul class="radio-wrapper space-y-[14px]">
  419. <li class="flex items-center" v-for="val,i in item.list" :key="i">
  420. <p class="mr-[14px]">{{ val.mainContent }}</p>
  421. <p class="radio-btn-group space-x-[14px]">
  422. <span
  423. v-for="option,index in val.options"
  424. :class="['radio-btn', { active: val.isActive === index }]"
  425. @click="handlerAlertOptions(item, val, index)"
  426. >{{ option }}</span>
  427. </p>
  428. </li>
  429. </ul>
  430. </ChatBaseCard>
  431. </template>
  432. <template v-if="item.biz === 'DECISION_SIMULATE'">
  433. <button class="
  434. px-[30px] py-[10px] mb-[20px]
  435. rounded-[8px]
  436. bg-white text-[13px]
  437. text-[#5E5E5E] hover:text-[#2454FF]"
  438. :disabled="item.isDisable"
  439. @click="handleModelVisible"
  440. >
  441. 水质预测推演
  442. </button>
  443. </template>
  444. <template v-if="item.biz === 'DECISION_TABLE'">
  445. <ChatAnswer
  446. :loading="item.loading"
  447. :delay-loading="item.delayLoading"
  448. :toggleVisibleIcons="false"
  449. >
  450. <div class="markdown-body text-[15px] break-all">
  451. <strong class="block mb-[16px]">推荐指标调整:</strong>
  452. <table>
  453. <thead>
  454. <tr>
  455. <th v-for="text in item.table.header" :key="text">{{ text }}</th>
  456. </tr>
  457. </thead>
  458. <tbody>
  459. <tr>
  460. <td v-for="text in item.table.body" :key="text">{{ text }}</td>
  461. </tr>
  462. </tbody>
  463. </table>
  464. <strong class="block mb-[16px]">预测推演结果:</strong>
  465. <span>未来三小时好氧池硝酸盐预测结果:{{ item.content }}</span>
  466. </div>
  467. </ChatAnswer>
  468. <button class="
  469. px-[30px] py-[10px] mb-[20px]
  470. rounded-[8px]
  471. bg-white text-[13px]
  472. text-[#5E5E5E] hover:text-[#2454FF]"
  473. :disabled="item.isDisable"
  474. @click="handleModelVisible"
  475. >
  476. 水质预测推演
  477. </button>
  478. </template>
  479. </section>
  480. <ChatAnswer
  481. :loading="answerLoading"
  482. :delay-loading="answerLoading"
  483. :toggleVisibleIcons="false"
  484. v-show="answerLoading"
  485. loadingText="内容生成中,大概需要50秒..."
  486. ></ChatAnswer>
  487. </TheChatView>
  488. </section>
  489. <CustomModal
  490. v-model:visible="visible"
  491. :current-data="modalData"
  492. @on-submit="handleSendSimulate"
  493. ></CustomModal>
  494. </template>