Browse Source

看板websocket

master
han\hanst 3 months ago
parent
commit
f8fe10cc86
  1. 2
      package.json
  2. 2
      src/router/index.js
  3. 219
      src/utils/websocket.js
  4. 1370
      src/views/modules/dashboard/inventory-board.vue
  5. 97
      src/views/modules/dashboard/robot-picking-board.vue
  6. 192
      src/views/modules/dashboard/slitting-board.vue
  7. 1882
      src/views/modules/dashboard/warehouse-3d-board.vue

2
package.json

@ -31,6 +31,8 @@
"npm": "^6.9.0", "npm": "^6.9.0",
"pubsub-js": "^1.9.3", "pubsub-js": "^1.9.3",
"sass-loader": "6.0.6", "sass-loader": "6.0.6",
"sockjs-client": "1.6.1",
"stompjs": "2.3.3",
"svg-sprite-loader": "3.7.3", "svg-sprite-loader": "3.7.3",
"vue": "2.5.16", "vue": "2.5.16",
"vue-cookie": "1.1.4", "vue-cookie": "1.1.4",

2
src/router/index.js

@ -28,7 +28,7 @@ const globalRoutes = [
{ path: '/dashboard-buffer-board', component: _import('modules/dashboard/buffer-board'), name: 'dashboard-buffer-board', meta: { title: '缓存区看板' } }, { path: '/dashboard-buffer-board', component: _import('modules/dashboard/buffer-board'), name: 'dashboard-buffer-board', meta: { title: '缓存区看板' } },
{ path: '/dashboard-workshop-feeding-board', component: _import('modules/dashboard/workshop-feeding-board'), name: 'dashboard-workshop-feeding-board', meta: { title: '车间AGV放料区看板' } }, { path: '/dashboard-workshop-feeding-board', component: _import('modules/dashboard/workshop-feeding-board'), name: 'dashboard-workshop-feeding-board', meta: { title: '车间AGV放料区看板' } },
{ path: '/dashboard-exception-board', component: _import('modules/dashboard/exception-board'), name: 'dashboard-exception-board', meta: { title: '异常处理区看板' } }, { path: '/dashboard-exception-board', component: _import('modules/dashboard/exception-board'), name: 'dashboard-exception-board', meta: { title: '异常处理区看板' } },
{ path: '/dashboard-master-board', component: _import('modules/dashboard/inventory-board'), name: 'dashboard-inventory-board', meta: { title: '库存分析看板' } }
{ path: '/dashboard-master-board', component: _import('modules/dashboard/warehouse-3d-board'), name: 'dashboard-inventory-board', meta: { title: '库存分析看板' } }
] ]
// 主入口路由(需嵌套上左右整体布局) // 主入口路由(需嵌套上左右整体布局)

219
src/utils/websocket.js

