更新项目 skills 并完善设计功率曲线前端
- skills: 移除 edge_collector 专属技能(OTA/协议/边缘主机/collector 等), 保留通用工程规范(analyze-questions/cpp-coding-style/git-commit/shell-scripting/ frontend-debug/frontend-dialog/frontend-ui-conventions),新增 wind_power_cal 项目技能(wind-project-overview/build-run/deploy/backend-conventions/ frontend-conventions/third-party-libs)与元技能 skill-builder; .agents/skills 与 .claude/skills 保持同步 - 前端: HomePage/App.css 增加设计功率曲线录入与图表展示 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,14 @@
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.headerActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.homeHeader h1 {
|
||||
color: #fff;
|
||||
font-size: 28px;
|
||||
@@ -65,7 +73,12 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fileButton input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.uploadButton.disabled,
|
||||
.fileButton.disabled,
|
||||
.primaryButton:disabled,
|
||||
.secondaryButton:disabled {
|
||||
cursor: not-allowed;
|
||||
@@ -105,6 +118,12 @@
|
||||
font-weight: 620;
|
||||
}
|
||||
|
||||
.panelHint {
|
||||
margin-top: 6px;
|
||||
color: #8f96a3;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.panelHeader span,
|
||||
.muted {
|
||||
color: #8f96a3;
|
||||
@@ -131,6 +150,95 @@
|
||||
color: #c7d2fe;
|
||||
}
|
||||
|
||||
.designPowerPanel {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.designActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.designActions span {
|
||||
color: #9ca3af;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.designMeta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin: -2px 0 12px;
|
||||
}
|
||||
|
||||
.designMeta span {
|
||||
border: 1px solid #343449;
|
||||
border-radius: 6px;
|
||||
background: #1a1a27;
|
||||
color: #cbd5e1;
|
||||
padding: 5px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.designTableWrap {
|
||||
max-height: 320px;
|
||||
overflow: auto;
|
||||
border: 1px solid #2a2a3a;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.designTable th:nth-child(1),
|
||||
.designTable th:nth-child(2),
|
||||
.designTable td:nth-child(1),
|
||||
.designTable td:nth-child(2) {
|
||||
width: 42%;
|
||||
}
|
||||
|
||||
.designTable th:nth-child(3),
|
||||
.designTable td:nth-child(3) {
|
||||
width: 16%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.tableInput {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 34px;
|
||||
border: 1px solid #343449;
|
||||
border-radius: 7px;
|
||||
background: #0f0f18;
|
||||
color: #e5e7eb;
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.tableInput:focus {
|
||||
border-color: #6366f1;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tableInput::placeholder {
|
||||
color: #5f6673;
|
||||
}
|
||||
|
||||
.textButton {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #93c5fd;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.textButton:hover {
|
||||
color: #bfdbfe;
|
||||
}
|
||||
|
||||
.emptyState,
|
||||
.emptyChart {
|
||||
display: flex;
|
||||
@@ -290,9 +398,14 @@ select:focus {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.chartFrame {
|
||||
position: relative;
|
||||
min-height: 540px;
|
||||
}
|
||||
|
||||
.uplotWrap {
|
||||
width: 100%;
|
||||
min-height: 360px;
|
||||
min-height: 540px;
|
||||
}
|
||||
|
||||
.uplot {
|
||||
@@ -321,6 +434,56 @@ select:focus {
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.chartLegend {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: clamp(88px, 6vw, 118px);
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.chartLegend span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #e5e7eb;
|
||||
font-size: 13px;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.legendDot,
|
||||
.legendLine {
|
||||
display: inline-block;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.legendDot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.legendDot.scatter {
|
||||
background: #fbbf24;
|
||||
}
|
||||
|
||||
.legendLine {
|
||||
width: 32px;
|
||||
height: 0;
|
||||
border-top: 3px solid;
|
||||
}
|
||||
|
||||
.legendLine.actual {
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.legendLine.design {
|
||||
border-color: #22c55e;
|
||||
}
|
||||
|
||||
.chartTools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -416,6 +579,10 @@ td {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.headerActions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.summaryGrid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@@ -29,7 +29,27 @@ const FIELD_EXCLUDES = {
|
||||
active_power: ['限功率', '限电', '时间', '累计'],
|
||||
};
|
||||
|
||||
const DESIGN_FIELDS = [
|
||||
{ key: 'wind_speed', label: '风速' },
|
||||
{ key: 'design_power', label: '设计功率' },
|
||||
];
|
||||
|
||||
const DESIGN_FIELD_HINTS = {
|
||||
wind_speed: ['风速', 'wind speed', 'windspeed', 'm/s'],
|
||||
design_power: ['设计功率', '理论功率', '标准功率', '功率', 'power', 'kw'],
|
||||
};
|
||||
|
||||
const CHUNK_SIZE = 4000;
|
||||
const CHART_HEIGHT = 540;
|
||||
|
||||
function createDesignRows(points) {
|
||||
const source = points?.length ? points : [{ wind_speed: '', design_power: '' }];
|
||||
return source.map((point, index) => ({
|
||||
id: `${Date.now()}_${index}_${Math.random().toString(16).slice(2)}`,
|
||||
wind_speed: point.wind_speed ?? '',
|
||||
design_power: point.design_power ?? '',
|
||||
}));
|
||||
}
|
||||
|
||||
function normalizeHeader(value) {
|
||||
return String(value ?? '')
|
||||
@@ -75,6 +95,41 @@ function inferMapping(headers) {
|
||||
return mapping;
|
||||
}
|
||||
|
||||
function inferDesignMapping(headers) {
|
||||
const normalized = headers.map((header) => ({
|
||||
header,
|
||||
normalized: normalizeHeader(header),
|
||||
}));
|
||||
const mapping = {};
|
||||
|
||||
for (const field of DESIGN_FIELDS) {
|
||||
const hints = DESIGN_FIELD_HINTS[field.key].map(normalizeHeader);
|
||||
let best = null;
|
||||
for (const item of normalized) {
|
||||
if (!item.normalized) {
|
||||
continue;
|
||||
}
|
||||
let score = 0;
|
||||
hints.forEach((hint, index) => {
|
||||
const weight = hints.length - index;
|
||||
if (item.normalized === hint) {
|
||||
score = Math.max(score, 100 + weight);
|
||||
} else if (item.normalized.includes(hint)) {
|
||||
score = Math.max(score, 60 + weight);
|
||||
} else if (hint.includes(item.normalized)) {
|
||||
score = Math.max(score, 20 + weight);
|
||||
}
|
||||
});
|
||||
if (!best || score > best.score) {
|
||||
best = { ...item, score };
|
||||
}
|
||||
}
|
||||
mapping[field.key] = best && best.score > 0 ? best.header : '';
|
||||
}
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
function excelSerialToDate(value) {
|
||||
const days = Number(value);
|
||||
if (!Number.isFinite(days) || days <= 0) {
|
||||
@@ -168,6 +223,49 @@ async function readExcelFile(file) {
|
||||
};
|
||||
}
|
||||
|
||||
async function readDesignPowerFile(file) {
|
||||
const parsedFile = await readExcelFile(file);
|
||||
const mapping = inferDesignMapping(parsedFile.headers);
|
||||
const windSpeedIndex = parsedFile.headers.indexOf(mapping.wind_speed);
|
||||
const designPowerIndex = parsedFile.headers.indexOf(mapping.design_power);
|
||||
|
||||
if (windSpeedIndex < 0 || designPowerIndex < 0) {
|
||||
throw new Error('设计功率文件未识别到风速列或设计功率列');
|
||||
}
|
||||
|
||||
const grouped = new Map();
|
||||
for (const row of parsedFile.rows) {
|
||||
const windSpeed = normalizeNumber(getCell(row, windSpeedIndex));
|
||||
const designPower = normalizeNumber(getCell(row, designPowerIndex));
|
||||
if (!Number.isFinite(windSpeed) || !Number.isFinite(designPower) ||
|
||||
windSpeed < 0 || designPower < 0) {
|
||||
continue;
|
||||
}
|
||||
const key = windSpeed.toFixed(6);
|
||||
const current = grouped.get(key) || { wind_speed: windSpeed, sum: 0, count: 0 };
|
||||
current.sum += designPower;
|
||||
current.count += 1;
|
||||
grouped.set(key, current);
|
||||
}
|
||||
|
||||
const points = Array.from(grouped.values())
|
||||
.map((item) => ({
|
||||
wind_speed: item.wind_speed,
|
||||
design_power: item.sum / item.count,
|
||||
}))
|
||||
.sort((left, right) => left.wind_speed - right.wind_speed);
|
||||
|
||||
if (!points.length) {
|
||||
throw new Error('设计功率文件没有可用的风速-功率数据');
|
||||
}
|
||||
|
||||
return {
|
||||
file_name: parsedFile.file_name,
|
||||
sheet_name: parsedFile.sheet_name,
|
||||
points,
|
||||
};
|
||||
}
|
||||
|
||||
function getCell(row, headerIndex) {
|
||||
if (headerIndex < 0) return '';
|
||||
return row[headerIndex];
|
||||
@@ -218,7 +316,13 @@ function SummaryCards({ summary }) {
|
||||
);
|
||||
}
|
||||
|
||||
function PowerCurveChart({ points, scatterPoints, filteredPoints, showFiltered }) {
|
||||
function PowerCurveChart({
|
||||
points,
|
||||
scatterPoints,
|
||||
filteredPoints,
|
||||
showFiltered,
|
||||
designPoints,
|
||||
}) {
|
||||
const chartRef = useRef(null);
|
||||
const validPoints = useMemo(
|
||||
() => (points || []).filter((point) => point.sample_count > 0),
|
||||
@@ -234,13 +338,29 @@ function PowerCurveChart({ points, scatterPoints, filteredPoints, showFiltered }
|
||||
Number.isFinite(point.wind_speed) && Number.isFinite(point.active_power)),
|
||||
[filteredPoints],
|
||||
);
|
||||
const validDesign = useMemo(
|
||||
() => (designPoints || []).filter((point) =>
|
||||
Number.isFinite(point.wind_speed) && Number.isFinite(point.design_power)),
|
||||
[designPoints],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartRef.current || !validPoints.length) return undefined;
|
||||
|
||||
const sortedCurve = [...validPoints].sort((left, right) => left.wind_speed - right.wind_speed);
|
||||
const curveX = sortedCurve.map((point) => point.wind_speed);
|
||||
const curveY = sortedCurve.map((point) => point.average_power);
|
||||
const sortedDesign = [...validDesign].sort((left, right) => left.wind_speed - right.wind_speed);
|
||||
const actualByWindSpeed = new Map(
|
||||
sortedCurve.map((point) => [point.wind_speed.toFixed(6), point.average_power]),
|
||||
);
|
||||
const designByWindSpeed = new Map(
|
||||
sortedDesign.map((point) => [point.wind_speed.toFixed(6), point.design_power]),
|
||||
);
|
||||
const curveX = Array.from(new Set([
|
||||
...sortedCurve.map((point) => point.wind_speed),
|
||||
...sortedDesign.map((point) => point.wind_speed),
|
||||
])).sort((left, right) => left - right);
|
||||
const actualY = curveX.map((windSpeed) => actualByWindSpeed.get(windSpeed.toFixed(6)) ?? null);
|
||||
const designY = curveX.map((windSpeed) => designByWindSpeed.get(windSpeed.toFixed(6)) ?? null);
|
||||
const maxScatterPower = validScatter.reduce(
|
||||
(max, point) => Math.max(max, point.active_power),
|
||||
0,
|
||||
@@ -248,14 +368,21 @@ function PowerCurveChart({ points, scatterPoints, filteredPoints, showFiltered }
|
||||
const maxFilteredPower = showFiltered
|
||||
? validFiltered.reduce((max, point) => Math.max(max, point.active_power), 0)
|
||||
: 0;
|
||||
const maxCurvePower = curveY.reduce((max, value) => Math.max(max, value), 0);
|
||||
const maxPower = Math.max(maxCurvePower, maxScatterPower, maxFilteredPower, 1);
|
||||
const maxCurvePower = sortedCurve.reduce(
|
||||
(max, point) => Math.max(max, point.average_power),
|
||||
0,
|
||||
);
|
||||
const maxDesignPower = sortedDesign.reduce(
|
||||
(max, point) => Math.max(max, point.design_power),
|
||||
0,
|
||||
);
|
||||
const maxPower = Math.max(maxCurvePower, maxDesignPower, maxScatterPower, maxFilteredPower, 1);
|
||||
const width = Math.max(chartRef.current.clientWidth || 760, 320);
|
||||
|
||||
chartRef.current.innerHTML = '';
|
||||
const chart = new uPlot({
|
||||
width,
|
||||
height: 360,
|
||||
height: CHART_HEIGHT,
|
||||
cursor: {
|
||||
drag: { x: true, y: true },
|
||||
},
|
||||
@@ -278,14 +405,22 @@ function PowerCurveChart({ points, scatterPoints, filteredPoints, showFiltered }
|
||||
series: [
|
||||
{},
|
||||
{
|
||||
label: '分箱平均功率',
|
||||
stroke: '#60a5fa',
|
||||
width: 2,
|
||||
points: { show: true, size: 6, fill: '#93c5fd', stroke: '#0f172a' },
|
||||
label: '实际功率',
|
||||
stroke: '#ef4444',
|
||||
width: 2.5,
|
||||
paths: uPlot.paths.spline(),
|
||||
points: { show: false },
|
||||
},
|
||||
{
|
||||
label: '设计功率',
|
||||
stroke: '#22c55e',
|
||||
width: 2.2,
|
||||
paths: uPlot.paths.spline(),
|
||||
points: { show: false },
|
||||
},
|
||||
],
|
||||
hooks: {
|
||||
draw: [
|
||||
drawAxes: [
|
||||
(u) => {
|
||||
const { ctx } = u;
|
||||
ctx.save();
|
||||
@@ -315,16 +450,25 @@ function PowerCurveChart({ points, scatterPoints, filteredPoints, showFiltered }
|
||||
},
|
||||
],
|
||||
},
|
||||
}, [curveX, curveY], chartRef.current);
|
||||
}, [curveX, actualY, designY], chartRef.current);
|
||||
|
||||
return () => chart.destroy();
|
||||
}, [validPoints, validScatter, validFiltered, showFiltered]);
|
||||
}, [validPoints, validScatter, validFiltered, validDesign, showFiltered]);
|
||||
|
||||
if (!validPoints.length) {
|
||||
return <div className="emptyChart">暂无可绘制的曲线数据</div>;
|
||||
}
|
||||
|
||||
return <div className="uplotWrap" ref={chartRef} />;
|
||||
return (
|
||||
<div className="chartFrame">
|
||||
<div className="chartLegend">
|
||||
<span><i className="legendDot scatter" />散点数据</span>
|
||||
<span><i className="legendLine actual" />实际功率</span>
|
||||
{validDesign.length > 0 && <span><i className="legendLine design" />设计功率</span>}
|
||||
</div>
|
||||
<div className="uplotWrap" ref={chartRef} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
@@ -337,6 +481,9 @@ export default function HomePage() {
|
||||
const [result, setResult] = useState(null);
|
||||
const [selectedFan, setSelectedFan] = useState('');
|
||||
const [showFiltered, setShowFiltered] = useState(false);
|
||||
const [readingDesign, setReadingDesign] = useState(false);
|
||||
const [designRows, setDesignRows] = useState(() => createDesignRows());
|
||||
const [designMeta, setDesignMeta] = useState(null);
|
||||
|
||||
const headers = files[0]?.headers || [];
|
||||
const missingFields = REQUIRED_FIELDS.filter((field) => !mapping[field.key]);
|
||||
@@ -356,6 +503,28 @@ export default function HomePage() {
|
||||
() => files.reduce((sum, file) => sum + file.row_count, 0),
|
||||
[files],
|
||||
);
|
||||
const designCurve = useMemo(() => {
|
||||
const grouped = new Map();
|
||||
for (const row of designRows) {
|
||||
const windSpeed = normalizeNumber(row.wind_speed);
|
||||
const designPower = normalizeNumber(row.design_power);
|
||||
if (!Number.isFinite(windSpeed) || !Number.isFinite(designPower) ||
|
||||
windSpeed < 0 || designPower < 0) {
|
||||
continue;
|
||||
}
|
||||
const key = windSpeed.toFixed(6);
|
||||
const current = grouped.get(key) || { wind_speed: windSpeed, sum: 0, count: 0 };
|
||||
current.sum += designPower;
|
||||
current.count += 1;
|
||||
grouped.set(key, current);
|
||||
}
|
||||
return Array.from(grouped.values())
|
||||
.map((item) => ({
|
||||
wind_speed: item.wind_speed,
|
||||
design_power: item.sum / item.count,
|
||||
}))
|
||||
.sort((left, right) => left.wind_speed - right.wind_speed);
|
||||
}, [designRows]);
|
||||
|
||||
async function handleFilesChange(event) {
|
||||
const selectedFiles = Array.from(event.target.files || []);
|
||||
@@ -381,6 +550,56 @@ export default function HomePage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDesignFileChange(event) {
|
||||
const [file] = Array.from(event.target.files || []);
|
||||
if (!file) return;
|
||||
|
||||
setReadingDesign(true);
|
||||
setError('');
|
||||
try {
|
||||
const design = await readDesignPowerFile(file);
|
||||
setDesignRows(createDesignRows(design.points));
|
||||
setDesignMeta({
|
||||
file_name: design.file_name,
|
||||
sheet_name: design.sheet_name,
|
||||
point_count: design.points.length,
|
||||
});
|
||||
} catch (err) {
|
||||
setError(err.message || '读取设计功率失败');
|
||||
} finally {
|
||||
setReadingDesign(false);
|
||||
event.target.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function handleDesignRowChange(rowId, field, value) {
|
||||
setDesignRows((prev) => prev.map((row) => (
|
||||
row.id === rowId ? { ...row, [field]: value } : row
|
||||
)));
|
||||
setDesignMeta((prev) => prev ? { ...prev, edited: true } : { edited: true });
|
||||
}
|
||||
|
||||
function handleAddDesignRow() {
|
||||
setDesignRows((prev) => [
|
||||
...prev,
|
||||
...createDesignRows([{ wind_speed: '', design_power: '' }]),
|
||||
]);
|
||||
setDesignMeta((prev) => prev ? { ...prev, edited: true } : { edited: true });
|
||||
}
|
||||
|
||||
function handleDeleteDesignRow(rowId) {
|
||||
setDesignRows((prev) => {
|
||||
const next = prev.filter((row) => row.id !== rowId);
|
||||
return next.length ? next : createDesignRows();
|
||||
});
|
||||
setDesignMeta((prev) => prev ? { ...prev, edited: true } : { edited: true });
|
||||
}
|
||||
|
||||
function handleClearDesignRows() {
|
||||
setDesignRows(createDesignRows());
|
||||
setDesignMeta(null);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!files.length || missingFields.length) {
|
||||
setError('请先上传文件并完成所有字段映射');
|
||||
@@ -455,21 +674,106 @@ export default function HomePage() {
|
||||
<h1>风电功率计算平台</h1>
|
||||
<p className="subtitle">多 Excel 导入、字段映射、数据清洗与功率曲线计算</p>
|
||||
</div>
|
||||
<label className={`uploadButton ${reading ? 'disabled' : ''}`}>
|
||||
<input
|
||||
type="file"
|
||||
accept=".xls,.xlsx"
|
||||
multiple
|
||||
disabled={reading || submitting}
|
||||
onChange={handleFilesChange}
|
||||
/>
|
||||
{reading ? '解析中' : '上传 Excel'}
|
||||
</label>
|
||||
<div className="headerActions">
|
||||
<label className={`uploadButton ${reading ? 'disabled' : ''}`}>
|
||||
<input
|
||||
type="file"
|
||||
accept=".xls,.xlsx"
|
||||
multiple
|
||||
disabled={reading || submitting}
|
||||
onChange={handleFilesChange}
|
||||
/>
|
||||
{reading ? '解析中' : '上传 Excel'}
|
||||
</label>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{error && <div className="alert error">{error}</div>}
|
||||
{progress && <div className="alert info">{progress}</div>}
|
||||
|
||||
<section className="panel designPowerPanel">
|
||||
<div className="panelHeader">
|
||||
<div>
|
||||
<h2>设计功率曲线</h2>
|
||||
<p className="panelHint">可手动填写风速和设计功率,也可上传 Excel 自动加载后再修改。</p>
|
||||
</div>
|
||||
<div className="designActions">
|
||||
<span>{designCurve.length ? `${designCurve.length.toLocaleString()} 个有效点` : '未启用'}</span>
|
||||
<label className={`secondaryButton fileButton ${readingDesign ? 'disabled' : ''}`}>
|
||||
<input
|
||||
type="file"
|
||||
accept=".xls,.xlsx"
|
||||
disabled={readingDesign}
|
||||
onChange={handleDesignFileChange}
|
||||
/>
|
||||
{readingDesign ? '解析中' : '导入 Excel'}
|
||||
</label>
|
||||
<button className="secondaryButton" type="button" onClick={handleAddDesignRow}>
|
||||
新增一行
|
||||
</button>
|
||||
<button className="secondaryButton" type="button" onClick={handleClearDesignRows}>
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{designMeta && (
|
||||
<div className="designMeta">
|
||||
<span>{designMeta.file_name || '手动填写'}</span>
|
||||
{designMeta.sheet_name && <span>{designMeta.sheet_name}</span>}
|
||||
{designMeta.point_count > 0 && <span>导入 {designMeta.point_count.toLocaleString()} 行</span>}
|
||||
{designMeta.edited && <span>已手动修改</span>}
|
||||
</div>
|
||||
)}
|
||||
<div className="designTableWrap">
|
||||
<table className="designTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>风速 (m/s)</th>
|
||||
<th>设计功率 (kW)</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{designRows.map((row) => (
|
||||
<tr key={row.id}>
|
||||
<td>
|
||||
<input
|
||||
className="tableInput"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={row.wind_speed}
|
||||
placeholder="例如 3.0"
|
||||
onChange={(event) => handleDesignRowChange(row.id, 'wind_speed', event.target.value)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
className="tableInput"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
value={row.design_power}
|
||||
placeholder="例如 120.0"
|
||||
onChange={(event) => handleDesignRowChange(row.id, 'design_power', event.target.value)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
className="textButton"
|
||||
type="button"
|
||||
onClick={() => handleDeleteDesignRow(row.id)}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="workspace">
|
||||
<div className="panel">
|
||||
<div className="panelHeader">
|
||||
@@ -610,6 +914,7 @@ export default function HomePage() {
|
||||
scatterPoints={selectedScatter}
|
||||
filteredPoints={selectedFiltered}
|
||||
showFiltered={showFiltered}
|
||||
designPoints={designCurve}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user