@@ -56,6 +56,8 @@ const api = {
5656 overview : ( hours ) => call ( `/api/history/overview?hours=${ hours } ` ) ,
5757 usage : ( range ) => call ( `/api/usage?range=${ range } &tz=${ encodeURIComponent ( Intl . DateTimeFormat ( ) . resolvedOptions ( ) . timeZone ) } ` ) ,
5858 ownAnalytics : ( range ) => call ( `/api/own/analytics?range=${ range } &tz=${ encodeURIComponent ( Intl . DateTimeFormat ( ) . resolvedOptions ( ) . timeZone ) } ` ) ,
59+ ownAdminKeys : ( ) => call ( "/api/own/admin-keys" ) ,
60+ saveOwnAdminKeys : ( keys ) => call ( "/api/own/admin-keys" , { method : "PUT" , body : { keys } } ) ,
5961 saveSettings : ( b ) => call ( "/api/settings" , { method : "PUT" , body : b } ) ,
6062 notifications : ( ) => call ( "/api/notifications" ) ,
6163 addChannel : ( b ) => call ( "/api/notifications/channels" , { body : b } ) ,
@@ -1189,23 +1191,93 @@ function profitSection(p) {
11891191 </div>
11901192 <div class="st-balance"><div class="sub">${ u . enabled } /${ u . total } 个渠道启用</div></div>
11911193 </div>` ) . join ( "" ) } </div>` : "" ;
1194+ const resoldCny = p . resoldCny || 0 ;
1195+ const incomeSub = [
1196+ resoldCny > 0 ? `含转售管理员 Key ${ cny ( resoldCny ) } ` : "" ,
1197+ p . adminUsageCny > 0 ? `管理员自用 ${ cny ( p . adminUsageCny ) } (计成本不计收入)` : "" ,
1198+ ] . filter ( Boolean ) . join ( " · " ) ;
11921199 return `
11931200 <div class="section-head" data-toc="利润分析"><h2>利润分析</h2>
1194- <span class="muted">收入 = 普通用户消费 × 售价汇率(不含管理员/root)· 成本按各上游口径(窗口 ${ p . windowDays } 天)</span></div>
1201+ <span class="muted">收入 = 普通用户消费 × 售价汇率(不含管理员/root,除非该 Key 已标为转售 )· 成本按各上游口径(窗口 ${ p . windowDays } 天)</span></div>
11951202 <div class="stats" style="grid-template-columns:repeat(4,1fr)">
11961203 <div class="stat-card"><div class="label">期内收入</div><div class="value">${ cny ( p . incomeCny ) } </div>${
1197- p . adminUsageCny > 0 ? `<div class="stat-sub">管理员另消耗 ${ cny ( p . adminUsageCny ) } (计成本不计收入) </div>` : ""
1204+ incomeSub ? `<div class="stat-sub">${ incomeSub } </div>` : ""
11981205 } </div>
11991206 <div class="stat-card"><div class="label">期内成本</div><div class="value">${ cny ( p . totalCostCny ) } </div></div>
12001207 <div class="stat-card"><div class="label">利润</div><div class="value ${ profitCls } ">${ cny ( p . profitCny ) } </div></div>
12011208 <div class="stat-card"><div class="label">利润率</div><div class="value ${ profitCls } ">${ p . marginPct != null ? p . marginPct + "%" : "—" } </div></div>
12021209 </div>
1210+ ${ resoldSection ( p ) }
12031211 ${ p . costs . length ? `<div class="panel" style="margin-bottom:14px">${ costRows } </div>`
12041212 : '<div class="usage-errors" style="margin-bottom:14px"><span>没有渠道能匹配到监控中的站点(按 URL 比对),成本暂计 ¥0</span></div>' }
12051213 ${ unmatchedRows }
12061214 <div class="section-head" style="margin-top:16px" data-toc="用量图表"><h2>用量分析</h2></div>` ;
12071215}
12081216
1217+ // 转售管理员 Key 区块:已标记 Key 的收入明细 + 打开管理器按钮
1218+ function resoldSection ( p ) {
1219+ const rows = ( p . resoldKeys || [ ] ) . map ( ( k ) => `
1220+ <div class="st-row profit-row">
1221+ <div class="st-main" style="cursor:default">
1222+ <div class="st-name">${ esc ( k . username ) } / ${ esc ( k . tokenName ) } ${
1223+ k . error ? ` <span class="demo-tag tag-warn">查询失败</span>` : ""
1224+ } </div>
1225+ <div class="st-meta">${ k . error ? esc ( k . error ) : "转售给下游 · 计入收入" } </div>
1226+ </div>
1227+ <div class="st-balance"><div class="amt">${ cny ( k . cny || 0 ) } </div><div class="sub">期内收入</div></div>
1228+ </div>` ) . join ( "" ) ;
1229+ return `
1230+ <div class="section-head" style="margin-top:16px" data-toc="转售 Key">
1231+ <h2>管理员转售 Key</h2>
1232+ <button class="btn btn-ghost" id="manageResold">管理转售 Key</button></div>
1233+ ${ rows ? `<div class="panel" style="margin-bottom:14px">${ rows } </div>`
1234+ : `<div class="usage-errors" style="margin-bottom:14px"><span>还没有标记转售 Key。若某个管理员/root 账号的 API Key 实际给了下游,点「管理转售 Key」勾选它,其消费即计入收入。</span></div>` }
1235+ <div id="resoldManager"></div>` ;
1236+ }
1237+
1238+ // 渲染转售 Key 管理器:按管理员账号分组,逐个 Key 复选
1239+ function renderResoldManager ( box , accounts ) {
1240+ const ROLE = { 10 : "管理员" , 100 : "root" } ;
1241+ if ( ! accounts || ! accounts . length ) {
1242+ box . innerHTML = `<div class="usage-errors" style="margin-bottom:14px"><span>没有找到管理员/root 账号(role ≥ 10)。</span></div>` ;
1243+ return ;
1244+ }
1245+ const rate = rateOf ( state . ownData . station ) ;
1246+ const keyRow = ( user , name , checked , usedUsd ) => `
1247+ <label class="resold-key">
1248+ <input type="checkbox" data-user="${ esc ( user ) } " data-token="${ esc ( name ) } " ${ checked ? "checked" : "" } >
1249+ <span class="resold-key-name">${ esc ( name ) || "(未命名)" } </span>
1250+ ${ usedUsd != null ? `<span class="resold-key-used">累计用 ${ cny ( usedUsd * rate ) } </span>` : "" }
1251+ </label>` ;
1252+ const groups = accounts . map ( ( a ) => {
1253+ const head = `<div class="st-name" style="margin:2px 0 8px">${ esc ( a . username ) } <span class="demo-tag">${ ROLE [ a . role ] || "role " + a . role } </span></div>` ;
1254+ // 可枚举:直接列出所有 Key 复选
1255+ if ( a . enumerable ) {
1256+ if ( ! a . tokens . length ) return `<div class="resold-group">${ head } <div class="st-meta">该账号没有 API Key</div></div>` ;
1257+ return `<div class="resold-group">${ head } ${ a . tokens . map ( ( t ) => keyRow ( a . username , t . name , t . flagged , t . usedUsd ) ) . join ( "" ) } </div>` ;
1258+ }
1259+ // 不可枚举(如 root,接口不允许越权列 Key):回显已标记的 + 手动填 Key 名
1260+ const existing = a . tokens . map ( ( t ) => keyRow ( a . username , t . name , true , null ) ) . join ( "" ) ;
1261+ return `<div class="resold-group" data-acct="${ esc ( a . username ) } ">
1262+ ${ head }
1263+ <div class="st-meta" style="margin-bottom:6px">此账号无法自动列出 Key(${ esc ( a . error || "接口限制" ) } )。若它有转售 Key,请手动填 Key 名:</div>
1264+ <div class="resold-keys">${ existing } </div>
1265+ <div class="resold-add">
1266+ <input class="input small" placeholder="Key 名(token_name)" data-addfor="${ esc ( a . username ) } ">
1267+ <button class="btn btn-ghost" data-resold-add="${ esc ( a . username ) } ">添加</button>
1268+ </div>
1269+ </div>` ;
1270+ } ) . join ( "" ) ;
1271+ box . innerHTML = `
1272+ <div class="panel" style="margin-bottom:14px;padding:16px">
1273+ <div class="st-meta" style="margin-bottom:10px">勾选实际转售给下游的 Key,其期内消费将从「管理员自用(成本)」改计入「收入 × 售价汇率」。Key 名可能跨账号重名,故按「账号 + Key 名」定位。</div>
1274+ ${ groups }
1275+ <div style="margin-top:12px;display:flex;gap:8px">
1276+ <button class="btn btn-primary" id="resoldSave">保存并重算</button>
1277+ </div>
1278+ </div>` ;
1279+ }
1280+
12091281// 分用户消费横向条形图(¥)
12101282function drawOwnUsers ( wrap , users ) {
12111283 if ( ! users . length ) {
@@ -1790,6 +1862,52 @@ $(".main").addEventListener("click", async (e) => {
17901862 return ;
17911863 }
17921864
1865+ // 管理员转售 Key:打开管理器 / 收起 / 保存
1866+ if ( e . target . closest ( "#manageResold" ) ) {
1867+ const box = $ ( "#resoldManager" ) ;
1868+ if ( box . dataset . open === "1" ) { box . dataset . open = "0" ; box . innerHTML = "" ; return ; }
1869+ box . dataset . open = "1" ;
1870+ box . innerHTML = `<div class="panel" style="margin-bottom:14px"><div class="empty" style="padding:20px">正在拉取管理员账号的 Key…</div></div>` ;
1871+ try {
1872+ const { accounts } = await api . ownAdminKeys ( ) ;
1873+ renderResoldManager ( box , accounts ) ;
1874+ } catch ( err ) {
1875+ box . innerHTML = `<div class="usage-errors" style="margin-bottom:14px"><span>⚠ 拉取失败:${ esc ( err . message ) } </span></div>` ;
1876+ }
1877+ return ;
1878+ }
1879+ const addBtn = e . target . closest ( "[data-resold-add]" ) ;
1880+ if ( addBtn ) {
1881+ const user = addBtn . dataset . resoldAdd ;
1882+ const input = document . querySelector ( `input[data-addfor="${ CSS . escape ( user ) } "]` ) ;
1883+ const name = ( input ?. value || "" ) . trim ( ) ;
1884+ if ( ! name ) return ;
1885+ const list = addBtn . closest ( ".resold-group" ) . querySelector ( ".resold-keys" ) ;
1886+ // 已存在则不重复添加
1887+ if ( ! [ ...list . querySelectorAll ( "input" ) ] . some ( ( c ) => c . dataset . token === name ) ) {
1888+ const label = document . createElement ( "label" ) ;
1889+ label . className = "resold-key" ;
1890+ label . innerHTML = `<input type="checkbox" data-user="${ esc ( user ) } " data-token="${ esc ( name ) } " checked>
1891+ <span class="resold-key-name">${ esc ( name ) } </span>` ;
1892+ list . appendChild ( label ) ;
1893+ }
1894+ input . value = "" ;
1895+ return ;
1896+ }
1897+ if ( e . target . closest ( "#resoldSave" ) ) {
1898+ const keys = [ ...document . querySelectorAll ( "#resoldManager input[type=checkbox]:checked" ) ]
1899+ . map ( ( c ) => ( { username : c . dataset . user , tokenName : c . dataset . token } ) ) ;
1900+ const btn = e . target . closest ( "#resoldSave" ) ;
1901+ btn . disabled = true ;
1902+ try {
1903+ await api . saveOwnAdminKeys ( keys ) ;
1904+ toast ( `已保存 ${ keys . length } 个转售 Key,正在重算利润…` ) ;
1905+ $ ( "#resoldManager" ) . dataset . open = "0" ;
1906+ await loadOwn ( true ) ;
1907+ } catch ( err ) { toast ( err . message , "err" ) ; btn . disabled = false ; }
1908+ return ;
1909+ }
1910+
17931911 // 规则开关 / 保存
17941912 const ruleToggle = e . target . closest ( "[data-rule]" ) ;
17951913 if ( ruleToggle ) {
0 commit comments