22 Commits

Author SHA1 Message Date
mmc
2dfd975b46 优化新版航向角显示 和中英文 2026-09-01 17:24:25 +08:00
mmc
d4e19d519b 修改 2026-08-31 16:57:45 +08:00
mmc
9212b45ece 修复地图中 鼠标悬浮显示机器 优化显示登录用用户的角色 2026-08-31 14:16:36 +08:00
mmc
b5916dd408 修复admin可以看到所有用户 2026-08-31 14:10:24 +08:00
mmc
206b6915e5 修改 2026-08-31 12:40:23 +08:00
mmc
5189fdb621 修复 控制台警告信息 图标大小在初始化 会警告 长宽-1 2026-08-31 11:27:25 +08:00
mmc
db539893ce 优化 mqtt 退出路由 不订阅消息 2026-08-31 11:06:05 +08:00
mmc
a1922cdeaf 优化代码消除 控制台的报错信息 2026-08-31 10:30:21 +08:00
mmc
674721504d 优化一加载 显示的地图内容 2026-08-28 15:26:38 +08:00
mmc
0ec7b3ea88 根据模式显示 区域还是线 2026-08-28 14:38:45 +08:00
mmc
2263b190f5 和算法联调显示规划路径 并且 显示实时路径成功 2026-08-28 10:03:08 +08:00
mmc
075352a33b 修改 2026-08-26 10:39:19 +08:00
mmc
2a531f9e32 地图打点和清除 打点标志 2026-08-25 16:54:36 +08:00
mmc
a3b2f14620 测权限控制 释放权限 优化ws连接 2026-08-25 11:11:29 +08:00
mmc
d6b5c9ad44 电站地址接入 2026-08-21 09:46:53 +08:00
mmc
36d1517eef 杀死页面 和切换页面 释放控制权限 2026-08-20 17:24:35 +08:00
mmc
30ce706280 控制权限 修改 不需要判断web是否有权限 2026-08-20 16:45:12 +08:00
mmc
f22c2bedfe 优化 ws切换的时候的时序差 导致遗留上次的消息 2026-08-20 10:07:46 +08:00
mmc
be8576b887 和算法测试成功第一步 状态界面看到经纬度信息和保存可以成功 2026-08-19 17:00:07 +08:00
mmc
5073f64240 中英文优化 2026-08-19 11:16:43 +08:00
mmc
e363fb2799 登录默认选中 2026-08-19 11:10:22 +08:00
mmc
81a9ed3dd1 路由组件拆分 2026-08-19 11:09:58 +08:00
67 changed files with 1748 additions and 12109 deletions

23
env.js
View File

@@ -9,17 +9,23 @@ const currentEnv = import.meta.env?.MODE || process.env.NODE_ENV;
// 环境配置映射
const envConfig = {
[ENV.DEV]: {
//baseURL: 'http://192.168.2.59:8081',//http://192.168.2.56:8081
//baseURL: 'http://1.95.137.212:8081',//http://192.168.2.56:8081
baseURL: 'http://1.95.137.212:59003',//http://192.168.2.56:8081
////baseURL: 'http://1.95.137.212:59015',//http://192.168.2.56:8081
//WS_URL: 'ws://192.168.2.59:9002/ws/',
//mqtt_url: "ws://1.95.137.212:8083/mqtt",
//geoserver_url: "http://192.168.2.59:7000/geoserver"
baseURL: 'http://1.95.137.212:8081',//http://192.168.2.56:8081
//baseURL: 'http://1.95.137.212:8081',//http://192.168.2.56:8081
//baseURL: 'http://192.168.2.62:8081',//http://192.168.2.56:8081
WS_URL: 'ws://1.95.137.212:59005/ws/',
//baseURL: 'https://serviceri.satabot.com',
WS_URL: 'ws://1.95.137.212:9002/ws/',
//WS_URL: 'ws://1.95.137.212:9002/ws/',
//WS_URL: 'ws://192.168.2.62:9002/ws/',
mqtt_url: "ws://1.95.137.212:8083/mqtt",
geoserver_url: "http://1.95.137.212:59018/geoserver"
geoserver_url: "http://1.95.137.212:7900/geoserver"
//baseURL: 'http://1.95.137.212:59015',//http://192.168.2.56:8081
////baseURL: 'https://serviceri.satabot.com',
@@ -35,12 +41,15 @@ const envConfig = {
//mqtt_url: "ws://1.95.137.212:8083/mqtt",
//geoserver_url: "http://1.95.137.212:59018/geoserver"
baseURL: 'http://1.95.137.212:59003',//http://192.168.2.56:8081
WS_URL: 'ws://1.95.137.212:59005/ws/',
baseURL: 'http://1.95.137.212:8081',//http://192.168.2.56:8081
//baseURL: 'http://1.95.137.212:8081',//http://192.168.2.56:8081
//baseURL: 'https://serviceri.satabot.com',
WS_URL: 'ws://1.95.137.212:9002/ws/',
//WS_URL: 'ws://1.95.137.212:9002/ws/',
mqtt_url: "ws://1.95.137.212:8083/mqtt",
geoserver_url: "http://1.95.137.212:59018/geoserver"
geoserver_url: "http://1.95.137.212:7900/geoserver"
},
};

View File

@@ -61,3 +61,4 @@ export default function App() {
</I18nextProvider>
);
}

View File

