From 1e194858457ed449205c01f1a087781006641d08 Mon Sep 17 00:00:00 2001 From: cloud Date: Fri, 17 Jul 2026 17:40:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E4=B8=AA=E6=A9=A1=E7=9A=AE=E6=93=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/config/server_config.json | 1 + frontend/web_app/src/App.css | 49 +++++++ frontend/web_app/src/pages/HomePage.jsx | 167 +++++++++++++++++++++--- 3 files changed, 198 insertions(+), 19 deletions(-) diff --git a/backend/config/server_config.json b/backend/config/server_config.json index 071b1b1..37b51f1 100644 --- a/backend/config/server_config.json +++ b/backend/config/server_config.json @@ -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": "", diff --git a/frontend/web_app/src/App.css b/frontend/web_app/src/App.css index dc47007..39dfc5f 100644 --- a/frontend/web_app/src/App.css +++ b/frontend/web_app/src/App.css @@ -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; diff --git a/frontend/web_app/src/pages/HomePage.jsx b/frontend/web_app/src/pages/HomePage.jsx index dbf426b..eefc94c 100644 --- a/frontend/web_app/src/pages/HomePage.jsx +++ b/frontend/web_app/src/pages/HomePage.jsx @@ -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 ( -
+
散点数据 实际功率 @@ -670,7 +689,9 @@ function PowerCurveChart({
{editMode && brush.visible && (
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 ? '退出编辑' : '进入编辑'} + {isEditMode && ( +
+ + +
+ )}