warehouse P0修复: http.js(lang_storage_key)+SignalR内存泄漏(LRU)+Pinia store合并(7文件import同步)+Camera type导入修复

This commit is contained in:
2026-06-04 03:08:18 +08:00
parent 85600d0c80
commit fa170e55a9
14 changed files with 77 additions and 144 deletions

View File

@@ -38,6 +38,8 @@ if (!axios.defaults.baseURL.endsWith('/')) {
axios.defaults.baseURL += '/' axios.defaults.baseURL += '/'
} }
const lang_storage_key = 'lang'
let ipAddress = axios.defaults.baseURL; let ipAddress = axios.defaults.baseURL;
if (!ipAddress || ipAddress == '/') { if (!ipAddress || ipAddress == '/') {
ipAddress = window.location.origin + '/'; ipAddress = window.location.origin + '/';

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
import { watch, computed } from "vue" import { watch, computed } from "vue"
import { useMapStore } from "../store/useMapStore" import { useMapStore } from "../stores/mapStore"
// 可以使用pinia等管理全局数据这里只是方便演示, 直接注入了上层提供的数据 // 可以使用pinia等管理全局数据这里只是方便演示, 直接注入了上层提供的数据
const store = useMapStore() const store = useMapStore()

View File

@@ -1,7 +1,7 @@
<script setup> <script setup>
import { computed } from "vue" import { computed } from "vue"
import { useNavi } from "../hooks/useNavi" import { useNavi } from "../hooks/useNavi"
import { useMapStore } from "../store/useMapStore" import { useMapStore } from "../stores/mapStore"
// 可以使用pinia等管理全局数据这里只是方便演示, 直接注入了上层提供的数据 // 可以使用pinia等管理全局数据这里只是方便演示, 直接注入了上层提供的数据
const store = useMapStore() const store = useMapStore()

View File

@@ -1,7 +1,7 @@
<script setup> <script setup>
import { inject, onBeforeUnmount } from "vue" import { inject, onBeforeUnmount } from "vue"
import { useFence } from "../hooks/useFence" import { useFence } from "../hooks/useFence"
import { useMapStore } from "../store/useMapStore" import { useMapStore } from "../stores/mapStore"
const { THREE } = VgoMap const { THREE } = VgoMap

View File

@@ -1,7 +1,7 @@
<script setup> <script setup>
import { inject, ref, watch, onBeforeUnmount } from "vue" import { inject, ref, watch, onBeforeUnmount } from "vue"
import { useClassPolygon } from "../hooks/useClassPolygon" import { useClassPolygon } from "../hooks/useClassPolygon"
import { useMapStore } from "../store/useMapStore" import { useMapStore } from "../stores/mapStore"
import http from "../api/http" import http from "../api/http"
const { THREE } = VgoMap const { THREE } = VgoMap

View File

@@ -1,7 +1,7 @@
<script setup> <script setup>
import { ref, watch, onBeforeUnmount } from "vue" import { ref, watch, onBeforeUnmount } from "vue"
import { useSkyLight } from "../hooks/useSkyLight" import { useSkyLight } from "../hooks/useSkyLight"
import { useMapStore } from "../store/useMapStore" import { useMapStore } from "../stores/mapStore"
import CustomSwitch from "./CustomSwitch.vue" import CustomSwitch from "./CustomSwitch.vue"
const store = useMapStore() const store = useMapStore()

View File

@@ -1,7 +1,7 @@
<script setup> <script setup>
import { ref, watch } from "vue" import { ref, watch } from "vue"
import { useWeatherEffect, WeatherType } from "../hooks/useWeather" import { useWeatherEffect, WeatherType } from "../hooks/useWeather"
import { useMapStore } from "../store/useMapStore" import { useMapStore } from "../stores/mapStore"
import CustomSwitch from "./CustomSwitch.vue" import CustomSwitch from "./CustomSwitch.vue"
const store = useMapStore() const store = useMapStore()

View File

@@ -1,29 +0,0 @@
import { ref, computed } from "vue"
import { defineStore } from "pinia"
export const useMapStore = defineStore('map', () => {
const map = ref(null)
const polygonDataAll = computed(() => {
const outDoor = map.value?.mapData?.polygonData ?? []
const inDoor = map?.mapData?.build?.reduce((result, build) => {
build.floor.forEach(fItem => {
result.push(...fItem.polygonData)
})
return result
}, []) ?? []
return [...outDoor, ...inDoor]
})
function setMap(mapInstance) {
map.value = mapInstance
}
return {
map,
polygonDataAll,
setMap,
}
})

View File

@@ -1,48 +1,34 @@
import { ref } from "vue" import { ref, computed } from "vue"
import { defineStore } from "pinia" import { defineStore } from "pinia"
export const useMapStore = defineStore('map', () => { export const useMapStore = defineStore('map', () => {
const map = ref(null) const map = ref(null)
const lastClickedPosition = ref(null) // 存储地图最后点击位置的经纬度 const lastClickedPosition = ref(null)
const editingMode = ref(false) // 编辑状态标识 const editingMode = ref(false)
function setMap(mapInstance) { /** 聚合所有楼层+室外的 polygonData */
map.value = mapInstance const polygonDataAll = computed(() => {
} const outDoor = map.value?.mapData?.polygonData ?? []
const builds = map.value?.mapData?.build ?? []
const inDoor = builds.reduce((result, build) => {
build.floor?.forEach(f => { result.push(...(f.polygonData ?? [])) })
return result
}, [])
return [...outDoor, ...inDoor]
})
// 设置地图最后点击位置的经纬度 function setMap(mapInstance) { map.value = mapInstance }
function setLastClickedPosition(lng, lat) { function setLastClickedPosition(lng, lat) { lastClickedPosition.value = { lng, lat } }
lastClickedPosition.value = { lng, lat }
}
// 设置编辑状态
function setEditingMode(mode) { function setEditingMode(mode) {
editingMode.value = mode editingMode.value = mode
if(mode){ map.value?.setViewMode(mode ? '2D' : '3D')
map.value.setViewMode('2D')
}
else{
map.value.setViewMode('3D')
}
}
// 获取编辑状态
function getEditingMode() {
if(editingMode.value){
return editingMode.value
}
else{
return false
}
} }
function getEditingMode() { return editingMode.value }
return { return {
map, map, setMap,
setMap, lastClickedPosition, setLastClickedPosition,
lastClickedPosition, editingMode, setEditingMode, getEditingMode,
setLastClickedPosition, polygonDataAll,
editingMode,
setEditingMode,
getEditingMode
} }
}) })

View File

@@ -6,7 +6,7 @@ import Sky from "../components/Sky.vue"
import DisplayColor from "../components/DisplayColor.vue" import DisplayColor from "../components/DisplayColor.vue"
import DisplayRouteLine from "../components/DisplayRouteLine.vue" import DisplayRouteLine from "../components/DisplayRouteLine.vue"
import Weather from "../components/Weather.vue" import Weather from "../components/Weather.vue"
import { useMapStore } from "../store/useMapStore" import { useMapStore } from "../stores/mapStore"
const { VgoMap } = window const { VgoMap } = window
let mapId = /#\/(\d+)/.exec(location.hash)[1] let mapId = /#\/(\d+)/.exec(location.hash)[1]

View File

@@ -2,89 +2,60 @@ import * as signalR from "@microsoft/signalr";
import { ElNotification } from "element-plus"; import { ElNotification } from "element-plus";
import { ElMessageBox } from "element-plus"; import { ElMessageBox } from "element-plus";
import store from "@/store/index"; import store from "@/store/index";
export default function (http, receive) { export default function (http, receive) {
let connection; let connection
let messageQueue = []; // LRU: 最近 500 条 ID防内存无限增长
let isProcessingQueue = false; const MAX_IDS = 500
let displayedMessageIds = new Set(); // 用于存储已显示的消息ID避免重复显示 const displayedIds = new Set()
const idList = []
// 消息队列处理函数 - 移到闭包内部,确保可以访问所有必要的变量 function addId(id) {
function processMessageQueue() { if (displayedIds.has(id)) return
if (messageQueue.length === 0) { displayedIds.add(id)
isProcessingQueue = false; idList.push(id)
return; if (idList.length > MAX_IDS) {
} displayedIds.delete(idList.shift())
isProcessingQueue = true;
const currentMessage = messageQueue.shift();
// 直接调用receive回调处理消息显示
if (receive) {
try {
receive(currentMessage);
console.log('成功调用receive回调处理消息:', currentMessage);
} catch (error) {
console.error('调用receive回调时出错:', error);
} }
} }
function hasId(id) { return displayedIds.has(id) }
// 可以设置一定的延迟,避免消息显示过于密集 // 重试启动: 最多 5 次, 间隔 2s
setTimeout(() => { async function startWithRetry(conn, retries = 5) {
processMessageQueue(); for (let i = 1; i <= retries; i++) {
}, 3000); // 3秒后处理下一条消息 try { await conn.start(); return }
catch (e) { if (i < retries) await new Promise(r => setTimeout(r, 2000)) }
}
console.error('SignalR 连接失败 (5次重试后放弃)')
} }
http.post("api/user/GetCurrentUserInfo").then((result) => { http.post("api/user/GetCurrentUserInfo").then(async (result) => {
console.log(result,'result'); if (!result?.data?.userName) {
console.error('获取用户信息失败: 缺少 userName')
return
}
connection = new signalR.HubConnectionBuilder() connection = new signalR.HubConnectionBuilder()
.withAutomaticReconnect() .withAutomaticReconnect()
.withUrl( .withUrl('/hub/message?userName=' + result.data.userName)
`/hub/message?userName=${result.data.userName}`, .build()
{
//withCredentials: true // 启用凭证模式 startWithRetry(connection)
// accessTokenFactory: () => store.getters.getToken()
} connection.onreconnected(function (id) {
) if (import.meta.env.DEV) console.log('SignalR reconnected:', id)
//.withUrl(`${http.ipAddress}message`) })
.build();
connection.start().catch((err) => console.log(err.message));
//自动重连成功后的处理
connection.onreconnected((connectionId) => {
console.log(connectionId, 'connectionId');
});
connection.on("ReceiveHomePageMessage", function (data) { connection.on("ReceiveHomePageMessage", function (data) {
console.log('接收到新消息:', data); if (data?.id && hasId(data.id)) return
if (data?.id) addId(data.id)
// 检查消息是否已显示过
if (data.id && displayedMessageIds.has(data.id)) {
console.log('消息已显示过,跳过:', data.id);
return;
}
// 将消息ID添加到已显示集合
if (data.id) {
displayedMessageIds.add(data.id);
console.log('添加到已显示集合:', data.id);
}
// 添加到store
// store.getters.data().pushMessage(data);
console.log(data, 'data_after_push');
// 直接调用receive回调
if (receive) { if (receive) {
try { try { receive(data) }
receive(data); catch (e) { console.error('receive callback error:', e) }
console.log('成功调用receive回调:', data);
} catch (error) {
console.error('调用receive回调时出错:', error);
} }
} else { })
console.warn('receive回调未定义无法处理消息'); }).catch(function (e) {
} console.error('getUserInfo failed:', e)
}); })
}).catch(error => {
console.error('获取用户信息时出错:', error);
});
} }

View File

@@ -61,7 +61,8 @@
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { VideoPlay } from '@element-plus/icons-vue' import { VideoPlay } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { fetchCameras, gwGet, Camera } from '@/api/gateway' import { fetchCameras, gwGet } from '@/api/gateway'
import type { Camera } from '@/api/gateway'
const cameras = ref<Camera[]>([]) const cameras = ref<Camera[]>([])
const selectedCamera = ref<Camera | null>(null) const selectedCamera = ref<Camera | null>(null)

View File

@@ -92,7 +92,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { VideoCamera } from '@element-plus/icons-vue' import { VideoCamera } from '@element-plus/icons-vue'
import { fetchCameras, gwGet, gwPost, Camera } from '@/api/gateway' import { fetchCameras, gwGet, gwPost } from '@/api/gateway'
import type { Camera } from '@/api/gateway'
const cameras = ref<Camera[]>([]) const cameras = ref<Camera[]>([])
const selectedCamera = ref<Camera | null>(null) const selectedCamera = ref<Camera | null>(null)

View File

@@ -106,7 +106,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import { fetchCameras, gwGet, gwPost, Camera } from '@/api/gateway' import { fetchCameras, gwGet, gwPost } from '@/api/gateway'
import type { Camera } from '@/api/gateway'
const cameras = ref<Camera[]>([]) const cameras = ref<Camera[]>([])
const streamUrls = ref<Record<string, string>>({}) const streamUrls = ref<Record<string, string>>({})