@@ -455,6 +455,15 @@ export function hasPermission(params) {
})
}
///iot/workRecord/getTaskPath?taskId=74 get 获取算法回传的路径
export function getTaskPath(params) {
return request({
url: '/iot/workRecord/getTaskPath',
method: 'get',
params
})
}

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
@@ -86,7 +86,6 @@ export default function AppLayout() {
list.forEach(item => {
map.set(item.menuId, {
...item,
key: item.path,
label: item.menuName,
icon: getIcon(item.path),
@@ -240,6 +239,11 @@ export default function AppLayout() {
/>
)}
<span style={{ fontSize: 'clamp(12px, 1vw, 14px)' }}>{userInfo?.nickName || userInfo?.username}</span>
{userInfo?.roleKey === "admin" && <Tag color="red">管理员</Tag>}
{userInfo?.roleKey === "manager" && <Tag color="orange">组织管理员</Tag>}
{userInfo?.roleKey === "siteManager" && <Tag color="blue">场站管理员</Tag>}
</Space>
<Button type="text" danger icon={<LogoutOutlined />} onClick={handleLogout} style={{ fontSize: 'clamp(12px, 1vw, 14px)' }}>
{t('layout.logout')}
@@ -295,6 +299,7 @@ export default function AppLayout() {
src="https://images.unsplash.com/photo-1509391366360-2e959784a276?ixlib=rb-1.2.1&auto=format&fit=crop&w=1200&q=80"
/>
<div style={{ fontSize: 11, color: '#666', lineHeight: 1.8 }}>
{/*<div>{JSON.stringify(currentStation, null, 2)}</div>*/}
<div>{t('layout.stationAddress')}:{currentStation.address || t('layout.defaultStationAddress')}</div>
<div>{t('layout.stationType')}:{currentStation.type || t('layout.groundStation')}</div>
<div style={{ marginTop: 4, display: 'flex', alignItems: 'center', color: '#41ad0a' }}>

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState, useMemo, useImperativeHandle, forwardRef, useCallback } from 'react';
import React, { useEffect, useRef, useState, useMemo, useImperativeHandle, forwardRef, useCallback } from 'react';
import carImg from '../assets/images/carplane.png';
import inspectImg from "../assets/images/inspectCar.png"
import planeImg from '../assets/images/planeicon.png';
@@ -27,7 +27,7 @@ const CYAN_GLOW_COLOR = new window.Cesium.Color(
);
window.Cesium.Ion.defaultAccessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJmNjk0NjI1MC0wZGYyLTQ1YzItYjJmZi0wY2M5ODEwODllYzIiLCJpZCI6Mzk3MTQxLCJpYXQiOjE3NzI1MjkwMjV9.U6qmakhDgXJVXwLFyWzSxAUOFZRrDkIsRUPP6CCdnKg";
const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine, finishPoint, drawRealTime, realtimePosition, realtimeType, pathPoints = [], devices = [], onCameraClick, focusLocation, points = [], markedPoints = [], onPathChange, from, onClearDraw }, ref) => {
const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine, finishPoint, drawRealTime, realtimePosition, realtimeType, pathPoints = [], devices = [], onCameraClick, focusLocation, points = [], markedPoints = [], onPathChange, from, onClearDraw, onMarkPoint, routeMode = '', polyPoints }, ref) => {
const containerRef = useRef(null);
const viewerRef = useRef(null);
const tooltipRef = useRef(null);
@@ -37,8 +37,11 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
const alarmMapRef = useRef(new Map());
const iotMapRef = useRef(new Map());
const planLineRef = useRef(null); // points 规划航线
const polygonRef = useRef(null); // coverage 区域多边形
const navLineRef = useRef(null); // 完成点实时轨迹
const markedPointsRef = useRef([]); // 标记点实体
const { t } = useTranslation();
const realtimeEntityRef = useRef(null);
const realtimeLineRef = useRef(null);
@@ -151,6 +154,7 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
}
});
}
console.log(newPoints, "newPoints")
onPathChange && onPathChange(newPoints);
};
@@ -200,7 +204,7 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
}
return;
}
const validPoints = points.filter(p => {
const validPoints = points?.filter(p => {
const lon = Number(p.lng);
const lat = Number(p.lat);
@@ -293,6 +297,159 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
};
const drawPolygon = (points) => {
const viewer = viewerRef.current;
if (!viewer) return;
const isRealtimeMode = !!(
realtimePosition?.lon &&
realtimePosition?.lat &&
!isNaN(Number(realtimePosition.lon)) &&
!isNaN(Number(realtimePosition.lat))
);
const Cesium = window.Cesium;
// 每次绘制区域前,先移除上一次绘制的地块
if (polygonRef.current) {
try {
viewer.entities.remove(polygonRef.current);
polygonRef.current = null;
} catch (e) { }
}
if (planLineRef.current) {
try {
viewer.entities.remove(planLineRef.current);
planLineRef.current = null;
} catch (e) { }
}
const validPoints = points?.filter(p => {
const lon = Number(p.lng);
const lat = Number(p.lat);
return (
!isNaN(lon) &&
!isNaN(lat) &&
Math.abs(lon) < 180 &&
Math.abs(lat) < 90 &&
lon !== 0 &&
lat !== 0
);
});
if (validPoints.length === 0) return;
const positions = validPoints.map(point =>
Cesium.Cartesian3.fromDegrees(
Number(point.lng),
Number(point.lat)
)
);
// coverage(区域覆盖)模式下以多边形 polygon 框绘制,否则为折线
const isCoverage = routeMode === 'coverage';
let trackLine;
if (isCoverage) {
trackLine = viewer.entities.add({
polygon: {
hierarchy: positions,
material: Cesium.Color.ORANGE.withAlpha(0.2),
outline: true,
outlineColor: Cesium.Color.ORANGE.withAlpha(0.8),
height: 0,
clampToGround: true,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
});
} else {
trackLine = viewer.entities.add({
polyline: {
positions: positions,
width: 2,
material: Cesium.Color.ORANGE.withAlpha(0.8),
clampToGround: true,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
});
}
polygonRef.current = trackLine;
// ======================
// 🚀 非实时模式 -> 自动飞到轨迹
// ======================
if (!isRealtimeMode) {
let minLon = 180, maxLon = -180;
let minLat = 90, maxLat = -90;
validPoints.forEach(p => {
minLon = Math.min(minLon, p.lng);
maxLon = Math.max(maxLon, p.lng);
minLat = Math.min(minLat, p.lat);
maxLat = Math.max(maxLat, p.lat);
});
if (getCameraMode() !== "track") return;
// 单个点
if (validPoints.length === 1) {
const p = validPoints[0];
viewer.scene.requestRender();
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(
p.lng,
p.lat,
800
),
duration: 1.2
});
viewer.scene.requestRender();
} else {
const rectangle = Cesium.Rectangle.fromDegrees(
minLon,
minLat,
maxLon,
maxLat
);
viewer.scene.requestRender();
viewer.camera.flyTo({
destination: rectangle,
duration: 1.2
});
viewer.scene.requestRender();
}
}
};
useEffect(() => {
if (polyPoints && polyPoints.length > 0) {
drawPolygon(polyPoints)
} else {
// polyPoints 被清空时,移除已绘制的区域
const viewer = viewerRef.current;
if (viewer && polygonRef.current) {
try {
viewer.entities.remove(polygonRef.current);
polygonRef.current = null;
} catch (e) { }
}
}
}, [polyPoints])
useEffect(() => {
if (viewerRef.current) {
@@ -343,8 +500,7 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
}
}, [markedPoints]);
function showRobotTooltip(tooltip, robot, position) {
const { t } = useTranslation();
function showRobotTooltip(tooltip, robot, position, t) {
const statusMap = {
1: { text: t('common.working'), color: "#10b981" },
2: { text: t('devices.idle'), color: "#f59e0b" },
@@ -484,7 +640,7 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
!isNaN(Number(realtimePosition.lon)) &&
!isNaN(Number(realtimePosition.lat));
const hasTrack = points && points.length > 0;
const hasTrack = (points && points.length > 0) || polyPoints?.length > 0;
if (hasRealtime) return "realtime";
if (hasTrack) return "track";
@@ -495,9 +651,12 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
const layerIdentifier = `${workspace}:${layerName}`;
let baseUrl = wmsUrl;
const cesiumLayers = [];
if (index == 2 && from == "other") {
flyToLayer(layerIdentifier, baseUrl, 100);
}
//if (index == 2 && from == "other") {
// //flyToLayer(layerIdentifier, baseUrl, 100);
//}
//if (index == 0 && from == "other") {
// flyToLayer(layerIdentifier, baseUrl, 100);
//}
//getLayerBoundsAndFit(wmsUrl, layerName);
if (baseUrl.includes('/gwc/service/wms')) {
@@ -1027,7 +1186,7 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
return;
}
const validPoints = tracePoints.filter(p => {
const validPoints = tracePoints?.filter(p => {
const lng = Number(p.lng);
const lat = Number(p.lat);
return !isNaN(lng) && !isNaN(lat) && Math.abs(lng) < 180 && Math.abs(lat) < 90 && lng !== 0 && lat !== 0;
@@ -1087,8 +1246,17 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
if (viewer) {
if (planLineRef.current) {
viewer.entities.remove(planLineRef.current);
planLineRef.current = null;
}
if (polygonRef.current) {
viewer.entities.remove(polygonRef.current);
polygonRef.current = null;
}
viewer.scene.requestRender();
}
prevIfClearPlanLine.current = ifClearPlanLine;
@@ -1397,7 +1565,7 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
const picked = viewer.scene.pick(movement.endPosition);
if (window.Cesium.defined(picked) && picked.id?.properties?.robot) {
const robot = picked.id.properties.robot.getValue();
showRobotTooltip(tooltipRef.current, robot, movement.endPosition);
showRobotTooltip(tooltipRef.current, robot, movement.endPosition, t);
} else {
tooltipRef.current.style.display = "none";
}
@@ -1412,8 +1580,8 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
}, window.Cesium.ScreenSpaceEventType.LEFT_CLICK);
handler.setInputAction((movement) => {
//if (from !== "device-control")
return;
if (from !== "deviceStatus")
return;
const cartesian = viewer.camera.pickEllipsoid(movement.position, viewer.scene.globe.ellipsoid);
if (!cartesian) return;
const cartographic = window.Cesium.Cartographic.fromCartesian(cartesian);
@@ -1423,6 +1591,7 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
const newPoints = [...drawPoints, newPoint];
setDrawPoints(newPoints);
updateDrawPath(newPoints);
onMarkPoint?.({ lon, lat });
}, window.Cesium.ScreenSpaceEventType.LEFT_CLICK);
if (onClearDraw) {
@@ -1599,7 +1768,6 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
// 如果有航向角,设置旋转
let rotation = 0;
console.log('realtimePosition.heading:', realtimePosition.heading);
if (realtimePosition?.heading !== undefined && !isNaN(Number(realtimePosition.heading))) {
@@ -1854,17 +2022,27 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
captureScreenshot
}));
return <>
<div ref={containerRef} className="w-full h-full bg-slate-900 overflow-hidden" />
<div
ref={tooltipRef}
style={{
position: "absolute", display: "none", pointerEvents: "none",
background: "rgba(0,0,0,0.75)", color: "#fff", padding: "8px 10px",
borderRadius: "4px", fontSize: "12px", zIndex: 9999, whiteSpace: "nowrap"
}}
/>
</>;
return (
<div className="w-full h-full bg-slate-900 overflow-hidden relative">
<div ref={containerRef} className="w-full h-full overflow-hidden" />
<div
ref={tooltipRef}
style={{
position: "absolute",
display: "none",
pointerEvents: "none",
background: "rgba(0,0,0,0.75)",
color: "#fff",
padding: "8px 10px",
borderRadius: "4px",
fontSize: "12px",
zIndex: 99999,
whiteSpace: "nowrap",
}}
/>
</div>
);
});
export default CesiumMap;

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState } from 'react';
import {
Form,
Input,
@@ -180,7 +180,7 @@ const BasicSettings = () => {
<Row gutter={[8, 8]} style={{ marginBottom: 8 }}>
{topCards.map((item, idx) => (
<Col key={idx} style={{ minWidth: '168px', flex: '1 0 auto', maxWidth: "250px" }}>
<AntdCard bordered={false} bodyStyle={{ padding: '12px 16px' }} style={{ borderRadius: 10, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}>
<AntdCard variant="borderless" styles={{ body: { padding: '12px 16px' } }} style={{ borderRadius: 10, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ width: 40, height: 40, borderRadius: 10, background: item.bg, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{React.cloneElement(item.icon as React.ReactElement, { size: 20, style: item.icon.props.style ? { ...item.icon.props.style, fontSize: 20 } : { fontSize: 20 } })}
@@ -205,9 +205,9 @@ const BasicSettings = () => {
{/* 基础配置 */}
<AntdCard
title={<Space><SettingOutlined style={{ color: '#1677ff' }} /><b>{t('systemSetting.basicSettings')}</b></Space>}
bordered={false}
variant="borderless"
style={{ borderRadius: 10, marginBottom: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}
bodyStyle={{ padding: '16px' }}
styles={{ body: { padding: '16px' } }}
>
<Form form={form} layout="vertical">
<Row gutter={16}>
@@ -286,9 +286,9 @@ const BasicSettings = () => {
{/* 通知与告警策略 */}
<AntdCard
title={<Space><BellOutlined style={{ color: '#1677ff' }} /><b>{t('systemSetting.alert2')}</b></Space>}
bordered={false}
variant="borderless"
style={{ borderRadius: 10, marginBottom: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}
bodyStyle={{ padding: '16px' }}
styles={{ body: { padding: '16px' } }}
>
<Row gutter={[12, 12]} style={{ marginBottom: 8 }}>
{[
@@ -329,9 +329,9 @@ const BasicSettings = () => {
{/* 用户与权限预览 */}
<AntdCard
title={<Space><Users size={16} style={{ color: '#1677ff' }} /><b>{t('systemSetting.fixed2RolePermissionFixed2OverviewFixed2')}</b></Space>}
bordered={false}
variant="borderless"
style={{ borderRadius: 10, boxShadow: '0 2px 8px rgba(0,0,0,0.04)', marginBottom: 0 }}
bodyStyle={{ padding: '12px' }}
styles={{ body: { padding: '12px' } }}
>
<Table
size="small"
@@ -340,6 +340,7 @@ const BasicSettings = () => {
empty={false}
rowKey="name"
pagination={{ size: 'small', total: 4, pageSize: 4, showTotal: (total) => `共 ${total} 条` }}
columns={[
{ title: t('systemSetting.s36'), dataIndex: 'name', key: 'name', fixed: 'left', width: 80 },
@@ -356,7 +357,7 @@ const BasicSettings = () => {
'数据分析师': { color: '#722ed1', bg: '#f9f0ff' },
};
const s = config[role] || { color: '#8c8c8c', bg: '#f5f5f5' };
return <Tag bordered={false} style={{ color: s.color, background: s.bg, fontSize: 10, margin: 0 }}>{role}</Tag>;
return <Tag variant="borderless" style={{ color: s.color, background: s.bg, fontSize: 10, margin: 0 }}>{role}</Tag>;
}
},
{ title: t('users.phone'), dataIndex: 'phone', key: 'phone', width: 110 },
@@ -397,9 +398,9 @@ const BasicSettings = () => {
{/* 角色权限概览 */}
<AntdCard
title={<Space><Users size={16} style={{ color: '#1677ff' }} /><b>{t('systemSetting.fixed2RolePermissionFixed2OverviewFixed2')}</b></Space>}
bordered={false}
variant="borderless"
style={{ borderRadius: 10, marginBottom: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}
bodyStyle={{ padding: '10px' }}
styles={{ body: { padding: '10px' } }}
>
<Row gutter={[8, 8]}>
{roleOverview.map((role, idx) => (
@@ -428,9 +429,9 @@ const BasicSettings = () => {
<AntdCard
title={<Space><ApiOutlined style={{ color: '#1677ff' }} /><b>{t('systemSetting.fixed2DeviceAccessFixed2StatusFixed2')}</b></Space>}
extra={<Button type="link" size="small" style={{ fontSize: 11, padding: 0 }}>{t('common.detail')}<RightOutlined style={{ fontSize: 9 }} /></Button>}
bordered={false}
variant="borderless"
style={{ borderRadius: 10, marginBottom: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}
bodyStyle={{ padding: '12px' }}
styles={{ body: { padding: '12px' } }}
>
<div style={{ display: 'flex', gap: 12 }}>
<div style={{ flex: '0 0 90px' }}>
@@ -487,9 +488,9 @@ const BasicSettings = () => {
{/* 数据与安全 */}
<AntdCard
title={<Space><Shield size={16} style={{ color: '#1677ff' }} /><b>{t('systemSetting.dataSafety')}</b></Space>}
bordered={false}
variant="borderless"
style={{ borderRadius: 10, marginBottom: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}
bodyStyle={{ padding: '12px' }}
styles={{ body: { padding: '12px' } }}
>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))', gap: '10px 16px' }}>
{[
@@ -518,9 +519,9 @@ const BasicSettings = () => {
<AntdCard
title={<Space><Wrench size={16} style={{ color: '#1677ff' }} /><b>{t('systemSetting.systemMaintenance')}</b></Space>}
extra={<Button type="link" size="small" danger style={{ fontSize: 10, padding: 0 }}>{t('systemSetting.s192')}</Button>}
bordered={false}
variant="borderless"
style={{ borderRadius: 10, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}
bodyStyle={{ padding: '12px' }}
styles={{ body: { padding: '12px' } }}
>
<Row gutter={[8, 8]}>
{[

View File

@@ -104,7 +104,7 @@ const LogManagement = () => {
<Text type="secondary" style={{ fontSize: 12 }}>{t('systemSetting.equipmentControlActionsSuperAdminViewFixed2')}</Text>
</div>
<AntdCard bordered={false} style={{ borderRadius: 10, marginBottom: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }} bodyStyle={{ padding: 12 }}>
<AntdCard variant="borderless" style={{ borderRadius: 10, marginBottom: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }} styles={{ body: { padding: 12 } }}>
<Row gutter={[8, 8]} align="middle">
<Col xs={24} sm={8} md={5}>
<Select
@@ -142,7 +142,7 @@ const LogManagement = () => {
</Row>
</AntdCard>
<AntdCard bordered={false} style={{ borderRadius: 10, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }} bodyStyle={{ padding: 4 }}>
<AntdCard variant="borderless" style={{ borderRadius: 10, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }} styles={{ body: { padding: 4 } }}>
<Table
size="small"
rowKey="id"

View File

@@ -102,7 +102,7 @@ const OnlineUserPage = () => {
{/* ====================== Tab 1:在线用户 ====================== */}
{activeTab === 'onlineUser' && (
<Card bordered={false} style={{ borderRadius: 8 }}>
<Card variant="borderless" style={{ borderRadius: 8 }}>
{/* 搜索栏 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 20 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
@@ -140,14 +140,14 @@ const OnlineUserPage = () => {
showTotal: (total) => `${t('common.total')} ${total} ${t('common.items')}`,
showQuickJumper: true
}}
bordered={false}
variant="borderless"
/>
</Card>
)}
{/* ====================== Tab 2:其他页面(你可以随便改内容) ====================== */}
{activeTab === 'other' && (
<Card bordered={false} style={{ borderRadius: 8, padding: '40px 0', textAlign: 'center' }}>
<Card variant="borderless" style={{ borderRadius: 8, padding: '40px 0', textAlign: 'center' }}>
<h2>{t('systemSetting.fixed2OthersContentFixed2')}</h2>
<p>{t('systemSetting.statusCpuContentFixed2')}</p>
</Card>

View File

@@ -484,7 +484,7 @@ export default function SmartDeviceManagement({
))}
</Select>
</Form.Item>
<Form.Item label={t('systemSetting.leader')} name="userId" rules={[{ required: true, message: t('systemSetting.fixed2PleaseSelectFixed2LeaderFixed2') }]}>
<Form.Item label={t('systemSetting.leader')} name="userId" >
<Select
placeholder={t('systemSetting.leaderFixed2')}
allowClear

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import {
Layout, Tabs, Card, Row, Col, Button, Typography, Space, Modal, Form, Tree, message,
Input, Popconfirm, Divider, InputNumber, Pagination, Table, Select,
@@ -130,12 +130,13 @@ export default function UserRolePage() {
const params = {
pageNum: userPagination.pageNum,
pageSize: userPagination.pageSize,
orgId: userInfo?.roleKey == "manager" ? userInfo?.orgId : currentStation?.orgId,
...(userInfo?.roleKey == "manager"
orgId: userInfo?.roleKey === "manager" ? userInfo?.orgId : currentStation?.orgId,
...(userInfo?.roleKey === "admin" && stationId
? {}
: { siteId: stationId }
)
};
console.log(userInfo?.roleKey)
getUser(params).then(res => {
if (res.code === 200) {
@@ -171,7 +172,7 @@ export default function UserRolePage() {
useEffect(() => {
getUserData();
getRoleForSelect();
}, [userPagination.pageNum, userPagination.pageSize,]);
}, [userPagination.pageNum, userPagination.pageSize, stationId]);
// ====================== 角色操作 ======================
const handleAddRole = () => {

View File

@@ -185,7 +185,7 @@ const VideoManagement = () => {
</div>
<Card bordered={false} style={{ borderRadius: 10, marginBottom: 8 }} bodyStyle={{ padding: 12 }}>
<Card variant="borderless" style={{ borderRadius: 10, marginBottom: 8 }} styles={{ body: { padding: 12 } }}>
<Row gutter={[8, 8]} align="middle">
<Col xs={24} sm={10} md={7}>
<Text style={{ fontSize: 12, color: '#666' }}>{t('systemSetting.s0')}</Text>
@@ -225,7 +225,7 @@ const VideoManagement = () => {
</Row>
</Card>
<Card bordered={false} style={{ borderRadius: 10 }} bodyStyle={{ padding: 4 }}>
<Card variant="borderless" style={{ borderRadius: 10 }} styles={{ body: { padding: 4 } }}>
{loading ? (
<div style={{ textAlign: 'center', padding: 40 }}><Spin /></div>
) : !channel ? (

View File

@@ -61,44 +61,50 @@ class WebSocketManager {
// ==================== 连接管理 ====================
public connect(serialNumber: string, wsUrl: string): void {
console.log("创建开始");//ws://192.168.2.56:9002/ws/
// 清除之前的重连计时器
console.log("创建开始");
// 清除重连计时器
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (serialNumber == null || serialNumber.trim() === "") {
console.log("serialNumber 是空/空白,停止执行");
return;
}
// 如果序列号相同且已连接,跳过
if (this.ws && this.currentSerialRef === serialNumber && this.status === 'connected') {
console.log('WebSocket already connected for this device');
return;
}
// 如果序列号不同,先关闭旧连接
if (this.ws && this.currentSerialRef !== serialNumber) {
// ========== 重点修复:关闭旧连接,并且清空旧ws的事件监听,防止旧消息回调执行 ==========
if (this.ws) {
// 解绑所有事件,阻止旧ws继续触发onmessage/onclose
this.ws.onopen = null;
this.ws.onmessage = null;
this.ws.onerror = null;
this.ws.onclose = null;
this.close();
this.ws = null;
}
this.currentSerialRef = serialNumber;
this.setStatus('connecting');
this.isManualCloseRef = false;
// 提前赋值,不要等到new WebSocket之后
this.wsSerialRef = serialNumber;
console.log('Connecting WebSocket for', serialNumber);
if (serialNumber == null || serialNumber.trim() === "") {
console.log("serialNumber 是空/空白,停止执行");
return;
}
this.ws = new WebSocket(wsUrl);
this.wsSerialRef = serialNumber;
this.ws.onopen = () => {
console.log('WebSocket Connected');
this.setStatus('connected');
const dataStr = localStorage.getItem('userInfo');
if (!dataStr) {
console.error('用户信息未找到,无法发送认证包');
@@ -109,18 +115,21 @@ class WebSocketManager {
console.log('WebSocket%', token);
console.log('WebSocket%', parse);
console.log("开始发送认证包");//ws://192.168.2.56:9002/ws/
// 发送 AUTH 认证消息
console.log("开始发送认证包");
this.send({
type: 'AUTH',
token: token,
userName: parse.username,
deviceId: serialNumber,
});
};
this.ws.onmessage = (event) => {
// 【防御二次校验】只有当前活跃serial才处理消息,旧连接残留消息直接丢弃
if (this.wsSerialRef !== this.currentSerialRef) {
console.log('丢弃过期ws消息');
return;
}
try {
const data = JSON.parse(event.data);
this.handleMessage(data);
@@ -137,7 +146,7 @@ class WebSocketManager {
const { code, reason, wasClean } = event;
console.log('[WebSocket closed]', 'code =', code, 'reason =', reason || '(no reason)', 'wasClean =', wasClean);
// 如果不是当前设备的 ws,忽略
// 不是当前设备直接忽略
if (this.wsSerialRef !== this.currentSerialRef) {
console.log('🟡 ignore old ws close');
return;
@@ -146,15 +155,13 @@ class WebSocketManager {
this.ws = null;
this.setStatus('disconnected');
// 主动关闭不重连
if (this.isManualCloseRef || !this.currentSerialRef) {
return;
}
// 5 秒后自动重连
this.reconnectTimer = setTimeout(() => {
if (this.currentSerialRef === this.wsSerialRef) {
this.connect(this.currentSerialRef, wsUrl);
this.connect(this.currentSerialRef!, wsUrl);
}
}, 5000);
};
@@ -164,21 +171,17 @@ class WebSocketManager {
public close(): void {
this.isManualCloseRef = true;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.ws) {
console.log('Closing WebSocket');
this.ws.close();
this.ws = null;
try {
this.ws.close(1000, 'manual close');
} catch (e) {
console.warn('ws close exception', e);
}
}
this.currentSerialRef = null;
this.wsSerialRef = null;
this.setStatus('disconnected');
}
// ==================== 消息发送 ====================

View File

@@ -618,8 +618,8 @@ export default function AlarmCenterPage() {
].map((item, idx) => (
<Col key={idx} style={{ minWidth: '200px', flex: '1 1 auto', maxWidth: "400px" }}>
<Card
bordered={false}
bodyStyle={{ padding: '16px 20px' }}
variant="borderless"
styles={{ body: { padding: '16px 20px' } }}
style={{ borderRadius: 8, height: 120 }}
>
<Row align="middle" gutter={16} style={{ height: '100%' }}>
@@ -658,7 +658,7 @@ export default function AlarmCenterPage() {
</Row>
<Row gutter={8} style={{ marginBottom: 8 }}>
<Col span={24}>
<Card bordered={false} bodyStyle={{ padding: '16px 24px' }} style={{ borderRadius: 8 }}>
<Card variant="borderless" styles={{ body: { padding: '16px 24px' } }} style={{ borderRadius: 8 }}>
<Row gutter={[8, 8]} align="middle">
<Col>
<div style={{ fontSize: 12, color: '#4E5969', marginBottom: 4 }}>{t('alerts.alertLevel')}</div>
@@ -747,7 +747,7 @@ export default function AlarmCenterPage() {
{/* 表格 */}
<Row gutter={16} style={{ marginBottom: 8 }}>
<Col flex="auto" style={{ minWidth: 0 }}>
<Card bordered={false} bodyStyle={{ padding: '16px 24px' }} style={{ borderRadius: 8 }}>
<Card variant="borderless" styles={{ body: { padding: '16px 24px' } }} style={{ borderRadius: 8 }}>
<Spin spinning={loading}>
<Table
rowKey="id"
@@ -774,7 +774,7 @@ export default function AlarmCenterPage() {
{/* 三个图表 */}
<Row gutter={16}>
<Col span={8}>
<Card bordered={false} title={t('alerts.alertTrend')} style={{ borderRadius: 8 }}>
<Card variant="borderless" title={t('alerts.alertTrend')} style={{ borderRadius: 8 }}>
<UniversalLineChart
data={getAlarmTrendData()}
xKey="date"
@@ -807,7 +807,7 @@ export default function AlarmCenterPage() {
</Card>
</Col>
<Col span={8}>
<Card bordered={false} title={t('alerts.alertTypeProportion')} style={{ borderRadius: 8 }}>
<Card variant="borderless" title={t('alerts.alertTypeProportion')} style={{ borderRadius: 8 }}>
<UniversalPieChart
data={pieData}
height={220}
@@ -818,7 +818,7 @@ export default function AlarmCenterPage() {
</Card>
</Col>
<Col span={8}>
<Card bordered={false} title={t('alerts.alertStatusStatistics')} style={{ borderRadius: 8 }}>
<Card variant="borderless" title={t('alerts.alertStatusStatistics')} style={{ borderRadius: 8 }}>
<UniversalBarChart
data={[
{

View File

@@ -99,7 +99,7 @@ const AlarmStatistics = () => {
return (
<div style={{ padding: 24, background: '#f5f7fa', minHeight: '100vh' }}>
<Card title={t('alerts.alertOverview')} bordered={false} style={{ marginBottom: 24, borderRadius: 8 }}>
<Card title={t('alerts.alertOverview')} variant="borderless" style={{ marginBottom: 24, borderRadius: 8 }}>
<Row gutter={16} align="middle">
<Col xs={24} sm={12} md={6}>
<Space size={16}>
@@ -153,13 +153,13 @@ const AlarmStatistics = () => {
<Row gutter={16}>
<Col xs={24} md={12}>
<Card bordered={false} style={{ borderRadius: 8 }}>
<Card variant="borderless" style={{ borderRadius: 8 }}>
<div ref={barChartRef} style={{ width: '100%', height: 360 }} />
</Card>
</Col>
<Col xs={24} md={12}>
<Card bordered={false} style={{ borderRadius: 8 }}>
<Card variant="borderless" style={{ borderRadius: 8 }}>
<div ref={pieChartRef} style={{ width: '100%', height: 360 }} />
</Card>
</Col>

View File

@@ -459,8 +459,17 @@ const RobotControlCenter = ({ setView }) => {
}
const getControl = (serialnum) => {
setIsConnecting(true);
let userParse = null;
try {
const dataStr = localStorage.getItem("userInfo");
userParse = dataStr ? JSON.parse(dataStr) : null;
} catch (e) {
console.error("用户信息解析失败", e);
}
const data = { platform: "web", deviceId: serialnum };
const token = userParse.token || Cookies.get("Admin-Token");
const data = { platform: "web", deviceId: serialnum, token: token };
getJurisdiction(data).then(res => {
if (res?.data?.code === 200 && res?.data?.data?.remoteControl == true) {
console.log("获取远程遥控权限成功");

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef, useMemo } from 'react';
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import {
Layout, Card, Row, Col, Statistic, Tag, Button, Table,
Typography, Space, Progress, Badge, Timeline, message,
@@ -291,7 +291,7 @@ export default function DeviceOverviewPage() {
}, [selectedDevice, uavState, persistentHostData, persistentDroneData?.wind_speed]);
// MQTT消息处理
const handleMqttMessage = (topic, message) => {
const handleMqttMessage = useCallback((topic, message) => {
try {
@@ -398,7 +398,7 @@ export default function DeviceOverviewPage() {
} catch (err) {
console.error('MQTT解析失败', err);
}
};
}, []);
// MQTT订阅
const mqttTopics = useMemo(() => {
@@ -417,6 +417,7 @@ export default function DeviceOverviewPage() {
url: envConfig.mqtt_url,
topic: mqttTopics,
onMessage: handleMqttMessage,
enabled: mqttTopics.length > 0,
});
// 机器人视频
@@ -586,8 +587,8 @@ export default function DeviceOverviewPage() {
{dynamicTopCards.map((card, idx) => (
<Col key={idx} style={{ minWidth: '200px', flex: '1 0 auto', maxWidth: "260px" }}>
<Card
bordered={false}
bodyStyle={{ padding: '20px 20px', display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' }}
variant="borderless"
styles={{ body: { padding: '20px 20px', display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' } }}
style={{ borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.06)', height: 101 }}
>
{/* 左侧文字区域 */}
@@ -627,7 +628,7 @@ export default function DeviceOverviewPage() {
<Row gutter={[8, 8]}>
<Col xs={24} lg={14}>
<Card title={<><EnvironmentOutlined />{t('devices.fixed2Equipment')}</>} extra={<Button type="text" icon={<SettingOutlined />} />} bordered={false} style={{ borderRadius: 8, marginBottom: 8 }} bodyStyle={{ padding: 0 }}>
<Card title={<><EnvironmentOutlined />{t('devices.fixed2Equipment')}</>} extra={<Button type="text" icon={<SettingOutlined />} />} variant="borderless" style={{ borderRadius: 8, marginBottom: 8 }} styles={{ body: { padding: 0 } }}>
<div style={{ width: '100%', height: 350, borderRadius: 8, position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', zIndex: 9999, top: 12, left: 12, background: 'rgba(255,255,255,0.95)', borderRadius: 6, padding: 8 }}>
<div style={{ fontSize: 11, fontWeight: 60, marginBottom: 4 }}>{t('devices.s22')}</div>
@@ -645,7 +646,7 @@ export default function DeviceOverviewPage() {
</div>
</Card>
<Card title={<><SettingOutlined />{t('devices.fixed2EquipmentListFixed2')}</>} bordered={false} style={{ borderRadius: 8, marginBottom: 8 }}>
<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 },
@@ -720,7 +721,7 @@ export default function DeviceOverviewPage() {
/>
</Card>
<Card title={<><SettingOutlined /> <span >{t('devices.fixed2ControlFixed2LogDesc6Alert')}</span></>} bordered={false} style={{ borderRadius: 8 }} extra={<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><Button size="small" style={{ borderRadius: 4, border: '1px solid #d9d9d9' }}>{t('devices.fixed2AllLevels')}</Button></div>}>
<Card title={<><SettingOutlined /> <span >{t('devices.fixed2ControlFixed2LogDesc6Alert')}</span></>} variant="borderless" style={{ borderRadius: 8 }} extra={<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><Button size="small" style={{ borderRadius: 4, border: '1px solid #d9d9d9' }}>{t('devices.fixed2AllLevels')}</Button></div>}>
<Table rowKey="time" size="small" pagination={false} dataSource={logs} scroll={{ x: 'max-content' }}
columns={[
{ title: t('common.time'), dataIndex: 'time', width: 90, render: t => <span style={{ fontSize: 13 }}>{t}</span> },
@@ -736,7 +737,7 @@ export default function DeviceOverviewPage() {
<Col xs={24} lg={10}>
<Card title={<><SettingOutlined style={{ marginRight: 6, }} />{t('devices.deviceControlPanel')}</>} bordered={false} style={{ borderRadius: 8, marginBottom: 8 }} bodyStyle={{ padding: 12, paddingBottom: 0 }}>
<Card title={<><SettingOutlined style={{ marginRight: 6, }} />{t('devices.deviceControlPanel')}</>} variant="borderless" style={{ borderRadius: 8, marginBottom: 8 }} styles={{ body: { padding: 12, paddingBottom: 0 } }}>
<Select
style={{ width: "80%", marginBottom: 8 }}
placeholder={t('workOrder.selectDeviceFixed2')}
@@ -750,8 +751,8 @@ export default function DeviceOverviewPage() {
String(option?.label || "").toLowerCase().includes(input.toLowerCase())
}
options={[
{ label: <span>devices.robot</span>, options: devices.map(item => ({ value: item.device_sn || item.serialNumber, label: item.deviceAlias || item.deviceName || '未知设备' })) },
{ label: <span>constants.uav</span>, options: planedevices.map(item => ({ value: item.device_sn, label: item.drone_callsign || item.callsign || '未知设备' })) },
{ label: <span>{t('devices.robot')}</span>, options: devices.map(item => ({ value: item.device_sn || item.serialNumber, label: item.deviceAlias || item.deviceName || '未知设备' })) },
{ label: <span>{t('constants.uav')}</span>, options: planedevices.map(item => ({ value: item.device_sn, label: item.drone_callsign || item.callsign || '未知设备' })) },
]}
/>
@@ -946,7 +947,7 @@ export default function DeviceOverviewPage() {
)}
</Card>
<Card title={<><ApiOutlined />{t('devices.fixed2AiActions')}</>} bordered={false} style={{ borderRadius: 12, marginBottom: 8 }}>
<Card title={<><ApiOutlined />{t('devices.fixed2AiActions')}</>} variant="borderless" style={{ borderRadius: 12, marginBottom: 8 }}>
<Row gutter={16} style={{ marginBottom: 8 }}>
<Col span={6}><div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '6px 12px 4px', borderRadius: 12, border: '1px solid #e8e8e8' }}><div style={{ width: 30, height: 30, borderRadius: '50%', background: 'rgba(255,77,79,0.15)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#ff4d4f', fontSize: 20 }}><span role="img" aria-label="hotspot">🔥</span></div><div><div style={{ fontSize: 11, color: '#666' }}>{t('devices.hotspotSuspect')}</div><div style={{ fontSize: 22, fontWeight: 700, color: '#222' }}>2 <span style={{ fontSize: 12 }}>{t('devices.s32')}</span></div></div></div></Col>
<Col span={6}><div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '6px 12px 4px', borderRadius: 12, border: '1px solid #e8e8e8' }}><div style={{ width: 30, height: 30, borderRadius: '50%', background: 'rgba(82,196,26,0.15)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#52c41a', fontSize: 16 }}><BranchesOutlined /></div><div><div style={{ fontSize: 11, color: '#666' }}>{t('devices.s33')}</div><div style={{ fontSize: 22, fontWeight: 700, color: '#222' }}>3 <span style={{ fontSize: 12 }}>{t('devices.s34')}</span></div></div></div></Col>
@@ -977,7 +978,7 @@ export default function DeviceOverviewPage() {
</Card>
{(selectedDevice?.productName?.includes(t('constants.uav')) || selectedDevice?.device_sn) && (
<Card title={t('devices.s60')} bordered={false} style={{ borderRadius: 12, marginBottom: 8 }}>
<Card title={t('devices.s60')} variant="borderless" style={{ borderRadius: 12, marginBottom: 8 }}>
<div style={{ display: "flex", justifyContent: "space-between", gap: 12, alignItems: "center" }}>
<div style={{ display: "flex", border: "1px solid #e8e8e8", borderRadius: 12, overflow: "hidden", flex: 1 }}>
<div style={{ flex: 1, textAlign: 'center', whiteSpace: "nowrap", padding: "12px 4px", borderRight: "1px solid #e8e8e8" }}><div style={{ fontSize: 11, color: '#666', fontWeight: 600 }}>{t('devices.s44')}</div><div style={{ fontSize: 14, fontWeight: 700, color: '#222', marginTop: 2 }}>{fix2(realtimeDevice?.wind_speed)} m/s</div></div>
@@ -997,7 +998,7 @@ export default function DeviceOverviewPage() {
)}
{(!selectedDevice?.productName?.includes(t('constants.uav')) && !selectedDevice?.device_sn) && (
<Card title={<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14, fontWeight: 600 }}><RobotOutlined style={{ color: '#165DFF' }} />{t('devices.fixed2RobotStatusFixed2')}</div>} bordered={false} style={{ borderRadius: 12, marginBottom: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.06)', border: '1px solid #f0f0f0' }}>
<Card title={<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14, fontWeight: 600 }}><RobotOutlined style={{ color: '#165DFF' }} />{t('devices.fixed2RobotStatusFixed2')}</div>} variant="borderless" style={{ borderRadius: 12, marginBottom: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.06)', border: '1px solid #f0f0f0' }}>
<Row gutter={16} style={{ alignItems: 'center' }}>
<Col span={8}>
<div style={{ padding: '12px 16px', borderRadius: 10, background: '#F7F8FA', textAlign: 'center', border: '1px solid transparent', transition: 'all 0.2s' }}
@@ -1060,7 +1061,7 @@ export default function DeviceOverviewPage() {
</Card>
)}
<Card title={<><ClockCircleOutlined />{t('devices.fixed2TaskSuspended2Fixed2ExecutionTime')}</>} bordered={false} style={{ borderRadius: 12 }} extra={<div style={{ fontSize: 12, color: '#666' }}></div>}>
<Card title={<><ClockCircleOutlined />{t('devices.fixed2TaskSuspended2Fixed2ExecutionTime')}</>} variant="borderless" style={{ borderRadius: 12 }} extra={<div style={{ fontSize: 12, color: '#666' }}></div>}>
<Timeline mode="left">
{taskTimeline.map((item, idx) => (
<Timeline.Item key={idx} color={item.color} label={<div style={{ fontSize: 11, color: '#999' }}>{item.time}</div>}>

View File

@@ -1,4 +1,4 @@
import React, {
import React, {
useState,
useEffect,
useMemo,
@@ -78,6 +78,7 @@ export default function DeviceStatusPage() {
const [arrivedPoint, setArrivedPoint] = useState(null);//完成点
const [isLoopMode, setIsLoopMode] = useState(true);
const controllerRef = useRef<DeviceController | null>(null);
const isFirstConnect = useRef(true);
const [messageApi, contextHolder] = message.useMessage();
@@ -95,6 +96,7 @@ export default function DeviceStatusPage() {
const [connecting, setConnecting] = useState(false);
const [wsStatus, setWsStatus] = useState('disconnected');
const [wsDeviceInfo, setWsDeviceInfo] = useState({});
const [locationInfo, setLocationInfo] = useState({});
// 👇 加上这两个
@@ -121,7 +123,8 @@ export default function DeviceStatusPage() {
const [isSaveModalVisible, setIsSaveModalVisible] = useState(false); // 保存模态框可见性
const [plotName, setPlotName] = useState(''); // 地块名称
const [isSaving, setIsSaving] = useState(false); // 是否正在保存
const mapRef = useRef(null); // 地图 ref
const mapRef = useRef(null);
const clearDrawRef = useRef<(() => void) | null>(null); // 地图 ref
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
@@ -244,7 +247,7 @@ export default function DeviceStatusPage() {
};
// 当MQTT数据更新时,更新持久化数据(只在有新数据时更新)
const handleMqttMessage = (topic, message) => {
const handleMqttMessage = useCallback((topic, message) => {
try {
const parsedMessage = JSON.parse(message);
@@ -352,7 +355,7 @@ export default function DeviceStatusPage() {
} catch (err) {
console.error('DeviceStatusPage MQTT 消息解析失败:', err);
}
};
}, []);
// 根据选中设备动态构建MQTT主题
const mqttTopics = useMemo(() => {
@@ -374,6 +377,7 @@ export default function DeviceStatusPage() {
url: envConfig.mqtt_url,
topic: mqttTopics,
onMessage: handleMqttMessage,
enabled: mqttTopics.length > 0,
});
// 合并无人机和机场数据
@@ -388,7 +392,7 @@ export default function DeviceStatusPage() {
const runningStatus = useMemo(() => {
// 判断对象是否为空
if ((!wsDeviceInfo || Object.keys(wsDeviceInfo).length === 0) && !isUAV) {
if ((!wsDeviceInfo || Object.keys(wsDeviceInfo).length === 0) && (!locationInfo || Object.keys(locationInfo).length === 0) && !isUAV) {
return {};
}
if (isUAV) {
@@ -403,8 +407,9 @@ export default function DeviceStatusPage() {
: {
...base,
...wsDeviceInfo,
...locationInfo
};
}, [device, wsDeviceInfo, isUAV]);
}, [device, wsDeviceInfo, isUAV, locationInfo]);
const { video: robotVideo, error, ready } = useWebRTCPlayer(
(!isUAV && isVideoPlaying) ? (device?.serialNumber || '') : '',
@@ -418,6 +423,20 @@ export default function DeviceStatusPage() {
// `webrtc://1.95.137.212/live/livestream/${device?.serialNumber}`,
//);
const getVal = (val) => val === "no_rain" ? t('devices.rainNo') : (val || '-');
const fixStatusMap: Record<string, string> = {
'no_fix': t('devices.noFix'),
'single': t('devices.single'),
'dgnss': t('devices.dgnss'),
'ins': t('devices.ins'),
'rtk_fixed': t('devices.rtkFixed'),
'rtk_float': t('devices.rtkFloat'),
'single_no_heading': t('devices.singleNoHeading'),
'dgnss_no_heading': t('devices.dgnssNoHeading'),
'rtk_fixed_no_heading': t('devices.rtkFixedNoHeading'),
'rtk_float_no_heading': t('devices.rtkFloatNoHeading'),
}
const realtimePosition = React.useMemo(() => {
// 1. 无人机:优先用 MQTT 实时数据
@@ -433,8 +452,8 @@ export default function DeviceStatusPage() {
}
// 2. 机器人:从 wsDeviceInfo / runningStatus 取实时经纬度
const robotLon = runningStatus.longitude || wsDeviceInfo.longitude;
const robotLat = runningStatus.latitude || wsDeviceInfo.latitude;
const robotLon = runningStatus.longitude || locationInfo.longitude;
const robotLat = runningStatus.latitude || locationInfo.latitude;
if (robotLon && robotLat) {
return {
@@ -453,6 +472,7 @@ export default function DeviceStatusPage() {
const controller = new DeviceController(userInfo, messageApi);
controller.onDeviceInfoUpdate = setWsDeviceInfo;
controller.onLocationInfoUpdate = setLocationInfo;
controller.onControlStatusChange = setIsControlled;
controller.onHasControlRightChange = setHasControlRight;
controller.onRequestControlStatusChange = setConnecting;
@@ -480,13 +500,19 @@ export default function DeviceStatusPage() {
useEffect(() => {
if (!device || !controllerRef.current || !device?.serialNumber || isUAV) {
//controllerRef.current?.destroy();
//setWsDeviceInfo({});
return;
}
setWsDeviceInfo({});
setLocationInfo({});
controllerRef.current.connectDevice(device);
if (isFirstConnect.current) {
isFirstConnect.current = false;
controllerRef.current.connectDevice(device);
} else {
controllerRef.current.switchDevice(device);
}
}, [device?.serialNumber]);
@@ -648,111 +674,35 @@ export default function DeviceStatusPage() {
messageApi.success(t('devices.pointMarked', { count: markedPoints.length + 1 }));
};
// Map click to add marked point
const handleMapMarkPoint = (point) => {
setMarkedPoints([...markedPoints, { lon: Number(point.lon), lat: Number(point.lat) }]);
messageApi.success(t('devices.pointMarked', { count: markedPoints.length + 1 }));
};
// 清空标记点
const handleClearPoints = () => {
setMarkedPoints([]);
setCompleteFlag(false);
setSavePath(null);
clearDrawRef.current?.();
};
const getPathPoints = (coords) => {
console.log("getPathPoints", coords)
//setPathPoints(coords);
if (!coords || coords.length === 0) return;
const newWps = coords.map((p, i) => ({
id: i + 1,
lon: p.lon.toFixed(6),
lat: p.lat.toFixed(6),
}));
console.log("newWps", newWps)
//setWaypoints(newWps);
};
// 完成功能 - 调用路径规划接口
const handleComplete = async () => {
//如果时弓子模式
if (isLoopMode) {
if (markedPoints.length < 3) {
messageApi.warning(t('devices.minThreePoints'));
return;
}
try {
// 准备路径规划请求参数
const positionPoints = [...markedPoints];
const requestParams = {
reference: {
lat: positionPoints[0].lat,
lon: positionPoints[0].lon,
},
heading: 0,
outer: {
position: positionPoints.map(point => ({
lat: point.lat,
lon: point.lon,
})),
sideWidth: 0.0,
},
holes: [],
workType: 0,
};
const response = await fetch('https://servicepathplan.satabot.com/api/path', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestParams),
});
const result = await response.json();
if (result) {
const path = result?.data?.path?.map(p => {
return {
lng: p.lon,
lat: p.lat
}
})
setPoints(path);
setSavePath({
path: path, // 存储返回的路径结果
outer: requestParams?.outer?.position?.map(p => {
return {
lng: p.lon,
lat: p.lat
}
}
),
planModel: 0,
});
setCompleteFlag(true);
messageApi.success(t('devices.pathPlanDone'));
}
} catch (error) {
console.error('路径规划失败:', error);
messageApi.error(t('devices.pathPlanFailRetry'));
}
} else {
const positionPoints = [...markedPoints].map(item => {
return {
lng: item.lon,
lat: item.lat
}
});
setPoints(positionPoints);
setSavePath({
path: positionPoints, // 存储返回的路径结果
outer: [],
planModel: 2,
});
setCompleteFlag(true);
}
};
// 打开保存模态框
const handleOpenSaveModal = () => {
if (!completeFlag) {
messageApi.warning(t('devices.completePathFirst'));
return;
}
setPlotName('');
setIsSaveModalVisible(true);
};
// 保存功能
const handleSave = async () => {
if (!plotName.trim()) {
messageApi.warning(t('devices.inputPlotName'));
@@ -761,6 +711,15 @@ export default function DeviceStatusPage() {
setIsSaving(true);
try {
// ✅重点:接收handleComplete返回的path数据,不再读组件savePath state
const pathResult = await handleComplete();
if (!pathResult) {
messageApi.warning(t('devices.saveFail'));
// 路径规划失败/校验不通过直接终止
return;
}
// 1. 获取地图截图
let screenshotDataUrl = null;
if (mapRef.current) {
@@ -776,18 +735,20 @@ export default function DeviceStatusPage() {
const file = dataUrlToFile(screenshotDataUrl, "screenshot.jpg");
console.log("blob", file)
if (file instanceof File) {
formData.append('file', file);
}
}
// 4. 添加 workRecord
// 4. 添加 workRecord,直接使用函数返回的 pathResult,不读取state savePath
const workRecord = {
workName: plotName,
siteId: stationId,
jsonData: savePath,
orgId: currentStation?.orgId,
jsonData: pathResult, // 直接用返回值,彻底规避state异步延迟
mode: isLoopMode ? "coverage" : "single_point",//"区域还是单点"
};
console.log("workRecord", workRecord)
formData.append(
"workRecord",
new Blob([JSON.stringify(workRecord)], {
@@ -795,8 +756,6 @@ export default function DeviceStatusPage() {
})
);
//formData.append('workRecord', JSON.stringify(workRecord));
// 5. 调用保存接口
const res = await addRoute(formData);
@@ -818,6 +777,95 @@ export default function DeviceStatusPage() {
}
};
/**
* 修改handleComplete:计算完成后 return路径对象;失败返回null
*/
const handleComplete = async () => {
//如果是闭环模式
if (isLoopMode) {
if (markedPoints.length < 3) {
messageApi.warning(t('devices.minThreePoints'));
return null; //校验失败返回null
}
try {
// 准备路径规划请求参数
let positionPoints = [...markedPoints];
// =========新增:保证闭环,第一个点和最后一个点一致=========
const firstPt = positionPoints[0];
const lastPt = positionPoints[positionPoints.length - 1];
// 判断经纬度是否相等,不相等就把首点追加到末尾
if (!(firstPt.lat === lastPt.lat && firstPt.lon === lastPt.lon)) {
positionPoints.push({ ...firstPt });
}
const requestParams = {
outer: {
position: positionPoints.map(point => ({
lat: point.lat,
lng: point.lon,
})),
},
};
const path = requestParams?.outer?.position
setPoints(path);
const resultObj = {
path: path,
outer: requestParams?.outer?.position?.map(p => {
return {
lng: p.lon,
lat: p.lat
}
}),
};
setSavePath(resultObj);
setCompleteFlag(true);
messageApi.success(t('devices.pathPlanDone'));
return resultObj; // ✅返回计算好的数据给上层调用
} catch (error) {
console.error('路径规划失败:', error);
messageApi.error(t('devices.pathPlanFailRetry'));
return null;
}
} else {
// 单点模式
const positionPoints = [...markedPoints].map(item => {
return {
lng: item.lon,
lat: item.lat
}
});
setPoints(positionPoints);
const resultObj = {
path: positionPoints,
outer: [],
};
setSavePath(resultObj);
setCompleteFlag(true);
return resultObj; // ✅返回
}
};
// 打开保存模态框
const handleOpenSaveModal = () => {
//if (!completeFlag) {
// messageApi.warning(t('devices.completePathFirst'));
// return;
//}
setPlotName('');
setIsSaveModalVisible(true);
};
const realtimeType = useMemo(() => {
return isUAV ? 'plane' : 'car';
}, [isUAV]);
@@ -856,8 +904,8 @@ export default function DeviceStatusPage() {
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<Text strong>{t('devices.pathPlanning')}:</Text>
<Text type="secondary">{t('devices.markedPointsCount', { count: markedPoints.length })}</Text>
<Text strong>{t('devices.pathPlanning')}:</Text>
<Text type="secondary">{t('devices.markedPointsCount', { count: markedPoints.length })}</Text>
</div>
<Space size="middle">
<Space>
@@ -882,18 +930,18 @@ export default function DeviceStatusPage() {
>
{t('devices.clearMarks')}
</Button>
<Button
{/*<Button
type="primary"
ghost
onClick={handleComplete}
disabled={markedPoints.length < 2}
>
{t('devices.complete')}
</Button>
</Button>*/}
<Button
type="primary"
onClick={handleOpenSaveModal}
disabled={!completeFlag}
//disabled={!completeFlag}
>
{t('common.save')}
</Button>
@@ -961,12 +1009,12 @@ export default function DeviceStatusPage() {
title={
<>
<VideoCameraOutlined />
{t('devices.stationVideo')}
{t('devices.stationVideo')}
</>
}
bordered={false}
variant="borderless"
style={{ borderRadius: 12, marginBottom: 8 }}
bodyStyle={{ padding: 12 }}
styles={{ body: { padding: 12 } }}
>
<div style={{ borderRadius: 8, overflow: 'hidden', height: 300 }}>
<DroneVideoPlayer
@@ -1008,13 +1056,13 @@ export default function DeviceStatusPage() {
setClearPlanLine((v) => v + 1);
}}
>
{t('devices.clearPlannedRoute')}
{t('devices.clearPlanRoute')}
</Button>
</Space>
}
bordered={false}
variant="borderless"
style={{ borderRadius: 12 }}
bodyStyle={{ padding: 12, height: 400 }}
styles={{ body: { padding: 12, height: 400 } }}
>
<CesiumMap
ref={mapRef}
@@ -1023,11 +1071,13 @@ export default function DeviceStatusPage() {
markedPoints={markedPoints}
realtimePosition={realtimePosition}
realtimeType={realtimeType}
onPathChange={getPathPoints}
drawRealTime={true}
finishPoint={arrivedPoint}
ifClearNavLine={clearNavLine}
ifClearPlanLine={clearPlanLine}
from="deviceStatus"
from="deviceStatus" onMarkPoint={handleMapMarkPoint} onClearDraw={(fn) => { clearDrawRef.current = fn; }}
/>
</Card>
</Col>
@@ -1039,12 +1089,12 @@ export default function DeviceStatusPage() {
title={
<>
<VideoCameraOutlined />
{t('devices.droneVideo')}
{t('devices.droneVideo')}
</>
}
bordered={false}
variant="borderless"
style={{ borderRadius: 12, marginBottom: 8 }}
bodyStyle={{ padding: 12 }}
styles={{ body: { padding: 12 } }}
>
<div style={{ borderRadius: 8, overflow: 'hidden', height: "100%" }}>
<DroneVideoPlayer
@@ -1071,9 +1121,9 @@ export default function DeviceStatusPage() {
{t('devices.realtimeVideoMonitor')}
</>
}
bordered={false}
variant="borderless"
style={{ borderRadius: 12, marginBottom: 8 }}
bodyStyle={{ padding: 12, height: 'calc(100vh - 220px)', minHeight: 500 }}
styles={{ body: { padding: 12, height: 'calc(100vh - 220px)', minHeight: 500 } }}
extra={
<Button
@@ -1103,43 +1153,39 @@ export default function DeviceStatusPage() {
}}>
<div style={{ marginBottom: 6, fontWeight: 600, color: '#1890ff' }}>{t('devices.robotStatus')}</div>
<div style={{ display: 'flex', gap: '3px 10px', flexWrap: 'wrap' }}>
<span>{t('devices.rtBattery')}</span>
<span>{t('devices.rtBattery')}:</span>
<span style={{ color: (runningStatus.battery || 0) < 20 ? '#ff4d4f' : '#52c41a' }}>
{runningStatus.battery !== undefined ? `${runningStatus.battery}%` : '-'}
</span>
<span>{t('devices.rtVoltage')}</span>
<span>{t('devices.rtVoltage')}:</span>
<span>{runningStatus.voltage !== undefined ? `${Number(runningStatus.voltage).toFixed(2)}V` : '-'}</span>
<span>{t('devices.rtLeftMotor')}</span>
<span>{t('devices.rtLeftMotor')}:</span>
<span>{runningStatus.leftCurrent !== undefined ? `${Number(runningStatus.leftCurrent).toFixed(2)}A` : '-'}</span>
<span>{t('devices.rtRightMotor')}</span>
<span>{t('devices.rtRightMotor')}:</span>
<span>{runningStatus.rightCurrent !== undefined ? `${Number(runningStatus.rightCurrent).toFixed(2)}A` : '-'}</span>
<span>{t('devices.rtLeftSpeed')}</span>
<span>{t('devices.rtLeftSpeed')}:</span>
<span>{runningStatus.leftMeasureSpeed !== undefined ? `${Number(runningStatus.leftMeasureSpeed).toFixed(1)}rpm` : '-'}</span>
<span>{t('devices.rtRightSpeed')}</span>
<span>{t('devices.rtRightSpeed')}:</span>
<span>{runningStatus.rightMeasureSpeed !== undefined ? `${Number(runningStatus.rightMeasureSpeed).toFixed(1)}rpm` : '-'}</span>
<span>{t('devices.rtLeftTemp')}</span>
<span>{t('devices.rtLeftTemp')}:</span>
<span>{runningStatus.leftMotorTemp !== undefined ? `${Number(runningStatus.leftMotorTemp).toFixed(1)}℃` : '-'}</span>
<span>{t('devices.rtRightTemp')}</span>
<span>{t('devices.rtRightTemp')}:</span>
<span>{runningStatus.rightMotorTemp !== undefined ? `${Number(runningStatus.rightMotorTemp).toFixed(1)}℃` : '-'}</span>
<span>{t('devices.rtMainTemp')}</span>
<span>{t('devices.rtMainTemp')}:</span>
<span>{runningStatus.chipTemp !== undefined ? `${Number(runningStatus.chipTemp).toFixed(1)}℃` : '-'}</span>
<span>{t('devices.rtHeadingAngle')}</span>
<span>{t('devices.rtHeadingAngle')}:</span>
<span>{runningStatus.yaw !== undefined ? `${Number(runningStatus.yaw).toFixed(1)}°` : '-'}</span>
<span>{t('devices.rtPitchAngle')}</span>
<span>{t('devices.rtPitchAngle')}:</span>
<span>{runningStatus.pitch !== undefined ? `${Number(runningStatus.pitch).toFixed(1)}°` : '-'}</span>
<span>{t('devices.rtHeadingStatus')}</span>
<span>{t('devices.rtHeadingStatus')}:</span>
<span>{runningStatus.headingStatus !== undefined ? `${runningStatus.headingStatus == 1 ? t('devices.rtInitialized') : t('devices.uninitialized')}` : '-'}</span>
<span>{t('devices.rtPositioning')}</span>
<span>{t('devices.rtPositioning')}:</span>
<span>
{runningStatus.qual == 0 ? t('constants.locationInvalid') :
runningStatus.qual == 1 ? t('devices.gpsSingle') :
runningStatus.qual == 2 ? t('devices.dgpsDiff') :
runningStatus.qual == 4 ? t('devices.rtkFixed2') :
runningStatus.qual == 5 ? t('devices.rtkFloat') : t('constants.locationUnknown')}
{runningStatus.fix_status ? fixStatusMap[runningStatus.fix_status] : t('constants.locationUnknown')}
</span>
<span>{t('devices.rtSatellites')}</span>
<span>{t('devices.rtSatellites')}:</span>
<span>{t('devices.satelliteCount', { count: runningStatus.satelliteCnt || 0 })}</span>
<span>{t('devices.rtMode')}</span>
<span>{t('devices.rtMode')}:</span>
<span>
{runningStatus.controlMode == 1 ? t('devices.localMode') :
runningStatus.controlMode == 3 ? t('devices.remoteMode') : t('constants.locationUnknown')}
@@ -1244,11 +1290,8 @@ export default function DeviceStatusPage() {
<span>{runningStatus.headingStatus !== undefined ? `${runningStatus.headingStatus == 1 ? t('devices.rtInitialized') : t('devices.uninitialized')}` : '-'}</span>
<span>{t('devices.rtPositioning')}</span>
<span>
{runningStatus.qual == 0 ? t('constants.locationInvalid') :
runningStatus.qual == 1 ? t('devices.gpsSingle') :
runningStatus.qual == 2 ? t('devices.dgpsDiff') :
runningStatus.qual == 4 ? t('devices.rtkFixed2') :
runningStatus.qual == 5 ? t('devices.rtkFloat') : t('constants.locationUnknown')}
{runningStatus.fix_status ? fixStatusMap[runningStatus.fix_status] : t('constants.locationUnknown')}
</span>
<span>{t('devices.rtSatellites')}</span>
<span>{t('devices.satelliteCount', { count: runningStatus.satelliteCnt || 0 })}</span>
@@ -1294,13 +1337,13 @@ export default function DeviceStatusPage() {
setClearPlanLine((v) => v + 1);
}}
>
{t('devices.clearPlannedRoute')}
{t('devices.clearPlanRoute')}
</Button>
</Space>
}
bordered={false}
variant="borderless"
style={{ borderRadius: 12, marginBottom: 8, flex: 1, minHeight: 280 }}
bodyStyle={{ padding: 12, height: 'calc((100vh - 260px) / 2)', minHeight: 280 }}
styles={{ body: { padding: 12, height: 'calc((100vh - 260px) / 2)', minHeight: 280 } }}
>
<CesiumMap
ref={mapRef}
@@ -1313,7 +1356,7 @@ export default function DeviceStatusPage() {
finishPoint={arrivedPoint}
ifClearNavLine={clearNavLine}
ifClearPlanLine={clearPlanLine}
from="deviceStatus"
from="deviceStatus" onMarkPoint={handleMapMarkPoint} onClearDraw={(fn) => { clearDrawRef.current = fn; }}
/>
</Card>
@@ -1322,13 +1365,15 @@ export default function DeviceStatusPage() {
title={
<>
<ControlOutlined />
{t('devices.remoteControl')}
{t('devices.remoteControl')}
</>
}
bordered={false}
variant="borderless"
style={{ borderRadius: 12, background: '#0f172a', flex: 1, minHeight: 280, }}
headStyle={{ color: '#fff' }}
bodyStyle={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: 'calc(100% - 57px)' }}
styles={{
header: { color: '#fff', },
body: { display: 'flex', justifyContent: 'center', alignItems: 'center', height: 'calc(100% - 57px)' },
}}
>
<XboxController
onGamepadUpdate={handleGamepadUpdate}

View File

@@ -1,11 +1,12 @@
import React, { useState, useEffect, useRef, useMemo } from 'react';
import React, { useState, useEffect, useRef, useMemo } from 'react';
import {
Row, Col, Card, Button, Space, Table, Tag, Typography, Progress, Badge,
Tabs, Input, Switch, Divider, Tooltip, Timeline, Select, message, DatePicker,
Modal, InputNumber,
TimePicker,
Popconfirm,
App
App,
Radio
} from 'antd';
import {
PlusOutlined, ImportOutlined, NodeIndexOutlined,
@@ -35,6 +36,7 @@ import {
getSiteDeviceTaskHistory,
getRouteByTask,
deleteRoute,
getTaskPath,
} from '../../api/device';
import dayjs from 'dayjs';
import DraggableBox from '../Draggable';
@@ -64,6 +66,8 @@ export default function RobotTaskPage() {
const [taskList, setTaskList] = useState([]);
const [arrivedPoint, setArrivedPoint] = useState(null);//完成点
const { modal } = App.useApp();
const [polyPoints, setPolyPoints] = useState([]);
@@ -75,6 +79,7 @@ export default function RobotTaskPage() {
const [focusLocation, setFocusLocation] = useState(null);
const [points, setPoints] = useState([]);
const [routeMode, setRouteMode] = useState('');
const [clearNavLine, setClearNavLine] = useState(0);
const [clearPlanLine, setClearPlanLine] = useState(0);
@@ -116,6 +121,8 @@ export default function RobotTaskPage() {
robot: null,
dateRange: null,
});
// 取消任务模式 normal普通取消 force强制取消
const [cancelMode, setCancelMode] = useState<'normal' | 'force'>('normal');
@@ -137,13 +144,26 @@ export default function RobotTaskPage() {
});
const [messageApi, contextHolder] = message.useMessage();
const controllerRef = useRef<DeviceController | null>(null);
const isFirstConnect = useRef(true);
const fixStatusMap: Record<string, string> = {
'no_fix': t('devices.noFix'),
'single': t('devices.single'),
'dgnss': t('devices.dgnss'),
'ins': t('devices.ins'),
'rtk_fixed': t('devices.rtkFixed'),
'rtk_float': t('devices.rtkFloat'),
'single_no_heading': t('devices.singleNoHeading'),
'dgnss_no_heading': t('devices.dgnssNoHeading'),
'rtk_fixed_no_heading': t('devices.rtkFixedNoHeading'),
'rtk_float_no_heading': t('devices.rtkFloatNoHeading'),
}
const handleFinish = (data) => {
setExecute(false);
setShowVideo(false); // 任务完成关闭视频
utils.message.success(t('devices.fixed2TaskSuspended2Fixed2CompletedFixed2'));
//if (controllerRef.current) {
// controllerRef.current.destroy?.();
//}
setClearNavLine(clearNavLine + 1);
setSelectedRobotForExecute(null);
setSelectedRouteForExecute(null);
@@ -152,6 +172,7 @@ export default function RobotTaskPage() {
fetchTaskPoolList();
}
const handleFinishEdPoint = (point) => {
setArrivedPoint(point);
@@ -171,7 +192,8 @@ export default function RobotTaskPage() {
} catch (err) {
console.error('解析任务路径数据失败:', err);
}
setPoints(pathPoints);
setRouteMode('coverage')
setPolyPoints(pathPoints);
}
} catch (err) {
console.error('获取任务路径失败:', err);
@@ -236,7 +258,7 @@ export default function RobotTaskPage() {
setExecute(true);
setExecutingRobot(targetDevice);
setClearNavLine(v => v + 1); // 清空地图轨迹
controllerRef.current?.connectDevice(targetDevice);
if (isFirstConnect.current) { isFirstConnect.current = false; controllerRef.current?.connectDevice(targetDevice); } else { controllerRef.current?.switchDevice(targetDevice); }
setShowVideo(true);
};
@@ -244,9 +266,12 @@ export default function RobotTaskPage() {
useEffect(() => {
const controller = new DeviceController(userInfo, messageApi);
controller.onDeviceInfoUpdate = setRobotRealtimePosition;
controller.onLocationInfoUpdate = setRobotRealtimePosition;
controller.onIsFinished = handleFinish;
controller.handleArrivedPoint = handleFinishEdPoint;
controller.onRoutePost = setPoints;
controller.init();
@@ -279,6 +304,7 @@ export default function RobotTaskPage() {
const runningTask = taskPoolList.find(item => ['EXECUTING', 'PAUSE'].includes(item.taskStaus));
if (!runningTask) return;
fetchRouteDetail(runningTask?.id);
// 找到对应的设备
const targetDevice = devices.find(d => d.serialNumber === runningTask.deviceId);
@@ -296,7 +322,7 @@ export default function RobotTaskPage() {
setExecute(true);
setExecutingRobot(targetDevice);
setClearNavLine(v => v + 1); // 清空地图轨迹
controllerRef.current?.connectDevice(targetDevice);
if (isFirstConnect.current) { isFirstConnect.current = false; controllerRef.current?.connectDevice(targetDevice); } else { controllerRef.current?.switchDevice(targetDevice); }
setShowVideo(true);
}, [devices, taskPoolList, executingRobot, runningPoolTask]);
@@ -386,57 +412,14 @@ export default function RobotTaskPage() {
}
};
useEffect(() => {
if (!selectedPathWork?.id) {
//console.log('获取任务列表', selectedPathWork?.id);
if (!selectedRobotForExecute?.id || !execute) {
setPoints([]);
return;
}
// 调用接口获取路线详情
const fetchRouteDetail = async () => {
try {
const res = await getSingleRouteList({ id: selectedPathWork.id });
if (res?.code === 200 && res?.data) {
let pathPoints = [];
let outerPoints = [];
let method = '';//0弓子 2 回子
try {
// 尝试解析 JSON
if (res.data.jsonData) {
const data = typeof res.data.jsonData === 'string'
? JSON.parse(res.data.jsonData)
: res.data.jsonData;
pathPoints = data?.path || [];
outerPoints = data?.outer || [];
method = data?.planModel;
}
} catch (err) {
console.error('解析路线数据失败:', err);
}
if (pathPoints.length === 0) {
utils.message.warning(t('devices.route2'));
}
//if (outerPoints.length === 0 && method == 2) {
// utils.message.warning(t('devices.route2'));
//}
//if (method == 0) {
setPoints(pathPoints);
//} else {
// setPoints(outerPoints);
//}
} else {
setPoints([]);
}
} catch (err) {
console.error('获取路线详情失败:', err);
setPoints([]);
}
};
fetchRouteDetail();
}, [selectedPathWork]);
}, [selectedRobotForExecute?.id]);
// 监听任务池列表变化,自动加载第一个任务的路径
useEffect(() => {
@@ -445,6 +428,79 @@ export default function RobotTaskPage() {
}
}, [taskPoolList]);
const getRouteOut = async () => {
try {
const res = await getSingleRouteList({ id: selectedPathWork?.id });
// coverage(区域覆盖)绘制为多边形,其余为折线/单点
const mode = res?.data?.mode;
setRouteMode(mode);
if (res?.code === 200 && res?.data?.jsonData) {
let pathPoints = [];
try {
// 尝试解析 JSON
pathPoints = res?.data?.jsonData?.path || [];
} catch (err) {
console.error('解析路线数据失败:', err);
}
if (pathPoints.length === 0) {
utils.message.warning(t('devices.route2'));
}
setPolyPoints(pathPoints);
} else {
setPolyPoints([]);
setRouteMode('');
}
} catch (err) {
console.error('获取路线详情失败:', err);
setPolyPoints([]);
setRouteMode('');
}
}
useEffect(() => {
getRouteOut();
}, [selectedPathWork?.id])
// 调用接口获取路线详情
const fetchRouteDetail = async (id) => {
console.log(id, "获取正在执行任务的id")
try {
const params = {
taskId: id,
}
const res = await getTaskPath(params)
//const res = await getSingleRouteList({ id: selectedPathWork.id });
if (res?.code === 200 && res?.data) {
let pathPoints = [];
try {
// 尝试解析 JSON
pathPoints = res.data || [];
} catch (err) {
console.error('解析路线数据失败:', err);
}
if (pathPoints.length === 0) {
utils.message.warning(t('devices.route2'));
}
setPoints(pathPoints);
} else {
setPoints([]);
}
} catch (err) {
console.error('获取路线详情失败:', err);
setPoints([]);
}
};
@@ -460,7 +516,7 @@ export default function RobotTaskPage() {
setExecute(true);
setExecutingRobot(targetDevice);
setClearNavLine(v => v + 1); // 清空地图轨迹
controllerRef.current?.connectDevice(targetDevice);
if (isFirstConnect.current) { isFirstConnect.current = false; controllerRef.current?.connectDevice(targetDevice); } else { controllerRef.current?.switchDevice(targetDevice); }
setShowVideo(true);
}
}
@@ -513,7 +569,7 @@ export default function RobotTaskPage() {
setSelectedRobotForExecute(null);
// 调用接口检查是否有路径点
const res = await getSingleRouteList({ id: route.id });
const res = await getSingleRouteList({ id: route?.id });
let hasPathPoints = false;
if (res?.code === 200 && res?.data) {
@@ -566,7 +622,7 @@ export default function RobotTaskPage() {
fetchTaskList();
fetchTaskPoolList();
fetchTaskHistoryList();
controllerRef.current?.connectDevice(selectedRobotForExecute);
if (isFirstConnect.current) { isFirstConnect.current = false; controllerRef.current?.connectDevice(selectedRobotForExecute); } else { controllerRef.current?.switchDevice(selectedRobotForExecute); }
setShowVideo(true); // 连接成功后自动打开视频
} else {
@@ -653,13 +709,14 @@ export default function RobotTaskPage() {
};
// 取消任务
const handleCancelTask = async (task) => {
const handleCancelTask = async (task, mode) => {
try {
const params = {
taskId: task.id,
//deviceId: task.deviceId,
siteId: stationId,
orgId: task.orgId,
forceCancel: mode === 'force' ? true : false,
};
const res = await cancelRoute(params);
if (res.code === 200 && res.data == true) {
@@ -673,6 +730,8 @@ export default function RobotTaskPage() {
setSelectedRobotForExecute(null);
setSelectedRouteForExecute(null);
} else {
utils.message.error(t('devices.taskCancelFail'));
}
@@ -832,15 +891,17 @@ export default function RobotTaskPage() {
dataIndex: 'taskStaus',
width: 100,
render: (status, record) => {
console.log('status', status, record);
const statusMap = {
'NEW': { color: 'default', text: t('devices.statusNew') },
'EXECUTING': { color: 'processing', text: t('workOrder.inProgress') },
'PAUSE': { color: 'warning', text: t('devices.statusPaused') },
'FINISH': { color: 'success', text: t('devices.statusFinished') },
'FAILED': { color: 'error', text: t('devices.statusFailed') }
'FAILED': { color: 'error', text: t('devices.statusFailed') },
"CANCELING": { color: 'warning', text: t('devices.statusCanceling') },
};
const item = statusMap[status] || { color: 'default', text: record.taskStausTranslate || t('constants.locationUnknown') };
return <Tag color={item.color}>{item.text}</Tag>;
return <Tag color={item.color}>{record.taskStausTranslate}</Tag>;
}
},
{
@@ -889,7 +950,8 @@ export default function RobotTaskPage() {
'EXECUTING': { color: 'processing', text: t('workOrder.inProgress') },
'PAUSE': { color: 'warning', text: t('devices.statusPaused') },
'FINISH': { color: 'success', text: t('devices.statusFinished') },
'FAILED': { color: 'error', text: t('devices.statusFailed') }
'FAILED': { color: 'error', text: t('devices.statusFailed') },
"CANCELING": { color: 'warning', text: t('devices.statusCanceling') },
};
const item = statusMap[status] || { color: 'default', text: t('constants.locationUnknown') };
return <Tag color={item.color}>{item.text}</Tag>;
@@ -909,27 +971,46 @@ export default function RobotTaskPage() {
<>
{/*{record.taskStaus === 'NEW' && (*/}
<Popconfirm
title={t('devices.confirmCancelTask')}
onConfirm={() => handleCancelTask(record)}
okText={t('common.ok')} cancelText={t('common.cancel')}
>
<Button type="text" size="small" danger icon={<CloseOutlined />} onClick={(e) => e.stopPropagation()}>
{t('common.cancel')}
</Button>
</Popconfirm>
<Popconfirm
title={t('devices.confirmCancelTask')}
description={
<Radio.Group
value={cancelMode}
onChange={(e) => setCancelMode(e.target.value)}
style={{ marginTop: 8 }}
>
<Space direction="vertical">
<Radio value="normal">{t('devices.normalCancel')}</Radio>
<Radio value="force">{t('devices.forceCancel')}</Radio>
</Space>
</Radio.Group>
}
onConfirm={() => handleCancelTask(record, cancelMode)}
okText={t('common.ok')}
cancelText={t('common.cancel')}
>
<Button
type="text"
size="small"
danger
icon={<CloseOutlined />}
onClick={(e) => e.stopPropagation()}
>
{t('common.cancel')}
</Button>
</Popconfirm>
{/*//)}*/}
{/*{record.taskStaus === 'EXECUTING' && (*/}
<Popconfirm
title={t('devices.confirmPauseTask')}
onConfirm={() => handlePauseTask(record)}
okText={t('common.ok')} cancelText={t('common.cancel')}
>
<Button type="text" size="small" style={{ color: '#FF9500' }} onClick={(e) => e.stopPropagation()}>
{t('video.pause')}
</Button>
</Popconfirm>
<Popconfirm
title={t('devices.confirmPauseTask')}
onConfirm={() => handlePauseTask(record)}
okText={t('common.ok')} cancelText={t('common.cancel')}
>
<Button type="text" size="small" style={{ color: '#FF9500' }} onClick={(e) => e.stopPropagation()}>
{t('video.pause')}
</Button>
</Popconfirm>
{/*)}/*/}
{record.taskStaus === 'PAUSE' && (
@@ -1043,12 +1124,14 @@ export default function RobotTaskPage() {
{topCards.map((item, index) => (
<Col xs={12} sm={8} md={4} key={index}>
<Card
bordered={false}
bodyStyle={{
padding: '12px 14px',
height: '100%',
display: 'flex',
alignItems: 'center'
variant="borderless"
styles={{
body: {
padding: '12px 14px',
height: '100%',
display: 'flex',
alignItems: 'center'
}
}}
style={{
...cardStyle,
@@ -1136,7 +1219,7 @@ export default function RobotTaskPage() {
extra={
<Space size={4} wrap> {/* Space开启自动换行,小屏按钮自动折行 */}
<Button size="small" icon={<DeleteOutlined />} type="primary" style={{ borderRadius: 4 }} onClick={() => setClearNavLine(v => v + 1)}>{t('devices.clearRealtimeTrack')}</Button>
<Button size="small" icon={<DeleteOutlined />} danger style={{ borderRadius: 4 }} onClick={() => { setPoints([]); setClearPlanLine(v => v + 1); }}>{t('devices.clearPlanRoute')}</Button>
<Button size="small" icon={<DeleteOutlined />} danger style={{ borderRadius: 4 }} onClick={() => { setPoints([]); setRouteMode(""); setClearPlanLine(v => v + 1); }}>{t('devices.clearPlanRoute')}</Button>
{/*<Button size="small" icon={<PlusOutlined />} style={{ borderRadius: 4 }}>{t('common.addNew')}</Button>
<Button size="small" icon={<ImportOutlined />} style={{ borderRadius: 4 }}>{t('common.import')}</Button>
<Button size="small" icon={<NodeIndexOutlined />} style={{ borderRadius: 4 }}>{t('devices.optimize')}</Button>
@@ -1144,8 +1227,8 @@ export default function RobotTaskPage() {
{/*<Button size="small" icon={<Maximize2 size={12} />} />*/}
</Space>
}
bordered={false}
bodyStyle={{ padding: 0, position: 'relative' }}
variant="borderless"
styles={{ body: { padding: 0, position: 'relative' } }}
style={{ ...cardStyle, marginBottom: 10 }}
>
<div style={{ width: '100%', height: 420, background: '#e8f3ff center/cover', position: 'relative' }}>
@@ -1154,12 +1237,14 @@ export default function RobotTaskPage() {
points={points}
focusLocation={focusLocation}
realtimePosition={realtimePosition}
polyPoints={polyPoints}
realtimeType="robot"
from="device-control"
from="deviceStatus"
drawRealTime={execute}
finishPoint={arrivedPoint}
ifClearNavLine={clearNavLine}
ifClearPlanLine={clearPlanLine}
routeMode={routeMode}
/>
{/* 机器人状态悬浮面板 - 绝对定位左上角 */}
@@ -1197,6 +1282,19 @@ export default function RobotTaskPage() {
<span style={{ color: '#9eff9e' }}>{t('devices.remoteMode')}</span>
)}
</span>
{/* const fixStatusMap: Record<string, string> = {
'no_fix': t('devices.noFix'),
'single': t('devices.single'),
'dgnss': t('devices.dgnss'),
'ins': t('devices.ins'),
'rtk_fixed': t('devices.rtkFixed'),
'rtk_float': t('devices.rtkFloat'),
'single_no_heading': t('devices.singleNoHeading'),
'dgnss_no_heading': t('devices.dgnssNoHeading'),
'rtk_fixed_no_heading': t('devices.rtkFixedNoHeading'),
'rtk_float_no_heading': t('devices.rtkFloatNoHeading'),
}
*/}
<span>
{t('devices.heading')}
@@ -1209,11 +1307,7 @@ export default function RobotTaskPage() {
<span>
{t('devices.location')}
{(() => {
const qual = robotRealtimePosition?.qual;
const info = getLocationQuality(qual);
return <span style={{ color: info.color }}>{info.text}</span>;
})()}
{robotRealtimePosition?.fix_status ? fixStatusMap[robotRealtimePosition?.fix_status] : t('constants.locationUnknown')}
</span>
{/* 视频开关 */}
@@ -1236,8 +1330,8 @@ export default function RobotTaskPage() {
{/* 机器人任务模块,带Tab切换 */}
<Card
size="small"
bordered={false}
bodyStyle={{ padding: 0, display: 'flex', flexDirection: 'column', height: "calc(100%)", overflow: "auto" }}
variant="borderless"
styles={{ body: { padding: 0, display: 'flex', flexDirection: 'column', height: "calc(100%)", overflow: "auto" } }}
style={{ ...cardStyle, maxHeight: "300px", flex: 1, marginBottom: 0 }}
>
@@ -1468,31 +1562,52 @@ export default function RobotTaskPage() {
<Col xs={24} xl={10} style={{ display: "flex", flexDirection: "column" }} >
<Card
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>{t('devices.operationSuggestion')}</span>}
bordered={false}
bodyStyle={{ padding: '12px 16px' }}
variant="borderless"
styles={{ body: { padding: '12px 16px' } }}
style={{ ...cardStyle, marginBottom: 8 }}
>
<Timeline mode="left">
<Timeline.Item label="09:30" color="green">
<Text strong>{t('devices.timelineMow', { robot: 'RBT-01' })}</Text>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.timelineMowDetail')}</div>
</Timeline.Item>
<Timeline.Item label="11:00" color="blue">
<Text strong>{t('devices.timelineInspect', { robot: 'RBT-02' })}</Text>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.timelineInspectDetail')}</div>
</Timeline.Item>
<Timeline.Item label="14:00" color="gray">
<Text strong>{t('devices.timelineMaintenance')}</Text>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.timelineMaintenanceDetail')}</div>
</Timeline.Item>
</Timeline>
<Timeline
mode="left"
items={[
{
color: 'green',
label: '09:30',
children: (
<>
<Text strong>{t('devices.timelineMow', { robot: 'RBT-01' })}</Text>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.timelineMowDetail')}</div>
</>
)
},
{
color: 'blue',
label: '11:00',
children: (
<>
<Text strong>{t('devices.timelineInspect', { robot: 'RBT-02' })}</Text>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.timelineInspectDetail')}</div>
</>
)
},
{
color: 'gray',
label: '14:00',
children: (
<>
<Text strong>{t('devices.timelineMaintenance')}</Text>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.timelineMaintenanceDetail')}</div>
</>
)
}
]}
/>
<Button type="primary" block style={{ marginTop: 16, borderRadius: 18 }}>{t('devices.autoGenerateTodayPlan')}</Button>
</Card>
<Card
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>{t('devices.deviceEnvironment')}</span>}
bordered={false}
bodyStyle={{ padding: '16px' }}
variant="borderless"
styles={{ body: { padding: '16px' } }}
style={{ ...cardStyle, flex: 1 }}
>
<Row gutter={[8, 8]}>
@@ -1544,8 +1659,8 @@ export default function RobotTaskPage() {
/>
</div>
}
bordered={false}
bodyStyle={{ padding: '12px', height: 400, overflow: 'auto' }}
variant="borderless"
styles={{ body: { padding: '12px', height: 400, overflow: 'auto' } }}
style={cardStyle}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
@@ -1676,8 +1791,8 @@ export default function RobotTaskPage() {
<Col xs={24} xl={18}>
<Card
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>{t('devices.taskBoard')}</span>}
bordered={false}
bodyStyle={{ padding: '12px' }}
variant="borderless"
styles={{ body: { padding: '12px' } }}
style={{ ...cardStyle, height: '100%' }}
>
<Row gutter={16}>
@@ -1815,6 +1930,7 @@ const NewTaskModal = ({ isOpen, onClose, robots, selectedPathWork, handlGetTaskL
if (selectedPathWork) {
setFormData(prev => ({ ...prev, path: selectedPathWork }));
}
}, [selectedPathWork]);
// ====================== 周/月选择 ======================

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import {
Row, Col, Card, Button, Space, Table, Tag, Typography, Progress, Badge,
Tabs, Input, Switch, Divider, Tooltip, Timeline, Select, message, DatePicker,
@@ -93,7 +93,7 @@ export default function WayLinePage({ onRefresh }) {
// 当MQTT数据更新时,更新持久化数据(只在有新数据时更新)
const handleMqttMessage = (topic, message) => {
const handleMqttMessage = useCallback((topic, message) => {
try {
const parsedMessage = JSON.parse(message);
@@ -201,7 +201,7 @@ export default function WayLinePage({ onRefresh }) {
} catch (err) {
console.error('WayLinePage MQTT 消息解析失败:', err);
}
};
}, []);
// 根据选中设备动态构建MQTT主题
const mqttTopics = React.useMemo(() => {
@@ -223,6 +223,7 @@ export default function WayLinePage({ onRefresh }) {
url: envConfig.mqtt_url,
topic: mqttTopics,
onMessage: handleMqttMessage,
enabled: mqttTopics.length > 0,
});
// 优先使用持久化数据,保持显示稳定
@@ -831,7 +832,7 @@ export default function WayLinePage({ onRefresh }) {
<Row gutter={[16, 16]} style={{ marginBottom: 8 }}>
{topCards.map((item, index) => (
<Col xs={12} sm={8} md={4} key={index}>
<Card bordered={false} bodyStyle={{ padding: '12px 14px' }} style={cardStyle}>
<Card variant="borderless" styles={{ body: { padding: '12px 14px' } }} style={cardStyle}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ width: 36, height: 36, background: item.bg, borderRadius: 10, display: 'flex', justifyContent: 'center', alignItems: 'center', color: item.color }}>
{React.cloneElement(item.icon, { size: 18 })}
@@ -890,8 +891,8 @@ export default function WayLinePage({ onRefresh }) {
<Button size="small" icon={<Maximize2 size={12} />} />
</Space>
}
bordered={false}
bodyStyle={{ padding: 0, position: 'relative' }}
variant="borderless"
styles={{ body: { padding: 0, position: 'relative' } }}
style={{ ...cardStyle, marginBottom: 10 }}
>
<div style={{ width: '100%', height: 420, background: '#e8f3ff url(https://picsum.photos/seed/solar_map_v2/1600/1000) center/cover', position: 'relative' }}>
@@ -957,8 +958,8 @@ export default function WayLinePage({ onRefresh }) {
/>
</Tooltip>
}
bordered={false}
bodyStyle={{ padding: 0, display: 'flex', flexDirection: 'column', height: "calc(100% - 40px)", overflow: "auto" }}
variant="borderless"
styles={{ body: { padding: 0, display: 'flex', flexDirection: 'column', height: "calc(100% - 40px)", overflow: "auto" } }}
style={{ ...cardStyle, maxHeight: "250px", flex: 1, marginBottom: 0 }}
>
<Table
@@ -982,8 +983,8 @@ export default function WayLinePage({ onRefresh }) {
<Col xs={24} xl={10} style={{ display: "flex", flexDirection: "column" }} >
<Card
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>{t('devices.taskOrchestrationAI')}</span>}
bordered={false}
bodyStyle={{ padding: '12px 16px' }}
variant="borderless"
styles={{ body: { padding: '12px 16px' } }}
style={{ ...cardStyle, marginBottom: 8 }}
>
<Table
@@ -1025,8 +1026,8 @@ export default function WayLinePage({ onRefresh }) {
<Card
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>{t('devices.waylineParamsTitle')}</span>}
extra={<Text type="secondary" style={{ fontSize: 12 }}>{t('devices.currentWayline')}{selectedTask?.name || '-'}</Text>}
bordered={false}
bodyStyle={{ padding: '16px' }}
variant="borderless"
styles={{ body: { padding: '16px' } }}
style={{ ...cardStyle, marginBottom: 8 }}
>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '8px', marginBottom: 8 }}>
@@ -1141,8 +1142,8 @@ export default function WayLinePage({ onRefresh }) {
<Card
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>{t('devices.weatherWindowTitle')}</span>}
bordered={false}
bodyStyle={{ padding: '16px' }}
variant="borderless"
styles={{ body: { padding: '16px' } }}
style={{ ...cardStyle, flex: 1, }}
>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: '8px', marginBottom: 16 }}>
@@ -1248,8 +1249,8 @@ export default function WayLinePage({ onRefresh }) {
/>
</div>
}
bordered={false}
bodyStyle={{ padding: '12px', display: 'flex', flexDirection: 'column', height: 480 }}
variant="borderless"
styles={{ body: { padding: '12px', display: 'flex', flexDirection: 'column', height: 480 } }}
style={{ ...cardStyle, height: '100%' }}
>
<Input
@@ -1313,8 +1314,8 @@ export default function WayLinePage({ onRefresh }) {
<Card
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>{t('devices.ganttViewTitle')} <Text type="secondary" style={{ fontSize: 11, fontWeight: 400, marginLeft: 12 }}></Text></span>}
extra={<MoreOutlined style={{ cursor: 'pointer', fontSize: 18 }} />}
bordered={false}
bodyStyle={{ padding: 16 }}
variant="borderless"
styles={{ body: { padding: 16 } }}
style={{ ...cardStyle, height: '100%' }}
>
<div style={{ position: 'relative', border: '1px solid #F2F3F5', borderRadius: 8, padding: '16px 12px' }}>
@@ -1393,8 +1394,8 @@ export default function WayLinePage({ onRefresh }) {
</Space>
</div>
}
bordered={false}
bodyStyle={{ padding: 16 }}
variant="borderless"
styles={{ body: { padding: 16 } }}
style={{ ...cardStyle, height: '100%', maxHeight: 480, overflow: "auto" }}
>
<Row gutter={10}>

View File

@@ -116,7 +116,7 @@ const XboxController = ({ onGamepadUpdate, handleControlModal, isControlled, isC
return (
<div className="relative h-[252px] aspect-[4/3] flex items-center justify-center "
onClick={() => {
if (connected && !isControlled && hasControlRight) {
if (!isControlled) {
console.log("点击了无控制权限,发起权限请求");
// 示例:调用你代码中已有的权限请求弹窗逻辑
handleControlModal({ control: true, type: "switchPermission" });
@@ -125,7 +125,7 @@ const XboxController = ({ onGamepadUpdate, handleControlModal, isControlled, isC
{/* Gamepad Status Badge */}
<div
className={`absolute top-0 right-0 px-2 py-0.5 rounded text-[8px] font-bold tracking-widest border
${connected || !hasControlRight
${connected
? isConnecting
? 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30'
: isControlled
@@ -137,8 +137,6 @@ const XboxController = ({ onGamepadUpdate, handleControlModal, isControlled, isC
>
{!connected ? (
t('devices.controllerNotConnected')
) : !hasControlRight ? (
t('devices.otherWebConnected')
) : isConnecting ? (
t('devices.control2')
) : isControlled ? (

View File

@@ -1,8 +1,8 @@
import throttle from 'lodash/throttle';
import { wsManager } from '../WebSocketManager.ts';
import {
getControlRight,
getJurisdiction,
releaseControl
} from '../../api/device.ts';
import envConfig from '../../../env';
@@ -17,7 +17,7 @@ export default class DeviceController {
envConfig.WS_URL || 'wss://ri.satabot.com/ws/';
this.connectedDeviceSn = null;
this.currentDevice = null; // 保存当前设备
this.currentDevice = null;
this.requestDeviceId = '';
@@ -27,22 +27,27 @@ export default class DeviceController {
this.wsStatus = 'disconnected';
this.wsDeviceInfo = {};
this.locationInfo = {};
this.controlTimeout = null;
this.keepAliveInterval = null; // 后台保活定时器
this.keepAliveInterval = null;
this.onDeviceInfoUpdate = null;
this.onLocationInfoUpdate = null;
this.onControlStatusChange = null;
this.onWsStatusChange = null;
this.onHasControlRightChange = null;
this.onRequestControlStatusChange = null;
this.onPermissionRequest = null;
this.onRoutePost = null;
this.onIsFinished = null;
this.handleArrivedPoint = null;
this.handleWsMessage = this.handleWsMessage.bind(this);
this.handleWsStatus = this.handleWsStatus.bind(this);
this.handleVisibilityChange = this.handleVisibilityChange.bind(this);
this.handleBeforeUnload = this.handleBeforeUnload.bind(this);
this.sendGamepad = throttle(
this.sendGamepad.bind(this),
@@ -55,36 +60,38 @@ export default class DeviceController {
this.unSubMessage = wsManager.onMessage(this.handleWsMessage);
this.unSubStatus = wsManager.onStatusChange(this.handleWsStatus);
// ========== 【集成】页面可见性监听 ==========
document.addEventListener('visibilitychange', this.handleVisibilityChange);
document.addEventListener('beforeunload', this.handleBeforeUnload);
}
levae() {
wsManager.close();
// ========== 【集成】后台保活定时 ==========
//this.startKeepAlive();
}
// ========== 【集成】页面切前台重连 ==========
handleVisibilityChange() {
// 切后台:关闭 WS
if (document.visibilityState === 'hidden') {
console.log('🔌 页面切后台,关闭 WebSocket');
console.log('Page hidden, release control');
//this.release();
wsManager.close();
this.connectedDeviceSn = null;
this.isControlled = false;
this.onControlStatusChange?.(false);
}
if (document.visibilityState === 'visible' && this.currentDevice) {
if (!this.currentDevice?.serialNumber) return;
console.log('✅ 页面切回前台,自动重连 WebSocket');
console.log('Page visible, reconnect WebSocket');
setTimeout(() => {
this.connectDevice(this.currentDevice);
}, 500);
}
}
// ========== 【集成】后台保活 ==========
handleBeforeUnload() {
console.log('Browser closing/refreshing, release control');
this.release();
}
startKeepAlive() {
this.keepAliveInterval = setInterval(() => {
if (
@@ -92,47 +99,50 @@ export default class DeviceController {
this.wsStatus !== 'connected' &&
this.currentDevice?.serialNumber
) {
console.log('🔄 后台保活:尝试重连 WebSocket');
console.log('Keep alive: reconnect WebSocket');
this.connectDevice(this.currentDevice);
}
}, 8000);
}
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);
}
async release() {
if (!this.connectedDeviceSn) return;
const data = {
platform: 'web',
type: 1,
deviceId: this.connectedDeviceSn,
token: this.userInfo.token,
};
const url = `${envConfig.baseURL}/forward/device/releaseControl`;
try {
await releaseControl(data);
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
navigator.sendBeacon(url, blob);
console.log('releaseControl sent');
} catch (e) {
console.error('Failed to send releaseControl:', e);
}
}
destroy() {
this.clearControlTimeout();
// 清理保活
if (this.keepAliveInterval) {
clearInterval(this.keepAliveInterval);
this.keepAliveInterval = null;
}
// 移除监听
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
document.removeEventListener('beforeunload', this.handleBeforeUnload);
this.release();
this.levae();
this.unSubMessage?.();
this.unSubStatus?.();
//this.release();
//wsManager.close();
this.connectedDeviceSn = null;
}
handleWsStatus(status) {
@@ -148,7 +158,6 @@ export default class DeviceController {
async connectDevice(device) {
if (!device) return;
// 保存当前设备,用于保活/重连
this.currentDevice = device;
if (!device.serialNumber) return;
@@ -156,31 +165,17 @@ export default class DeviceController {
this.connectedDeviceSn = null;
this.wsDeviceInfo = {};
this.locationInfo = {};
this.isControlled = false;
this.onControlStatusChange?.(false);
wsManager.close();
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);
console.log('Connecting to', this.wsUrl);
wsManager.connect(sn, this.wsUrl);
this.connectedDeviceSn = sn;
if (!this.hasControlRight) {
this.messageApi.warning(i18n.t('devices.fixed2OthersControlFixed2'));
return;
}
await this.getControl(sn);
} catch (err) {
console.error(err);
@@ -188,11 +183,42 @@ export default class DeviceController {
}
}
async switchDevice(device) {
if (!device) return;
if (!device.serialNumber) return;
const newSn = device.serialNumber;
if (this.connectedDeviceSn === newSn && this.wsStatus === 'connected') {
console.log('switchDevice: same device, skip');
return;
}
console.log('Connecting to switchDevice: switching to', newSn);
this.currentDevice = device;
this.connectedDeviceSn = newSn;
this.wsDeviceInfo = {};
this.locationInfo = {};
this.isControlled = false;
this.onControlStatusChange?.(false);
wsManager.send({
type: 'AUTH',
token: this.userInfo.token,
userName: this.userInfo.username,
deviceId: newSn,
});
await this.getControl(newSn);
}
async getControl(serialnum) {
try {
const res = await getJurisdiction({
platform: 'web',
deviceId: serialnum,
token: this.userInfo.token,
});
this.isControlled = res?.code === 200 && res?.data?.remoteControl === true;
@@ -206,10 +232,7 @@ export default class DeviceController {
requestControl() {
if (this.isRequestControl) return;
if (!this.hasControlRight) {
this.messageApi.warning(i18n.t('devices.noneControl'));
return;
}
this.isRequestControl = true;
this.onRequestControlStatusChange?.(true);
@@ -252,7 +275,7 @@ export default class DeviceController {
}
handleWsMessage(msg) {
console.log('📨 收到消息:', msg);
console.log('WS message:', msg);
switch (msg.type) {
case 'deviceInfo': {
const data = {};
@@ -263,6 +286,15 @@ export default class DeviceController {
this.onDeviceInfoUpdate?.(data);
break;
}
case 'locationInfo': {
const data = {};
msg.data.forEach((item) => {
data[item.name] = item.value;
});
this.locationInfo = data;
this.onLocationInfoUpdate?.(data);
break;
}
case 'AUTH': {
if (msg.isAuth) {
@@ -277,7 +309,7 @@ export default class DeviceController {
this.isRequestControl = false;
this.onRequestControlStatusChange?.(false);
if (msg.success === false || msg?.respond?.switchResult === false) {
if (msg?.success === false || msg?.respond?.switchResult === false) {
this.isControlled = false;
this.onControlStatusChange?.(false);
this.messageApi.error({ key: 'control', content: msg.message || i18n.t('devices.controlFailed') });
@@ -291,19 +323,16 @@ export default class DeviceController {
case "device_task_change": {
if (msg.deviceId != this.connectedDeviceSn) return;
this.onIsFinished?.(true);
console.log('🔌 控制结果任务变更,', msg);
console.log('task change:', msg);
break;
}
//完成点
case "device_task_arrive_point": {
if (msg.deviceId != this.connectedDeviceSn) return;
this.handleArrivedPoint?.(msg.entity);
console.log('🔌 到达完成点,', msg);
console.log('arrive point:', msg);
break;
}
case 'switchPermission': {
const platform = (msg.platform || '').toLowerCase();
if (!platform || platform === 'null') return;
@@ -330,6 +359,14 @@ export default class DeviceController {
});
break;
case 'route_post':
console.log('route post:', msg);
if (msg.deviceId) {
this.onRoutePost?.(msg.data);
}
break;
default:
break;
}

View File

@@ -1,6 +1,5 @@
import React from 'react';
import React, { useEffect, useRef, useState } from 'react';
import {
ResponsiveContainer,
LineChart,
Line,
XAxis,
@@ -15,8 +14,10 @@ import {
Bar,
} from 'recharts';
// =========================
// 默认颜色池
// =========================
const DEFAULT_COLORS = [
'#1677ff',
'#52c41a',
@@ -29,11 +30,114 @@ const DEFAULT_COLORS = [
];
// =========================
// 通用自适应容器
// =========================
//
// 不再使用 ResponsiveContainer。
// 通过 ResizeObserver 获取真实尺寸,
// 只有 width / height > 0 时才渲染 Recharts。
// 这样可以彻底避免:
//
// width(-1) and height(-1)
//
// =========================
const ChartResizeContainer = ({
children,
width = '100%',
height = 360,
className,
style,
}) => {
const containerRef = useRef(null);
const [size, setSize] = useState({
width: 0,
height: 0,
});
useEffect(() => {
const element = containerRef.current;
if (!element) {
return;
}
const updateSize = () => {
const rect = element.getBoundingClientRect();
const nextWidth = Math.floor(rect.width);
const nextHeight = Math.floor(rect.height);
// 非法尺寸直接忽略
if (nextWidth <= 0 || nextHeight <= 0) {
return;
}
setSize((prev) => {
if (
prev.width === nextWidth &&
prev.height === nextHeight
) {
return prev;
}
return {
width: nextWidth,
height: nextHeight,
};
});
};
// 首次主动测量
updateSize();
// 后续监听尺寸变化
const observer = new ResizeObserver(() => {
updateSize();
});
observer.observe(element);
return () => {
observer.disconnect();
};
}, []);
return (
<div
ref={containerRef}
className={className}
style={{
width,
height,
minWidth: 0,
minHeight: 0,
boxSizing: 'border-box',
overflow: 'hidden',
...style,
}}
>
{size.width > 0 && size.height > 0
? React.cloneElement(children, {
width: size.width,
height: size.height,
})
: null}
</div>
);
};
// =========================
// Universal Line Chart
// =========================
export const UniversalLineChart = ({
data,
data = [],
xKey,
series,
series = [],
width = '100%',
height = 360,
showGrid = true,
@@ -43,6 +147,7 @@ export const UniversalLineChart = ({
smooth = true,
dot = false,
className,
style,
}) => {
return (
<div
@@ -50,48 +155,80 @@ export const UniversalLineChart = ({
style={{
width,
height,
minWidth: 0,
minHeight: 0,
boxSizing: 'border-box',
background: backgroundColor,
borderRadius: 12,
padding: 12,
overflow: 'hidden',
...style,
}}
>
<ResponsiveContainer width="100%" height="100%">
<ChartResizeContainer
width="100%"
height="100%"
>
<LineChart data={data}>
{showGrid && (
<CartesianGrid strokeDasharray="3 3" opacity={0.25} />
<CartesianGrid
strokeDasharray="3 3"
opacity={0.25}
/>
)}
<XAxis dataKey={xKey} />
<YAxis />
{showTooltip && <Tooltip />}
{showLegend && <Legend />}
{series.map((item, index) => (
<Line
key={item.key}
type={smooth ? 'monotone' : 'linear'}
type={
smooth
? 'monotone'
: 'linear'
}
dataKey={item.key}
name={item.name}
stroke={item.color || DEFAULT_COLORS[index % DEFAULT_COLORS.length]}
strokeWidth={item.strokeWidth || 2}
strokeDasharray={item.dashed ? '6 6' : undefined}
stroke={
item.color ||
DEFAULT_COLORS[
index %
DEFAULT_COLORS.length
]
}
strokeWidth={
item.strokeWidth || 2
}
strokeDasharray={
item.dashed
? '6 6'
: undefined
}
dot={dot}
activeDot={{ r: 6 }}
/>
))}
</LineChart>
</ResponsiveContainer>
</ChartResizeContainer>
</div>
);
};
// =========================
// Universal Bar Chart
// =========================
export const UniversalBarChart = ({
data,
data = [],
xKey,
series,
series = [],
width = '100%',
height = 360,
showGrid = true,
@@ -102,6 +239,7 @@ export const UniversalBarChart = ({
radius = 6,
stack = false,
className,
style,
}) => {
return (
<div
@@ -109,21 +247,34 @@ export const UniversalBarChart = ({
style={{
width,
height,
minWidth: 0,
minHeight: 0,
boxSizing: 'border-box',
background: backgroundColor,
borderRadius: 12,
padding: 12,
overflow: 'hidden',
...style,
}}
>
<ResponsiveContainer width="100%" height="100%">
<ChartResizeContainer
width="100%"
height="100%"
>
<BarChart data={data}>
{showGrid && (
<CartesianGrid strokeDasharray="3 3" opacity={0.25} />
<CartesianGrid
strokeDasharray="3 3"
opacity={0.25}
/>
)}
<XAxis dataKey={xKey} />
<YAxis />
{showTooltip && <Tooltip />}
{showLegend && <Legend />}
{series.map((item, index) => (
@@ -131,22 +282,35 @@ export const UniversalBarChart = ({
key={item.key}
dataKey={item.key}
name={item.name}
fill={item.color || DEFAULT_COLORS[index % DEFAULT_COLORS.length]}
fill={
item.color ||
DEFAULT_COLORS[
index %
DEFAULT_COLORS.length
]
}
barSize={barSize}
radius={radius}
stackId={stack ? 'stack' : undefined}
stackId={
stack
? 'stack'
: undefined
}
/>
))}
</BarChart>
</ResponsiveContainer>
</ChartResizeContainer>
</div>
);
};
// =========================
// Universal Pie Chart
// =========================
export const UniversalPieChart = ({
data,
data = [],
width = '100%',
height = 360,
showLegend = true,
@@ -157,7 +321,7 @@ export const UniversalPieChart = ({
paddingAngle = 2,
showLabel = true,
className,
style
style,
}) => {
return (
<div
@@ -165,15 +329,23 @@ export const UniversalPieChart = ({
style={{
width,
height,
minWidth: 0,
minHeight: 0,
boxSizing: 'border-box',
background: backgroundColor,
borderRadius: 12,
padding: 12,
...style
overflow: 'hidden',
...style,
}}
>
<ResponsiveContainer width="100%" height="100%">
<ChartResizeContainer
width="100%"
height="100%"
>
<PieChart>
{showTooltip && <Tooltip />}
{showLegend && <Legend />}
<Pie
@@ -190,37 +362,86 @@ export const UniversalPieChart = ({
{data.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.color || DEFAULT_COLORS[index % DEFAULT_COLORS.length]}
fill={
entry.color ||
DEFAULT_COLORS[
index %
DEFAULT_COLORS.length
]
}
/>
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</ChartResizeContainer>
</div>
);
};
// =========================
// 使用示例
// =========================
export const DemoCharts = () => {
const lineData = [
{ month: '1月', uv: 400, pv: 240, amt: 240 },
{ month: '2月', uv: 300, pv: 456, amt: 300 },
{ month: '3月', uv: 500, pv: 139, amt: 200 },
{ month: '4月', uv: 278, pv: 390, amt: 278 },
{ month: '5月', uv: 189, pv: 480, amt: 189 },
{
month: '1月',
uv: 400,
pv: 240,
amt: 240,
},
{
month: '2月',
uv: 300,
pv: 456,
amt: 300,
},
{
month: '3月',
uv: 500,
pv: 139,
amt: 200,
},
{
month: '4月',
uv: 278,
pv: 390,
amt: 278,
},
{
month: '5月',
uv: 189,
pv: 480,
amt: 189,
},
];
const pieData = [
{ name: 'Vue', value: 40 },
{ name: 'React', value: 35 },
{ name: 'Angular', value: 25 },
{
name: 'Vue',
value: 40,
},
{
name: 'React',
value: 35,
},
{
name: 'Angular',
value: 25,
},
];
return (
<div className="grid grid-cols-1 gap-6 p-6">
<div
className="
grid
grid-cols-1
gap-6
p-6
min-w-0
"
>
<UniversalLineChart
data={lineData}
xKey="month"

View File

@@ -1,53 +1,188 @@
import { useEffect, useRef } from 'react';
import { useEffect, useRef, useCallback, useState } from 'react';
import mqtt, { MqttClient } from 'mqtt';
interface Options {
url: string;
topic: string | string[];
onMessage: (topic: string, message: string) => void;
/* ============================================================
* MQTT 连接池 —— 同一 URL 共享一个连接,避免重复建连
* ============================================================ */
const connectionPool = new Map<string, { client: MqttClient; refCount: number }>();
function getOrCreateClient(url: string, options?: mqtt.IClientOptions): MqttClient {
const entry = connectionPool.get(url);
if (entry) {
entry.refCount++;
return entry.client;
}
const client = mqtt.connect(url, {
reconnectPeriod: 3000,
clean: true,
...options,
});
connectionPool.set(url, { client, refCount: 1 });
// 连接断开后自动清理池条目
client.on('close', () => {
const cur = connectionPool.get(url);
if (cur && cur.client === client) {
connectionPool.delete(url);
}
});
return client;
}
export function useMqtt({ url, topic, onMessage }: Options) {
const clientRef = useRef<MqttClient | null>(null);
function releaseClient(url: string): void {
const entry = connectionPool.get(url);
if (!entry) return;
entry.refCount--;
if (entry.refCount <= 0) {
entry.client.end(true);
connectionPool.delete(url);
}
}
/* ============================================================
* useMqtt Hook
* ============================================================ */
interface UseMqttOptions {
/** broker 地址,如 wss://xxx/mqtt */
url: string;
/** 要订阅的主题(字符串或数组),传空则不订阅 */
topic: string | string[];
/** 收到消息的回调 */
onMessage: (topic: string, message: string) => void;
/** 是否启用订阅,默认 true;设为 false 时不会建立连接 */
enabled?: boolean;
/** MQTT 连接额外配置(可选) */
connectOptions?: mqtt.IClientOptions;
}
interface UseMqttReturn {
/** 当前连接状态 */
connected: boolean;
/** 手动发布消息 */
publish: (topic: string, message: string) => void;
}
export function useMqtt({
url,
topic,
onMessage,
enabled = true,
connectOptions,
}: UseMqttOptions): UseMqttReturn {
const clientRef = useRef<MqttClient | null>(null);
const onMessageRef = useRef(onMessage);
const [connected, setConnected] = useState(false);
// 始终保持 onMessage 回调为最新引用,避免闭包过期
useEffect(() => {
const client = mqtt.connect(url, {
reconnectPeriod: 3000,
onMessageRef.current = onMessage;
}, [onMessage]);
// 将 topic 统一转为数组,方便做 diff
const topicList = (Array.isArray(topic) ? topic : [topic]).filter(Boolean);
const topicKey = topicList.join(',');
/* ---------- 核心:建连 / 订阅 / 清理 ---------- */
useEffect(() => {
if (!enabled || !url) return;
const client = getOrCreateClient(url, {
username: 'maibu',
password: 'jsmbzn520',
clean: true,
...connectOptions,
});
clientRef.current = client;
client.on('connect', () => {
console.log('MQTT connected');
client.subscribe(topic);
});
client.on('message', (t, payload) => {
onMessage(t, payload.toString());
});
client.on('error', err => {
console.error('MQTT error', err);
});
return () => {
client.end(true);
/* --- 连接成功 → 订阅主题 --- */
const handleConnect = () => {
setConnected(true);
if (topicList.length > 0) {
client.subscribe(topicList, (err) => {
if (err) console.error('[useMqtt] subscribe failed:', err);
});
}
};
}, [url, JSON.stringify(topic)]);
return clientRef;
/* --- 收到消息 → 回调 --- */
const handleMessage = (t: string, payload: Buffer) => {
console.log(payload.toString(), "订阅无人机数据")
onMessageRef.current(t, payload.toString());
};
/* --- 连接状态变化 --- */
const handleReconnect = () => setConnected(false);
const handleOffline = () => setConnected(false);
const handleError = (err: Error) => {
console.error('[useMqtt] error:', err);
};
client.on('connect', handleConnect);
client.on('message', handleMessage);
client.on('reconnect', handleReconnect);
client.on('offline', handleOffline);
client.on('error', handleError);
// 如果连接已经建立,直接订阅
if (client.connected && topicList.length > 0) {
client.subscribe(topicList);
setConnected(true);
}
/* --- 清理:退出路由时取消订阅 & 释放连接 --- */
return () => {
// 取消当前 hook 订阅的主题
if (topicList.length > 0) {
client.unsubscribe(topicList);
}
// 移除本 hook 注册的事件监听
client.removeListener('connect', handleConnect);
client.removeListener('message', handleMessage);
client.removeListener('reconnect', handleReconnect);
client.removeListener('offline', handleOffline);
client.removeListener('error', handleError);
setConnected(false);
// 释放连接池引用(refCount=0 时自动断开)
releaseClient(url);
clientRef.current = null;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [enabled, url, topicKey]);
/* ---------- 主题热更新:topic 变化时 diff 订阅 ---------- */
const prevTopicKeyRef = useRef(topicKey);
useEffect(() => {
const client = clientRef.current;
if (!client || !client.connected) return;
const prevKey = prevTopicKeyRef.current;
prevTopicKeyRef.current = topicKey;
if (prevKey === topicKey) return;
// 取消旧主题
const prevTopics = prevKey ? prevKey.split(',').filter(Boolean) : [];
if (prevTopics.length > 0) {
client.unsubscribe(prevTopics);
}
// 订阅新主题
if (topicList.length > 0) {
client.subscribe(topicList);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [topicKey]);
/* ---------- 手动发布 ---------- */
const publish = useCallback((t: string, message: string) => {
const client = clientRef.current;
if (client && client.connected) {
client.publish(t, message);
} else {
console.warn('[useMqtt] publish skipped: not connected');
}
}, []);
return { connected, publish };
}
/* 使用 */
// useMqtt({
// url: 'wss://broker.emqx.io:8084/mqtt',
// topic: 'robot/status',
// onMessage: (_, message) => {
// setMsg(message);
// },
// });

View File

@@ -154,6 +154,11 @@ h4, .ant-typography h4 { font-size: clamp(1rem, 1.5vw, 1.25rem) !important; }
background-color: #f0f7ff !important;
}
.recharts-responsive-container>div{
width: 100% !important;
height: 100% !important;
}

View File

@@ -241,6 +241,7 @@ const en = {
},
devices: {
deviceManagement: 'Device Management',
distributable: 'Distributable',
deviceOverview: 'Device Overview',
deviceStatus: 'Device Status',
waylineTask: 'Wayline Tasks',
@@ -659,6 +660,8 @@ const en = {
optimize: 'Optimize',
optionalEmpty: 'Optional Empty',
otherWebControlling: 'Other Web Controlling',
platformRequestControl: "{{platform}} platformRequestControl",
outDock: 'Out Dock',
outdoor: 'Outdoor',
panorama: 'Panorama',
@@ -927,6 +930,8 @@ const en = {
unitJia: '',
unitTai: '',
unitTiao: '',
normalCancel: "normalCancel",
forceCancel: "forceCancel",
unknownDevice: 'Unknown Device',
unknownDrone: 'Unknown Drone',
unknownObject: 'Unknown Object',
@@ -987,6 +992,9 @@ const en = {
working: 'Working',
workingCurrent: 'Working Current',
zoomLens: 'Zoom Lens',
controllerNotConnected: "controllerNotConnected",
otherWebConnected: "otherWebConnected",
clickToRequestControl: "clickToRequestControl",
fixed2TaskSuspended2: 'Fixed2 Task Suspended2 态势',
@@ -1021,11 +1029,18 @@ const en = {
fixed2Video: 'Fixed2 Video 已配',
batteryFixed2: '电池 Battery Fixed2',
noFix: 'noFix',
single: 'single',
dgnss: 'dgnss',
ins: 'ins',
singleNoHeading: 'singleNoHeading',
dgnssNoHeading: 'dgnssNoHeading',
rtkFixedNoHeading: 'rtkFixedNoHeading',
rtkFloatNoHeading: 'rtkFloatNoHeading',
s62: 'Signal/Satellite',
s72: 'Wind Speed',
distributable: 'Distributable',
UnitsRobot: '',
UnitsDrone: '',
executing: 'Executing',
@@ -1280,9 +1295,10 @@ const en = {
failedFixed22: '权限校验 Failed Fixed2',
noneControl: 'None Control 权限',
noneControl: 'None Control jurisdiction',
fixed2ControlFixed2: 'Fixed2 Control 请求 超时 Fixed2',
controlFailed: "controlFailed",
},
video: {
onlineVideo: 'Online Video',
@@ -3174,7 +3190,6 @@ const en = {
deptRnd: 'R&D Department',
editMenu: 'Edit Menu',
editOrganization: 'Edit Organization',
@@ -3376,7 +3391,6 @@ const en = {
abnormal: 'Abnormal',
addSuccess: 'Add Success',
confirmDeleteRole: 'Confirm Delete Role',
editRole: 'Edit Role',
editSuccess: 'Edit Success',
manageMembers: 'Manage Members',
menuPermissionConfig: 'Menu Permission Config',

View File

@@ -243,7 +243,18 @@ const zh = {
},
devices: {
noFix: '无定位',
single: '单点定位',
dgnss: '差分定位',
ins: '惯导定位',
rtkFixed: 'RTK固定解',
rtkFloat: 'RTK浮点解',
singleNoHeading: '单点定位(无航向)',
dgnssNoHeading: '差分定位(无航向)',
rtkFixedNoHeading: 'RTK固定解(无航向)',
rtkFloatNoHeading: 'RTK浮点解(无航向)',
deviceManagement: '设备管理',
distributable: '可调度',
deviceOverview: '设备总览',
deviceStatus: '设备状态',
waylineTask: '航线任务',
@@ -321,10 +332,13 @@ const zh = {
statusPaused: '暂停中',
statusFinished: '执行成功',
statusFailed: '执行失败',
statusCanceling: '取消中',
confirmDeleteTask: '确定要删除该任务吗?',
confirmCancelTask: '确定要取消该任务吗?',
confirmPauseTask: '确定要暂停该任务吗?',
confirmResumeTask: '确定要恢复该任务吗?',
normalCancel: "普通取消",
forceCancel: "强制取消",
recover: '恢复',
statTodayPlan: '今日机器人计划',
unitPlan: '条',
@@ -341,7 +355,7 @@ const zh = {
good: '良好',
robotRouteMonitor: '机器人路径规划与监控',
clearRealtimeTrack: '清空实时轨迹',
clearPlanRoute: '清空规划航线',
clearPlanRoute: '清空规划区域',
controlMode: '控制模式:',
localMode: '本地',
remoteMode: '远程',
@@ -369,7 +383,6 @@ const zh = {
currentTemp: '当前温度',
humidity: '环境湿度',
positionAccuracy: '定位精度',
rtkFixed: 'RTK 固定',
networkLatency: '网络延迟',
waterRiskWarning: '注意:区域 B 存在积水风险,建议避开',
presetRoutes: '预设路线',
@@ -413,7 +426,8 @@ const zh = {
selectExecDate: '选择执行日期',
execTime: '执行时间',
saveTask: '保存任务',
abMode: '模式',
abMode: '自定义模式',
saveFail: '保存失败',
activatedTime: '已激活时间',
aiAction1: 'AI',
aiAction2: 'AI',
@@ -444,14 +458,14 @@ const zh = {
apiRequestException: 'API请求异常',
autoFetch: '自动获取',
autoResumeFromBreakpoint: '自动恢复从断点',
backView: '返回查看',
backView: '后视角',
batteryCapacity: '电量容量',
batteryWithPercent: '电量带百分比',
batteryWithValue: '电量带值',
blockUnit: '区块单位',
boardTaskFetchFail: '看板任务获取失败',
boundDrone: '边界无人机',
bowMode: '船首模式',
bowMode: '弓字模式',
canWorkNormally: '可工作正常',
cancelReturnHome: '取消返航首页',
chargePoint: '充电点',
@@ -484,8 +498,8 @@ const zh = {
controlLogTitle: '控制日志标题',
controlRequestContent: '控制请求内容',
countUnit: '数量单位',
coverClosed: '覆盖已关闭',
coverOpen: '覆盖打开',
coverClosed: '已关闭',
coverOpen: '打开',
coverageEquipment: '覆盖设备',
coverageValue: '{{types}}/{{units}}',
createSuccess: '创建成功',
@@ -554,11 +568,17 @@ const zh = {
fillRequiredFields: '填写必填字段',
firmwareVersion: '固件版本',
fixed: '固定',
controllerNotConnected: "控制器未连接",
otherWebConnected: "其他网页已连接",
clickToRequestControl: "点击请求控制",
controlling: "控制中",
fixed2: '',
fixedEnv: '固定环境',
flightAltitude: '飞行高度',
flightWindSpeed: '飞行风速度',
frontView: '前查看',
frontView: '前视角',
ganttViewTitle: '甘特查看标题',
generateTask: '生成任务',
gnssQuality: 'GNSS质量',
@@ -575,7 +595,7 @@ const zh = {
headingAngle: '航向角度',
horizontalSpeed: '水平速度',
hotspotSuspect: '热斑疑似',
inDock: '内停靠',
inDock: '舱内',
inProgressDesc: '进行中',
indoor: '室内',
inputCustomAlias: '输入自定义别名',
@@ -591,7 +611,7 @@ const zh = {
issueTaskFirst: '问题任务先',
landingDockSn: '降落停靠序列号',
landingDockSnLabel: '降落停靠序列号标签',
leftView: '左查看',
leftView: '左视角',
leftWheelDrive: '左轮驱动',
legendTitle: '图例标题',
levelHigh: '级别高',
@@ -610,12 +630,12 @@ const zh = {
loginInfoExpired: '登录提示已过期',
lostControlAction: '丢失控制操作',
lostControlActionLabel: '丢失控制操作标签',
markPoint: '标记点',
markPoint: '打点',
markedPointsCount: '已标记点数量',
minBattery: '最小电量',
minBatteryLabel: '最小电量标签',
minExecBatteryLabel: '最小执行电量标签',
minThreePoints: '最小三点',
minThreePoints: '最少三个点',
modeAuto: '模式自动',
modeDebugLocal: '模式调试本地',
modeDebugRemote: '模式调试远程',
@@ -662,7 +682,7 @@ const zh = {
optimize: '优化',
optionalEmpty: '可选空',
otherWebControlling: '其他网页控制中',
outDock: '外停靠',
outDock: '舱外',
outdoor: '室外',
panorama: '全景',
paramArmChannel: '参数布防渠道',
@@ -728,8 +748,8 @@ const zh = {
positionInStation: '位置内电站',
precipStatus: '降水状态',
productType: '产品类型',
rainNo: '雨无',
rainYes: '雨是',
rainNo: '无',
rainYes: '降雨',
rainfall: '降雨',
realtimeMonitorList: '实时监控列表',
realtimePicture: '实时图片',
@@ -762,7 +782,7 @@ const zh = {
returnHomeAltitudeLabel: '返航首页高度标签',
returnHomeMode: '返航首页模式',
returnHomeModeLabel: '返航首页模式标签',
rightView: '右查看',
rightView: '右视角',
rightWheelDrive: '右轮驱动',
robotExecutingCount: '机器人执行中数量',
robotInTaskCount: '机器人内任务数量',
@@ -777,34 +797,33 @@ const zh = {
routeLoadedWithName: '路线已加载带名称',
routeNoPathPoints: '路线编号路径点',
routeSavedLocal: '路线已保存本地',
rtBattery: '实时电量',
rtHeadingAngle: '实时航向角度',
rtHeadingStatus: '实时航向状态',
rtInitialized: '实时已初始化',
rtLeftMotor: '实时左电机',
rtLeftSpeed: '实时左速度',
rtLeftTemp: '实时左温度',
rtMainTemp: '实时主温度',
rtMode: '实时模式',
rtPitchAngle: '实时俯仰角度',
rtPositioning: '实时定位',
rtRightMotor: '实时右电机',
rtRightSpeed: '实时右速度',
rtRightTemp: '实时右温度',
rtSatellites: '实时卫星',
rtVoltage: '实时电压',
rtBattery: '电量',
rtHeadingAngle: '航向角度',
rtHeadingStatus: '航向状态',
rtInitialized: '已初始化',
rtLeftMotor: '左电机',
rtLeftSpeed: '左速度',
rtLeftTemp: '左温度',
rtMainTemp: '主温度',
rtMode: '模式',
rtPitchAngle: '俯仰角度',
rtPositioning: '定位',
rtRightMotor: '右电机',
rtRightSpeed: '右速度',
rtRightTemp: '右温度',
rtSatellites: '卫星数量',
rtVoltage: '电压',
rthMode: '返航模式',
rthOptimal: '返航最优',
rthPreset: '返航预设',
rtkCount: 'RTK数量',
rtkEnvLabel: 'RTK环境标签',
rtkFixed2: 'RTK',
rtkFloat: 'RTK浮点',
rtkFixed2: 'RTK固定',
rtkHighPrecision: 'RTK高精度',
rtkModeDesc: 'RTK模式描述',
rtkStatus: 'RTK状态',
safeRthBattery: '安全返航电量',
satelliteCount: '卫星数量',
satelliteCount: '卫星数量 {{count}}',
savePathTitle: '保存路径标题',
savePlan: '保存计划',
saving: '节省',
@@ -812,11 +831,11 @@ const zh = {
searchWaylinePlaceholder: '搜索航线占位',
selectDevice: '选择设备',
selectDrone: '选择无人机',
selectExecuteDroneFirst: '选择执行无人机先',
selectNoDrone: '选择无无人机',
selectStartTimePlaceholder: '选择开始时间占位',
selectExecuteDroneFirst: '执行无人机',
selectNoDrone: '无无人机',
selectStartTimePlaceholder: '开始时间',
selectTaskFirst: '选择任务先',
selectTaskStartTime: '选择任务开始时间',
selectTaskStartTime: '任务开始时间',
selectWayline: '选择航线',
selectedWaylineColon: '已选航线冒号',
signalSatellite: '信号卫星',
@@ -830,18 +849,18 @@ const zh = {
stationStatus: '电站状态',
stationTopology: '电站拓扑',
stationVideo: '电站视频',
statusExecFailed: '状态执行失败',
statusExecSuccess: '状态执行成功',
statusExecFailed: '执行失败',
statusExecSuccess: '执行成功',
statusPendingApproval: '状态待处理审批',
statusPreparing: '状态准备中',
statusRestored: '状态已恢复',
statusStartFailed: '状态开始失败',
statusSuggestExec: '状态建议执行',
statusTerminated: '状态已终止',
statusTimeout: '状态超时',
statusUpdateSuccess: '状态更新成功',
statusWaitExec: '状态等待执行',
statusWaitingStart: '状态等待中开始',
statusPreparing: '准备中',
statusRestored: '已恢复',
statusStartFailed: '开始失败',
statusSuggestExec: '建议执行',
statusTerminated: '已终止',
statusTimeout: '超时',
statusUpdateSuccess: '更新成功',
statusWaitExec: '等待执行',
statusWaitingStart: '等待中开始',
stopVideoBtn: '停止视频按钮',
suggestAction: '建议操作',
suggestTime: '建议时间',
@@ -1292,6 +1311,9 @@ const zh = {
cancel: "取消",
UnitsRobot: "台",
UnitsDrone: "架",
platformRequestControl: "{{platform}} 正在请求控制权限",
controlFailed: "controlFailed",
},
video: {
onlineVideo: '在线视频',
@@ -2641,7 +2663,6 @@ const zh = {
s36: '用户名',
s37: '关联产品',
editMenu: '编辑菜单',
addMenu: '新增菜单',
editSite: '编辑场站',
addSite: '新增场站',
@@ -3331,17 +3352,17 @@ const zh = {
male: '男',
female: '女',
notSet: '未设置',
basicInfo: '基础提示',
basicInfo: '基本信息',
changePassword: '变更密码',
confirmPasswordPlaceholder: '确认密码占位',
confirmPasswordPlaceholder: '确认密码',
confirmPasswordRequired: '确认密码必填',
emailInvalid: '邮件无效',
emailPlaceholder: '邮件占位',
newPasswordPlaceholder: '新密码占位',
oldPasswordPlaceholder: '旧密码占位',
newPasswordPlaceholder: '新密码',
oldPasswordPlaceholder: '旧密码',
passwordLengthError: '密码长度错误',
phoneInvalid: '电话无效',
remarkPlaceholder: '备注占位',
remarkPlaceholder: '备注',
requestFailed: '请求失败',
},
users: {

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState } from 'react';
import {
Row, Col, Card, Button, Tag, Table, Typography, Space, Statistic, message, Divider, Tooltip, Avatar, Empty
} from 'antd';
@@ -210,8 +210,8 @@ export default function AIDiagnosisPage() {
].map((item, index) => (
<Col key={index} xs={24} sm={12} md={8} xl={4}>
<Card
bordered={false}
bodyStyle={{ padding: '20px 16px' }}
variant="borderless"
styles={{ body: { padding: '20px 16px' } }}
style={{ ...cardStyle, height: '100%' }}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
@@ -245,8 +245,8 @@ export default function AIDiagnosisPage() {
<Card
title={<span style={{ fontSize: 15, fontWeight: 700, color: '#1D2129' }}>{t('aiAnalysis.fixed2UavInspection')}<InfoCircleOutlined style={{ marginLeft: 6, color: '#86909C', fontSize: 13 }} /></span>}
extra={<Text type="secondary" style={{ fontSize: 12 }}>{t('aiAnalysis.fixed2Inspection202505200930')}<CalendarOutlined style={{ marginLeft: 4 }} /></Text>}
bordered={false}
bodyStyle={{ padding: '16px 20px' }}
variant="borderless"
styles={{ body: { padding: '16px 20px' } }}
style={{ ...cardStyle, height: '100%' }}
>
<Row gutter={[12, 12]}>
@@ -288,8 +288,8 @@ export default function AIDiagnosisPage() {
<span>{t('aiAnalysis.s9')}<b style={{ color: '#1D2129' }}>160</b></span>
<span>{t('aiAnalysis.fixed2Abnormal')}<b style={{ color: '#F53F3F' }}>8</b></span>
</Space>}
bordered={false}
bodyStyle={{ padding: '16px 20px' }}
variant="borderless"
styles={{ body: { padding: '16px 20px' } }}
style={{ ...cardStyle, height: '100%' }}
>
<Row gutter={[20, 20]}>
@@ -383,8 +383,8 @@ export default function AIDiagnosisPage() {
<Col xs={24} lg={7}>
<Card
title={<span style={{ fontSize: 15, fontWeight: 700, color: '#1D2129' }}>{t('aiAnalysis.s13')}<InfoCircleOutlined style={{ marginLeft: 6, color: '#86909C', fontSize: 13 }} /></span>}
bordered={false}
bodyStyle={{ padding: '16px 20px' }}
variant="borderless"
styles={{ body: { padding: '16px 20px' } }}
style={{ ...cardStyle, height: '100%' }}
>
<div style={{ fontSize: 12, color: '#86909C', marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
@@ -432,8 +432,8 @@ export default function AIDiagnosisPage() {
<Col xs={24} lg={10}>
<Card
title={<span style={{ fontSize: 15, fontWeight: 700, color: '#1D2129' }}>{t('aiAnalysis.generation')}<InfoCircleOutlined style={{ marginLeft: 6, color: '#86909C', fontSize: 13 }} /></span>}
bordered={false}
bodyStyle={{ padding: '16px 20px' }}
variant="borderless"
styles={{ body: { padding: '16px 20px' } }}
style={{ ...cardStyle, height: '100%' }}
>
<div style={{ display: 'flex', gap: 20 }}>
@@ -487,8 +487,8 @@ export default function AIDiagnosisPage() {
<Col xs={24} lg={7}>
<Card
bordered={false}
bodyStyle={{ padding: 0 }}
variant="borderless"
styles={{ body: { padding: 0 } }}
style={{ ...cardStyle, height: '100%' }}
>
{/* AI诊断总结 */}
@@ -498,6 +498,7 @@ export default function AIDiagnosisPage() {
<span style={{ fontSize: 12, color: '#86909C' }}>{t('aiAnalysis.fixed2UpdatedAt202505200945')}</span>
</div>
<Table
rowKey="key"
columns={[
{ title: t('aiAnalysis.s36'), dataIndex: 'diagnosis', width: 78 },
{ title: t('roles.status'), dataIndex: 'status', render: renderStatusTag, },
@@ -524,6 +525,7 @@ export default function AIDiagnosisPage() {
<div style={{ padding: '12px 14px' }}>
<div style={{ fontSize: 14, fontWeight: 700, color: '#1D2129', marginBottom: 10 }}>{t('aiAnalysis.fixed2Ai')}</div>
<Table
rowKey="key"
columns={[
{ title: t('aiAnalysis.s39'), dataIndex: 'measure', ellipsis: true },
{ title: t('workOrder.priority'), dataIndex: 'priority', render: renderPriorityTag, width: 58 },
@@ -564,8 +566,8 @@ export default function AIDiagnosisPage() {
{/* 底部:数据来源 */}
<Card
title={<span style={{ fontSize: 15, fontWeight: 700, color: '#1D2129' }}>{t('aiAnalysis.sourceFixed2')}</span>}
bordered={false}
bodyStyle={{ padding: '16px 20px' }}
variant="borderless"
styles={{ body: { padding: '16px 20px' } }}
style={cardStyle}
>
<Row gutter={[16, 16]}>

View File

@@ -35,7 +35,6 @@ export default function AlarmCenterPage() {
<div style={{ display: 'flex', paddingBottom: "4px" }}>
<div style={{ padding: '8px 12px 0' }}>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>{t('router.alerts')}</Typography.Title>
<Text type="secondary" style={{ fontSize: '12px' }}>{t('alerts.subtitle')}</Text>
</div>
<Header style={{ padding: 0, borderBottom: '1px solid #f0f0f0', position: 'sticky', top: 0, zIndex: 10, height: 40, lineHeight: '40px' }}>

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState } from 'react';
import {
Row, Col, Card, Button, Table, Tag, Typography, Space, Statistic, DatePicker,
Divider, Progress, Badge, Select, Tooltip, Avatar
@@ -226,8 +226,8 @@ export default function CleanOptimizationPage() {
].map((item, index) => (
<Col key={index} style={{ minWidth: '168px', flex: '1 0 auto', maxWidth: "250px" }}>
<Card
bordered={false}
bodyStyle={{ padding: '16px' }}
variant="borderless"
styles={{ body: { padding: '16px' } }}
style={{ borderRadius: 8, height: 120 }}
>
<div style={{ display: 'flex', justifyContent: 'space-between', height: '100%' }}>
@@ -298,7 +298,7 @@ export default function CleanOptimizationPage() {
<Row gutter={[8, 8]} style={{ display: 'flex', alignItems: 'stretch' }}>
{/* 左侧:分析图表 */}
<Col xs={24} xl={6} style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<Card title={<span style={{ fontSize: 14, fontWeight: 700 }}>{t('clean.s6')}</span>} bordered={false} style={cardStyle} bodyStyle={{ padding: 16 }}>
<Card title={<span style={{ fontSize: 14, fontWeight: 700 }}>{t('clean.s6')}</span>} variant="borderless" style={cardStyle} styles={{ body: { padding: 16 } }}>
<div style={{ fontSize: 12, color: '#86909C', marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<span>{t('clean.s7')}</span>
<Space size={12}>
@@ -329,7 +329,7 @@ export default function CleanOptimizationPage() {
/>
</Card>
<Card title={<span style={{ fontSize: 14, fontWeight: 700 }}>{t('clean.fixed2Generation')}<Text type="secondary" style={{ fontWeight: 400, fontSize: 12 }}>{t('clean.near7Days')}</Text></span>} bordered={false} style={cardStyle} bodyStyle={{ padding: 16 }}>
<Card title={<span style={{ fontSize: 14, fontWeight: 700 }}>{t('clean.fixed2Generation')}<Text type="secondary" style={{ fontWeight: 400, fontSize: 12 }}>{t('clean.near7Days')}</Text></span>} variant="borderless" style={cardStyle} styles={{ body: { padding: 16 } }}>
<div style={{ position: 'relative', height: "100%" }}>
<UniversalPieChart
data={pieChartData}
@@ -346,7 +346,7 @@ export default function CleanOptimizationPage() {
</div>
</Card>
<Card title={<span style={{ fontSize: 14, fontWeight: 700 }}>{t('clean.fixed2Cleaning2')}<Text type="secondary" style={{ fontWeight: 400, fontSize: 12 }}>{t('clean.nearLast')}</Text></span>} bordered={false} style={cardStyle} bodyStyle={{ padding: 16 }}>
<Card title={<span style={{ fontSize: 14, fontWeight: 700 }}>{t('clean.fixed2Cleaning2')}<Text type="secondary" style={{ fontWeight: 400, fontSize: 12 }}>{t('clean.nearLast')}</Text></span>} variant="borderless" style={cardStyle} styles={{ body: { padding: 16 } }}>
<UniversalBarChart
data={compareChartData}
xKey="date"
@@ -367,7 +367,7 @@ export default function CleanOptimizationPage() {
/>
</Card>
<Card title={<span style={{ fontSize: 14, fontWeight: 700 }}>{t('clean.fixed2CleaningFixed2')}</span>} bordered={false} style={cardStyle} bodyStyle={{ padding: 16 }}>
<Card title={<span style={{ fontSize: 14, fontWeight: 700 }}>{t('clean.fixed2CleaningFixed2')}</span>} variant="borderless" style={cardStyle} styles={{ body: { padding: 16 } }}>
<Row gutter={[16, 16]}>
<Col span={12}>
<div style={{ fontSize: 11, color: '#86909C', marginBottom: 4 }}>{t('clean.generation')}</div>
@@ -398,9 +398,9 @@ export default function CleanOptimizationPage() {
<Col xs={24} xl={11} style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<Card
title={<span style={{ fontSize: 15, fontWeight: 700 }}>{t('clean.recommendedCleanBlock')}</span>}
bordered={false}
variant="borderless"
style={{ ...cardStyle, }}
bodyStyle={{ padding: 0, position: 'relative', overflow: 'hidden', minHeight: 500 }}
styles={{ body: { padding: 0, position: 'relative', overflow: 'hidden', minHeight: 500 } }}
>
<div style={{ background: '#f8fafc', padding: '10px 20px', display: 'flex', justifyContent: 'space-between', borderBottom: '1px solid #f0f0f0', flexWrap: 'wrap', gap: 8 }}>
<Space size={16} wrap style={{ fontSize: 11 }}>
@@ -455,11 +455,12 @@ export default function CleanOptimizationPage() {
<Card
title={<span style={{ fontSize: 15, fontWeight: 700 }}>{t('clean.fixed2Cleaning2')}</span>}
extra={<Button type="link" size="small">{t('devices.viewMore')}<ChevronRight size={14} /></Button>}
bordered={false}
variant="borderless"
style={cardStyle}
bodyStyle={{ padding: 0 }}
styles={{ body: { padding: 0 } }}
>
<Table
rowKey="area"
columns={[
{ title: t('clean.s45'), dataIndex: 'area', key: 'area' },
{ title: t('workOrder.priority'), dataIndex: 'priority', key: 'priority', render: renderPriorityTag },
@@ -479,11 +480,12 @@ export default function CleanOptimizationPage() {
{/* 右侧:决策建议与参考 */}
<Col xs={24} xl={7} style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<Card title={<span style={{ fontSize: 14, fontWeight: 700 }}>{t('clean.cleaning2')}</span>} bordered={false} style={cardStyle} bodyStyle={{ padding: 0 }}>
<Card title={<span style={{ fontSize: 14, fontWeight: 700 }}>{t('clean.cleaning2')}</span>} variant="borderless" style={cardStyle} styles={{ body: { padding: 0 } }}>
<div style={{ background: '#f8fafc', padding: '8px 16px', borderBottom: '1px solid #f0f0f0' }}>
<Text type="secondary" style={{ fontSize: 11 }}>{t('clean.algorithmModel')}</Text>
</div>
<Table
rowKey="id"
columns={[
{ title: t('clean.s48'), dataIndex: 'id', key: 'id', width: 85 },
{ title: t('workOrder.priority'), dataIndex: 'priority', key: 'priority', render: (p) => <span style={{ color: p === t('workOrder.high') ? '#F53F3F' : '#F7BA1E', fontWeight: 600 }}>{p}</span>, width: 65 },
@@ -498,7 +500,7 @@ export default function CleanOptimizationPage() {
/>
</Card>
<Card title={<span style={{ fontSize: 14, fontWeight: 700 }}>{t('clean.fixed2Cleaning2')}</span>} bordered={false} style={cardStyle} bodyStyle={{ padding: 16 }}>
<Card title={<span style={{ fontSize: 14, fontWeight: 700 }}>{t('clean.fixed2Cleaning2')}</span>} variant="borderless" style={cardStyle} styles={{ body: { padding: 16 } }}>
<Row gutter={12}>
<Col span={12}>
<div style={{ background: '#F9F0FF', borderRadius: 12, padding: 16, border: '1px solid #D3ADF7', height: '100%' }}>
@@ -529,7 +531,7 @@ export default function CleanOptimizationPage() {
</Row>
</Card>
<Card title={<span style={{ fontSize: 14, fontWeight: 700 }}>{t('clean.fixed2WeatherAdvice2')}<Text type="secondary" style={{ fontSize: 12, fontWeight: 400 }}>({t('clean.plan', { name: '01' })})</Text></span>} bordered={false} style={cardStyle} bodyStyle={{ padding: 16 }}>
<Card title={<span style={{ fontSize: 14, fontWeight: 700 }}>{t('clean.fixed2WeatherAdvice2')}<Text type="secondary" style={{ fontSize: 12, fontWeight: 400 }}>({t('clean.plan', { name: '01' })})</Text></span>} variant="borderless" style={cardStyle} styles={{ body: { padding: 16 } }}>
<Row gutter={12}>
<Col span={8}>
<div style={{ background: '#f8fafc', padding: '10px 4px', borderRadius: 8, textAlign: 'center', border: '1px solid #f0f0f0' }}>
@@ -561,7 +563,7 @@ export default function CleanOptimizationPage() {
</Row>
</Card>
<Card title={<span style={{ fontSize: 14, fontWeight: 700 }}>{t('clean.fixed2Cleaning')}</span>} bordered={false} style={cardStyle} bodyStyle={{ padding: 16 }}>
<Card title={<span style={{ fontSize: 14, fontWeight: 700 }}>{t('clean.fixed2Cleaning')}</span>} variant="borderless" style={cardStyle} styles={{ body: { padding: 16 } }}>
<Row gutter={[16, 16]}>
{[
{ icon: <Droplets size={18} color="#165DFF" />, bg: '#E8F3FF', label: t('clean.s52'), value: t('clean.tonUnit', { value: '18' }), sub: t('clean.waterSaveWithRobot', { value: '30%' }), subColor: '#00B42A' },

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { Card, Row, Col, Statistic, Button, Space, Tag, Typography, Table, message } from 'antd';
import {
CloudOutlined, ThunderboltOutlined, BarChartOutlined,
@@ -254,7 +254,7 @@ export default function StationDashboard() {
maxWidth: '240px',
minWidth: '160px'
}}>
<Card bordered={false} style={{ ...cardStyle, height: "100%" }} bodyStyle={{ padding: '12px' }}>
<Card variant="borderless" style={{ ...cardStyle, height: "100%" }} styles={{ body: { padding: '12px' } }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ color: '#000', fontSize: '13px', fontWeight: 600, marginBottom: 4 }}>{item.title}</div>
@@ -282,7 +282,7 @@ export default function StationDashboard() {
{/* 左侧图表 */}
<Col xs={24} xl={6}>
<Space direction="vertical" size={8} style={{ width: '100%' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.generationTrend')}</span>} extra={<Space><SyncOutlined /><FullscreenOutlined /></Space>} bordered={false} style={cardStyle} bodyStyle={{ padding: '8px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.generationTrend')}</span>} extra={<Space><SyncOutlined /><FullscreenOutlined /></Space>} variant="borderless" style={cardStyle} styles={{ body: { padding: '8px' } }}>
<div style={{ fontSize: 11, color: '#666', marginBottom: 8 }}>今日发电功率趋势 (kW)</div>
<UniversalLineChart
data={powerTrendData}
@@ -297,7 +297,7 @@ export default function StationDashboard() {
]}
/>
</Card>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.irradianceTempTrend')}</span>} bordered={false} style={cardStyle} bodyStyle={{ padding: '8px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.irradianceTempTrend')}</span>} variant="borderless" style={cardStyle} styles={{ body: { padding: '8px' } }}>
<UniversalLineChart
data={irradianceData}
xKey="time"
@@ -311,7 +311,7 @@ export default function StationDashboard() {
]}
/>
</Card>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.inverterEfficiency')}</span>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.inverterEfficiency')}</span>} variant="borderless" style={cardStyle} styles={{ body: { padding: '12px' } }}>
<UniversalBarChart
data={invEfficiencyData}
xKey="device"
@@ -322,7 +322,7 @@ export default function StationDashboard() {
series={[{ key: 'efficiency', name: '效率', color: '#165DFF' }]}
/>
</Card>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.maintenanceProgress')}</span>} extra={<Button type="link" size="small">{t('home.fixed2MoreGt')}</Button>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.maintenanceProgress')}</span>} extra={<Button type="link" size="small">{t('home.fixed2MoreGt')}</Button>} variant="borderless" style={cardStyle} styles={{ body: { padding: '12px' } }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<UniversalPieChart
data={taskProgressData}
@@ -358,9 +358,9 @@ export default function StationDashboard() {
</div>
</div>
}
bordered={false}
variant="borderless"
style={cardStyle}
bodyStyle={{ padding: 0, position: 'relative', overflow: 'hidden', height: 500 }}
styles={{ body: { padding: 0, position: 'relative', overflow: 'hidden', height: 500 } }}
>
<div style={{ height: "100%", background: '#111827', borderRadius: 8, width: '100%', overflow: 'hidden', position: "relative" }}>
{/*<iframe
@@ -378,8 +378,9 @@ export default function StationDashboard() {
<Row gutter={16}>
<Col span={12}>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.recentInspection')}</span>} extra={<Button type="link" size="small">{t('home.fixed2MoreGt')}</Button>} bordered={false} style={{ ...cardStyle, height: "100%" }} bodyStyle={{ padding: '0 12px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.recentInspection')}</span>} extra={<Button type="link" size="small">{t('home.fixed2MoreGt')}</Button>} variant="borderless" style={{ ...cardStyle, height: "100%" }} styles={{ body: { padding: '0 12px' } }}>
<Table
rowKey="name"
dataSource={patrolRecords}
pagination={false}
size="small"
@@ -393,7 +394,7 @@ export default function StationDashboard() {
</Card>
</Col>
<Col span={12}>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.aiAnalysisSummary')}</span>} bordered={false} style={{ ...cardStyle, height: "100%" }} bodyStyle={{ padding: '12px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.aiAnalysisSummary')}</span>} variant="borderless" style={{ ...cardStyle, height: "100%" }} styles={{ body: { padding: '12px' } }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
@@ -441,7 +442,7 @@ export default function StationDashboard() {
{/* 右侧 */}
<Col xs={24} xl={6}>
<Space direction="vertical" size={8} style={{ width: '100%' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('alerts.realtimeAlert')}</span>} extra={<Button type="link" size="small" onClick={() => navigate('/alerts/all')}>{t('home.fixed2MoreGt')}</Button>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('alerts.realtimeAlert')}</span>} extra={<Button type="link" size="small" onClick={() => navigate('/alerts/all')}>{t('home.fixed2MoreGt')}</Button>} variant="borderless" style={cardStyle} styles={{ body: { padding: '12px' } }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 12, padding: '0 4px 8px', borderBottom: '1px solid #f0f0f0', fontSize: 13, color: '#666' }}>
<span style={{ flex: 3 }}>{t('alerts.alarmContent')}</span>
<span style={{ flex: 1, textAlign: 'center' }}>{t('alerts.level')}</span>
@@ -465,7 +466,7 @@ export default function StationDashboard() {
})}
</Card>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.deviceOnlineStatus')}</span>} extra={<Button type="link" size="small">{t('home.fixed2MoreGt')}</Button>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.deviceOnlineStatus')}</span>} extra={<Button type="link" size="small">{t('home.fixed2MoreGt')}</Button>} variant="borderless" style={cardStyle} styles={{ body: { padding: '12px' } }}>
<Row gutter={[16, 16]}>
{[
{ name: t('home.solarPanel'), count: '320/320', icon: <AppstoreOutlined /> },
@@ -490,7 +491,7 @@ export default function StationDashboard() {
</Row>
</Card>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.weatherInfo')}</span>} extra={<Button type="link" size="small">{t('home.gt')}</Button>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.weatherInfo')}</span>} extra={<Button type="link" size="small">{t('home.gt')}</Button>} variant="borderless" style={cardStyle} styles={{ body: { padding: '12px' } }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'start', gap: 20 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<WeatherDisplay type={weatherType} />
@@ -510,7 +511,7 @@ export default function StationDashboard() {
{/* ====================================== */}
{/* 👇 清洗建议 + 生成工单按钮(已完善) */}
{/* ====================================== */}
<Card title={<span style={{ fontWeight: 800 }}>{t('clean.cleaningSuggestion')}</span>} extra={<Button type="link" size="small">{t('home.fixed2MoreGt')}</Button>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('clean.cleaningSuggestion')}</span>} extra={<Button type="link" size="small">{t('home.fixed2MoreGt')}</Button>} variant="borderless" style={cardStyle} styles={{ body: { padding: '12px' } }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 16 }}>
<div style={{ width: 80, height: 80, position: 'relative' }}>
<UniversalPieChart

View File

@@ -113,6 +113,8 @@ export default function Login() {
<Form.Item
name="agreement"
valuePropName="checked"
initialValue={true}
rules={[{ validator: (_, value) => value ? Promise.resolve() : Promise.reject(new Error(t('login.agreeRequired'))) }]}
>
<Checkbox>

View File

@@ -146,7 +146,7 @@ const Profile: React.FC = () => {
{/* 左侧信息卡片 */}
<Col xs={24} sm={24} md={8} lg={7} xl={6}>
<Card
bordered={false}
variant="borderless"
style={{
borderRadius: 12,
boxShadow: '0 2px 12px rgba(0,0,0,0.06)',
@@ -220,7 +220,7 @@ const Profile: React.FC = () => {
{/* 右侧表单卡片 */}
<Col xs={24} sm={24} md={16} lg={17} xl={18}>
<Card
bordered={false}
variant="borderless"
style={{
borderRadius: 12,
boxShadow: '0 2px 12px rgba(0,0,0,0.06)',

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState } from 'react';
import {
Card, Row, Col, Table, Select, Input, Tag,
Typography, Button, Space, Avatar, Pagination
@@ -121,9 +121,9 @@ const RealTimeMonitorPage = () => {
<span style={{ display: 'flex', alignItems: 'center' }}><span style={{ color: '#1890ff', marginLeft: 4 }}>{t('page.s2')}</span></span>
</div>
}
bordered={false}
variant="borderless"
style={{ ...cardStyle, borderRadius: 8 }}
bodyStyle={{ padding: 0, position: 'relative', height: 400, overflow: 'hidden' }}
styles={{ body: { padding: 0, position: 'relative', height: 400, overflow: 'hidden' } }}
>
<div style={{ height: "100%", background: '#111827', borderRadius: 8, width: '100%', overflow: 'hidden', position: "relative" }}>
{/*<iframe
@@ -149,7 +149,7 @@ const RealTimeMonitorPage = () => {
{/* 设备实时监控列表 */}
<Card
title={<span style={{ fontWeight: 800, fontSize: 13 }}>{t('page.fixed2DeviceFixed2RealtimeMonitorListFixed2')}</span>}
bordered={false}
variant="borderless"
style={{ ...cardStyle, borderRadius: 8 }}
extra={
<Space size={8}>
@@ -173,7 +173,7 @@ const RealTimeMonitorPage = () => {
/>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 12, alignItems: 'center' }}>
<span style={{ color: '#999', fontSize: 11 }}>{t('page.56')}</span>
<Pagination size="small" total={56} current={1} pageSize={10} showSizeChanger />
<Pagination size="small" total={56} defaultCurrent={1} defaultPageSize={10} showSizeChanger />
</div>
</Card>
</Space>
@@ -193,7 +193,7 @@ const RealTimeMonitorPage = () => {
{ title: t('home.ambientTemperature'), value: '28.0', unit: '℃', sub: '较昨日 +1.2 ℃', color: '#165DFF', icon: <CloudOutlined /> },
].map((item, idx) => (
<Col span={8} key={idx}>
<Card bordered={false} style={{ ...cardStyle, height: '100%', borderRadius: 8 }} bodyStyle={{ padding: '8px' }}>
<Card variant="borderless" style={{ ...cardStyle, height: '100%', borderRadius: 8 }} styles={{ body: { padding: '8px' } }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 6 }}>
<div style={{ color: item.color, fontSize: 18 }}>{item.icon}</div>
<div style={{ flex: 1, minWidth: 0 }}>
@@ -211,7 +211,7 @@ const RealTimeMonitorPage = () => {
</Row>
{/* 实时功率曲线 */}
<Card title={<span style={{ fontWeight: 800 }}>{t('page.powerCurveFixed2')}</span>} extra={<Select defaultValue="today" size="small"><Option value="today">{t('page.s11')}</Option></Select>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('page.powerCurveFixed2')}</span>} extra={<Select defaultValue="today" size="small"><Option value="today">{t('page.s11')}</Option></Select>} variant="borderless" style={cardStyle} styles={{ body: { padding: '12px' } }}>
<UniversalLineChart
height={180}
data={powerTrendData}
@@ -232,7 +232,7 @@ const RealTimeMonitorPage = () => {
</Card>
{/* 逆变器功率对比 */}
<Card title={<span style={{ fontWeight: 800 }}>{t('page.inverterPowerCompare')}</span>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('page.inverterPowerCompare')}</span>} variant="borderless" style={cardStyle} styles={{ body: { padding: '12px' } }}>
<UniversalBarChart
height={180}
data={inverterPowerData}
@@ -253,7 +253,7 @@ const RealTimeMonitorPage = () => {
{/* 热力图与在线率 */}
<Row gutter={8}>
<Col span={12}>
<Card title={<span style={{ fontWeight: 800 }}>{t('page.stringCurrentHeatmap')}</span>} extra={<Select defaultValue="a" size="small" style={{ width: 60 }}><Option value="a">{t('page.s12')}</Option></Select>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('page.stringCurrentHeatmap')}</span>} extra={<Select defaultValue="a" size="small" style={{ width: 60 }}><Option value="a">{t('page.s12')}</Option></Select>} variant="borderless" style={cardStyle} styles={{ body: { padding: '12px' } }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{['INV-01', 'INV-02', 'INV-03', 'INV-04', 'INV-05'].map((name, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
@@ -292,7 +292,7 @@ const RealTimeMonitorPage = () => {
</Card>
</Col>
<Col span={12}>
<Card title={<span style={{ fontWeight: 800 }}>{t('devices.deviceOnlineRate')}</span>} bordered={false} style={{ ...cardStyle, height: "100%" }} bodyStyle={{ padding: '12px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('devices.deviceOnlineRate')}</span>} variant="borderless" style={{ ...cardStyle, height: "100%" }} styles={{ body: { padding: '12px' } }}>
<div style={{ position: 'relative', height: 160 }}>
<UniversalPieChart
height={160}

View File

@@ -223,8 +223,8 @@ export default function ReportCenterPage() {
{overviewCards.map((card, idx) => (
<Col key={idx} style={{ minWidth: '180px', flex: '1 0 auto', }}>
<Card
bordered={false}
bodyStyle={{ padding: '20px 20px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}
variant="borderless"
styles={{ body: { padding: '20px 20px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' } }}
style={{ borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.06)' }}
>
{/* 左侧文字区域 */}
@@ -307,7 +307,7 @@ export default function ReportCenterPage() {
{/* 左侧主内容区 */}
<Col xs={24} xl={16} >
{/* 月度综合经营分析 */}
<Card bordered={false} style={cardStyle} bodyStyle={{ padding: '20px' }} title={<span style={{ fontSize: 15, fontWeight: 700, color: '#1D2129' }}>{t('report.monthlyOperationAnalysis')}</span>}>
<Card variant="borderless" style={cardStyle} styles={{ body: { padding: '20px' } }} title={<span style={{ fontSize: 15, fontWeight: 700, color: '#1D2129' }}>{t('report.monthlyOperationAnalysis')}</span>}>
<Row gutter={24}>
<Col span={17}>
<div style={{ fontSize: 11, color: '#86909C', marginBottom: 16, display: 'flex', justifyContent: 'flex-start', gap: 20 }}>
@@ -365,9 +365,9 @@ export default function ReportCenterPage() {
<Row gutter={[8, 8]} style={{ marginTop: 8 }}>
<Col span={8}>
<Card
bordered={false}
variant="borderless"
style={{ ...cardStyle, height: "100%" }}
bodyStyle={{ padding: '16px' }}
styles={{ body: { padding: '16px' } }}
title={<span style={{ fontSize: 14, fontWeight: 700 }}>{t('report.generationMeteorology')}</span>}
>
<div style={{ fontSize: 10, color: '#86909C', marginBottom: 12 }}>
@@ -392,9 +392,9 @@ export default function ReportCenterPage() {
<Col span={8}>
<Card
bordered={false}
variant="borderless"
style={{ ...cardStyle, height: "100%" }}
bodyStyle={{ padding: '16px' }}
styles={{ body: { padding: '16px' } }}
title={<span style={{ fontSize: 14, fontWeight: 700 }}>{t('report.workOrderAlertClosure')}</span>}
>
<div style={{ fontSize: 10, color: '#86909C', marginBottom: 12 }}>
@@ -420,9 +420,9 @@ export default function ReportCenterPage() {
<Col span={8}>
<Card
bordered={false}
variant="borderless"
style={{ ...cardStyle, height: "100%" }}
bodyStyle={{ padding: '16px' }}
styles={{ body: { padding: '16px' } }}
title={<span style={{ fontSize: 14, fontWeight: 700 }}>{t('report.aiAnalysisStatistics')}</span>}
>
<div style={{ width: '100%', boxSizing: 'border-box' }}>
@@ -481,7 +481,7 @@ export default function ReportCenterPage() {
</Col>
</Row>
{/* 订阅与分发 */}
<Card bordered={false} style={{ ...cardStyle, marginTop: 8 }} bodyStyle={{ padding: '20px' }} title={<span style={{ fontSize: 15, fontWeight: 700, color: '#1D2129' }}>{t('report.subscriptionDispatch')}</span>}>
<Card variant="borderless" style={{ ...cardStyle, marginTop: 8 }} styles={{ body: { padding: '20px' } }} title={<span style={{ fontSize: 15, fontWeight: 700, color: '#1D2129' }}>{t('report.subscriptionDispatch')}</span>}>
<Row gutter={[16, 16]} wrap>
<Col xs={24} sm={12} md={8}>
<div style={{ fontSize: 12, color: '#86909C', marginBottom: 12 }}>{t('report.subscribers', { count: 6 })}</div>
@@ -526,7 +526,7 @@ export default function ReportCenterPage() {
{/* 右侧边栏 */}
<Col xs={24} xl={8}>
{/* 报表目录 / 快捷下载 */}
<Card bordered={false} style={cardStyle} bodyStyle={{ padding: 0 }} title={<span style={{ fontSize: 15, fontWeight: 700 }}>{t('report.reportDirectory')}</span>} extra={<Button type="link" size="small">{t('common.more')} <ChevronRight size={14} /></Button>}>
<Card variant="borderless" style={cardStyle} styles={{ body: { padding: 0 } }} title={<span style={{ fontSize: 15, fontWeight: 700 }}>{t('report.reportDirectory')}</span>} extra={<Button type="link" size="small">{t('common.more')} <ChevronRight size={14} /></Button>}>
<Table
rowKey="name"
columns={[
@@ -556,7 +556,7 @@ export default function ReportCenterPage() {
</Card>
{/* 模板中心 */}
<Card bordered={false} style={{ ...cardStyle, marginTop: 8 }} bodyStyle={{ padding: '16px' }} title={<span style={{ fontSize: 15, fontWeight: 700 }}>{t('report.templateCenter')}</span>} extra={<Button type="link" size="small">{t('common.more')} <ChevronRight size={14} /></Button>}>
<Card variant="borderless" style={{ ...cardStyle, marginTop: 8 }} styles={{ body: { padding: '16px' } }} title={<span style={{ fontSize: 15, fontWeight: 700 }}>{t('report.templateCenter')}</span>} extra={<Button type="link" size="small">{t('common.more')} <ChevronRight size={14} /></Button>}>
<Row gutter={[12, 12]}>
{reportTemplates.map((tpl, idx) => (
<Col span={12} key={idx}>
@@ -575,7 +575,7 @@ export default function ReportCenterPage() {
</Card>
{/* 报表生成记录 */}
<Card bordered={false} style={{ ...cardStyle, marginTop: 8 }} bodyStyle={{ padding: 0 }} title={<span style={{ fontSize: 15, fontWeight: 700 }}>{t('report.generationRecords')}</span>} extra={<Button type="link" size="small">{t('common.more')} <ChevronRight size={14} /></Button>}>
<Card variant="borderless" style={{ ...cardStyle, marginTop: 8 }} styles={{ body: { padding: 0 } }} title={<span style={{ fontSize: 15, fontWeight: 700 }}>{t('report.generationRecords')}</span>} extra={<Button type="link" size="small">{t('common.more')} <ChevronRight size={14} /></Button>}>
<Table
rowKey="name"
columns={[

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState } from 'react';
import {
Card, Row, Col, Button, Space, Input, Select, Table, Tag, Typography, Dropdown, Menu,
Timeline, Badge, Divider, Tooltip
@@ -158,8 +158,8 @@ export default function VideoMonitorPage() {
].map((item, index) => (
<Col key={index} style={{ minWidth: '168px', flex: '1 0 auto', maxWidth: "250px" }}>
<Card
bordered={false}
bodyStyle={{ padding: '16px' }}
variant="borderless"
styles={{ body: { padding: '16px' } }}
style={{ borderRadius: 8, height: 120 }}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', height: '100%' }}>
@@ -201,9 +201,9 @@ export default function VideoMonitorPage() {
<Button type="link" size="small" icon={<FullscreenOutlined />}>{t('video.fullScreen')}</Button>
</Space>
}
bordered={false}
variant="borderless"
style={cardStyle}
bodyStyle={{ padding: 16 }}
styles={{ body: { padding: 16 } }}
>
<Row gutter={[12, 12]}>
<Col span={14}>
@@ -221,7 +221,7 @@ export default function VideoMonitorPage() {
<ScissorOutlined style={{ color: '#fff', fontSize: 18, cursor: 'pointer' }} />
<CameraOutlined style={{ color: '#fff', fontSize: 18, cursor: 'pointer' }} />
<ExpandOutlined style={{ color: '#fff', fontSize: 18, cursor: 'pointer' }} />
<Select defaultValue={t('video.high')} size="small" style={{ width: 80 }} bordered={false} className="video-select">
<Select defaultValue={t('video.high')} size="small" style={{ width: 80 }} variant="borderless" className="video-select">
<Option value={t('video.super')}>{t('video.super')}</Option>
<Option value={t('video.high')}>{t('video.high')}</Option>
<Option value={t('video.standard')}>{t('video.standard')}</Option>
@@ -265,9 +265,9 @@ export default function VideoMonitorPage() {
<Card
title={<span style={{ fontSize: 16, fontWeight: 800, color: '#1D2129' }}>{t('video.fixed2AiVideoIntelligentAnalysisFixed2')}</span>}
extra={<Button type="link" size="small">{t('common.settings')}<ChevronRight size={14} /></Button>}
bordered={false}
variant="borderless"
style={{ ...cardStyle, height: '100%' }}
bodyStyle={{ padding: '16px 20px' }}
styles={{ body: { padding: '16px 20px' } }}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{[
@@ -308,11 +308,12 @@ export default function VideoMonitorPage() {
</Select>
</Space>
}
bordered={false}
variant="borderless"
style={cardStyle}
bodyStyle={{ padding: 0 }}
styles={{ body: { padding: 0 } }}
>
<Table
rowKey="key"
columns={camColumns}
dataSource={camList}
pagination={false}
@@ -327,9 +328,9 @@ export default function VideoMonitorPage() {
<Card
title={<span style={{ fontSize: 15, fontWeight: 800, color: '#1D2129' }}>{t('video.eventTimeline')}</span>}
extra={<Select defaultValue={t('devices.allTypes')} size="small" style={{ width: 100 }}><Option value="全部类型">{t('devices.allTypes')}</Option></Select>}
bordered={false}
variant="borderless"
style={cardStyle}
bodyStyle={{ padding: '16px 20px' }}
styles={{ body: { padding: '16px 20px' } }}
>
<Timeline style={{ marginTop: 8 }}>
{[
@@ -357,9 +358,9 @@ export default function VideoMonitorPage() {
<Card
title={<span style={{ fontSize: 15, fontWeight: 800, color: '#1D2129' }}>{t('video.alert')}</span>}
extra={<Button type="link" size="small">{t('devices.viewMore')}<ChevronRight size={14} /></Button>}
bordered={false}
variant="borderless"
style={cardStyle}
bodyStyle={{ padding: '16px 20px' }}
styles={{ body: { padding: '16px 20px' } }}
>
<div style={{ display: 'flex', flexWrap: "wrap", gap: 12, paddingBottom: 8 }} >
{[

View File

@@ -915,7 +915,7 @@ const WorkOrderPage = () => {
<Row gutter={[8, 8]} style={{ marginBottom: 8 }}>
{stats.map((item, index) => (
<Col key={index} xs={24} sm={12} md={8} xl={4}>
<Card bordered={false} bodyStyle={{ padding: '20px 16px' }} style={cardStyle}>
<Card variant="borderless" styles={{ body: { padding: '20px 16px' } }} style={cardStyle}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontSize: 'clamp(12px, 0.8vw, 13px)', color: '#86909C', marginBottom: 2 }}>{item.title}</div>
@@ -940,7 +940,7 @@ const WorkOrderPage = () => {
</Row>
{/* 筛选栏 */}
<Card bordered={false} bodyStyle={{ padding: '8px 16px 0px 16px' }} style={{ ...cardStyle, marginBottom: 8 }}>
<Card variant="borderless" styles={{ body: { padding: '8px 16px 0px 16px' } }} style={{ ...cardStyle, marginBottom: 8 }}>
<Form form={queryForm} layout="inline" initialValues={{
orderStatus: 1
}}>
@@ -976,7 +976,7 @@ const WorkOrderPage = () => {
{/* 左侧:列表 + 底部图表 */}
<Col xs={24} xl={15}>
<Card
bordered={false}
variant="borderless"
title={
<Space size={8}>
<Text strong style={{ fontSize: 15, color: '#1D2129' }}>{t('workOrder.listFixed2')}</Text>
@@ -1002,7 +1002,7 @@ const WorkOrderPage = () => {
)}
</Space>
}
bodyStyle={{ padding: 0 }}
styles={{ body: { padding: 0 } }}
style={{ ...cardStyle, marginBottom: 8 }}
>
@@ -1037,7 +1037,7 @@ const WorkOrderPage = () => {
<Row gutter={[12, 12]}>
<Col xs={24} md={9}>
<Card bordered={false} title={<Text strong style={{ fontSize: 14 }}>{t('workOrder.fixed2OrderStatus')}</Text>} style={{ ...cardStyle, height: 380 }}>
<Card variant="borderless" title={<Text strong style={{ fontSize: 14 }}>{t('workOrder.fixed2OrderStatus')}</Text>} style={{ ...cardStyle, height: 380 }}>
<div style={{ height: 300, position: 'relative' }}>
<Pie
data={pieData}
@@ -1059,7 +1059,7 @@ const WorkOrderPage = () => {
</Col>
<Col xs={24} md={15}>
<Card
bordered={false}
variant="borderless"
title={<Text strong style={{ fontSize: 14 }}>{t('workOrder.fixed2TaskSuspended2')}</Text>}
extra={
<Button
@@ -1070,7 +1070,7 @@ const WorkOrderPage = () => {
</Button>
}
style={{ ...cardStyle, height: 380 }}
bodyStyle={{ padding: '16px' }}
styles={{ body: { padding: '16px' } }}
>
{/* 日期切换栏 */}
<div style={{ marginBottom: 16 }}>
@@ -1206,7 +1206,7 @@ const WorkOrderPage = () => {
{/* 右侧:详情 + AI + 告警 */}
<Col xs={24} xl={9}>
<Card bordered={false} style={cardStyle} bodyStyle={{ padding: '20px' }}>
<Card variant="borderless" style={cardStyle} styles={{ body: { padding: '20px' } }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<Text strong style={{ fontSize: 16, color: '#1D2129' }}>{t('workOrder.detailFixed2')}</Text>
<Text type="secondary" style={{ fontSize: 12 }}>{t('workOrder.fixed2OrderNo')}<span style={{ color: '#4E5969', fontWeight: 600 }}>{currentDetail?.orderNo || '--'}</span></Text>
@@ -1464,8 +1464,8 @@ const WorkOrderPage = () => {
</Card>
<Card bordered={false} title={<Text strong style={{ fontSize: 14, color: '#1D2129' }}>{t('workOrder.aiSuggestion')}</Text>}
style={{ ...cardStyle, marginTop: 8 }} bodyStyle={{ padding: '16px' }}>
<Card variant="borderless" title={<Text strong style={{ fontSize: 14, color: '#1D2129' }}>{t('workOrder.aiSuggestion')}</Text>}
style={{ ...cardStyle, marginTop: 8 }} styles={{ body: { padding: '16px' } }}>
<Row gutter={12} style={{ marginBottom: 16 }}>
<Col span={8}>
<div style={{ background: '#F9F0FF', padding: '16px 8px', borderRadius: 12, textAlign: 'center', height: '100%', border: '1px solid #F3E8FF' }}>

View File

@@ -1,334 +1,4 @@
import React, { lazy, Suspense } from 'react';
import { createBrowserRouter } from 'react-router-dom';
import type { RouteObject } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
// 🔥 所有组件全部改为懒加载
const VideoMonitorPage = lazy(() => import('../pages/VideoMonitorPage'));
const AIDiagnosisPage = lazy(() => import('../pages/AIAnalysis'));
const CleanOptimizationPage = lazy(() => import('../pages/CleanOptimization'));
const WorkOrderPage = lazy(() => import('../pages/WorkOrder'));
const ReportCenterPage = lazy(() => import('../pages/ReportCenter'));
const RealTimeMonitorPage = lazy(() => import('../pages/RealTimeMonitor'));
const Agri = lazy(() => import('../pages/Agri'));
const Layout = lazy(() => import('../components/Layout'));
const ProtectedRoute = lazy(() => import('../components/ProtectedRoute'));
const Login = lazy(() => import('../pages/Login'));
const Home = lazy(() => import('../pages/Home'));
const Video = lazy(() => import('../pages/Video'));
const Alerts = lazy(() => import('../pages/Alerts'));
const Downloads = lazy(() => import('../pages/Downloads'));
const Roles = lazy(() => import('../pages/Roles'));
const Users = lazy(() => import('../pages/Users'));
const MenuList = lazy(() => import('../components/SystemSetting/MenuList'));
const Organization = lazy(() => import('../components/SystemSetting/Organization'));
const StationManage = lazy(() => import('../components/SystemSetting/StationManage'));
const SystemSetting = lazy(() => import('../pages/SystemSetting'));
const Profile = lazy(() => import('../pages/Profile'));
const DeviceStatus = lazy(() => import('../components/devices/DeviceStatusPage'));
const WayLineTask = lazy(() => import('../components/devices/WayLinePage'));
const RobotTask = lazy(() => import('../components/devices/RobotTaskPage'));
const DevicesOverview = lazy(() => import('../components/devices/DeviceOverviewPage'));
const BasicSettings = lazy(() => import('../components/SystemSetting/BasicSettings'));
const UserRolePage = lazy(() => import('../components/SystemSetting/UserPage'));
const DeviceAccessPage = lazy(() => import('../components/SystemSetting/DeviceAccessPage'));
const OnlineUserPage = lazy(() => import('../components/SystemSetting/OnlineUser'));
const DeviceManagementPage = lazy(() => import('../components/SystemSetting/DeviceManagement'));
const SystemMaintenancePage = lazy(() => import('../components/SystemSetting/SystemMaintain'));
const LogManagement = lazy(() => import('../components/SystemSetting/LogManagement'));
const VideoManagement = lazy(() => import('../components/SystemSetting/VideoManagement'));
//const ErrorManagement = lazy(() => import('../components/SystemSetting/ErrorManagement'));
//const AlarmSettingsManagement = lazy(() => import('../components/SystemSetting/AlarmSettingsManagement'));
//const WorkOrderSettingsManagement = lazy(() => import('../components/SystemSetting/WorkOrderSettingsManagement'));
// 加载中动画
function PageFallback() {
return (
<div className="flex items-center justify-center h-full min-h-[200px]">
<div className="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin" />
</div>
);
}
// 占位页(国际化)
const NoticePolicyPlaceholder = () => {
const { t } = useTranslation();
return (
<div style={{ padding: 20 }}>
<h2>{t('router.noticePolicy')}</h2>
</div>
);
};
const DataSafetyPlaceholder = () => {
const { t } = useTranslation();
return (
<div style={{ padding: 20 }}>
<h2>{t('router.dataSafety')}</h2>
</div>
);
};
const ModuleDeveloping = () => {
const { t } = useTranslation();
return (
<div className="flex flex-col items-center justify-center h-full gap-4 text-gray-400 select-none">
<span className="text-6xl opacity-20">🚧</span>
<p className="text-lg font-medium">{t('router.moduleDeveloping')}</p>
</div>
);
};
// ====================== 全部懒加载的路由配置 ======================
const routes: RouteObject[] = [
{
path: '/login',
element: (
<Suspense fallback={<PageFallback />}>
<Login />
</Suspense>
),
},
{
element: (
<Suspense fallback={<PageFallback />}>
<ProtectedRoute />
</Suspense>
),
children: [
{
element: (
<Suspense fallback={<PageFallback />}>
<Layout />
</Suspense>
),
children: [
{
index: true,
element: (
<Suspense fallback={<PageFallback />}>
<Home />
</Suspense>
)
},
{
path: 'overview',
element: (
<Suspense fallback={<PageFallback />}>
<Home />
</Suspense>
)
},
{
path: 'devices/overview',
element: (
<Suspense fallback={<PageFallback />}>
<DevicesOverview />
</Suspense>
)
},
{
path: 'devices/status',
element: (
<Suspense fallback={<PageFallback />}>
<DeviceStatus />
</Suspense>
)
},
{
path: 'devices/wayLineTask',
element: (
<Suspense fallback={<PageFallback />}>
<WayLineTask />
</Suspense>
)
},
{
path: 'devices/robotTask',
element: (
<Suspense fallback={<PageFallback />}>
<RobotTask />
</Suspense>
)
},
{
path: 'video',
element: (
<Suspense fallback={<PageFallback />}>
<Video />
</Suspense>
)
},
{
path: 'RealtimeMonitor',
element: (
<Suspense fallback={<PageFallback />}>
<RealTimeMonitorPage />
</Suspense>
)
},
{
path: 'monitor',
element: (
<Suspense fallback={<PageFallback />}>
<VideoMonitorPage />
</Suspense>
)
},
{
path: 'agri',
element: (
<Suspense fallback={<PageFallback />}>
<Agri />
</Suspense>
)
},
{
path: 'alerts/all',
element: (
<Suspense fallback={<PageFallback />}>
<Alerts />
</Suspense>
)
},
{
path: 'AIanalysis',
element: (
<Suspense fallback={<PageFallback />}>
<AIDiagnosisPage />
</Suspense>
)
},
{
path: 'clean',
element: (
<Suspense fallback={<PageFallback />}>
<CleanOptimizationPage />
</Suspense>
)
},
{
path: 'task',
element: (
<Suspense fallback={<PageFallback />}>
<WorkOrderPage />
</Suspense>
)
},
{
path: 'table',
element: (
<Suspense fallback={<PageFallback />}>
<ReportCenterPage />
</Suspense>
)
},
{
path: 'systemSetting',
element: (
<Suspense fallback={<PageFallback />}>
<SystemSetting />
</Suspense>
),
children: [
{ index: true, element: <Suspense fallback={<PageFallback />}><BasicSettings /></Suspense> },
{ path: 'basic', element: <Suspense fallback={<PageFallback />}><BasicSettings /></Suspense> },
{ path: 'User', element: <Suspense fallback={<PageFallback />}><UserRolePage /></Suspense> },
{ path: 'menu', element: <Suspense fallback={<PageFallback />}><MenuList /></Suspense> },
{ path: 'org', element: <Suspense fallback={<PageFallback />}><Organization /></Suspense> },
{ path: 'site', element: <Suspense fallback={<PageFallback />}><StationManage /></Suspense> },
{ path: 'notice', element: <Suspense fallback={<PageFallback />}><NoticePolicyPlaceholder /></Suspense> },
{ path: 'deviceAccess', element: <Suspense fallback={<PageFallback />}><DeviceAccessPage /></Suspense> },
{ path: 'dataSafety', element: <Suspense fallback={<PageFallback />}><DataSafetyPlaceholder /></Suspense> },
{ path: 'systemMaintenance', element: <Suspense fallback={<PageFallback />}><SystemMaintenancePage /></Suspense> },
{ path: 'monitor', element: <Suspense fallback={<PageFallback />}><OnlineUserPage /></Suspense> },
{ path: 'devices', element: <Suspense fallback={<PageFallback />}><DeviceManagementPage /></Suspense> },
{ path: 'log', element: <Suspense fallback={<PageFallback />}><LogManagement /></Suspense> },
{ path: 'videoManage', element: <Suspense fallback={<PageFallback />}><VideoManagement /></Suspense> },
]
},
{
path: 'downloads',
element: (
<Suspense fallback={<PageFallback />}>
<Downloads />
</Suspense>
)
},
{
path: 'manage/users',
element: (
<Suspense fallback={<PageFallback />}>
<Users />
</Suspense>
)
},
{
path: 'manage/roles',
element: (
<Suspense fallback={<PageFallback />}>
<Roles />
</Suspense>
)
},
{
path: 'manage/menuList',
element: (
<Suspense fallback={<PageFallback />}>
<MenuList />
</Suspense>
)
},
{
path: 'organization',
element: (
<Suspense fallback={<PageFallback />}>
<Organization />
</Suspense>
)
},
{
path: 'station',
element: (
<Suspense fallback={<PageFallback />}>
<StationManage />
</Suspense>
)
},
{
path: 'system',
element: (
<Suspense fallback={<PageFallback />}>
<SystemSetting />
</Suspense>
)
},
{
path: 'profile',
element: (
<Suspense fallback={<PageFallback />}>
<Profile />
</Suspense>
)
},
{
path: '*',
element: <ModuleDeveloping />,
},
],
},
],
},
];
import { routes } from './routes';
export const router = createBrowserRouter(routes);

219
src/router/routes.tsx Normal file
View File

@@ -0,0 +1,219 @@
import React, { lazy, Suspense } from 'react';
import type { RouteObject } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
// 🔥 所有组件全部改为懒加载
const VideoMonitorPage = lazy(() => import('../pages/VideoMonitorPage'));
const AIDiagnosisPage = lazy(() => import('../pages/AIAnalysis'));
const CleanOptimizationPage = lazy(() => import('../pages/CleanOptimization'));
const WorkOrderPage = lazy(() => import('../pages/WorkOrder'));
const ReportCenterPage = lazy(() => import('../pages/ReportCenter'));
const RealTimeMonitorPage = lazy(() => import('../pages/RealTimeMonitor'));
const Agri = lazy(() => import('../pages/Agri'));
const Layout = lazy(() => import('../components/Layout'));
const ProtectedRoute = lazy(() => import('../components/ProtectedRoute'));
const Login = lazy(() => import('../pages/Login'));
const Home = lazy(() => import('../pages/Home'));
const Video = lazy(() => import('../pages/Video'));
const Alerts = lazy(() => import('../pages/Alerts'));
const Downloads = lazy(() => import('../pages/Downloads'));
const Roles = lazy(() => import('../pages/Roles'));
const Users = lazy(() => import('../pages/Users'));
const MenuList = lazy(() => import('../components/SystemSetting/MenuList'));
const Organization = lazy(() => import('../components/SystemSetting/Organization'));
const StationManage = lazy(() => import('../components/SystemSetting/StationManage'));
const SystemSetting = lazy(() => import('../pages/SystemSetting'));
const Profile = lazy(() => import('../pages/Profile'));
const DeviceStatus = lazy(() => import('../components/devices/DeviceStatusPage'));
const WayLineTask = lazy(() => import('../components/devices/WayLinePage'));
const RobotTask = lazy(() => import('../components/devices/RobotTaskPage'));
const DevicesOverview = lazy(() => import('../components/devices/DeviceOverviewPage'));
const BasicSettings = lazy(() => import('../components/SystemSetting/BasicSettings'));
const UserRolePage = lazy(() => import('../components/SystemSetting/UserPage'));
const DeviceAccessPage = lazy(() => import('../components/SystemSetting/DeviceAccessPage'));
const OnlineUserPage = lazy(() => import('../components/SystemSetting/OnlineUser'));
const DeviceManagementPage = lazy(() => import('../components/SystemSetting/DeviceManagement'));
const SystemMaintenancePage = lazy(() => import('../components/SystemSetting/SystemMaintain'));
const LogManagement = lazy(() => import('../components/SystemSetting/LogManagement'));
const VideoManagement = lazy(() => import('../components/SystemSetting/VideoManagement'));
// 加载中动画
export function PageFallback() {
return (
<div className="flex items-center justify-center h-full min-h-[200px]">
<div className="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin" />
</div>
);
}
// 封装高阶组件,消除重复 Suspense
export const withSuspense = (Comp: React.LazyExoticComponent<React.FC>) => {
return <Suspense fallback={<PageFallback />}>{<Comp />}</Suspense>;
};
// 占位页(国际化)
const NoticePolicyPlaceholder = () => {
const { t } = useTranslation();
return (
<div style={{ padding: 20 }}>
<h2>{t('router.noticePolicy')}</h2>
</div>
);
};
const DataSafetyPlaceholder = () => {
const { t } = useTranslation();
return (
<div style={{ padding: 20 }}>
<h2>{t('router.dataSafety')}</h2>
</div>
);
};
const ModuleDeveloping = () => {
const { t } = useTranslation();
return (
<div className="flex flex-col items-center justify-center h-full gap-4 text-gray-400 select-none">
<span className="text-6xl opacity-20">🚧</span>
<p className="text-lg font-medium">{t('router.moduleDeveloping')}</p>
</div>
);
};
// ====================== 路由配置数组 ======================
export const routes: RouteObject[] = [
{
path: '/login',
element: withSuspense(Login),
},
{
element: withSuspense(ProtectedRoute),
children: [
{
element: withSuspense(Layout),
children: [
{
index: true,
element: withSuspense(Home)
},
{
path: 'overview',
element: withSuspense(Home)
},
{
path: 'devices/overview',
element: withSuspense(DevicesOverview)
},
{
path: 'devices/status',
element: withSuspense(DeviceStatus)
},
{
path: 'devices/wayLineTask',
element: withSuspense(WayLineTask)
},
{
path: 'devices/robotTask',
element: withSuspense(RobotTask)
},
{
path: 'video',
element: withSuspense(Video)
},
{
path: 'RealtimeMonitor',
element: withSuspense(RealTimeMonitorPage)
},
{
path: 'monitor',
element: withSuspense(VideoMonitorPage)
},
{
path: 'agri',
element: withSuspense(Agri)
},
{
path: 'alerts/all',
element: withSuspense(Alerts)
},
{
path: 'AIanalysis',
element: withSuspense(AIDiagnosisPage)
},
{
path: 'clean',
element: withSuspense(CleanOptimizationPage)
},
{
path: 'task',
element: withSuspense(WorkOrderPage)
},
{
path: 'table',
element: withSuspense(ReportCenterPage)
},
{
path: 'systemSetting',
element: withSuspense(SystemSetting),
children: [
{ index: true, element: withSuspense(BasicSettings) },
{ path: 'basic', element: withSuspense(BasicSettings) },
{ path: 'User', element: withSuspense(UserRolePage) },
{ path: 'menu', element: withSuspense(MenuList) },
{ path: 'org', element: withSuspense(Organization) },
{ path: 'site', element: withSuspense(StationManage) },
{ path: 'notice', element: <Suspense fallback={<PageFallback />}><NoticePolicyPlaceholder /></Suspense> },
{ path: 'deviceAccess', element: withSuspense(DeviceAccessPage) },
{ path: 'dataSafety', element: <Suspense fallback={<PageFallback />}><DataSafetyPlaceholder /></Suspense> },
{ path: 'systemMaintenance', element: withSuspense(SystemMaintenancePage) },
{ path: 'monitor', element: withSuspense(OnlineUserPage) },
{ path: 'devices', element: withSuspense(DeviceManagementPage) },
{ path: 'log', element: withSuspense(LogManagement) },
{ path: 'videoManage', element: withSuspense(VideoManagement) },
]
},
{
path: 'downloads',
element: withSuspense(Downloads)
},
{
path: 'manage/users',
element: withSuspense(Users)
},
{
path: 'manage/roles',
element: withSuspense(Roles)
},
{
path: 'manage/menuList',
element: withSuspense(MenuList)
},
{
path: 'organization',
element: withSuspense(Organization)
},
{
path: 'station',
element: withSuspense(StationManage)
},
{
path: 'system',
element: withSuspense(SystemSetting)
},
{
path: 'profile',
element: withSuspense(Profile)
},
{
path: '*',
element: <ModuleDeveloping />,
},
],
},
],
},
];

File diff suppressed because it is too large Load Diff

View File

@@ -1,80 +0,0 @@
const fs = require('fs');
const path = require('path');
const root = 'src';
const exts = ['.tsx', '.ts', '.jsx', '.js'];
const excludeDirs = ['locales', 'node_modules'];
const cjk = /[\u4e00-\u9fff]/;
function walk(dir) {
const out = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (excludeDirs.includes(entry.name)) continue;
out.push(...walk(full));
} else if (exts.includes(path.extname(entry.name))) {
out.push(full);
}
}
return out;
}
// Load locale keys (flattened)
function load(localePath) {
let src = fs.readFileSync(localePath, 'utf8');
src = src.replace(/const\s+\w+\s*=\s*\{/, 'var obj = {');
src = src.replace(/export\s+default\s+\w+;?/, '');
src = src.replace(/\bconst\b/g, 'var');
return new Function(src + '\nreturn obj;')();
}
function flatten(obj, prefix = '', res = {}) {
for (const k of Object.keys(obj)) {
const key = prefix ? prefix + '.' + k : k;
if (obj[k] && typeof obj[k] === 'object' && !Array.isArray(obj[k])) flatten(obj[k], key, res);
else res[key] = obj[k];
}
return res;
}
const zhF = flatten(load('src/locales/zh/index.ts'));
const enF = flatten(load('src/locales/en/index.ts'));
// 1) Find all t('...') / i18n.t('...') referenced keys
const files = walk(root);
const refKeys = new Set();
const refRegex = /(?:i18n\.t|[^a-zA-Z]t)\(\s*['"`]([a-zA-Z][a-zA-Z0-9_.]*?)['"`]/g;
for (const f of files) {
const content = fs.readFileSync(f, 'utf8');
let m;
while ((m = refRegex.exec(content))) {
refKeys.add(m[1]);
}
}
const refArr = [...refKeys].sort();
const missingInZh = refArr.filter(k => !(k in zhF));
const missingInEn = refArr.filter(k => !(k in enF));
console.log('=== t() referenced keys missing in ZH (' + missingInZh.length + ') ===');
console.log(missingInZh.join('\n'));
console.log('\n=== t() referenced keys missing in EN (' + missingInEn.length + ') ===');
console.log(missingInEn.join('\n'));
console.log('\nTotal referenced keys:', refArr.length);
// 2) Find user-facing hardcoded Chinese strings.
// Exclude lines that are only comments or console.* (developer-facing).
const userHits = [];
for (const f of files) {
const lines = fs.readFileSync(f, 'utf8').split('\n');
lines.forEach((line, i) => {
const t = line.trim();
if (!cjk.test(line)) return;
if (t.startsWith('//') || t.startsWith('*') || t.startsWith('/*')) return;
if (/^\{\/\*/.test(t)) return; // {/* ... */}
// exclude console.* (browser console only)
if (/\bconsole\.(log|error|warn|info|debug)\s*\(/.test(line)) return;
userHits.push({ file: f, line: i + 1, text: t.slice(0, 220) });
});
}
console.log('\n=== USER-FACING hardcoded CJK lines (' + userHits.length + ') ===');
for (const h of userHits) {
console.log(h.file + ':' + h.line + ' | ' + h.text);
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,45 +0,0 @@
const fs = require('fs');
const path = require('path');
function load(localePath) {
let src = fs.readFileSync(localePath, 'utf8');
// strip TS type annotations and the const/export lines to a plain object
// replace "const zh = {" -> "var obj = {", remove "export default zh;"
src = src.replace(/const\s+\w+\s*=\s*\{/, 'var obj = {');
src = src.replace(/export\s+default\s+\w+;?/, '');
src = src.replace(/\bconst\b/g, 'var');
const moduleWrap = new Function(src + '\nreturn obj;');
return moduleWrap();
}
function flatten(obj, prefix = '', res = {}) {
for (const k of Object.keys(obj)) {
const key = prefix ? prefix + '.' + k : k;
if (obj[k] && typeof obj[k] === 'object' && !Array.isArray(obj[k])) {
flatten(obj[k], key, res);
} else {
res[key] = obj[k];
}
}
return res;
}
const zhRaw = load('src/locales/zh/index.ts');
const enRaw = load('src/locales/en/index.ts');
const zhF = flatten(zhRaw);
const enF = flatten(enRaw);
const zhKeys = Object.keys(zhF).sort();
const enKeys = Object.keys(enF).sort();
const onlyZh = zhKeys.filter(k => !(k in enF));
const onlyEn = enKeys.filter(k => !(k in zhF));
console.log('=== Keys in ZH but MISSING in EN (' + onlyZh.length + ') ===');
console.log(onlyZh.join('\n'));
console.log('');
console.log('=== Keys in EN but MISSING in ZH (' + onlyEn.length + ') ===');
console.log(onlyEn.join('\n'));
console.log('');
console.log('ZH total keys:', zhKeys.length, ' EN total keys:', enKeys.length);

View File

@@ -1,34 +0,0 @@
const fs = require('fs');
const path = require('path');
const root = 'src';
const exts = ['.tsx', '.ts', '.jsx', '.js'];
const excludeDirs = ['locales', 'node_modules'];
function walk(dir) {
const out = [];
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, e.name);
if (e.isDirectory()) { if (!excludeDirs.includes(e.name)) out.push(...walk(full)); }
else if (exts.includes(path.extname(e.name))) out.push(full);
}
return out;
}
const refRegex = /(?:i18n\.t|[^a-zA-Z]t)\(\s*['"`]([a-zA-Z][a-zA-Z0-9_.]*?)['"`]/g;
const files = walk(root);
let count = 0;
const byFile = {};
for (const f of files) {
const content = fs.readFileSync(f, 'utf8');
let m;
const found = [];
while ((m = refRegex.exec(content))) { found.push(m[1]); }
if (found.length) { byFile[f] = found; count += found.length; }
}
console.log('Total raw t() matches:', count);
// show a few files with their matches
let shown = 0;
for (const f of Object.keys(byFile)) {
if (shown > 25) break;
console.log('\n' + f + ' (' + byFile[f].length + '):');
console.log(' ' + byFile[f].slice(0, 12).join(', '));
shown++;
}

View File

@@ -1,15 +0,0 @@
const fs = require('fs');
const rxJSX = />([^<>`{}()]*[一-鿿][^<>`{}()]*)</g;
const rxAttr = /([A-Za-z_][\w-]*)=(["'`])((?:\\.|(?!\2)[^{}()])*?[一-鿿]+(?:\\.|(?!\2)[^{}()])*?)\2/g;
const rxMsg = /(message\.(?:error|success|warning|info|loading|open))\(\s*(["'`])((?:\\.|(?!\2)[^{}()])*?[一-鿿]+(?:\\.|(?!\2)[^{}()])*?)\2/g;
const rxProp = /(^|[^.\w])((?:title|content|okText|cancelText|message|description|label|placeholder|text|tooltip|subTitle|subtitle)):\s*(["'`])((?:\\.|(?!\3)[^{}()])*?[一-鿿]+(?:\\.|(?!\3)[^{}()])*?)\3/g;
const SKIP_ATTR = new Set(['key','id','path','href','src','to','type','name','value','className','data','mode','status','field','prop','ref']);
const UI_PROP = new Set(['title','content','okText','cancelText','message','description','label','placeholder','text','tooltip','subTitle','subtitle']);
function keyFor(s){ return 'NS.KEY'; } // dummy
let s = fs.readFileSync('src/components/SystemSetting/BasicSettings.tsx','utf8');
s = s.replace(rxJSX, (m,inner)=>'>{t(\''+'NS.'+inner.trim()+'\')}<');
s = s.replace(rxAttr, (m,attr,q,val)=>{ if(SKIP_ATTR.has(attr)) return m; if(val.includes('${')) return m; return attr+'={t(\'NS.'+val.trim()+'\')}'; });
s = s.replace(rxMsg, (m,fn,q,val)=> fn+"(t('NS."+val.trim()+"'))");
s = s.replace(rxProp, (m,pre,prop,q,val)=>{ if(!UI_PROP.has(prop)) return m; return pre+prop+": t('NS."+val.trim()+"')"; });
const lines = s.split('\n');
for (let i=68;i<82;i++){ console.log((i+1)+': '+lines[i]); }

View File

@@ -1,23 +0,0 @@
const fs = require('fs');
function load(p){let s=fs.readFileSync(p,'utf8');s=s.replace(/const\s+\w+\s*=\s*\{/,'var obj = {');s=s.replace(/export\s+default\s+\w+;?/,'');s=s.replace(/\bconst\b/g,'var');return new Function(s+'\nreturn obj;')();}
const zh=load('src/locales/zh/index.ts'); const en=load('src/locales/en/index.ts');
function flat(o,p,r){p=p||'';r=r||{};for(const k of Object.keys(o)){const key=p?p+'.'+k:k;if(o[k]&&typeof o[k]==='object'&&!Array.isArray(o[k]))flat(o[k],key,r);else r[key]=o[k];}return r;}
const zf=flat(zh), ef=flat(en);
console.log('zh flat keys:',Object.keys(zf).length,' en flat keys:',Object.keys(ef).length);
for(const k of ['common.completed','home.underMaintenance','roles.abnormal']) console.log(' en has '+k+':',k in ef,' (zh:',k in zf,')');
// duplicate base-key detection per namespace (text scan)
function dupScan(file){
const text=fs.readFileSync(file,'utf8');
const re=/^\t([A-Za-z]+):\s*\{/gm; let m; const dups=[];
while((m=re.exec(text))){
const ns=m[1]; const start=m.index+m[0].length;
// find matching closing brace
let depth=1,i=start; while(i<text.length&&depth>0){const c=text[i];if(c==='{')depth++;else if(c==='}')depth--;i++;}
const block=text.slice(start,i-1);
const keys={}; let dm; const kr=/^\s*([A-Za-z0-9_'"][A-Za-z0-9_'"]*):/gm;
while((dm=kr.exec(block))){const key=dm[1].replace(/'/g,''); if(key in keys) dups.push(ns+'.'+key); keys[key]=1;}
}
return dups;
}
const dz=dupScan('src/locales/zh/index.ts'); const de=dupScan('src/locales/en/index.ts');
console.log('zh duplicate base keys:',dz.length, dz.slice(0,10)); console.log('en duplicate base keys:',de.length, de.slice(0,10));

View File

@@ -1,51 +0,0 @@
const fs = require('fs');
const path = require('path');
const root = 'src';
const exts = ['.tsx', '.ts', '.jsx', '.js'];
const excludeDirs = ['locales', 'node_modules'];
// Regex to match a run of CJK characters (and surrounding simple chars) inside a string-ish
// We capture any quoted string (single/double/backtick) containing CJK.
const cjk = /[\u4e00-\u9fff]/;
function walk(dir) {
const out = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (excludeDirs.includes(entry.name)) continue;
out.push(...walk(full));
} else if (exts.includes(path.extname(entry.name))) {
out.push(full);
}
}
return out;
}
const files = walk(root);
let total = 0;
const perFile = [];
for (const f of files) {
const lines = fs.readFileSync(f, 'utf8').split('\n');
const hits = [];
lines.forEach((line, i) => {
// skip pure comment lines (// ... or * ...)
const trimmed = line.trim();
if (trimmed.startsWith('//') || trimmed.startsWith('*')) return;
if (!cjk.test(line)) return;
hits.push({ line: i + 1, text: line.trim() });
});
if (hits.length) {
perFile.push({ file: f, hits });
total += hits.length;
}
}
console.log('Total files with CJK:', perFile.length, 'Total CJK lines:', total);
for (const pf of perFile) {
console.log('\n### ' + pf.file + ' (' + pf.hits.length + ')');
for (const h of pf.hits) {
console.log(' L' + h.line + ': ' + h.text.slice(0, 200));
}
}

View File

@@ -1,25 +0,0 @@
// Quote any object key that starts with a digit (invalid as unquoted JS key).
// Flat key string (ns.123abc) is unchanged, so t('ns.123abc') still resolves.
const fs = require('fs');
function fix(file) {
let s = fs.readFileSync(file, 'utf8');
let n = 0;
s = s.replace(/^(\s*)([0-9][A-Za-z0-9_]*)(:\s)/gm, (m, ind, key, colon) => { n++; return ind + "'" + key + "'" + colon; });
fs.writeFileSync(file, s, 'utf8');
console.log(file + ' -> quoted ' + n + ' digit-leading keys');
}
fix('src/locales/zh/index.ts');
fix('src/locales/en/index.ts');
// verify both parse
function load(p) {
let s = fs.readFileSync(p, 'utf8');
s = s.replace(/const\s+\w+\s*=\s*\{/, 'var obj = {');
s = s.replace(/export\s+default\s+\w+;?/, '');
s = s.replace(/\bconst\b/g, 'var');
return new Function(s + '\nreturn obj;')();
}
try { const z = load('src/locales/zh/index.ts'); console.log('zh parses OK, top keys:', Object.keys(z).length); }
catch (e) { console.log('zh PARSE FAIL:', e.message.split('\n')[0]); }
try { const e2 = load('src/locales/en/index.ts'); console.log('en parses OK, top keys:', Object.keys(e2).length); }
catch (e) { console.log('en PARSE FAIL:', e.message.split('\n')[0]); }

View File

@@ -1,563 +0,0 @@
// Generaate missing locale keys (best-effort) for zh + en.
const fs = require('fs');
const path = require('path');
const root = 'src';
const exts = ['.tsx', '.ts', '.jsx', '.js'];
const excludeDirs = ['locales', 'node_modules', 'i18n'];
function walk(dir) {
const out = [];
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, e.name);
if (e.isDirectory()) { if (!excludeDirs.includes(e.name)) out.push(...walk(full)); }
else if (exts.includes(path.extname(e.name))) out.push(full);
}
return out;
}
function load(p) {
let s = fs.readFileSync(p, 'utf8');
s = s.replace(/const\s+\w+\s*=\s*\{/, 'var obj = {');
s = s.replace(/export\s+default\s+\w+;?/, '');
s = s.replace(/\bconst\b/g, 'var');
return new Function(s + '\nreturn obj;')();
}
function flat(o, p, r) {
p = p || ''; r = r || {};
for (const k of Object.keys(o)) {
const key = p ? p + '.' + k : k;
if (o[k] && typeof o[k] === 'object' && !Array.isArray(o[k])) flat(o[k], key, r);
else r[key] = o[k];
}
return r;
}
function unflat() { return {}; }
const zhObj = load('src/locales/zh/index.ts');
const enObj = load('src/locales/en/index.ts');
const zhMap = flat(zhObj);
const enMap = flat(enObj);
// ---- token dictionaries ----
// camelCase token -> Chinese (for inferring zh of missing keys)
const tokZh = {
tl: '时间线', timeline: '时间线', mock: '模拟', device: '设备', devices: '设备',
sensor: '传感器', camera: '摄像头', station: '基站', battery: '电量', temp: '温度', temperature: '温度',
uav: '无人机', drone: '无人机', mower: '割草机', robot: '机器人', video: '视频',
status: '状态', task: '任务', tasks: '任务', complete: '完成', completed: '已完成', completing: '完成中',
start: '开始', end: '结束', startcleaning: '开始清洗', startinspection: '开始巡检', startmowing: '开始除草',
standby: '待机', ready: '就绪', return: '返航', supply: '补给', finish: '完成', inspection: '巡检', inspect: '巡检',
mow: '除草', maintenance: '维护', repair: '维修', clean: '清洗', cleaning: '清洗',
msg: '消息', message: '消息', no: '编号', alarm: '告警', rule: '规则', rules: '规则',
param: '参数', params: '参数', left: '左', right: '右', forward: '前', backward: '后', gain: '增益',
speed: '速度', chip: '芯片', uid: 'UID', sign: '标识', route: '路线', routes: '路线', path: '路径', point: '点', points: '点',
indoor: '室内', outdoor: '室外', wide: '广角', zoom: '变焦', lens: '镜头', ir: '红外', switch: '切换',
fail: '失败', failed: '失败', success: '成功', fetch: '获取', save: '保存', create: '创建', created: '已创建',
generate: '生成', export: '导出', import: '导入', download: '下载', upload: '上传', search: '搜索', reset: '重置',
confirm: '确认', cancel: '取消', view: '查看', select: '选择', selected: '已选', name: '名称', type: '类型',
time: '时间', date: '日期', total: '共', count: '数量', today: '今日', yesterday: '昨日',
week: '周', month: '月', day: '日', year: '年', online: '在线', offline: '离线',
error: '错误', errors: '错误', warning: '警告', info: '提示', critical: '严重', important: '重要', general: '一般',
pending: '待处理', closed: '已关闭', handled: '已处理', restored: '已恢复', ignored: '已忽略', falsealarm: '误报',
level: '级别', source: '来源', result: '结果', trend: '趋势', distribution: '分布', statistics: '统计',
subscription: '订阅', history: '历史', realtime: '实时', overview: '概览', analysis: '分析', diagnosis: '诊断',
defect: '缺陷', thermal: '热成像', component: '组件', string: '组串', inverter: '逆变器', efficiency: '效率',
risk: '风险', confidence: '置信度', location: '位置', suggestion: '建议', plan: '计划', execute: '执行',
mowing: '除草', inspect: '巡检', work: '工作', order: '工单', user: '用户', role: '角色', roles: '角色',
permission: '权限', menu: '菜单', organization: '组织', notice: '通知', data: '数据', safety: '安全',
access: '接入', protocol: '协议', longitude: '经度', latitude: '纬度', timezone: '时区', capacity: '容量',
address: '地址', leader: '负责人', contact: '联系', icon: '图标', sort: '排序', parent: '上级',
directory: '目录', button: '按钮', theme: '主题', light: '浅色', dark: '深色', language: '语言', version: '版本',
serial: '序列', verify: '验证', bind: '绑定', code: '码', list: '列表', refresh: '刷新', delete: '删除',
edit: '编辑', add: '新增', update: '更新', enable: '启用', disable: '停用', normal: '正常', abnormal: '异常',
loading: '加载中', generating: '生成中', processing: '处理中', exported: '已导出', uploadfile: '上传文件',
uploadimage: '上传图片', startanalysis: '开始分析', analysisresult: '分析结果', detected: '检测到',
noresult: '无结果', none: '无', unknown: '未知', invalid: '无效', gps: 'GPS', dgps: 'DGPS', rtk: 'RTK',
fixed: '固定', float: '浮点', config: '配置', configured: '已配置', connection: '连接', connect: '连接',
request: '请求', permissionreq: '权限请求', recover: '恢复', resume: '恢复', pause: '暂停', stopped: '停用',
working: '作业中', workingrobots: '作业中机器人', stat: '统计', alert: '告警', alerts: '告警',
deviceenvironment: '设备环境', currenttemp: '当前温度', humidity: '环境湿度', positionaccuracy: '定位精度',
networklatency: '网络延迟', waterrisk: '积水风险', preset: '预设', searchplaceholder: '搜索', unnamed: '未命名',
deleteroute: '删除航线', executeroute: '执行航线', taskboard: '任务看板', pendingtask: '待执行任务',
executing: '执行中', executingellipsis: '执行中', pausedtask: '已暂停任务', executeroutetask: '执行路线任务',
routeinfo: '路线信息', routename: '路线名称', selectexecdevice: '选择执行设备', panorama: '全景',
wdsun: '日', wdmon: '一', wtue: '二', wdwend: '三', wdthu: '四', wdfri: '五', wdsat: '六',
deviceserial: '设备编号', fillrequired: '请填写', selectexectime: '选择执行时间', taskcreated: '任务创建成功',
createfailed: '创建失败', createtaskerror: '创建任务异常', newtask: '新建任务', inputtaskname: '请输入任务名称',
executeroutelabel: '执行路线', selectroute: '选择路线', repeatplan: '重复计划', execdate: '执行日期',
selectexecdate: '选择执行日期', exectime: '执行时间', savetask: '保存任务', devicecontrol: '设备控制',
realtimevideo: '实时视频', takeoff: '起飞', land: '降落', returnhome: '返航', emergencystop: '紧急停止',
rotateleft: '左旋', rotateright: '右旋', gimbalpitch: '云台俯仰', gimbályaw: '云台偏航', zoomin: '放大', zoomout: '缩小',
wayline: '航线', waylinetask: '航线任务', robottask: '机器人任务', devicename: '设备名称', devicetype: '设备类型',
devicemodel: '设备型号', onlinestatus: '在线状态', lastheartbeat: '最后心跳', batterylevel: '电量',
signalstrength: '信号强度', control: '控制', uploadwayline: '上传航线', downloadwayline: '下载航线',
executetask: '执行任务', taskname: '任务名称', taskstatus: '任务状态', flighttime: '飞行时间', altitude: '高度',
distance: '距离', starttime: '开始时间', endtime: '结束时间', drone: '无人机', camera2: '摄像头',
creator: '创建者', executionplan: '执行计划', executioncycle: '执行周期', statusnew: '新建',
statuspaused: '暂停中', statusfinished: '执行成功', statusfailed: '执行失败', confirmdeletetask: '确定要删除该任务吗',
confirmcanceltask: '确定要取消该任务吗', confirmpausetask: '确定要暂停该任务吗', clearrealtime: '清空实时',
cleartrack: '清空实时轨迹', clearplanroute: '清空规划航线', controlmode: '控制模式', localmode: '本地',
remotemode: '远程', heading: '航向', initialized: '已初始化', uninitialized: '未初始化', taskpool: '任务池',
taskexecutionplan: '任务执行计划', taskhistory: '历史任务', createtask: '创建任务', startdate: '开始日期',
enddate: '结束日期', allrobots: '全部机器人', operationsuggestion: '作业编排建议', good: '良好',
devicelocation: '设备定位', devicevideo: '设备视频', robotpanorama: '机器人全景',
dayplan: '按天计划', weekplan: '按周计划', monthplan: '按月计划', daysuffix: '日', unknownplan: '未知计划',
mon: '周一', tue: '周二', wed: '周三', thu: '周四', fri: '周五', sat: '周六', sun: '周日',
vs: '较', unit: '单位', unitplan: '条', unitrobot: '台', unittimes: '次', completionrate: '完成率',
alertscount: '告警数量', costsaving: '节省成本', monthlyaccumulated: '本月累计', daily: '日', target: '目标',
installedcapacity: '装机容量', generationtrend: '发电趋势', irradiance: '辐照度', actual: '实际', predict: '预测',
invertterefficiency: '逆变器效率', maintenanceprogress: '运维进度', stationoverview: '电站概览',
recentinspection: '近期巡检', aianalysissummary: 'AI分析摘要', realtimealerts: '实时告警',
deviceonlinestatus: '设备在线状态', weatherinfo: '天气信息', cleaningsuggestion: '清洗建议',
cleanordergenerated: '清洗工单生成成功', pleaseelectstation: '请选择电站', generatecleanorder: '生成清洗工单',
kwh: 'kWh', kwp: 'kWp', undermaintenance: '维护中', abnormal2: '异常', normal2: '正常',
moduleinfo: '模块信息', heat: '热', hotspot: '热斑', component0: '组件', datapoint: '数据点', datapoints: '数据点',
exportfilename: '导出文件', processing2: '处理中', loadfailed: '加载失败', loaddetailfailed: '加载详情失败',
requesterror: '请求错误', handlesuccess: '处理成功', handlefail: '处理失败', alarmno: '告警编号',
devicedatas: '设备数据', working2: '作业中', mockdevice: '模拟设备', mocksensor: '模拟传感器',
mocktemp: '模拟温度', mocktempexceed: '模拟温度超限', mockbattery: '模拟电量', mockbatterylow: '模拟电量低',
mockvideosignal: '模拟视频信号', mockvideosignallost: '模拟视频信号丢失', mockrule: '模拟规则',
mocksub: '模拟订阅', mocksubops: '模拟运维', mocksubadmin: '模拟管理员', mocksubdevice: '模拟设备',
sms: '短信', email: '邮件', apppush: 'App推送', subscriptionname: '订阅名称', notifychannel: '通知渠道',
devicesensor: '设备传感器', devicebattery: '设备电量', alarmlevel: '告警级别', alarmtype: '告警类型',
alarmsource: '告警来源', alarmtime: '告警时间', handlestatus: '处理状态', handleresult: '处理结果',
handletime: '处理时间', handler: '处理人', handleremark: '处理备注', handle: '处理', closealert: '关闭告警',
confirmalert: '确认告警', exportalert: '导出告警', critical2: '严重', important2: '重要', general2: '一般',
info2: '提示', reopen: '重新打开', sevenDayTrend: '近7天趋势', sevenDayTrend2: '近7天趋势',
criticalAlert: '严重告警', importantAlert: '重要告警', generalAlert: '一般告警', infoAlert: '提示告警',
daySuffix2: '日', daySuffix3: '日', exported2: '已导出', loadDetailFailed2: '加载详情失败',
requestError2: '请求错误', handleSuccess2: '处理成功', handleFail2: '处理失败', alarmNo2: '告警编号',
locationUnknown: '未知位置', none2: '无', good2: '良好',
taskCompletedMsg: '任务完成消息', confirmDeleteTitle: '确认删除', confirmDeleteContent: '确认删除内容',
confirmDeleteOk: '确认删除', routeDeletedSuccess: '路线删除成功', deleteFailRetry: '删除失败重试',
deleteFailNetwork: '删除失败网络', taskListRefreshFail: '任务列表刷新失败', routeNoPathPoints: '路线无路径点',
waylineFallback: '航线兜底', waylineListFetchFail: '航线列表获取失败', fillRequiredFields: '请填写必填项',
createTaskSuccess: '创建任务成功', createTaskFail: '创建任务失败', createTaskException: '创建任务异常',
trendYesterday: '较昨日', trendFlat: '持平', taskListFetchFail: '任务列表获取失败',
fetchParamFail: '参数获取失败', paramSaveSuccess: '参数保存成功', paramSaveFail: '参数保存失败',
paramSaveException: '参数保存异常', paramLeftForwardGain: '左前增益', paramLeftBackwardGain: '左后增益',
paramRightForwardGain: '右前增益', paramRightBackwardGain: '右后增益', paramRunSpeed: '运行速度',
paramChipUidSign: '芯片UID标识', timelineStatusStart: '开始', timelineStatusReady: '就绪',
timelineStatusReturn: '返航', timelineStatusComplete: '完成', tlStartCleaning: '开始清洗',
tlStartInspection: '开始巡检', tlStartMowing: '开始除草', tlStandbyReady: '待机就绪',
tlReturnSupply: '返航补给', tlFinishInspection: '完成巡检', tlFinishMowing: '完成除草',
stationCameraNotFound: '基站摄像头未找到', switchStationCameraFail: '切换基站摄像头失败',
switchDroneLensFail: '切换无人机镜头失败', wideLens: '广角镜头', zoomLens: '变焦镜头', irLens: '红外镜头',
rainNo: '无雨', pointMarked: '已标记点', minThreePoints: '至少三个点', pathPlanDone: '路径规划完成',
pathPlanFailRetry: '路径规划失败重试', completePathFirst: '请先完成路径', inputPlotName: '输入区域名称',
connectionFailed: '连接失败', permissionRequestTitle: '权限请求', requestControlPermission: '请求控制权限',
selectNoDrone: '未选择无人机', commandIssued: '指令已下发', commandFailed: '指令下发失败',
videoConfigured: '视频已配置', noVideo: '无视频', working3: '作业中', online2: '在线', offline2: '离线',
// generic fallbacks
page: '页面', setting: '设置', settings: '设置', manage: '管理', management: '管理', center: '中心',
report2: '报表', log2: '日志', profile: '个人中心', login: '登录', logout: '退出', station2: '电站',
monitor2: '监控', ai: 'AI', optimization: '优化', task2: '任务', order2: '工单', alert2: '告警',
device2: '设备', system2: '系统', user2: '用户', role2: '角色',
};
// Comprehensive English-token -> Chinese lexicon (covers tokens from missing keys)
const lex = {
a01: '', ab: '', abnormal: '异常', account: '账号', accumulated: '累计', action: '操作', activated: '已激活', active: '启用',
add: '新增', added: '已添加', additional: '额外', admin: '管理员', advice: '建议', after: '之后', ai: 'AI', airport: '机场', airspace: '空域',
alarm: '告警', alert: '告警', alerts: '告警', algo: '算法', algorithm: '算法', alias: '别名', all: '全部', altitude: '高度', ambient: '环境',
analysis: '分析', and: '与', angle: '角度', api: 'API', app: '应用', approval: '审批', area: '区域', arm: '布防', array: '阵列', assign: '分配',
assigned: '已分配', async: '异步', auto: '自动', average: '平均', avg: '平均', avoid: '避开', back: '返回', backward: '后退', basic: '基础',
batch: '批量', battery: '电量', belonging: '所属', block: '区块', board: '看板', bound: '边界', bow: '船首', breakpoint: '断点', btn: '按钮',
by: '按', calendar: '日历', call: '呼叫', camera: '摄像头', cameras: '摄像头', can: '可', cancel: '取消', canceled: '已取消', cancelled: '已取消',
capacity: '容量', cargo: '货物', cause: '原因', center: '中心', change: '变更', channel: '渠道', channels: '渠道', charge: '充电', chassis: '底盘',
check: '检查', chip: '芯片', choose: '选择', clean: '清洗', cleaning: '清洗', clear: '清空', cleared: '已清空', click: '点击', climb: '爬升',
closed: '已关闭', closure: '闭合', code: '码', col: '列', colon: '冒号', comm: '通信', command: '指令', communicating: '通信中', communication: '通信',
compare: '对比', complete: '完成', completed: '已完成', completion: '完成', component: '组件', condition: '条件', conditions: '条件',
config: '配置', configured: '已配置', confirm: '确认', connected: '已连接', connection: '连接', contact: '联系', content: '内容', continuous: '连续',
control: '控制', controlling: '控制中', coordinated: '协同', count: '数量', cover: '覆盖', coverage: '覆盖', create: '创建', created: '已创建',
critical: '严重', cum: '累计', current: '当前', curve: '曲线', custom: '自定义', cycle: '周期', daily: '每日', data: '数据', date: '日期',
day: '日', days: '天', debug: '调试', default: '默认', del: '删除', delete: '删除', deleted: '已删除', demo: '演示', desc: '描述', description: '描述',
detail: '详情', details: '详情', detect: '检测', detected: '已检测', detection: '检测', deviation: '偏差', device: '设备', devices: '设备',
dgps: 'DGPS', diagnosis: '诊断', diff: '差异', ding: '钉钉', directory: '目录', dispatch: '派单', dispatchable: '可派单', display: '显示',
distribution: '分布', dock: '停靠', dome: '穹顶', done: '已完成', down: '下降', drag: '拖拽', drive: '驱动', drone: '无人机', drones: '无人机',
dual: '双', duty: '值班', edit: '编辑', effect: '效果', email: '邮件', emergency: '紧急', empty: '空', enabled: '已启用', end: '结束', ended: '已结束',
energy: '能量', engineer: '工程师', env: '环境', equipment: '设备', error: '错误', estimated: '预计', event: '事件', events: '事件', exceed: '超限',
excel: 'Excel', exception: '异常', exec: '执行', execute: '执行', executed: '已执行', executing: '执行中', execution: '执行', expired: '已过期',
export: '导出', fail: '失败', failed: '失败', failure: '失败', fallback: '兜底', fan: '风扇', fence: '围栏', fetch: '获取', fields: '字段', file: '文件',
fill: '填写', finish: '完成', fire: '火警', firmware: '固件', first: '先', fixed: '固定', flag: '标志', flat: '持平', flight: '飞行', float: '浮点',
fly: '飞行', forbidden: '禁止', formats: '格式', forward: '前进', found: '找到', frequency: '频率', fri: '周五', from: '从', front: '前', fuel: '燃料',
full: '全', gain: '增益', gantt: '甘特', gate: '闸门', ge: '地理', gear: '档位', general: '一般', generate: '生成', generated: '已生成',
generating: '生成中', generation: '发电', generator: '发电机', geojson: 'GeoJSON', get: '获取', gnss: 'GNSS', good: '良好', gps: 'GPS', gr: '电网',
grid: '电网', group: '分组', handle: '处理', handled: '已处理', handler: '处理人', hangar: '机库', has: '有', header: '表头', heading: '航向',
health: '健康', heatmap: '热力图', height: '高度', high: '高', hint: '提示', history: '历史', home: '首页', home2: '首页', horizontal: '水平',
hotspot: '热斑', hour: '小时', id: 'ID', image: '图片', immediate: '立即', important: '重要', improvement: '提升', in: '内', incident: '事件',
indoor: '室内', inference: '推理', info: '提示', initialized: '已初始化', input: '输入', inspect: '巡检', inspection: '巡检', integrated: '集成',
intelligent: '智能', interactive: '交互', intercom: '对讲', interval: '间隔', intrusion: '入侵', invalid: '无效', inverter: '逆变器', ir: '红外',
irradiance: '辐照度', irreversible: '不可逆', issue: '问题', issued: '已下发', item: '项', items: '项', json: 'JSON', key: '键', keyword: '关键词',
knife: '刀', label: '标签', landing: '降落', language: '语言', last: '最后', layout: '布局', left: '左', legend: '图例', length: '长度', lens: '镜头',
level: '级别', levels: '级别', lift: '抬升', limit: '限制', link: '链接', list: '列表', live: '直播', load: '加载', loaded: '已加载', local: '本地',
location: '位置', log: '日志', login: '登录', logs: '日志', loitering: '徘徊', lost: '丢失', low: '低', machine: '机器', main: '主', manage: '管理',
management: '管理', manager: '管理员', manual: '手动', map: '地图', mark: '标记', marked: '已标记', marks: '标记', mask: '掩码', members: '成员',
menu: '菜单', meteorology: '气象', mid: '中间', mileage: '里程', min: '最小', mock: '模拟', mode: '模式', model: '模型', models: '模型',
modified: '已修改', modify: '修改', mon: '周一', monitor: '监控', month: '月', monthly: '每月', more: '更多', motion: '运动', motor: '电机', mount: '挂载',
mow: '除草', mowing: '除草', msg: '消息', name: '名称', near: '附近', needed: '需要', network: '网络', new: '新', nickname: '昵称', night: '夜间',
no: '无', none: '无', normal: '正常', normally: '正常', north: '北', not: '未', notification: '通知', notify: '通知', now: '现在', number: '编号',
object: '对象', obstacle: '障碍', obtained: '已获取', occur: '发生', of: '的', offline: '离线', ok: '确定', old: '旧', om: '运维', on: '开', one: '一',
online: '在线', only: '仅', open: '打开', operate: '操作', operation: '操作', ops: '运维', optimal: '最优', optimization: '优化', optimize: '优化',
optional: '可选', or: '或', orchestration: '编排', order: '工单', orders: '工单', org: '组织', original: '原始', other: '其他', out: '外', outdoor: '室外',
over: '超过', overlay: '叠加', overview: '概览', page: '页面', panel: '面板', panorama: '全景', param: '参数', params: '参数', password: '密码',
path: '路径', pause: '暂停', paused: '已暂停', pdf: 'PDF', pending: '待处理', per: '每', percent: '百分比', perimeter: '周长', perm: '权限',
permission: '权限', person: '人员', personnel: '人员', phase: '相', phone: '电话', picture: '图片', pitch: '俯仰', pixels: '像素', placeholder: '占位',
plan: '计划', plane: '飞机', planned: '已计划', planning: '规划', platform: '平台', play: '播放', please: '请', plot: '区域', point: '点', points: '点',
polarity: '极性', port: '端口', position: '位置', positioning: '定位', power: '功率', pr: '光伏', precip: '降水', precision: '精度', predicted: '预测',
preparing: '准备中', preset: '预设', preview: '预览', priority: '优先级', probe: '探测', processing: '处理中', product: '产品', progress: '进度',
property: '属性', proportion: '比例', protect: '保护', ptz: '云台', pump: '泵', push: '推送', quality: '质量', quantity: '数量', rain: '雨',
rainfall: '降雨', range: '范围', rate: '率', rated: '额定', ratio: '比例', rbt: '机器人', read: '读取', ready: '就绪', realtime: '实时', rear: '后',
recent: '近期', recharge: '充电', recheck: '复检', recognition: '识别', recommended: '建议', record: '记录', recording: '录制', records: '记录',
recurring: '重复', refresh: '刷新', registry: '注册', related: '关联', relay: '中继', remark: '备注', remote: '远程', repeat: '重复', repellent: '驱避',
replay: '回放', report: '报表', reports: '报表', request: '请求', requesting: '请求中', required: '必填', reset: '重置', restored: '已恢复', result: '结果',
resume: '恢复', resumed: '已恢复', retry: '重试', return: '返航', returned: '已返航', revenue: '收益', reverse: '反向', right: '右', risk: '风险',
robot: '机器人', role: '角色', roles: '角色', room: '房间', root: '根', route: '路线', routes: '路线', rt: '实时', rth: '返航', rtk: 'RTK', rule: '规则',
run: '运行', running: '运行中', safe: '安全', sat: '卫星', satellite: '卫星', satellites: '卫星', save: '保存', saved: '已保存', saving: '节省',
schedule: '计划', scheduled: '已计划', screen: '屏幕', search: '搜索', seg: '分割', segment: '段', segmentation: '分割', select: '选择', selected: '已选',
send: '发送', sensor: '传感器', sent: '已发送', server: '服务器', services: '服务', servo: '伺服', setting: '设置', settings: '设置', seven: '七',
short: '短', show: '显示', sign: '标识', signal: '信号', single: '单', site: '站点', situation: '态势', size: '大小', smart: '智能', smoke: '烟', sms: '短信',
sn: '序列号', snapshots: '快照', sort: '排序', source: '来源', south: '南', span: '跨度', speed: '速度', standby: '待机', start: '开始', started: '已开始',
station: '电站', statistics: '统计', status: '状态', statuses: '状态', stop: '停止', stopped: '已停止', storage: '存储', store: '存储', stream: '流',
streams: '流', string: '组串', sub: '订阅', submit: '提交', subscribers: '订阅者', subscription: '订阅', subtitle: '字幕', success: '成功', suffix: '后缀',
suggest: '建议', suggested: '建议', suggestion: '建议', suitability: '适宜性', suitable: '适宜', summary: '摘要', sun: '太阳', super: '超级',
supervisor: '主管', supply: '补给', supported: '支持', supports: '支持', suspect: '疑似', suspend: '暂停', suspended: '已暂停', swap: '切换',
switch: '切换', symbol: '符号', sync: '同步', system: '系统', tai: '台', talk: '通话', task: '任务', tasks: '任务', temp: '温度', temperature: '温度',
template: '模板', terminated: '已终止', theoretical: '理论', thermal: '热成像', three: '三', threshold: '阈值', throttle: '油门', thu: '周四',
tiao: '条', time: '时间', timed: '定时', timeline: '时间线', timeout: '超时', timestamp: '时间戳', title: '标题', tl: '时间线', to: '到', today: '今日',
top: '顶部', topology: '拓扑', total: '总', tracking: '跟踪', trend: '趋势', trigger: '触发', triggered: '已触发', tue: '周二', turn: '转向', two: '二',
type: '类型', types: '类型', tyre: '轮胎', uav: '无人机', uid: 'UID', unit: '单位', unknown: '未知', up: '上升', update: '更新', upgrading: '升级中',
upload: '上传', usage: '用量', use4g: '4G', user: '用户', users: '用户', uuid: 'UUID', value: '值', version: '版本', vertical: '垂直', video: '视频',
view: '查看', visual: '可视', voltage: '电压', vs: '较', wait: '等待', waiting: '等待中', walk: '行走', wall: '墙', wash: '清洗', way: '路径',
wayline: '航线', waypoint: '航点', waypoints: '航点', weather: '天气', web: '网页', wed: '周三', weed: '杂草', week: '周', weekday: '工作日',
weekly: '每周', west: '西', wheel: '轮', which: '哪个', wide: '广角', width: '宽度', wifi: 'WiFi', wind: '风', window: '窗口', wing: '机翼', with: '带',
work: '工作', working: '作业中', workspace: '工作区', yes: '是', yesterday: '昨日', zero: '零', zone: '区域', zoom: '缩放',
};
const NOUNS_WITH_NO = new Set(['alarm', 'device', 'order', 'task', 'route', 'serial', 'station', 'work', 'user', 'role', 'id', 'sn', 'uav', 'drone', 'robot']);
// Chinese phrase/char -> English (for translating hardcoded chinese strings)
const zhEn = {
'保存': 'Save', '保存修改': 'Save Changes', '取消': 'Cancel', '确认': 'Confirm', '确定': 'OK', '删除': 'Delete',
'编辑': 'Edit', '新增': 'Add', '查询': 'Search', '搜索': 'Search', '重置': 'Reset', '提交': 'Submit', '关闭': 'Close',
'返回': 'Back', '导出': 'Export', '导入': 'Import', '下载': 'Download', '上传': 'Upload', '刷新': 'Refresh',
'查看': 'View', '查看全部': 'View All', '查看详情': 'View Details', '操作': 'Action', '状态': 'Status', '类型': 'Type',
'名称': 'Name', '描述': 'Description', '备注': 'Remark', '时间': 'Time', '日期': 'Date', '创建时间': 'Created At',
'更新时间': 'Updated At', '启用': 'Enable', '停用': 'Disable', '正常': 'Normal', '加载中': 'Loading',
'加载中...': 'Loading...', '暂无数据': 'No Data', '成功': 'Success', '失败': 'Failed', '删除成功': 'Deleted',
'删除失败': 'Delete failed', '编辑成功': 'Updated', '编辑失败': 'Update failed', '新增成功': 'Added',
'新增失败': 'Add failed', '保存成功': 'Saved', '保存失败': 'Save failed', '确定删除?': 'Are you sure to delete?',
'请选择': 'Please select', '请输入': 'Please enter', '全部': 'All', '是': 'Yes', '否': 'No', '男': 'Male', '女': 'Female',
'未知': 'Unknown', '未设置': 'Not set', '共': 'Total', '条': 'Items', '全选': 'Select all', '批量删除': 'Batch delete',
'已选': 'Selected', '更多': 'More', '展开': 'Expand', '收起': 'Collapse', '已完成': 'Completed',
'星期一': 'Monday', '星期二': 'Tuesday', '星期三': 'Wednesday', '星期四': 'Thursday', '星期五': 'Friday',
'星期六': 'Saturday', '星期日': 'Sunday',
'光伏机器人环境感知服务平台': 'PV Plant Intelligent Monitoring & O&M System',
'请选择电站': 'Please select a station', '全屏': 'Full Screen', '退出全屏': 'Exit Full Screen', '退出登录': 'Logout',
'电站地址': 'Station Address', '电站类型': 'Station Type', '运行状态': 'Running Status', '正常运行': 'Normal Running',
'地面电站': 'Ground Station', '1MW 光伏电站': '1MW PV Plant', '河南省林州': 'Linzhou, Henan',
'模块开发中': 'Module Developing',
'请输入账号': 'Enter account', '请输入密码': 'Enter password', '账号': 'Account', '密码': 'Password',
'登录': 'Login', '登录成功': 'Login success', '登录失败': 'Login failed', '欢迎': 'Welcome',
'用户协议': 'User Agreement', '隐私政策': 'Privacy Policy', '我已阅读并同意': 'I have read and agree',
'请阅读并同意用户协议': 'Please read and agree to the user agreement',
'总览首页': 'Overview', '设备管理': 'Device Management', '设备总览': 'Device Overview', '设备状态': 'Device Status',
'航线任务': 'Wayline Task', '机器人任务': 'Robot Task', '在线视频': 'Online Video', '实时监控': 'Real-time Monitor',
'告警中心': 'Alert Center', 'AI诊断分析': 'AI Diagnosis', '清洗优化': 'Cleaning Optimization', '工单任务': 'Work Order',
'报表中心': 'Report Center', '系统设置': 'System Settings', '日志管理': 'Log Management', '视频管理': 'Video Management',
'视频下载': 'Video Download', '用户管理': 'User Management', '角色管理': 'Role Management', '菜单管理': 'Menu Management',
'组织管理': 'Organization Management', '场站管理': 'Station Management', '通知策略页面': 'Notice Policy',
'数据与安全页面': 'Data & Safety', '个人中心': 'Profile', '请选择菜单': 'Please select a menu',
'当前功率': 'Current Power', '系统可用率': 'System Availability', '告警数量': 'Alert Count',
'节省运维成本': 'O&M Cost Saving', '日目标': 'Daily Target', '装机容量': 'Installed Capacity', '昨日': 'Yesterday',
'未处理': 'Unhandled', '已关闭': 'Closed', '本月累计节省': 'Monthly Accumulated Saving', '发电趋势': 'Generation Trend',
'辐照度与温度趋势': 'Irradiance & Temperature Trend', '逆变器效率对比': 'Inverter Efficiency', '运维任务进度': 'O&M Progress',
'电站概览': 'Station Overview', '近期巡检记录': 'Recent Inspection', 'AI 分析摘要': 'AI Analysis Summary',
'实时告警': 'Real-time Alerts', '设备在线状态': 'Device Online Status', '天气信息': 'Weather Info',
'清洗建议': 'Cleaning Suggestion', '实际': 'Actual', '预测': 'Predicted', '辐照度': 'Irradiance', '温度': 'Temperature',
'汇流箱': 'Combiner Box', '逆变器': 'Inverter', '巡检机器人': 'Inspection Robot', '异常': 'Abnormal', '维护中': 'Under Maintenance',
'生成清洗工单': 'Generate Cleaning Order', '请先选择电站': 'Please select station first', '清洗工单生成成功': 'Cleaning order created',
'kWh': 'kWh', 'kWp': 'kWp',
'设备名称': 'Device Name', '设备类型': 'Device Type', '设备型号': 'Device Model', '在线状态': 'Online Status',
'最后心跳': 'Last Heartbeat', '电量': 'Battery', '信号强度': 'Signal Strength', '位置': 'Location', '控制': 'Control',
'上传航线': 'Upload Wayline', '下载航线': 'Download Wayline', '执行任务': 'Execute Task', '任务名称': 'Task Name',
'任务状态': 'Task Status', '飞行时间': 'Flight Time', '高度': 'Altitude', '速度': 'Speed', '距离': 'Distance',
'开始时间': 'Start Time', '结束时间': 'End Time', '无人机': 'UAV', '割草机': 'Mower', '机器人': 'Robot', '摄像头': 'Camera',
'在线': 'Online', '离线': 'Offline', '待机': 'Standby', '运行中': 'Running', '充电中': 'Charging', '空闲': 'Idle',
'忙碌': 'Busy', '故障': 'Fault', '设备控制': 'Device Control', '实时视频': 'Real-time Video', '起飞': 'Takeoff',
'降落': 'Land', '返航': 'Return', '暂停': 'Pause', '继续': 'Resume', '紧急停止': 'Emergency Stop', '前进': 'Forward',
'后退': 'Backward', '左转': 'Turn Left', '右转': 'Turn Right', '上升': 'Up', '下降': 'Down', '左旋': 'Rotate Left',
'右旋': 'Rotate Right', '云台俯仰': 'Gimbal Pitch', '云台偏航': 'Gimbal Yaw', '放大': 'Zoom In', '缩小': 'Zoom Out',
'创建者': 'Creator', '执行计划': 'Execution Plan', '周日': 'Sun', '周一': 'Mon', '周二': 'Tue', '周三': 'Wed',
'周四': 'Thu', '周五': 'Fri', '周六': 'Sat', '执行周期': 'Execution Cycle', '新建': 'New', '暂停中': 'Paused',
'执行成功': 'Finished', '执行失败': 'Failed', '确定要删除该任务吗?': 'Delete this task?', '确定要取消该任务吗?': 'Cancel this task?',
'确定要暂停该任务吗?': 'Pause this task?', '恢复': 'Resume', '今日机器人计划': 'Today Robot Plan',
'进行中': 'In Progress', '作业中机器人': 'Working Robots', '台': 'Units', '作业中': 'Working', '累计作业里程': 'Total Distance',
'较昨日': 'vs Yesterday', '任务完成率': 'Task Completion Rate', '异常告警': 'Abnormal Alerts', '次': 'Times',
'在线率': 'Online Rate', '良好': 'Good', '机器人路径规划与监控': 'Robot Path Planning & Monitor',
'清空实时轨迹': 'Clear Track', '清空规划航线': 'Clear Plan', '控制模式:': 'Control Mode:', '本地': 'Local', '远程': 'Remote',
'航向:': 'Heading:', '已初始化': 'Initialized', '未初始化': 'Uninitialized', '定位:': 'Location:', '视频:': 'Video:',
'任务池': 'Task Pool', '任务执行计划': 'Task Execution Plan', '历史任务': 'Task History', '创建任务': 'Create Task',
'开始日期': 'Start Date', '结束日期': 'End Date', '全部机器人': 'All Robots', '作业编排建议': 'Operation Suggestion',
'开始除草任务': 'Start Mowing', '预计耗时 2 小时,当前进度 45%': 'ETA 2h, progress 45%', '预定巡检任务': 'Scheduled Inspection',
'待审批,建议执行区域 C': 'Pending, suggest area C', '设备例行维护': 'Routine Maintenance', '所有割草机返回充电站': 'All mowers return',
'智能生成今日计划': 'Auto-generate Today Plan', '设备环境与状态': 'Device Environment & Status', '当前温度': 'Current Temp',
'环境湿度': 'Humidity', '定位精度': 'Position Accuracy', 'RTK 固定': 'RTK Fixed', '网络延迟': 'Network Latency',
'注意:区域 B 存在积水风险,建议避开': 'Caution: area B flooding risk, avoid', '预设路线': 'Preset Routes',
'搜索...': 'Search...', '未命名路线': 'Unnamed Route', '删除航线': 'Delete Route', '执行航线': 'Execute Route',
'任务看板': 'Task Board', '待执行': 'Pending', '正在执行...': 'Executing...', '已暂停': 'Paused', '执行路线任务': 'Execute Route Task',
'执行': 'Execute', '路线信息': 'Route Info', '路线名称:': 'Route Name:', '请选择执行设备': 'Select Device', '机器人全景视频 - ': 'Robot Panorama - ',
'设备编号': 'Device No', '路线': 'Route', '请填写:': 'Please fill:', '请选择执行时间': 'Select Exec Time',
'任务创建成功': 'Task Created', '创建失败': 'Create Failed', '创建任务异常': 'Create Task Error', '新建任务': 'New Task',
'请输入任务名称': 'Enter Task Name', '执行路线': 'Execute Route', '选择路线': 'Select Route', '重复计划': 'Repeat Plan',
'天': 'Day', '周': 'Week', '月': 'Month', '执行日期': 'Exec Date', '选择执行日期': 'Select Exec Date', '执行时间': 'Exec Time',
'保存任务': 'Save Task',
'视频监控': 'Video Monitor', '实时监控': 'Real-time Monitor', '视频墙': 'Video Wall', '视频下载': 'Video Download',
'截图': 'Snapshot', '录制': 'Record', '播放': 'Play', '停止': 'Stop', '静音': 'Mute', '取消静音': 'Unmute', '直播': 'Live',
'回放': 'Replay', '选择设备': 'Select Device', '选择摄像头': 'Select Camera', '暂无视频': 'No Video', '视频加载中...': 'Loading video...',
'画质': 'Quality', '流畅': 'Fluency', '标清': 'SD', '高清': 'HD', '超清': 'FHD', '下载时间': 'Download Time',
'文件大小': 'File Size', '文件名': 'File Name', '下载状态': 'Download Status', '下载中': 'Downloading', '已下载': 'Downloaded',
'下载失败': 'Download Failed', '监控模式': 'Monitor Mode', '单画面': 'Single', '四画面': 'Quad', '九画面': 'Nine', '十六画面': 'Sixteen',
'告警概览': 'Alert Overview', '实时告警': 'Real-time Alert', '历史告警': 'Alert History', '告警规则': 'Alert Rules',
'告警统计': 'Alert Statistics', '告警订阅': 'Alert Subscription', '告警级别': 'Alert Level', '告警类型': 'Alert Type',
'告警来源': 'Alert Source', '告警时间': 'Alert Time', '处理状态': 'Handle Status', '处理结果': 'Handle Result',
'处理时间': 'Handle Time', '处理人': 'Handler', '处理备注': 'Handle Remark', '处理': 'Handle', '关闭告警': 'Close Alert',
'确认告警': 'Confirm Alert', '导出告警': 'Export Alert', '严重': 'Critical', '重要': 'Important', '一般': 'General',
'提示': 'Info', '待处理': 'Pending', '已处理': 'Handled', '已恢复': 'Restored', '已忽略': 'Ignored', '无法处理': 'Unable',
'厂家处理中': 'Manufacturer', '误报': 'False Alarm', '告警总数': 'Total Alerts', '今日告警': 'Today Alerts',
'未处理告警': 'Unhandled Alerts', '处理率': 'Handle Rate', '无人机故障': 'UAV Fault', '割草机故障': 'Mower Fault',
'其他设备故障': 'Other Device Fault', '服务器故障': 'Server Failure', '系统错误': 'System Error', '告警趋势': 'Alert Trend',
'告警分布': 'Alert Distribution',
'AI诊断分析': 'AI Diagnosis', '热成像分析': 'Thermal Analysis', '缺陷检测': 'Defect Detection', '温度分析': 'Temp Analysis',
'热斑检测': 'Hotspot Detection', '组件分析': 'Component Analysis', '组串分析': 'String Analysis', '逆变器分析': 'Inverter Analysis',
'上传图片': 'Upload Image', '开始分析': 'Start Analysis', '分析结果': 'Analysis Result', '缺陷类型': 'Defect Type',
'缺陷等级': 'Defect Level', '置信度': 'Confidence', '位置': 'Location', '处理建议': 'Suggestion', '异常温度': 'Abnormal Temp',
'最高温度': 'Max Temp', '平均温度': 'Avg Temp', '最低温度': 'Min Temp', '正常温度': 'Normal Temp', '热斑区域': 'Hotspot Area',
'风险等级': 'Risk Level', '高风险': 'High Risk', '中风险': 'Medium Risk', '低风险': 'Low Risk', '无风险': 'No Risk',
'检测到缺陷': 'Defects Detected', '未检测到缺陷': 'No Defect', '分析历史': 'Analysis History', '重新分析': 'Re-analyze',
'清洗优化': 'Cleaning Optimization', '清洗任务': 'Cleaning Task', '清洗建议': 'Cleaning Suggestion', '清洗计划': 'Cleaning Plan',
'清洗记录': 'Cleaning Record', '清洗效果': 'Cleaning Effect', '清洗前效率': 'Eff Before', '清洗后效率': 'Eff After',
'效率提升': 'Eff Improvement', '预计收益': 'Est Revenue', '清洗成本': 'Clean Cost', '净收益': 'Net Revenue', '优先级': 'Priority',
'建议日期': 'Suggest Date', '清洗区域': 'Area', '清洗方式': 'Method', '人工清洗': 'Manual', '机器人清洗': 'Robot',
'水清洗': 'Water', '干清洗': 'Dry', '状态': 'Status', '待清洗': 'Pending', '清洗中': 'Cleaning', '已清洗': 'Cleaned',
'已取消': 'Cancelled', '生成工单': 'Generate Order', '污损率': 'Soiling Rate', '功率损失': 'Power Loss',
'上次清洗日期': 'Last Clean', '建议清洗日期': 'Next Clean',
'工单任务': 'Work Order', '工单标题': 'Order Title', '工单类型': 'Order Type', '工单状态': 'Order Status',
'执行开始时间': 'Exec Start Time', '关联设备': 'Related Device', '关联区域': 'Related Area', '当前状态': 'Current Status',
'任务描述': 'Task Description', '附件素材': 'Attachments', '执行工单': 'Execute Order', '修改': 'Modify', '挂起': 'Suspend',
'完成': 'Complete', '派单': 'Dispatch', '修改工单': 'Modify Order', '工单派单': 'Dispatch Order', '近7天全部关联告警': '7d Alerts',
'请输入工单标题': 'Enter Title', '请选择工单类型': 'Select Type', '请选择优先级': 'Select Priority', '确认修改': 'Confirm Modify',
'确认派单': 'Confirm Dispatch', '确认执行': 'Confirm Execute', '确认完成': 'Confirm Complete', '选择接收人员': 'Select Receiver',
'请选择派单人员': 'Select Dispatcher', '选择执行设备': 'Select Device', '确定挂起?': 'Suspend?', '已创建': 'Created',
'执行中': 'In Progress', '已挂起': 'Suspended', '已完成': 'Completed', '待处理': 'Pending', '高': 'High', '中': 'Medium', '低': 'Low',
'AI 建议与关联信息': 'AI Suggestion', '建议携带': 'Suggested Equipment', '红外热像设备': 'Thermal Camera',
'优先复检组件': 'Prioritize Recheck', '预计影响发电': 'Est Impact', '关联告警趋势': 'Alert Trend', '关联告警列表': 'Alert List',
'近7天无告警记录': 'No alerts in 7d', '近 7 天': '(7d)', '高告警': 'High', '中告警': 'Medium', '低告警': 'Low',
'巡检': 'Inspection', '检修': 'Maintenance', '清洗': 'Cleaning', '维修': 'Repair', '运维': 'O&M', 'AI': 'AI',
'割草机故障': 'Mower Fault', '无人机故障': 'UAV Fault', '光伏组件故障': 'PV Module Fault', '清洗': 'Clean', '维护': 'Maintain',
'维修': 'Repair', '其他': 'Other',
'报表中心': 'Report Center', '发电量报表': 'Generation Report', '设备报表': 'Device Report', '告警报表': 'Alert Report',
'工单报表': 'Work Order Report', '巡检报表': 'Inspection Report', '清洗报表': 'Cleaning Report', '效率报表': 'Efficiency Report',
'自定义报表': 'Custom Report', '日报': 'Daily', '周报': 'Weekly', '月报': 'Monthly', '季报': 'Quarterly', '年报': 'Yearly',
'日期范围': 'Date Range', '生成报表': 'Generate Report', '导出报表': 'Export Report', '总发电量': 'Total Generation',
'峰值功率': 'Peak Power', '平均功率': 'Avg Power', '告警总数': 'Total Alerts', '工单总数': 'Total Orders', '完成率': 'Completion',
'平均效率': 'Avg Efficiency',
'系统设置': 'System Settings', '基础设置': 'Basic Settings', '用户管理': 'User Management', '菜单管理': 'Menu Management',
'组织管理': 'Organization Management', '场站管理': 'Station Management', '通知策略': 'Notice Policy', '设备接入': 'Device Access',
'数据与安全': 'Data & Safety', '系统维护': 'System Maintenance', '在线用户': 'Online Users', '设备管理': 'Device Management',
'日志管理': 'Log Management', '视频管理': 'Video Management', '系统名称': 'System Name', '系统版本': 'System Version',
'系统描述': 'System Description', '默认语言': 'Default Language', '系统Logo': 'System Logo', '主题': 'Theme', '浅色': 'Light',
'深色': 'Dark', '跟随系统': 'Follow System', '用户账号': 'Username', '用户昵称': 'Nickname', '手机号': 'Phone', '邮箱': 'Email',
'角色': 'Role', '密码': 'Password', '不填则不修改': 'Leave blank to keep', '语言': 'Language', '分配角色': 'Assign Role',
'性别': 'Gender', '状态': 'Status', '新增用户': 'Add User', '编辑用户': 'Edit User', '重置密码': 'Reset Password',
'确定将密码重置为 123456 吗?': 'Reset password to 123456?', '密码已重置为:123456': 'Password reset to 123456',
'中文': 'Chinese', '英语': 'English', '菜单名称': 'Menu Name', '菜单类型': 'Menu Type', '路由地址': 'Route Path',
'菜单图标': 'Menu Icon', '排序': 'Sort', '权限标识': 'Permission', '上级菜单': 'Parent Menu', '目录': 'Directory',
'菜单': 'Menu', '按钮': 'Button', '组织名称': 'Org Name', '组织类型': 'Org Type', '上级组织': 'Parent Org', '负责人': 'Leader',
'联系电话': 'Contact', '场站名称': 'Station Name', '场站类型': 'Station Type', '场站地址': 'Station Address',
'装机容量': 'Capacity', '场站状态': 'Station Status', '经度': 'Longitude', '纬度': 'Latitude', '时区': 'Timezone',
'日志类型': 'Log Type', '登录日志': 'Login Log', '操作日志': 'Operation Log', '错误日志': 'Error Log', '系统日志': 'System Log',
'操作人': 'Operator', '操作内容': 'Content', '操作时间': 'Time', 'IP地址': 'IP', '视频名称': 'Video Name', '视频地址': 'Video URL',
'视频类型': 'Video Type', '视频状态': 'Video Status', '备份数据库': 'Backup DB', '清除缓存': 'Clear Cache',
'系统信息': 'System Info', '磁盘使用': 'Disk', '内存使用': 'Memory', 'CPU使用率': 'CPU', '服务器状态': 'Server Status',
'运行时长': 'Uptime', '接入协议': 'Protocol', '接入状态': 'Status', '接入时间': 'Time', 'SN码': 'SN', '验证码': 'Verify Code',
'绑定设备': 'Bind Device', '错误编码': 'Error Code', '错误名称': 'Error Name', '错误来源': 'Error Source', '错误等级': 'Error Level',
'错误描述': 'Error Desc', '校验字段': 'Check Field', '对比规则': 'Compare Rule', '对比数值': 'Compare Value',
'区间最小值': 'Min', '区间最大值': 'Max', '请输入错误编码': 'Enter Error Code', '请输入错误名称': 'Enter Error Name',
'请选择来源': 'Select Source', '请选择等级': 'Select Level', '请选择错误来源': 'Select Error Source',
'请选择错误等级': 'Select Error Level', '请填写校验字段名': 'Enter Field', '请选择对比规则': 'Select Rule',
'请填写最小值': 'Enter Min', '请填写最大值': 'Enter Max', '请填写对比值': 'Enter Value',
'例如:ERR-ROBOT-001': 'e.g. ERR-ROBOT-001', '如:battery、speed、temperature': 'e.g. battery, speed',
'最小值': 'Min', '最大值': 'Max', '数值/文本,多个用逗号分隔': 'comma separated', '详细描述触发该错误的场景': 'Describe scenario',
'出现该错误后的处理方案': 'Handling plan', '新增错误规则': 'Add Error Rule', '编辑错误规则': 'Edit Error Rule',
'查看错误规则详情': 'View Error Rule', '确定要删除错误 "': 'Delete error "', '获取错误列表失败': 'Get error list failed',
'操作失败': 'Operation failed',
'个人中心': 'Profile', '获取个人信息失败': 'Get profile failed', '修改成功': 'Modified', '修改失败': 'Modify failed',
'密码修改成功': 'Password changed', '头像上传成功': 'Avatar uploaded', '上传失败': 'Upload failed',
'两次输入的密码不一致': 'Passwords mismatch', '更换头像': 'Change Avatar', '用户名称': 'Username', '用户昵称': 'Nickname',
'手机号码': 'Phone', '用户邮箱': 'Email', '所属角色': 'Role', '旧密码': 'Old Password', '新密码': 'New Password',
'确认密码': 'Confirm Password', '请输入用户昵称': 'Enter Nickname', '请输入手机号码': 'Enter Phone', '保存修改': 'Save Changes',
'确认修改': 'Confirm Modify',
'用户管理': 'User Management', '用户账号': 'Username', '用户昵称': 'Nickname', '手机号': 'Phone', '邮箱': 'Email',
'角色': 'Role', '状态': 'Status', '操作': 'Actions', '编辑': 'Edit', '删除': 'Delete', '用户账号': 'Username',
'用户昵称': 'Nickname', '手机号码': 'Phone', '密码': 'Password', '语言': 'Language', '分配角色': 'Assign Role',
'状态': 'Status', '性别': 'Gender', '不填则不修改': 'Leave blank', '中文': 'Chinese', '英语': 'English', '正常': 'Normal',
'停用': 'Disabled', '男': 'Male', '女': 'Female', '未知': 'Unknown', '批量删除(已选 ': 'Batch delete (', '重置密码': 'Reset Password',
'密码已重置为:123456': 'Password reset to 123456', '确定将密码重置为 123456 吗?': 'Reset to 123456?',
'角色管理': 'Role Management', '新增角色': 'Add Role', '角色名称': 'Role Name', '权限字符': 'Role Key', '排序': 'Sort',
'状态': 'Status', '备注': 'Remark', '操作': 'Actions', '编辑': 'Edit', '删除': 'Delete', '分配权限': 'Assign Permissions',
'数据权限': 'Data Scope', '菜单权限': 'Menu Permissions', '角色描述': 'Role Description', '正常': 'Normal', '停用': 'Disabled',
'异常': 'Abnormal',
'登录已过期,请重新登录': 'Session expired, please login again', '网络异常,请稍后重试': 'Network error, retry later',
'晴': 'Sunny', '多云': 'Cloudy', '阴': 'Overcast', '雨': 'Rainy',
};
// ---- collect referenced keys ----
const refRegex = /(?:i18n\.t|[^a-zA-Z]t)\(\s*['"`]([a-zA-Z][a-zA-Z0-9_.]*?)['"`]/g;
const files = walk(root);
const refKeys = new Set();
for (const f of files) {
const c = fs.readFileSync(f, 'utf8');
let m;
while ((m = refRegex.exec(c))) refKeys.add(m[1]);
}
// ---- helpers ----
function camelToWords(s) {
return s.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2').split(/[\s._]+/).filter(Boolean);
}
function inferZhFromKey(key) {
const leaf = key.split('.').pop();
const words = camelToWords(leaf).map(w => w.toLowerCase());
const out = [];
for (let i = 0; i < words.length; i++) {
const w = words[i];
let zh = lex[w] || tokZh[w] || (w.length <= 2 && w === w.toUpperCase() ? w : '');
if (w === 'no') zh = (i > 0 && NOUNS_WITH_NO.has(words[i - 1])) ? '编号' : '无';
if (!zh) continue;
if (out[out.length - 1] !== zh) out.push(zh);
}
return out.join('');
}
function inferEnFromKey(key) {
const leaf = key.split('.').pop();
const words = camelToWords(leaf);
const STOP = new Set(['of', 'and', 'the', 'to', 'in', 'on', 'with', 'a', 'an', 'or', 'for']);
const out = [];
for (let i = 0; i < words.length; i++) {
let w = words[i];
if (w.length <= 2 || w === w.toUpperCase()) { /* keep acronym */ }
else if (i > 0 && STOP.has(w.toLowerCase())) w = w.toLowerCase();
else w = w.charAt(0).toUpperCase() + w.slice(1);
if (out[out.length - 1] !== w) out.push(w);
}
return out.join(' ');
}
function translateZh(zh) {
if (zhEn[zh]) return zhEn[zh];
// greedy phrase substitution for multi-char matches
let s = zh;
// sort keys by length desc for greedy match
const keys = Object.keys(zhEn).sort((a, b) => b.length - a.length);
for (const k of keys) { if (k.length >= 2) s = s.split(k).join('\u0001' + zhEn[k] + '\u0001'); }
// now translate remaining single chars / unknown
const segs = s.split('\u0001');
const res = segs.map(seg => {
if (zhEn[seg]) return zhEn[seg];
// try per-char? skip unknown chars to avoid garbage
return seg;
});
return res.join(' ').replace(/\s+/g, ' ').trim();
}
// ---- determine additions ----
const zhAdd = {}; // ns -> {key: val}
const enAdd = {};
const zhCjk = {};
const enCjk = {};
function addKey(fullKey, zhVal, enVal, isCjk) {
const dot = fullKey.indexOf('.');
const ns = dot > -1 ? fullKey.slice(0, dot) : '';
const leaf = dot > -1 ? fullKey.slice(dot + 1) : fullKey;
const zmap = isCjk ? zhCjk : zhAdd;
const emap = isCjk ? enCjk : enAdd;
zmap[ns] = zmap[ns] || {};
emap[ns] = emap[ns] || {};
if (!(fullKey in zhMap)) zmap[ns][leaf] = zhVal;
if (!(fullKey in enMap)) emap[ns][leaf] = enVal;
}
// referenced keys missing
let missingCount = 0;
for (const k of refKeys) {
if (!(k in zhMap) && !(k in enMap)) {
missingCount++;
addKey(k, inferZhFromKey(k), inferEnFromKey(k));
}
}
// hardcoded chinese -> keys (reuse existing by value; else new key under a file-based ns)
// map file path to namespace
function nsForFile(f) {
const rel = f.replace(root + '\\', '').replace(/\\/g, '/');
const base = path.basename(f).replace(/\.(tsx|ts|jsx|js)$/, '');
// try to infer from existing namespaces by matching folder/file
const lower = rel.toLowerCase();
if (lower.includes('systemsetting')) return 'systemSetting';
if (lower.includes('devices/') || lower.includes('device')) return 'devices';
if (lower.includes('alerts')) return 'alerts';
if (lower.includes('aianalysis') || lower.includes('aidiagnos')) return 'aiAnalysis';
if (lower.includes('clean')) return 'clean';
if (lower.includes('report')) return 'report';
if (lower.includes('workorder')) return 'workOrder';
if (lower.includes('login')) return 'login';
if (lower.includes('profile')) return 'profile';
if (lower.includes('users')) return 'users';
if (lower.includes('roles')) return 'roles';
if (lower.includes('home')) return 'home';
if (lower.includes('video')) return 'video';
if (lower.includes('router') || lower.includes('layout')) return 'layout';
return 'page';
}
const CJK = /[一-鿿]/;
let cjkCount = 0;
const cjkSeen = new Set();
for (const f of files) {
const lines = fs.readFileSync(f, 'utf8').split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('//') || trimmed.startsWith('*')) continue;
if (/console\./.test(line)) continue;
if (!CJK.test(line)) continue;
// extract CJK substrings that look like user-facing text (skip pure comment lines)
const matches = line.match(/[一-鿿][一-鿿,。、::()()\sA-Za-z0-9%·\-/]*[一-鿿]/g);
if (!matches) continue;
for (const m of matches) {
const txt = m.trim();
if (txt.length < 2) continue;
if (cjkSeen.has(txt)) continue;
cjkSeen.add(txt);
cjkCount++;
// reuse existing key if zh value equals this
let reuseKey = null;
for (const ek of Object.keys(zhMap)) { if (zhMap[ek] === txt) { reuseKey = ek; break; } }
if (reuseKey) continue; // already have it
const ns = nsForFile(f);
// build a key from file base + counter
const base = path.basename(f).replace(/\.(tsx|ts|jsx|js)$/, '').replace(/[^a-zA-Z0-9]/g, '');
const key = ns + '.' + base + '_' + (cjkSeen.size);
addKey(key, txt, translateZh(txt), true);
}
}
}
console.log('Referenced keys total:', refKeys.size);
console.log('Missing referenced keys (added):', missingCount);
console.log('Hardcoded CJK unique strings seen:', cjkCount);
console.log('New keys to add (zh):', Object.values(zhAdd).reduce((s, o) => s + Object.keys(o).length, 0));
// save additions for next step
fs.writeFileSync('tmp_missing.json', JSON.stringify({ zhAdd, enAdd }, null, 2));
fs.writeFileSync('tmp_cjk.json', JSON.stringify({ zhCjk, enCjk }, null, 2));
console.log('Wrote tmp_missing.json and tmp_cjk.json');

View File

@@ -1,64 +0,0 @@
// Inject missing keys into zh/index.ts and en/index.ts, preserving structure.
const fs = require('fs');
function load(p) {
let s = fs.readFileSync(p, 'utf8');
s = s.replace(/const\s+\w+\s*=\s*\{/, 'var obj = {');
s = s.replace(/export\s+default\s+\w+;?/, '');
s = s.replace(/\bconst\b/g, 'var');
return new Function(s + '\nreturn obj;')();
}
function flat(o, p, r) {
p = p || ''; r = r || {};
for (const k of Object.keys(o)) {
const key = p ? p + '.' + k : k;
if (o[k] && typeof o[k] === 'object' && !Array.isArray(o[k])) flat(o[k], key, r);
else r[key] = o[k];
}
return r;
}
function esc(v) {
return String(v).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
}
const adds = require('./tmp_missing.json');
const zhAdd = adds.zhAdd, enAdd = adds.enAdd;
function inject(file, addsMap, label) {
const text = fs.readFileSync(file, 'utf8');
const lines = text.split('\n');
// find namespaces present
const present = new Set();
for (const ns of Object.keys(addsMap)) {
let nsStart = -1;
for (let i = 0; i < lines.length; i++) {
if (/^\t[A-Za-z_][A-Za-z0-9_]*:\s*\{/.test(lines[i]) && lines[i].trim().startsWith(ns + ':')) { nsStart = i; break; }
}
if (nsStart < 0) { console.log(' [WARN] namespace not found, skip: ' + ns); continue; }
// compute closing brace line by brace depth
let depth = 0; let closeLine = -1;
for (let i = nsStart; i < lines.length; i++) {
const op = (lines[i].match(/\{/g) || []).length;
const cl = (lines[i].match(/\}/g) || []).length;
depth += op - cl;
if (depth === 0) { closeLine = i; break; }
}
if (closeLine < 0) { console.log(' [WARN] no close for ' + ns); continue; }
// build insertion block (sorted)
const keys = Object.keys(addsMap[ns]).sort();
const block = keys.map(k => '\t\t' + k + ": '" + esc(addsMap[ns][k]) + "',").join('\n');
lines.splice(closeLine, 0, block);
console.log(' ' + ns + ': +' + keys.length + ' keys');
}
fs.writeFileSync(file, lines.join('\n'), 'utf8');
// verify parse
const obj = load(file);
const map = flat(obj);
console.log(' ' + label + ' parsed OK, total flat keys: ' + Object.keys(map).length);
}
console.log('Injecting into zh/index.ts ...');
inject('src/locales/zh/index.ts', zhAdd, 'zh');
console.log('Injecting into en/index.ts ...');
inject('src/locales/en/index.ts', enAdd, 'en');
console.log('Done.');

View File

@@ -1,75 +0,0 @@
// Inject the hardcoded-Chinese keys (tmp_newkeys.json) into locale files,
// preserving formatting by inserting before each namespace's closing brace.
const fs = require('fs');
const path = require('path');
function load(p) {
let s = fs.readFileSync(p, 'utf8');
s = s.replace(/const\s+\w+\s*=\s*\{/, 'var obj = {');
s = s.replace(/export\s+default\s+\w+;?/, '');
s = s.replace(/\bconst\b/g, 'var');
return new Function(s + '\nreturn obj;')();
}
function flat(o, p, r) { p = p || ''; r = r || {}; for (const k of Object.keys(o)) { const key = p ? p + '.' + k : k; if (o[k] && typeof o[k] === 'object' && !Array.isArray(o[k])) flat(o[k], key, r); else r[key] = o[k]; } return r; }
const newKeys = JSON.parse(fs.readFileSync('tmp_newkeys.json', 'utf8'));
const zhFlat = flat(load('src/locales/zh/index.ts'));
const enFlat = flat(load('src/locales/en/index.ts'));
function esc(v) { return String(v).replace(/\\/g, '\\\\').replace(/'/g, "\\'"); }
function insertIntoNs(text, ns, entries, indentUnit) {
// entries: [[base, value], ...] for keys NOT already present
const re = new RegExp('(\\n[ \\t]*)' + ns + ':\\s*\\{');
const m = re.exec(text);
if (!m) return null;
let i = m.index + m[0].length;
let depth = 1, j = i;
while (j < text.length && depth > 0) { const c = text[j]; if (c === '{') depth++; else if (c === '}') depth--; j++; }
const closeIdx = j - 1;
const indent = m[1];
const childIndent = indent + indentUnit;
const qk = (k) => /^[0-9]/.test(k) ? "'" + k + "'" : k;
const block = entries.map(([k, v]) => childIndent + qk(k) + ": '" + esc(v) + "',").join('\n');
return text.slice(0, closeIdx) + block + '\n' + text.slice(closeIdx);
}
function addNamespace(text, ns, entries, indentUnit) {
// find top-level const X = { ... } closing brace
const m = /const\s+\w+\s*=\s*\{/.exec(text);
let i = m.index + m[0].length;
let depth = 1, j = i;
while (j < text.length && depth > 0) { const c = text[j]; if (c === '{') depth++; else if (c === '}') depth--; j++; }
const closeIdx = j - 1;
const indent = '\n' + indentUnit;
const childIndent = indent + indentUnit;
const qk = (k) => /^[0-9]/.test(k) ? "'" + k + "'" : k;
const block = entries.map(([k, v]) => childIndent + qk(k) + ": '" + esc(v) + "',").join('\n');
const nsBlock = indent + ns + ': {' + '\n' + block + '\n' + indent + '}';
return text.slice(0, closeIdx) + ',' + nsBlock + text.slice(closeIdx);
}
for (const file of ['src/locales/zh/index.ts', 'src/locales/en/index.ts']) {
let text = fs.readFileSync(file, 'utf8');
const flatMap = file.includes('zh') ? zhFlat : enFlat;
const src = file.includes('zh') ? newKeys.zh : newKeys.en;
let added = 0;
for (const ns of Object.keys(src)) {
const entries = [];
for (const base of Object.keys(src[ns])) {
const key = ns + '.' + base;
if (key in flatMap) continue; // already present
entries.push([base, src[ns][base]]);
}
if (!entries.length) continue;
const before = text;
let res = insertIntoNs(text, ns, entries, '\t');
if (res === null) {
// namespace missing (e.g., 'page') -> create it
res = addNamespace(text, ns, entries, '\t');
}
if (res !== null && res !== before) { text = res; added += entries.length; }
}
fs.writeFileSync(file, text, 'utf8');
console.log(file + ' -> added ' + added + ' keys');
}

View File

@@ -1,9 +0,0 @@
const a = require('./tmp_missing.json');
const z = a.zhAdd, e = a.enAdd;
const ns = ['devices','alerts','aiAnalysis','clean','workOrder','systemSetting','home'];
for (const n of ns) {
const zk = z[n] || {}, ek = e[n] || {};
const keys = Object.keys(zk).slice(0, 14);
console.log('=== ' + n + ' (' + Object.keys(zk).length + ') ===');
for (const k of keys) { console.log(' ' + k + ' => ZH[' + zk[k] + '] EN[' + ek[k] + ']'); }
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,118 +0,0 @@
{
"zh": {
"devices": {
"uavUavSn": "未选中无人机或无人机SN不存在",
"fixed2SuccessFixed2": "指令下发成功",
"fixed2AbnormalFixed2": "接口请求异常",
"waylineTasksFixed2": "请先选择航线任务",
"fixed2ActionsFixed2Fixed2AbnormalFixed2": "操作接口异常",
"noDetailFixed2": "暂无详情",
"detailFixed2Fixed2AbnormalFixed2": "获取详情接口异常",
"fixed2DeviceFixed2InfoFixed2AbnormalFixed2": "设备信息异常",
"loginFixed2": "请先登录",
"fixed2LoginFixed2InfoLoginFixed2": "登录信息失效,请重新登录",
"fixed2OthersWebControlFixed2": "其他web端正在控制",
"abnormalRequestFailed": "网络异常或请求失败,请重试",
"noWaylineDataFixed2": "暂无航线数据",
"deleteFixed2Waypoint": "已删除航点并同步地图",
"waypoint2": "已清空所有航点与地图轨迹",
"selectTaskStartTimeFixed2": "请选择任务开始时间",
"fixed2Fixed2Abnormal": "接口异常,请检查",
"fixed2NoWaypointsSaveFixed2": "无航点可保存",
"fixed2UavSaveFixed2Local": "无人机不支持保存本地航线",
"addNewFixed2SaveLocalFixed2": "✅ 航线已新增保存到本地",
"taskSuspended2Fixed23": "请先下发任务",
"fixed2OthersControlFixed2": "其他端正在控制",
"failedFixed22": "权限校验失败",
"noneControl": "无控制权限",
"fixed2ControlFixed2": "控制请求超时"
},
"systemSetting": {
"fixed2MenuFixed2LoadFailedFixed2": "菜单加载失败",
"s26": "请先生成",
"notFixed2Search": "未查询到数据",
"fixed2SearchFixed2FailedFixed2": "查询失败",
"fixed2PleaseSelectDeleteRoleFixed2": "请选择要删除的角色",
"fixed2PleaseSelectDelete2": "请选择要删除的用户"
},
"home": {
"cleaningOrderGeneratedSuccessfullyFixed2": "✅ 清洗工单生成成功",
"failedFixed2": "❌ 工单生成接口调用失败"
},
"workOrder": {
"fixed2DispatchOrderFixed2SuccessFixed2": "工单派单成功",
"fixed2DispatchAbnormalFixed2": "派单请求异常",
"listFixed2Fixed2FailedFixed2": "获取工单列表失败",
"s3": "请先选择工单",
"completedFixed2": "工单已完成",
"completeFailFixed2": "工单完成失败",
"detailFixed2Fixed2FailedFixed2": "获取工单详情失败",
"s7": "请先选择一条工单",
"fixed2ModifyOrderFixed2SuccessFixed2": "修改工单成功",
"fixed2ModifyOrderFixed2FailedFixed2": "修改工单失败",
"executeSuccessFixed2": "工单执行成功",
"executeFailFixed2": "工单执行失败",
"suspendedFixed2": "工单已挂起",
"fixed2SuspendFixed2FailedFixed2": "挂起失败",
"fixed2PleaseSelectDelete": "请选择要删除的工单"
}
},
"en": {
"devices": {
"uavUavSn": "未选中 UAV 或 UAV SN不存在",
"fixed2SuccessFixed2": "Fixed2 指令 下发 Success Fixed2",
"fixed2AbnormalFixed2": "Fixed2 接口 请求 Abnormal Fixed2",
"waylineTasksFixed2": "请先选择 Wayline Tasks Fixed2",
"fixed2ActionsFixed2Fixed2AbnormalFixed2": "Fixed2 Actions Fixed2 接口 Fixed2 Abnormal Fixed2",
"noDetailFixed2": "暂 no Detail Fixed2",
"detailFixed2Fixed2AbnormalFixed2": "获取 Detail Fixed2 接口 Fixed2 Abnormal Fixed2",
"fixed2DeviceFixed2InfoFixed2AbnormalFixed2": "Fixed2 Device Fixed2 Info Fixed2 Abnormal Fixed2",
"loginFixed2": "请先 Login Fixed2",
"fixed2LoginFixed2InfoLoginFixed2": "Fixed2 Login Fixed2 Info 失效,请重新 Login Fixed2",
"fixed2OthersWebControlFixed2": "Fixed2 Others web端正在 Control Fixed2",
"abnormalRequestFailed": "网络 Abnormal 或 Request Failed ,请重试",
"noWaylineDataFixed2": "暂 no Wayline Data Fixed2",
"deleteFixed2Waypoint": "已 Delete Fixed2 Waypoint 并同步地图",
"waypoint2": "已清空所有 Waypoint 与地图轨迹",
"selectTaskStartTimeFixed2": "请 Select Task Start Time Fixed2",
"fixed2Fixed2Abnormal": "Fixed2 接口 Fixed2 Abnormal ,请检查",
"fixed2NoWaypointsSaveFixed2": "Fixed2 no Waypoints 可 Save Fixed2",
"fixed2UavSaveFixed2Local": "Fixed2 UAV 不支持 Save Fixed2 Local 航线",
"addNewFixed2SaveLocalFixed2": "✅ 航线已 Add New Fixed2 Save 到 Local Fixed2",
"taskSuspended2Fixed23": "请先下发 Task Suspended2 Fixed2",
"fixed2OthersControlFixed2": "Fixed2 Others 端正在 Control Fixed2",
"failedFixed22": "权限校验 Failed Fixed2",
"noneControl": "None Control 权限",
"fixed2ControlFixed2": "Fixed2 Control 请求 超时 Fixed2"
},
"systemSetting": {
"fixed2MenuFixed2LoadFailedFixed2": "Fixed2 Menu Fixed2 Load Failed Fixed2",
"s26": "请先生成",
"notFixed2Search": "Not Fixed2 Search 到数据",
"fixed2SearchFixed2FailedFixed2": "Fixed2 Search Fixed2 Failed Fixed2",
"fixed2PleaseSelectDeleteRoleFixed2": "Fixed2 Please select 要 Delete 的 Role Fixed2",
"fixed2PleaseSelectDelete2": "Fixed2 Please select 要 Delete 的用户"
},
"home": {
"cleaningOrderGeneratedSuccessfullyFixed2": "✅ Cleaning order generated successfully Fixed2",
"failedFixed2": "❌ 工单生成 接口 调用 Failed Fixed2"
},
"workOrder": {
"fixed2DispatchOrderFixed2SuccessFixed2": "Fixed2 Dispatch Order Fixed2 Success Fixed2",
"fixed2DispatchAbnormalFixed2": "Fixed2 Dispatch 请求 Abnormal Fixed2",
"listFixed2Fixed2FailedFixed2": "获取 工单 List Fixed2 Fixed2 Failed Fixed2",
"s3": "请先选择工单",
"completedFixed2": "工单 Completed Fixed2",
"completeFailFixed2": "工单 Complete Fail Fixed2",
"detailFixed2Fixed2FailedFixed2": "获取 工单 Detail Fixed2 Fixed2 Failed Fixed2",
"s7": "请先选择一条工单",
"fixed2ModifyOrderFixed2SuccessFixed2": "Fixed2 Modify Order Fixed2 Success Fixed2",
"fixed2ModifyOrderFixed2FailedFixed2": "Fixed2 Modify Order Fixed2 Failed Fixed2",
"executeSuccessFixed2": "工单 Execute Success Fixed2",
"executeFailFixed2": "工单 Execute Fail Fixed2",
"suspendedFixed2": "工单 Suspended Fixed2",
"fixed2SuspendFixed2FailedFixed2": "Fixed2 Suspend Fixed2 Failed Fixed2",
"fixed2PleaseSelectDelete": "Fixed2 Please select 要 Delete 的工单"
}
}
}

View File

@@ -1,28 +0,0 @@
const esbuild = require('esbuild');
const fs = require('fs');
const path = require('path');
(async () => {
const files = [
'components/SystemSetting/BasicSettings.tsx',
'components/SystemSetting/SerialNumGenerate.jsx',
'components/SystemSetting/DroneManagement.tsx',
'components/SystemSetting/SmartDeviceManagement.tsx',
'components/SystemSetting/AlarmSettingsManagement.tsx',
'components/SystemSetting/LogManagement.tsx',
'components/SystemSetting/OnlineUser.tsx',
'components/SystemSetting/VideoManagement.tsx',
'components/SystemSetting/WorkOrderSettingsManagement.tsx'
];
for (const f of files) {
const s = fs.readFileSync('src/' + f, 'utf8');
const ext = path.extname(f);
const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : ext === '.jsx' ? 'jsx' : 'js';
try {
await esbuild.transform(s, { loader, target: 'es2020' });
console.log('ORIG OK ' + f);
} catch (e) {
console.log('ORIG FAIL ' + f + ' :: ' + String(e.message).split('\n').slice(0, 4).join(' | '));
}
}
console.log('DONE');
})();

View File

@@ -1,33 +0,0 @@
src/components/devices/DeviceControl.tsx
src/components/devices/deviceController.js
src/components/devices/DeviceOverviewPage.tsx
src/components/devices/RobotTaskPage.tsx
src/components/LanguageSwitcher.tsx
src/components/SystemSetting/AlarmSettingsManagement.tsx
src/components/SystemSetting/AllDeviceManagement.tsx
src/components/SystemSetting/BasicSettings.tsx
src/components/SystemSetting/CameraManagement.tsx
src/components/SystemSetting/DeviceAccessPage.tsx
src/components/SystemSetting/DeviceManagement.tsx
src/components/SystemSetting/DroneManagement.tsx
src/components/SystemSetting/FlvPlayer.tsx
src/components/SystemSetting/LogManagement.tsx
src/components/SystemSetting/MenuList.tsx
src/components/SystemSetting/OnlineUser.tsx
src/components/SystemSetting/Organization.tsx
src/components/SystemSetting/SerialNumGenerate.jsx
src/components/SystemSetting/SmartDeviceManagement.tsx
src/components/SystemSetting/StationManage.tsx
src/components/SystemSetting/SystemMaintain.tsx
src/components/SystemSetting/UserPage.tsx
src/components/SystemSetting/VideoManagement.tsx
src/components/SystemSetting/WorkOrderSettingsManagement.tsx
src/components/VideoPlayer.tsx
src/pages/Agri.jsx
src/pages/AIAnalysis.tsx
src/pages/CleanOptimization.tsx
src/pages/Home.tsx
src/pages/RealTimeMonitor.tsx
src/pages/SystemSetting.tsx
src/pages/VideoMonitorPage.tsx
src/pages/WorkOrder.tsx

View File

@@ -1,43 +0,0 @@
const fs = require('fs');
const path = require('path');
const root = 'src';
const exts = ['.tsx', '.ts', '.jsx', '.js'];
const excludeDirs = ['locales', 'node_modules', 'i18n'];
const CJK = /[一-鿿]/;
function walk(dir) {
const out = [];
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, e.name);
if (e.isDirectory()) { if (!excludeDirs.includes(e.name)) out.push(...walk(full)); }
else if (exts.includes(path.extname(e.name))) out.push(full);
}
return out;
}
const files = walk(root);
const result = [];
for (const f of files) {
const lines = fs.readFileSync(f, 'utf8').split('\n');
let userFacing = 0;
let totalCJK = 0;
for (const line of lines) {
const trimmed = line.trim();
// skip pure comments
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) continue;
// skip console.* lines
if (/console\./.test(line)) continue;
// skip lines that are only a comment after code (heuristic: contains // and no JSX/t() before)
if (CJK.test(line)) {
totalCJK++;
// user-facing heuristics: JSX text, string props, antd message, Option
const isCommentOnly = /\/\/.*[一-鿿]/.test(line) && !/(>|placeholder|title|label|content|message\.|notification\.|Modal\.|Tooltip|text=|children)/.test(line);
if (!isCommentOnly) userFacing++;
}
}
if (totalCJK > 0) result.push({ f: f.replace(root + '\\', ''), totalCJK, userFacing });
}
result.sort((a, b) => b.userFacing - a.userFacing);
console.log('Files with CJK:', result.length);
console.log('Total CJK lines (all):', result.reduce((s, r) => s + r.totalCJK, 0));
console.log('Estimated user-facing CJK lines:', result.reduce((s, r) => s + r.userFacing, 0));
console.log('\nPer-file (user-facing est):');
for (const r of result) console.log(String(r.userFacing).padStart(5), '\t', r.f);

View File

@@ -1,148 +0,0 @@
// Conservative hardcoded-Chinese -> t() replacement with esbuild syntax gate.
// Scope: files already using t(); only unambiguous UI contexts; braces/parens excluded;
// files that fail esbuild transform are NOT written.
const fs = require('fs');
const path = require('path');
const esbuild = require('esbuild');
const root = 'src';
const exts = ['.tsx', '.ts', '.jsx', '.js'];
const excludeDirs = ['locales', 'node_modules', 'i18n'];
function walk(dir) {
const out = [];
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, e.name);
if (e.isDirectory()) { if (!excludeDirs.includes(e.name)) out.push(...walk(full)); }
else if (exts.includes(path.extname(e.name))) out.push(full);
}
return out;
}
function load(p) {
let s = fs.readFileSync(p, 'utf8');
s = s.replace(/const\s+\w+\s*=\s*\{/, 'var obj = {');
s = s.replace(/export\s+default\s+\w+;?/, '');
s = s.replace(/\bconst\b/g, 'var');
return new Function(s + '\nreturn obj;')();
}
function flat(o, p, r) { p = p || ''; r = r || {}; for (const k of Object.keys(o)) { const key = p ? p + '.' + k : k; if (o[k] && typeof o[k] === 'object' && !Array.isArray(o[k])) flat(o[k], key, r); else r[key] = o[k]; } return r; }
const zhMap = flat(load('src/locales/zh/index.ts'));
const enMap = flat(load('src/locales/en/index.ts'));
const valToKey = {}; const valToEn = {};
for (const k of Object.keys(zhMap)) { if (typeof zhMap[k] === 'string') { if (!(zhMap[k] in valToKey)) valToKey[zhMap[k]] = k; if (typeof enMap[k] === 'string') valToEn[zhMap[k]] = enMap[k]; } }
const allKeys = new Set([...Object.keys(zhMap), ...Object.keys(enMap)]);
const phraseKeys = Object.keys(valToEn).sort((a, b) => b.length - a.length);
function translateZh(zh) {
if (valToEn[zh]) return valToEn[zh];
let s = zh;
for (const k of phraseKeys) { if (k.length >= 2) s = s.split(k).join('\u0001' + valToEn[k] + '\u0001'); }
return s.split('\u0001').map(seg => valToEn[seg] || seg).join(' ').replace(/\s+/g, ' ').trim() || zh;
}
function nsForFile(f) {
const rel = f.replace(root + '\\', '').replace(/\\/g, '/').toLowerCase();
if (rel.includes('systemsetting')) return 'systemSetting';
if (rel.includes('devices/') || rel.includes('device')) return 'devices';
if (rel.includes('alerts')) return 'alerts';
if (rel.includes('aianalysis') || rel.includes('aidiagnos')) return 'aiAnalysis';
if (rel.includes('clean')) return 'clean';
if (rel.includes('report')) return 'report';
if (rel.includes('workorder')) return 'workOrder';
if (rel.includes('login')) return 'login';
if (rel.includes('profile')) return 'profile';
if (rel.includes('users')) return 'users';
if (rel.includes('roles')) return 'roles';
if (rel.includes('home')) return 'home';
if (rel.includes('video')) return 'video';
if (rel.includes('router') || rel.includes('layout')) return 'layout';
return 'page';
}
function camel(s) {
s = s.replace(/[^A-Za-z0-9]+/g, ' ').trim().toLowerCase();
const parts = s.split(' ').filter(Boolean);
if (!parts.length) return '';
return parts.map((p, i) => i === 0 ? p : p.charAt(0).toUpperCase() + p.slice(1)).join('');
}
const SKIP_ATTR = new Set(['key', 'id', 'path', 'href', 'src', 'to', 'type', 'name', 'value', 'className', 'data', 'mode', 'status', 'field', 'prop', 'ref']);
const UI_PROP = new Set(['title', 'content', 'okText', 'cancelText', 'message', 'description', 'label', 'placeholder', 'text', 'tooltip', 'subTitle', 'subtitle']);
const rxJSX = />([^<>`{}()]*[一-鿿][^<>`{}()]*)</g;
const rxAttr = /([A-Za-z_][\w-]*)=(["'`])((?:\\.|(?!\2)[^{}()])*?[一-鿿]+(?:\\.|(?!\2)[^{}()])*?)\2/g;
const rxMsg = /([A-Za-z_$][\w$]*\.(?:error|success|warning|info|loading|open))\(\s*(["'`])((?:\\.|(?!\2)[^{}()])*?[一-鿿]+(?:\\.|(?!\2)[^{}()])*?)\2\)/g;
const rxProp = /(^|[^.\w])((?:title|content|okText|cancelText|message|description|label|placeholder|text|tooltip|subTitle|subtitle)):\s*(["'`])((?:\\.|(?!\3)[^{}()])*?[一-鿿]+(?:\\.|(?!\3)[^{}()])*?)\3/g;
function loaderFor(f) { const e = path.extname(f); return e === '.tsx' ? 'tsx' : e === '.ts' ? 'ts' : e === '.jsx' ? 'jsx' : 'js'; }
const files = walk(root);
const newZh = {}, newEn = {};
const pending = [];
const balanceWarns = [];
let skippedNoT = 0;
for (const f of files) {
const src0 = fs.readFileSync(f, 'utf8');
if (!/useTranslation|[^A-Za-z]t\(/.test(src0)) { continue; }
if (!/[一-鿿]/.test(src0)) continue;
const map = {};
function keyFor(s) {
const t = s.trim();
if (!t) return null;
if (t in map) return map[t];
if (t in valToKey) { map[t] = valToKey[t]; return map[t]; }
const ns = nsForFile(f);
let base = camel(translateZh(t)) || ('s' + Object.keys(map).length);
let cand = ns + '.' + base;
let i = 2;
while (allKeys.has(cand) || cand in (newZh[ns] || {})) cand = ns + '.' + base + (i++);
map[t] = cand;
const leaf = cand.slice(ns.length + 1);
newZh[ns] = newZh[ns] || {}; newEn[ns] = newEn[ns] || {};
newZh[ns][leaf] = t; newEn[ns][leaf] = translateZh(t);
return cand;
}
let s = src0;
let rep = 0;
s = s.replace(rxJSX, (m, inner) => { const k = keyFor(inner); if (!k) return m; rep++; return '>{t(\'' + k + '\')}<'; });
s = s.replace(rxAttr, (m, attr, q, val) => {
if (SKIP_ATTR.has(attr)) return m;
if (val.includes('${')) return m;
const k = keyFor(val); if (!k) return m; rep++; return attr + '={t(\'' + k + '\')}';
});
s = s.replace(rxMsg, (m, fn, q, val) => { if (fn.startsWith('console.')) return m; const k = keyFor(val); if (!k) return m; rep++; return fn + "(t('" + k + "'))"; });
s = s.replace(rxProp, (m, pre, prop, q, val) => { if (!UI_PROP.has(prop)) return m; const k = keyFor(val); if (!k) return m; rep++; return pre + prop + ": t('" + k + "')"; });
if (rep > 0) {
const ob = (s.match(/[({]/g) || []).length, cb = (s.match(/[)}]/g) || []).length;
const o0 = (src0.match(/[({]/g) || []).length, c0 = (src0.match(/[)}]/g) || []).length;
if (ob !== cb) balanceWarns.push(f.replace(root + '\\', '') + ' braces ' + ob + '/' + cb + ' (orig ' + o0 + '/' + c0 + ')');
pending.push({ f, s, rep });
}
}
(async () => {
const modified = [];
let totalRep = 0;
for (const p of pending) {
let ok = false; let errMsg = '';
try { await esbuild.transform(p.s, { loader: loaderFor(p.f), target: 'es2020' }); ok = true; }
catch (err) { errMsg = String(err.message); }
if (ok) {
if (!process.env.DRY) fs.writeFileSync(p.f, p.s, 'utf8');
modified.push(p.f);
totalRep += p.rep;
} else {
console.log(' --- detail for ' + p.f.replace(root + '\\', '') + ' ---');
console.log(errMsg);
}
}
console.log('Files written/verified:', modified.length);
console.log('Replacements made:', totalRep);
console.log('Balance warnings (informational):', balanceWarns.length);
balanceWarns.forEach(w => console.log(' [WARN] ' + w));
console.log('New locale keys (zh):', Object.values(newZh).reduce((a, o) => a + Object.keys(o).length, 0));
fs.writeFileSync('tmp_newkeys.json', JSON.stringify({ zh: newZh, en: newEn }, null, 2));
console.log('Modified files:');
modified.forEach(f => console.log(' ' + f.replace(root + '\\', '')));
})();

View File

@@ -1,45 +0,0 @@
const fs = require('fs');
const path = require('path');
const dir = 'src';
const exts = ['.tsx', '.ts', '.jsx', '.js'];
function walk(d) {
let r = [];
for (const f of fs.readdirSync(d)) {
const p = path.join(d, f);
if (fs.statSync(p).isDirectory()) {
if (p.includes('locales')) continue;
r = r.concat(walk(p));
} else if (exts.some((e) => f.endsWith(e))) {
r.push(p);
}
}
return r;
}
// strip comments (line + block) and template strings, then find remaining Chinese
function stripCommentsAndStrings(code) {
// Remove block comments
code = code.replace(/\/\*[\s\S]*?\*\//g, '');
// Remove line comments (careful with URLs, but ok)
code = code.replace(/^\s*\/\/.*$/gm, '');
return code;
}
let total = 0;
for (const f of walk(dir)) {
const code = fs.readFileSync(f, 'utf8');
const cleaned = stripCommentsAndStrings(code);
const lines = cleaned.split('\n');
lines.forEach((line, i) => {
if (/[\u4e00-\u9fa5]/.test(line)) {
total++;
const trimmed = line.trim();
if (trimmed.length > 0 && !trimmed.startsWith('//')) {
console.log(f + ':' + (i + 1) + ': ' + line.trim().slice(0, 120));
}
}
});
}
console.log('TOTAL non-comment lines with Chinese:', total);

View File

@@ -1,16 +0,0 @@
// Detect possible HTML-string false matches: a JS string literal that now
// contains an injected {t(...)} (e.g. '<span>{t('x')}</span>').
const fs = require('fs');
const files = [
"src/components/devices/DeviceControl.tsx","src/components/devices/deviceController.js","src/components/devices/DeviceOverviewPage.tsx","src/components/devices/RobotTaskPage.tsx","src/components/LanguageSwitcher.tsx","src/components/SystemSetting/AlarmSettingsManagement.tsx","src/components/SystemSetting/AllDeviceManagement.tsx","src/components/SystemSetting/BasicSettings.tsx","src/components/SystemSetting/CameraManagement.tsx","src/components/SystemSetting/DeviceAccessPage.tsx","src/components/SystemSetting/DeviceManagement.tsx","src/components/SystemSetting/DroneManagement.tsx","src/components/SystemSetting/FlvPlayer.tsx","src/components/SystemSetting/LogManagement.tsx","src/components/SystemSetting/MenuList.tsx","src/components/SystemSetting/OnlineUser.tsx","src/components/SystemSetting/Organization.tsx","src/components/SystemSetting/SerialNumGenerate.jsx","src/components/SystemSetting/SmartDeviceManagement.tsx","src/components/SystemSetting/StationManage.tsx","src/components/SystemSetting/SystemMaintain.tsx","src/components/SystemSetting/UserPage.tsx","src/components/SystemSetting/VideoManagement.tsx","src/components/SystemSetting/WorkOrderSettingsManagement.tsx","src/components/VideoPlayer.tsx","src/pages/Agri.jsx","src/pages/AIAnalysis.tsx","src/pages/CleanOptimization.tsx","src/pages/Home.tsx","src/pages/RealTimeMonitor.tsx","src/pages/SystemSetting.tsx","src/pages/VideoMonitorPage.tsx","src/pages/WorkOrder.tsx"
];
const rx = /(['"`])<[^'"`]*\{t\(/g;
let total = 0;
for (const f of files) {
const lines = fs.readFileSync(f, 'utf8').split('\n');
lines.forEach((line, idx) => {
rx.lastIndex = 0; let m;
while ((m = rx.exec(line))) { console.log(f.replace('src\\', '') + ' L' + (idx + 1) + ': ' + line.trim()); total++; }
});
}
console.log('Potential HTML-string false matches:', total);

View File

@@ -1,27 +0,0 @@
const fs = require('fs');
function load(p) {
let s = fs.readFileSync(p, 'utf8');
s = s.replace(/const\s+\w+\s*=\s*\{/, 'var obj = {');
s = s.replace(/export\s+default\s+\w+;?/, '');
s = s.replace(/\bconst\b/g, 'var');
return new Function(s + '\nreturn obj;')();
}
function flat(o, p, r) {
p = p || '';
r = r || {};
for (const k of Object.keys(o)) {
const key = p ? p + '.' + k : k;
if (o[k] && typeof o[k] === 'object' && !Array.isArray(o[k])) flat(o[k], key, r);
else r[key] = o[k];
}
return r;
}
try {
const z = flat(load('src/locales/zh/index.ts'));
const e = flat(load('src/locales/en/index.ts'));
console.log('ZH keys', Object.keys(z).length, 'EN keys', Object.keys(e).length);
console.log('aiAnalysis.abnormalString in ZH?', 'aiAnalysis.abnormalString' in z);
console.log('workOrder.pleaseInputTitle in ZH?', 'workOrder.pleaseInputTitle' in z);
} catch (err) {
console.log('ERROR:', err.message);
}

View File

@@ -1,12 +0,0 @@
const a = require('./tmp_missing.json');
const z = a.zhAdd;
const tokens = new Set();
for (const ns of Object.keys(z)) {
for (const leaf of Object.keys(z[ns])) {
const words = leaf.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2').split(/[\s._]+/).filter(Boolean);
for (const w of words) tokens.add(w.toLowerCase());
}
}
const arr = [...tokens].sort();
console.log('Token count:', arr.length);
console.log(arr.join(' '));

View File

@@ -1,8 +0,0 @@
const fs = require('fs');
function load(p){let s=fs.readFileSync(p,'utf8');s=s.replace(/const\s+\w+\s*=\s*\{/,'var obj = {');s=s.replace(/export\s+default\s+\w+;?/,'');s=s.replace(/\bconst\b/g,'var');return new Function(s+'\nreturn obj;')();}
function flat(o,p,r){p=p||'';r=r||{};for(const k of Object.keys(o)){const key=p?p+'.'+k:k;if(o[k]&&typeof o[k]==='object'&&!Array.isArray(o[k]))flat(o[k],key,r);else r[key]=o[k];}return r;}
const z=flat(load('src/locales/zh/index.ts'));const e=flat(load('src/locales/en/index.ts'));
console.log('ZH',Object.keys(z).length,'EN',Object.keys(e).length);
console.log('common.completed in EN?', 'common.completed' in e);
console.log('home.underMaintenance in EN?', 'home.underMaintenance' in e);
console.log('roles.abnormal in EN?', 'roles.abnormal' in e);

View File

@@ -1,44 +0,0 @@
const fs = require('fs');
const path = require('path');
const root = 'src';
const exts = ['.tsx', '.ts', '.jsx', '.js'];
const excludeDirs = ['locales', 'node_modules', 'i18n'];
function walk(dir) {
const out = [];
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, e.name);
if (e.isDirectory()) { if (!excludeDirs.includes(e.name)) out.push(...walk(full)); }
else if (exts.includes(path.extname(e.name))) out.push(full);
}
return out;
}
function load(p) {
let s = fs.readFileSync(p, 'utf8');
s = s.replace(/const\s+\w+\s*=\s*\{/, 'var obj = {');
s = s.replace(/export\s+default\s+\w+;?/, '');
s = s.replace(/\bconst\b/g, 'var');
return new Function(s + '\nreturn obj;')();
}
function flat(o, p, r) { p = p || ''; r = r || {}; for (const k of Object.keys(o)) { const key = p ? p + '.' + k : k; if (o[k] && typeof o[k] === 'object' && !Array.isArray(o[k])) flat(o[k], key, r); else r[key] = o[k]; } return r; }
const zhMap = flat(load('src/locales/zh/index.ts'));
const enMap = flat(load('src/locales/en/index.ts'));
const refRegex = /(?:i18n\.t|[^a-zA-Z]t)\(\s*['"`]([a-zA-Z][a-zA-Z0-9_.]*?)['"`]/g;
const files = walk(root);
const refKeys = new Set();
for (const f of files) {
const c = fs.readFileSync(f, 'utf8');
let m; while ((m = refRegex.exec(c))) refKeys.add(m[1]);
}
let missZh = [], missEn = [], missBoth = [];
for (const k of refKeys) {
if (!(k in zhMap)) missZh.push(k);
if (!(k in enMap)) missEn.push(k);
if (!(k in zhMap) && !(k in enMap)) missBoth.push(k);
}
console.log('Referenced unique keys:', refKeys.size);
console.log('Missing in ZH:', missZh.length);
console.log('Missing in EN:', missEn.length);
console.log('Missing in BOTH:', missBoth.length);
if (missBoth.length) console.log('SAMPLE missing-both:', missBoth.slice(0, 20).join(', '));

View File

@@ -1,49 +0,0 @@
// Final check: every t('key') / i18n.t('key') reference in src must exist in locale.
const fs = require('fs');
const path = require('path');
function load(p) {
let s = fs.readFileSync(p, 'utf8');
s = s.replace(/const\s+\w+\s*=\s*\{/, 'var obj = {');
s = s.replace(/export\s+default\s+\w+;?/, '');
s = s.replace(/\bconst\b/g, 'var');
return new Function(s + '\nreturn obj;')();
}
function flat(o, p, r) { p = p || ''; r = r || {}; for (const k of Object.keys(o)) { const key = p ? p + '.' + k : k; if (o[k] && typeof o[k] === 'object' && !Array.isArray(o[k])) flat(o[k], key, r); else r[key] = o[k]; } return r; }
const zh = flat(load('src/locales/zh/index.ts'));
const en = flat(load('src/locales/en/index.ts'));
const have = new Set([...Object.keys(zh), ...Object.keys(en)]);
const root = 'src';
const exts = ['.tsx', '.ts', '.jsx', '.js'];
function walk(dir) {
const out = [];
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, e.name);
if (e.isDirectory()) { if (!['locales', 'node_modules'].includes(e.name)) out.push(...walk(full)); }
else if (exts.includes(path.extname(e.name))) out.push(full);
}
return out;
}
const rx = /[^A-Za-z]t\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
const missing = {}; // key -> [files]
let total = 0;
for (const f of walk(root)) {
const s = fs.readFileSync(f, 'utf8');
let m; rx.lastIndex = 0;
while ((m = rx.exec(s))) {
const k = m[1];
if (k.startsWith('_')) continue; // i18n internal
total++;
if (!have.has(k)) (missing[k] = missing[k] || []).push(f.replace(root + '\\', ''));
}
}
const keys = Object.keys(missing);
console.log('Total t() references scanned:', total);
console.log('Distinct missing keys:', keys.length);
if (keys.length) {
for (const k of keys.slice(0, 60)) console.log(' MISSING ' + k + ' <- ' + [...new Set(missing[k])].join(', '));
}
// also report keys whose zh value is itself a raw key-like (english) — quality check
let rawLike = 0;
for (const k of Object.keys(zh)) { const v = zh[k]; if (typeof v === 'string' && /^[a-z][a-zA-Z0-9]*\.[a-zA-Z0-9.]+$/.test(v)) rawLike++; }
console.log('zh entries whose value looks like a key (possible untranslated):', rawLike);

View File

@@ -1,74 +0,0 @@
const fs = require('fs');
const path = require('path');
const ts = require('typescript');
function transpile(file) {
const code = fs.readFileSync(file, 'utf8');
const out = ts.transpileModule(code, {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2019 },
});
return out.outputText;
}
function load(file) {
const out = transpile(file);
const module = { exports: {} };
const fn = new Function('module', 'exports', out);
fn(module, module.exports);
return module.exports.default || module.exports;
}
function flatten(obj, prefix = '') {
const keys = new Set();
for (const k of Object.keys(obj)) {
const v = obj[k];
const p = prefix ? prefix + '.' + k : k;
if (v && typeof v === 'object' && !Array.isArray(v)) {
for (const sk of flatten(v, p)) keys.add(sk);
} else {
keys.add(p);
}
}
return keys;
}
const zhKeys = flatten(load('src/locales/zh/index.ts'));
const enKeys = flatten(load('src/locales/en/index.ts'));
console.log('zh keys:', zhKeys.size, 'en keys:', enKeys.size);
const dir = 'src';
const exts = ['.tsx', '.ts', '.jsx', '.js'];
function walk(d) {
let r = [];
for (const f of fs.readdirSync(d)) {
const p = path.join(d, f);
if (fs.statSync(p).isDirectory()) {
r = r.concat(walk(p));
} else if (exts.some((e) => f.endsWith(e)) && !p.includes('locales')) {
r.push(p);
}
}
return r;
}
const used = new Set();
const fileKey = {};
for (const f of walk(dir)) {
const code = fs.readFileSync(f, 'utf8');
const re = /(?:[^.\w]|^)(?:t|i18n\.t)\(\s*['"]([a-zA-Z0-9_.-]+)['"]/g;
let m;
while ((m = re.exec(code)) !== null) {
const k = m[1];
used.add(k);
(fileKey[k] = fileKey[k] || new Set()).add(f);
}
}
console.log('Total distinct keys used in source:', used.size);
const missing = [...used].filter((k) => !zhKeys.has(k) || !enKeys.has(k));
console.log('Keys used but missing in locale files:', missing.length);
for (const k of missing.sort()) {
console.log(' ' + k + ' <- ' + [...(fileKey[k] || [])].join(', '));
}
// Also detect literal raw t('...') keys that might be dynamic (skip)