@ -0,0 +1,219 @@
/**
* WebSocket 工具类
*
* 功能说明
* 1. 封装WebSocket连接管理
* 2. 自动重连机制
* 3. 心跳检测
* 4. 订阅管理
*
* 使用示例
* import WebSocketClient from '@/utils/websocket'
*
* // 连接
* WebSocketClient.connect('http://localhost:8080/ws/dashboard', () => {
* console.log('连接成功')
* // 订阅主题
* WebSocketClient.subscribe('/topic/dashboard/robot-picking', (data) => {
* console.log('收到数据:', data)
* })
* })
*/
import SockJS from 'sockjs-client'
import Stomp from 'stompjs'
class WebSocketClient {
constructor() {
this.stompClient = null
this.connected = false
this.reconnectAttempts = 0
this.maxReconnectAttempts = 20
this.reconnectInterval = 3000
this.subscriptions = new Map()
this.heartbeatInterval = null
this.url = null
this.onConnectedCallback = null
}
/**
* 连接WebSocket服务器
*
* @param {string} url WebSocket服务器地址
* @param {function} onConnected 连接成功回调
* @param {function} onError 连接错误回调
*/
connect(url, onConnected, onError) {
this.url = url
this.onConnectedCallback = onConnected
console.log('[WebSocket] 正在连接服务器...', url)
try {
const socket = new SockJS(url)
this.stompClient = Stomp.over(socket)
// 禁用调试日志(生产环境)
// 开发环境可以设置为 console.log
this.stompClient.debug = null
this.stompClient.connect(
{},
frame => {
console.log('[WebSocket] ✅ 连接成功')
this.connected = true
this.reconnectAttempts = 0
this.startHeartbeat()
if (onConnected) {
onConnected(frame)
}
},
error => {
console.error('[WebSocket] ❌ 连接失败:', error)
this.connected = false
this.handleDisconnect()
if (onError) {
onError(error)
}
}
)
} catch (error) {
console.error('[WebSocket] 连接异常:', error)
if (onError) {
onError(error)
}
}
}
/**
* 订阅主题
*
* @param {string} topic 主题名称
* @param {function} callback 消息回调函数
* @returns {string} 订阅ID
*/
subscribe(topic, callback) {
if (!this.connected || !this.stompClient) {
console.warn('[WebSocket] 未连接,无法订阅主题:', topic)
return null
}
console.log('[WebSocket] 订阅主题:', topic)
const subscription = this.stompClient.subscribe(topic, message => {
try {
const data = JSON.parse(message.body)
callback(data)
} catch (error) {
console.error('[WebSocket] 解析消息失败:', error)
}
})
const subscriptionId = topic
this.subscriptions.set(subscriptionId, subscription)
return subscriptionId
}
/**
* 取消订阅
*
* @param {string} subscriptionId 订阅ID
*/
unsubscribe(subscriptionId) {
const subscription = this.subscriptions.get(subscriptionId)
if (subscription) {
subscription.unsubscribe()
this.subscriptions.delete(subscriptionId)
console.log('[WebSocket] 取消订阅:', subscriptionId)
}
}
/**
* 发送消息
*
* @param {string} destination 目标地址
* @param {object} data 消息数据
*/
send(destination, data) {
if (!this.connected || !this.stompClient) {
console.warn('[WebSocket] 未连接,无法发送消息')
return
}
this.stompClient.send(destination, {}, JSON.stringify(data))
}
/**
* 断开连接
*/
disconnect() {
if (this.stompClient) {
this.stompClient.disconnect(() => {
console.log('[WebSocket] 已断开连接')
})
this.connected = false
this.stopHeartbeat()
}
}
/**
* 处理断开连接
*/
handleDisconnect() {
this.connected = false
this.stopHeartbeat()
// 自动重连
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++
console.log(`[WebSocket] 尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`)
setTimeout(() => {
if (this.url && this.onConnectedCallback) {
this.connect(this.url, this.onConnectedCallback)
}
}, this.reconnectInterval)
} else {
console.error('[WebSocket] 达到最大重连次数,放弃重连')
}
}
/**
* 启动心跳检测
*/
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
if (this.connected && this.stompClient) {
// 发送心跳消息
// 注意:如果后端没有心跳接口,可以注释掉这行
// this.send('/app/heartbeat', { timestamp: Date.now() })
}
}, 30000) // 每30秒发送一次心跳
}
/**
* 停止心跳检测
*/
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval)
this.heartbeatInterval = null
}
}
/**
* 检查连接状态
*
* @returns {boolean} 是否已连接
*/
isConnected() {
return this.connected
}
}
// 导出单例
export default new WebSocketClient()

1370
src/views/modules/dashboard/inventory-board.vue
File diff suppressed because it is too large
View File

97
src/views/modules/dashboard/robot-picking-board.vue

