Compare commits

...

8 Commits

  1. 2
      README.md
  2. 30
      client/src/app.less
  3. 2
      client/src/sections/wuyuanbiaoba/components/CameraView.jsx
  4. 164
      client/src/sections/wuyuanbiaoba/components/RealtimeCharts.jsx
  5. 4
      client/src/sections/wuyuanbiaoba/components/RealtimeDataTable.jsx
  6. 16
      client/src/sections/wuyuanbiaoba/components/TargetList.jsx
  7. 16
      client/src/sections/wuyuanbiaoba/components/TemplateList.jsx
  8. 3
      client/src/sections/wuyuanbiaoba/container/index.jsx
  9. 2
      package.json

2
README.md

@ -48,7 +48,7 @@ npm run pack
### 离线版本说明
离线部署包构建指令
```shell
npm run build:offline
npm run pack:offline
```
该指令执行后会连接到某个远程的linux环境,进行部署包的构建:详情请查看 `script/package-app-offline.sh` 文件

30
client/src/app.less

@ -5,6 +5,36 @@
user-select: none;
}
// Keep tab panes inside the viewport so long target lists can scroll internally.
.target-tabs {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.target-tabs > .ant-tabs-content-holder {
flex: 1;
min-height: 0;
}
.target-tabs .ant-tabs-content,
.target-tabs .ant-tabs-tabpane {
height: 100%;
min-height: 0;
}
.target-tabs .ant-tabs-tabpane:not(.ant-tabs-tabpane-hidden) {
display: flex;
flex-direction: column;
}
// Keep Ant Design's inactive tab panes hidden. The active-pane flex rule above
// must not override this class, otherwise both list panes render vertically.
.target-tabs .ant-tabs-tabpane.ant-tabs-tabpane-hidden {
display: none;
}
// 美化滚动条
::-webkit-scrollbar {
width: 8px;

2
client/src/sections/wuyuanbiaoba/components/CameraView.jsx

@ -3,7 +3,7 @@ import { useWebSocket } from "../actions/websocket.jsx";
import { message } from "antd";
// import { useTargetStorage } from "../hooks/useTargetStorage.js";
const MAX_RECTANGLES = 20;
const MAX_RECTANGLES = 35;
const getNextTargetNameIndex = (rectangles) => {
return rectangles.reduce((maxIndex, rectangle) => {

164
client/src/sections/wuyuanbiaoba/components/RealtimeCharts.jsx

@ -32,6 +32,152 @@ ChartJS.register(
const { Title } = Typography;
// 使用 HTML Tooltip,避免 Chart.js Canvas 将多标靶内容裁剪掉。
const getOrCreateExternalTooltip = (chart) => {
const canvas = chart.canvas;
let tooltipEl = canvas._externalTooltipEl;
if (!tooltipEl) {
tooltipEl = document.createElement("div");
tooltipEl.className = "chartjs-external-tooltip";
Object.assign(tooltipEl.style, {
position: "fixed",
maxWidth: "260px",
maxHeight: "min(280px, calc(100vh - 20px))",
overflowY: "auto",
padding: "10px 12px",
background: "#ffffff",
border: "1px solid #d9d9d9",
borderRadius: "4px",
boxShadow: "0 3px 12px rgba(0, 0, 0, 0.18)",
color: "#333333",
fontSize: "12px",
lineHeight: "1.6",
whiteSpace: "nowrap",
pointerEvents: "none",
opacity: "0",
zIndex: "20",
transform: "translate(-50%, -100%)",
transition: "opacity 0.08s ease",
});
tooltipEl.addEventListener("mouseenter", () => {
// Tooltip 已经离开图表后,不允许仅靠再次移入旧位置重新显示。
if (tooltipEl.dataset.active !== "true") {
return;
}
if (tooltipEl._hideTimer) {
clearTimeout(tooltipEl._hideTimer);
tooltipEl._hideTimer = null;
}
tooltipEl.dataset.hovered = "true";
tooltipEl.style.opacity = "1";
tooltipEl.style.pointerEvents = "auto";
});
tooltipEl.addEventListener("mouseleave", () => {
tooltipEl.dataset.hovered = "false";
tooltipEl._hideTimer = setTimeout(() => {
if (tooltipEl.dataset.hovered !== "true") {
tooltipEl.style.opacity = "0";
tooltipEl.style.pointerEvents = "none";
tooltipEl.dataset.active = "false";
}
}, 150);
});
document.body.appendChild(tooltipEl);
canvas._externalTooltipEl = tooltipEl;
}
return tooltipEl;
};
const externalTooltipHandler = ({ chart, tooltip }) => {
const tooltipEl = getOrCreateExternalTooltip(chart);
if (tooltip.opacity === 0) {
if (tooltipEl.dataset.hovered !== "true") {
if (tooltipEl._hideTimer) {
clearTimeout(tooltipEl._hideTimer);
}
tooltipEl._hideTimer = setTimeout(() => {
if (tooltipEl.dataset.hovered !== "true") {
tooltipEl.style.opacity = "0";
tooltipEl.style.pointerEvents = "none";
tooltipEl.dataset.active = "false";
}
}, 150);
}
return;
}
if (tooltipEl._hideTimer) {
clearTimeout(tooltipEl._hideTimer);
tooltipEl._hideTimer = null;
}
tooltipEl.replaceChildren();
(tooltip.title || []).forEach((title) => {
const titleEl = document.createElement("div");
titleEl.style.fontWeight = "600";
titleEl.style.marginBottom = "4px";
titleEl.textContent = title;
tooltipEl.appendChild(titleEl);
});
(tooltip.body || []).forEach((bodyItem) => {
(bodyItem.lines || []).forEach((line, index) => {
const lineEl = document.createElement("div");
lineEl.style.display = "flex";
lineEl.style.alignItems = "center";
const color = tooltip.labelColors?.[tooltip.body.indexOf(bodyItem)];
if (color) {
const markerEl = document.createElement("span");
Object.assign(markerEl.style, {
width: "8px",
height: "8px",
marginRight: "6px",
borderRadius: "50%",
// 与折线保持一致,优先使用不透明的 borderColor。
background: color.borderColor || color.backgroundColor,
flex: "0 0 auto",
});
lineEl.appendChild(markerEl);
}
const textEl = document.createElement("span");
textEl.textContent = line;
lineEl.appendChild(textEl);
tooltipEl.appendChild(lineEl);
});
});
const canvasRect = chart.canvas.getBoundingClientRect();
const positionX = canvasRect.left + tooltip.caretX;
const positionY = canvasRect.top + tooltip.caretY;
const tooltipHeight = tooltipEl.offsetHeight;
const tooltipWidth = tooltipEl.offsetWidth;
const spaceAbove = positionY;
const spaceBelow = window.innerHeight - positionY;
const canPlaceAbove =
spaceAbove >= tooltipHeight + 8 || spaceBelow < tooltipHeight + 8;
const clampedX = Math.max(
tooltipWidth / 2 + 8,
Math.min(positionX, window.innerWidth - tooltipWidth / 2 - 8)
);
// 使用 fixed 定位,避免 Tooltip 超出图表后撑高整个页面。
// 上方空间不足时放到数据点下方,避免鼠标向上移动时离开可交互区域。
tooltipEl.style.transform = canPlaceAbove
? "translate(-50%, calc(-100% - 8px))"
: "translate(-50%, 8px)";
tooltipEl.style.opacity = "1";
tooltipEl.style.pointerEvents = "auto";
tooltipEl.dataset.active = "true";
tooltipEl.style.left = `${clampedX}px`;
tooltipEl.style.top = `${positionY}px`;
};
const RealtimeCharts = ({ tableData, lastUpdateTime, onDataExport,exportCount = 200, setExportCount }) => {
const xChartRef = useRef(null);
const yChartRef = useRef(null);
@ -39,6 +185,18 @@ const RealtimeCharts = ({ tableData, lastUpdateTime, onDataExport,exportCount =
// 添加标靶显示状态控制
const [visibleTargets, setVisibleTargets] = useState({});
// 外部 Tooltip 挂载在 body 上,组件卸载时主动清理,避免遗留 DOM。
useEffect(() => {
return () => {
[xChartRef.current, yChartRef.current].forEach((chart) => {
const tooltipEl = chart?.canvas?._externalTooltipEl;
if (tooltipEl?.parentNode) {
tooltipEl.parentNode.removeChild(tooltipEl);
}
});
};
}, []);
// 添加数据验证
if (!Array.isArray(tableData)) {
return <div>数据格式错误</div>;
@ -314,6 +472,8 @@ const RealtimeCharts = ({ tableData, lastUpdateTime, onDataExport,exportCount =
display: false, // 隐藏默认图例,使用自定义控制器
},
tooltip: {
enabled: false,
external: externalTooltipHandler,
filter: function (tooltipItem) {
return tooltipItem.parsed.y !== null;
},
@ -584,6 +744,8 @@ const RealtimeCharts = ({ tableData, lastUpdateTime, onDataExport,exportCount =
border: "1px solid #e8e8e8",
minHeight: "280px",
padding: "16px",
position: "relative",
overflow: "visible",
}}
>
<Line
@ -602,6 +764,8 @@ const RealtimeCharts = ({ tableData, lastUpdateTime, onDataExport,exportCount =
border: "1px solid #e8e8e8",
minHeight: "280px",
padding: "16px",
position: "relative",
overflow: "visible",
}}
>
<Line

4
client/src/sections/wuyuanbiaoba/components/RealtimeDataTable.jsx

@ -49,8 +49,8 @@ const RealtimeDataTable = ({ realtimeData }) => {
</Title>
<div
style={{
height: "calc(600px - 60px - 32px)",
overflow: "hidden",
height: "calc(800px - 60px - 32px)",
overflow: "auto",
}}
>
<Table

16
client/src/sections/wuyuanbiaoba/components/TargetList.jsx

@ -62,18 +62,28 @@ const TargetList = ({
];
return (
<div style={{ flex: 1, overflow: "auto", padding: "0 16px" }}>
<div
style={{
flex: 1,
minHeight: 0,
display: "flex",
flexDirection: "column",
padding: "0 16px",
overflow: "hidden",
}}
>
<div style={{ flex: 1, minHeight: 0, overflowY: "auto" }}>
<Table
dataSource={targetListData}
columns={targetColumns}
pagination={false}
size="small"
style={{ height: "100%" }}
showHeader={false}
/>
</div>
<Button
type="primary"
style={{ marginTop: "16px", width: "100%" }}
style={{ margin: "16px 0", width: "100%", flexShrink: 0 }}
onClick={onClickClearAll}
>
一键清零

16
client/src/sections/wuyuanbiaoba/components/TemplateList.jsx

@ -74,19 +74,29 @@ const TemplateList = ({
];
return (
<div style={{ flex: 1, overflow: "auto", padding: "0 16px" }}>
<div
style={{
flex: 1,
minHeight: 0,
display: "flex",
flexDirection: "column",
padding: "0 16px",
overflow: "hidden",
}}
>
<div style={{ flex: 1, minHeight: 0, overflowY: "auto" }}>
<Table
dataSource={tempListData}
columns={tempColumns}
pagination={false}
size="small"
style={{ height: "100%" }}
showHeader={false}
/>
</div>
{tempListData.length < 3 && (
<Button
type="primary"
style={{ marginTop: "16px", width: "100%" }}
style={{ margin: "16px 0", width: "100%", flexShrink: 0 }}
onClick={onAddTemplate}
>
添加新模板

3
client/src/sections/wuyuanbiaoba/container/index.jsx

@ -576,16 +576,19 @@ const WuyuanbiaobaContent = () => {
<div
style={{
flex: 1,
minHeight: 0,
backgroundColor: "white",
display: "flex",
flexDirection: "column",
}}
>
<Tabs
className="target-tabs"
defaultActiveKey="target-list"
items={tabItems}
style={{
height: "100%",
minHeight: 0,
display: "flex",
flexDirection: "column",
}}

2
package.json

@ -1,6 +1,6 @@
{
"name": "wuyuanbiaoba-web",
"version": "1.1.5-tag2",
"version": "1.1.7",
"main": "index.html",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",

Loading…
Cancel
Save