Files
web-Iot/src/components/devices/deviceController.js

347 lines
8.4 KiB
JavaScript
Raw Normal View History

2026-05-20 15:12:51 +08:00
import throttle from 'lodash/throttle';
import { wsManager } from '../WebSocketManager.ts';
2026-05-20 15:12:51 +08:00
import {
getControlRight,
getJurisdiction,
} from '../../api/device.ts';
import envConfig from '../../../env';
export default class DeviceController {
constructor(userInfo, messageApi) {
this.userInfo = userInfo;
this.messageApi = messageApi;
2026-05-20 15:12:51 +08:00
this.wsUrl =
envConfig.WS_URL || 'wss://ri.satabot.com/ws/';
this.connectedDeviceSn = null;
this.currentDevice = null; // 保存当前设备
2026-05-20 15:12:51 +08:00
this.requestDeviceId = '';
2026-05-20 15:12:51 +08:00
this.isControlled = false;
this.isRequestControl = false;
this.hasControlRight = false;
2026-05-20 15:12:51 +08:00
this.wsStatus = 'disconnected';
2026-05-20 15:12:51 +08:00
this.wsDeviceInfo = {};
this.controlTimeout = null;
this.keepAliveInterval = null; // 后台保活定时器
this.onDeviceInfoUpdate = null;
this.onControlStatusChange = null;
this.onWsStatusChange = null;
2026-05-20 15:12:51 +08:00
this.onHasControlRightChange = null;
this.onRequestControlStatusChange = null;
this.onPermissionRequest = null;
this.onIsFinished = null;
this.handleArrivedPoint = null;
this.handleWsMessage = this.handleWsMessage.bind(this);
2026-05-20 15:12:51 +08:00
this.handleWsStatus = this.handleWsStatus.bind(this);
this.handleVisibilityChange = this.handleVisibilityChange.bind(this);
2026-05-20 15:12:51 +08:00
this.sendGamepad = throttle(
this.sendGamepad.bind(this),
50,
{ leading: true, trailing: true }
2026-05-20 15:12:51 +08:00
);
}
init() {
this.unSubMessage = wsManager.onMessage(this.handleWsMessage);
this.unSubStatus = wsManager.onStatusChange(this.handleWsStatus);
2026-05-20 15:12:51 +08:00
// ========== 【集成】页面可见性监听 ==========
document.addEventListener('visibilitychange', this.handleVisibilityChange);
// ========== 【集成】后台保活定时 ==========
//this.startKeepAlive();
}
// ========== 【集成】页面切前台重连 ==========
handleVisibilityChange() {
// 切后台:关闭 WS
if (document.visibilityState === 'hidden') {
console.log('🔌 页面切后台,关闭 WebSocket');
2026-06-04 15:39:46 +08:00
this.release();
wsManager.close();
2026-06-04 15:39:46 +08:00
this.connectedDeviceSn = null;
this.isControlled = false;
this.onControlStatusChange?.(false);
}
if (document.visibilityState === 'visible' && this.currentDevice) {
if (!this.currentDevice?.serialNumber) return;
console.log('✅ 页面切回前台,自动重连 WebSocket');
setTimeout(() => {
this.connectDevice(this.currentDevice);
}, 500);
}
}
// ========== 【集成】后台保活 ==========
startKeepAlive() {
this.keepAliveInterval = setInterval(() => {
if (
this.connectedDeviceSn &&
this.wsStatus !== 'connected' &&
this.currentDevice?.serialNumber
) {
console.log('🔄 后台保活:尝试重连 WebSocket');
this.connectDevice(this.currentDevice);
}
}, 8000);
}
2026-06-04 15:39:46 +08:00
release() {
if (this.isControlled && this.connectedDeviceSn) {
const data = {
platform: "web",
type: 1
};
const url = `${envConfig.baseURL}/forward/device/releaseControl`;
try {
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
navigator.sendBeacon(url, blob);
} catch (e) {
console.error('Failed to send releaseControl beacon:', e);
}
}
}
destroy() {
2026-05-20 15:12:51 +08:00
this.clearControlTimeout();
// 清理保活
if (this.keepAliveInterval) {
clearInterval(this.keepAliveInterval);
this.keepAliveInterval = null;
}
// 移除监听
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
2026-05-20 15:12:51 +08:00
this.unSubMessage?.();
this.unSubStatus?.();
2026-06-04 15:39:46 +08:00
this.release();
2026-05-20 15:12:51 +08:00
//wsManager.close();
2026-06-04 15:39:46 +08:00
this.connectedDeviceSn = null;
}
2026-05-20 15:12:51 +08:00
handleWsStatus(status) {
this.wsStatus = status;
this.onWsStatusChange?.(status);
if (status === 'disconnected') {
this.isControlled = false;
2026-05-20 15:12:51 +08:00
this.onControlStatusChange?.(false);
}
}
async connectDevice(device) {
if (!device) return;
// 保存当前设备,用于保活/重连
this.currentDevice = device;
if (!device.serialNumber) return;
2026-05-20 15:12:51 +08:00
const sn = device.serialNumber;
this.connectedDeviceSn = null;
this.wsDeviceInfo = {};
2026-05-20 15:12:51 +08:00
this.isControlled = false;
this.onControlStatusChange?.(false);
//wsManager.close();
2026-05-20 15:12:51 +08:00
try {
const res = await getControlRight({
userName: this.userInfo.username,
sourceType: 1,
token: this.userInfo.token,
});
this.hasControlRight = res?.code === 200 && res.data == true;
this.onHasControlRightChange?.(this.hasControlRight);
2026-05-20 15:12:51 +08:00
wsManager.connect(sn, this.wsUrl);
this.connectedDeviceSn = sn;
2026-05-20 15:12:51 +08:00
if (!this.hasControlRight) {
this.messageApi.warning('其他端正在控制');
2026-05-20 15:12:51 +08:00
return;
}
2026-05-20 15:12:51 +08:00
2026-06-04 15:39:46 +08:00
2026-05-20 15:12:51 +08:00
await this.getControl(sn);
} catch (err) {
2026-05-20 15:12:51 +08:00
console.error(err);
this.messageApi.error('权限校验失败');
}
}
2026-05-20 15:12:51 +08:00
async getControl(serialnum) {
try {
const res = await getJurisdiction({
platform: 'web',
deviceId: serialnum,
});
this.isControlled = res?.code === 200 && res?.data?.remoteControl === true;
this.onControlStatusChange?.(this.isControlled);
2026-05-20 15:12:51 +08:00
} catch (err) {
console.error(err);
this.isControlled = false;
this.onControlStatusChange?.(false);
}
}
requestControl() {
if (this.isRequestControl) return;
2026-05-20 15:12:51 +08:00
if (!this.hasControlRight) {
this.messageApi.warning('无控制权限');
return;
}
this.isRequestControl = true;
2026-05-20 15:12:51 +08:00
this.onRequestControlStatusChange?.(true);
this.messageApi.loading({
key: 'control',
2026-05-20 15:12:51 +08:00
content: '请求控制中...',
duration: 0,
});
2026-05-20 15:12:51 +08:00
this.sendPermissionRequest(true, 'switchPermission');
2026-05-20 15:12:51 +08:00
this.clearControlTimeout();
this.controlTimeout = setTimeout(() => {
if (!this.isRequestControl) return;
this.isRequestControl = false;
this.onRequestControlStatusChange?.(false);
this.messageApi.destroy('control');
this.messageApi.error('控制请求超时');
this.clearControlTimeout();
}, 20000);
2026-05-20 15:12:51 +08:00
}
clearControlTimeout() {
if (this.controlTimeout) {
clearTimeout(this.controlTimeout);
this.controlTimeout = null;
}
}
sendPermissionRequest(data, type) {
wsManager.send({
type,
switchResult: data,
token: this.userInfo.token,
deviceId: type === 'switchPermission' ? this.connectedDeviceSn : this.requestDeviceId,
2026-05-20 15:12:51 +08:00
userName: this.userInfo.username,
});
}
handleWsMessage(msg) {
2026-06-04 15:39:46 +08:00
console.log('📨 收到消息:', msg);
switch (msg.type) {
2026-05-20 15:12:51 +08:00
case 'deviceInfo': {
const data = {};
2026-05-20 15:12:51 +08:00
msg.data.forEach((item) => {
data[item.name] = item.value;
});
this.wsDeviceInfo = data;
2026-05-20 15:12:51 +08:00
this.onDeviceInfoUpdate?.(data);
break;
2026-05-20 15:12:51 +08:00
}
case 'AUTH': {
if (msg.isAuth) {
this.getControl(this.connectedDeviceSn);
}
break;
2026-05-20 15:12:51 +08:00
}
case 'switchResult': {
2026-05-20 15:12:51 +08:00
this.clearControlTimeout();
this.isRequestControl = false;
this.onRequestControlStatusChange?.(false);
if (msg.success === false || msg?.respond?.switchResult === false) {
2026-05-20 15:12:51 +08:00
this.isControlled = false;
this.onControlStatusChange?.(false);
this.messageApi.error({ key: 'control', content: msg.message || '控制失败' });
2026-05-20 15:12:51 +08:00
return;
}
this.isControlled = true;
this.onControlStatusChange?.(true);
this.messageApi.success({ key: 'control', content: '控制成功' });
break;
2026-05-20 15:12:51 +08:00
}
case "device_task_change": {
if (msg.deviceId != this.connectedDeviceSn) return;
this.onIsFinished?.(true);
console.log('🔌 控制结果任务变更,', msg);
}
//完成点
case "device_task_arrive_point": {
if (msg.deviceId != this.connectedDeviceSn) return;
this.handleArrivedPoint?.(msg.entity);
console.log('🔌 到达完成点,', msg);
}
2026-05-20 15:12:51 +08:00
case 'switchPermission': {
const platform = (msg.platform || '').toLowerCase();
if (!platform || platform === 'null') return;
2026-05-20 15:12:51 +08:00
this.requestDeviceId = msg.deviceId;
this.onPermissionRequest?.({
title: '权限请求',
content: `${msg.platform} 请求控制设备`,
});
break;
2026-05-20 15:12:51 +08:00
}
case 'detectionReport':
if (msg.data?.deviceId !== this.connectedDeviceSn) return;
const { isSecure, obstacle } = msg.data;
if (isSecure) return;
const type = obstacle?.type;
const distance = (obstacle?.distance || 0).toFixed(2);
const types = { 0: '未知', 1: '人', 2: '车辆', 3: '围栏', 4: '坑洞', 5: '草丛' };
this.messageApi.warning({
key: 'obstacle-warning',
content: `检测到 ${types[type] ?? '未知物'},距离 ${distance}m`,
duration: 3,
});
break;
2026-05-20 15:12:51 +08:00
default:
break;
}
}
sendGamepad(state) {
if (!this.isControlled || !this.connectedDeviceSn) return;
wsManager.send({
2026-05-20 15:12:51 +08:00
type: 'remoteControl',
2026-05-21 09:47:24 +08:00
remoteData: state,
token: this.userInfo.token,
userName: this.userInfo.username,
});
}
}