@@ -5,6 +5,8 @@ import * as XLSX from 'xlsx';
import {
deleteWindJob ,
finishWindJob ,
getWindSchemes ,
saveWindSchemeDescription ,
startWindJob ,
uploadWindChunk ,
} from '../utils/api' ;
@@ -15,6 +17,9 @@ const REQUIRED_FIELDS = [
{ key : 'wind_speed' , label : '风速' } ,
{ key : 'active_power' , label : '有功功率' } ,
{ key : 'generator_speed' , label : '发电机转速' } ,
{ key : 'blade_pitch_1' , label : '1#叶片角度' } ,
{ key : 'blade_pitch_2' , label : '2#叶片角度' } ,
{ key : 'blade_pitch_3' , label : '3#叶片角度' } ,
] ;
const FIELD _HINTS = {
@@ -23,6 +28,9 @@ const FIELD_HINTS = {
wind _speed : [ '风速' , '平均风速' , 'wind speed' , 'windspeed' ] ,
active _power : [ '平均有功功率' , '有功功率' , 'active power' , 'power' ] ,
generator _speed : [ '发电机转速' , '平均发电机转速' , 'generator speed' , 'rpm' ] ,
blade _pitch _1 : [ '1#叶片变桨角度' , '1#叶片角度' , '1叶片变桨角度' , '1叶片角度' , '1号叶片角度' , '叶片1角度' , '桨角1' , '变桨角1' , 'pitch 1' , 'pitch angle 1' , 'blade pitch 1' ] ,
blade _pitch _2 : [ '2#叶片变桨角度' , '2#叶片角度' , '2叶片变桨角度' , '2叶片角度' , '2号叶片角度' , '叶片2角度' , '桨角2' , '变桨角2' , 'pitch 2' , 'pitch angle 2' , 'blade pitch 2' ] ,
blade _pitch _3 : [ '3#叶片变桨角度' , '3#叶片角度' , '3叶片变桨角度' , '3叶片角度' , '3号叶片角度' , '叶片3角度' , '桨角3' , '变桨角3' , 'pitch 3' , 'pitch angle 3' , 'blade pitch 3' ] ,
} ;
const FIELD _EXCLUDES = {
@@ -41,6 +49,27 @@ const DESIGN_FIELD_HINTS = {
const CHUNK _SIZE = 4000 ;
const CHART _HEIGHT = 540 ;
const DEFAULT _SCHEME _ID = 'scheme_one' ;
const SCHEME _TWO _ID = 'scheme_two' ;
const DEFAULT _SCHEMES = [
{ id : 'scheme_one' , name : '方案一' , description : '通用方案' } ,
{
id : SCHEME _TWO _ID ,
name : '方案二' ,
description : '桨角筛选方案,使用三支叶片角度平均值、600s 平均风速和手填转速/功率参数' ,
parameters : {
grid _connected _speed : 1030 ,
rated _generator _speed : 1755 ,
rated _power : 2000 ,
} ,
} ,
] ;
const DEFAULT _SCHEME _TWO _PARAMS = {
grid _connected _speed : '1030' ,
rated _generator _speed : '1755' ,
rated _power : '2000' ,
} ;
function createDesignRows ( points ) {
const source = points ? . length ? points : [ { wind _speed : '' , design _power : '' } ] ;
@@ -289,6 +318,9 @@ function buildStandardRows(files, mapping) {
wind _speed : normalizeNumber ( getCell ( row , localIndexes . wind _speed ) ) ,
active _power : normalizeNumber ( getCell ( row , localIndexes . active _power ) ) ,
generator _speed : normalizeNumber ( getCell ( row , localIndexes . generator _speed ) ) ,
blade _pitch _1 : normalizeNumber ( getCell ( row , localIndexes . blade _pitch _1 ) ) ,
blade _pitch _2 : normalizeNumber ( getCell ( row , localIndexes . blade _pitch _2 ) ) ,
blade _pitch _3 : normalizeNumber ( getCell ( row , localIndexes . blade _pitch _3 ) ) ,
} ) ;
}
}
@@ -296,6 +328,69 @@ function buildStandardRows(files, mapping) {
return { rows , indexes } ;
}
function formatTimestampForFile ( date = new Date ( ) ) {
return ` ${ date . getFullYear ( ) } ${ pad ( date . getMonth ( ) + 1 ) } ${ pad ( date . getDate ( ) ) } `
+ ` ${ pad ( date . getHours ( ) ) } ${ pad ( date . getMinutes ( ) ) } ${ pad ( date . getSeconds ( ) ) } ` ;
}
function sanitizeFileName ( value ) {
return String ( value || '未选择风机' ) . replace ( /[\\/:*?"<>|]/g , '_' ) ;
}
function exportValidRowsToExcel ( fanId , rows ) {
const exportRows = ( rows || [ ] ) . map ( ( row ) => ( {
风机编号 : row . fan _id || fanId || '' ,
采样时间 : row . time || '' ,
平均功率 : row . active _power ? ? '' ,
平均转速 : row . generator _speed ? ? '' ,
平均风速 : row . wind _speed ? ? '' ,
'3个叶片变桨角平均值' : row . pitch _angle _average ? ? '' ,
} ) ) ;
const worksheet = XLSX . utils . json _to _sheet ( exportRows ) ;
const workbook = XLSX . utils . book _new ( ) ;
XLSX . utils . book _append _sheet ( workbook , worksheet , '有效数据' ) ;
XLSX . writeFile (
workbook ,
` 有效数据_ ${ sanitizeFileName ( fanId ) } _ ${ formatTimestampForFile ( ) } .xlsx ` ,
) ;
}
function interpolateDesignPower ( points , windSpeed ) {
if ( ! points . length ) return null ;
if ( windSpeed < points [ 0 ] . wind _speed || windSpeed > points [ points . length - 1 ] . wind _speed ) {
return null ;
}
for ( let index = 0 ; index < points . length ; index += 1 ) {
const point = points [ index ] ;
if ( Math . abs ( point . wind _speed - windSpeed ) < 0.000001 ) {
return point . design _power ;
}
if ( point . wind _speed > windSpeed && index > 0 ) {
const left = points [ index - 1 ] ;
const right = point ;
const span = right . wind _speed - left . wind _speed ;
if ( span <= 0 ) return left . design _power ;
const ratio = ( windSpeed - left . wind _speed ) / span ;
return left . design _power + ( right . design _power - left . design _power ) * ratio ;
}
}
return points [ points . length - 1 ] . design _power ;
}
function schemeParamsToState ( parameters ) {
return {
grid _connected _speed : String (
parameters ? . grid _connected _speed ? ? DEFAULT _SCHEME _TWO _PARAMS . grid _connected _speed ,
) ,
rated _generator _speed : String (
parameters ? . rated _generator _speed ? ? DEFAULT _SCHEME _TWO _PARAMS . rated _generator _speed ,
) ,
rated _power : String ( parameters ? . rated _power ? ? DEFAULT _SCHEME _TWO _PARAMS . rated _power ) ,
} ;
}
function SummaryCards ( { summary } ) {
if ( ! summary ) return null ;
const cards = [
@@ -352,15 +447,12 @@ function PowerCurveChart({
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 designY = curveX . map ( ( windSpeed ) => interpolateDesignPower ( sortedDesign , windSpeed ) ) ;
const maxScatterPower = validScatter . reduce (
( max , point ) => Math . max ( max , point . active _power ) ,
0 ,
@@ -409,6 +501,7 @@ function PowerCurveChart({
stroke : '#ef4444' ,
width : 2.5 ,
paths : uPlot . paths . spline ( ) ,
spanGaps : true ,
points : { show : false } ,
} ,
{
@@ -416,6 +509,7 @@ function PowerCurveChart({
stroke : '#22c55e' ,
width : 2.2 ,
paths : uPlot . paths . spline ( ) ,
spanGaps : true ,
points : { show : false } ,
} ,
] ,
@@ -430,7 +524,7 @@ function PowerCurveChart({
const y = u . valToPos ( point . active _power , 'y' , true ) ;
if ( Number . isFinite ( x ) && Number . isFinite ( y ) ) {
ctx . beginPath ( ) ;
ctx . arc ( x , y , 1.6 , 0 , Math . PI * 2 ) ;
ctx . arc ( x , y , 2 , 0 , Math . PI * 2 ) ;
ctx . fill ( ) ;
}
}
@@ -441,7 +535,7 @@ function PowerCurveChart({
const y = u . valToPos ( point . active _power , 'y' , true ) ;
if ( Number . isFinite ( x ) && Number . isFinite ( y ) ) {
ctx . beginPath ( ) ;
ctx . arc ( x , y , 1.9 , 0 , Math . PI * 2 ) ;
ctx . arc ( x , y , 2 , 0 , Math . PI * 2 ) ;
ctx . fill ( ) ;
}
}
@@ -484,9 +578,17 @@ export default function HomePage() {
const [ readingDesign , setReadingDesign ] = useState ( false ) ;
const [ designRows , setDesignRows ] = useState ( ( ) => createDesignRows ( ) ) ;
const [ designMeta , setDesignMeta ] = useState ( null ) ;
const [ schemes , setSchemes ] = useState ( DEFAULT _SCHEMES ) ;
const [ selectedSchemeId , setSelectedSchemeId ] = useState ( DEFAULT _SCHEME _ID ) ;
const [ schemeDescription , setSchemeDescription ] = useState ( DEFAULT _SCHEMES [ 0 ] . description ) ;
const [ savingScheme , setSavingScheme ] = useState ( false ) ;
const [ schemeMessage , setSchemeMessage ] = useState ( '' ) ;
const [ schemeTwoParams , setSchemeTwoParams ] = useState ( DEFAULT _SCHEME _TWO _PARAMS ) ;
const headers = files [ 0 ] ? . headers || [ ] ;
const missingFields = REQUIRED _FIELDS . filter ( ( field ) => ! mapping [ field . key ] ) ;
const selectedScheme = schemes . find ( ( scheme ) => scheme . id === selectedSchemeId ) || schemes [ 0 ] ;
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
@@ -526,6 +628,95 @@ export default function HomePage() {
. sort ( ( left , right ) => left . wind _speed - right . wind _speed ) ;
} , [ designRows ] ) ;
useEffect ( ( ) => {
let canceled = false ;
async function loadSchemes ( ) {
try {
const data = await getWindSchemes ( ) ;
if ( canceled ) return ;
const loadedSchemes = data . schemes ? . length ? data . schemes : DEFAULT _SCHEMES ;
const defaultSchemeId = data . default _scheme _id || DEFAULT _SCHEME _ID ;
const defaultScheme = loadedSchemes . find ( ( scheme ) => scheme . id === defaultSchemeId )
|| loadedSchemes [ 0 ] ;
setSchemes ( loadedSchemes ) ;
setSelectedSchemeId ( defaultScheme . id ) ;
setSchemeDescription ( defaultScheme . description || '' ) ;
const schemeTwo = loadedSchemes . find ( ( scheme ) => scheme . id === SCHEME _TWO _ID ) ;
setSchemeTwoParams ( schemeParamsToState ( schemeTwo ? . parameters ) ) ;
} catch ( err ) {
if ( ! canceled ) {
setSchemeMessage ( err . message || '读取方案配置失败,已使用默认方案' ) ;
}
}
}
loadSchemes ( ) ;
return ( ) => {
canceled = true ;
} ;
} , [ ] ) ;
function handleSchemeChange ( value ) {
const nextScheme = schemes . find ( ( scheme ) => scheme . id === value ) ;
setSelectedSchemeId ( value ) ;
setSchemeDescription ( nextScheme ? . description || '' ) ;
if ( value === SCHEME _TWO _ID ) {
setSchemeTwoParams ( schemeParamsToState ( nextScheme ? . parameters ) ) ;
}
setSchemeMessage ( '' ) ;
}
function handleSchemeTwoParamChange ( field , value ) {
setSchemeTwoParams ( ( prev ) => ( {
... prev ,
[ field ] : value ,
} ) ) ;
}
async function handleSaveSchemeDescription ( ) {
if ( ! selectedScheme ) return ;
setSavingScheme ( true ) ;
setError ( '' ) ;
setSchemeMessage ( '' ) ;
try {
const gridConnectedSpeed = normalizeNumber ( schemeTwoParams . grid _connected _speed ) ;
const ratedGeneratorSpeed = normalizeNumber ( schemeTwoParams . rated _generator _speed ) ;
const ratedPower = normalizeNumber ( schemeTwoParams . rated _power ) ;
if ( selectedScheme . id === SCHEME _TWO _ID &&
( ! Number . isFinite ( gridConnectedSpeed ) || gridConnectedSpeed <= 0 ||
! Number . isFinite ( ratedGeneratorSpeed ) || ratedGeneratorSpeed <= 0 ||
! Number . isFinite ( ratedPower ) || ratedPower <= 0 ) ) {
throw new Error ( '方案二参数必须为正数' ) ;
}
const data = await saveWindSchemeDescription ( selectedScheme . id , {
description : schemeDescription ,
... ( selectedScheme . id === SCHEME _TWO _ID ? {
parameters : {
grid _connected _speed : gridConnectedSpeed ,
rated _generator _speed : ratedGeneratorSpeed ,
rated _power : ratedPower ,
} ,
} : { } ) ,
} ) ;
const savedScheme = data . scheme ;
setSchemes ( ( prev ) => prev . map ( ( scheme ) => (
scheme . id === savedScheme . id ? { ... scheme , ... savedScheme } : scheme
) ) ) ;
setSchemeDescription ( savedScheme . description || '' ) ;
if ( savedScheme . id === SCHEME _TWO _ID ) {
setSchemeTwoParams ( schemeParamsToState ( savedScheme . parameters ) ) ;
}
setSchemeMessage ( '已保存' ) ;
} catch ( err ) {
setError ( err . message || '保存失败' ) ;
} finally {
setSavingScheme ( false ) ;
}
}
async function handleFilesChange ( event ) {
const selectedFiles = Array . from ( event . target . files || [ ] ) ;
if ( ! selectedFiles . length ) return ;
@@ -605,6 +796,16 @@ export default function HomePage() {
setError ( '请先上传文件并完成所有字段映射' ) ;
return ;
}
const gridConnectedSpeed = normalizeNumber ( schemeTwoParams . grid _connected _speed ) ;
const ratedGeneratorSpeed = normalizeNumber ( schemeTwoParams . rated _generator _speed ) ;
const ratedPower = normalizeNumber ( schemeTwoParams . rated _power ) ;
if ( isSchemeTwo &&
( ! Number . isFinite ( gridConnectedSpeed ) || gridConnectedSpeed <= 0 ||
! Number . isFinite ( ratedGeneratorSpeed ) || ratedGeneratorSpeed <= 0 ||
! Number . isFinite ( ratedPower ) || ratedPower <= 0 ) ) {
setError ( '方案二需要填写并网转速、额定转速和额定功率,且必须为正数' ) ;
return ;
}
let jobId = '' ;
setSubmitting ( true ) ;
@@ -647,6 +848,12 @@ export default function HomePage() {
iqr _upper _multiplier : 2 ,
minimum _generator _speed : 1 ,
generator _speed _k : 0.9 ,
scheme _id : selectedSchemeId ,
... ( isSchemeTwo ? {
grid _connected _speed : gridConnectedSpeed ,
rated _generator _speed : ratedGeneratorSpeed ,
rated _power : ratedPower ,
} : { } ) ,
} ,
} ) ;
setResult ( calculation ) ;
@@ -691,6 +898,102 @@ export default function HomePage() {
{ error && < div className = "alert error" > { error } < / div > }
{ progress && < div className = "alert info" > { progress } < / div > }
< section className = "panel schemePanel" >
< div className = "panelHeader" >
< div >
< h2 > 计算方案 < / h2 >
< p className = "panelHint" > 选择本次计算使用的清洗方案 , 方案描述可保存为服务端共享配置 。 < / p >
< / div >
< span > { selectedScheme ? . name || '方案一' } < / span >
< / div >
< div className = "schemeGrid" >
< label className = "fieldControl" >
< span > 方案选择 < / span >
< select
value = { selectedSchemeId }
onChange = { ( event ) => handleSchemeChange ( event . target . value ) }
disabled = { submitting }
>
{ schemes . map ( ( scheme ) => (
< option key = { scheme . id } value = { scheme . id } > { scheme . name } < / option >
) ) }
< / select >
< / label >
< label className = "schemeDescriptionControl" >
< span > 方案描述 < / span >
< input
type = "text"
value = { schemeDescription }
onChange = { ( event ) => {
setSchemeDescription ( event . target . value ) ;
setSchemeMessage ( '' ) ;
} }
disabled = { submitting || savingScheme }
/ >
< / label >
< / div >
{ isSchemeTwo && (
< div className = "schemeParamGrid" >
< label className = "fieldControl" >
< span > 并网转速 ( rpm ) < / span >
< input
type = "number"
min = "0"
step = "0.01"
value = { schemeTwoParams . grid _connected _speed }
onChange = { ( event ) => handleSchemeTwoParamChange (
'grid_connected_speed' ,
event . target . value ,
) }
disabled = { submitting }
placeholder = "请输入"
/ >
< / label >
< label className = "fieldControl" >
< span > 额定转速 ( rpm ) < / span >
< input
type = "number"
min = "0"
step = "0.01"
value = { schemeTwoParams . rated _generator _speed }
onChange = { ( event ) => handleSchemeTwoParamChange (
'rated_generator_speed' ,
event . target . value ,
) }
disabled = { submitting }
placeholder = "请输入"
/ >
< / label >
< label className = "fieldControl" >
< span > 额定功率 ( kW ) < / span >
< input
type = "number"
min = "0"
step = "0.1"
value = { schemeTwoParams . rated _power }
onChange = { ( event ) => handleSchemeTwoParamChange (
'rated_power' ,
event . target . value ,
) }
disabled = { submitting }
placeholder = "请输入"
/ >
< / label >
< / div >
) }
< div className = "schemeFooter" >
< span > { schemeMessage } < / span >
< button
className = "secondaryButton"
type = "button"
onClick = { handleSaveSchemeDescription }
disabled = { submitting || savingScheme }
>
{ savingScheme ? '保存中' : '保存' }
< / button >
< / div >
< / section >
< section className = "panel designPowerPanel" >
< div className = "panelHeader" >
< div >
@@ -860,6 +1163,12 @@ export default function HomePage() {
< span > 计算完成 < / span >
< / div >
< SummaryCards summary = { result . summary } / >
{ result . scheme && (
< div className = "paramStrip" >
< span > 方案 : { result . scheme . name } < / span >
< span > { result . scheme . description } < / span >
< / div >
) }
< div className = "reasonList" >
{ Object . entries ( result . summary ? . invalid _reasons || { } ) . map ( ( [ reason , count ] ) => (
< span key = { reason } > { reason } : { Number ( count ) . toLocaleString ( ) } < / span >
@@ -883,6 +1192,14 @@ export default function HomePage() {
>
{ showFiltered ? '隐藏滤除点' : '显示滤除点' }
< / button >
< button
className = "secondaryButton"
type = "button"
onClick = { ( ) => exportValidRowsToExcel ( selectedFan , selectedScatter ) }
disabled = { ! selectedScatter . length }
>
导出有效数据
< / button >
< select
className = "fanSelect"
value = { selectedFan }