<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>10kV线路 抽油机脉动负载 · 飞轮+无功补偿 · 计及无功线损</title>
<style>
body{margin:0;background:#0d1b2a;color:#e0e1dd;font-family:"Microsoft YaHei",sans-serif;}
.wrap{max-width:1100px;margin:0 auto;padding:16px;}
h2{text-align:center;color:#ffd166;margin:8px 0 4px;}
.sub{text-align:center;color:#8d99ae;font-size:13px;margin-bottom:12px;}
canvas{display:block;width:100%;background:#132238;border-radius:10px;}
.panel{display:flex;flex-wrap:wrap;gap:10px;margin-top:12px;}
.card{flex:1;min-width:130px;background:#1b2a41;border-radius:8px;padding:10px 12px;border-left:4px solid #457b9d;}
.card .k{font-size:12px;color:#8d99ae;}
.card .v{font-size:18px;font-weight:bold;color:#a8dadc;margin-top:2px;}
.card.good{border-left-color:#2a9d8f;}
.card.good .v{color:#7ae582;}
.card.warn{border-left-color:#e76f51;}
.card.warn .v{color:#f4a261;}
.card.fly{border-left-color:#9d4edd;}
.card.fly .v{color:#c77dff;}
.card.max{border-left-color:#ffd166;background:#233046;}
.card.max .v{color:#ffd166;font-size:20px;}
.ctrl{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-top:12px;background:#1b2a41;padding:10px 14px;border-radius:8px;}
.ctrl label{font-size:13px;color:#c9d6df;}
input[type=range]{width:150px;accent-color:#ffd166;}
.btn{background:#ffd166;color:#0d1b2a;border:none;padding:6px 16px;border-radius:6px;font-weight:bold;cursor:pointer;}
.btn.ghost{background:#2a3b57;color:#c9d6df;}
.legend{font-size:12px;color:#8d99ae;margin-top:8px;text-align:center;}
</style>
</head>
<body>
<div class="wrap">
<h2>10kV 输电线路 · 抽油机脉动负载 · 飞轮 + 无功补偿 · 计及无功线损</h2>
<div class="sub">线路 10km | LGJ-70(R≈4.7Ω)| 总电流 I=√(I_P²+I_Q²) | 线损=3I²R | 飞轮平有功脉动,无功补偿降 Q</div>
<canvas id="cv" width="1100" height="340"></canvas>
<div class="ctrl">
<label>冲程周期:<b id="perlab">8</b> s</label>
<input type="range" id="per" min="4" max="20" step="1" value="8">
<label>上冲程峰值:<b id="pkLab">700</b> kW</label>
<input type="range" id="pk" min="550" max="900" step="10" value="700">
<label>下冲程谷值:<b id="vlLab">250</b> kW</label>
<input type="range" id="vl" min="-100" max="450" step="10" value="250">
<label>负载功率因数:<b id="pflab">0.40</b></label>
<input type="range" id="pf" min="0.20" max="0.95" step="0.05" value="0.40">
<label>无功补偿后功率因数:<b id="pfclab">0.95</b></label>
<input type="range" id="pfc" min="0.40" max="1.00" step="0.05" value="0.95">
<button class="btn" id="toggle">暂停</button>
<button class="btn ghost" id="reset">重置统计</button>
</div>
<div class="panel">
<div class="card"><div class="k">负载有功 P</div><div class="v" id="pl">-- kW</div></div>
<div class="card"><div class="k">负载无功 Q</div><div class="v" id="ql">-- kvar</div></div>
<div class="card fly"><div class="k">飞轮功率</div><div class="v" id="pfv">-- kW</div></div>
<div class="card"><div class="k">线路侧 P</div><div class="v" id="pg">-- kW</div></div>
<div class="card"><div class="k">线路侧 Q</div><div class="v" id="qg">-- kvar</div></div>
<div class="card warn"><div class="k">无补偿线损</div><div class="v" id="l0">-- kW</div></div>
<div class="card good"><div class="k">有飞轮+无功补偿线损</div><div class="v" id="l1">-- kW</div></div>
<div class="card fly"><div class="k">飞轮SOC</div><div class="v" id="soc">-- %</div></div>
</div>
<div class="panel" style="margin-top:10px;">
<div class="card good"><div class="k">日节电量(24h折算)</div><div class="v" id="ed">-- kWh</div></div>
<div class="card good"><div class="k">年节电量(×365)</div><div class="v" id="ey">-- kWh</div></div>
<div class="card"><div class="k">累计统计(演示时间)</div><div class="v" id="ec">-- kWh</div></div>
<div class="card"><div class="k">累计节电占比</div><div class="v" id="er">-- %</div></div>
</div>
<div class="panel" style="margin-top:10px;">
<div class="card max"><div class="k">日最大节电量(理论·计无功)</div><div class="v" id="edm">-- kWh</div></div>
<div class="card max"><div class="k">年最大节电量(理论·计无功)</div><div class="v" id="eym">-- kWh</div></div>
<div class="card max"><div class="k">最大节电对应线损降幅</div><div class="v" id="erm">-- %</div></div>
<div class="card max"><div class="k">其中无功补偿贡献占比</div><div class="v" id="erq">-- %</div></div>
</div>
<div class="legend">橙色=抽油机有功功率|紫色=飞轮功率|蓝色=线路侧有功|无功按负载功率因数与补偿后功率因数实时计算</div>
</div>
<script>
const cv=document.getElementById('cv'), ctx=cv.getContext('2d');
const W=cv.width, H=cv.height;
const U=10, R=4.7;
let period=8, peak=700, valley=250, pfLoad=0.40, pfComp=0.95, running=true, t=0;
const perSl=document.getElementById('per'), perLab=document.getElementById('perlab');
const pkSl=document.getElementById('pk'), pkLab=document.getElementById('pkLab');
const vlSl=document.getElementById('vl'), vlLab=document.getElementById('vlLab');
const pfSl=document.getElementById('pf'), pfLab=document.getElementById('pflab');
const pfcSl=document.getElementById('pfc'), pfcLab=document.getElementById('pfclab');
const btn=document.getElementById('toggle'), resetBtn=document.getElementById('reset');
const plEl=document.getElementById('pl'), qlEl=document.getElementById('ql'), pfvEl=document.getElementById('pfv');
const pgEl=document.getElementById('pg'), qgEl=document.getElementById('qg');
const l0El=document.getElementById('l0'), l1El=document.getElementById('l1'), socEl=document.getElementById('soc');
const edEl=document.getElementById('ed'), eyEl=document.getElementById('ey'), ecEl=document.getElementById('ec'), erEl=document.getElementById('er');
const edmEl=document.getElementById('edm'), eymEl=document.getElementById('eym'), ermEl=document.getElementById('erm'), erqEl=document.getElementById('erq');
perSl.oninput=()=>{period=+perSl.value; perLab.textContent=period; calcMax();};
pkSl.oninput=()=>{peak=+pkSl.value; pkLab.textContent=peak; calcMax();};
vlSl.oninput=()=>{valley=+vlSl.value; vlLab.textContent=valley; calcMax();};
pfSl.oninput=()=>{pfLoad=+pfSl.value; pfLab.textContent=pfLoad.toFixed(2); calcMax();};
pfcSl.oninput=()=>{pfComp=+pfcSl.value; pfcLab.textContent=pfComp.toFixed(2); calcMax();};
btn.onclick=()=>{running=!running; btn.textContent=running?'暂停':'继续';};
resetBtn.onclick=()=>{energyLossNoFly=0; energyLossFly=0; simTime=0;};
const N=360;
const histPload=new Array(N).fill((peak+valley)/2);
const histPfly=new Array(N).fill(0);
const histPgrid=new Array(N).fill((peak+valley)/2);
let soc=60;
let energyLossNoFly=0, energyLossFly=0;
let simTime=0;
const DT=0.05;
// 总电流(含无功):I=sqrt(I_P^2+I_Q^2)
// 三相:I_P=P/(√3 U), I_Q=Q/(√3 U)
// 线损 = 3 I^2 R = (P^2+Q^2)/(U^2) * R /1000 (kW, P kW, Q kvar, U kV, R Ω)
function lossPQ(P,Q){ return (P*P+Q*Q)/(U*U)*R/1000; }
// 抽油机典型上下冲程波形(有功 P)
function loadPower(phase){
const p=((phase%1)+1)%1;
const upEnd=0.45, trans1=0.10, downStart=0.55, downEnd=0.95;
let v;
if(p<upEnd){
const x=p/upEnd;
v=peak*(0.85+0.15*Math.sin(Math.PI*x));
} else if(p<downStart){
const x=(p-upEnd)/(downStart-upEnd);
v=peak+(valley-peak)*smooth(x);
} else if(p<downEnd){
const x=(p-downStart)/(downEnd-downStart);
v=valley*(0.8+0.2*Math.sin(Math.PI*x));
} else {
const x=(p-downEnd)/(1-downEnd);
v=valley+(peak-valley)*smooth(x);
}
return v;
}
function smooth(x){ return x*x*(3-2*x); }
// 负载无功:随有功变化但功率因数近似恒定(简化:Q=|P|*tanφ_load)
function loadQ(P){
const tan=Math.tan(Math.acos(pfLoad));
return P*tan;
}
// 补偿后线路侧无功:Q_comp = |P|*tanφ_comp,但补偿容量有限,不能超过负载Q
function gridQ(P){
const tan=Math.tan(Math.acos(pfComp));
const qTarget=Math.abs(P)*tan;
const qLoad=Math.abs(loadQ(P));
return Math.min(qTarget,qLoad); // 补偿后无功不超过负载无功
}
// 理论最大节电:一个周期积分
// 无补偿:P=loadPower, Q=loadQ(P)
// 有补偿:P=周期均值(飞轮完全拉平), Q=gridQ(Pavg)近似(无功随有功)
function calcMax(){
const steps=1440, T=period;
let sumP=0, eNo=0, eFlyP=0, eFlyQ=0;
// 先求周期平均有功
for(let i=0;i<steps;i++){
const ph=i/steps;
sumP+=loadPower(ph);
}
const pAvg=sumP/steps;
const qAvg=gridQ(pAvg);
// 无补偿积分
for(let i=0;i<steps;i++){
const ph=i/steps;
const P=loadPower(ph);
const Q=loadQ(P);
eNo+=lossPQ(P,Q)*(T/steps)/3600;
}
// 有补偿:P拉平到pAvg,Q=gridQ(pAvg)
const eFly=lossPQ(pAvg,qAvg)*T/3600;
// 分解贡献:仅飞轮(无功仍为负载Q的均值) vs 仅无功补偿(有功仍脉动)
let sumQ=0;
for(let i=0;i<steps;i++){
const ph=i/steps;
const P=loadPower(ph);
sumQ+=loadQ(P);
}
const qLoadAvg=sumQ/steps;
const eFlyOnly=lossPQ(pAvg,qLoadAvg)*T/3600; // 只有飞轮,无功未补
const eQOnly=lossPQ(0,0); // 占位
// 仅无功补偿(有功仍脉动,无功降为gridQ)
let eQcomp=0;
for(let i=0;i<steps;i++){
const ph=i/steps;
const P=loadPower(ph);
eQcomp+=lossPQ(P,gridQ(P))*(T/steps)/3600;
}
const saveOne=eNo-eFly;
const dailyMax=saveOne*(86400/T);
const yearlyMax=dailyMax*365;
const lossNoDaily=eNo*(86400/T);
const ratio=lossNoDaily>0?(dailyMax/lossNoDaily)*100:0;
// 无功补偿贡献占比:仅无功补偿节电 / 总节电
const saveQ=eNo-eQcomp;
const qShare=saveOne>0?(saveQ/saveOne)*100:0;
edmEl.textContent=Math.round(dailyMax).toLocaleString()+' kWh';
eymEl.textContent=Math.round(yearlyMax).toLocaleString()+' kWh';
ermEl.textContent=ratio.toFixed(1)+' %';
erqEl.textContent=qShare.toFixed(0)+' %';
}
calcMax();
function step(){
if(!running) return;
t+=DT; simTime+=DT;
const phase=(t/period)%1;
const P=loadPower(phase);
const Q=loadQ(P);
const pAvg=(peak*0.45+valley*0.4+(peak+valley)/2*0.15);
let pfly=pAvg-P;
const pfMax=450;
pfly=Math.max(-pfMax,Math.min(pfMax,pfly));
soc -= pfly*DT/(500*36)*100;
soc=Math.max(10,Math.min(95,soc));
const Pg=P+pfly;
const Qg=gridQ(P); // 线路侧无功(补偿后)
const l0=lossPQ(P,Q), l1=lossPQ(Pg,Qg);
energyLossNoFly += l0*DT/3600;
energyLossFly += l1*DT/3600;
histPload.push(P); histPload.shift();
histPfly.push(pfly); histPfly.shift();
histPgrid.push(Pg); histPgrid.shift();
plEl.textContent=P.toFixed(0)+' kW';
qlEl.textContent=Q.toFixed(0)+' kvar';
pfvEl.textContent=pfly.toFixed(0)+' kW';
pgEl.textContent=Pg.toFixed(0)+' kW';
qgEl.textContent=Qg.toFixed(0)+' kvar';
l0El.textContent=l0.toFixed(2)+' kW';
l1El.textContent=l1.toFixed(2)+' kW';
socEl.textContent=soc.toFixed(1)+' %';
const saveKW=Math.max(0,l0-l1);
const daily=saveKW*24;
edEl.textContent=Math.round(daily).toLocaleString()+' kWh';
eyEl.textContent=Math.round(daily*365).toLocaleString()+' kWh';
ecEl.textContent=Math.round(energyLossNoFly-energyLossFly).toLocaleString()+' kWh';
const ratio=energyLossNoFly>0?(energyLossNoFly-energyLossFly)/energyLossNoFly*100:0;
erEl.textContent=ratio.toFixed(1)+' %';
}
function drawGrid(){
ctx.strokeStyle='#22344d'; ctx.lineWidth=1;
for(let y=40;y<=H-30;y+=50){ctx.beginPath();ctx.moveTo(40,y);ctx.lineTo(W-20,y);ctx.stroke();}
ctx.fillStyle='#5c6b7a'; ctx.font='10px sans-serif'; ctx.textAlign='right';
ctx.fillText('kW', 35, 45);
for(let y=40;y<=H-30;y+=50){
const val=Math.round(1000-(y-40)/(H-70)*1400);
ctx.fillText(val, 35, y+3);
}
}
function mapY(p){ const pmin=-400,pmax=1000; return H-30-(p-pmin)/(pmax-pmin)*(H-70); }
function drawCurve(arr,color,width){
ctx.strokeStyle=color; ctx.lineWidth=width; ctx.beginPath();
for(let i=0;i<N;i++){
const x=40+i/N*(W-60), y=mapY(arr[i]);
if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
}
ctx.stroke();
}
function draw(){
ctx.clearRect(0,0,W,H);
drawGrid();
drawCurve(histPload,'#f4a261',2);
drawCurve(histPfly,'#c77dff',1.5);
drawCurve(histPgrid,'#4cc9f0',2.5);
ctx.textAlign='left'; ctx.font='12px sans-serif';
ctx.fillStyle='#f4a261'; ctx.fillText('■ 抽油机有功', 55, 20);
ctx.fillStyle='#c77dff'; ctx.fillText('■ 飞轮功率', 190, 20);
ctx.fillStyle='#4cc9f0'; ctx.fillText('■ 线路侧有功', 290, 20);
ctx.fillStyle='#ffd166'; ctx.font='bold 15px sans-serif'; ctx.textAlign='right';
ctx.fillText('年最大节电 '+eymEl.textContent, W-20, 22);
ctx.font='11px sans-serif'; ctx.textAlign='center';
ctx.fillStyle='#f4a261'; ctx.fillText('上冲程', 120, H-10);
ctx.fillStyle='#8d99ae'; ctx.fillText('下冲程', 260, H-10);
const sx=W-130, sy=30, sw=100, sh=10;
ctx.fillStyle='#1b2a41'; ctx.fillRect(sx,sy,sw,sh);
ctx.fillStyle='#9d4edd'; ctx.fillRect(sx,sy,sw*soc/100,sh);
ctx.strokeStyle='#5c6b7a'; ctx.strokeRect(sx,sy,sw,sh);
ctx.fillStyle='#c77dff'; ctx.font='11px sans-serif'; ctx.textAlign='left'; ctx.fillText('飞轮SOC',sx,sy-5);
}
function loop(){ step(); draw(); requestAnimationFrame(loop); }
loop();
</script>
</body>
</html>