WaterView.vue 19 KB

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