From 7fbce1ecd95b4e12ceda0a5b874ec8f3951625f7 Mon Sep 17 00:00:00 2001 From: wujingjing <gersonwu@qq.com> Date: 星期五, 17 一月 2025 17:58:59 +0800 Subject: [PATCH] WI水务智能助理 --- src/api/ai/chat.ts | 24 + src/components/chat/chatComponents/summaryCom/components/recordSetTable/map/Map.vue | 17 + vite.config.ts | 2 src/model/map/OLMap.ts | 18 + src/components/chat/smallChat/ChatInput.vue | 84 +++++++ src/components/chat/components/playBar/PlayBar.vue | 42 --- src/components/chat/smallChat/types.ts | 15 + src/components/chat/smallChat/index.vue | 254 +++++++++++++++++++++++ src/components/chat/smallChat/chatInput.scss | 154 ++++++++++++++ src/components/chat/chatComponents/summaryCom/components/recordSetTable/map/BasicMap.vue | 2 10 files changed, 563 insertions(+), 49 deletions(-) diff --git a/src/api/ai/chat.ts b/src/api/ai/chat.ts index 20bcaa5..df775d7 100644 --- a/src/api/ai/chat.ts +++ b/src/api/ai/chat.ts @@ -273,7 +273,24 @@ }, callback ); - +/** + * @description 娴佸紡澶фā鍨嬪璇� + * @param {FormData} params + **/ +export const agentStreamByPost = (params, callback: (chunkRes) => void, extraData: any = {}) => + streamReq( + { + url: `/chat/agent_stream`, + method: 'post', + data: params, + params: {}, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + ...extraData, + }, + callback + ); /** * @summary AI澶фā鍨嬪璇� */ @@ -503,8 +520,7 @@ }, }); - - export const question_stream_reply = (params) => +export const question_stream_reply = (params) => request({ url: `/chat/question_stream_reply`, method: 'post', @@ -512,4 +528,4 @@ headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, - }); \ No newline at end of file + }); diff --git a/src/components/chat/chatComponents/summaryCom/components/recordSetTable/map/BasicMap.vue b/src/components/chat/chatComponents/summaryCom/components/recordSetTable/map/BasicMap.vue index 912640b..75dd1d4 100644 --- a/src/components/chat/chatComponents/summaryCom/components/recordSetTable/map/BasicMap.vue +++ b/src/components/chat/chatComponents/summaryCom/components/recordSetTable/map/BasicMap.vue @@ -149,6 +149,8 @@ onMounted(() => { initMap(); + // window.olMap = olMap.value; + // window.map = olMap.value.map; }); </script> <style scoped lang="scss"></style> diff --git a/src/components/chat/chatComponents/summaryCom/components/recordSetTable/map/Map.vue b/src/components/chat/chatComponents/summaryCom/components/recordSetTable/map/Map.vue index 66c202e..6ba59b7 100644 --- a/src/components/chat/chatComponents/summaryCom/components/recordSetTable/map/Map.vue +++ b/src/components/chat/chatComponents/summaryCom/components/recordSetTable/map/Map.vue @@ -20,7 +20,14 @@ <Teleport to=".layout-parent"> <Transition name="fullscreen"> <div v-if="isRenderFullscreen" v-show="isFullscreen" class="absolute inset-0 z-50 w-full h-full"> - <BasicMap ref="fullScreenMapRef" :config="fullScreenMapConfig" class="h-full" :data="data" @markerClick="markerClick" @closeInfoWindow="closeInfoWindow" /> + <BasicMap + ref="fullScreenMapRef" + :config="fullScreenMapConfig" + class="h-full" + :data="data" + @markerClick="markerClick" + @closeInfoWindow="closeInfoWindow" + /> <div class="absolute right-2 top-2 cursor-pointer" @click="toggleFullScreen"> <el-tooltip content="閫�鍑哄叏灞忥紙Esc锛�" placement="top"> <div class="ywifont !text-[20px] text-black rounded-lg ywicon-tuichuquanping"></div> @@ -36,6 +43,7 @@ :tableHeight="240" /> </div> + <SmallChat class="absolute bottom-0 right-0" :olMap="fullScreenOlMap" /> </div> </Transition> </Teleport> @@ -44,17 +52,19 @@ <script setup lang="ts"> import 'ol/ol.css'; -import { nextTick, onDeactivated, onMounted, onUnmounted, ref } from 'vue'; +import { computed, nextTick, onDeactivated, onMounted, onUnmounted, ref } from 'vue'; import EquipCurve from '../components/EquipCurve.vue'; import BasicMap from './BasicMap.vue'; -import { useCompRef } from '/@/utils/types'; +import SmallChat from '/@/components/chat/smallChat/index.vue'; import { GaoDeSourceType } from '/@/model/map/OLMap'; +import { useCompRef } from '/@/utils/types'; const isRenderFullscreen = ref(false); const isFullscreen = ref(false); const props = defineProps(['data']); const normalMapRef = useCompRef(BasicMap); const fullScreenMapRef = useCompRef(BasicMap); +const fullScreenOlMap = computed(() => fullScreenMapRef.value?.olMap); const emit = defineEmits(['equipClick', 'closeInfoWindow']); const markerClick = (row) => { @@ -71,7 +81,6 @@ emit('closeInfoWindow'); } }; - const fullScreenMapConfig = ref({ sourceType: GaoDeSourceType.Vector, diff --git a/src/components/chat/components/playBar/PlayBar.vue b/src/components/chat/components/playBar/PlayBar.vue index fbcd44a..aba898c 100644 --- a/src/components/chat/components/playBar/PlayBar.vue +++ b/src/components/chat/components/playBar/PlayBar.vue @@ -31,10 +31,11 @@ placeholder="鍦ㄨ繖閲岃緭鍏ユ偍鐨勯棶棰樺紑濮嬪拰AI瀵硅瘽" clearable > + </el-input> <InputTip ref="inputTipRef" :inputValue="inputValue" @updateInputValue="updateInputValue" :isHome="isHome" /> </div> - <div class="h100 flex items-center"> + <div class="h100 flex items-end"> <div class="upload_img space-y"> <div class="imgbox cursor-pointer flex items-center"> <el-button @@ -111,46 +112,7 @@ type: String, }); const inputRef = ref<InputInstance>(null); -// useSyncMsg({ -// msgList: props.msgList, -// updateLoadIndex: () => {}, -// }); -const test = () => { - // useSyncMsg({ - // msgList: props.msgList, - // updateLoadIndex: () => {}, - // }); - return; - const recentIds = [ - { id: 'a1b2c3d4', time: '2024-03-27 15:42:33' }, - { id: 'e5f6g7h8', time: '2024-02-15 09:23:45' }, - { id: 'i9j0k1l2', time: '2024-05-08 14:37:21' }, - { id: 'm3n4o5p6', time: '2024-01-30 11:55:16' }, - { id: 'q7r8s9t0', time: '2024-07-12 16:48:59' }, - { id: 'u1v2w3x4', time: '2024-04-03 10:15:27' }, - { id: 'y5z6a7b8', time: '2024-06-21 13:29:44' }, - { id: 'c9d0e1f2', time: '2024-08-09 17:52:38' }, - { id: 'g3h4i5j6', time: '2024-09-14 12:33:51' }, - { id: 'k7l8m9n0', time: '2024-10-25 08:19:07' }, - ]; - // const userHistoryIds = reverse(msgList.value.filter((item) => item.role === RoleEnum.user).map((item) => item.historyId)); - const currentHistoryIds = [ - { id: 'a1b2c3d4', time: '2024-03-27 15:42:33' }, - { id: 'e5f6g7h8', time: '2024-02-15 09:23:45' }, - // {id: 'i9j0k1l2', time: '2024-05-08 14:37:21'}, - // {id: 'm3n4o5p6', time: '2024-01-30 11:55:16'}, - { id: 'q7r8s9t0', time: '2024-07-12 16:48:59' }, - // {id: 'u1v2w3x4', time: '2024-04-03 10:15:27'}, - { id: 'y5z6a7b8', time: '2024-06-21 13:29:44' }, - { id: 'c9d0e1f2', time: '2024-08-09 17:52:38' }, - { id: 'g3h4i5j6', time: '2024-09-14 12:33:51' }, - // {id: 'k7l8m9n0', time: '2024-10-25 08:19:07'}, - ]; - const unSyncedHistoryIds = differenceBy(recentIds, currentHistoryIds, 'id'); - const unSyncedUserMsgs = []; - // const unSyncedMsgs =await loadReplyData(unSyncedUserMsgs); -}; const updateInputValue = (val) => { inputValue.value = val; }; diff --git a/src/components/chat/smallChat/ChatInput.vue b/src/components/chat/smallChat/ChatInput.vue new file mode 100644 index 0000000..7e78a9f --- /dev/null +++ b/src/components/chat/smallChat/ChatInput.vue @@ -0,0 +1,84 @@ +<template> + <div class="playInput !w-full hl_input rounded-[22px] input-border input-shadow"> + <div class="set-input"> + <!-- @input="inputText" --> + <el-input + ref="inputRef" + class="question-input relative align-bottom set-inputAnswer" + type="textarea" + resize="none" + maxlength="1024" + :autosize="{ minRows: 1, maxRows: 3 }" + v-elInputFocus + show-word-limit + v-model="inputText" + placeholder="鍦ㄨ繖閲岃緭鍏ユ偍鐨勯棶棰樺紑濮嬪拰AI瀵硅瘽" + clearable + @keyup.enter="emits('sendClick')" + > + </el-input> + </div> + <div class="h100 flex items-end"> + <div class="upload_img space-y"> + <div class="imgbox cursor-pointer flex items-center"> + <el-button + title="娓呴櫎" + class="cursor-pointer !text-gray-500" + link + style="margin-left: unset; margin-right: 0px" + @click="clearTextarea" + icon="ele-Close" + v-if="inputText" + > + </el-button> + <el-tooltip placement="top" content="鍋滄鐢熸垚" v-if="isTalking"> + <div class="cursor-pointer !ml-0 size-[38px] bg-[#1d86ff] relative !z-10 rounded-full flex-center" link> + <div + class="size-[38px] bg-black text-white stop-breathe box-content rounded-full flex-center" + @click="emits('stopGenClick')" + > + <span class="ywifont ywicon-jieshu"></span> + </div> + </div> + </el-tooltip> + <el-tooltip v-else placement="top" content="鍙戦��"> + <div class="size-[38px] rounded-full bg-black flex-center" @click="emits('sendClick')"> + <img src="/static/images/wave/QueryImg.png" /> + </div> + </el-tooltip> + </div> + </div> + </div> + </div> +</template> + +<script setup lang="ts" name="ChatInput"> +import { onMounted } from 'vue'; +import { Logger } from '/@/model/logger/Logger'; +import { agentStreamByPost } from '/@/api/ai/chat'; + +const inputText = defineModel('modelValue', { + type: String, + default: '', +}); +const props = defineProps({ + isTalking: { + type: Boolean, + default: false, + }, +}); +const emits = defineEmits(['sendClick', 'stopGenClick']); + +const clearTextarea = () => { + inputText.value = ''; +}; + + +</script> +<style scoped lang="scss"> +@use './chatInput.scss'; +.playInput { + --y-padding: 7px; + --x-padding: 14px; +} +</style> diff --git a/src/components/chat/smallChat/chatInput.scss b/src/components/chat/smallChat/chatInput.scss new file mode 100644 index 0000000..71911cc --- /dev/null +++ b/src/components/chat/smallChat/chatInput.scss @@ -0,0 +1,154 @@ +.set-waterTitle { + line-height: 24px; + font-weight: 500; + font-size: 18px; + color: #3b4066; + vertical-align: middle; +} +strong { + font-size: 26px; + font-weight: 700; + margin-right: 12px; +} +.layout-logo-medium-img { + width: 28px; + margin-right: 7px; +} +.pc-roleList { + margin: 40px 0 26px; + position: relative; +} +.modelItem { + height: 34px; + padding: 0 16px; + border-radius: 17px; + border: 1px solid #00000020; + background-color: #f2f4f8; + transition: background-color 0.1s, border-color 0.1s, color 0.1s; + color: #333; + .set-icon { + width: 20px; + height: 20px; + position: relative; + } + .set-icon-more { + width: 16px; + height: 16px; + position: relative; + } + span { + margin-left: 8px; + font-weight: 500; + font-size: 15px; + } +} +.modelItemActive { + background-color: #1c86ff; + border-color: #1c86ff; + color: #fff; +} + +.input-shadow { + -webkit-box-shadow: 0 0 0 1px transparent, 0 3px 16px 0 #dee0f3; + box-shadow: 0 0 0 1px transparent, 0 3px 16px 0 #dee0f3; +} + +.input-border { + border: 1px solid #00000030; + + -webkit-transition: border-color 0.1s ease-in-out, -webkit-box-shadow 0.1s ease-in-out; + transition: border-color 0.1s ease-in-out, -webkit-box-shadow 0.1s ease-in-out; + -o-transition: border-color 0.1s ease-in-out, box-shadow 0.1s ease-in-out; + transition: border-color 0.1s ease-in-out, box-shadow 0.1s ease-in-out; + transition: border-color 0.1s ease-in-out, box-shadow 0.1s ease-in-out, -webkit-box-shadow 0.1s ease-in-out; +} +.playInput { + + --y-padding: 9px; + --x-padding: 7px; + align-items: flex-start; + width: 760px; + position: relative; + + padding: var(--y-padding) var(--x-padding) var(--y-padding) var(--x-padding); + + display: flex; + -webkit-box-align: end; + -ms-flex-align: end; + align-items: flex-end; + -webkit-box-sizing: border-box; + box-sizing: border-box; + + background-color: #fff; + .assembly { + position: relative; + align-self: flex-end; + margin-right: 12px; + .label { + height: 38px; + } + } + .set-input { + position: relative; + vertical-align: bottom; + font-size: 14px; + display: inline-block; + width: 100%; + .set-inputAnswer { + padding: 3px 0; + line-height: 20px; + border: none; + background-color: transparent; + color: #333; + font-size: 15px; + :deep(.el-textarea__inner) { + // 鍘婚櫎绾� + box-shadow: none; + padding:5px 2px + } + } + :deep(.el-input__wrapper) { + box-shadow: unset; + } + } + .upload_img { + .imgbox { + height: 38px; + .set-img-icon { + width: 38px; + height: 38px; + border-radius: 5px; + transition: background-color 0.1s ease-in-out; + } + .send { + width: 36px; + height: 36px; + border-radius: 50%; + background-color: #2c1e1d; + img { + margin: 4px 0 0 -2px; + } + } + } + } +} + +.stop-breathe { + @keyframes breathe { + 0%, + 100% { + transform: scale(0.6); + } + 50% { + transform: scale(1); + } + } + + animation: breathe 1.8s infinite ease-in-out; +} + +.question-input { + :deep(.el-input__count) { + @apply text-gray-400; + } +} diff --git a/src/components/chat/smallChat/index.vue b/src/components/chat/smallChat/index.vue new file mode 100644 index 0000000..e40d41a --- /dev/null +++ b/src/components/chat/smallChat/index.vue @@ -0,0 +1,254 @@ +<template> + <div class="relative"> + <el-tooltip v-if="!isOpen" content="浣犲ソ锛屾垜鏄疉I鍔╃悊锛屽彲浠ヨВ绛旈棶棰樸�佹帹鑽愯В鍐虫柟妗堢瓑" placement="right"> + <div + class="flex flex-col items-center gap-1 cursor-pointer bg-white rounded-lg shadow-lg p-1 absolute bottom-4 right-4 opacity-80" + @click="openChat" + > + <img :src="assistantPic" class="size-10" alt="AI澶村儚" /> + <span class="text-lg text-center w-5">AI鍔╂墜</span> + </div> + </el-tooltip> + <div v-else class="bg-white rounded-lg shadow-lg flex flex-col w-[400px] h-[600px] absolute bottom-4 right-4"> + <!-- 澶撮儴 --> + <div class="h-12 flex items-center justify-between px-4 border-b"> + <div class="text-base font-medium">WI姘村姟鏅鸿兘鍔╃悊</div> + <div class="flex items-center gap-2"> + <!-- <el-icon class="cursor-pointer text-gray-400 hover:text-gray-600"> + <Refresh /> + </el-icon> + <el-icon class="cursor-pointer text-gray-400 hover:text-gray-600"> + <FullScreen /> + </el-icon> + <el-icon class="cursor-pointer text-gray-400 hover:text-gray-600"> + <Star /> + </el-icon> --> + <el-icon class="cursor-pointer text-gray-400 hover:text-gray-600" @click="closeChat"> + <Close /> + </el-icon> + </div> + </div> + + <!-- 鍐呭鍖� --> + <div class="flex-1 overflow-y-auto p-4"> + <div v-if="isInit"> + <!-- 娆㈣繋璇� --> + <div class="flex flex-col items-center gap-4 mt-8"> + <img :src="assistantPic" class="w-16 h-16" alt="AI澶村儚" /> + <div class="text-lg">浣犲ソ, 鎴戞槸</div> + <div class="text-lg text-blue-500">WI姘村姟鏅鸿兘鍔╃悊</div> + </div> + <!-- 蹇嵎闂 --> + <div class="mt-8 flex flex-col gap-3 mx-10"> + <div + v-for="(item, index) in initQuestionList" + :key="index" + class="cursor-pointer hover:bg-gray-50 p-3 rounded-lg border border-solid border-gray-400" + @click="handleQuestionClick(item)" + > + {{ item }} + </div> + <!-- <div class="cursor-pointer hover:bg-gray-50 p-3 rounded-lg border border-solid border-gray-400"> + 闃块噷浜戜骇鍝佹�庝箞璐拱,閲嶇偣鏈夊摢浜涘姛鑳�? + </div> + <div class="cursor-pointer hover:bg-gray-50 p-3 rounded-lg border border-solid border-gray-400">AI 鍔╃悊鑳戒负鎴戝仛浠�涔�?</div> --> + </div> + </div> + <div v-else class="flex flex-col gap-4"> + <!-- 瀵硅瘽鍐呭 --> + <div class="flex flex-col gap-4" v-for="item in historyMessages" :key="item.id"> + <!-- 鐢ㄦ埛鎻愰棶 --> + <div class="flex gap-3" v-if="item.role === 'user'"> + <img :src="userPic" class="w-10 h-10" alt="鐢ㄦ埛澶村儚" /> + <div class="flex-1 bg-blue-100"> + <div class="p-4 rounded-lg">{{ item.content.value }}</div> + </div> + </div> + + <!-- AI鍥炵瓟 --> + <div class="flex gap-3" v-else-if="item.role === 'assistant'"> + <img :src="assistantPic" class="w-10 h-10" alt="AI澶村儚" /> + <div class="flex-1 bg-gray-100"> + <div v-if="(item.content as AssistantContent)?.isLoading" class="p-4 rounded-lg flex items-center"> + <el-icon class="animate-spin mr-2"><Loading /></el-icon> + AI姝e湪鎬濊�冧腑... + </div> + <div v-else class="p-4 rounded-lg"> + {{ item.content.value }} + </div> + </div> + </div> + </div> + </div> + </div> + + <!-- 搴曢儴杈撳叆妗� --> + <div class="p-4 border-t"> + <ChatInput v-model="inputText" @sendClick="sendClick" /> + </div> + </div> + </div> +</template> + +<script setup lang="ts" name="smallChat"> +import { computed, onMounted, ref } from 'vue'; +import ChatInput from './ChatInput.vue'; +import type { ChatMessage } from './types'; +import { AssistantContent } from './types'; +import { agentStreamByPost, question_stream_reply } from '/@/api/ai/chat'; +import { Logger } from '/@/model/logger/Logger'; +import type { OLMap } from '/@/model/map/OLMap'; +import assistantPic from '/static/images/role/assistant-200x192.png'; +import userPic from '/static/images/role/user-200x206.png'; + +const props = defineProps<{ + olMap?: OLMap; +}>(); +const isOpen = ref(false); +const inputText = ref(''); +const closeChat = () => { + isOpen.value = false; +}; +const historyMessages = ref<ChatMessage[]>([]); +const isInit = computed(() => historyMessages.value.length === 0); +const initQuestionList = ref(null); +const getLastAssistantMessage = () => { + const last = historyMessages.value[historyMessages.value.length - 1]; + const result = last.role === 'assistant' ? last : null; + return result as ChatMessage<AssistantContent>; +}; +let streamIsOpen = false; +const startStream = () => { + agentStreamByPost( + { + agent_id: 'a_019471cdb0667a83956b76ac97283f1c', + }, + (chunkRes) => { + if (!streamIsOpen) { + streamIsOpen = true; + } + Logger.info('agent stream response锛歕n\n' + JSON.stringify(chunkRes)); + if (chunkRes.mode === 'question') { + if (chunkRes.type === 'json') { + if (!initQuestionList.value) { + initQuestionList.value = chunkRes.value?.options ?? []; + } + activeQuestionChunk = chunkRes; + } + } + if (chunkRes.mode === 'map') { + if (chunkRes.type === 'string') { + const jsonData = JSON.parse(chunkRes.value); + handleMapCommand(jsonData); + } + } + + if (chunkRes.mode === 'finish') { + isOpen.value = false; + streamIsOpen = false; + const last = getLastAssistantMessage(); + if (last) { + last.content.value = '宸查��鍑�'; + last.content.isLoading = false; + } + } + } + ); +}; +const handleMapCommand = (command: any) => { + if (!command) return; + const last = getLastAssistantMessage(); + if (last) { + last.content.value = `宸叉墽琛屾搷浣�: ${command.operate}`; + switch (command.operate) { + case '鏀惧ぇ': + props.olMap.zoomIn(); + break; + + case '缂╁皬': + props.olMap.zoomOut(); + break; + } + last.content.isLoading = false; + } +}; + +const scrollToBottom = () => { + const chatContainer = document.querySelector('.chat-container'); + if (chatContainer) { + chatContainer.scrollTop = chatContainer.scrollHeight; + } +}; +const openChat = () => { + isOpen.value = true; + if (!streamIsOpen) { + startStream(); + } +}; +const applyMessage = () => { + const time = Date.now().toString(); + const userMessageId = `user-${time}`; + const userMessage: ChatMessage = { + id: userMessageId, + role: 'user', + content: { + value: inputText.value, + }, + }; + historyMessages.value.push(userMessage); + const assistantMessageId = `assistant-${time}`; + const assistantMessage: ChatMessage = { + id: assistantMessageId, + role: 'assistant', + content: { + value: '', + isLoading: true, + }, + }; + historyMessages.value.push(assistantMessage); + const userReactive = historyMessages.value.find((item) => item.id === userMessageId); + const assistantReactive = historyMessages.value.find((item) => item.id === assistantMessageId); + return [userReactive, assistantReactive]; +}; + +const resetActive = () => { + activeQuestionChunk = null; +}; + +const refreshMessageList = () => { + historyMessages.value = historyMessages.value.concat([]); +}; +const sendClick = async () => { + if (!activeQuestionChunk) return; + if (!inputText.value.trim()) return; + const lastMessage = historyMessages.value[historyMessages.value.length - 1]; + if (lastMessage && lastMessage.role === 'assistant' && (lastMessage.content as AssistantContent).isLoading) return; + const [userMessage, assistantMessage] = applyMessage(); + const question = inputText.value; + inputText.value = ''; + + const assistantContent = assistantMessage.content as AssistantContent; + const res = await question_stream_reply({ + select: question, + reply_id: activeQuestionChunk.value.reply_id, + }).catch(() => { + assistantContent.isLoading = false; + assistantContent.value = 'AI鍥炵瓟澶辫触'; + }); + resetActive(); +}; + +const handleQuestionClick = (item: string) => { + inputText.value = item; + sendClick(); +}; +const isFinish = ref(false); +let activeQuestionChunk = null; +onMounted(() => {}); +</script> + +<style scoped lang="scss"> +:deep(.el-input__wrapper) { + padding-right: 0; +} +</style> diff --git a/src/components/chat/smallChat/types.ts b/src/components/chat/smallChat/types.ts new file mode 100644 index 0000000..0e929cb --- /dev/null +++ b/src/components/chat/smallChat/types.ts @@ -0,0 +1,15 @@ +export type AssistantContent = { + value:string; + isLoading: boolean; +}; + +export type UserContent = { + value: string; +}; + +export interface ChatMessage<T = UserContent | AssistantContent> { + id: string; + role: 'user' | 'assistant'; + content: T; +} + diff --git a/src/model/map/OLMap.ts b/src/model/map/OLMap.ts index 4d51821..55c97df 100644 --- a/src/model/map/OLMap.ts +++ b/src/model/map/OLMap.ts @@ -267,6 +267,24 @@ olZoom.style.display = 'none'; } + /** + * 鏀惧ぇ鍦板浘 + */ + zoomIn() { + const view = this.map.getView(); + const zoom = view.getZoom(); + view.setZoom(zoom + 1); + } + + /** + * 缂╁皬鍦板浘 + */ + zoomOut() { + const view = this.map.getView(); + const zoom = view.getZoom(); + view.setZoom(zoom - 1); + } + getWMTS = () => { const projection = getProjection('EPSG:3857'); const projectionExtent = projection.getExtent(); diff --git a/vite.config.ts b/vite.config.ts index 27dae42..1c82ca0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -58,7 +58,7 @@ host: '0.0.0.0', port: env.VITE_PORT as unknown as number, open: JSON.parse(env.VITE_OPEN), - hmr: true, + hmr: false, proxy: { '/events': { target: 'http://localhost:3000', -- Gitblit v1.9.3