From 3503fada5e9bb478f3e5564f457e9959b6a6c3c3 Mon Sep 17 00:00:00 2001 From: wenshijun Date: Wed, 15 Apr 2026 08:32:42 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=98=E9=A6=96?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/ws.js | 132 +---- src/views/index.vue | 1234 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1263 insertions(+), 103 deletions(-) diff --git a/src/utils/ws.js b/src/utils/ws.js index 4278df6..310c26f 100644 --- a/src/utils/ws.js +++ b/src/utils/ws.js @@ -1,72 +1,29 @@ import { getToken } from '@/utils/auth' -/** - * 当前 WebSocket 实例 - */ let ws = null - -/** - * 重连定时器 - */ let reconnectTimer = null - -/** - * 是否人为关闭(用于区分主动关闭 vs 异常断开) - */ let manualClose = false - -/** - * 当前连接的部门ID(用于断线重连) - */ -let currentDeptId = null - -/** - * 当前消息回调函数 - */ let currentOnMessage = null +let retryCount = 0 -/** - * WebSocket路径 - */ const WS_PATH = '/ws' +const MAX_RECONNECT_DELAY = 30000 -/** - * 重连间隔(毫秒) - */ -const RECONNECT_INTERVAL = 5000 - -/** - * 获取 WebSocket 基础地址 - * 适配: - * 1. VITE_APP_BASE_API 是完整URL(http://xxx) - * 2. VITE_APP_BASE_API 是相对路径(/dev-api) - */ function getWsBaseUrl() { const baseApi = import.meta.env.VITE_APP_BASE_API || '' - // 如果是完整URL(http/https) if (/^https?:\/\//.test(baseApi)) { - // 转换为 ws/wss return baseApi.replace(/^http/, 'ws').replace(/\/$/, '') } - // 根据当前页面协议自动判断 ws / wss const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' return `${protocol}//${window.location.host}${baseApi}`.replace(/\/$/, '') } -/** - * 构建 WebSocket 连接地址 - * @param deptId 部门ID(用于数据隔离) - */ -function buildWsUrl(deptId) { +function buildWsUrl() { const url = new URL(`${getWsBaseUrl()}${WS_PATH}`) const token = getToken() - // 当前后端仍依赖 deptId 做数据隔离 - url.searchParams.set('deptId', deptId) - - // token 用于后续鉴权升级(预留) if (token) { url.searchParams.set('token', token) } @@ -74,88 +31,59 @@ function buildWsUrl(deptId) { return url.toString() } -/** - * 建立 WebSocket 连接 - * @param deptId 当前用户部门ID - * @param onMessage 消息回调函数 - */ -export function connectWs(deptId, onMessage) { - // 必须传 deptId(当前后端依赖) - if (!deptId) { - console.warn('[WebSocket] 缺少 deptId') +export function connectWs(onMessage) { + if (!getToken()) { + console.warn('[WebSocket] missing token') return } - currentDeptId = deptId currentOnMessage = onMessage manualClose = false - - // 清除旧的重连任务 clearReconnectTimer() - // 如果已经连接或正在连接,直接返回(防止重复连接) if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) { return } - // 创建 WebSocket 连接 - ws = new WebSocket(buildWsUrl(deptId)) + ws = new WebSocket(buildWsUrl()) - /** - * 连接成功 - */ ws.onopen = () => { - console.log('[WebSocket] 连接成功 deptId=', deptId) + retryCount = 0 + console.log('[WebSocket] connected') } - /** - * 收到消息 - */ ws.onmessage = (event) => { let data = event.data - // 尝试解析 JSON try { data = JSON.parse(event.data) } catch (e) { - // 非JSON消息保持原样 + // Keep plain text messages as-is. } - // 回调给业务层 currentOnMessage && currentOnMessage(data, event) } - /** - * 连接关闭 - */ ws.onclose = () => { - console.log('[WebSocket] 连接关闭 deptId=', currentDeptId) + console.log('[WebSocket] closed') ws = null - // 非人为关闭 → 自动重连 if (!manualClose) { reconnectTimer = setTimeout(() => { - console.log('[WebSocket] 开始重连 deptId=', currentDeptId) - connectWs(currentDeptId, currentOnMessage) - }, RECONNECT_INTERVAL) + console.log('[WebSocket] reconnecting') + connectWs(currentOnMessage) + }, getReconnectDelay()) } } - /** - * 连接异常 - */ ws.onerror = (error) => { - console.error('[WebSocket] 连接异常', error) + console.error('[WebSocket] error', error) } } -/** - * 主动关闭 WebSocket - */ export function closeWs() { manualClose = true - - // 清除重连任务 + retryCount = 0 clearReconnectTimer() if (ws) { @@ -164,36 +92,34 @@ export function closeWs() { } } -/** - * 发送消息 - * @param data 支持字符串或对象 - * @returns 是否发送成功 - */ +export function reconnectWs(onMessage = currentOnMessage) { + closeWs() + manualClose = false + connectWs(onMessage) +} + export function sendWs(data) { - // 连接未就绪直接返回 if (!ws || ws.readyState !== WebSocket.OPEN) { - console.warn('[WebSocket] 发送失败,连接未建立') + console.warn('[WebSocket] send failed, socket is not open') return false } - // 自动转JSON ws.send(typeof data === 'string' ? data : JSON.stringify(data)) return true } -/** - * 获取当前连接状态 - */ export function getWsState() { return ws ? ws.readyState : WebSocket.CLOSED } -/** - * 清除重连定时器 - */ +function getReconnectDelay() { + retryCount += 1 + return Math.min(5000 * retryCount, MAX_RECONNECT_DELAY) +} + function clearReconnectTimer() { if (reconnectTimer) { clearTimeout(reconnectTimer) reconnectTimer = null } -} \ No newline at end of file +} diff --git a/src/views/index.vue b/src/views/index.vue index 4c1ac56..0ac2b36 100644 --- a/src/views/index.vue +++ b/src/views/index.vue @@ -1,10 +1,1244 @@ + From 5a84f178f8ba76d26ebc48ff29a646ab28f2d42a Mon Sep 17 00:00:00 2001 From: wenshijun Date: Wed, 15 Apr 2026 08:32:52 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=98=E9=A6=96?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/worn/home.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/api/worn/home.js diff --git a/src/api/worn/home.js b/src/api/worn/home.js new file mode 100644 index 0000000..4303863 --- /dev/null +++ b/src/api/worn/home.js @@ -0,0 +1,43 @@ +import request from '@/utils/request' + +export function getHomeStat() { + return request({ + url: '/home/stat', + method: 'get' + }) +} + +export function getHomeDeviceStat() { + return request({ + url: '/home/device/stat', + method: 'get' + }) +} + +export function getHomeAlarmStat() { + return request({ + url: '/home/alarm/stat', + method: 'get' + }) +} + +export function getHomeAlarmTrend() { + return request({ + url: '/home/alarm/trend', + method: 'get' + }) +} + +export function getHomeAlarmType() { + return request({ + url: '/home/alarm/type', + method: 'get' + }) +} + +export function getHomeProjectList() { + return request({ + url: '/home/warehouse/list', + method: 'get' + }) +} From 8b2f021cbe83df1bd760cd73ff38ede71b2fd895 Mon Sep 17 00:00:00 2001 From: wenshijun Date: Wed, 15 Apr 2026 08:40:29 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=98=E9=A6=96?= =?UTF-8?q?=E9=A1=B5=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/index.vue | 83 ++++++++++++--------------------------------- 1 file changed, 22 insertions(+), 61 deletions(-) diff --git a/src/views/index.vue b/src/views/index.vue index 0ac2b36..3074507 100644 --- a/src/views/index.vue +++ b/src/views/index.vue @@ -38,7 +38,7 @@

近 7 天告警趋势

- 后续接入 ECharts 后可替换为真实折线 + 根据统计接口实时展示
@@ -76,7 +76,7 @@ 在线率 状态
-
+
{{ item.name }} {{ item.warehouseName }} {{ item.onlineRate }} @@ -163,55 +163,32 @@ import { } from '@/api/worn/home' const overview = reactive({ - projectTotal: 12, - warehouseTotal: 86, - deviceTotal: 342, - onlineCount: 328, - offlineCount: 14, - onlineRate: 95.9, - alarmToday: 23, - alarmUnhandled: 5 + projectTotal: 0, + warehouseTotal: 0, + deviceTotal: 0, + onlineCount: 0, + offlineCount: 0, + onlineRate: 0, + alarmToday: 0, + alarmUnhandled: 0 }) const exceptionStats = reactive({ - tempException: 8, - humidityException: 5, - gasOverCount: 12, - smokeAlarmCount: 3, - waterAlarmCount: 2, - total: 30 + total: 0 }) const realtimeAlarms = ref([]) -const projectList = ref([ - { name: '唐山项目', warehouseCount: 12, onlineRate: '98.2%', status: 'normal' }, - { name: '天津项目', warehouseCount: 8, onlineRate: '95.0%', status: 'warning' }, - { name: '北京项目', warehouseCount: 15, onlineRate: '100%', status: 'normal' }, - { name: '上海项目', warehouseCount: 10, onlineRate: '97.5%', status: 'normal' }, - { name: '宁德项目', warehouseCount: 7, onlineRate: '96.4%', status: 'normal' }, - { name: '苏州项目', warehouseCount: 9, onlineRate: '94.8%', status: 'warning' }, - { name: '广州项目', warehouseCount: 11, onlineRate: '99.1%', status: 'normal' }, - { name: '深圳项目', warehouseCount: 6, onlineRate: '93.7%', status: 'warning' } -]) +const projectList = ref([]) const realtimeEvents = ref([]) const recentEventKeys = new Map() const projectPage = ref(1) -const projectTotal = ref(0) const DUPLICATE_EVENT_WINDOW = 3000 const PROJECT_PAGE_SIZE = 5 -const alarmTrend = ref([ - { day: 'Mon', value: 36 }, - { day: 'Tue', value: 58 }, - { day: 'Wed', value: 44 }, - { day: 'Thu', value: 72 }, - { day: 'Fri', value: 50 }, - { day: 'Sat', value: 64 }, - { day: 'Sun', value: 42 } -]) +const alarmTrend = ref([]) const overviewCards = computed(() => [ { key: 'project', icon: 'P', label: '项目总数', value: overview.projectTotal, theme: '' }, @@ -252,7 +229,6 @@ function handleWsMessage(data) { overview.alarmToday += 1 overview.alarmUnhandled += 1 exceptionStats.total += 1 - updateExceptionStats(data) } } @@ -313,7 +289,6 @@ function loadAlarmType() { function loadProjectPage() { getHomeProjectList().then((res) => { const list = Array.isArray(res.data) ? res.data : [] - projectTotal.value = list.length projectList.value = list.map(normalizeProject).sort(sortProjectWarehouse) if (projectPage.value > projectPageTotal.value) { projectPage.value = projectPageTotal.value @@ -330,23 +305,27 @@ function changeProjectPage(step) { projectPage.value = nextPage } -function normalizeProject(item) { +function normalizeProject(item, index) { const onlineRate = toNumber(item.onlineRate, 0) + const name = String(item.parentName || item.projectName || item.name || '未命名项目').trim() + const warehouseName = String(item.warehouseName || item.deptName || '-').trim() + return { - name: item.parentName || item.projectName || item.name || '未命名项目', - warehouseName: item.warehouseName || item.deptName || '-', + key: `${name}-${warehouseName}-${index}`, + name, + warehouseName, onlineRate: `${onlineRate}%`, status: onlineRate >= 95 ? 'normal' : 'warning' } } function sortProjectWarehouse(a, b) { - const projectCompare = String(a.name).localeCompare(String(b.name), 'zh-CN') + const projectCompare = String(a.name).localeCompare(String(b.name), 'zh-Hans-CN', { numeric: true, sensitivity: 'base' }) if (projectCompare !== 0) { return projectCompare } - return String(a.warehouseName).localeCompare(String(b.warehouseName), 'zh-CN') + return String(a.warehouseName).localeCompare(String(b.warehouseName), 'zh-Hans-CN', { numeric: true, sensitivity: 'base' }) } function toNumber(value, fallback = 0) { @@ -604,24 +583,6 @@ function getTypeName(type) { return typeMap[type] || '数据上报' } -function updateExceptionStats(data) { - if (data?.type === 'env') { - if (data.temperature !== undefined || data.humidity !== undefined) { - exceptionStats.tempException += data.temperature !== undefined ? 1 : 0 - exceptionStats.humidityException += data.humidity !== undefined ? 1 : 0 - } - if (data.nh3 !== undefined || data.h2s !== undefined) { - exceptionStats.gasOverCount += 1 - } - } - if (data?.type === 'smoke') { - exceptionStats.smokeAlarmCount += 1 - } - if (data?.type === 'water' || data?.eventType === 'water') { - exceptionStats.waterAlarmCount += 1 - } -} - function pushRealtimeEvent(event) { realtimeEvents.value.unshift(event) realtimeEvents.value = realtimeEvents.value.slice(0, STREAM_TOTAL_SIZE) From 538b69583059ee95728d0cd4ff8024321c930f6d Mon Sep 17 00:00:00 2001 From: wenshijun Date: Wed, 15 Apr 2026 14:23:34 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E4=BB=93=E5=BA=93=E4=BC=A0=E6=84=9F?= =?UTF-8?q?=E5=99=A8=E8=AF=A6=E6=83=85=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/router/index.js | 13 + src/views/index.vue | 198 ++++- src/views/worn/warehouseDashboard/index.vue | 891 ++++++++++++++++++++ 3 files changed, 1096 insertions(+), 6 deletions(-) create mode 100644 src/views/worn/warehouseDashboard/index.vue diff --git a/src/router/index.js b/src/router/index.js index 07302ad..ebd78d9 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -83,6 +83,19 @@ export const constantRoutes = [ meta: { title: '个人中心', icon: 'user' } } ] + }, + { + path: '/worn/warehouse-dashboard', + component: Layout, + hidden: true, + children: [ + { + path: '', + component: () => import('@/views/worn/warehouseDashboard/index'), + name: 'WarehouseDashboard', + meta: { title: '仓库传感器看板', activeMenu: '/index' } + } + ] } ] diff --git a/src/views/index.vue b/src/views/index.vue index 3074507..b5245ff 100644 --- a/src/views/index.vue +++ b/src/views/index.vue @@ -13,6 +13,14 @@
+
+
+ WebSocket + 实时消息 +

{{ tickerMessage }}

+
+
+
{{ item.icon }}
@@ -76,7 +84,12 @@ 在线率 状态
-
+ @@ -1032,13 +1049,36 @@ onBeforeUnmount(() => { } .donut-card { - display: grid; - place-items: center; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; min-height: 184px; + padding: 20px 18px; + gap: 18px; +} + +.donut-head { + display: flex; + flex-direction: column; + gap: 4px; + text-align: left; + + strong { + font-size: 14px; + color: #0f172a; + } + + span { + font-size: 12px; + color: #64748b; + } } .donut { + position: relative; display: grid; + flex: 0 0 110px; width: 110px; height: 110px; place-items: center; @@ -1062,17 +1102,90 @@ onBeforeUnmount(() => { } strong { - font-size: 24px; + font-size: 28px; + font-weight: 900; color: #0f172a; } + .donut-total-tag { + position: relative; + z-index: 1; + margin-top: 2px; + font-size: 10px; + font-style: normal; + font-weight: 700; + color: #94a3b8; + } + span { - margin-top: 24px; + margin-top: 4px; font-size: 11px; + font-weight: 700; color: #64748b; } } +.donut-info { + display: flex; + flex: 1; + flex-direction: column; + justify-content: center; + gap: 12px; + min-width: 0; +} + +.donut-legend { + display: grid; + gap: 10px; +} + +.legend-item { + display: flex; + align-items: center; + gap: 8px; + min-height: 34px; + padding: 8px 10px; + font-size: 12px; + color: #334155; + background: rgba(255, 255, 255, 0.72); + border: 1px solid rgba(226, 232, 240, 0.82); + border-radius: 12px; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.86); + + i { + width: 10px; + height: 10px; + border-radius: 50%; + } + + &.danger i { + background: #ef4444; + } + + &.warning i { + background: #f59e0b; + } + + &.normal i { + background: #22c55e; + } +} + +@media (max-width: 1200px) { + .donut-card { + flex-direction: column; + align-items: center; + } + + .donut-head { + text-align: center; + } + + .donut-info { + width: 100%; + } +} + .alarm-list { flex: 1; min-height: 118px; diff --git a/src/views/worn/warehouseDashboard/index.vue b/src/views/worn/warehouseDashboard/index.vue index 872f120..e6d8e2a 100644 --- a/src/views/worn/warehouseDashboard/index.vue +++ b/src/views/worn/warehouseDashboard/index.vue @@ -57,6 +57,24 @@

{{ item.locationName }}

{{ item.typeName }}

{{ item.summary }} +
+ + +
@@ -87,8 +105,11 @@