@@ -187,20 +187,30 @@ function sparkSvg(pts) {
187187}
188188
189189function stationRow ( s ) {
190- // 固定成本渠道:不访问接口,只展示摊销信息
190+ // 固定成本渠道:不访问接口,展示当前生效各笔的摊销汇总
191191 if ( s . type === "fixed" ) {
192- const daily = s . fixedCostCny > 0 && s . fixedDays > 0 ? s . fixedCostCny / s . fixedDays : 0 ;
193- const pieces = [ `<span>日均摊销 ${ cny ( daily ) } </span>` , `<span>每次 ${ cny ( s . fixedCostCny || 0 ) } 管 ${ s . fixedDays || 0 } 天</span>` ] ;
194- let expired = false ;
195- if ( s . fixedStartDate && s . fixedDays > 0 ) {
196- const start = new Date ( s . fixedStartDate + "T00:00:00" ) ;
197- const end = start . getTime ( ) + s . fixedDays * 86400000 ;
198- const remain = Math . ceil ( ( end - Date . now ( ) ) / 86400000 ) ;
199- expired = remain <= 0 ;
200- pieces . push ( expired
201- ? `<span class="danger">已于 ${ fmtClock ( end ) . split ( " " ) [ 0 ] } 到期,续费请更新日期</span>`
202- : `<span${ remain <= 3 ? ' class="warn"' : "" } >${ esc ( s . fixedStartDate ) } 起 · 剩 ${ remain } 天</span>` ) ;
192+ const ps = Array . isArray ( s . fixedPurchases ) ? s . fixedPurchases : [ ] ;
193+ const nowMs = Date . now ( ) ;
194+ let daily = 0 , active = 0 , pendingStart = 0 , nextEnd = null ;
195+ for ( const p of ps ) {
196+ const d = p . amount > 0 && p . days > 0 ? p . amount / p . days : 0 ;
197+ if ( ! p . startDate ) { daily += d ; active ++ ; continue ; }
198+ const st = Date . parse ( p . startDate + "T00:00:00" ) ;
199+ const end = st + p . days * 86400000 ;
200+ if ( st > nowMs ) { pendingStart ++ ; continue ; }
201+ if ( end > nowMs ) {
202+ daily += d ; active ++ ;
203+ if ( nextEnd == null || end < nextEnd ) nextEnd = end ;
204+ }
205+ }
206+ const expiredAll = ps . length > 0 && active === 0 && pendingStart === 0 ;
207+ const pieces = [ `<span>日均摊销 ${ cny ( daily ) } </span>` , `<span>生效 ${ active } /${ ps . length } 笔</span>` ] ;
208+ if ( pendingStart ) pieces . push ( `<span>待生效 ${ pendingStart } 笔</span>` ) ;
209+ if ( nextEnd != null ) {
210+ const remain = Math . ceil ( ( nextEnd - nowMs ) / 86400000 ) ;
211+ pieces . push ( `<span${ remain <= 3 ? ' class="warn"' : "" } >最近一笔 ${ fmtClock ( nextEnd ) . split ( " " ) [ 0 ] } 到期(剩 ${ remain } 天)</span>` ) ;
203212 }
213+ if ( expiredAll ) pieces . push ( '<span class="danger">已全部到期,续费请追加付费记录</span>' ) ;
204214 return `
205215 <div class="st-row" data-id="${ s . id } ">
206216 <div class="st-plate">¥</div>
@@ -210,8 +220,8 @@ function stationRow(s) {
210220 <div class="st-predict">${ pieces . join ( "<span>·</span>" ) } </div>
211221 </div>
212222 <div class="st-balance">
213- <div class="amt${ expired ? " danger" : "" } ">${ cny ( expired ? 0 : daily ) } </div>
214- <div class="sub">${ expired ? "已到期" : "每天" } </div>
223+ <div class="amt${ expiredAll ? " danger" : "" } ">${ cny ( daily ) } </div>
224+ <div class="sub">${ expiredAll ? "已到期" : "每天" } </div>
215225 </div>
216226 <div class="st-actions">
217227 <button class="icon-btn" data-act="edit" title="编辑"><svg viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/></svg></button>
@@ -1277,6 +1287,7 @@ function render() {
12771287 $ ( "#pageTitle" ) . textContent = titles [ state . view ] [ 0 ] ;
12781288 $ ( "#pageSub" ) . textContent = titles [ state . view ] [ 1 ] ;
12791289 if ( state . view !== "own" ) document . querySelector ( ".own-toc" ) ?. remove ( ) ;
1290+ $ ( "#content" ) . classList . toggle ( "has-toc" , state . view === "own" ) ;
12801291 document . querySelectorAll ( ".nav-item" ) . forEach ( ( n ) => n . classList . toggle ( "active" , n . dataset . view === state . view ) ) ;
12811292 if ( state . view === "dashboard" ) renderDashboard ( ) ;
12821293 else if ( state . view === "stations" ) renderStations ( ) ;
@@ -1305,10 +1316,7 @@ function openModal(station) {
13051316 $ ( "#f-password" ) . value = "" ;
13061317 $ ( "#f-lowBalance" ) . value = station ?. lowBalanceUsd ?? "" ;
13071318 $ ( "#f-cnyRate" ) . value = station ?. cnyPerUsd ?? "" ;
1308- $ ( "#f-fixedCost" ) . value = station ?. fixedCostCny ?? "" ;
1309- $ ( "#f-fixedDays" ) . value = station ?. fixedDays ?? "" ;
1310- // 新建默认今天;编辑回显已存日期(本地时区的 YYYY-MM-DD)
1311- $ ( "#f-fixedStart" ) . value = station ? ( station . fixedStartDate || "" ) : new Date ( ) . toLocaleDateString ( "en-CA" ) ;
1319+ seedPurchaseRows ( station ?. fixedPurchases ) ;
13121320 $ ( "#f-own" ) . checked = ! ! station ?. isOwn ;
13131321 if ( station ) {
13141322 $ ( "#f-accessToken" ) . placeholder = station . hasAccessToken ? "已配置,留空保持不变" : "令牌 / JWT" ;
@@ -1349,6 +1357,34 @@ function syncCredFields() {
13491357}
13501358$ ( "#f-type" ) . onchange = syncCredFields ;
13511359
1360+ // ---- 固定成本付费记录编辑器 ---------------------------------------------------
1361+ function purchaseRowEl ( p = { } ) {
1362+ const row = document . createElement ( "div" ) ;
1363+ row . className = "purchase-row" ;
1364+ row . innerHTML = `
1365+ <input class="input" data-p="amount" placeholder="金额(¥)" value="${ p . amount ?? "" } " />
1366+ <input class="input" data-p="days" placeholder="天数" value="${ p . days ?? "" } " style="width:76px" />
1367+ <input class="input" type="date" data-p="startDate" value="${ p . startDate ?? "" } " style="width:148px" />
1368+ <button type="button" class="icon-btn" data-p="del" title="删除这笔"><svg viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"/></svg></button>` ;
1369+ row . querySelector ( '[data-p="del"]' ) . onclick = ( ) => row . remove ( ) ;
1370+ return row ;
1371+ }
1372+ function seedPurchaseRows ( list ) {
1373+ const box = $ ( "#f-purchases" ) ;
1374+ box . innerHTML = "" ;
1375+ const rows = list && list . length ? list : [ { startDate : new Date ( ) . toLocaleDateString ( "en-CA" ) } ] ;
1376+ rows . forEach ( ( p ) => box . appendChild ( purchaseRowEl ( p ) ) ) ;
1377+ }
1378+ $ ( "#f-addPurchase" ) . onclick = ( ) =>
1379+ $ ( "#f-purchases" ) . appendChild ( purchaseRowEl ( { startDate : new Date ( ) . toLocaleDateString ( "en-CA" ) } ) ) ;
1380+ function collectPurchases ( ) {
1381+ return [ ...document . querySelectorAll ( "#f-purchases .purchase-row" ) ] . map ( ( r ) => ( {
1382+ amount : r . querySelector ( '[data-p="amount"]' ) . value . trim ( ) ,
1383+ days : r . querySelector ( '[data-p="days"]' ) . value . trim ( ) ,
1384+ startDate : r . querySelector ( '[data-p="startDate"]' ) . value ,
1385+ } ) ) . filter ( ( p ) => p . amount !== "" || p . days !== "" ) ;
1386+ }
1387+
13521388$ ( "#modalSave" ) . onclick = async ( ) => {
13531389 const payload = {
13541390 name : $ ( "#f-name" ) . value . trim ( ) ,
@@ -1358,9 +1394,7 @@ $("#modalSave").onclick = async () => {
13581394 email : $ ( "#f-email" ) . value . trim ( ) ,
13591395 lowBalanceUsd : $ ( "#f-lowBalance" ) . value . trim ( ) ,
13601396 cnyPerUsd : $ ( "#f-cnyRate" ) . value . trim ( ) ,
1361- fixedCostCny : $ ( "#f-fixedCost" ) . value . trim ( ) ,
1362- fixedDays : $ ( "#f-fixedDays" ) . value . trim ( ) ,
1363- fixedStartDate : $ ( "#f-fixedStart" ) . value ,
1397+ fixedPurchases : collectPurchases ( ) ,
13641398 isOwn : $ ( "#f-type" ) . value === "newapi" && $ ( "#f-own" ) . checked ,
13651399 } ;
13661400 const at = $ ( "#f-accessToken" ) . value . trim ( ) ;
@@ -1370,8 +1404,9 @@ $("#modalSave").onclick = async () => {
13701404 if ( ak ) payload . apiKey = ak ;
13711405 if ( pw ) payload . password = pw ;
13721406 if ( payload . type === "fixed" ) {
1373- if ( ! ( Number ( payload . fixedCostCny ) > 0 ) ) return toast ( "请填写每次付费金额" , "err" ) ;
1374- if ( ! ( Number ( payload . fixedDays ) > 0 ) ) return toast ( "请填写覆盖天数" , "err" ) ;
1407+ const bad = payload . fixedPurchases . find ( ( p ) => ! ( Number ( p . amount ) > 0 ) || ! ( Number ( p . days ) > 0 ) ) ;
1408+ if ( bad ) return toast ( "每笔付费需填写金额与天数(均大于 0)" , "err" ) ;
1409+ if ( ! payload . fixedPurchases . length ) return toast ( "请至少填写一笔付费记录" , "err" ) ;
13751410 } else if ( ! payload . baseUrl ) {
13761411 return toast ( "请填写站点地址" , "err" ) ;
13771412 }
0 commit comments