diff --git a/.env.development b/.env.development index a76eccb..1397c6e 100644 --- a/.env.development +++ b/.env.development @@ -3,9 +3,9 @@ VITE_CLI_PORT = 8080 VITE_SERVER_PORT = 8888 VITE_BASE_API = /api VITE_FILE_API = /api -# VITE_BASE_PATH = http://192.168.1.9:8888 +VITE_BASE_PATH = http://192.168.1.9:8888 # VITE_BASE_PATH = http://192.168.110.98:8888 -VITE_BASE_PATH = https://www.xingoil.com/api +# VITE_BASE_PATH = https://www.xingoil.com/api VITE_POSITION = open VITE_EDITOR = code // VITE_EDITOR = webstorm 如果使用webstorm开发且要使用dom定位到代码行功能 请先自定添加 webstorm到环境变量 再将VITE_EDITOR值修改为webstorm diff --git a/src/App.vue b/src/App.vue index c00b2c7..eb4b1e5 100644 --- a/src/App.vue +++ b/src/App.vue @@ -11,8 +11,11 @@ import zhCn from 'element-plus/dist/locale/zh-cn.mjs' import Application from '@/components/application/index.vue' import { useAppStore } from '@/pinia' + import { useAlarmStream } from '@/hooks/useAlarmStream' useAppStore() + useAlarmStream() + defineOptions({ name: 'App' }) diff --git a/src/components/alarmBroadcast/index.vue b/src/components/alarmBroadcast/index.vue new file mode 100644 index 0000000..e907c55 --- /dev/null +++ b/src/components/alarmBroadcast/index.vue @@ -0,0 +1,282 @@ + + + + + diff --git a/src/components/application/index.vue b/src/components/application/index.vue index 4dda3ec..0e88484 100644 --- a/src/components/application/index.vue +++ b/src/components/application/index.vue @@ -1,39 +1,60 @@ diff --git a/src/hooks/useAlarmListener.js b/src/hooks/useAlarmListener.js new file mode 100644 index 0000000..ad44794 --- /dev/null +++ b/src/hooks/useAlarmListener.js @@ -0,0 +1,11 @@ +import { tryOnScopeDispose } from '@vueuse/core' +import { emitter } from '@/utils/bus' +import { ALARM_CREATED_EVENT } from '@/utils/alarm' + +/** 订阅全局告警推送,组件卸载时自动取消 */ +export function useAlarmListener(handler) { + if (typeof handler !== 'function') return + + emitter.on(ALARM_CREATED_EVENT, handler) + tryOnScopeDispose(() => emitter.off(ALARM_CREATED_EVENT, handler)) +} diff --git a/src/hooks/useAlarmStream.js b/src/hooks/useAlarmStream.js new file mode 100644 index 0000000..31772e2 --- /dev/null +++ b/src/hooks/useAlarmStream.js @@ -0,0 +1,119 @@ +import { ref, watch } from 'vue' +import { storeToRefs } from 'pinia' +import { tryOnScopeDispose } from '@vueuse/core' +import { useUserStore } from '@/pinia/modules/user' +import { emitter } from '@/utils/bus' +import { ALARM_CREATED_EVENT } from '@/utils/alarm' + +const ALARM_STREAM_PATH = '/device/alarmStream' +const RECONNECT_BASE_MS = 3000 +const RECONNECT_MAX_MS = 30000 + +const READY_STATE = ['CONNECTING', 'OPEN', 'CLOSED'] + +/** 构建 SSE 连接地址(走 /api 代理,与 axios 一致;token 走 query 鉴权) */ +export function buildAlarmStreamUrl(token) { + const base = `${import.meta.env.VITE_BASE_API}${ALARM_STREAM_PATH}` + if (!token) return base + const separator = base.includes('?') ? '&' : '?' + return `${base}${separator}token=${encodeURIComponent(token)}` +} + +function parseAlarmPayload(raw) { + const data = JSON.parse(raw) + return data?.alarm ?? data?.data ?? data +} + +export function useAlarmStream() { + const userStore = useUserStore() + const { token } = storeToRefs(userStore) + + let es = null + let reconnectTimer = null + let reconnectDelay = RECONNECT_BASE_MS + + const connected = ref(false) + + const clearReconnectTimer = () => { + if (reconnectTimer) { + clearTimeout(reconnectTimer) + reconnectTimer = null + } + } + + const disconnect = () => { + clearReconnectTimer() + if (es) { + es.close() + es = null + } + connected.value = false + reconnectDelay = RECONNECT_BASE_MS + } + + const scheduleReconnect = () => { + if (!token.value || reconnectTimer) return + reconnectTimer = setTimeout(() => { + reconnectTimer = null + connect() + }, reconnectDelay) + reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS) + } + + const handleAlarmEvent = (e) => { + reconnectDelay = RECONNECT_BASE_MS + console.log('[alarmStream] 收到 SSE 消息', { type: e.type, data: e.data }) + try { + const alarm = parseAlarmPayload(e.data) + if (!alarm) { + console.warn('[alarmStream] 消息中未找到告警数据', e.data) + return + } + emitter.emit(ALARM_CREATED_EVENT, alarm) + } catch (err) { + console.error('[alarmStream] 解析告警数据失败', err, e.data) + } + } + + const connect = () => { + if (!token.value || es) return + + const url = buildAlarmStreamUrl(token.value) + console.log('[alarmStream] EventSource 启动', url) + es = new EventSource(url, { withCredentials: true }) + + es.addEventListener('alarm.created', handleAlarmEvent) + es.onmessage = handleAlarmEvent + + es.onopen = () => { + connected.value = true + reconnectDelay = RECONNECT_BASE_MS + console.log('[alarmStream] EventSource 连接成功') + } + + es.onerror = () => { + const state = es?.readyState + console.warn('[alarmStream] EventSource 异常', READY_STATE[state] ?? state) + connected.value = state === EventSource.OPEN + + // CONNECTING 时浏览器会自动重连,不要手动 close + if (state === EventSource.CLOSED) { + es = null + scheduleReconnect() + } + } + } + + watch( + token, + (val) => { + disconnect() + if (val) connect() + }, + { immediate: true } + ) + + tryOnScopeDispose(disconnect) + + return { connected } +} diff --git a/src/pathInfo.json b/src/pathInfo.json index 0e5a5cc..4736c63 100644 --- a/src/pathInfo.json +++ b/src/pathInfo.json @@ -67,6 +67,13 @@ "/src/view/masterStation/index.vue": "MasterStation", "/src/view/masterStation/project/index.vue": "MasterStationProject", "/src/view/person/person.vue": "Person", + "/src/view/privateEquipment/equipment/components/detail/components/config/index.vue": "Index", + "/src/view/privateEquipment/equipment/components/detail/components/info/index.vue": "Index", + "/src/view/privateEquipment/equipment/components/detail/components/line/index.vue": "Index", + "/src/view/privateEquipment/equipment/components/detail/components/trend/index.vue": "Index", + "/src/view/privateEquipment/equipment/components/detail/index.vue": "Index", + "/src/view/privateEquipment/equipment/components/list/index.vue": "Index", + "/src/view/privateEquipment/equipment/index.vue": "Index", "/src/view/resourcesDepletion/electric/index.vue": "ResourcesDepletionElectric", "/src/view/resourcesDepletion/index.vue": "ResourcesDepletion", "/src/view/resourcesDepletion/load/index.vue": "ResourcesDepletionLoad", diff --git a/src/pinia/modules/project.js b/src/pinia/modules/project.js index 839cd2d..add6c14 100644 --- a/src/pinia/modules/project.js +++ b/src/pinia/modules/project.js @@ -1,22 +1,40 @@ import { defineStore } from 'pinia' import { ref } from 'vue' -const PROJECT_STORAGE_KEY = 'masterStation_current_project_id' +const PROJECT_STORAGE_KEY = 'masterStation_current_project' +const LEGACY_PROJECT_ID_KEY = 'masterStation_current_project_id' + +function loadSavedProject() { + try { + const raw = localStorage.getItem(PROJECT_STORAGE_KEY) + if (raw) { + const project = JSON.parse(raw) + if (project?.id) return project + } + + const legacyId = Number(localStorage.getItem(LEGACY_PROJECT_ID_KEY)) + if (legacyId) return { id: legacyId } + } catch { + // ignore invalid cache + } + return null +} export const useProjectStore = defineStore('project', () => { - const currentProject = ref(null) + const currentProject = ref(loadSavedProject()) const setCurrentProject = (project) => { currentProject.value = project || null if (project?.id) { - localStorage.setItem(PROJECT_STORAGE_KEY, String(project.id)) + localStorage.setItem(PROJECT_STORAGE_KEY, JSON.stringify(project)) + localStorage.setItem(LEGACY_PROJECT_ID_KEY, String(project.id)) + } else { + localStorage.removeItem(PROJECT_STORAGE_KEY) + localStorage.removeItem(LEGACY_PROJECT_ID_KEY) } } - const getSavedProjectId = () => { - const savedId = Number(localStorage.getItem(PROJECT_STORAGE_KEY)) - return savedId || null - } + const getSavedProjectId = () => currentProject.value?.id ?? null return { currentProject, diff --git a/src/utils/alarm.js b/src/utils/alarm.js new file mode 100644 index 0000000..e9e08a1 --- /dev/null +++ b/src/utils/alarm.js @@ -0,0 +1,73 @@ +export const ALARM_CREATED_EVENT = 'alarm.created' + +function pickField(alarm, keys, fallback = '') { + if (!alarm) return fallback + for (const key of keys) { + const value = alarm[key] + if (value !== undefined && value !== null && value !== '') return value + } + return fallback +} + +function formatDateTime(value) { + if (!value) return '' + const date = value instanceof Date ? value : new Date(value) + if (Number.isNaN(date.getTime())) return String(value) + const pad = (n) => String(n).padStart(2, '0') + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` +} + +/** 弹窗:设备位置行,如 2号楼, 12层, 橱柜3(98CC4D11AA3C)- */ +export function formatAlarmDeviceLine(alarm) { + if (!alarm) return '未知设备' + const location = pickField(alarm, ['location', 'address', 'projectAddress', 'projectName']) + const building = pickField(alarm, ['building', 'buildingName', 'buildingNo']) + const floor = pickField(alarm, ['floor', 'floorName', 'floorNo']) + const deviceName = pickField(alarm, ['deviceName', 'deviceAlias', 'aliasName', 'name', 'remark']) + const mac = pickField(alarm, ['mac', 'deviceNo', 'deviceMac']) + + const parts = [] + if (building) parts.push(building) + if (floor) parts.push(floor) + if (deviceName) { + parts.push(mac ? `${deviceName}(${mac})` : deviceName) + } else if (mac) { + parts.push(mac) + } + if (location && !parts.length) parts.push(location) + if (!parts.length) return '未知设备' + return `${parts.join(', ')}-` +} + +/** 弹窗:告警类型文案 */ +export function formatAlarmEventType(alarm) { + if (!alarm) return '未知告警' + return pickField(alarm, ['alarmTypeName', 'alarmType', 'warnTypeName', 'warnType', 'remark'], '未知告警') +} + +/** 弹窗:联系人信息 */ +export function formatAlarmContact(alarm) { + if (!alarm) return '' + const contactName = pickField(alarm, ['contactName', 'linkman', 'contact', 'contactPerson']) + const contactPhone = pickField(alarm, ['contactPhone', 'phone', 'mobile', 'tel']) + if (contactName && contactPhone) return `(联系人: ${contactName} 联系电话: ${contactPhone})` + if (contactName) return `(联系人: ${contactName})` + if (contactPhone) return `(联系电话: ${contactPhone})` + return '' +} + +/** 弹窗:告警时间 */ +export function formatAlarmTime(alarm) { + if (!alarm) return formatDateTime(new Date()) + const raw = pickField(alarm, ['CreatedAt', 'createdAt', 'alarmTime', 'alarmCreatedAt', 'time']) + return formatDateTime(raw) || formatDateTime(new Date()) +} + +export function formatAlarmMessage(alarm) { + if (!alarm) return '收到新告警' + const parts = [] + if (alarm.mac) parts.push(`设备: ${alarm.mac}`) + if (alarm.remark) parts.push(alarm.remark) + else if (alarm.alarmType) parts.push(alarm.alarmType) + return parts.join(' · ') || '收到新告警' +} diff --git a/src/view/masterStation/equipment/components/detail/components/info/index.vue b/src/view/masterStation/equipment/components/detail/components/info/index.vue index 6b03513..0df2e02 100644 --- a/src/view/masterStation/equipment/components/detail/components/info/index.vue +++ b/src/view/masterStation/equipment/components/detail/components/info/index.vue @@ -115,7 +115,7 @@ const tableData = ref([]) onMounted(() => { - // console.log('123123') + console.log(props.device) // getAlarmRecordListByPage() getAlarmRecordListByPage() }) diff --git a/src/view/masterStation/equipment/components/detail/index.vue b/src/view/masterStation/equipment/components/detail/index.vue index b022e00..11f440d 100644 --- a/src/view/masterStation/equipment/components/detail/index.vue +++ b/src/view/masterStation/equipment/components/detail/index.vue @@ -23,22 +23,31 @@ + + diff --git a/src/view/privateEquipment/equipment/components/detail/components/info/index.vue b/src/view/privateEquipment/equipment/components/detail/components/info/index.vue new file mode 100644 index 0000000..6b03513 --- /dev/null +++ b/src/view/privateEquipment/equipment/components/detail/components/info/index.vue @@ -0,0 +1,259 @@ + + + + + diff --git a/src/view/privateEquipment/equipment/components/detail/components/line/index.vue b/src/view/privateEquipment/equipment/components/detail/components/line/index.vue new file mode 100644 index 0000000..12baff0 --- /dev/null +++ b/src/view/privateEquipment/equipment/components/detail/components/line/index.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/src/view/privateEquipment/equipment/components/detail/components/trend/index.vue b/src/view/privateEquipment/equipment/components/detail/components/trend/index.vue new file mode 100644 index 0000000..0fb8fc0 --- /dev/null +++ b/src/view/privateEquipment/equipment/components/detail/components/trend/index.vue @@ -0,0 +1,847 @@ + + + + + diff --git a/src/view/privateEquipment/equipment/components/detail/index.vue b/src/view/privateEquipment/equipment/components/detail/index.vue new file mode 100644 index 0000000..b022e00 --- /dev/null +++ b/src/view/privateEquipment/equipment/components/detail/index.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/src/view/privateEquipment/equipment/components/list/index.vue b/src/view/privateEquipment/equipment/components/list/index.vue new file mode 100644 index 0000000..5f7dcb2 --- /dev/null +++ b/src/view/privateEquipment/equipment/components/list/index.vue @@ -0,0 +1,384 @@ + + + + + diff --git a/src/view/privateEquipment/equipment/index.vue b/src/view/privateEquipment/equipment/index.vue new file mode 100644 index 0000000..37536dc --- /dev/null +++ b/src/view/privateEquipment/equipment/index.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/view/superAdmin/menu/components/components-cascader.vue b/src/view/superAdmin/menu/components/components-cascader.vue index 2c505fa..cace0fa 100644 --- a/src/view/superAdmin/menu/components/components-cascader.vue +++ b/src/view/superAdmin/menu/components/components-cascader.vue @@ -8,12 +8,14 @@ filterable clearable class="component-path-control" + style="width: 100%" @change="emitChange" /> @@ -124,10 +126,7 @@ } const emitChange = () => { - emits( - 'change', - pathIsSelect.value ? activeComponent.value?.join('/') : tempPath.value - ) + emits('change', pathIsSelect.value ? activeComponent.value?.join('/') : tempPath.value) } @@ -142,8 +141,8 @@ .component-path-control { flex: 1 1 auto; - width: 0; - min-width: 0; + width: 100%; + min-width: 520px; } .component-path-toggle { diff --git a/vite.config.js b/vite.config.js index d37f825..5a499be 100644 --- a/vite.config.js +++ b/vite.config.js @@ -85,6 +85,8 @@ export default ({ mode }) => { // 需要代理的路径 例如 '/api' target: `${process.env.VITE_BASE_PATH}/`, // 代理到 目标路径 changeOrigin: true, + timeout: 0, + proxyTimeout: 0, rewrite: (path) => path.replace(new RegExp('^' + process.env.VITE_BASE_API), '') } }