@ -131,6 +131,7 @@
<script> <script>
import {robotPicking} from '@/api/dashboard/dashboard.js' import {robotPicking} from '@/api/dashboard/dashboard.js'
import WebSocketClient from '@/utils/websocket'
export default { export default {
name: 'RobotPickingBoard', name: 'RobotPickingBoard',
@ -139,42 +140,41 @@ export default {
return { return {
currentTime: '', currentTime: '',
timeInterval: null, timeInterval: null,
dataInterval: null,
//
containerPickingList: [
// WebSocket
useWebSocket: true, // 使WebSocketfalse
wsConnected: false, // WebSocket
wsSubscription: null, // WebSocketID
],
//
containerPickingList: [],
// //
materialPickingList: [
]
materialPickingList: []
} }
}, },
mounted() { mounted() {
//
this.updateTime() this.updateTime()
this.timeInterval = setInterval(() => { this.timeInterval = setInterval(() => {
this.updateTime() this.updateTime()
}, 1000) }, 1000)
//
this.getDataList()
// 10
this.dataInterval = setInterval(() => {
this.getDataList()
}, 10000)
// 使WebSocket
if (this.useWebSocket) {
this.initWebSocket()
}
}, },
beforeDestroy() { beforeDestroy() {
//
if (this.timeInterval) { if (this.timeInterval) {
clearInterval(this.timeInterval) clearInterval(this.timeInterval)
} }
if (this.dataInterval) {
clearInterval(this.dataInterval)
}
// WebSocket
this.disconnectWebSocket()
}, },
methods: { methods: {
@ -246,6 +246,69 @@ export default {
'等待分拣': 'status-pending' '等待分拣': 'status-pending'
} }
return statusMap[status] || 'status-pending' return statusMap[status] || 'status-pending'
},
/**
* 初始化WebSocket连接
*/
initWebSocket() {
var apiServer = (process.env.NODE_ENV !== 'production' && process.env.OPEN_PROXY ? '/proxyApi/' : window.SITE_CONFIG.baseUrl);
// WebSocket
const wsUrl = apiServer + 'ws/dashboard'
WebSocketClient.connect(
wsUrl,
() => {
this.wsConnected = true
//
this.wsSubscription = WebSocketClient.subscribe(
'/topic/dashboard/robot-picking',
this.handleWebSocketMessage
)
},
(error) => {
//
console.error('[机械臂拣选看板] WebSocket连接失败,降级到轮询模式', error)
this.wsConnected = false
this.fallbackToPolling()
}
)
},
/**
* 处理WebSocket推送的消息
*
* @param {object} message WebSocket推送的消息
*/
handleWebSocketMessage(message) {
if (message && message.code === 0) {
this.containerPickingList = message.data.containerList
this.materialPickingList = message.data.materialList
}
},
/**
* 断开WebSocket连接
*/
disconnectWebSocket() {
if (this.wsSubscription) {
WebSocketClient.unsubscribe(this.wsSubscription)
this.wsSubscription = null
}
if (this.wsConnected) {
WebSocketClient.disconnect()
this.wsConnected = false
console.log('[机械臂拣选看板] WebSocket已断开')
}
},
/**
* 降级到轮询模式
*/
fallbackToPolling() {
this.getDataList()
} }
} }
} }

192
src/views/modules/dashboard/slitting-board.vue

