WaterView.vue 15 KB

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