加个橡皮擦
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
"document_root": "./web",
|
||||
"upload_path": "uploads",
|
||||
"client_max_body_size": "10M",
|
||||
"client_max_memory_body_size": "20M",
|
||||
"max_connections": 1000,
|
||||
"log": {
|
||||
"log_path": "",
|
||||
|
||||
@@ -75,6 +75,45 @@
|
||||
color: #fde68a;
|
||||
}
|
||||
|
||||
.editToolGroup {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
border: 1px solid #343449;
|
||||
border-radius: 8px;
|
||||
background: #151520;
|
||||
}
|
||||
|
||||
.toolButton {
|
||||
min-height: 34px;
|
||||
border: 0;
|
||||
border-right: 1px solid #343449;
|
||||
background: transparent;
|
||||
color: #cbd5e1;
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toolButton:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.toolButton:hover {
|
||||
background: #24243a;
|
||||
}
|
||||
|
||||
.toolButton.active {
|
||||
background: rgba(251, 191, 36, 0.14);
|
||||
color: #fde68a;
|
||||
}
|
||||
|
||||
.toolButton.erase.active {
|
||||
background: rgba(248, 113, 113, 0.14);
|
||||
color: #fecaca;
|
||||
}
|
||||
|
||||
.uploadButton input {
|
||||
display: none;
|
||||
}
|
||||
@@ -607,6 +646,16 @@ select:focus {
|
||||
background: rgba(251, 191, 36, 0.16);
|
||||
}
|
||||
|
||||
.chartBrush.erase {
|
||||
border-color: rgba(248, 113, 113, 0.95);
|
||||
background: rgba(248, 113, 113, 0.08);
|
||||
box-shadow: 0 0 0 1px rgba(16, 16, 26, 0.8), 0 0 18px rgba(248, 113, 113, 0.24);
|
||||
}
|
||||
|
||||
.chartBrush.erase.dragging {
|
||||
background: rgba(248, 113, 113, 0.16);
|
||||
}
|
||||
|
||||
.chartTools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -49,7 +49,9 @@ const DESIGN_FIELD_HINTS = {
|
||||
|
||||
const CHUNK_SIZE = 4000;
|
||||
const CHART_HEIGHT = 540;
|
||||
const BRUSH_RADIUS = 16;
|
||||
const BRUSH_RADIUS = 10;
|
||||
const EDIT_TOOL_RESTORE = 'restore';
|
||||
const EDIT_TOOL_ERASE = 'erase';
|
||||
const DEFAULT_SCHEME_ID = 'scheme_one';
|
||||
const SCHEME_TWO_ID = 'scheme_two';
|
||||
const DEFAULT_SCHEMES = [
|
||||
@@ -436,7 +438,9 @@ function PowerCurveChart({
|
||||
showFiltered,
|
||||
designPoints,
|
||||
editMode,
|
||||
editTool,
|
||||
onRestorePoints,
|
||||
onErasePoints,
|
||||
}) {
|
||||
const frameRef = useRef(null);
|
||||
const chartRef = useRef(null);
|
||||
@@ -587,14 +591,17 @@ function PowerCurveChart({
|
||||
dragging: nextDragging,
|
||||
});
|
||||
};
|
||||
const restoreAt = (event) => {
|
||||
if (!chart.over || !onRestorePoints) return;
|
||||
const applyToolAt = (event) => {
|
||||
if (!chart.over) return;
|
||||
const rect = chart.over.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
const radiusSquared = BRUSH_RADIUS * BRUSH_RADIUS;
|
||||
const restored = [];
|
||||
for (const point of validFiltered) {
|
||||
const targetPoints = editTool === EDIT_TOOL_ERASE
|
||||
? validScatter
|
||||
: validFiltered.filter((point) => point.reason !== 'manual_erased');
|
||||
const hitPoints = [];
|
||||
for (const point of targetPoints) {
|
||||
const pointX = chart.valToPos(point.wind_speed, 'x');
|
||||
const pointY = chart.valToPos(point.active_power, 'y');
|
||||
if (!Number.isFinite(pointX) || !Number.isFinite(pointY)) {
|
||||
@@ -603,11 +610,16 @@ function PowerCurveChart({
|
||||
const dx = pointX - x;
|
||||
const dy = pointY - y;
|
||||
if (dx * dx + dy * dy <= radiusSquared) {
|
||||
restored.push(point);
|
||||
hitPoints.push(point);
|
||||
}
|
||||
}
|
||||
if (restored.length) {
|
||||
onRestorePoints(restored);
|
||||
if (!hitPoints.length) {
|
||||
return;
|
||||
}
|
||||
if (editTool === EDIT_TOOL_ERASE) {
|
||||
onErasePoints?.(hitPoints);
|
||||
} else {
|
||||
onRestorePoints?.(hitPoints);
|
||||
}
|
||||
};
|
||||
const handlePointerDown = (event) => {
|
||||
@@ -615,12 +627,12 @@ function PowerCurveChart({
|
||||
event.preventDefault();
|
||||
dragging = true;
|
||||
updateBrush(event, true);
|
||||
restoreAt(event);
|
||||
applyToolAt(event);
|
||||
};
|
||||
const handlePointerMove = (event) => {
|
||||
updateBrush(event);
|
||||
if (dragging) {
|
||||
restoreAt(event);
|
||||
applyToolAt(event);
|
||||
}
|
||||
};
|
||||
const handlePointerUp = (event) => {
|
||||
@@ -647,7 +659,9 @@ function PowerCurveChart({
|
||||
validDesign,
|
||||
showFiltered,
|
||||
editMode,
|
||||
editTool,
|
||||
onRestorePoints,
|
||||
onErasePoints,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -661,7 +675,12 @@ function PowerCurveChart({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`chartFrame ${editMode ? 'chartFrameEditing' : ''}`} ref={frameRef}>
|
||||
<div
|
||||
className={`chartFrame ${editMode ? 'chartFrameEditing' : ''} ${
|
||||
editTool === EDIT_TOOL_ERASE ? 'chartFrameErasing' : ''
|
||||
}`}
|
||||
ref={frameRef}
|
||||
>
|
||||
<div className="chartLegend">
|
||||
<span><i className="legendDot scatter" />散点数据</span>
|
||||
<span><i className="legendLine actual" />实际功率</span>
|
||||
@@ -670,7 +689,9 @@ function PowerCurveChart({
|
||||
<div className="uplotWrap" ref={chartRef} />
|
||||
{editMode && brush.visible && (
|
||||
<div
|
||||
className={`chartBrush ${brush.dragging ? 'dragging' : ''}`}
|
||||
className={`chartBrush ${editTool === EDIT_TOOL_ERASE ? 'erase' : ''} ${
|
||||
brush.dragging ? 'dragging' : ''
|
||||
}`}
|
||||
style={{
|
||||
width: BRUSH_RADIUS * 2,
|
||||
height: BRUSH_RADIUS * 2,
|
||||
@@ -693,7 +714,9 @@ export default function HomePage() {
|
||||
const [selectedFan, setSelectedFan] = useState('');
|
||||
const [showFiltered, setShowFiltered] = useState(false);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
const [editTool, setEditTool] = useState(EDIT_TOOL_RESTORE);
|
||||
const [restoredPointIds, setRestoredPointIds] = useState({});
|
||||
const [erasedPointIds, setErasedPointIds] = useState({});
|
||||
const [readingDesign, setReadingDesign] = useState(false);
|
||||
const [designRows, setDesignRows] = useState(() => createDesignRows());
|
||||
const [designMeta, setDesignMeta] = useState(null);
|
||||
@@ -710,9 +733,16 @@ export default function HomePage() {
|
||||
const isSchemeTwo = selectedSchemeId === SCHEME_TWO_ID;
|
||||
const selectedPoints = selectedFan && result?.curves ? result.curves[selectedFan] || [] : [];
|
||||
const selectedBins = selectedFan && result?.bins ? result.bins[selectedFan] || [] : [];
|
||||
const selectedScatter = selectedFan && result?.scatter_points
|
||||
const selectedScatterRaw = selectedFan && result?.scatter_points
|
||||
? result.scatter_points[selectedFan] || []
|
||||
: [];
|
||||
const selectedScatter = useMemo(
|
||||
() => withPointIds(selectedScatterRaw, selectedFan).map((point) => ({
|
||||
...point,
|
||||
edit_origin: 'scatter',
|
||||
})),
|
||||
[selectedScatterRaw, selectedFan],
|
||||
);
|
||||
const selectedFilteredRaw = selectedFan && result?.filtered_points
|
||||
? result.filtered_points[selectedFan] || []
|
||||
: [];
|
||||
@@ -724,17 +754,40 @@ export default function HomePage() {
|
||||
() => new Set(restoredPointIds[selectedFan] || []),
|
||||
[restoredPointIds, selectedFan],
|
||||
);
|
||||
const selectedErasedIdSet = useMemo(
|
||||
() => new Set(erasedPointIds[selectedFan] || []),
|
||||
[erasedPointIds, selectedFan],
|
||||
);
|
||||
const restoredFilteredPoints = useMemo(
|
||||
() => selectedFiltered.filter((point) => selectedRestoredIdSet.has(point.point_id)),
|
||||
() => selectedFiltered
|
||||
.filter((point) => selectedRestoredIdSet.has(point.point_id))
|
||||
.map((point) => ({ ...point, edit_origin: 'restored_filtered' })),
|
||||
[selectedFiltered, selectedRestoredIdSet],
|
||||
);
|
||||
const effectiveBaseScatter = useMemo(
|
||||
() => selectedScatter.filter((point) => !selectedErasedIdSet.has(point.point_id)),
|
||||
[selectedScatter, selectedErasedIdSet],
|
||||
);
|
||||
const erasedScatterPoints = useMemo(
|
||||
() => selectedScatter
|
||||
.filter((point) => selectedErasedIdSet.has(point.point_id))
|
||||
.map((point) => ({
|
||||
...point,
|
||||
reason: 'manual_erased',
|
||||
edit_origin: 'erased_scatter',
|
||||
})),
|
||||
[selectedScatter, selectedErasedIdSet],
|
||||
);
|
||||
const visibleFilteredPoints = useMemo(
|
||||
() => selectedFiltered.filter((point) => !selectedRestoredIdSet.has(point.point_id)),
|
||||
[selectedFiltered, selectedRestoredIdSet],
|
||||
() => [
|
||||
...selectedFiltered.filter((point) => !selectedRestoredIdSet.has(point.point_id)),
|
||||
...erasedScatterPoints,
|
||||
],
|
||||
[selectedFiltered, selectedRestoredIdSet, erasedScatterPoints],
|
||||
);
|
||||
const effectiveScatter = useMemo(
|
||||
() => [...selectedScatter, ...restoredFilteredPoints],
|
||||
[selectedScatter, restoredFilteredPoints],
|
||||
() => [...effectiveBaseScatter, ...restoredFilteredPoints],
|
||||
[effectiveBaseScatter, restoredFilteredPoints],
|
||||
);
|
||||
const selectedEstimatedParams = selectedFan && result?.estimated_params
|
||||
? result.estimated_params[selectedFan]
|
||||
@@ -760,11 +813,59 @@ export default function HomePage() {
|
||||
});
|
||||
}, [selectedFan]);
|
||||
|
||||
const handleErasePoints = useCallback((pointsToErase) => {
|
||||
if (!selectedFan || !pointsToErase.length) return;
|
||||
const restoredIdsToRemove = [];
|
||||
const erasedIdsToAdd = [];
|
||||
for (const point of pointsToErase) {
|
||||
const pointId = point.point_id || createPointId(point, selectedFan);
|
||||
if (point.edit_origin === 'restored_filtered') {
|
||||
restoredIdsToRemove.push(pointId);
|
||||
} else {
|
||||
erasedIdsToAdd.push(pointId);
|
||||
}
|
||||
}
|
||||
if (restoredIdsToRemove.length) {
|
||||
setRestoredPointIds((prev) => {
|
||||
const currentIds = new Set(prev[selectedFan] || []);
|
||||
let changed = false;
|
||||
for (const pointId of restoredIdsToRemove) {
|
||||
if (currentIds.delete(pointId)) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!changed) return prev;
|
||||
return {
|
||||
...prev,
|
||||
[selectedFan]: Array.from(currentIds),
|
||||
};
|
||||
});
|
||||
}
|
||||
if (erasedIdsToAdd.length) {
|
||||
setErasedPointIds((prev) => {
|
||||
const nextIds = new Set(prev[selectedFan] || []);
|
||||
let changed = false;
|
||||
for (const pointId of erasedIdsToAdd) {
|
||||
if (!nextIds.has(pointId)) {
|
||||
nextIds.add(pointId);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!changed) return prev;
|
||||
return {
|
||||
...prev,
|
||||
[selectedFan]: Array.from(nextIds),
|
||||
};
|
||||
});
|
||||
}
|
||||
}, [selectedFan]);
|
||||
|
||||
const handleToggleEditMode = useCallback(() => {
|
||||
setIsEditMode((prev) => {
|
||||
const next = !prev;
|
||||
if (next) {
|
||||
setShowFiltered(true);
|
||||
setEditTool(EDIT_TOOL_RESTORE);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
@@ -896,7 +997,9 @@ export default function HomePage() {
|
||||
setSelectedFan('');
|
||||
setShowFiltered(false);
|
||||
setIsEditMode(false);
|
||||
setEditTool(EDIT_TOOL_RESTORE);
|
||||
setRestoredPointIds({});
|
||||
setErasedPointIds({});
|
||||
try {
|
||||
const parsedFiles = [];
|
||||
for (const file of selectedFiles) {
|
||||
@@ -984,7 +1087,9 @@ export default function HomePage() {
|
||||
setResult(null);
|
||||
setShowFiltered(false);
|
||||
setIsEditMode(false);
|
||||
setEditTool(EDIT_TOOL_RESTORE);
|
||||
setRestoredPointIds({});
|
||||
setErasedPointIds({});
|
||||
setProgress('正在准备标准化数据');
|
||||
|
||||
try {
|
||||
@@ -1369,10 +1474,32 @@ export default function HomePage() {
|
||||
className={`secondaryButton ${isEditMode ? 'active' : ''}`}
|
||||
type="button"
|
||||
onClick={handleToggleEditMode}
|
||||
disabled={!selectedFiltered.length && !isEditMode}
|
||||
disabled={!selectedFiltered.length && !effectiveScatter.length && !isEditMode}
|
||||
>
|
||||
{isEditMode ? '退出编辑' : '进入编辑'}
|
||||
</button>
|
||||
{isEditMode && (
|
||||
<div className="editToolGroup" aria-label="编辑工具">
|
||||
<button
|
||||
className={`toolButton ${
|
||||
editTool === EDIT_TOOL_RESTORE ? 'active' : ''
|
||||
}`}
|
||||
type="button"
|
||||
onClick={() => setEditTool(EDIT_TOOL_RESTORE)}
|
||||
>
|
||||
笔刷
|
||||
</button>
|
||||
<button
|
||||
className={`toolButton erase ${
|
||||
editTool === EDIT_TOOL_ERASE ? 'active' : ''
|
||||
}`}
|
||||
type="button"
|
||||
onClick={() => setEditTool(EDIT_TOOL_ERASE)}
|
||||
>
|
||||
橡皮擦
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className="secondaryButton"
|
||||
type="button"
|
||||
@@ -1414,7 +1541,9 @@ export default function HomePage() {
|
||||
showFiltered={showFiltered || isEditMode}
|
||||
designPoints={designCurve}
|
||||
editMode={isEditMode}
|
||||
editTool={editTool}
|
||||
onRestorePoints={handleRestorePoints}
|
||||
onErasePoints={handleErasePoints}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user