WaterView.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  1. <script setup lang="jsx">
  2. import { ref, watch } from 'vue';
  3. import { useRouter } from 'vue-router';
  4. import { NTabs, NTab, NSelect } from 'naive-ui';
  5. import { useChatStore } from '@/stores/modules/chatStore';
  6. import { BaseTable, ChatWelcome, RecodeSquareCardItem, TheSubMenu, TheChatView } from "@/components";
  7. import { useInfinite, useRecommend, useFetchStream, useScroll } from '@/composables';
  8. import { ChatBaseCard, ChatAnswer } from '@/components/Chat';
  9. import { CustomModal } from "./components";
  10. import { inColumns, outColumns } from './config';
  11. import { formatToData } from "@/utils/format";
  12. import { waterApi } from '@/api/water';
  13. const { recommendList } = useRecommend({ type: 1 });
  14. const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll();
  15. const { refetch, cancelFetch } = useFetchStream("/grpc/decisionStream", { methdos: 'POST' }, false);
  16. const { recordList, isFetching, onScrolltolower, onRestore } = useInfinite('/front/bigModel/warning/pageList', { type: 0, warningStatus: 0 });
  17. const router = useRouter();
  18. const chatStore = useChatStore();
  19. // 回答列表
  20. const answerResult = ref([]);
  21. // 获取最终回答流数据参数
  22. const flowParams = {
  23. feedback: '',
  24. category: '',
  25. warningId: '',
  26. simulate: '{}'
  27. };
  28. const answerLoading = ref(false);
  29. const textDataSources = ref(null);
  30. const warningActive = ref(0);
  31. const subMenuRef = ref(null);
  32. // 进出水数据
  33. const jsTableData = ref([]);
  34. const csTableData = ref([]);
  35. const visible = ref(false);
  36. const modalData = ref({});
  37. // 菜单水数据类型
  38. const waterTypeValue = ref('');
  39. const options = [
  40. { label: '全部', value: '' },
  41. { label: '进水', value: 0 },
  42. { label: '出水', value: 1 }
  43. ]
  44. const mockData = [
  45. {
  46. "createBy": "task-job",
  47. "createTime": "2025-02-22 18:08",
  48. "updateBy": "task-job",
  49. "updateTime": "2025-02-23 09:08",
  50. "remark": "0",
  51. "id": 4287,
  52. "type": 0,
  53. "category": "进水SS",
  54. "time": "2025-02-23 08:08",
  55. "reason": "进水SS超标报警",
  56. "warningVal": 900,
  57. "designVal": 315,
  58. "controlVal": null,
  59. "forecastVal": null,
  60. "functionWay": 0,
  61. "level": "一级",
  62. "status": 2,
  63. "isEmergency": 0,
  64. "offTime": "2025-02-23 09:08:00",
  65. "operator": null,
  66. "review": null,
  67. "useRecommend": null,
  68. "waterType": 0,
  69. "symbol": 0,
  70. "cwrwxz": 2.8571,
  71. "cwrwfhz": 2.1813,
  72. "delFlag": 0,
  73. "revision": 15,
  74. "counts": "2",
  75. "warningValStr": null,
  76. "warningStatus": null,
  77. "symbolDesc": "超标准值"
  78. },
  79. {
  80. "createBy": "task-job",
  81. "createTime": "2025-02-22 19:08",
  82. "updateBy": "task-job",
  83. "updateTime": "2025-02-22 22:08",
  84. "remark": "0",
  85. "id": 4289,
  86. "type": 0,
  87. "category": "出水总氮",
  88. "time": "2025-02-22 21:08",
  89. "reason": "出水总氮超管控报警",
  90. "warningVal": 12.58,
  91. "designVal": 15,
  92. "controlVal": 12,
  93. "forecastVal": null,
  94. "functionWay": 0,
  95. "level": "三级",
  96. "status": 2,
  97. "isEmergency": 0,
  98. "offTime": "2025-02-22 22:08:00",
  99. "operator": null,
  100. "review": null,
  101. "useRecommend": null,
  102. "waterType": 1,
  103. "symbol": 1,
  104. "cwrwxz": 1.0699,
  105. "cwrwfhz": 0.282,
  106. "delFlag": 0,
  107. "revision": 3,
  108. "counts": "4",
  109. "warningValStr": null,
  110. "warningStatus": null,
  111. "symbolDesc": "超管控值"
  112. },
  113. {
  114. "createBy": "task-job",
  115. "createTime": "2025-02-20 03:08",
  116. "updateBy": "task-job",
  117. "updateTime": "2025-02-20 08:08",
  118. "remark": "0",
  119. "id": 4256,
  120. "type": 0,
  121. "category": "进水SS",
  122. "time": "2025-02-20 07:08",
  123. "reason": "进水SS超标报警",
  124. "warningVal": 900,
  125. "designVal": 315,
  126. "controlVal": null,
  127. "forecastVal": null,
  128. "functionWay": 0,
  129. "level": "一级",
  130. "status": 2,
  131. "isEmergency": 0,
  132. "offTime": "2025-02-20 08:08:20",
  133. "operator": null,
  134. "review": null,
  135. "useRecommend": null,
  136. "waterType": 0,
  137. "symbol": 0,
  138. "cwrwxz": 1.0698,
  139. "cwrwfhz": 0.6046,
  140. "delFlag": 0,
  141. "revision": 5,
  142. "counts": "2",
  143. "warningValStr": null,
  144. "warningStatus": null,
  145. "symbolDesc": "超标准值"
  146. },
  147. {
  148. "createBy": "task-job",
  149. "createTime": "2025-02-19 17:08",
  150. "updateBy": "task-job",
  151. "updateTime": "2025-02-19 21:08",
  152. "remark": "0",
  153. "id": 4247,
  154. "type": 0,
  155. "category": "进水总磷",
  156. "time": "2025-02-19 20:08",
  157. "reason": "进水总磷超标报警",
  158. "warningVal": 8.32,
  159. "designVal": 7.1,
  160. "controlVal": null,
  161. "forecastVal": null,
  162. "functionWay": 0,
  163. "level": "一级",
  164. "status": 2,
  165. "isEmergency": 0,
  166. "offTime": "2025-02-19 21:08:20",
  167. "operator": null,
  168. "review": null,
  169. "useRecommend": null,
  170. "waterType": 0,
  171. "symbol": 0,
  172. "cwrwxz": 1.1554,
  173. "cwrwfhz": 0.7731,
  174. "delFlag": 0,
  175. "revision": 4,
  176. "counts": "2",
  177. "warningValStr": null,
  178. "warningStatus": null,
  179. "symbolDesc": "超标准值"
  180. },
  181. ]
  182. watch(() => waterTypeValue.value, curValue => {
  183. onRestore({ warningStatus: warningActive.value, waterType: curValue });
  184. })
  185. const handleModelVisible = () => {
  186. visible.value = true;
  187. }
  188. const resetConfiguration = () => {
  189. /**
  190. * 临时这样,后续统一处理
  191. * */
  192. textDataSources.value = '';
  193. answerLoading.value = false;
  194. answerResult.value = [];
  195. flowParams.feedback = '';
  196. flowParams.category = '';
  197. flowParams.warningId = '';
  198. flowParams.simulate = '{}';
  199. cancelFetch();
  200. }
  201. /**
  202. * 报警详情
  203. */
  204. const handleOpenContent = async (item) => {
  205. const { id, category, reason: title, counts } = item;
  206. if (id == flowParams.warningId) return;
  207. flowParams.category = category;
  208. flowParams.warningId = id;
  209. flowParams.feedback = '';
  210. flowParams.simulate = '{}';
  211. answerLoading.value = false;
  212. answerResult.value = [];
  213. const { data } = await waterApi.getWaringDetails(id);
  214. const showVal = JSON.parse(data.showVal);
  215. const { basic, jsData, csData } = showVal;
  216. try {
  217. const answer = JSON.parse(data.answer);
  218. const reportList = [];
  219. const alertList = [];
  220. let simulateObj = null;
  221. answer.map(item => {
  222. const answerObjItem = JSON.parse(item);
  223. switch (answerObjItem.biz) {
  224. case "DECISION_REPORT":
  225. reportList.push(answerObjItem.message);
  226. break
  227. case "DECISION_ALERT":
  228. alertList.push(answerObjItem);
  229. break
  230. case "DECISION_SIMULATE":
  231. if (warningActive.value === 1) return;
  232. const { off, on, pred } = JSON.parse(answerObjItem.message);
  233. simulateObj = {
  234. biz: 'DECISION_SIMULATE',
  235. off,
  236. on,
  237. pred,
  238. isDisable: false
  239. }
  240. modalData.value = simulateObj;
  241. }
  242. })
  243. if (reportList.length) {
  244. answerResult.value.push({
  245. biz: 'DECISION_REPORT',
  246. answer: reportList.join(""),
  247. loading: false,
  248. delayLoading: false
  249. })
  250. }
  251. if (alertList.length) {
  252. const [parseAnswer] = alertList.map(item => {
  253. item.message = Object.keys(item.message).map(key => ({ ...item.message[key], isActive: null }));
  254. return item;
  255. })
  256. answerResult.value.push({
  257. biz: 'DECISION_ALERT',
  258. loading: false,
  259. delayLoading: false,
  260. isAllSelect: false,
  261. list: parseAnswer?.message
  262. })
  263. }
  264. if (simulateObj) {
  265. answerResult.value.push(simulateObj);
  266. }
  267. } catch (error) {
  268. answerResult.value.push({
  269. biz: 'DECISION_REPORT',
  270. answer: data.answer,
  271. loading: false,
  272. delayLoading: false
  273. })
  274. }
  275. cancelFetch();
  276. // answerResult.value = [];
  277. // const reportList = [];
  278. // const alertList = [];
  279. // let simulateObj = null;
  280. // answer.map(item => {
  281. // const answerObjItem = JSON.parse(item);
  282. // switch(answerObjItem.biz) {
  283. // case "DECISION_REPORT":
  284. // reportList.push(answerObjItem.message);
  285. // break
  286. // case "DECISION_ALERT":
  287. // alertList.push(answerObjItem);
  288. // break
  289. // case "DECISION_SIMULATE":
  290. // console.log( "DECISION_SIMULATE", answerObjItem.message );
  291. // if (warningActive.value === 1) return;
  292. // const { off, on, pred } = JSON.parse(answerObjItem.message);
  293. // simulateObj = {
  294. // biz: 'DECISION_SIMULATE',
  295. // off,
  296. // on,
  297. // pred,
  298. // isDisable: false
  299. // }
  300. // modalData.value = simulateObj;
  301. // }
  302. // })
  303. // if ( reportList.length ) {
  304. // answerResult.value.push({
  305. // biz: 'DECISION_REPORT',
  306. // answer: reportList.join(""),
  307. // loading: false,
  308. // delayLoading: false
  309. // })
  310. // }
  311. // if ( alertList.length ) {
  312. // const [ parseAnswer ] = alertList.map(item => {
  313. // item.message = Object.keys(item.message).map(key => ({ ...item.message[key], isActive: null }));
  314. // return item;
  315. // })
  316. // answerResult.value.push({
  317. // biz: 'DECISION_ALERT',
  318. // loading: false,
  319. // delayLoading: false,
  320. // isAllSelect: false,
  321. // list: parseAnswer?.message
  322. // })
  323. // }
  324. // if (simulateObj) {
  325. // answerResult.value.push(simulateObj);
  326. // }
  327. basic.title = title;
  328. textDataSources.value = formatToData({
  329. dataSource: basic,
  330. warnKey: '报警值',
  331. statusVal: !!warningActive.value ? '系统关闭' : basic['状态']
  332. });
  333. /**
  334. * 临时修改 - 后续需要删除
  335. * */
  336. textDataSources.value = {
  337. ...textDataSources.value,
  338. list: textDataSources.value.list.map(item => {
  339. if (item.label === '持续时间') item.value = counts;
  340. return item
  341. })
  342. }
  343. jsTableData.value = [jsData];
  344. csTableData.value = [csData];
  345. }
  346. const onChangeTabs = warningStatus => {
  347. resetConfiguration();
  348. warningActive.value = warningStatus;
  349. onRestore({ warningStatus });
  350. subMenuRef.value.scrollToTop();
  351. }
  352. // 生成流数据
  353. const onRegenerate = async () => {
  354. answerLoading.value = true;
  355. const len = answerResult.value.length ? answerResult.value.length : 0;
  356. const tempReport = {
  357. biz: 'DECISION_REPORT',
  358. answer: '',
  359. loading: true,
  360. delayLoading: true,
  361. };
  362. let tempSimulate = null;
  363. answerLoading.value = answerResult.value[len - 1].biz !== 'DECISION_TABLE';
  364. const feedback = flowParams.feedback
  365. const params = {
  366. body: JSON.stringify({ ...flowParams, feedback: JSON.stringify(feedback) }),
  367. errorHandler: () => { },
  368. successHandler: data => {
  369. const item = JSON.parse(data);
  370. answerLoading.value = false;
  371. if (item.biz === 'DECISION_REPORT') {
  372. tempReport.answer += item.message;
  373. tempReport.delayLoading = false;
  374. answerResult.value[len] = { ...tempReport };
  375. }
  376. if (item.biz === 'DECISION_ALERT') {
  377. const list = Object.keys(item.message).map(key => ({ ...item.message[key], isActive: null }));
  378. answerResult.value.push({
  379. biz: 'DECISION_ALERT',
  380. loading: true,
  381. delayLoading: false,
  382. isAllSelect: false,
  383. list
  384. })
  385. }
  386. if (item.biz === 'DECISION_SIMULATE') {
  387. const lastAnswerItem = answerResult.value[len - 1];
  388. if (lastAnswerItem.biz === 'DECISION_TABLE') {
  389. answerResult.value[len - 1] = {
  390. ...lastAnswerItem,
  391. content: JSON.parse(item.message).pred.join(", ")
  392. }
  393. } else {
  394. const { off, on, pred } = JSON.parse(item.message);
  395. tempSimulate = {
  396. biz: 'DECISION_SIMULATE',
  397. off,
  398. on,
  399. pred,
  400. isDisable: false
  401. }
  402. modalData.value = tempSimulate;
  403. }
  404. }
  405. scrollToBottomIfAtBottom();
  406. }
  407. }
  408. try {
  409. await refetch(params);
  410. const answerItem = answerResult.value[answerResult.value.length - 1];
  411. if (answerItem?.biz) {
  412. answerItem.loading = false;
  413. answerItem.delayLoading = false;
  414. if (answerItem.biz === 'DECISION_TABLE') {
  415. scrollToBottom()
  416. }
  417. }
  418. if (tempSimulate) {
  419. answerResult.value.push(tempSimulate);
  420. }
  421. setTimeout(() => {
  422. scrollToBottomIfAtBottom();
  423. }, 500)
  424. }
  425. catch (error) {
  426. console.log("exist error .....", error);
  427. }
  428. }
  429. // 回答选项点击
  430. const handlerAlertOptions = (item, val, index) => {
  431. const { list, isAllSelect } = item;
  432. if (isAllSelect) return;
  433. val.isActive = index;
  434. const isExists = list.find(({ isActive }) => isActive === null);
  435. if (!isExists) {
  436. item.isAllSelect = true;
  437. const result = item.list
  438. .map(({ id, options, isActive }) => ({ [id]: options[isActive] }))
  439. .reduce((accumulator, currentValue) => {
  440. Object.keys(currentValue).forEach(key => accumulator[key] = currentValue[key]);
  441. return accumulator;
  442. }, {});
  443. const newResult = { ...flowParams.feedback, ...result };
  444. flowParams.feedback = newResult;
  445. onRegenerate();
  446. }
  447. }
  448. // 开始预测
  449. const handleSendSimulate = ({ simulate, table }) => {
  450. const len = answerResult.value.length;
  451. flowParams.simulate = simulate;
  452. answerResult.value[len - 1].isDisable = true;
  453. answerResult.value.push({
  454. biz: 'DECISION_TABLE',
  455. loading: true,
  456. delayLoading: false,
  457. table,
  458. isDisable: false
  459. })
  460. scrollToBottom();
  461. onRegenerate();
  462. }
  463. // 欢迎页提交
  464. const handleWelcomeRecommend = question => {
  465. chatStore.setChatQuestion(question);
  466. router.push('/answer');
  467. }
  468. </script>
  469. <template>
  470. <section class="flex items-start h-full" id="warning">
  471. <TheSubMenu title="水质报警" @scrollToLower="onScrolltolower" :loading="isFetching" ref="subMenuRef">
  472. <template #top>
  473. <div class="border-[#DAE5ED]">
  474. <n-tabs type="line" justify-content="space-evenly">
  475. <n-tab name="oasis" tab="正在报警" @click="onChangeTabs(0)"></n-tab>
  476. <n-tab name="thebeatles" tab="历史报警" @click="onChangeTabs(1)"></n-tab>
  477. </n-tabs>
  478. <div class="select-card">
  479. <n-select v-model:value="waterTypeValue" :options="options" size="tiny" />
  480. </div>
  481. </div>
  482. </template>
  483. <div class="px-[12px] py-[14px] text-[#5e5e5e]">
  484. <p v-show="!recordList.length" class="pt-[30px] text-[12px] text-[#999] text-center">暂无报警数据</p>
  485. <div class="grid grid-cols-1 gap-[12px]">
  486. <div v-show="warningActive == 1">
  487. <RecodeSquareCardItem v-for="item, index in mockData" :key="item.id" :item="item"
  488. @on-click="handleOpenContent" :style="{ marginBottom: index < 3 ? '10px' : '' }" />
  489. </div>
  490. <RecodeSquareCardItem v-for="item in recordList" :key="item.id" :item="item" @on-click="handleOpenContent" />
  491. </div>
  492. </div>
  493. </TheSubMenu>
  494. <TheChatView ref="scrollRef" :is-footer="false">
  495. <ChatWelcome title="您好,我是LibraAI工艺管控助手" card-title="常见处理方案:" :sub-title="[
  496. '水质报警功能针对五大核心指标实时监测,发现异常后将推送给相关人员决策方案',
  497. '报警时间为每小时警报,请大家及时处理'
  498. ]" :card-content="recommendList" @on-click="handleWelcomeRecommend" v-if="!textDataSources" />
  499. <ChatBaseCard v-if="textDataSources">
  500. <div class="waring-answer-wrapper">
  501. <dl class="message-inner warning-info_medium ">
  502. <dt class="mb-[2px] font-bold text-[#1A2029]">{{ textDataSources?.title }}</dt>
  503. <dd v-for="item, index in textDataSources?.list" :key="index"><span
  504. :class="{ 'text-[#F44C49]': item.isWarning }">{{ item.label }}: {{ item.value }}</span></dd>
  505. </dl>
  506. <div class="table-inner">
  507. <div class="warning-table mb-[8px]">
  508. <div class="title">
  509. <span>当前进水数据:</span>
  510. </div>
  511. <div class="main">
  512. <BaseTable :columns="inColumns" :data="jsTableData"></BaseTable>
  513. </div>
  514. </div>
  515. <div class="warning-table">
  516. <div class="title">
  517. <span>当前出水数据:</span>
  518. </div>
  519. <div class="main">
  520. <BaseTable :columns="outColumns" :data="csTableData"></BaseTable>
  521. </div>
  522. </div>
  523. </div>
  524. </div>
  525. </ChatBaseCard>
  526. <section v-for="item, index in answerResult" :key="index">
  527. <template v-if="item.biz === 'DECISION_REPORT'">
  528. <ChatAnswer :loading="item.loading" :delay-loading="item.delayLoading" :toggleVisibleIcons="false"
  529. :content="item.answer"></ChatAnswer>
  530. </template>
  531. <template v-if="item.biz === 'DECISION_ALERT'">
  532. <ChatBaseCard :loading="item.loading" :delay-loading="item.delayLoading" :toggleVisibleIcons="false">
  533. <p class="mb-[15px] font-bold text-[#1A2029]">需要确定以下问题,完成决策方案:</p>
  534. <ul class="radio-wrapper space-y-[14px]">
  535. <li class="flex items-center" v-for="val, i in item.list" :key="i">
  536. <p class="mr-[14px]">{{ val.mainContent }}</p>
  537. <p class="radio-btn-group space-x-[14px]">
  538. <span v-for="option, index in val.options" :class="['radio-btn', { active: val.isActive === index }]"
  539. @click="handlerAlertOptions(item, val, index)">{{ option }}</span>
  540. </p>
  541. </li>
  542. </ul>
  543. </ChatBaseCard>
  544. </template>
  545. <template v-if="item.biz === 'DECISION_SIMULATE'">
  546. <button class="
  547. px-[30px] py-[10px] mb-[20px]
  548. rounded-[8px]
  549. bg-white text-[13px]
  550. text-[#5E5E5E] hover:text-[#2454FF]" :disabled="item.isDisable" @click="handleModelVisible">
  551. 水质预测推演
  552. </button>
  553. </template>
  554. <template v-if="item.biz === 'DECISION_TABLE'">
  555. <ChatAnswer :loading="item.loading" :delay-loading="item.delayLoading" :toggleVisibleIcons="false"
  556. class="reset-chart">
  557. <div class="markdown-body text-[15px] break-all">
  558. <strong class="block mb-[16px]">推荐指标调整:</strong>
  559. <div class="custom-table-wrapper">
  560. <table>
  561. <thead>
  562. <tr>
  563. <th v-for="text in item.table.header" :key="text">{{ text }}</th>
  564. </tr>
  565. </thead>
  566. <tbody class="text-center">
  567. <tr>
  568. <td v-for="text in item.table.body" :key="text">{{ text }}</td>
  569. </tr>
  570. </tbody>
  571. </table>
  572. </div>
  573. <strong class="block mb-[16px]">预测推演结果:</strong>
  574. <span>以上指标达成后,预计{{ flowParams.category }}可达到:{{ item.content }}</span>
  575. </div>
  576. </ChatAnswer>
  577. <button class="
  578. px-[30px] py-[10px] mb-[20px]
  579. rounded-[8px]
  580. bg-white text-[13px]
  581. text-[#5E5E5E] hover:text-[#2454FF]" :disabled="item.isDisable" @click="handleModelVisible">
  582. 水质预测推演
  583. </button>
  584. </template>
  585. </section>
  586. <ChatAnswer :loading="answerLoading" :delay-loading="answerLoading" :toggleVisibleIcons="false"
  587. v-show="answerLoading" loadingText="内容生成中,大概需要1分钟..."></ChatAnswer>
  588. </TheChatView>
  589. </section>
  590. <CustomModal v-model:visible="visible" :current-data="modalData" @on-submit="handleSendSimulate"></CustomModal>
  591. </template>
  592. <style lang="scss">
  593. .reset-chart {
  594. .markdown-body {
  595. .custom-table-wrapper {
  596. width: 100%;
  597. overflow: hidden;
  598. padding: 10px;
  599. table td,
  600. table th {
  601. white-space: normal !important;
  602. }
  603. }
  604. }
  605. }
  606. .select-card {
  607. padding: 15px 10px 0 10px;
  608. }
  609. </style>