使用Vue.js和WebSocket打造实时库存仪表盘
大家好!今天我将分享一个简单却强大的实时库存仪表盘项目,基于Vue.js和WebSocket技术。这个项目适合初学者学习前端实时数据处理,也能为你的技术博客或作品集增添亮点!通过这个教程,你将学会如何使用WebSocket实现实时数据更新,创建用户友好的界面,并处理断线重连等实际场景。快来跟我一起动手打造吧!
项目简介
这个实时库存仪表盘展示了库存预警和总库存量,通过WebSocket从服务器获取实时数据。界面简洁直观,包含动态时间显示、库存数据列表和错误提示弹窗。整个项目使用Vue.js 2.6.14和纯HTML/CSS,无需复杂依赖,打开浏览器即可运行,适合快速上手和分享。
功能亮点
实时数据更新:通过WebSocket接收服务器推送的库存数据,动态更新界面。
心跳机制:每10秒发送心跳消息,确保连接稳定。
断线重连:连接断开后自动每3秒尝试重连,并显示友好提示。
简洁界面:包含实时时间、库存预警列表和总库存展示,易于扩展。
零依赖部署:通过CDN加载Vue.js,无需本地开发环境。
项目代码解析
以下是项目的核心代码(完整代码见文末)。我将重点讲解WebSocket的关键逻辑和实现细节。
1. HTML结构
仪表盘的HTML结构分为三部分:
头部:显示标题和实时时间。
内容区:展示库存预警列表和总库存量。
弹窗:在WebSocket连接失败时显示提示。
<div id="app" class="container"><div class="header"><h1>实时库存仪表盘</h1><span class="time">{{ currentTime }}</span></div><div class="content"><div class="data-box"><h2>库存预警</h2><div class="data-item" v-for="product in products" :key="product.id">{{ product.name }}: {{ product.quantity }} (万)</div></div><div class="data-box"><h2>库存总量</h2><div class="data-item">总库存: {{ totalStock }} 万</div></div></div><div class="popup" :class="{ show: isPopupVisible }"><p>数据连接失败,正在尝试重新连接...<br>请确保网络正常或联系技术人员。</p></div>
</div>
2. WebSocket核心逻辑
WebSocket是实现实时数据更新的关键。以下是Vue.js中与WebSocket相关的核心方法:
连接WebSocket:初始化WebSocket连接,发送初始消息,并启动心跳机制。
connectWebSocket() {this.ws = new WebSocket('ws://1.94.0.197:6061/websocket');this.ws.onopen = () => {console.log('WebSocket已连接');if (this.isReconnecting) {this.isPopupVisible = false;this.isReconnecting = false;}this.ws.send(JSON.stringify({ type: 'init' }));this.startHeartbeat();};// ... 其他事件处理
}
心跳机制:每10秒发送心跳消息,保持连接活跃。
startHeartbeat() {this.heartbeatInterval = setInterval(() => {if (this.ws && this.ws.readyState === WebSocket.OPEN) {this.ws.send(JSON.stringify({ type: 'heartbeat' }));}}, 10000);
}
断线重连:连接断开后,每3秒尝试重连,并显示弹窗提示。
reconnect() {this.isReconnecting = true;setTimeout(() => {console.log('尝试重连WebSocket...');this.connectWebSocket();}, 3000);
}
数据更新:接收服务器数据并更新界面。
updateData(data) {this.products = data.getLowStock || [];this.totalStock = data.getCount?.totalStock || 0;
}
3. CSS样式
样式设计简洁现代,使用了浅色背景和卡片式布局,确保界面清晰且美观:
容器:居中显示,带阴影和圆角。
弹窗:居中弹出,红色文字提示错误。
响应式:适配不同屏幕尺寸。
.container {max-width: 800px;margin: 0 auto;background: white;padding: 20px;border-radius: 8px;box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.popup {position: fixed;top: 50%;left: 50%;transform: translate(-50%, -50%);background: white;padding: 20px;border-radius: 8px;box-shadow: 0 4px 16px rgba(0,0,0,0.2);display: none;
}
.popup.show { display: block; }
4. Vue.js逻辑
数据绑定:使用Vue的响应式数据(如products和totalStock)动态更新界面。
生命周期管理:
mounted:启动时间更新和WebSocket连接。
beforeDestroy:清理定时器和WebSocket连接,避免内存泄漏。
时间显示:每秒更新当前时间,格式为“YYYY-MM-DD HH:mm:ss”。
updateTime() {const now = new Date();this.currentTime = now.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit',hour: '2-digit', minute: '2-digit', second: '2-digit'});
}
完整代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>WebSocket 实时库存仪表盘</title><script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script><style>body { font-family: Arial, sans-serif; background-color: #f0f2f5; margin: 0; padding: 20px; }.container { max-width: 800px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }.header { display: flex; justify-content: space-between; align-items: center; background: #2c3e50; color: white; padding: 10px 20px; border-radius: 8px 8px 0 0; }.header h1 { margin: 0; font-size: 24px; }.time { font-size: 16px; }.content { padding: 20px; }.data-box { margin-bottom: 20px; }.data-box h2 { margin: 0 0 10px; font-size: 18px; color: #34495e; }.data-item { padding: 10px; background: #ecf0f1; border-radius: 4px; margin: 5px 0; }.popup { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.2); display: none; }.popup.show { display: block; }.popup p { margin: 0; font-size: 16px; color: #e74c3c; text-align: center; }</style>
</head>
<body><div id="app" class="container"><div class="header"><h1>实时库存仪表盘</h1><span class="time">{{ currentTime }}</span></div><div class="content"><div class="data-box"><h2>库存预警</h2><div class="data-item" v-for="product in products" :key="product.id">{{ product.name }}: {{ product.quantity }} (万)</div></div><div class="data-box"><h2>库存总量</h2><div class="data-item">总库存: {{ totalStock }} 万</div></div></div><div class="popup" :class="{ show: isPopupVisible }"><p>数据连接失败,正在尝试重新连接...<br>请确保网络正常或联系技术人员。</p></div></div><script>new Vue({el: '#app',data() {return {products: [],totalStock: 0,currentTime: '',isPopupVisible: false,ws: null,isReconnecting: false,heartbeatInterval: null,timer: null};},mounted() {this.updateTime();this.timer = setInterval(this.updateTime, 1000);this.connectWebSocket();},beforeDestroy() {clearInterval(this.timer);clearInterval(this.heartbeatInterval);if (this.ws) this.ws.close();},methods: {updateTime() {const now = new Date();this.currentTime = now.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit',hour: '2-digit', minute: '2-digit', second: '2-digit'});},connectWebSocket() {this.ws = new WebSocket('ws://1.94.0.197:6061/websocket');this.ws.onopen = () => {console.log('WebSocket已连接');if (this.isReconnecting) {this.isPopupVisible = false;this.isReconnecting = false;}this.ws.send(JSON.stringify({ type: 'init' }));this.startHeartbeat();};this.ws.onmessage = (event) => {const data = JSON.parse(event.data);this.updateData(data);};this.ws.onerror = (error) => {console.error('WebSocket错误:', error);};this.ws.onclose = () => {console.log('WebSocket连接关闭');this.isPopupVisible = true;this.stopHeartbeat();if (!this.isReconnecting) {this.reconnect();}};},reconnect() {this.isReconnecting = true;setTimeout(() => {console.log('尝试重连WebSocket...');this.connectWebSocket();}, 3000);},startHeartbeat() {this.heartbeatInterval = setInterval(() => {if (this.ws && this.ws.readyState === WebSocket.OPEN) {this.ws.send(JSON.stringify({ type: 'heartbeat' }));}}, 10000);},stopHeartbeat() {clearInterval(this.heartbeatInterval);},updateData(data) {this.products = data.getLowStock || [];this.totalStock = data.getCount?.totalStock || 0;}}});</script>
</body>
</html>