@ -63,7 +63,7 @@
<td class="text-center">{{ item.storageLocation }}</td> <td class="text-center">{{ item.storageLocation }}</td>
<td class="text-center">{{ item.palletCode }}</td> <td class="text-center">{{ item.palletCode }}</td>
<td class="text-center">{{ item.pickingLocation }}</td> <td class="text-center">{{ item.pickingLocation }}</td>
<td class="text-left">{{ item.materialName }}</td>
<td class="text-center">{{ item.materialName }}</td>
<td class="text-right">{{ item.quantity }}</td> <td class="text-right">{{ item.quantity }}</td>
<td class="text-center"> <td class="text-center">
<span :class="['status-badge', getStatusClass(item.status)]"> <span :class="['status-badge', getStatusClass(item.status)]">
@ -127,6 +127,7 @@
<script> <script>
import {slittingBoard} from '@/api/dashboard/dashboard.js' import {slittingBoard} from '@/api/dashboard/dashboard.js'
import WebSocketClient from '@/utils/websocket'
export default { export default {
name: 'SlittingBoard', name: 'SlittingBoard',
@ -135,121 +136,75 @@ export default {
return { return {
currentTime: '', currentTime: '',
timeInterval: null, timeInterval: null,
dataInterval: null,
// WebSocket
useWebSocket: true, // 使WebSocketfalse
wsConnected: false, // WebSocket
wsSubscription: null, // WebSocketID
// //
assistArmList: [
{
storageLocation: 'FQ01',
palletCode: 'G00100',
pickingLocation: '1',
materialName: '70000213',
quantity: '400',
status: '已到达'
},
{
storageLocation: 'FQ01',
palletCode: 'G00100',
pickingLocation: '2',
materialName: '70000235',
quantity: '203',
status: '已到达'
},
{
storageLocation: 'FQ01',
palletCode: 'G00100',
pickingLocation: '3',
materialName: '70000237',
quantity: '500',
status: '已到达'
},
{
storageLocation: 'FQ01',
palletCode: 'G00100',
pickingLocation: '4',
materialName: '70002546',
quantity: '500',
status: '已到达'
},
{
storageLocation: 'FQ02',
palletCode: 'G00200',
pickingLocation: '1',
materialName: '70000033',
quantity: '500',
status: '已到达'
},
{
storageLocation: 'FQ02',
palletCode: 'G00200',
pickingLocation: '2',
materialName: '70000212',
quantity: '2000',
status: '已到达'
}
],
assistArmList: [],
// //
slittingInboundList: [
{
storageLocation: 'FQ05',
palletCode: 'W000001',
taskType: '入库',
status: 'AGV取货中'
},
{
storageLocation: 'FQ06',
palletCode: 'W000002',
taskType: '移库',
status: 'AGV运送中'
},
{
storageLocation: 'FQ07',
palletCode: 'G000001',
taskType: '分切退料',
status: '已组盘'
}
]
slittingInboundList: []
} }
}, },
mounted() { mounted() {
//
this.updateTime() this.updateTime()
this.timeInterval = setInterval(() => { this.timeInterval = setInterval(() => {
this.updateTime() this.updateTime()
}, 1000) }, 1000)
//
this.getDataList()
// 10
this.dataInterval = setInterval(() => {
this.getDataList()
}, 10000)
// 使WebSocket
if (this.useWebSocket) {
this.initWebSocket()
}
}, },
beforeDestroy() { beforeDestroy() {
//
if (this.timeInterval) { if (this.timeInterval) {
clearInterval(this.timeInterval) clearInterval(this.timeInterval)
} }
if (this.dataInterval) {
clearInterval(this.dataInterval)
}
// WebSocket
this.disconnectWebSocket()
}, },
methods: { methods: {
/** /**
* 获取数据列表 * 获取数据列表
*
* <p><b>功能说明</b>从后端API获取分切区看板实时数据</p>
* <p><b>数据更新策略</b>覆盖而非追加避免内存累积</p>
*/ */
getDataList() { getDataList() {
slittingBoard({}).then(({data}) => { slittingBoard({}).then(({data}) => {
if (data && data.code === 200) { if (data && data.code === 200) {
console.log('获取分切区数据成功:', data.data) console.log('获取分切区数据成功:', data.data)
// TODO:
// if (data.data) {
// this.assistArmList = data.data.assistArmList || []
// this.slittingInboundList = data.data.slittingInboundList || []
// }
//
if (data.data) {
//
if (data.data.assistArmList && data.data.assistArmList.length > 0) {
this.assistArmList = data.data.assistArmList
console.log('助力臂区数据已更新,共', this.assistArmList.length, '条')
} else {
console.log('暂无助力臂区数据')
}
//
if (data.data.slittingInboundList && data.data.slittingInboundList.length > 0) {
this.slittingInboundList = data.data.slittingInboundList
console.log('分切入库区数据已更新,共', this.slittingInboundList.length, '条')
} else {
console.log('暂无分切入库区数据')
}
}
} else {
console.error('获取分切区数据失败: 返回码不正确')
} }
}).catch(error => { }).catch(error => {
console.error('获取分切区数据失败:', error) console.error('获取分切区数据失败:', error)
@ -285,6 +240,69 @@ export default {
'等待': 'status-pending' '等待': 'status-pending'
} }
return statusMap[status] || 'status-pending' return statusMap[status] || 'status-pending'
},
/**
* 初始化WebSocket连接
*/
initWebSocket() {
var apiServer = (process.env.NODE_ENV !== 'production' && process.env.OPEN_PROXY ? '/proxyApi/' : window.SITE_CONFIG.baseUrl);
// WebSocket
const wsUrl = apiServer + 'ws/dashboard'
WebSocketClient.connect(
wsUrl,
() => {
this.wsConnected = true
//
this.wsSubscription = WebSocketClient.subscribe(
'/topic/dashboard/slitting-board',
this.handleWebSocketMessage
)
},
(error) => {
//
console.error('[分切区看板] WebSocket连接失败,降级到轮询模式', error)
this.wsConnected = false
this.fallbackToPolling()
}
)
},
/**
* 处理WebSocket推送的消息
*
* @param {object} message WebSocket推送的消息
*/
handleWebSocketMessage(message) {
if (message && message.code === 0) {
this.assistArmList = message.data.assistArmList || []
this.slittingInboundList = message.data.slittingInboundList || []
}
},
/**
* 断开WebSocket连接
*/
disconnectWebSocket() {
if (this.wsSubscription) {
WebSocketClient.unsubscribe(this.wsSubscription)
this.wsSubscription = null
}
if (this.wsConnected) {
WebSocketClient.disconnect()
this.wsConnected = false
console.log('[分切区看板] WebSocket已断开')
}
},
/**
* 降级到轮询模式
*/
fallbackToPolling() {
this.getDataList()
} }
} }
} }

1882
src/views/modules/dashboard/warehouse-3d-board.vue
File diff suppressed because it is too large
View File

Loading…
Cancel
Save