From 37179a65229455f540dc3ca62d31c4716e15d12d Mon Sep 17 00:00:00 2001 From: gerson <1405270578@qq.com> Date: 星期六, 07 九月 2024 14:32:17 +0800 Subject: [PATCH] 集成 chat组件 --- src/components/chat/Chat.vue | 383 +++++ src/components/chat/chatComponents/normalTextCom/NormalTextCom.vue | 13 src/components/form/datepicker/constants.ts | 3 src/components/chat/chatComponents/mapCom/TestData.ts | 287 ++++ src/components/chat/chatComponents/summaryCom/SummaryCom.vue | 35 src/components/chat/chatComponents/summaryCom/components/recordSetTable/RecordSetTable.vue | 136 + src/components/chat/hooks/useQueryProcess.ts | 41 src/components/chat/components/model/Record.ts | 68 src/components/chat/chatComponents/summaryCom/components/deviceLastValue/types.ts | 21 src/utils/util.ts | 26 src/views/project/yw/lowCode/sqlAmis/SqlAmis.vue | 1 src/components/chat/chatComponents/hooks/useDrawChatChart.ts | 42 src/api/supervisorAdmin/index.ts | 13 src/components/chat/chatComponents/summaryCom/components/recordSet/components/YRange.vue | 48 src/components/chat/chatComponents/summaryCom/components/recordSet/components/Timestamp.vue | 59 src/components/chat/chatComponents/summaryCom/components/deviceLastValue/DeviceLastValueCom.vue | 328 ++++ customer_list/yw/static/images/role/assistant-200x192.png | 0 src/components/chat/chatComponents/common.ts | 126 + src/components/chat/chatComponents/mapCom/img/monitor-point.svg | 1 src/components/chat/hooks/useAssistantContentOpt.ts | 144 ++ src/components/chat/libs/markdown.ts | 21 src/components/chat/chatComponents/summaryCom/components/recordSet/components/constants.ts | 2 src/components/chat/chatComponents/summaryCom/components/summary/Summary.vue | 32 src/components/chat/chatComponents/summaryCom/components/recordSet/RecordSet.vue | 457 ++++++ src/components/chat/chatComponents/types.ts | 18 src/components/chat/chatComponents/summaryCom/components/amisPage/testData.json | 424 ++++++ package-lock.json | 82 + src/components/chat/chatComponents/mapCom/types.ts | 18 src/components/chat/chatComponents/summaryCom/components/recordSet/RecordSetDialog.vue | 91 + src/api/ai/chat.ts | 82 + src/components/chat/chatComponents/htmlCom/HTMLCom.vue | 24 src/components/chat/components/model/types.ts | 61 src/components/chat/chatComponents/summaryCom/components/recordSet/types.ts | 64 src/components/chat/chatComponents/mapCom/MapCom.vue | 130 + src/components/chat/chatComponents/recordSetCom/RecordSetCom.vue | 112 + src/components/chat/chatComponents/summaryCom/components/amisPage/AmisPage.vue | 28 src/components/chat/components/Loading/Loading.vue | 114 + src/components/chat/hooks/useScrollToBottom.ts | 37 src/components/chat/chatComponents/summaryCom/components/types.ts | 31 src/components/chat/chatComponents/summaryCom/components/recordSet/components/TimeRange.vue | 146 ++ src/components/chat/chatComponents/summaryCom/components/recordSet/components/List.vue | 60 src/components/chat/chatComponents/summaryCom/components/recordSet/components/types.ts | 18 src/components/chat/chatComponents/summaryCom/components/deviceLastValue/constants.ts | 18 src/components/chat/model/Record.ts | 68 customer_list/yw/static/images/role/user-200x206.png | 0 src/components/chat/chatComponents/knowledgeCom/KnowledgeCom.vue | 34 package.json | 4 src/components/chat/chatComponents/summaryCom/components/deviceLastValue/MonitorContent.vue | 117 + src/components/chat/model/types.ts | 61 src/components/chat/libs/gpt.ts | 21 50 files changed, 4,148 insertions(+), 2 deletions(-) diff --git a/customer_list/yw/static/images/role/assistant-200x192.png b/customer_list/yw/static/images/role/assistant-200x192.png new file mode 100644 index 0000000..69a2d41 --- /dev/null +++ b/customer_list/yw/static/images/role/assistant-200x192.png Binary files differ diff --git a/customer_list/yw/static/images/role/user-200x206.png b/customer_list/yw/static/images/role/user-200x206.png new file mode 100644 index 0000000..37359c5 --- /dev/null +++ b/customer_list/yw/static/images/role/user-200x206.png Binary files differ diff --git a/package-lock.json b/package-lock.json index b5d147c..51ae22b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@codemirror/lang-xml": "^6.1.0", "@codemirror/state": "6.x", "@element-plus/icons-vue": "^2.1.0", + "@iframe-resizer/vue": "^5.1.5", "@types/three": "^0.164.1", "@uiw/codemirror-theme-vscode": "^4.23.0", "@wangeditor/editor": "^5.1.23", @@ -29,11 +30,13 @@ "echarts-wordcloud": "^2.1.0", "element-plus": "^2.2.36", "fast-xml-parser": "^4.4.1", + "highlight.js": "^11.7.0", "js-cookie": "^3.0.1", "js-table2excel": "^1.0.3", "json-bigint": "^1.0.0", "json-editor-vue3": "^1.1.1", "lodash": "^4.17.21", + "markdown-it": "^13.0.1", "mitt": "^3.0.0", "moment": "^2.29.4", "nprogress": "^0.2.0", @@ -763,6 +766,30 @@ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "deprecated": "Use @eslint/object-schema instead", "dev": true + }, + "node_modules/@iframe-resizer/core": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/@iframe-resizer/core/-/core-5.3.0.tgz", + "integrity": "sha512-6yJBzMIbVMZA0KDtuTweyE0YiiYpG9p2041WEIurtP6X2RL++tJLu8xqF28S0agKg1OGpBRrv3P6mGxsVMWoLg==", + "funding": { + "type": "individual", + "url": "https://iframe-resizer.com/pricing/" + } + }, + "node_modules/@iframe-resizer/vue": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/@iframe-resizer/vue/-/vue-5.3.0.tgz", + "integrity": "sha512-IUNJjHJLgvj5hIYTsrqufAe6Qk8ByKH+IHTUZTeYXp2yBlUEtXLRfKVFv9Q2pjsoXTtJTsbaDTu9rqvMcs11MQ==", + "dependencies": { + "@iframe-resizer/core": "5.3.0" + }, + "funding": { + "type": "individual", + "url": "https://iframe-resizer.com/pricing/" + }, + "peerDependencies": { + "vue": "^2.6.0 || ^3.0.0" + } }, "node_modules/@interactjs/actions": { "version": "1.10.27", @@ -2244,8 +2271,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-union": { "version": "2.1.0", @@ -3938,6 +3964,14 @@ "node": ">= 0.4" } }, + "node_modules/highlight.js": { + "version": "11.10.0", + "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.10.0.tgz", + "integrity": "sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/html-void-elements": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-2.0.1.tgz", @@ -4302,6 +4336,14 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", @@ -4391,6 +4433,37 @@ "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } + }, + "node_modules/markdown-it": { + "version": "13.0.2", + "resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-13.0.2.tgz", + "integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" }, "node_modules/memoize-one": { "version": "6.0.0", @@ -5786,6 +5859,11 @@ "node": ">=4.2.0" } }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz", diff --git a/package.json b/package.json index 199c228..42fd8d7 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,10 @@ "fix-memory-limit": "cross-env LIMIT=8048 increase-memory-limit" }, "dependencies": { + "highlight.js": "^11.7.0", + "markdown-it": "^13.0.1", + "@iframe-resizer/vue": "^5.1.5", + "@amap/amap-jsapi-loader": "^1.0.1", "@amap/amap-jsapi-types": "^0.0.15", "@codemirror/lang-json": "^6.0.1", diff --git a/src/api/ai/chat.ts b/src/api/ai/chat.ts new file mode 100644 index 0000000..1bfa871 --- /dev/null +++ b/src/api/ai/chat.ts @@ -0,0 +1,82 @@ +import request from '/@/utils/request'; +export interface RecordSetValues { + names: string[]; + values: Array<Array<any>>; + type: string; + title: string; +} +export const GetHistoryAnswer = async (params, req: any = request) => { + return req({ + url: '/history/get_history_answer', + method: 'POST', + data: params, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); +}; +export const filterQuery = async (params, req: any = request) => { + return req({ + url: 'chat/filter_query', + method: 'POST', + data: params, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); +}; + +export const queryScadaTimeValues = async (params, req: any = request) => { + return req({ + url: 'data/query_scada_time_values', + method: 'POST', + data: params, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); +}; + + +/** + * @summary 璁剧疆鍘嗗彶瀵硅瘽鐘舵�侊紙鏈缃細NULL锛岄《1锛岃俯0 + */ +export const SetHistoryAnswerState = async (params, req: any = request) => { + return req({ + url: '/history/set_history_answer_state', + method: 'POST', + data: params, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); +}; + +/** @description 鍏宠仈鏌ヨ */ +export const extCallQuery = async (params, req: any = request) => { + return req({ + url: 'chat/ext_call_query', + method: 'POST', + data: params, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); +}; + +/** + * 鏌ヨ闂杩涘害 + * @param params + * @param req + * @returns + */ +export const getQuestionProcess = async (params, req: any = request) => { + return req({ + url: 'chat/get_question_process', + method: 'POST', + data: params, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); +}; \ No newline at end of file diff --git a/src/api/supervisorAdmin/index.ts b/src/api/supervisorAdmin/index.ts index 0340e02..6d07a6b 100644 --- a/src/api/supervisorAdmin/index.ts +++ b/src/api/supervisorAdmin/index.ts @@ -87,3 +87,16 @@ data: params, }); }; + + + +/** + * @summary description + */ +export const checkSupervisorValidate = async (params, req: any = request) => { + return req({ + url: '/admin/supervisor/check_amis_supervisor_validate', + method: 'POST', + data: params, + }); +}; \ No newline at end of file diff --git a/src/components/chat/Chat.vue b/src/components/chat/Chat.vue new file mode 100644 index 0000000..7991371 --- /dev/null +++ b/src/components/chat/Chat.vue @@ -0,0 +1,383 @@ +<template> + <div class="flex h-full"> + <div class="flex flex-col h-full flex-auto"> + <div class="h-full flex flex-col items-center overflow-y-auto"> + <div ref="chatListDom" class="h-full" :style="{ width: chatWidth }"> + <div + class="group flex px-4 py-6 hover:bg-slate-100 rounded-lg relative" + :class="{ 'flex-row-reverse': item.role === RoleEnum.user }" + v-for="(item, index) of computedMessageList" + :key="index" + > + <img + class="rounded-full size-12 flex-0" + :class="{ 'mr-4': item.role === RoleEnum.assistant, 'ml-4': item.role === RoleEnum.user }" + :src="roleImageMap[item.role]" + alt="" + srcset="" + /> + <div class="flex-auto flex" :class="{ 'justify-end': item.role === RoleEnum.user }"> + <div class="inline-flex flex-col" :class="{ 'w-full': item.role === RoleEnum.assistant }"> + <div class="w-full" v-if="item.content?.values"> + <div + class="text-sm rounded-[6px] p-4 leading-relaxed" + :style="{ backgroundColor: item.role === RoleEnum.user ? 'rgb(197 224 255)' : 'white' }" + > + <div v-if="item.content.errCode === ErrorCode.Message" class="flex-column w-full"> + <p class="text-red-500"> + {{ item.content.errMsg }} + </p> + <div class="mt-5 flex items-center" v-if="showFixQuestion(item)"> + <div class="text-gray-600 flex-0"> + {{ item.content.origin.err_json.fix_question.title + '锛�' }} + </div> + <div class="ml-1 space-x-2 inline-flex flex-wrap"> + <div + v-for="fixItem in item.content.origin.err_json.fix_question?.values" + :key="fixItem" + class="bg-gray-200 p-3 hover:bg-[#c5e0ff] hover:text-[#1c86ff] cursor-pointer rounded-lg" + @click="fixQuestionClick(fixItem, item.content.origin)" + > + {{ fixItem.title }} + </div> + </div> + </div> + </div> + <template v-else> + <component :is="answerTypeMapCom[item.content.type]" :data="item.content.values" :originData="item" /> + + <div + v-if="item.role === RoleEnum.assistant && item.content.origin?.ext_call_list" + class="flex font-bold items-center mt-6" + > + <div class="flex-0 mb-auto -mr-4">鍏宠仈鍔熻兘锛�</div> + <!-- <div class="space-x-5 flex flex-wrap"> + <div + v-for="callItem in item.content.origin?.ext_call_list" + :key="callItem.call_ext_id" + @click="relativeQueryClick(callItem)" + class="cursor-pointer hover:underline first-of-type:ml-5" + > + {{ callItem.question }} + </div> + </div> --> + </div> + </template> + </div> + + <!-- 鎿嶄綔 --> + <div v-if="item.role === RoleEnum.assistant" class="absolute flex items-center right-0 mr-4 mt-2 space-x-2"> + <div + class="flex items-center justify-center size-[15px]" + v-if="item.content?.type === AnswerType.Text || item.content?.type === AnswerType.Knowledge" + > + <i + class="p-2 ywifont ywicon-copy cursor-pointer hover:text-[#0284ff] hover:!text-[18px]" + @click="copyClick(item)" + /> + </div> + <template v-if="item.content.errCode !== ErrorCode.Message"> + <div class="flex items-center justify-center size-[15px]"> + <i + :class="{ 'text-[#0284ff]': item.state === AnswerState.Like }" + class="p-2 ywifont ywicon-dianzan cursor-pointer hover:text-[#0284ff] font-medium hover:!text-[18px]" + @click="likeClick(item)" + /> + </div> + <div class="flex items-center justify-center size-[15px]"> + <i + :class="{ 'text-[#0284ff]': item.state === AnswerState.Unlike }" + class="p-2 ywifont ywicon-buzan cursor-pointer hover:text-[#0284ff] !text-[13px] hover:!text-[15px]" + @click="unLikeClick(item)" + /> + </div> + </template> + + <div class="flex items-center justify-center size-[15px] relative"> + <i + class="p-2 ywifont ywicon-wentifankui cursor-pointer hover:text-[#0284ff] !text-[13px] hover:!text-[15px]" + @click=" + ($event) => + feedbackClick( + $event, + item, + computedMessageList + .filter((v) => v.role === RoleEnum.assistant) + .findIndex((v) => v.historyId === item.historyId) + ) + " + /> + <FeedbackPanel + v-show="feedbackIsShow && currentFeedbackMapItem === item" + ref="feedbackPanelRef" + v-model:isShow="feedbackIsShow" + v-model:content="feedbackContent" + :chatItem="currentFeedbackMapItem" + :position="feedbackPosition" + /> + </div> + </div> + </div> + + <Loading v-if="isTalking && index === messageList.length - 1" class="w-fit" /> + </div> + </div> + </div> + <div v-if="showAskMore" class="ml-4 mt-5 text-sm"> + <div class="text-gray-600 mb-5">浣犲彲浠ョ户缁棶鎴戯細</div> + <div class="space-y-2 inline-flex flex-col"> + <div + v-for="item in computedMessageList.at(-1).content.askMoreList" + :key="item.history_id" + class="bg-white p-3 hover:bg-[#c5e0ff] hover:text-[#1c86ff] cursor-pointer rounded-lg" + @click="askMoreClick(item)" + > + {{ item.question }} + </div> + </div> + </div> + </div> + </div> + + <div class="sticky bottom-0 w-full p-6 pb-8 bg-[rgb(247,248,250)] flex justify-center"> + <PlayBar + v-model:voicePageIsShow="voicePageIsShow" + :isTalking="isTalking" + :isHome="false" + v-model="messageContent.values" + @sendClick="sendClick" + :style="{ width: chatWidth }" + ></PlayBar> + </div> + </div> + + <!-- <CustomDrawer v-model:isShow="drawerIsShow" @updateChatInput="updateChatInput" /> --> + </div> +</template> + +<script setup lang="ts"> +import _ from 'lodash'; +import { computed, ref } from 'vue'; +import FeedbackPanel from './components/FeedbackPanel.vue'; +import Loading from './components/Loading/Loading.vue'; +import { useAssistantContentOpt } from './hooks/useAssistantContentOpt'; +import { useScrollToBottom } from './hooks/useScrollToBottom'; +import type { ChatContent } from './model/types'; +import { AnswerState, AnswerType, RoleEnum, answerTypeMapCom, roleImageMap, type ChatMessage } from './model/types'; +import { GetHistoryAnswer, extCallQuery } from '/@/api/ai/chat'; +import PlayBar from '/@/components/chat/components/playBar/PlayBar.vue'; +import { ErrorCode } from '/@/utils/request'; +import { checkSupervisorValidate } from '/@/api/supervisorAdmin'; + +const emit = defineEmits<{ + (event: 'question',inputText): any; +}>(); + +const chatWidth = '75%'; +const voicePageIsShow = ref(false); +let isTalking = ref(false); +let messageContent = ref<ChatContent>({ + type: AnswerType.Text, + values: '', +}); + +const chatListDom = ref<HTMLDivElement>(); +const messageList = ref<ChatMessage[]>([]); +const computedMessageList = computed(() => { + return messageList.value.filter((v) => !!v); +}); + +const parseContent = (res) => { + if (!res) return null; + let content: ChatContent = { + type: AnswerType.Text, + values: '瑙f瀽澶辫触锛�', + }; + + switch (res.answer_type) { + case AnswerType.RecordSet: + content = { + type: AnswerType.RecordSet, + values: res.values, + }; + break; + case AnswerType.Text: + content = { + type: AnswerType.Text, + values: res.values ?? res.answer, + }; + break; + + case AnswerType.Knowledge: + content = { + type: AnswerType.Knowledge, + values: res.knowledge, + }; + + break; + case AnswerType.Summary: + content = { + type: AnswerType.Summary, + values: res.summary, + }; + break; + case AnswerType.Url: + content = { + type: AnswerType.Url, + values: res.url, + }; + break; + case AnswerType.Map: + content = { + type: AnswerType.Map, + values: res.values, + }; + break; + default: + content = { + type: AnswerType.Text, + values: '瑙f瀽澶辫触锛�', + }; + break; + } + content.askMoreList = _.orderBy(res.context_history, [(item) => Number(item.radio)], ['desc']); + content.errCode = res?.err_code; + content.errMsg = res?.json_msg; + content.origin = res; + return content; +}; + +let questionRes = null; +const questionAi = async (text) => { + const params = { + id: 'net3_summary', + question: text, + } as any; + + // const res = await emit('question',text); + const res = await checkSupervisorValidate(params); + questionRes = res?.values; + const content = parseContent(res?.values); + return content; +}; + +const clearMessageContent = () => + (messageContent.value = { + type: AnswerType.Text, + values: '', + }); + +const getAnswerById = async (historyId: string) => { + return await GetHistoryAnswer({ + history_id: historyId, + }); +}; + +const sendChatMessage = async (content: ChatContent = messageContent.value, cb?: any, isCallExtParams?: any) => { + if (!content?.values || isTalking.value) return; + const isNewChat = messageList.value.length === 0; + + let resMsgContent: ChatContent = null; + + try { + isTalking.value = true; + const userItem: ChatMessage = { role: RoleEnum.user, content } as any; + const assistantItem: ChatMessage = { role: RoleEnum.assistant, content: null, state: AnswerState.Null } as any; + // 鍙戦�佸綋鍓� + messageList.value.push(userItem); + // 娓呯┖杈撳叆妗� + clearMessageContent(); + + // 鍑虹幇鍥炲锛岀疆绌哄嚭鐜扮瓑寰呭姩鐢� + messageList.value.push(assistantItem); + if (isCallExtParams) { + const extRes = await extCallQuery(isCallExtParams); + questionRes = extRes; + resMsgContent = parseContent(extRes); + } else { + resMsgContent = await questionAi(content.values); + } + + userItem.historyId = questionRes.history_id; + userItem.content.values = questionRes?.question ?? userItem.content.values; + assistantItem.historyId = questionRes.history_id; + appendLastMessageContent(resMsgContent); + } catch (error: any) { + // appendLastMessageContent({ + // type: AnswerType.Text, + // values: '鍙戠敓閿欒锛�', + // }); + } finally { + isTalking.value = false; + } +}; + +const sendClick = (cb) => { + sendChatMessage(messageContent.value, cb); +}; +const appendLastMessageContent = (content: ChatContent) => { + if (messageList.value.at(-1)) { + messageList.value.at(-1).content = content; + } +}; + +const { forbidScroll } = useScrollToBottom({ + chatListDom: chatListDom, + displayMessageList: computedMessageList, +}); + +//#region ====================== 鍏宠仈鏌ヨ ====================== +// const relativeQueryClick = async (val) => { +// sendChatMessage( +// { +// type: AnswerType.Text, +// values: val.question, +// }, +// undefined, +// { +// history_group_id: currentRouteId, +// question: val.question, +// call_ext_id: val.call_ext_id, +// call_ext_args: val.agrs ? JSON.stringify(val.agrs) : null, +// } +// ); +// }; +//#endregion + +const { + copyClick, + likeClick, + unLikeClick, + feedbackPosition, + feedbackIsShow, + feedbackContent, + feedbackPanelRef, + currentFeedbackMapItem, + feedbackClick, + askMoreClick, + fixQuestionClick, + preQuestion, + showFixQuestion, + showAskMore, +} = useAssistantContentOpt({ + forbidScroll, + sendChatMessage, + displayMessageList: computedMessageList, +}); + +//#region ====================== 渚ц竟鏍廳rawer ====================== +const drawerIsShow = ref(false); + +const updateChatInput = (content) => { + messageContent.value.values = content; +}; +//#endregion +</script> + +<style scoped> +pre { + font-family: -apple-system, 'Noto Sans', 'Helvetica Neue', Helvetica, 'Nimbus Sans L', Arial, 'Liberation Sans', 'PingFang SC', + 'Hiragino Sans GB', 'Noto Sans CJK SC', 'Source Han Sans SC', 'Source Han Sans CN', 'Microsoft YaHei', 'Wenquanyi Micro Hei', + 'WenQuanYi Zen Hei', 'ST Heiti', SimHei, 'WenQuanYi Zen Hei Sharp', sans-serif; +} +</style> diff --git a/src/components/chat/chatComponents/common.ts b/src/components/chat/chatComponents/common.ts new file mode 100644 index 0000000..0188716 --- /dev/null +++ b/src/components/chat/chatComponents/common.ts @@ -0,0 +1,126 @@ +import type * as echarts from 'echarts'; +import { buildProps } from 'element-plus/es/utils/vue/props/runtime'; +import _ from 'lodash'; +import type { ExtractPropTypes, PropType } from 'vue'; +import { axisLabelFormatter } from '/@/utils/chart'; + +export const timeDataOptionToContent = (opt) => { + const headerList = [opt.xAxis[0]] + .concat(opt.yAxis) + .map((item) => `<td>${item.name}</td>`) + .join(''); + const title = opt.title?.[0]?.text ?? ''; + let table = + '<div style="border:1px solid black"><h3>' + + title + + '</h3><table style="width:100%;text-align:center"><tbody><tr>' + + headerList + + '</tr>'; + const timeData = new Set(); + const dataMap = opt.series.map((item) => { + for (const subItem of item.data) { + timeData.add(subItem[0]); + } + return new Map(item.data); + }); + const bodyList = Array.from(timeData) + .toSorted((a, b) => { + return (a as any).localeCompare(b); + }) + .map((item) => { + return `<tr><td>${item}</td>${dataMap.map((itemMap) => `<td>${itemMap.get(item) ?? ''}</td>`)}</tr>`; + }) + .join(''); + table += bodyList; + + table += '</tbody></table></div>'; + return table; +}; + +export const PATH_ICON = { + scatter: + 'path://M445.7 609.8c0 19.4 10.3 37.3 27.1 46.9 16.8 9.7 37.4 9.7 54.2 0 16.8-9.7 27.1-27.6 27.1-46.9 0-29.9-24.3-54.2-54.2-54.2s-54.2 24.3-54.2 54.2z m0 0M179.2 613.8c-42.2 0-76.5 34.3-76.5 76.5s34.3 76.5 76.5 76.5 76.5-34.3 76.5-76.5-34.3-76.5-76.5-76.5z m0 0M144.9 401.1c0 29 23.5 52.5 52.5 52.5s52.5-23.5 52.5-52.5-23.5-52.5-52.5-52.5-52.5 23.5-52.5 52.5z m0 0M598.7 404c0 42.2 34.3 76.5 76.5 76.5 42.3 0 76.5-34.3 76.5-76.5 0-42.3-34.3-76.5-76.5-76.5-42.3 0-76.5 34.3-76.5 76.5z m0 0M849.3 169.2c-42.2 0-76.5 34.3-76.5 76.5s34.3 76.5 76.5 76.5 76.5-34.3 76.5-76.5-34.3-76.5-76.5-76.5z m0 0M261.6 583.1c0 13.2 7.1 25.5 18.5 32.1 11.5 6.6 25.6 6.6 37.1 0s18.5-18.9 18.5-32.1c0-20.5-16.6-37.1-37.1-37.1-20.4 0.1-37 16.7-37 37.1z m0 0M276.8 425.1c0 42.3 34.3 76.5 76.5 76.5 42.3 0 76.5-34.3 76.5-76.5s-34.3-76.5-76.5-76.5-76.5 34.3-76.5 76.5z m0 0M445.7 421.4c0 18.5 9.9 35.5 25.8 44.8 16 9.2 35.7 9.2 51.7 0s25.8-26.3 25.8-44.8c0-28.5-23.1-51.7-51.7-51.7-28.5 0-51.6 23.2-51.6 51.7z m0 0M398.2 208.8c0 42.3 34.3 76.5 76.5 76.5s76.5-34.3 76.5-76.5c0-42.3-34.3-76.5-76.5-76.5s-76.5 34.3-76.5 76.5z m0 0M693.7 599.2c0 42.3 34.3 76.5 76.5 76.5s76.5-34.3 76.5-76.5-34.3-76.5-76.5-76.5c-42.3 0-76.5 34.3-76.5 76.5z m0 0M62.1 828.9H959v60.7H62.1z', + bar: 'path://M580.8 228.8h-136v500.8h136V228.8z m-40 460.8h-56V268.8h56v420.8zM788.8 420.8h-136v308.8h136V420.8z m-40 268.8h-56V460.8h56v228.8zM372.8 326.4h-136v401.6h136V326.4z m-40 363.2h-56V366.4h56v323.2zM208 788.8h608v40H208z', + + line: 'path://M664.1 783c-3.8 0-7.6-0.2-11.3-0.5-36.3-3.1-68.9-20.6-96.9-52.1-28.4-32-52.5-79.4-71.4-140.8-32.4-105-66.1-182.6-100.3-230.6-25.5-35.8-50.2-54-73.4-54h-0.4c-38.8 0.4-84.8 51-129.3 142.7-37 76.2-59.4 153-59.7 153.8L64 582.6c1-3.4 24.3-83.1 63.7-164.3 56.8-117 118.1-176.6 182.1-177.3h1c43 0 83.8 26.7 121.2 79.3 38.2 53.7 75.1 137.5 109.5 249.3 29.3 94.9 68.3 145.1 116 149.1 21.3 1.8 45.6-5.2 72.2-20.9 24.5-14.4 50.3-35.8 76.8-63.5 49.6-51.9 87.7-111.9 100.1-137.6L960 526c-14.6 30.4-56.4 96.4-111.4 154-30.4 31.8-60.6 56.6-89.9 73.9-32.8 19.3-64.6 29.1-94.6 29.1z', +}; + +export const SCATTER_SYMBOL_SIZE = 4; + +export const chatComProps = buildProps({ + data: { + type: Object as PropType<any>, + }, + originData: { + type: Object as PropType<any>, + }, +} as const); +export type ChatComPropsType = ExtractPropTypes<typeof chatComProps>; + +export const getChatChartOption = () => { + const option = { + grid: { + // bottom: 120, + // right: '15%', + top: 65, + left: 65, + right: 45, + }, + tooltip: { + show: true, + trigger: 'axis', + + }, + toolbox: { + show: true, + feature: { + dataZoom: { + yAxisIndex: 'none', + }, + + myBar: { + title: '杞寲涓烘煴鐘跺浘', + show: true, + icon: PATH_ICON.bar, + }, + + myScatter: { + title: '杞寲涓烘暎鐐瑰浘', + show: true, + icon: PATH_ICON.scatter, + }, + myLine: { + title: '杞寲涓烘洸绾垮浘', + show: true, + icon: PATH_ICON.line, + }, + dataView: { + readOnly: true, + optionToContent: timeDataOptionToContent, + }, + saveAsImage: {}, + }, + }, + + title: { + left: 'center', + textStyle: { + fontSize: 14, + }, + }, + xAxis: { + type: 'time', + }, + yAxis: { + type: 'value', + axisLabel: { + formatter: axisLabelFormatter, + }, + }, + dataZoom: { + type: 'inside', + }, + } as echarts.EChartsOption; + + return _.cloneDeep(option); +}; diff --git a/src/components/chat/chatComponents/hooks/useDrawChatChart.ts b/src/components/chat/chatComponents/hooks/useDrawChatChart.ts new file mode 100644 index 0000000..fc80503 --- /dev/null +++ b/src/components/chat/chatComponents/hooks/useDrawChatChart.ts @@ -0,0 +1,42 @@ +import * as echarts from 'echarts'; +import type { Ref } from 'vue'; +import { onMounted, shallowRef } from 'vue'; +import { debounce } from '/@/utils/util'; + +export type UseDrawChatChartOption = { + chartRef: Ref<HTMLDivElement>; + drawChart: () => void; +}; +export const useDrawChatChart = (option: UseDrawChatChartOption) => { + const { chartRef, drawChart } = option; + const chartInstance = shallowRef<echarts.ECharts>(null); + + let resizeChart = null; + const chartContainerResize = ({ width, height }) => { + resizeChart?.(width, height); + }; + + onMounted(() => { + setTimeout(() => { + const parent = chartRef.value.parentElement; + const parentBound = parent.getBoundingClientRect(); + chartInstance.value = echarts.init(chartRef.value, undefined, { + width: parentBound.width, + height: parentBound.height, + }); + resizeChart = debounce((width, height) => { + chartInstance.value.resize({ + width: width, + height: height, + }); + }); + + drawChart(); + }, 100); + }); + + return { + chartContainerResize, + chartInstance, + }; +}; diff --git a/src/components/chat/chatComponents/htmlCom/HTMLCom.vue b/src/components/chat/chatComponents/htmlCom/HTMLCom.vue new file mode 100644 index 0000000..d6e2cb8 --- /dev/null +++ b/src/components/chat/chatComponents/htmlCom/HTMLCom.vue @@ -0,0 +1,24 @@ +<template> + <IframeResizer + class="iframe-resizer" + license="GPLv3" + :src="data.url" + width="100%" + frameborder="no" + border="0" + marginwidth="0" + marginheight="0" + scrolling="no" + + /> +</template> + +<script setup lang="ts"> +import IframeResizer from '@iframe-resizer/vue/iframe-resizer.vue'; +import { ref } from 'vue'; +import { chatComProps } from '../common'; +const iframeRef = ref<HTMLIFrameElement>(null); + +const props = defineProps(chatComProps); +</script> +<style scoped lang="scss"></style> diff --git a/src/components/chat/chatComponents/knowledgeCom/KnowledgeCom.vue b/src/components/chat/chatComponents/knowledgeCom/KnowledgeCom.vue new file mode 100644 index 0000000..dff677d --- /dev/null +++ b/src/components/chat/chatComponents/knowledgeCom/KnowledgeCom.vue @@ -0,0 +1,34 @@ +<template> + <div class="space-y-7"> + <div v-for="(item, index) in data" :key="index"> + <div v-html="md.render(item.answer)"></div> + <div class="space-y-1 mt-2"> + <div v-for="(cItem, index) in item.contexts" :key="index"> + <div class="text-blue-500 cursor-pointer inline-block" @click="pageLinkClick(cItem)"> + <SvgIcon name="ele-Link" /> + <span class="ml-2">{{ cItem.metadata?.Title }}</span> + </div> + </div> + </div> + </div> + </div> +</template> + +<script setup lang="ts"> +import { md } from '../../libs/markdown'; +import { chatComProps } from '../common'; + + +const props = defineProps(chatComProps); + +const pageLinkClick = (item) => { + const nwin = window.open(''); + nwin.document.write(md.render(item.page_content)) + nwin.focus(); + if(item.metadata.Title){ + nwin.document.title = item.metadata.Title + + } +}; +</script> +<style scoped lang="scss"></style> diff --git a/src/components/chat/chatComponents/mapCom/MapCom.vue b/src/components/chat/chatComponents/mapCom/MapCom.vue new file mode 100644 index 0000000..299b2c7 --- /dev/null +++ b/src/components/chat/chatComponents/mapCom/MapCom.vue @@ -0,0 +1,130 @@ +<template> + <div class="h-[70vh] relative"> + <div ref="containerRef" class="h-full"></div> + <div v-if="bottomBarIsShow" class="absolute w-full bottom-0 bg-white border-gray-300 border border-solid"> + <div + class="w-28 h-5 absolute left-1/2 -translate-x-1/2 -translate-y-[100%] cursor-pointer bg-[#4974f3] rounded-t-lg flex-center" + @click="toggleShowChart" + > + <div + class="ywifont -rotate-90 text-white !text-[13px]" + :class="{ 'ywicon-zuoyoujiantou': chartIsShow, 'ywicon-zuoyoujiantou1': !chartIsShow }" + ></div> + </div> + <RecordSet v-if="chartIsShow" chartHeight="23vh" :data="CHART_DATA" class="mt-2" /> + </div> + </div> +</template> + +<script setup lang="ts"> +import { onMounted, ref } from 'vue'; +import { chatComProps } from '../common'; +import RecordSet from '../summaryCom/components/recordSet/RecordSet.vue'; +import { CHART_DATA } from './TestData'; +import monitorPointPic from './img/monitor-point.svg'; +import type { MapData } from './types'; +import type { GaoDePosition, LabelMarkerData } from '/@/model/map/GaoDeMap'; +import { GaoDeMap } from '/@/model/map/GaoDeMap'; + +let gaoDeMap = new GaoDeMap(); +const containerRef = ref<HTMLDivElement>(null); +const props = defineProps(chatComProps) as { + data: MapData; +}; +const createInfoWindow = () => { + const dom = `<div class="w-48 bg-white p-1 flex flex-col border-blue-500 border-solid border"> + <div class="bg-[#ca0dab] flex py-1 text-white px-5"> + <div class="pointer-title text-nowrap overflow-hidden text-ellipsis mx-auto"></div> + </div> + + </div>`; + + // const dom = `<div class="w-32 bg-white p-1 flex flex-col border-blue-500 border-solid border"> + // <div class="bg-[#ca0dab] flex-center py-1 text-white"> + // 姘存湀浜� + // </div> + // <div class="flex flex-col mt-2"> + // <div class="flex w-full text-blue-800"> + // <div class="overflow-hidden text-nowrap overflow-ellipsis self-end flex-auto text-right"> + // 0.72332 + // </div> + // <div class="flex-0 ml-4"> + // m + // </div> + + // </div> + + // </div> + // </div>`; + return dom; +}; + +const updateInfoWindow = (title: string) => { + const pointerTitle = infoWindow.dom.querySelector('.pointer-title'); + pointerTitle.innerHTML = title +''; +}; + +const addMarkerLayer = () => { + const dataList = (props.data?.values ?? []).map<LabelMarkerData>((item) => ({ + position: [item.posx, item.posy], + textColor: item.color, + extData: item, + title: item.title, + })); + gaoDeMap.addMarkerLayer(dataList, { + markerOpt: { + icon: { + url: monitorPointPic, + size: 30, + }, + click(e, label) { + if (!bottomBarIsShow.value) { + bottomBarIsShow.value = true; + } + if (!chartIsShow.value) { + chartIsShow.value = true; + } + infoWindow.open(gaoDeMap.map, label.getPosition() as any); + const extData = label.getExtData(); + updateInfoWindow(extData.title); + + }, + }, + layerOpt: { + // allowCollision:false + }, + }); +}; +const bottomBarIsShow = ref(false); +const chartIsShow = ref(true); +const toggleShowChart = () => { + chartIsShow.value = !chartIsShow.value; +}; + +let infoWindow: AMap.InfoWindow; + +onMounted(async () => { + await gaoDeMap.init({ + container: containerRef.value, + aMapOption: { + resizeEnable: true, + }, + }); + const southWest: GaoDePosition = [props.data.minx, props.data.miny]; + const northEast: GaoDePosition = [props.data.maxx, props.data.maxy]; + gaoDeMap.zoomToRect(southWest, northEast); + gaoDeMap.applyBasicPlugins(); + addMarkerLayer(); + infoWindow = new AMap.InfoWindow({ + content: createInfoWindow(), + offset: [3,-34], + closeWhenClickMap: true, + }); + +}); +</script> +<style scoped lang="scss"> +:deep(.amap-info-content) { + padding: 0; +} +</style> diff --git a/src/components/chat/chatComponents/mapCom/TestData.ts b/src/components/chat/chatComponents/mapCom/TestData.ts new file mode 100644 index 0000000..6b8e9cb --- /dev/null +++ b/src/components/chat/chatComponents/mapCom/TestData.ts @@ -0,0 +1,287 @@ +export const CHART_DATA = { + cols: [ + { type: 'time', title: '鏃堕棿' }, + { type: 'name', title: '鍚嶇О' }, + { type: 'value', title: '鍊�' }, + ], + type: 'recordset', + chart: 'muli_line', + title: '浜斾竴骞垮満DN200鐬椂鍘嬪姏', + values: [ + ['2024-07-20 00:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.698], + ['2024-07-20 00:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.62575], + ['2024-07-20 00:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.49825], + ['2024-07-20 00:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.562], + ['2024-07-20 00:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.448667], + ['2024-07-20 00:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.1285], + ['2024-07-20 00:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.188], + ['2024-07-20 00:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.210667], + ['2024-07-20 00:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.26875], + ['2024-07-20 00:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.301333], + ['2024-07-20 00:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.31975], + ['2024-07-20 00:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.3325], + ['2024-07-20 01:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.2662], + ['2024-07-20 01:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.511], + ['2024-07-20 01:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.205], + ['2024-07-20 01:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.766], + ['2024-07-20 01:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.57475], + ['2024-07-20 01:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.358], + ['2024-07-20 01:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.686667], + ['2024-07-20 01:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.312667], + ['2024-07-20 01:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.21775], + ['2024-07-20 01:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.307], + ['2024-07-20 01:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.426], + ['2024-07-20 01:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.42175], + ['2024-07-20 02:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.62575], + ['2024-07-20 02:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.618667], + ['2024-07-20 02:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.686667], + ['2024-07-20 02:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.715], + ['2024-07-20 02:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.732], + ['2024-07-20 02:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.732], + ['2024-07-20 02:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.7915], + ['2024-07-20 02:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.766], + ['2024-07-20 02:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.8], + ['2024-07-20 02:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.82975], + ['2024-07-20 02:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.88075], + ['2024-07-20 02:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.936], + ['2024-07-20 03:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.93175], + ['2024-07-20 03:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.9292], + ['2024-07-20 03:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.9445], + ['2024-07-20 03:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.95725], + ['2024-07-20 03:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.947333], + ['2024-07-20 03:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.924667], + ['2024-07-20 03:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.90625], + ['2024-07-20 03:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.766], + ['2024-07-20 03:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.890667], + ['2024-07-20 03:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.890667], + ['2024-07-20 03:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.97], + ['2024-07-20 03:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.981333], + ['2024-07-20 04:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.856667], + ['2024-07-20 04:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.90625], + ['2024-07-20 04:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.924667], + ['2024-07-20 04:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.95725], + ['2024-07-20 04:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.77875], + ['2024-07-20 04:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.7966], + ['2024-07-20 04:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.70225], + ['2024-07-20 04:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.80425], + ['2024-07-20 04:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.8068], + ['2024-07-20 04:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.766], + ['2024-07-20 04:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.95725], + ['2024-07-20 04:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.811333], + ['2024-07-20 05:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.777333], + ['2024-07-20 05:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.7915], + ['2024-07-20 05:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.67675], + ['2024-07-20 05:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.664], + ['2024-07-20 05:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.46], + ['2024-07-20 05:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.37075], + ['2024-07-20 05:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.335333], + ['2024-07-20 05:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.414667], + ['2024-07-20 05:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.34525], + ['2024-07-20 05:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.335333], + ['2024-07-20 05:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.26875], + ['2024-07-20 05:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.34525], + ['2024-07-20 06:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.312667], + ['2024-07-20 06:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.21775], + ['2024-07-20 06:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.1285], + ['2024-07-20 06:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.103], + ['2024-07-20 06:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.0265], + ['2024-07-20 06:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.21775], + ['2024-07-20 06:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.1642], + ['2024-07-20 06:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.11575], + ['2024-07-20 06:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.11575], + ['2024-07-20 06:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.142667], + ['2024-07-20 06:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.369333], + ['2024-07-20 06:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.4345], + ['2024-07-20 07:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.6385], + ['2024-07-20 07:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.573333], + ['2024-07-20 07:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.766], + ['2024-07-20 07:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.722286], + ['2024-07-20 07:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.698], + ['2024-07-20 07:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.5365], + ['2024-07-20 07:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.52375], + ['2024-07-20 07:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.539333], + ['2024-07-20 07:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.7405], + ['2024-07-20 07:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.54925], + ['2024-07-20 07:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.5926], + ['2024-07-20 07:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.31975], + ['2024-07-20 08:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.39625], + ['2024-07-20 08:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.46], + ['2024-07-20 08:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.21775], + ['2024-07-20 08:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.409], + ['2024-07-20 08:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.063333], + ['2024-07-20 08:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.301333], + ['2024-07-20 08:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.1795], + ['2024-07-20 08:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.21775], + ['2024-07-20 08:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.972667], + ['2024-07-20 08:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.108667], + ['2024-07-20 08:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.9245], + ['2024-07-20 08:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.836667], + ['2024-07-20 09:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.791333], + ['2024-07-20 09:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.75875], + ['2024-07-20 09:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.870667], + ['2024-07-20 09:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.78], + ['2024-07-20 09:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.91175], + ['2024-07-20 09:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.018], + ['2024-07-20 09:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.9245], + ['2024-07-20 09:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.86075], + ['2024-07-20 09:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.6644], + ['2024-07-20 09:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.83525], + ['2024-07-20 09:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.98825], + ['2024-07-20 09:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.1744], + ['2024-07-20 10:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.98825], + ['2024-07-20 10:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.2815], + ['2024-07-20 10:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.312667], + ['2024-07-20 10:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.301333], + ['2024-07-20 10:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.29425], + ['2024-07-20 10:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.26875], + ['2024-07-20 10:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.358], + ['2024-07-20 10:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.267333], + ['2024-07-20 10:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.29425], + ['2024-07-20 10:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.346667], + ['2024-07-20 10:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.256], + ['2024-07-20 10:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.29425], + ['2024-07-20 11:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.176667], + ['2024-07-20 11:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.19225], + ['2024-07-20 11:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.307], + ['2024-07-20 11:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.2662], + ['2024-07-20 11:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.2305], + ['2024-07-20 11:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.24325], + ['2024-07-20 11:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.2152], + ['2024-07-20 11:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.205], + ['2024-07-20 11:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.19225], + ['2024-07-20 11:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.233333], + ['2024-07-20 11:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.233333], + ['2024-07-20 11:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.2815], + ['2024-07-20 12:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.154], + ['2024-07-20 12:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.233333], + ['2024-07-20 12:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.244667], + ['2024-07-20 12:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.2305], + ['2024-07-20 12:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.324], + ['2024-07-20 12:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.097333], + ['2024-07-20 12:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.16675], + ['2024-07-20 12:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.244667], + ['2024-07-20 12:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.26875], + ['2024-07-20 12:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.24325], + ['2024-07-20 12:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.307], + ['2024-07-20 12:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.369333], + ['2024-07-20 13:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.3325], + ['2024-07-20 13:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.2866], + ['2024-07-20 13:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.307], + ['2024-07-20 13:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.409], + ['2024-07-20 13:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.409], + ['2024-07-20 13:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.307], + ['2024-07-20 13:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.4855], + ['2024-07-20 13:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.573333], + ['2024-07-20 13:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.426], + ['2024-07-20 13:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.82975], + ['2024-07-20 13:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.919], + ['2024-07-20 13:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.573333], + ['2024-07-20 14:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.754667], + ['2024-07-20 14:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.307], + ['2024-07-20 14:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.584667], + ['2024-07-20 14:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.46], + ['2024-07-20 14:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.7405], + ['2024-07-20 14:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.732], + ['2024-07-20 14:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.75325], + ['2024-07-20 14:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.65125], + ['2024-07-20 14:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.664], + ['2024-07-20 14:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.72775], + ['2024-07-20 14:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.766], + ['2024-07-20 14:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.8068], + ['2024-07-20 15:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.817], + ['2024-07-20 15:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.75325], + ['2024-07-20 15:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.766], + ['2024-07-20 15:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.822667], + ['2024-07-20 15:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.70225], + ['2024-07-20 15:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.62575], + ['2024-07-20 15:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.618667], + ['2024-07-20 15:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.49825], + ['2024-07-20 15:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.664], + ['2024-07-20 15:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.698], + ['2024-07-20 15:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.664], + ['2024-07-20 15:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.62575], + ['2024-07-20 16:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.550667], + ['2024-07-20 16:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.49825], + ['2024-07-20 16:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.52375], + ['2024-07-20 16:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.550667], + ['2024-07-20 16:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.47275], + ['2024-07-20 16:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.52375], + ['2024-07-20 16:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.4906], + ['2024-07-20 16:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.511], + ['2024-07-20 16:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.511], + ['2024-07-20 16:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.539333], + ['2024-07-20 16:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.607333], + ['2024-07-20 16:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.60025], + ['2024-07-20 17:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.618667], + ['2024-07-20 17:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.63], + ['2024-07-20 17:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.5365], + ['2024-07-20 17:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.57475], + ['2024-07-20 17:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.562], + ['2024-07-20 17:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.437333], + ['2024-07-20 17:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.29425], + ['2024-07-20 17:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.414667], + ['2024-07-20 17:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.39625], + ['2024-07-20 17:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.409], + ['2024-07-20 17:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.375], + ['2024-07-20 17:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.256], + ['2024-07-20 18:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.31975], + ['2024-07-20 18:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.2458], + ['2024-07-20 18:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.3835], + ['2024-07-20 18:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.256], + ['2024-07-20 18:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.3478], + ['2024-07-20 18:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.154], + ['2024-07-20 18:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.31975], + ['2024-07-20 18:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.12], + ['2024-07-20 18:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.165333], + ['2024-07-20 18:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.212286], + ['2024-07-20 19:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.494], + ['2024-07-20 20:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.54925], + ['2024-07-20 20:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.57475], + ['2024-07-20 20:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.63], + ['2024-07-20 20:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.528], + ['2024-07-20 20:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.29425], + ['2024-07-20 20:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.52375], + ['2024-07-20 20:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.244667], + ['2024-07-20 20:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.24325], + ['2024-07-20 20:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.21775], + ['2024-07-20 20:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.95], + ['2024-07-20 20:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.176667], + ['2024-07-20 20:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.11575], + ['2024-07-20 21:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.12], + ['2024-07-20 21:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.03925], + ['2024-07-20 21:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.06475], + ['2024-07-20 21:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.108667], + ['2024-07-20 21:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.9245], + ['2024-07-20 21:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.95], + ['2024-07-20 21:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.9704], + ['2024-07-20 21:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.001], + ['2024-07-20 21:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.91175], + ['2024-07-20 21:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.0826], + ['2024-07-20 21:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.103], + ['2024-07-20 21:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.307], + ['2024-07-20 22:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.086], + ['2024-07-20 22:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.961333], + ['2024-07-20 22:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.9245], + ['2024-07-20 22:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 22.9755], + ['2024-07-20 22:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.052], + ['2024-07-20 22:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.052], + ['2024-07-20 22:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.1285], + ['2024-07-20 22:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.188], + ['2024-07-20 22:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.12], + ['2024-07-20 22:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.14125], + ['2024-07-20 22:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.063333], + ['2024-07-20 22:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.154], + ['2024-07-20 23:00:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.285143], + ['2024-07-20 23:05:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.1846], + ['2024-07-20 23:10:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.2815], + ['2024-07-20 23:15:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.358], + ['2024-07-20 23:20:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.4396], + ['2024-07-20 23:25:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.3325], + ['2024-07-20 23:30:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.4345], + ['2024-07-20 23:35:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.437333], + ['2024-07-20 23:40:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.437333], + ['2024-07-20 23:45:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.49825], + ['2024-07-20 23:50:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.256], + ['2024-07-20 23:55:00', '浜斾竴骞垮満DN200鐬椂鍘嬪姏', 23.46], + ], +}; diff --git a/src/components/chat/chatComponents/mapCom/img/monitor-point.svg b/src/components/chat/chatComponents/mapCom/img/monitor-point.svg new file mode 100644 index 0000000..c199370 --- /dev/null +++ b/src/components/chat/chatComponents/mapCom/img/monitor-point.svg @@ -0,0 +1 @@ +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1721287703583" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2874" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 816.3c-82.2 0-159.4-32-217.5-90.1-58.1-58.1-90.1-135.3-90.1-217.5 0-75.1 27.3-147.4 77-203.6l48 42.4c-39.3 44.5-61 101.7-61 161.2 0 134.3 109.3 243.6 243.6 243.6S755.6 643 755.6 508.7c0-59.5-21.7-116.9-61.1-161.4l47.9-42.4c49.8 56.2 77.2 128.6 77.2 203.8 0 82.2-32 159.4-90.1 217.5-58.1 58.1-135.3 90.1-217.5 90.1z" fill="#1c86ff" p-id="2875"></path><path d="M544 417.1V207.7h-64v209.4c-40.1 13.4-69.1 51.3-69.1 95.9 0 55.8 45.4 101.1 101.1 101.1S613.1 568.8 613.1 513c0-44.5-29-82.5-69.1-95.9z m-32 133.1c-20.5 0-37.1-16.7-37.1-37.1S491.6 476 512 476s37.1 16.7 37.1 37.1-16.6 37.1-37.1 37.1z" fill="#1c86ff" p-id="2876"></path><path d="M510 957.5c-60.3 0-118.7-11.8-173.8-35.1-53.2-22.5-100.9-54.7-141.9-95.7S121.1 738 98.6 684.8C75.3 629.7 63.5 571.3 63.5 511s11.8-118.7 35.1-173.8c22.5-53.2 54.7-100.9 95.7-141.9s88.7-73.2 141.9-95.7C391.3 76.3 449.7 64.5 510 64.5s118.7 11.8 173.8 35.1c53.2 22.5 100.9 54.7 141.9 95.7s73.2 88.7 95.7 141.9c23.3 55.1 35.1 113.5 35.1 173.8s-11.8 118.7-35.1 173.8c-22.5 53.2-54.7 100.9-95.7 141.9s-88.7 73.2-141.9 95.7c-55.1 23.3-113.5 35.1-173.8 35.1z m0-829c-210.9 0-382.5 171.6-382.5 382.5S299.1 893.5 510 893.5 892.5 721.9 892.5 511 720.9 128.5 510 128.5z" fill="#1c86ff" p-id="2877"></path></svg> \ No newline at end of file diff --git a/src/components/chat/chatComponents/mapCom/types.ts b/src/components/chat/chatComponents/mapCom/types.ts new file mode 100644 index 0000000..2aa15c9 --- /dev/null +++ b/src/components/chat/chatComponents/mapCom/types.ts @@ -0,0 +1,18 @@ +export interface MapData { + type: string + title: string + minx: number + maxx: number + miny: number + maxy: number + values: MapDataValue[] + } + + export interface MapDataValue { + type: string + posx: number + posy: number + color: string + title: string + } + \ No newline at end of file diff --git a/src/components/chat/chatComponents/normalTextCom/NormalTextCom.vue b/src/components/chat/chatComponents/normalTextCom/NormalTextCom.vue new file mode 100644 index 0000000..46eeafa --- /dev/null +++ b/src/components/chat/chatComponents/normalTextCom/NormalTextCom.vue @@ -0,0 +1,13 @@ +<template> + <div v-html="md.render(data)"></div> +</template> + +<script setup lang="ts"> +import { md } from '../../libs/markdown'; +import { chatComProps } from '../common'; + +const props = defineProps({ + data:String +}); +</script> +<style scoped lang="scss"></style> diff --git a/src/components/chat/chatComponents/recordSetCom/RecordSetCom.vue b/src/components/chat/chatComponents/recordSetCom/RecordSetCom.vue new file mode 100644 index 0000000..8f2ad08 --- /dev/null +++ b/src/components/chat/chatComponents/recordSetCom/RecordSetCom.vue @@ -0,0 +1,112 @@ +<template> + <el-tabs v-model="activeName" class="demo-tabs min-w-[38rem] flex-column"> + <el-tab-pane class="h-full" label="Chart" name="first"> + <div class="flex-auto" v-resize="chartContainerResize"> + <div ref="chartRef" class="h-full"></div> + </div> + </el-tab-pane> + <el-tab-pane class="h-full" label="Data" name="second"> + <el-table :data="data.values" style="width: 100%" cellClassName="text-sm" headerCellClassName="text-sm"> + <el-table-column v-for="(item, index) in data?.names" :label="item" :key="index"> + <template #default="scope"> + {{ scope.row[index] }} + </template> + </el-table-column> + </el-table> + </el-tab-pane> + </el-tabs> +</template> +<script lang="ts" setup> +import type * as echarts from 'echarts'; +import _ from 'lodash'; +import { ref } from 'vue'; +import { SCATTER_SYMBOL_SIZE, chatComProps, getChatChartOption } from '../common'; +import { useDrawChatChart } from '../hooks/useDrawChatChart'; +const activeName = ref('first'); +const chartRef = ref<HTMLDivElement>(null); + +const defaultChartType = 'line'; +const props = defineProps(chatComProps); + +const drawChart = () => { + chartInstance.value.setOption( + _.defaultsDeep(getChatChartOption(), { + grid: { + bottom: '5%', + }, + + toolbox: { + feature: { + myBar: { + onclick: () => { + chartInstance.value.setOption({ + series: { + data: props.data.values, + type: 'bar', + symbol: 'none', + }, + }); + }, + }, + + myScatter: { + onclick: () => { + chartInstance.value.setOption({ + data: props.data.values, + type: 'scatter', + symbol: 'circle', + symbolSize: SCATTER_SYMBOL_SIZE, + }); + }, + }, + myLine: { + onclick: () => { + chartInstance.value.setOption({ + data: props.data.values, + type: 'line', + symbol: 'none', + smooth: true, + }); + }, + }, + }, + }, + + title: { + text: props.data.title, + }, + xAxis: { + name: props.data?.names[0], + }, + yAxis: { + name: props.data?.names[1], + }, + series: [ + { + data: props.data.values, + symbol: 'none', + smooth: true, + type: defaultChartType, + }, + ], + } as echarts.EChartsOption) + ); +}; +const { chartContainerResize, chartInstance } = useDrawChatChart({ chartRef, drawChart }); +</script> + +<style scoped lang="scss"> +.el-tabs { + :deep(.el-tabs__header) { + flex: 0 0 auto; + } + :deep(.el-tabs__content) { + flex: 1; + .el-tab-pane { + display: flex; + flex-direction: column; + min-height: 24rem; + } + } +} +</style> diff --git a/src/components/chat/chatComponents/summaryCom/SummaryCom.vue b/src/components/chat/chatComponents/summaryCom/SummaryCom.vue new file mode 100644 index 0000000..189921d --- /dev/null +++ b/src/components/chat/chatComponents/summaryCom/SummaryCom.vue @@ -0,0 +1,35 @@ +<template> + <div class="w-full space-y-3"> + <template v-if="parsedData && parsedData.length > 0"> + <component + v-for="(item, index) in parsedData" + :key="item.id" + :id="item.id" + :is="summaryAnswerTypeMapCom[item.type]" + :data="item" + :originData="originData" + :summaryIndex="index" + ></component> + </template> + <!-- <AmisPageTest /> --> + </div> +</template> + +<script setup lang="ts"> +import { computed } from 'vue'; +import { chatComProps } from '../common'; +import { SummaryAnswerType, summaryAnswerTypeMapCom } from './components/types'; +// import AmisPageTest from './components/amisPage/AmisPageTest.vue'; +const props = defineProps(chatComProps); + +const parsedData = computed(() => { + const newData = (props.data ?? []).map((item) => { + if (item.type === SummaryAnswerType.RecordSet && item.chart === 'table') { + item.type = SummaryAnswerType.RecordSetTable; + } + return item; + }); + return newData; +}); +</script> +<style scoped lang="scss"></style> diff --git a/src/components/chat/chatComponents/summaryCom/components/amisPage/AmisPage.vue b/src/components/chat/chatComponents/summaryCom/components/amisPage/AmisPage.vue new file mode 100644 index 0000000..4e26c7c --- /dev/null +++ b/src/components/chat/chatComponents/summaryCom/components/amisPage/AmisPage.vue @@ -0,0 +1,28 @@ +<template> + <div> + <span v-if="data?.title" class="text-base font-bold flex-center">{{ data?.title }}</span> + <AMISRenderer :schema="data?.amis_json" :locals="data?.amis_data" /> + </div> +</template> + +<script setup lang="ts"> +import type { PropType } from 'vue'; +import AMISRenderer from '/@/components/amis/AMISRenderer.vue'; + +// import 鍛ㄧぞ浼氬瓨閿�姣� from './testData/鍛ㄧぞ浼氬瓨閿�姣�.json' +// import 瀹㈡埛鎯呭喌 from './testData/瀹㈡埛鎯呭喌.json' + +// import 甯傚満缁煎悎鐘舵�� from './testData/甯傚満缁煎悎鐘舵��.json' +// import 閿�鍞搴﹂攢閲� from './testData/閿�鍞搴﹂攢閲�.json' +// import 缁忔祹杩愯 from './testData/缁忔祹杩愯.json' +// import testData from './testData.json' + +const props = defineProps({ + data: { + type: Object as PropType<any>, + }, +}); + + +</script> +<style scoped lang="scss"></style> diff --git a/src/components/chat/chatComponents/summaryCom/components/amisPage/testData.json b/src/components/chat/chatComponents/summaryCom/components/amisPage/testData.json new file mode 100644 index 0000000..e25a5ab --- /dev/null +++ b/src/components/chat/chatComponents/summaryCom/components/amisPage/testData.json @@ -0,0 +1,424 @@ +{ + "type": "page", + "id": "u:bad3f34e15c1", + "title": "閿�鍞搴﹂攢閲�", + "body": [ + { + "type": "flex", + "id": "u:9fc2a7909ab0", + "items": [ + { + "type": "container", + "body": [ + { + "type": "table-view", + "trs": [ + { + "background": "#F7F7F7", + "tds": [ + { + "body": { + "type": "tpl", + "wrapperComponent": "", + "tpl": "鍦板尯", + "id": "u:fcd843188090" + }, + "id": "u:115c8c523ab3" + }, + { + "body": { + "type": "tpl", + "wrapperComponent": "", + "tpl": "鐑熻崏鍚�", + "id": "u:f1453a2e11f6" + }, + "id": "u:ff36072024db", + "width": 215.5 + }, + { + "body": { + "type": "tpl", + "wrapperComponent": "", + "tpl": "閿�閲�", + "id": "u:afefdd621d12" + }, + "id": "u:3066304d8519" + } + ], + "id": "u:80ceaef8a6a1", + "height": 44 + }, + { + "tds": [ + { + "body": [ + { + "type": "tpl", + "wrapperComponent": "", + "tpl": "鍗庡寳鍦板尯", + "id": "u:a8b388a2ec7c" + } + ], + "id": "u:1e47918dbad8" + }, + { + "body": [ + { + "type": "tpl", + "tpl": "鍗椾含", + "inline": true, + "wrapperComponent": "", + "id": "u:9ddb428ec014" + } + ], + "id": "u:2816fc14a1d4" + }, + { + "body": { + "type": "tpl", + "wrapperComponent": "", + "tpl": "382994", + "id": "u:04f3b72b0b64" + }, + "id": "u:aa5d09a6d0c7" + } + ], + "id": "u:cadd57703de0", + "height": 44 + }, + { + "tds": [ + { + "body": { + "type": "tpl", + "wrapperComponent": "", + "tpl": "涓崡鍦板尯", + "id": "u:a0389abbed7e" + }, + "id": "u:04778c3e37a9" + }, + { + "body": { + "type": "tpl", + "wrapperComponent": "", + "tpl": "鐜夋邯", + "id": "u:c920488d04e7" + }, + "id": "u:041e86eee08f" + }, + { + "body": { + "type": "tpl", + "wrapperComponent": "", + "tpl": "234554", + "id": "u:18a2014d5e0b", + "maxLine": 2321423 + }, + "id": "u:f101b1f4e14e" + } + ], + "id": "u:6eab80ddca59", + "height": 44 + }, + { + "tds": [ + { + "body": { + "type": "tpl", + "wrapperComponent": "", + "tpl": "瑗垮崡鍦板尯", + "id": "u:0fb619f91806" + }, + "id": "u:de232323b71a" + }, + { + "body": [ + { + "type": "tpl", + "tpl": "涓崕", + "inline": true, + "wrapperComponent": "", + "id": "u:d0bc2b1558b4" + } + ], + "id": "u:2f07492deaa7" + }, + { + "body": { + "type": "tpl", + "wrapperComponent": "", + "tpl": "435463", + "id": "u:3577ce80f306" + }, + "id": "u:1e94b54401f7" + } + ], + "id": "u:b5b4f3a75485", + "height": 43 + }, + { + "tds": [ + { + "body": { + "type": "tpl", + "wrapperComponent": "", + "tpl": "瑗垮寳鍦板尯", + "id": "u:ce9c80378a1d" + }, + "id": "u:7af839fa58f9" + }, + { + "body": { + "type": "tpl", + "wrapperComponent": "", + "tpl": "鑺欒搲鐜�", + "id": "u:3459e0c78922" + }, + "id": "u:26d5ad53a5ac" + }, + { + "body": { + "type": "tpl", + "wrapperComponent": "", + "tpl": "64563543", + "id": "u:8623aaa2642b" + }, + "id": "u:f10464ace5ef" + } + ], + "id": "u:e5b8d15c2b4a", + "height": 48 + } + ], + "id": "u:286f1761479c", + "themeCss": { + "baseControlClassName": { + "padding-and-margin:default": { + "marginTop": "var(--sizes-size-2)", + "marginRight": "var(--sizes-size-2)", + "marginBottom": "var(--sizes-size-2)", + "marginLeft": "var(--sizes-size-2)" + } + } + } + } + ], + "size": "none", + "style": { + "position": "static", + "display": "block", + "flex": "1 1 auto", + "flexGrow": 1 + }, + "mobile": { + "style": { + "padding": 0, + "margin": 0 + } + }, + "wrapperBody": false, + "isFixedHeight": false, + "isFixedWidth": false, + "id": "u:8fcecf1e670c" + }, + { + "type": "container", + "body": [ + { + "type": "tpl", + "tpl": "閿�閲忓墠鍥涚殑鍝佽鍜屾暟閲�(鏉�)", + "inline": true, + "wrapperComponent": "", + "id": "u:e755fb569b63", + "themeCss": { + "baseControlClassName": { + "padding-and-margin:default": { + "paddingTop": "var(--sizes-size-5)", + "paddingRight": "var(--sizes-size-5)", + "paddingBottom": "var(--sizes-size-5)", + "paddingLeft": "var(--sizes-size-5)", + "marginTop": "var(--sizes-size-3)", + "marginRight": "var(--sizes-size-3)", + "marginBottom": "var(--sizes-size-3)", + "marginLeft": "var(--sizes-size-3)" + }, + "font:default": { + "fontSize": "var(--fonts-size-6)" + } + } + } + }, + { + "type": "progress", + "mode": "line", + "value": 40, + "strokeWidth": 9, + "valueTpl": "${value}%", + "id": "u:0a829cacfc23", + "placeholder": "-", + "progressClassName": "", + "map": [ + { + "color": "#528afb", + "value": 20 + } + ], + "className": "p-xs" + }, + { + "type": "tpl", + "tpl": "鍗椾含", + "inline": true, + "wrapperComponent": "", + "id": "u:01d253c393e6", + "themeCss": { + "baseControlClassName": { + "padding-and-margin:default": { + "marginTop": "var(--sizes-size-9)", + "marginRight": "var(--sizes-size-9)", + "marginBottom": "var(--sizes-size-9)", + "marginLeft": "var(--sizes-size-9)" + } + } + } + }, + { + "type": "progress", + "mode": "line", + "value": 60, + "strokeWidth": 9, + "valueTpl": "${value}%", + "id": "u:5d4e2a882a92", + "placeholder": "-", + "progressClassName": "", + "map": [ + { + "color": "#528afb", + "value": 20 + } + ], + "className": "p-xs" + }, + { + "type": "tpl", + "tpl": "涓崕", + "inline": true, + "wrapperComponent": "", + "id": "u:5d7403fe6a93", + "themeCss": { + "baseControlClassName": { + "padding-and-margin:default": { + "marginTop": "var(--sizes-size-9)", + "marginRight": "var(--sizes-size-9)", + "marginBottom": "var(--sizes-size-9)", + "marginLeft": "var(--sizes-size-9)" + } + } + } + }, + { + "type": "progress", + "mode": "line", + "value": 34, + "strokeWidth": 9, + "valueTpl": "${value}%", + "id": "u:d85d1fdcd367", + "placeholder": "-", + "progressClassName": "", + "map": [ + { + "color": "#528afb", + "value": 20 + } + ], + "className": "p-xs" + }, + { + "type": "tpl", + "tpl": "鐜夋邯", + "inline": true, + "wrapperComponent": "", + "id": "u:695ed7cb1479", + "themeCss": { + "baseControlClassName": { + "padding-and-margin:default": { + "marginTop": "var(--sizes-size-9)", + "marginRight": "var(--sizes-size-9)", + "marginBottom": "var(--sizes-size-9)", + "marginLeft": "var(--sizes-size-9)" + } + } + } + }, + { + "type": "progress", + "mode": "line", + "value": 24, + "strokeWidth": 9, + "valueTpl": "${value}%", + "id": "u:294c476cfd16", + "placeholder": "-", + "progressClassName": "", + "map": [ + { + "color": "#528afb", + "value": 20 + } + ], + "className": "p-xs" + }, + { + "type": "tpl", + "tpl": "鑺欒搲鐜�", + "inline": true, + "wrapperComponent": "", + "id": "u:9720acc51721", + "themeCss": { + "baseControlClassName": { + "padding-and-margin:default": { + "marginTop": "var(--sizes-size-9)", + "marginRight": "var(--sizes-size-9)", + "marginBottom": "var(--sizes-size-9)", + "marginLeft": "var(--sizes-size-9)" + } + } + } + } + ], + "size": "none", + "style": { + "position": "static", + "display": "block", + "flex": "1 1 auto", + "flexGrow": 1 + }, + "wrapperBody": false, + "isFixedHeight": false, + "isFixedWidth": false, + "id": "u:5c102f463e25" + } + ], + "style": { + "position": "relative", + "rowGap": "10px", + "columnGap": "10px", + "flexWrap": "nowrap", + "inset": "auto", + "mobile": { + "flexDirection": "column", + "marginTop": "18px" + } + }, + "isFixedHeight": false, + "isFixedWidth": false + } + ], + "bodyClassName": "m:p-0", + "headerClassName": "m:pt-0 m:pl-0 m:pb-1", + + "asideResizor": false, + "pullRefresh": { + "disabled": true + }, + "regions": ["body", "header"] +} diff --git a/src/components/chat/chatComponents/summaryCom/components/deviceLastValue/DeviceLastValueCom.vue b/src/components/chat/chatComponents/summaryCom/components/deviceLastValue/DeviceLastValueCom.vue new file mode 100644 index 0000000..63c58e4 --- /dev/null +++ b/src/components/chat/chatComponents/summaryCom/components/deviceLastValue/DeviceLastValueCom.vue @@ -0,0 +1,328 @@ +<template> + <!-- 瀹炴椂鐩戞祴鍒楄〃 --> + <div ref="containerRef" class="w-full flex justify-center" v-resize="resizeHandler"> + <div class="inline-block"> + <div + :class="`space-y-[${THICK_BORDER_WIDTH}px]`" + :style="{ + border: `${THICK_BORDER_WIDTH}px solid ${BORDER_COLOR}`, + backgroundColor: BORDER_COLOR, + }" + > + <div class="flex-center font-bold text-base bg-[#8db4e2]" :style="{ height: `${CELL_HEIGHT}px` }"> + {{ data.title }} + </div> + <div + v-for="(rowChunk, index) in currentRowChunkList" + :key="index" + :class="`space-y-[${THIN_BORDER_WIDTH}px]`" + :style="{ + backgroundColor: BORDER_COLOR, + }" + > + <!-- 琛ㄥご锛屽拰琛ㄦ牸鍐呭绗竴琛� --> + <div + :class="`space-y-[${THICK_BORDER_WIDTH}px]`" + :style="{ + backgroundColor: BORDER_COLOR, + }" + > + <div + class="flex" + :class="`space-x-[${THICK_BORDER_WIDTH}px]`" + :style="{ + backgroundColor: BORDER_COLOR, + }" + > + <div + v-for="(item, index) in maxColsNum" + :key="item" + :class="COL_HEADER_CELL_CLASS" + :style="{ + width: `${index === 0 ? firstColWidth : restColWidth}px`, + height: `${CELL_HEIGHT}px`, + backgroundColor: COL_HEADER_CELL_BG_COLOR, + }" + > + {{ index === 0 ? '' : rowChunk[index - 1]?.OTITLE }} + </div> + </div> + <MonitorContent + v-if="firstRow" + :firstColWidth="firstColWidth" + :restColWidth="restColWidth" + :title="firstRow.title" + :type="firstRow.id" + :values="rowChunk" + @itemClick="valueClick" + /> + </div> + <!-- 鍓╀綑琛� --> + <MonitorContent + v-for="(row, index) in restRows" + :key="index" + :firstColWidth="firstColWidth" + :restColWidth="restColWidth" + :title="row.title" + :type="row.id" + :values="rowChunk" + @itemClick="valueClick" + /> + </div> + </div> + <el-pagination + style="margin-bottom: 0 !important" + small + hide-on-single-page + v-model:current-page="pageIndex" + v-model:page-size="pageSize" + layout="total,prev,pager,next,jumper" + :total="total" + @current-change="handleCurrentChange" + /> + </div> + <RecordSetDialog v-model="chartDlgIsShow" :otype="chartDlgMapRow?.OTYPE" :oname="chartDlgMapRow?.ONAME" :indexName="indexName"/> + </div> +</template> + +<script setup lang="ts"> +import _ from 'lodash'; +import { computed, onActivated, onMounted, ref } from 'vue'; +import MonitorContent from './MonitorContent.vue'; +import { debounce, getTextWidth } from '/@/utils/util'; + +import { chatComProps } from '../../../common'; +import { + BORDER_COLOR, + CELL_HEIGHT, + CELL_MAX_WIDTH, + COL_HEADER_CELL_BG_COLOR, + COL_HEADER_CELL_CLASS, + PAGE_HEIGHT, + ROW_HEADER_CELL_MAX_WIDTH, + THICK_BORDER_WIDTH, + THIN_BORDER_WIDTH, +} from './constants'; +import type { Monitor, MonitorValue } from './types'; +import RecordSetDialog from '../recordSet/RecordSetDialog.vue'; + +const props = defineProps(chatComProps) as { + data: Monitor; +}; +const total = props.data?.values?.length ?? 0; +const pageIndex = ref(null); +const pageSize = ref(null); + +const containerRef = ref<HTMLDivElement>(null); +const measureWidthOffset = 2; +/** @description 娴嬮噺棣栧垪鍐呭瀹藉害 */ +const rowHeaderCellContentWidth = computed(() => { + if (!props.data.rows || props.data.rows.length === 0) return 0; + let maxLen = 0; + let maxTitle = ''; + for (const item of props.data.rows) { + const { title } = item; + const len = title.gblen(); + if (len > maxLen) { + maxLen = len; + maxTitle = title; + } + } + let maxWidth = getTextWidth(maxTitle, { + size: '0.875rem', + }); + + maxWidth += measureWidthOffset; + + if (maxWidth > ROW_HEADER_CELL_MAX_WIDTH) { + maxWidth = ROW_HEADER_CELL_MAX_WIDTH; + } + + return maxWidth; +}); +/** @description 娴嬮噺鍏朵粬鍒楀唴瀹瑰搴� */ +const colHeaderCellContentWidth = computed(() => { + if (!props.data.values || props.data.values.length === 0) return 0; + let maxLen = 0; + let maxTitle = ''; + for (const item of props.data.values) { + const { OTITLE } = item; + const len = OTITLE.gblen(); + if (len > maxLen) { + maxLen = len; + maxTitle = OTITLE; + } + } + + let maxWidth = getTextWidth(maxTitle, { + size: '0.875rem', + }); + maxWidth += measureWidthOffset; + + if (maxWidth > CELL_MAX_WIDTH) { + maxWidth = CELL_MAX_WIDTH; + } + + return maxWidth; +}); + +const firstColWidth = ref(rowHeaderCellContentWidth.value); +const restColWidth = ref(colHeaderCellContentWidth.value); +const calcMaxRowsNum = (groupCount: number, height, extraHeight = 0) => { + return Math.floor( + (height - 2 * THICK_BORDER_WIDTH - CELL_HEIGHT - extraHeight) / + (CELL_HEIGHT * groupCount + 2 * THICK_BORDER_WIDTH + THIN_BORDER_WIDTH * (groupCount - 2)) + ); +}; +let maxColsNum = ref<number>(null); +const resizeEvent = ({ width, height }) => { + if (width === 0 || height === 0) { + return; + } + // 鎸夋渶澶у搴︾畻鏈�澶у垪鏁� + maxColsNum.value = Math.floor( + (width - THICK_BORDER_WIDTH + colHeaderCellContentWidth.value - rowHeaderCellContentWidth.value) / + (colHeaderCellContentWidth.value + THICK_BORDER_WIDTH) + ); + + const currentWidth = + (maxColsNum.value - 1) * colHeaderCellContentWidth.value + + rowHeaderCellContentWidth.value + + THICK_BORDER_WIDTH * (maxColsNum.value - 1) + + THICK_BORDER_WIDTH * 2; + let restWidth = width - currentWidth; + if (restWidth > 0) { + // 灏藉彲鑳藉垎缁欑涓�鍒� + if (rowHeaderCellContentWidth.value + restWidth > ROW_HEADER_CELL_MAX_WIDTH) { + restWidth = rowHeaderCellContentWidth.value + restWidth - ROW_HEADER_CELL_MAX_WIDTH; + firstColWidth.value = ROW_HEADER_CELL_MAX_WIDTH; + } else { + firstColWidth.value = rowHeaderCellContentWidth.value + restWidth; + restWidth = 0; + } + + // 鍏朵綑鍒嗙粰鍏朵粬鍒� + if (restWidth !== 0) { + const averageWidth = restWidth / (maxColsNum.value - 1); + const currentWidth = colHeaderCellContentWidth.value + averageWidth; + restColWidth.value = currentWidth > CELL_MAX_WIDTH ? CELL_MAX_WIDTH : currentWidth; + } + } + + const groupCount = (props.data?.rows?.length ?? 0) + 1; + + // 鎸夋渶澶ч珮搴︾畻鏈�澶ц鏁� + let rowsNum = calcMaxRowsNum(groupCount, height); + + // 鎸夋渶澶у垪鏁伴摵 + const maxColsRowsNum = Math.ceil(total / maxColsNum.value); + const isNeedPage = maxColsRowsNum > rowsNum; + rowsNum = isNeedPage ? calcMaxRowsNum(groupCount, height, PAGE_HEIGHT) : maxColsRowsNum; + // rowsNum 琛岋紝maxColsNum鍒楋紝绗竴鍒椾笉绠� + pageSize.value = isNeedPage + ? rowsNum * (maxColsNum.value - 1) + : total % (maxColsNum.value - 1) === 0 + ? total + : (Math.floor(total / (maxColsNum.value - 1)) + 1) * (maxColsNum.value - 1); + pageIndex.value = 1; + // isNeedPage 鏄惁鍒嗛〉锛宺owsNum 琛屾暟锛宮axColsNum 鍒楁暟锛� +}; +const resizeHandler = debounce(resizeEvent); + +const handleCurrentChange = () => {}; + +const firstRow = computed(() => props.data?.rows?.[0]); +const restRows = computed(() => props.data?.rows?.slice(1)); +const pageChunkList = computed(() => { + const chunkResult = _.chunk(props.data.values ?? [], pageSize.value); + const last = chunkResult.at(-1); + if (last) { + const restNum = pageSize.value - last.length; + const emptyData = _.fill(Array(restNum), { + ONAME: '', + OTIME: '', + OTITLE: '', + OTYPE: '', + } as MonitorValue); + const lastData = last.concat(emptyData); + chunkResult[chunkResult.length - 1] = lastData; + } + + return chunkResult; +}); +const currentPageChunk = computed(() => { + pageChunkList.value; + if (pageIndex.value == null) return []; + const pageChunk = pageChunkList.value[pageIndex.value - 1]; + + return pageChunk; +}); + +const currentRowChunkList = computed(() => { + if (!currentPageChunk.value || currentPageChunk.value.length === 0) return []; + const chunkResult = _.chunk(currentPageChunk.value, maxColsNum.value - 1); + return chunkResult; +}); + +//#region ====================== 鐐瑰嚮鐪嬫洸绾� ====================== + +const chartDlgIsShow = ref(false); +const chartDlgMapRow = ref(null); +/** @description 鎸囨爣鍚嶇О */ +const indexName = ref(null); +const valueClick = (item,type) => { + chartDlgMapRow.value = item; + chartDlgIsShow.value = true; + indexName.value = type; +}; +//#endregion + +// 璁$畻鏈�澶у垪鏁� +// (x-1)* cellWidth + rowHeaderCellContentWidth.value+thickBorderWidth*(x-1)+thickBorderWidth*2 <= width; + +// x*(cellWidth+thickBorderWidth)+thickBorderWidth - cellWidth + rowHeaderCellContentWidth.value<=width; +// x<= (width-thickBorderWidth + cellWidth-rowHeaderCellContentWidth.value)/(cellWidth+thickBorderWidth); + +// const groupCount = (TEST_DATA?.rows?.length ?? 0) + 1; +// 璁$畻鏈�澶ц鏁� +// y * (cellHeight * groupCount) + +// (y - 1) * (2 * thickBorderWidth) +cellHeight+thickBorderWidth +// thickBorderWidth + +// thickBorderWidth * 2 + +// y * (groupCount - 2) * thinBorderWidth <= +// height; + +// (cellHeight * groupCount + 2 * thickBorderWidth + thinBorderWidth(groupCount - 2) * y + thickBorderWidth <=height; + +// y<= (height-thickBorderWidth)/(cellHeight * groupCount + 2 * thickBorderWidth + thinBorderWidth(groupCount - 2) ) + +onMounted(() => { + resizeEvent({ + width: containerRef.value.clientWidth, + height: 0.7 * document.body.clientHeight, + }); +}); +</script> +<style scoped lang="scss"> +:deep(.space-y-\[2px\] > :not([hidden]) ~ :not([hidden])) { + --tw-space-y-reverse: 0; + margin-top: calc(2px * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(2px * var(--tw-space-y-reverse)); +} +:deep(.space-y-\[1px\] > :not([hidden]) ~ :not([hidden])) { + --tw-space-y-reverse: 0; + margin-top: calc(1px * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(1px * var(--tw-space-y-reverse)); +} + +:deep(.space-x-\[2px\] > :not([hidden]) ~ :not([hidden])) { + --tw-space-x-reverse: 0; + margin-right: calc(2px * var(--tw-space-x-reverse)); + margin-left: calc(2px * calc(1 - var(--tw-space-x-reverse))); +} +:deep(.space-x-\[1px\] > :not([hidden]) ~ :not([hidden])) { + --tw-space-x-reverse: 0; + margin-right: calc(1px * var(--tw-space-x-reverse)); + margin-left: calc(1px * calc(1 - var(--tw-space-x-reverse))); +} +</style> diff --git a/src/components/chat/chatComponents/summaryCom/components/deviceLastValue/MonitorContent.vue b/src/components/chat/chatComponents/summaryCom/components/deviceLastValue/MonitorContent.vue new file mode 100644 index 0000000..23721e6 --- /dev/null +++ b/src/components/chat/chatComponents/summaryCom/components/deviceLastValue/MonitorContent.vue @@ -0,0 +1,117 @@ +<template> + <div + class="flex" + :class="`space-x-[${THICK_BORDER_WIDTH}px]`" + :style="{ + backgroundColor: BORDER_COLOR, + }" + > + <div + :class="ROW_HEADER_CELL_CLASS" + :style="{ + width: `${firstColWidth}px`, + height: `${CELL_HEIGHT}px`, + }" + > + {{ title }} + </div> + <div + v-for="(item, index) in values" + :key="index" + :class="CONTENT_CELL_CLASS" + :style="{ + width: `${restColWidth}px`, + height: `${CELL_HEIGHT}px`, + }" + > + <span + class="cursor-pointer" + @mouseover="valueMouseOver($event, item)" + @mouseleave="valueMouseLeave" + @click="emit('itemClick', item, type)" + > + {{ item[type] }} + </span> + </div> + <div + v-show="hoverState.show && hoverState.data" + class="z-40 fixed p-2 bg-white" + style="transform-origin: center top" + :style="{ + left: hoverState.left + 'px', + top: hoverState.top + 'px', + }" + > + <div v-if="hoverState.data?.OTITLE" class="font-bold mb-1">{{ hoverState.data?.OTITLE }}</div> + <div class="w-full space-y-1"> + <div v-if="hoverState.data?.OTYPE" class="flex"> + <div class="w-8">绫诲瀷</div> + <div class="before:content-[':'] before:pr-1.5">{{ hoverState.data?.OTYPE }}</div> + </div> + <div v-if="hoverState.data?.ONAME" class="flex"> + <div class="w-8">缂栧彿</div> + <div class="before:content-[':'] before:pr-1.5">{{ hoverState.data?.ONAME }}</div> + </div> + <div v-if="hoverState.data?.[type] || hoverState.data?.[type] === 0" class="flex"> + <div class="w-8">鐩戞祴</div> + <div class="before:content-[':'] before:pr-1.5">{{ hoverState.data?.[type] }}</div> + </div> + <div class="flex" v-if="hoverState.data?.OTIME"> + <div class="w-8">鏃堕棿</div> + <div class="before:content-[':'] before:pr-1.5">{{ hoverState.data?.OTIME }}</div> + </div> + </div> + </div> + </div> +</template> + +<script setup lang="ts"> +import { reactive, type PropType } from 'vue'; +import { BORDER_COLOR, CELL_HEIGHT, CONTENT_CELL_CLASS, ROW_HEADER_CELL_CLASS, THICK_BORDER_WIDTH } from './constants'; +import type { MonitorValue } from './types'; + +const emit = defineEmits(['itemClick']); +const props = defineProps({ + /** @description 鏍囬 */ + title: { + type: String, + }, + /** @description 绫诲瀷 */ + type: { + type: String, + }, + /** @description 鍊� */ + values: { + type: Object as PropType<MonitorValue[]>, + }, + firstColWidth: { + type: Number, + }, + + restColWidth: { + type: Number, + }, +}); + +const hoverState = reactive({ + left: 0, + top: 0, + show: false, + data: null as MonitorValue, +}); + +const TIP_OFFSET = 10; +const valueMouseOver = (e, item: MonitorValue) => { + hoverState.show = true; + const { pageX, pageY } = e; + hoverState.left = pageX + TIP_OFFSET; + hoverState.top = pageY + TIP_OFFSET; + hoverState.data = item; +}; + +const valueMouseLeave = () => { + hoverState.show = false; + hoverState.data = null; +}; +</script> +<style scoped lang="scss"></style> diff --git a/src/components/chat/chatComponents/summaryCom/components/deviceLastValue/constants.ts b/src/components/chat/chatComponents/summaryCom/components/deviceLastValue/constants.ts new file mode 100644 index 0000000..7af0c75 --- /dev/null +++ b/src/components/chat/chatComponents/summaryCom/components/deviceLastValue/constants.ts @@ -0,0 +1,18 @@ + +export const CONTENT_CELL_CLASS = 'bg-white flex-center flex-0 !text-[#7331a5] cursor-default'; +export const ROW_HEADER_CELL_CLASS = 'bg-white flex-center flex-0 cursor-default'; +export const ROW_HEADER_CELL_MAX_WIDTH = 198; +export const COL_HEADER_CELL_CLASS = 'font-bold flex-center flex-0 cursor-default'; +export const COL_HEADER_CELL_BG_COLOR = '#c5d9f1' +export const CELL_MAX_WIDTH = 198; +export const CELL_HEIGHT = 32; +export const THIN_BORDER_WIDTH = 1; +export const THICK_BORDER_WIDTH = 2; +export const PAGE_HEIGHT = 39; + +export const rowCount = 5; +export const BORDER_COLOR = 'rgb(115 168 231)'; + + +export const FIRST_COL_MAX_OFFSET = 4; +export const REST_COL_MAX_OFFSET = 4; \ No newline at end of file diff --git a/src/components/chat/chatComponents/summaryCom/components/deviceLastValue/types.ts b/src/components/chat/chatComponents/summaryCom/components/deviceLastValue/types.ts new file mode 100644 index 0000000..6d663c5 --- /dev/null +++ b/src/components/chat/chatComponents/summaryCom/components/deviceLastValue/types.ts @@ -0,0 +1,21 @@ +import type { SummaryAnswerType } from '../types'; + +export type MonitorRow = { + id: string; + title: string; +}; + +export type MonitorValue = { + OTYPE: string; + ONAME: string; + OTITLE: string; + OTIME: string; + ZD: number; + YL: number; +}; +export type Monitor = { + title:string; + type: SummaryAnswerType.DeviceLastValue; + rows: MonitorRow[]; + values: MonitorValue[]; +}; diff --git a/src/components/chat/chatComponents/summaryCom/components/recordSet/RecordSet.vue b/src/components/chat/chatComponents/summaryCom/components/recordSet/RecordSet.vue new file mode 100644 index 0000000..70dab88 --- /dev/null +++ b/src/components/chat/chatComponents/summaryCom/components/recordSet/RecordSet.vue @@ -0,0 +1,457 @@ +<!-- 鏄ㄦ棩渚涙按绠$綉姒傚喌 --> +<template> + <div class="w-full"> + <div class="flex mb-4 flex-wrap"> + <!-- TimeRange v-model 璺� @change 涓殑鍊间細涓嶄竴鏍凤紝浠change 涓负鍑� --> + <template v-if="visibleParams && visibleParams.length > 0"> + <component + class="flex-0 m-1" + v-model="paramsValueList[index].value" + v-for="(item, index) in visibleParams as any" + :key="item.id" + :id="item.id" + :is="recordSetMapCom[item.type]" + :data="item" + :originData="originData" + @change="(val) => handleQueryChange(val, item)" + :disabled="chartLoading" + ></component> + </template> + <slot> </slot> + + <YRange v-model="yRange" @input="yRangeInput" /> + <el-checkbox class="m-1" v-model="isMultiCompare" label="澶氭棩瀵规瘮" @change="multiCompareChange"></el-checkbox> + </div> + <div :style="{ height: chartHeight }" v-resize="chartContainerResize" v-loading="chartLoading"> + <div ref="chartRef"></div> + </div> + </div> +</template> + +<script setup lang="ts"> +import type * as echarts from 'echarts'; +import _ from 'lodash'; +import moment from 'moment'; +import type { PropType } from 'vue'; +import { computed, ref } from 'vue'; +import { SCATTER_SYMBOL_SIZE, getChatChartOption } from '../../../common'; +import { useDrawChatChart } from '../../../hooks/useDrawChatChart'; +import YRange from './components/YRange.vue'; +import type { RecordSet, RecordSetParamsItem } from './types'; +import { RecordSetParamsType, recordSetMapCom } from './types'; +import { filterQuery } from '/@/api/ai/chat'; +import { deepClone } from '/@/utils/other'; +import { debounce } from '/@/utils/util'; +import { ChartTypeEnum } from '../../../types'; + +const chartRef = ref<HTMLDivElement>(null); +const defaultDisplayType = 'line'; +const yRange = ref({ + min: null as number, + max: null as number, +}); +// const props = defineProps({ +// data: { +// type: Object as PropType<RecordSet>, +// }, +// }); + +const props = defineProps({ + data: { + type: Object as PropType<any>, + }, + originData: { + type: Object as PropType<any>, + }, + summaryIndex: { + type: Number, + }, + chartHeight: { + type: String, + default: '20rem', + }, +}) as { + data: RecordSet; +}; +const chartLoading = ref(false); + +const visibleParams = computed(() => { + const visibleList = props.data?.params?.filter((item) => !item?.hide) ?? []; + + const newList: RecordSetParamsItem[] = []; + let nextMatchIndex = null; + for (let index = 0; index < visibleList.length; index++) { + if (nextMatchIndex === index) continue; + const current = visibleList[index]; + const currentAny = current as any; + if (index !== visibleList.length - 1 && currentAny.type === RecordSetParamsType.StartTime) { + const next = visibleList[index + 1]; + const nextAny = next as any; + + if (nextAny.group === currentAny.group && nextAny.type === RecordSetParamsType.EndTime) { + newList.push({ + type: RecordSetParamsType.TimeRange, + value: [currentAny.value, nextAny.value], + group: currentAny.group, + range: [currentAny, nextAny], + } as any); + nextMatchIndex = index + 1; + } else { + newList.push(current); + } + } else { + newList.push(current); + } + } + + return newList; +}); +const paramsValueList = ref(deepClone(visibleParams.value)); +let groupedValues = null; +let timeIndex = undefined; +let valueIndex = undefined; +let nameIndex = undefined; + +let timeCol = null; +let valueCol = null; + +let preData = null; + +let activeChartType: ChartTypeEnum = ChartTypeEnum.Line; + +const getChartTypeSeriesOption = (type: ChartTypeEnum) => { + let result = {}; + switch (type) { + case ChartTypeEnum.Bar: + result = { + type: 'bar', + symbol: 'none', + }; + + break; + case ChartTypeEnum.Line: + result = { + type: 'line', + symbol: 'none', + smooth: true, + }; + + break; + + case ChartTypeEnum.Scatter: + result = { + type: 'scatter', + symbol: 'circle', + symbolSize: SCATTER_SYMBOL_SIZE, + }; + + break; + default: + break; + } + + return result; +}; + +const setNewOption = (series?: any[], extraOption: echarts.EChartsOption = {}) => { + const isEmpty = !series || series.length === 0; + if (isEmpty) { + series = Object.keys(groupedValues).map((item) => { + const values = groupedValues[item]; + return { + name: item === 'default' ? '' : item, + data: values.map((item) => [item[timeIndex], item[valueIndex]]), + type: defaultDisplayType, + symbol: 'none', + smooth: true, + }; + }); + } + const combineOption = _.defaultsDeep(extraOption, getChatChartOption(), { + grid: { + bottom: 20, + }, + legend: { + top: 19, + show: series?.length > 1, + type: 'scroll', + }, + toolbox: { + show: true, + feature: { + myBar: { + onclick: () => { + activeChartType = ChartTypeEnum.Bar; + chartInstance.value.setOption({ + series: series.map((item) => ({ + ...item, + ...getChartTypeSeriesOption(activeChartType), + })), + }); + }, + }, + + myScatter: { + onclick: () => { + activeChartType = ChartTypeEnum.Scatter; + + chartInstance.value.setOption({ + series: series.map((item) => ({ + ...item, + ...getChartTypeSeriesOption(activeChartType), + })), + }); + }, + }, + myLine: { + onclick: () => { + activeChartType = ChartTypeEnum.Line; + chartInstance.value.setOption({ + series: series.map((item) => ({ + ...item, + ...getChartTypeSeriesOption(activeChartType), + })), + }); + }, + }, + }, + }, + + title: { + text: preData?.title, + }, + xAxis: { + name: timeCol?.title, + }, + yAxis: { + name: valueCol?.title, + /** @description 涓嶅己鍒朵繚鐣�0 */ + scale: true, + }, + series: series, + } as echarts.EChartsOption); + chartInstance.value.setOption(combineOption, { + notMerge: true, + }); +}; + +const handleData = () => { + const data = props.data; + if (!data || !data.cols || !data.values) { + return; + } + preData = data; + const xType = 'time'; + timeIndex = data.cols.findIndex((item) => item.type === 'time'); + if (timeIndex === -1) { + timeIndex = 0; + } + timeCol = data.cols[timeIndex]; + + valueIndex = data.cols.findIndex((item) => item.type === 'value'); + if (valueIndex === -1) { + valueIndex = 2; + } + valueCol = data.cols[valueIndex]; + + let nameCol = null; + groupedValues = null; + if (data.chart === 'muli_line') { + nameIndex = data.cols.findIndex((item) => item.type === 'name'); + if (nameIndex === -1) { + nameIndex = 1; + } + nameCol = data.cols[nameIndex]; + groupedValues = _.groupBy(data.values, (item) => item[nameIndex]); + } else if (data.chart === 'single_line') { + groupedValues = { + default: data.values, + }; + } else { + // 榛樿閮藉綋muli_line + let nameIndex = data.cols.findIndex((item) => item.type === 'name'); + if (nameIndex === -1) { + nameIndex = 1; + } + nameCol = data.cols[nameIndex]; + groupedValues = _.groupBy(data.values, (item) => item[nameIndex]); + } +}; + +const drawChart = () => { + const data = props.data; + if (!data || !data.cols || !data.values) { + return; + } + handleData(); + setNewOption(); +}; +const { chartContainerResize, chartInstance } = useDrawChatChart({ chartRef, drawChart }); + +// 鏇存崲鍒楄〃 +const changeMap = new Map<string, string>(null); + +const handleQueryChange = async (val: any, item: RecordSetParamsItem) => { + if (!val) return; + + const historyId = (props as any).originData.historyId; + const summaryIndex = (props as any).summaryIndex; + let res = null; + + try { + if (item.type === RecordSetParamsType.TimeRange) { + changeMap.set(item.range[0].id, val[0]), changeMap.set(item.range[1].id, val[1]); + } else { + changeMap.set(item.id, val); + } + const paramsObj = {}; + for (const [key, value] of changeMap) { + paramsObj[key] = value; + } + const params = { + history_id: historyId, + query_index: summaryIndex, + param_json: JSON.stringify(paramsObj), + }; + res = await filterQuery(params); + chartLoading.value = true; + } finally { + chartLoading.value = false; + } + + const title = res?.values?.title; + const values = res?.values?.values ?? []; + groupedValues = _.groupBy(values, (item) => item[nameIndex]); + + if (isMultiCompare.value) { + handleMultiCompare(); + } else { + chartInstance.value.setOption({ + title: { + text: title, + }, + series: + groupedValues && + Object.keys(groupedValues).map((item) => { + const values = groupedValues[item]; + return { + data: values.map((item) => [item[timeIndex], item[valueIndex]]), + }; + }), + }); + } +}; + +const getSingleDayOption = (day=COMMON_DAY) => ({ + tooltip: { + show: true, + trigger: 'axis', + formatter(params) { + const itemList = params.map((item, index) => { + return `<div style="margin: ${index === 0 ? 0 : 10}px 0 0; line-height: 1"> + <div style="margin: 0px 0 0; line-height: 1"> + ${item.marker}<span style="font-size: 14px; color: #666; font-weight: 400; margin-left: 2px" + >${item.seriesName}</span + ><span style="float: right; margin-left: 20px; font-size: 14px; color: #666; font-weight: 900">${item.data[1]}</span> + <div style="clear: both"></div> + </div> + <div style="clear: both"></div> + </div>`; + }); + + const result = `<div style="margin: 0px 0 0; line-height: 1"> + <div style="margin: 0px 0 0; line-height: 1"> + <div style="font-size: 14px; color: #666; font-weight: 400; line-height: 1">${params?.[0]?.data[0]?.slice(10, 16)}</div> + <div style="margin: 10px 0 0; line-height: 1"> + ${itemList.join('')} + <div style="clear: both"></div> + </div> + <div style="clear: both"></div> + </div> + <div style="clear: both"></div> + </div>`; + return result; + }, + }, + xAxis: { + min: day + ' 00:00:00', + max: day + ' 23:59:59', + splitNumber: 10, + axisLabel: { + formatter: (val) => { + const newVal = moment(val).format('HH:mm'); + return newVal; + }, + showMaxLabel: true, + }, + }, +} as echarts.EChartsOption); +//#region ====================== 璁剧疆Y鑼冨洿 ====================== +const debounceSetYRange = debounce((val) => { + chartInstance.value.setOption({ + yAxis: { + min: val.min, + max: val.max, + }, + }); +}); + +const yRangeInput = (val) => { + debounceSetYRange(val); +}; + +//#endregion + +//#region ====================== 澶氭棩瀵规瘮 ====================== +// 澶氭棩瀵规瘮鍩哄噯鏃堕棿 +const COMMON_DAY = '2024-07-26'; + +const isMultiCompare = ref(false); +const handleMultiCompare = () => { + if (!isMultiCompare.value) return; + const cloneData = deepClone(groupedValues); + const seriesData = Object.keys(cloneData).reduce((preVal, curVal, curIndex, arr) => { + const values = cloneData[curVal]; + const isMulti = arr.length > 1; + const groupByDateValues = _.groupBy(values, (item) => moment(item[timeIndex]).format('YYYY-MM-DD')); + for (const key in groupByDateValues) { + if (Object.prototype.hasOwnProperty.call(groupByDateValues, key)) { + const val = groupByDateValues[key]; + + const newVal = val.map((item) => { + // 鏂板悕绉� + item[nameIndex] = isMulti ? `${curVal}_${key}` : `${key}`; + item[timeIndex] = COMMON_DAY + ' ' + moment(item[timeIndex]).format('HH:mm:ss'); + return item; + }); + + preVal.push(newVal); + } + } + return preVal; + }, []); + const series = seriesData.map<echarts.SeriesOption>((item) => ({ + name: item[0]?.[nameIndex], + data: item.map((item) => [item[timeIndex], item[valueIndex]]), + ...getChartTypeSeriesOption(activeChartType), + })); + setNewOption(series, getSingleDayOption()); +}; +const multiCompareChange = (val) => { + if (!groupedValues) return; + if (val) { + handleMultiCompare(); + } else { + setNewOption(); + } +}; +//#endregion + +defineExpose({ + drawChart, + isMultiCompare, + handleMultiCompare, + handleData, +}); +</script> +<style scoped lang="scss"></style> diff --git a/src/components/chat/chatComponents/summaryCom/components/recordSet/RecordSetDialog.vue b/src/components/chat/chatComponents/summaryCom/components/recordSet/RecordSetDialog.vue new file mode 100644 index 0000000..17e3a83 --- /dev/null +++ b/src/components/chat/chatComponents/summaryCom/components/recordSet/RecordSetDialog.vue @@ -0,0 +1,91 @@ +<template> + <el-dialog :destroy-on-close="true" v-model="isShow" draggable :close-on-click-modal="false" :title="chartValues?.title"> + <RecordSet chartHeight="30rem" ref="recordSetRef" :data="chartValues"> + <TimeRange ref="timeRangeRef" v-model="queryRange" class="flex-0 m-1" @change="timeRangeChange" /> + <List class="flex-0 m-1" v-model="stepTime" :data="listData" @change="selectStepChange" /> + </RecordSet> + </el-dialog> +</template> + +<script setup lang="ts"> +import { nextTick, ref, watch } from 'vue'; +import RecordSet from './RecordSet.vue'; +import { queryScadaTimeValues } from '/@/api/ai/chat'; +import { useCompRef } from '/@/utils/types'; +import { getRecentDateRange } from '/@/utils/util'; +import { formatDate } from '/@/utils/formatTime'; +import TimeRange from './components/TimeRange.vue'; +import List from './components/List.vue'; + +const props = defineProps(['otype', 'oname', 'indexName']); + +const isShow = defineModel({ + type: Boolean, +}); + +const recordSetRef = useCompRef(RecordSet); +const timeRangeRef = useCompRef(TimeRange); + +const listData = { + list: [ + { title: '5鍒嗛挓', value: '5 minutes' }, + { title: '10鍒嗛挓', value: '10 minutes' }, + { title: '鍗婂皬鏃�', value: '30 minutes' }, + { title: '1灏忔椂', value: '1 hours' }, + { title: '1澶�', value: '1 days' }, + ], + type: 'list', + title: '鏃堕暱', + value: '5 minutes', +} as any; + +const queryRange = ref<string[]>(null); +const timeRangeChange = (val) => { + setChartData(); +}; + +const selectStepChange = (val) => { + setChartData(); +}; +const stepTime = ref('5 minutes'); +const chartValues = ref(null); +const setChartData = async () => { + const res = await queryScadaTimeValues({ + // 璁惧绫诲瀷 + ptype: props.otype, + // 璁惧鍚嶇О + pname: props.oname, + otype: props.indexName, + start_time: timeRangeRef.value.formatDateValue[0], + end_time: timeRangeRef.value.formatDateValue[1], + step_time: stepTime.value, + }); + chartValues.value = res.values; + chartValues.value.chart = 'single_line'; + nextTick(() => { + setTimeout(() => { + if(recordSetRef.value.isMultiCompare){ + recordSetRef.value.handleData(); + recordSetRef.value.handleMultiCompare(); + }else{ + recordSetRef.value.drawChart(); + } + }, 0); + }); +}; + +watch( + () => isShow.value, + (val) => { + if (!val) { + return; + } + queryRange.value = getRecentDateRange(1).map((item) => formatDate(item)); + nextTick(()=>{ + setChartData(); + + }) + } +); +</script> +<style scoped lang="scss"></style> diff --git a/src/components/chat/chatComponents/summaryCom/components/recordSet/components/List.vue b/src/components/chat/chatComponents/summaryCom/components/recordSet/components/List.vue new file mode 100644 index 0000000..9f6ac7d --- /dev/null +++ b/src/components/chat/chatComponents/summaryCom/components/recordSet/components/List.vue @@ -0,0 +1,60 @@ +<template> + <el-select + class="w-32" + :style="{width:selectWidth}" + v-model="selectValue" + @change="changeValue" + :disabled="disabled" + :placeholder="data.title" + > + <el-option v-for="item in data.list" :key="item.value" :value="item.value" :label="item.title"></el-option> + </el-select> +</template> + +<script setup lang="ts"> +import { ref, type PropType, computed } from 'vue'; +import type { ListParam } from '../types'; +import { getTextWidth } from '/@/utils/util'; + +const props = defineProps({ + data: { + type: Object as PropType<ListParam>, + }, + disabled: { + type: Boolean, + default: false, + }, +}); + +const emit = defineEmits(['change']); +const SELECT_OFFSET = 47; +const selectWidth = computed(() => { + if (props.data?.list?.length > 0) { + // 浠ユ渶澶у瓧闀夸负瀹藉害 + const widthList = props.data.list.map((item) => + getTextWidth(item.title, { + size: fontSize.value, + }) + ); + const maxWidth = Math.max(...widthList); + const realWidth = maxWidth + SELECT_OFFSET; + return realWidth + 'px'; + } else { + return 0; + } +}); + +const fontSize = ref('14px'); +const selectValue = defineModel({ + type: String, +}); + +const changeValue = (val) => { + emit('change', val); +}; +</script> +<style scoped lang="scss"> +:deep(.el-input) { + font-size: v-bind(fontSize); +} +</style> diff --git a/src/components/chat/chatComponents/summaryCom/components/recordSet/components/TimeRange.vue b/src/components/chat/chatComponents/summaryCom/components/recordSet/components/TimeRange.vue new file mode 100644 index 0000000..674e9bd --- /dev/null +++ b/src/components/chat/chatComponents/summaryCom/components/recordSet/components/TimeRange.vue @@ -0,0 +1,146 @@ +<template> + <div class="flex items-center"> + <div class="flex items-center space-x-1"> + <div + class="ywifont ywicon-pre" + :class="{ 'cursor-not-allowed': !offsetClickIsAllow, 'cursor-pointer': offsetClickIsAllow }" + @click="preDayClick" + ></div> + <el-date-picker + style="width: 240px" + v-model="dateValue" + type="daterange" + :start-placeholder="START_PLACEHOLDER" + :end-placeholder="END_PLACEHOLDER" + :value-format="valueFormat" + :format="DEFAULT_FORMATS_DATE" + :disabled-date="disabledDate" + :clearable="false" + :disabled="disabled" + @change="datePickerChange" + > + <template v-for="(value, name) in $slots" #[name]="slotData"> + <slot :name="name" v-bind="slotData || {}"></slot> + </template> + </el-date-picker> + <div + class="ywifont ywicon-next" + :class="{ 'cursor-not-allowed': !offsetClickIsAllow, 'cursor-pointer': offsetClickIsAllow }" + @click="nextDayClick" + ></div> + </div> + + <div class="ml-2 inline-flex items-center space-x-2 text-[14px]"> + <div + @click="quickPickRangeClick(parseInt(item))" + class="border border-solid rounded-md px-2 cursor-pointer" + :class="{ 'bg-[#1677ff]': parseInt(item) === quickPickValue, 'text-white': parseInt(item) === quickPickValue }" + v-for="item in Object.keys(timeRangeEnumMapTitle)" + :key="item" + > + {{ timeRangeEnumMapTitle[item] }} + </div> + </div> + </div> +</template> + +<script setup lang="ts"> +import { ElDatePicker } from 'element-plus'; +import { definePropType } from 'element-plus/es/utils/vue/props/runtime'; +import { ref, type PropType, computed, watch } from 'vue'; +import type { TimeRangeParam } from '../types'; +import type { TimeRangeEnum } from './types'; +import { timeRangeEnumMapTitle, timeRangeEnumMapValue } from './types'; +import { + CURRENT_DAY, + DEFAULT_FORMATS_DATE, + DEFAULT_FORMATS_TIME, + END_PLACEHOLDER, + RANGE_SEPARATOR, + START_PLACEHOLDER, +} from '/@/components/form/datepicker/constants'; +import { formatDate } from '/@/utils/formatTime'; +import moment from 'moment'; + +const valueFormat = DEFAULT_FORMATS_DATE + ' ' + DEFAULT_FORMATS_TIME; +const props = defineProps({ + data: { + type: Object as PropType<TimeRangeParam>, + }, + disabled: { + type: Boolean, + default: false, + }, +}); +const dateValue = defineModel({ + type: definePropType<[string, string]>(Array), +}); +const emit = defineEmits(['change']); + +/** + * 闇�瑕佸 dateValue 鏍煎紡鍖栵紝dataValue 缁撴潫鏃堕棿涓嶆槸23:59:59 + */ +const formatDateValue = computed({ + get: () => { + if (!dateValue.value) return null; + return [moment(dateValue.value[0]).format('YYYY-MM-DD 00:00:00'), moment(dateValue.value[1]).format('YYYY-MM-DD 23:59:59')] as [ + string, + string + ]; + }, + set: (value) => { + dateValue.value = value; + }, +}); + +const disabledDate = (date: Date) => { + return date > CURRENT_DAY; +}; + +const resetQuickPickValue = () =>{ + quickPickValue.value = null; +} +const quickPickValue = ref<TimeRangeEnum>(null); +const quickPickRangeClick = (val: TimeRangeEnum) => { + if (quickPickValue.value === val) return; + + quickPickValue.value = val; + formatDateValue.value = timeRangeEnumMapValue[val]().map((item) => formatDate(item)) as [string, string]; +}; + +const offsetClickIsAllow = computed(() => !!dateValue.value && !props.disabled); +const preDayClick = () => { + if (!dateValue.value) return; + dateValue.value[0] = moment(dateValue.value[0]).subtract(1, 'day').format('YYYY-MM-DD HH:mm:ss'); + dateValue.value = [...dateValue.value]; + resetQuickPickValue(); + +}; + +const nextDayClick = () => { + if (!dateValue.value) return; + dateValue.value[1] = moment(dateValue.value[1]).add(1, 'day').format('YYYY-MM-DD HH:mm:ss'); + dateValue.value = [...dateValue.value]; + resetQuickPickValue(); +}; + +const datePickerChange = (va) => { + resetQuickPickValue(); +}; + +watch( + () => formatDateValue.value, + (val) => { + emit('change', val); + } +); + +defineExpose({ + formatDateValue +}) +</script> +<style scoped lang="scss"> +:deep(.el-date-editor .el-range__close-icon--hidden) { + display: none; +} +</style> diff --git a/src/components/chat/chatComponents/summaryCom/components/recordSet/components/Timestamp.vue b/src/components/chat/chatComponents/summaryCom/components/recordSet/components/Timestamp.vue new file mode 100644 index 0000000..073dcba --- /dev/null +++ b/src/components/chat/chatComponents/summaryCom/components/recordSet/components/Timestamp.vue @@ -0,0 +1,59 @@ +<template> + <el-date-picker + class="timestamp" + style="width: 130px" + @change="changeValue" + v-model="selectValue" + type="date" + :placeholder="props.data.title" + value-format="YYYY-MM-DD HH:mm:ss" + format="YYYY-MM-DD" + :shortcuts="shortcuts" + :disabled-date="disabledCurrentDate" + :clearable="false" + :disabled="disabled" + /> +</template> + +<script setup lang="ts"> +import type { PropType } from 'vue'; +import type { TimestampParam } from '../types'; +import { getRecentDate } from '/@/utils/util'; +import { disabledCurrentDate } from '/@/components/form/datepicker/constants'; +const props = defineProps({ + data: { + type: Object as PropType<TimestampParam>, + }, + disabled: { + type: Boolean, + default: false, + }, +}); +const emit = defineEmits(['change']); + +const shortcuts = [ + { + text: '浠婂ぉ', + value: () => getRecentDate(0), + }, + { + text: '涓夊ぉ鍓�', + value: () => getRecentDate(3), + }, + { + text: '涓冨ぉ鍓�', + value: () => getRecentDate(7), + }, +]; +const selectValue = defineModel({ + type: String, +}); +const changeValue = (val) => { + emit('change', val); +}; +</script> +<style scoped lang="scss"> +.timestamp { + width: 180px; +} +</style> diff --git a/src/components/chat/chatComponents/summaryCom/components/recordSet/components/YRange.vue b/src/components/chat/chatComponents/summaryCom/components/recordSet/components/YRange.vue new file mode 100644 index 0000000..bbfbadc --- /dev/null +++ b/src/components/chat/chatComponents/summaryCom/components/recordSet/components/YRange.vue @@ -0,0 +1,48 @@ +<template> + <div class="flex items-center"> + <el-input-number + placeholder="鏈�灏忓��" + :style="{ + width: inputWidth, + }" + class="rounded-full" + v-model="yRange.min" + @input="(val) => numInput(val, 'min')" + :controls="false" + ></el-input-number> + <span class="bg-[#cdcdcd] h-[32px] inline-block flex-center">~</span> + <el-input-number + placeholder="鏈�澶у��" + :style="{ + width: inputWidth, + }" + class="round" + v-model="yRange.max" + :controls="false" + @input="(val) => numInput(val, 'max')" + ></el-input-number> + </div> +</template> + +<script setup lang="ts"> +import { watch, type PropType, ref } from 'vue'; + +const emit = defineEmits(['input']); + +const yRange = ref({ + min: null, + max: null, +}); + +const realYRange = ref({ + min:null, + max:null +}) +const inputWidth = '82px'; + +const numInput = (val: any, type: 'min' | 'max') => { + realYRange.value[type] = val; + emit('input', realYRange.value); +}; +</script> +<style scoped lang="scss"></style> diff --git a/src/components/chat/chatComponents/summaryCom/components/recordSet/components/constants.ts b/src/components/chat/chatComponents/summaryCom/components/recordSet/components/constants.ts new file mode 100644 index 0000000..5280b64 --- /dev/null +++ b/src/components/chat/chatComponents/summaryCom/components/recordSet/components/constants.ts @@ -0,0 +1,2 @@ +// 鏈�澶ч�夋嫨鍐呭瀹藉害 +export const MAX_SELECT_CONTENT_WIDTH = 12; \ No newline at end of file diff --git a/src/components/chat/chatComponents/summaryCom/components/recordSet/components/types.ts b/src/components/chat/chatComponents/summaryCom/components/recordSet/components/types.ts new file mode 100644 index 0000000..3945154 --- /dev/null +++ b/src/components/chat/chatComponents/summaryCom/components/recordSet/components/types.ts @@ -0,0 +1,18 @@ +import { getRecentDate, getRecentDateRange } from '/@/utils/util'; + +export const enum TimeRangeEnum { + CurrentDay, + ThreeDay, + SevenDay, +} + +export const timeRangeEnumMapTitle = { + [TimeRangeEnum.CurrentDay]: '褰撴棩', + [TimeRangeEnum.ThreeDay]: '杩戜笁鏃�', + [TimeRangeEnum.SevenDay]: '杩戜竷鏃�', +}; +export const timeRangeEnumMapValue = { + [TimeRangeEnum.CurrentDay]: () => getRecentDateRange(1), + [TimeRangeEnum.ThreeDay]: () => getRecentDateRange(3), + [TimeRangeEnum.SevenDay]: () => getRecentDateRange(7), +}; diff --git a/src/components/chat/chatComponents/summaryCom/components/recordSet/types.ts b/src/components/chat/chatComponents/summaryCom/components/recordSet/types.ts new file mode 100644 index 0000000..7bf7638 --- /dev/null +++ b/src/components/chat/chatComponents/summaryCom/components/recordSet/types.ts @@ -0,0 +1,64 @@ +import List from './components/List.vue'; +import Timestamp from './components/Timestamp.vue'; +import TimeRange from './components/TimeRange.vue'; + +export const enum RecordSetParamsType { + List = 'list', + /** @description 鍚庣鏍煎紡 */ + StartTime = 'start_time', + EndTime = 'end_time', + /** @description start 鍜� end 鍚堝苟涓轰竴涓� range */ + TimeRange = 'time_range', +} + +export type BaseParam = { + id: string; + title: string; + hide?: boolean; +}; + +export type ListParamListItem = { + title: string; + value: string; +}; +export type ListParam = { + type: RecordSetParamsType.List; + value: string; + list: ListParamListItem[]; +} & BaseParam; + +//#region ====================== 鍚庣鏁版嵁鏍煎紡 ====================== +export type TimeRangeBackEndParamType = RecordSetParamsType.StartTime | RecordSetParamsType.EndTime; +export type TimeRangeBackEndParam = { + type: TimeRangeBackEndParamType; + value: string; + // 灞炰簬鍚屼竴涓� group 閰嶅 + group: string; +} & BaseParam; +//#endregion +//#region ====================== 鏁村悎 start 鍜� end锛屽緱鍒板墠绔牸寮� ====================== +export type TimeRangeParamValue = [ + string, + string +] +export type TimeRangeParam = { + type: RecordSetParamsType.TimeRange; + value: TimeRangeParamValue; + // 灞炰簬鍚屼竴涓� group 閰嶅 + group: string; + range?: [TimeRangeBackEndParam, TimeRangeBackEndParam]; +} & BaseParam; +//#endregion + + +export type RecordSetParamsItem = ListParam | TimeRangeParam | TimeRangeBackEndParam; +export type RecordSet = { + params?: RecordSetParamsItem[]; +} & Record<string, any>; + +export const recordSetMapCom = { + [RecordSetParamsType.List]: List, + [RecordSetParamsType.TimeRange]: TimeRange, + [RecordSetParamsType.StartTime]:Timestamp, + [RecordSetParamsType.EndTime]:Timestamp +}; diff --git a/src/components/chat/chatComponents/summaryCom/components/recordSetTable/RecordSetTable.vue b/src/components/chat/chatComponents/summaryCom/components/recordSetTable/RecordSetTable.vue new file mode 100644 index 0000000..f1f59ae --- /dev/null +++ b/src/components/chat/chatComponents/summaryCom/components/recordSetTable/RecordSetTable.vue @@ -0,0 +1,136 @@ +<!-- 鏌ヨ鏈�鏂拌鍛婁俊鎭� --> +<template> + <div> + <span v-if="data?.title" class="text-base font-bold flex-center mb-5">{{ data?.title }}</span> + <div class="w-full" style="height: 70vh" ref="containerRef" v-resize="resizeHandler"> + <el-table + ref="tableRef" + border + :cell-style="{ textAlign: 'center' }" + :header-cell-style="{ textAlign: 'center' }" + :data="data?.values" + highlight-current-row + class="w-full h-full" + cellClassName="text-sm" + headerCellClassName="text-sm" + > + <template v-if="data?.cols?.length > 0"> + <el-table-column + v-for="(item, index) in colList" + :label="item.title" + :width="item.width" + :sortable="item.type === 'time'" + :key="index" + :prop="index + ''" + show-overflow-tooltip + /> + </template> + </el-table> + </div> + </div> +</template> + +<script setup lang="ts"> +import type { TableInstance } from 'element-plus'; +import _ from 'lodash'; +import { onMounted, ref, type PropType } from 'vue'; +import { debounce, getTextWidth } from '/@/utils/util'; + +const props = defineProps({ + data: { + type: Object as PropType<any>, + }, +}); + +const colList = ref([]); +const containerRef = ref<HTMLDivElement>(null); +const tableRef = ref<TableInstance>(null); +const measureWidthOffset = 28; + +const resizeEvent = ({ width, height }) => { + if (width === 0 || height === 0) { + return; + } + if (props.data?.cols?.length > 0 && props.data?.values?.length > 0) { + const maxStrList = props.data.cols.map((item) => item.title); + const maxLenList = props.data.cols.map((item) => item.title.gblen()); + for (const item of props.data.values) { + item.map((subItem, index) => { + const subItemLen = subItem?.gblen(); + if (maxLenList[index] < subItemLen) { + maxLenList[index] = subItemLen; + maxStrList[index] = subItem; + } + }); + } + // 鎬诲 + let sumWidth = 0; + let maxWidthList = maxStrList.map((item, index) => { + const width = + getTextWidth(item, { + size: '0.875rem', + }) + measureWidthOffset; + sumWidth += width; + return { + index, + maxWidth: width, + }; + }); + if (sumWidth <= width) { + maxWidthList = maxWidthList.map((item) => ({ + ...item, + width: (item.maxWidth / sumWidth) * width, + })); + // 鍏堟弧瓒冲皬鐨勶紝鍓╀綑绌洪棿鎸夋瘮渚嬪潎鍒� + } else { + let curWidth = 0; + const sortedWidthList = _.sortBy(maxWidthList, 'maxWidth'); + let restWidth = width; + let notFitStartIndex = 1; + for (let index = 0; index < sortedWidthList.length; index++) { + const item = sortedWidthList[index]; + curWidth += item.maxWidth; + if (curWidth < width || index === 0) { + maxWidthList[item.index].width = item.maxWidth; + continue; + } + restWidth = width - (curWidth - item.maxWidth); + notFitStartIndex = index; + break; + } + let sumRestMaxWidth = 0; + const notFitIndexList = sortedWidthList.slice(notFitStartIndex).map((item) => { + sumRestMaxWidth += item.maxWidth; + return item.index; + }); + + // 骞冲潎鍒嗛厤鍓╀綑绌洪棿 + for (const item of notFitIndexList) { + maxWidthList[item].width = restWidth * (maxWidthList[item].maxWidth / sumRestMaxWidth); + if (maxWidthList[item].maxWidth <= restWidth) { + maxWidthList[item].width = restWidth; + } + + // maxWidthList[item].width = undefined; + } + } + colList.value = props.data.cols.map((item, index) => ({ + ...item, + width: maxWidthList[index].width, + })); + } else { + colList.value = []; + } + tableRef.value.doLayout(); +}; + +const resizeHandler = debounce(resizeEvent); + +onMounted(() => { + resizeEvent({ + width: containerRef.value.clientWidth, + height: 0.7 * document.body.clientHeight, + }); +}); +</script> +<style scoped lang="scss"></style> diff --git a/src/components/chat/chatComponents/summaryCom/components/summary/Summary.vue b/src/components/chat/chatComponents/summaryCom/components/summary/Summary.vue new file mode 100644 index 0000000..2a32be0 --- /dev/null +++ b/src/components/chat/chatComponents/summaryCom/components/summary/Summary.vue @@ -0,0 +1,32 @@ +<!-- 鏄ㄦ棩渚涙按绠$綉姒傚喌 --> +<template> + <div class="w-full"> + <span class="text-base font-bold">{{ data.title }}</span> + <el-table ref="tableRefList" class="w-full mt-5" :data="[{}]" cellClassName="text-sm" headerCellClassName="text-sm"> + <el-table-column v-for="(col, index) in data.values" :label="col.title" :key="index"> + <template #default="scope"> + {{ col?.value }} + </template> + </el-table-column> + </el-table> + </div> +</template> + +<script setup lang="ts"> +import type { TableInstance } from 'element-plus'; +import { onMounted, ref } from 'vue'; +import { chatComProps } from '../../../common'; + +const props = defineProps(chatComProps); + +const tableRef = ref<TableInstance>(); +const doTableLayout = () => { + tableRef.value?.doLayout(); +}; +onMounted(() => { + setTimeout(() => { + doTableLayout(); + }, 300); +}); +</script> +<style scoped lang="scss"></style> diff --git a/src/components/chat/chatComponents/summaryCom/components/types.ts b/src/components/chat/chatComponents/summaryCom/components/types.ts new file mode 100644 index 0000000..0113c5f --- /dev/null +++ b/src/components/chat/chatComponents/summaryCom/components/types.ts @@ -0,0 +1,31 @@ +import HTMLCom from '../../htmlCom/HTMLCom.vue'; +import MapCom from '../../mapCom/MapCom.vue'; +import RecordSet from './recordSet/RecordSet.vue'; +import RecordSetTable from './recordSetTable/RecordSetTable.vue'; +import AmisPage from './amisPage/AmisPage.vue'; + + +import Summary from './summary/Summary.vue'; +import DeviceLastValueCom from './deviceLastValue/DeviceLastValueCom.vue'; + +export const enum SummaryAnswerType { + RecordSet = 'recordset', + Summary = 'summary', + Url = 'url', + Map = 'map', + DeviceLastValue='device_last_value', + /** @description 鍚庣骞舵病鏈夊鍔犱竴涓柊鐨� table 绫诲瀷锛岃�屾槸褰撴垚 recordset 鐨勪竴绉嶇壒鍒� */ + RecordSetTable = 'recordsetTable', + AmisPage="amis_page" +} + +export const summaryAnswerTypeMapCom = { + [SummaryAnswerType.RecordSet]: RecordSet, + [SummaryAnswerType.Summary]: Summary, + [SummaryAnswerType.Url]: HTMLCom, + [SummaryAnswerType.Map]: MapCom, + [SummaryAnswerType.DeviceLastValue]:DeviceLastValueCom, + [SummaryAnswerType.RecordSetTable]:RecordSetTable, + [SummaryAnswerType.AmisPage]:AmisPage + +}; diff --git a/src/components/chat/chatComponents/types.ts b/src/components/chat/chatComponents/types.ts new file mode 100644 index 0000000..a98a373 --- /dev/null +++ b/src/components/chat/chatComponents/types.ts @@ -0,0 +1,18 @@ + +export const enum ChartTypeEnum { + Scatter, + Line, + Bar, +} + +export const chartTypeMapName = { + [ChartTypeEnum.Line]: '鎶樼嚎鍥�', + [ChartTypeEnum.Scatter]: '鏁g偣鍥�', + [ChartTypeEnum.Bar]: '鏌辩姸鍥�', +}; + +export const chartTypeMapEchart = { + [ChartTypeEnum.Scatter]: 'scatter', + [ChartTypeEnum.Line]: 'line', + [ChartTypeEnum.Bar]: 'bar', +}; diff --git a/src/components/chat/components/Loading/Loading.vue b/src/components/chat/components/Loading/Loading.vue new file mode 100644 index 0000000..80d4286 --- /dev/null +++ b/src/components/chat/components/Loading/Loading.vue @@ -0,0 +1,114 @@ +<template> + <div class="com__box flex-center"> + <div class="loading"> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + </div> + </div> + </template> + + <style scoped> + .loading, + .loading > div { + position: relative; + box-sizing: border-box; + } + + .loading { + display: block; + font-size: 0; + color: #000; + } + + .loading.la-dark { + color: #333; + } + + .loading > div { + display: inline-block; + float: none; + background-color: currentColor; + border: 0 solid currentColor; + } + + .loading { + width: 17px; + height: 17px; + } + + .loading > div { + width: 3px; + height: 3px; + margin: 1px; + border-radius: 100%; + animation-name: ball-grid-beat; + animation-iteration-count: infinite; + } + + .loading > div:nth-child(1) { + animation-duration: 0.65s; + animation-delay: 0.03s; + } + + .loading > div:nth-child(2) { + animation-duration: 1.02s; + animation-delay: 0.09s; + } + + .loading > div:nth-child(3) { + animation-duration: 1.06s; + animation-delay: -0.69s; + } + + .loading > div:nth-child(4) { + animation-duration: 1.5s; + animation-delay: -0.41s; + } + + .loading > div:nth-child(5) { + animation-duration: 1.6s; + animation-delay: 0.04s; + } + + .loading > div:nth-child(6) { + animation-duration: 0.84s; + animation-delay: 0.07s; + } + + .loading > div:nth-child(7) { + animation-duration: 0.68s; + animation-delay: -0.66s; + } + + .loading > div:nth-child(8) { + animation-duration: 0.93s; + animation-delay: -0.76s; + } + + .loading > div:nth-child(9) { + animation-duration: 1.24s; + animation-delay: -0.76s; + } + + @keyframes ball-grid-beat { + 0% { + opacity: 1; + } + + 50% { + opacity: 0.35; + } + + 100% { + opacity: 1; + } + } + </style> + \ No newline at end of file diff --git a/src/components/chat/components/model/Record.ts b/src/components/chat/components/model/Record.ts new file mode 100644 index 0000000..818c5a4 --- /dev/null +++ b/src/components/chat/components/model/Record.ts @@ -0,0 +1,68 @@ +import type { RecordSetValues } from '/@/api/ai/chat'; + +export class RecordSet { + names: string[]; + values: Array<any[]>; + title: string; + constructor(data: RecordSetValues) { + this.names = data.names ?? []; + this.values = data.values ?? []; + this.title = data.title; + } + + generateHTML() { + const thList = this.names.map((item) => { + return ` <th class="text-left py-3 px-4 uppercase font-semibold text-sm">${item}</th>`; + }); + const trList = this.values.map((item) => { + const tdList = ((item??[])).map((subItem) => ` <td class="text-left py-3 px-4">${subItem}</td>`); + return ` <tr> + ${tdList} + </tr>`; + }); + return ` + <div class="md:px-32 py-8 w-full"> + <div class="shadow overflow-hidden rounded border-b border-gray-200"> + <table class="min-w-full bg-white"> + <thead class="bg-gray-800 text-white"> + <tr> + ${thList} + </tr> + </thead> + <tbody class="text-gray-700"> + ${trList} + </tbody> + </table> + </div> + </div>`; + } +} + + + +const record = new RecordSet({ + "json_ok": true, + "question": "鏄ㄦ棩浜斾竴骞垮満鍘嬪姏", + "answer_type": "recordset", + "values": { + "names": [ + "yesterday", + "max_pressure" + ], + "values": [ + [ + "2024-06-28 00:00:00", + 24.378 + ], + [ + "2024-06-29 00:00:00", + 24.276 + ] + ], + "type": "records", + "title": "鏄ㄦ棩浜斾竴骞垮満(D_GW_04)鐨勬渶澶у帇鍔涘��" + } + }.values) + + const html = record.generateHTML(); + diff --git a/src/components/chat/components/model/types.ts b/src/components/chat/components/model/types.ts new file mode 100644 index 0000000..02a53ce --- /dev/null +++ b/src/components/chat/components/model/types.ts @@ -0,0 +1,61 @@ +import RecordSetCom from '../chatComponents/recordSetCom/RecordSetCom.vue'; +import NormalTextCom from '../chatComponents/normalTextCom/NormalTextCom.vue'; +import knowledgeCom from '../chatComponents/knowledgeCom/KnowledgeCom.vue'; +import SummaryCom from '../chatComponents/summaryCom/SummaryCom.vue'; + +import assistantPic from '/static/images/role/assistant-200x192.png'; +import userPic from '/static/images/role/user-200x206.png'; +export const enum AnswerType { + Knowledge = 'knowledge', + RecordSet = 'recordset', + Text = 'text', + Summary = 'summary', + Url = 'url', + Map = 'map', +} + +export const answerTypeMapCom = { + [AnswerType.Knowledge]: knowledgeCom, + [AnswerType.RecordSet]: RecordSetCom, + [AnswerType.Text]: NormalTextCom, + [AnswerType.Summary]: SummaryCom, +}; + +export const enum RoleEnum { + user = 'user', + assistant = 'assistant', +} +export const AnswerState = { + Null: null, + Like: '1', + Unlike: '0', +}; + +export type AnswerStateType = typeof AnswerState; +export type ContextHistory = { + /** @description 鏁板瓧瀛楃涓� */ + ratio: string; + history_id: string; + question: string; +}; + +export type ChatContent = { + type: AnswerType; + values: any; + askMoreList?: ContextHistory[]; + errCode?: string; + errMsg?: string; + origin?: any; +}; + +export interface ChatMessage { + historyId: string; + role: RoleEnum; + content?: ChatContent; + state?: null | '1' | '0'; +} + +export const roleImageMap = { + [RoleEnum.user]: userPic, + [RoleEnum.assistant]: assistantPic, +}; diff --git a/src/components/chat/hooks/useAssistantContentOpt.ts b/src/components/chat/hooks/useAssistantContentOpt.ts new file mode 100644 index 0000000..3617953 --- /dev/null +++ b/src/components/chat/hooks/useAssistantContentOpt.ts @@ -0,0 +1,144 @@ +import { ElMessage } from 'element-plus'; +import type { ComputedRef, Ref } from 'vue'; +import { computed, nextTick, ref } from 'vue'; +import useClipboard from 'vue-clipboard3'; +import type { ChatMessage } from '../model/types'; +import { AnswerState, AnswerType, RoleEnum } from '../model/types'; +import { SetHistoryAnswerState } from '/@/api/ai/chat'; +import { onClickOutside } from '@vueuse/core'; + +export type AssistantContentOptOption = { + forbidScroll: Ref<boolean>; + sendChatMessage: any; + displayMessageList: ComputedRef<ChatMessage[]>; +}; + +export const useAssistantContentOpt = (option: AssistantContentOptOption) => { + const { forbidScroll, sendChatMessage, displayMessageList } = option; + const { toClipboard } = useClipboard(); + const preQuestion = ref(null); + + const copyClick = (item) => { + const type = item.content.type; + let text = ''; + if (type === AnswerType.Knowledge) { + text = item.content.values?.map((item) => item.answer).join('\n\n') ?? ''; + } else { + text = item.content.values; + } + ElMessage.success('澶嶅埗鎴愬姛'); + toClipboard(text); + }; + + const likeClick = async (item) => { + const toSetState = item.state === AnswerState.Like ? AnswerState.Null : AnswerState.Like; + const res = await SetHistoryAnswerState({ + history_id: item.historyId, + answer_state: toSetState, + }); + item.state = toSetState; + forbidScroll.value = true; + nextTick(() => { + forbidScroll.value = false; + }); + }; + + const unLikeClick = async (item) => { + const toSetState = item.state === AnswerState.Unlike ? AnswerState.Null : AnswerState.Unlike; + const res = await SetHistoryAnswerState({ + history_id: item.historyId, + answer_state: toSetState, + }); + item.state = toSetState; + + forbidScroll.value = true; + nextTick(() => { + forbidScroll.value = false; + }); + }; + const feedbackPosition = ref({ + x: 0, + y: 0, + }); + + const feedbackIsShow = ref(false); + const feedbackContent = ref(''); + const feedbackPanelRef = ref<HTMLDivElement>(null); + const currentFeedbackMapItem = ref(null); + const curFeedbackIndex = ref(0); + const feedbackClick = async (e, item, index) => { + currentFeedbackMapItem.value = item; + curFeedbackIndex.value = index; + const offsetX = -4; + const offsetY = -8; + feedbackIsShow.value = true; + nextTick(() => { + feedbackPosition.value = { + x: -feedbackPanelRef.value[index].$el.clientWidth + offsetX, + y: -feedbackPanelRef.value[index].$el.clientHeight + offsetY, + }; + }); + }; + + onClickOutside( + computed(() => feedbackPanelRef.value?.[curFeedbackIndex.value]), + (e) => { + feedbackIsShow.value = false; + feedbackContent.value = ''; + } + ); + // useClickOther( + // computed(() => feedbackPanelRef.value?.[curFeedbackIndex.value]), + // feedbackIsShow, + // () => { + // feedbackIsShow.value = false; + // feedbackContent.value = ''; + // } + // ); + const showAskMore = computed(() => { + if (!displayMessageList.value || displayMessageList.value.length === 0) return false; + const last = displayMessageList.value.at(-1); + const isShow = last?.role === RoleEnum.assistant && last?.content?.values && last.content?.askMoreList?.length > 0; + return isShow; + }); + + const showFixQuestion = (item) => { + const isShow = item?.role === RoleEnum.assistant && item?.content?.values && item.content?.origin?.err_json?.fix_question; + return isShow; + }; + const askMoreClick = (item) => { + if (!item.question) return; + sendChatMessage({ type: AnswerType.Text, values: item.question }); + }; + + const fixQuestionClick = (item, originData) => { + if (!item.question) return; + preQuestion.value = originData?.question; + try { + sendChatMessage({ + type: AnswerType.Text, + values: item.question, + }); + } finally { + preQuestion.value = null; + } + }; + + return { + copyClick, + likeClick, + unLikeClick, + feedbackPosition, + feedbackIsShow, + feedbackContent, + feedbackPanelRef, + currentFeedbackMapItem, + curFeedbackIndex, + feedbackClick, + askMoreClick, + fixQuestionClick, + preQuestion, + showAskMore, + showFixQuestion, + }; +}; diff --git a/src/components/chat/hooks/useQueryProcess.ts b/src/components/chat/hooks/useQueryProcess.ts new file mode 100644 index 0000000..00d970d --- /dev/null +++ b/src/components/chat/hooks/useQueryProcess.ts @@ -0,0 +1,41 @@ +import { ref } from 'vue'; +import { getQuestionProcess } from '/@/api/ai/chat'; + +export const useQueryProcess = () => { + const processId = ref(''); + const QUERY_PROCESS_INTERVAL = 1000; + const process = ref(''); + let processTimer = null; + let finishProcess = true; + + const queryProcessApi = async () => { + const res = await getQuestionProcess({ + process_id: processId.value, + }).catch((err) => { + process.value = err; + }); + + process.value = res.process; + finishProcess = true; + }; + + const queryProcess = () => { + processTimer = setInterval(() => { + if (!finishProcess) return; + finishProcess = false; + queryProcessApi(); + }, QUERY_PROCESS_INTERVAL); + }; + + const clearQueryProcess = () => { + process.value = ''; + clearInterval(processTimer); + }; + + return { + processId, + process, + queryProcess, + clearQueryProcess + }; +}; diff --git a/src/components/chat/hooks/useScrollToBottom.ts b/src/components/chat/hooks/useScrollToBottom.ts new file mode 100644 index 0000000..b23e8a4 --- /dev/null +++ b/src/components/chat/hooks/useScrollToBottom.ts @@ -0,0 +1,37 @@ +import type { ComputedRef, Ref } from 'vue'; +import { nextTick, onActivated, ref, watch } from 'vue'; +import type { ChatMessage } from '../model/types'; + +export type UseScrollToBottomOption = { + chatListDom: Ref<HTMLDivElement>; + displayMessageList: ComputedRef<ChatMessage[]>; +}; + +export const useScrollToBottom = (option:UseScrollToBottomOption) => { + const {chatListDom,displayMessageList} = option; + + const scrollToBottom = () => { + if (!chatListDom.value) return; + chatListDom.value.lastElementChild?.scrollIntoView(); + }; + const forbidScroll = ref(false); + watch( + displayMessageList, + () => { + if (forbidScroll.value) return; + nextTick(() => scrollToBottom()); + }, + { + deep: true, + } + ); + + onActivated(() => { + if (forbidScroll.value) return; + nextTick(() => scrollToBottom()); + }); + + return { + forbidScroll + }; +}; diff --git a/src/components/chat/libs/gpt.ts b/src/components/chat/libs/gpt.ts new file mode 100644 index 0000000..2a90398 --- /dev/null +++ b/src/components/chat/libs/gpt.ts @@ -0,0 +1,21 @@ +import type { ChatMessage } from '../types'; + +export async function chat(messageList: ChatMessage[], apiKey: string) { + const result = await fetch('https://api.openai.com/v1/chat/completions', { + method: 'post', + // signal: AbortSignal.timeout(8000), + // 寮�鍚悗鍒拌揪璁惧畾鏃堕棿浼氫腑鏂祦寮忚緭鍑� + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + model: 'gpt-3.5-turbo', + stream: true, + messages: messageList, + }), + }).catch(error=>{ + throw error + }); + return result; +} diff --git a/src/components/chat/libs/markdown.ts b/src/components/chat/libs/markdown.ts new file mode 100644 index 0000000..0a002fe --- /dev/null +++ b/src/components/chat/libs/markdown.ts @@ -0,0 +1,21 @@ +import highlight from 'highlight.js'; +import Markdown from 'markdown-it'; + +const mdOptions: Markdown.Options = { + linkify: true, + typographer: true, + breaks: true, + langPrefix: 'language-', + // 浠g爜楂樹寒 + highlight(str, lang) { + if (lang && highlight.getLanguage(lang)) { + try { + return '<pre class="hljs"><code>' + highlight.highlight(lang, str, true).value + '</code></pre>'; + } catch (__) { + } + } + return ''; + }, +}; + +export const md = new Markdown(mdOptions); diff --git a/src/components/chat/model/Record.ts b/src/components/chat/model/Record.ts new file mode 100644 index 0000000..818c5a4 --- /dev/null +++ b/src/components/chat/model/Record.ts @@ -0,0 +1,68 @@ +import type { RecordSetValues } from '/@/api/ai/chat'; + +export class RecordSet { + names: string[]; + values: Array<any[]>; + title: string; + constructor(data: RecordSetValues) { + this.names = data.names ?? []; + this.values = data.values ?? []; + this.title = data.title; + } + + generateHTML() { + const thList = this.names.map((item) => { + return ` <th class="text-left py-3 px-4 uppercase font-semibold text-sm">${item}</th>`; + }); + const trList = this.values.map((item) => { + const tdList = ((item??[])).map((subItem) => ` <td class="text-left py-3 px-4">${subItem}</td>`); + return ` <tr> + ${tdList} + </tr>`; + }); + return ` + <div class="md:px-32 py-8 w-full"> + <div class="shadow overflow-hidden rounded border-b border-gray-200"> + <table class="min-w-full bg-white"> + <thead class="bg-gray-800 text-white"> + <tr> + ${thList} + </tr> + </thead> + <tbody class="text-gray-700"> + ${trList} + </tbody> + </table> + </div> + </div>`; + } +} + + + +const record = new RecordSet({ + "json_ok": true, + "question": "鏄ㄦ棩浜斾竴骞垮満鍘嬪姏", + "answer_type": "recordset", + "values": { + "names": [ + "yesterday", + "max_pressure" + ], + "values": [ + [ + "2024-06-28 00:00:00", + 24.378 + ], + [ + "2024-06-29 00:00:00", + 24.276 + ] + ], + "type": "records", + "title": "鏄ㄦ棩浜斾竴骞垮満(D_GW_04)鐨勬渶澶у帇鍔涘��" + } + }.values) + + const html = record.generateHTML(); + diff --git a/src/components/chat/model/types.ts b/src/components/chat/model/types.ts new file mode 100644 index 0000000..02a53ce --- /dev/null +++ b/src/components/chat/model/types.ts @@ -0,0 +1,61 @@ +import RecordSetCom from '../chatComponents/recordSetCom/RecordSetCom.vue'; +import NormalTextCom from '../chatComponents/normalTextCom/NormalTextCom.vue'; +import knowledgeCom from '../chatComponents/knowledgeCom/KnowledgeCom.vue'; +import SummaryCom from '../chatComponents/summaryCom/SummaryCom.vue'; + +import assistantPic from '/static/images/role/assistant-200x192.png'; +import userPic from '/static/images/role/user-200x206.png'; +export const enum AnswerType { + Knowledge = 'knowledge', + RecordSet = 'recordset', + Text = 'text', + Summary = 'summary', + Url = 'url', + Map = 'map', +} + +export const answerTypeMapCom = { + [AnswerType.Knowledge]: knowledgeCom, + [AnswerType.RecordSet]: RecordSetCom, + [AnswerType.Text]: NormalTextCom, + [AnswerType.Summary]: SummaryCom, +}; + +export const enum RoleEnum { + user = 'user', + assistant = 'assistant', +} +export const AnswerState = { + Null: null, + Like: '1', + Unlike: '0', +}; + +export type AnswerStateType = typeof AnswerState; +export type ContextHistory = { + /** @description 鏁板瓧瀛楃涓� */ + ratio: string; + history_id: string; + question: string; +}; + +export type ChatContent = { + type: AnswerType; + values: any; + askMoreList?: ContextHistory[]; + errCode?: string; + errMsg?: string; + origin?: any; +}; + +export interface ChatMessage { + historyId: string; + role: RoleEnum; + content?: ChatContent; + state?: null | '1' | '0'; +} + +export const roleImageMap = { + [RoleEnum.user]: userPic, + [RoleEnum.assistant]: assistantPic, +}; diff --git a/src/components/form/datepicker/constants.ts b/src/components/form/datepicker/constants.ts index 5f5d2af..24d77ce 100644 --- a/src/components/form/datepicker/constants.ts +++ b/src/components/form/datepicker/constants.ts @@ -35,3 +35,6 @@ value: () => getRecentDate(2), }, ]; +export const disabledCurrentDate = (date: Date) => { + return date > CURRENT_DAY; +}; \ No newline at end of file diff --git a/src/utils/util.ts b/src/utils/util.ts index 5e60f34..72d1e41 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -708,3 +708,29 @@ if (num == null) return ''; return num.toFixed(precision).replace(/\.?0+$/, ''); }; + + +type GetTextWidthOption = { + size?: string; + family?: string; +}; + +export function getTextWidth(text: string, option: GetTextWidthOption) { + if (!text) return 0; + const { size = '14px', family = 'Microsoft YaHei' } = option; + const spanEle = document.createElement('span'); + document.body.appendChild(spanEle); + + spanEle.style.font = 'times new roman'; + spanEle.style.fontSize = size; + spanEle.style.height = 'auto'; + spanEle.style.width = 'auto'; + spanEle.style.position = 'absolute'; + spanEle.style.whiteSpace = 'no-wrap'; + spanEle.innerHTML = text; + + const width = spanEle.clientWidth; + + document.body.removeChild(spanEle); + return width; +} diff --git a/src/views/project/yw/lowCode/sqlAmis/SqlAmis.vue b/src/views/project/yw/lowCode/sqlAmis/SqlAmis.vue index fe812ca..40b150f 100644 --- a/src/views/project/yw/lowCode/sqlAmis/SqlAmis.vue +++ b/src/views/project/yw/lowCode/sqlAmis/SqlAmis.vue @@ -35,6 +35,7 @@ </div> </template> </el-table-column> --> + <el-table-column prop="id" label="id" width="130" fixed="left" show-overflow-tooltip> </el-table-column> <el-table-column prop="title" label="鏍囬" width="300" fixed="left" show-overflow-tooltip> </el-table-column> <el-table-column prop="prompt" label="鎻愮ず璇�" show-overflow-tooltip> </el-table-column> -- Gitblit v1.9.3