Compare commits

...

4 Commits

12 changed files with 1629 additions and 222 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import {
Layout, Menu, Button, Space, Select, Avatar, Card, Tag
@@ -15,7 +15,7 @@ import logo from '../assets/logo.png';
import { useSelector } from 'react-redux';
import { RootState, useAppDispatch } from '../store';
import { logout } from '../store/userSlice';
import { setCurrentStation, clearStation } from '../store/stationSlice';
import { setCurrentStation, clearStation, setStationList as setStoreStationList } from '../store/stationSlice';
import { getMenuList } from '../api/mune';
import { getStationByUserId } from '../api/stationManage';
import LanguageSwitcher from './LanguageSwitcher';
@@ -36,13 +36,19 @@ export default function AppLayout() {
const dispatch = useAppDispatch();
const { userInfo } = useSelector((state: RootState) => state.user);
const { currentStation, stationId } = useSelector((state: RootState) => state.station);
// 从 store 中同时取出 stationList(优先级最高,保证最新)
const { currentStation, stationId, stationList: storeStationList } = useSelector((state: RootState) => state.station);
// 真正用于展示和选择的列表:优先用 store 中的(StationManage 编辑后会同步),否则用本地接口的
const mergedStationList = storeStationList?.length ? storeStationList : stationList;
const getStationData = () => {
getStationByUserId().then(res => {
if (res.code === 200) {
const list = res.data || [];
setStationList(list);
// 同步到全局 store,确保各组件共享同一份最新列表
dispatch(setStoreStationList(list));
if (!currentStation && list.length > 0) {
dispatch(setCurrentStation(list[0]));
}
@@ -63,7 +69,9 @@ export default function AppLayout() {
};
const handleStationChange = (id: number) => {
const station = stationList.find(s => s.id === id);
// 优先从合并后的列表中取(包含 store 中最新编辑的数据),其次从本地 stationList
const station = mergedStationList.find(s => Number(s.id) === Number(id))
|| stationList.find(s => Number(s.id) === Number(id));
if (station) {
dispatch(setCurrentStation(station));
}
@@ -206,7 +214,7 @@ export default function AppLayout() {
value={stationId}
placeholder={t('layout.selectStation')}
onChange={handleStationChange}
options={stationList.map(s => ({ label: s.siteName, value: s.id }))}
options={mergedStationList.map(s => ({ label: s.siteName, value: s.id }))}
style={{
width: 'clamp(160px, 15vw, 220px)',
backgroundColor: '#0837a8',

View File

@@ -609,9 +609,9 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
`;
} else {
tooltip.innerHTML = `
<div style="font-weight:600;margin-bottom:4px;">📍 ${t('systemSetting.longitude')}:${lonStr}</div>
<div style="font-weight:600;">${t('systemSetting.latitude')}:${latStr}</div>
`;
<div style="font-weight:600;margin-bottom:4px;">📍 ${t('systemSetting.longitude')}:${lonStr}</div>
<div style="font-weight:600;">${t('systemSetting.latitude')}:${latStr}</div>
`;
}
}
@@ -1665,16 +1665,17 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
handler.setInputAction((movement) => {
const picked = viewer.scene.pick(movement.endPosition);
const pickedRobot = (window.Cesium.defined(picked) && picked.id?.properties?.robot) ? picked.id.properties.robot.getValue() : null;
const { lon, lat } = getCursorLngLat(viewer, movement.endPosition);
if (window.Cesium.defined(lon) && Number.isFinite(lon)) {
// ✅ 只有拾取到设备实体,才显示tooltip,空白区域直接隐藏
if (pickedRobot) {
const { lon, lat } = getCursorLngLat(viewer, movement.endPosition);
showCoordinateTooltip(tooltipRef.current, movement.endPosition, t, lon, lat, pickedRobot);
} else if (pickedRobot) {
showCoordinateTooltip(tooltipRef.current, movement.endPosition, t, NaN, NaN, pickedRobot);
} else {
tooltipRef.current.style.display = "none";
}
}, window.Cesium.ScreenSpaceEventType.MOUSE_MOVE);
handler.setInputAction((movement) => {
const picked = viewer.scene.pick(movement.position);
if (window.Cesium.defined(picked) && picked.id?.properties?.robot) {

View File

@@ -15,7 +15,12 @@ import { getOrgList } from '../../api/organization';
import { fetchPublishedLayers } from '../../lib/geoServer';
import { useSelector } from 'react-redux';
import { useAppDispatch, RootState } from '../../store';
import { setCurrentStation as setStoreStation } from '../../store/stationSlice';
import {
setCurrentStation as setStoreStation,
setStationList,
updateStationInList,
removeStationFromList,
} from '../../store/stationSlice';
import { useTranslation } from 'react-i18next';
const { Option } = Select;
@@ -96,12 +101,15 @@ export default function StationManage() {
};
getStationList(params).then(res => {
if (res.code === 200) {
setStationList(res.rows || []);
const list = res.rows || [];
setStationList(list);
setPagination({
...pagination,
total: res.total || res.rows?.length || 0
});
dispatch(setStationList(list));
}
});
};
@@ -193,11 +201,18 @@ export default function StationManage() {
saveSite(params).then(res => {
if (res.code === 200) {
messageApi.success(editRecord ? t('common.editSuccess') : t('common.addSuccess'));
getStationData();
// 编辑的场站若正是当前选中场站,则同步更新到 store(尤其 siteMapName)
if (editRecord && storeStation && editRecord.id === storeStation.id) {
dispatch(setStoreStation({ ...storeStation, ...params }));
// ========== 关键修复:同步更新全局 store ==========
// 优先使用接口返回的最新对象(res.data),否则回退到提交参数
const latestStation = { ...(editRecord || {}), ...params };
if (editRecord) {
// 编辑:更新 stationList 中对应项(slice 内部会自动同步 currentStation)
dispatch(updateStationInList(latestStation));
}
// ===================================================
getStationData();
setModalVisible(false);
} else {
messageApi.error(res.msg || t('common.saveFail'));
@@ -211,6 +226,8 @@ export default function StationManage() {
deleteSite([id]).then(res => {
if (res.code === 200) {
messageApi.success(t('common.deleteSuccess'));
// 同步从全局 store 中移除
dispatch(removeStationFromList(id));
getStationData();
} else {
messageApi.error(res.msg || t('common.deleteFail'));

View File

@@ -647,8 +647,8 @@ export default function DeviceOverviewPage() {
<Card title={<><SettingOutlined />{t('devices.fixed2EquipmentListFixed2')}</>} variant="borderless" style={{ borderRadius: 8, marginBottom: 8 }}>
<Table rowKey={(record) => record.id || record.serialNumber || record.device_sn} size="small" pagination={false}
columns={[
{ title: t('devices.fixed2EquipmentFixed2NameFixed2'), dataIndex: 'name', width: 140 },
{ title: t('common.type'), dataIndex: 'type', width: 90 },
{ title: t('devices.fixed2EquipmentFixed2NameFixed2'), dataIndex: 'name', width: 180 },
{ title: t('common.type'), dataIndex: 'type', },
{
title: t('workOrder.currentStatus'),
dataIndex: 'onlineStatus',
@@ -659,15 +659,16 @@ export default function DeviceOverviewPage() {
return <Tag color={color} style={{ margin: 0 }}>{text}</Tag>;
}
}, {
title: t('devices.abMode'), dataIndex: 'mode', width: 70, render: (text) => {
let color = 'default';
if (text === '作业中') color = 'processing';
if (text === t('devices.idle')) color = 'success';
if (text === '空闲中') color = 'success';
if (text === '现场调试') color = 'warning';
if (text === '远程调试') color = 'warning';
if (text === '固件升级中') color = 'error';
return <Tag color={color} style={{ margin: 0 }}>{text}</Tag>;
title: t('workOrder.currentStatus'), dataIndex: 'feStatus', width: 70, render: (text) => {
let color;
if (text == '1') color = 'processing';
if (text == '2') color = 'success';
if (text == '3') color = 'default'; // default 就是灰色标签
return <Tag color={color} style={{ margin: 0 }}>
{text == '1' ? "工作中" : text == '2' ? "空闲中" : "离线"}
</Tag>;
}
},
{

View File

@@ -606,7 +606,6 @@ export default function DeviceStatusPage() {
longitude: lng,
latitude: lat,
},
// 让地图取最新经纬度
longitude: lng,
latitude: lat,
name: device?.deviceAlias || device?.callsign || device?.deviceName || t('devices.device'),

View File

@@ -271,7 +271,46 @@ export default function RobotTaskPage() {
controller.onLocationInfoUpdate = setRobotRealtimePosition;
controller.onIsFinished = handleFinish;
controller.handleArrivedPoint = handleFinishEdPoint;
controller.onRoutePost = setPoints;
// 处理路线点回调:taskId必须和当前选中任务一致
controller.onRoutePost = (msg) => {
console.log('收到 route_post 消息:', msg);
const { taskId, data } = msg;
// 1. 先在任务池中查找该taskId对应的任务
const matchedTask = taskPoolList.find(task => String(task.id) === String(taskId));
// 2. 两种情况处理:
// a) 当前没有选中任务,且匹配到了任务池中的任务 → 自动选中该任务
// b) 当前已选中任务,且选中任务的id === 传来的taskId → 更新数据
const isSameAsSelected = runningPoolTask && String(runningPoolTask.id) === String(taskId);
const shouldSelectNewTask = !runningPoolTask && matchedTask;
if (isSameAsSelected || shouldSelectNewTask) {
console.log('taskId匹配成功,处理路线数据', {
taskId,
runningPoolTaskId: runningPoolTask?.id,
matchedTask
});
// 如果是匹配到新任务就设置选中状态
if (shouldSelectNewTask) {
setRunningPoolTask(matchedTask);
}
// 先直接用推送的data设置点(实时性)
if (data) {
setPoints(data);
}
} else {
console.log('taskId不匹配,忽略本次推送', {
receivedTaskId: taskId,
currentSelectedTaskId: runningPoolTask?.id,
matchedTaskFound: !!matchedTask
});
}
};

View File

@@ -67,6 +67,16 @@ export default class DeviceController {
wsManager.close();
}
disconnect() {
console.log('DeviceController disconnect: 断开 WebSocket');
wsManager.close();
this.connectedDeviceSn = null;
this.currentDevice = null;
this.wsDeviceInfo = {};
this.locationInfo = {};
this.isControlled = false;
this.onControlStatusChange?.(false);
}
handleVisibilityChange() {
if (document.visibilityState === 'hidden') {
@@ -362,7 +372,7 @@ export default class DeviceController {
case 'route_post':
console.log('route post:', msg);
if (msg.deviceId) {
this.onRoutePost?.(msg.data);
this.onRoutePost?.(msg);
}
break;

View File

@@ -93,6 +93,10 @@ const en = {
requestError: 'Request Error',
settings: 'Settings',
storeValue: 'Store Value',
action: 'Action',
locate: 'Locate',
minutes: 'min',
totalCount: 'Total {{count}}',
submitFail: 'Submit Fail',
timeRange: 'Time Range',
working: 'Working',
@@ -170,6 +174,9 @@ const en = {
ambientTemperature: 'Ambient Temperature',
current: 'Current',
currentIrradiance: 'Current Irradiance',
charge: 'Battery',
model: 'Model',
ratPower: 'Rated Power',
frequency: 'Frequency',
gridConnected: 'Grid Connected',
gridConnection: 'Grid Connection',
@@ -371,6 +378,36 @@ const en = {
networkLatency: 'Network Latency',
waterRiskWarning: 'Warning: Water accumulation risk in Area B, avoid it',
presetRoutes: 'Preset Routes',
all: 'All',
stop: 'Stop',
currentTask: 'Current Task',
realtimePosition: 'Real-time Position',
taskCompleted: 'Task completed',
deviceSerialNumberMissing: 'Device serial number missing',
confirmExecuteRoute: 'Confirm to execute this route?',
targetDevice: 'Target Device',
routePointCount: 'Route points',
taskExecuteFailed: 'Task execution failed',
taskPauseAbnormal: 'Task pause abnormal',
areYouSureCancelTask: 'Are you sure to cancel the task?',
taskCancelled: 'Task cancelled',
taskCancelAbnormal: 'Failed to cancel the task',
taskStatusNew: 'Pending',
taskStatusExecuting: 'Executing',
taskStatusPause: 'Paused',
taskStatusFinish: 'Finished',
taskStatusCancel: 'Cancelled',
unnamedTask: 'Unnamed Task',
planId: 'Plan ID',
expectedComplete: 'Expected completion',
noCurrentTask: 'No current task',
searchRoute: 'Search route',
noRouteTip: 'No preset routes',
pointCount: 'Point count',
deviceInfo: 'Device Info',
deviceCode: 'Device Code',
siteBelonging: 'Site',
deviceLocation: 'Device location',
searchPlaceholder: 'Search...',
unnamedRoute: 'Unnamed Route',
deleteRoute: 'Delete Route',
@@ -3604,6 +3641,13 @@ const en = {
fixed2Offline3: ' Offline 3',
powerCurveFixed2: 'Real-time power curve',
viewDeviceDetailAndTask: 'View device detail and task',
showAllDevices: 'Show All Devices',
showOnlySelected: 'Show Selected Only',
clickDeviceOnMapTip: 'Click a device on the map to locate and view details',
focusingDevice: 'Locating device: {{name}}',
foundDevices: 'Found {{count}} devices',
gridNorm: 'Grid Normal',
s11: 'today',

View File

@@ -97,6 +97,10 @@ const zh = {
requestError: '请求错误',
settings: '设置',
storeValue: '存储值',
action: '操作',
locate: '定位',
minutes: '分钟',
totalCount: '共 {{count}} 台',
submitFail: '提交失败',
timeRange: '时间范围',
working: '作业中',
@@ -174,6 +178,9 @@ const zh = {
ambientTemperature: '环境温度',
current: '当前',
currentIrradiance: '当前辐照度',
charge: '电量',
model: '型号',
ratPower: '额定功率',
frequency: '频率',
gridConnected: '电网已连接',
gridConnection: '电网连接',
@@ -386,6 +393,36 @@ const zh = {
networkLatency: '网络延迟',
waterRiskWarning: '注意:区域 B 存在积水风险,建议避开',
presetRoutes: '预设路线',
all: '全部',
stop: '停止',
currentTask: '当前任务',
realtimePosition: '实时位置',
taskCompleted: '任务已完成',
deviceSerialNumberMissing: '设备序列号缺失',
confirmExecuteRoute: '确认执行该路线?',
targetDevice: '目标设备',
routePointCount: '路径点数',
taskExecuteFailed: '任务执行失败',
taskPauseAbnormal: '任务暂停异常',
areYouSureCancelTask: '确定要取消任务?',
taskCancelled: '任务已取消',
taskCancelAbnormal: '任务取消失败',
taskStatusNew: '待执行',
taskStatusExecuting: '执行中',
taskStatusPause: '已暂停',
taskStatusFinish: '已完成',
taskStatusCancel: '已取消',
unnamedTask: '未命名任务',
planId: '任务ID',
expectedComplete: '预计完成',
noCurrentTask: '暂无当前任务',
searchRoute: '搜索路径',
noRouteTip: '暂无预设路径',
pointCount: '路径点数',
deviceInfo: '设备信息',
deviceCode: '设备编码',
siteBelonging: '所属场站',
deviceLocation: '设备位置',
searchPlaceholder: '搜索...',
unnamedRoute: '未命名路线',
deleteRoute: '删除航线',
@@ -427,6 +464,7 @@ const zh = {
execTime: '执行时间',
saveTask: '保存任务',
abMode: '自定义模式',
saveFail: '保存失败',
activatedTime: '已激活时间',
aiAction1: 'AI',
@@ -555,7 +593,7 @@ const zh = {
equipmentSituationMap: '设备态势地图',
execTimePoint: '执行时间点',
executeDeviceColon: '执行设备',
executeNow: '执行现在',
executeNow: '执行',
executeWayline: '执行航线',
executeWaylineTask: '执行航线任务',
executingTasks: '执行中任务',
@@ -3653,6 +3691,13 @@ const zh = {
fixed2Offline3: '离线 3',
powerCurveFixed2: '实时功率曲线',
viewDeviceDetailAndTask: '查看设备详情与任务',
showAllDevices: '显示全部设备',
showOnlySelected: '仅显示选中设备',
clickDeviceOnMapTip: '点击地图上的设备可定位并查看详情',
focusingDevice: '正在定位设备:{{name}}',
foundDevices: '已找到 {{count}} 台设备',
gridNorm: '并网正常',
s11: '今天',

View File

@@ -89,18 +89,28 @@ export default function StationDashboard() {
report_time: "2026-03-27 12:35:08"
});
const weatherType = getWeatherType(weatherdata.weather);
// 请求天气
// 请求天气:按当前场站地址/经纬度动态定位
useEffect(() => {
console.log('当前电站信息:', currentStation);
const fetchWeather = async () => {
try {
const res = await getWeatherData();
setWeatherData(res);
const options = {} as any;
if (currentStation?.address) {
options.city = currentStation.address;
}
const res = await getWeatherData(options);
setWeatherData({
...res,
city: res?.city || currentStation?.siteName || '',
district: res?.district || '',
});
} catch (err) {
console.error("天气接口请求失败", err);
}
};
fetchWeather();
}, []);
}, [currentStation]);
// 模拟数据
const stationOverview = {
todayEnergy: 4752.0,

File diff suppressed because it is too large Load Diff

View File

@@ -10,9 +10,20 @@ const loadStationFromStorage = () => {
}
};
// 从 localStorage 加载场站列表(可选,加速首屏)
const loadStationListFromStorage = () => {
try {
const list = localStorage.getItem('stationList');
return list ? JSON.parse(list) : [];
} catch {
return [];
}
};
const initialState = {
currentStation: loadStationFromStorage(), // 当前选中场站(完整对象)
stationId: loadStationFromStorage()?.id || null, // 方便直接用 id
stationList: loadStationListFromStorage(), // 全局场站列表(用于跨组件共享最新数据)
};
const stationSlice = createSlice({
@@ -24,19 +35,107 @@ const stationSlice = createSlice({
state.currentStation = action.payload;
state.stationId = action.payload?.id || null;
localStorage.setItem('currentStation', JSON.stringify(action.payload));
// 同时更新 stationList 中对应项(保证列表也是最新)
if (action.payload?.id && state.stationList?.length) {
const idx = state.stationList.findIndex(s => Number(s.id) === Number(action.payload.id));
if (idx >= 0) {
state.stationList[idx] = { ...state.stationList[idx], ...action.payload };
localStorage.setItem('stationList', JSON.stringify(state.stationList));
}
}
},
// 设置全局场站列表
setStationList: (state, action) => {
state.stationList = action.payload || [];
localStorage.setItem('stationList', JSON.stringify(state.stationList));
// 如果当前 currentStation 在列表中,同步更新为列表中的最新版本
if (state.stationId && state.stationList?.length) {
const latest = state.stationList.find(s => Number(s.id) === Number(state.stationId));
if (latest) {
state.currentStation = latest;
localStorage.setItem('currentStation', JSON.stringify(latest));
}
}
},
// 部分更新当前选中场站(不覆盖全部字段)
updateCurrentStation: (state, action) => {
if (!state.currentStation) return;
state.currentStation = { ...state.currentStation, ...action.payload };
state.stationId = state.currentStation?.id || state.stationId;
localStorage.setItem('currentStation', JSON.stringify(state.currentStation));
// 同步更新 stationList 中对应项
if (state.currentStation?.id && state.stationList?.length) {
const idx = state.stationList.findIndex(s => Number(s.id) === Number(state.currentStation.id));
if (idx >= 0) {
state.stationList[idx] = { ...state.stationList[idx], ...action.payload };
localStorage.setItem('stationList', JSON.stringify(state.stationList));
}
}
},
// 更新列表中的单个场站(编辑场站后调用)
updateStationInList: (state, action) => {
const updatedStation = action.payload;
if (!updatedStation?.id) return;
if (!state.stationList) state.stationList = [];
const idx = state.stationList.findIndex(s => Number(s.id) === Number(updatedStation.id));
if (idx >= 0) {
state.stationList[idx] = { ...state.stationList[idx], ...updatedStation };
} else {
state.stationList.push(updatedStation);
}
localStorage.setItem('stationList', JSON.stringify(state.stationList));
// 如果更新的场站正好是当前选中的,同步 currentStation
if (state.stationId && Number(state.stationId) === Number(updatedStation.id)) {
state.currentStation = { ...state.currentStation, ...updatedStation };
localStorage.setItem('currentStation', JSON.stringify(state.currentStation));
}
},
// 从列表中删除场站
removeStationFromList: (state, action) => {
const deletedId = action.payload;
if (!state.stationList) return;
state.stationList = state.stationList.filter(s => Number(s.id) !== Number(deletedId));
localStorage.setItem('stationList', JSON.stringify(state.stationList));
// 如果删除的是当前选中场站,清空 currentStation
if (state.stationId && Number(state.stationId) === Number(deletedId)) {
state.currentStation = null;
state.stationId = null;
localStorage.removeItem('currentStation');
}
},
// 清空场站(退出登录时用)
clearStation: (state) => {
state.currentStation = null;
state.stationId = null;
state.stationList = [];
localStorage.removeItem('currentStation');
localStorage.removeItem('stationList');
},
},
});
export const { setCurrentStation, clearStation } = stationSlice.actions;
export const {
setCurrentStation,
setStationList,
updateCurrentStation,
updateStationInList,
removeStationFromList,
clearStation,
} = stationSlice.actions;
export default stationSlice.reducer;
//使用const { currentStation, stationId } = useSelector((state: RootState) => state.station);
//使用const { currentStation, stationId, stationList } = useSelector((state: RootState) => state.station);