@@ -7,37 +7,43 @@
< style >
: root {
color-scheme : dark ;
font-family : Inter , ui-sans-serif , system-ui , - apple-system , BlinkMacSystemFont , "Segoe UI " , sans-serif ;
--bg : #0b0f17 ; --panel : #111722 ; --panel-raised : #151d2a ; --panel-soft : #0f1520 ;
--border : #263 142 ; --border-soft : #1d2736 ; --text : #e7edf 6 ; --muted : #8b98aa ;
--accent : #7dd3fc ; --accent-strong : #38bdf8 ; --accent-soft : #0c2b3 d ;
--success : #34d399 ; --danger : #fb7185 ; --warning : #fbbf24 ;
font-family : - apple-system , BlinkMacSystemFont , "Segoe UI" , Roboto , Oxygen , Ubuntu , Cantarell , "Fira Sans" , "Droid Sans" , "Helvetica Neue ", sans-serif ;
/* Keep these tokens aligned with CLIProxyAPI Manager's dark theme. */
--bg-primary : #1d1b18 ; --bg-secondary : #15 141 2 ; --bg-tertiary : #262320 ; --bg-hover : #2e2a2 6 ; --bg-quinary : #191714 ; --floating-surface : #2a2723 ;
--border-color : #3a3530 ; --border-primary : #4a453f ; --border-hover : #5a544 d ;
--text-primary : #f6f4f1 ; --text-secondary : #c9c3bb ; --text-tertiary : #9c958d ; --text-quaternary : #6f6962 ;
--primary-color : #8b8680 ; --primary-hover : #9a948e ; --primary-active : #a6a099 ;
--success-color : #10b981 ; --error-color : #c65746 ; --warning-color : #ffd862 ; --amber-color : #f59e0b ;
--bg : var ( - - bg - primary ) ; --panel : var ( - - bg - secondary ) ; --panel-raised : var ( - - floating - surface ) ; --panel-soft : var ( - - bg - quinary ) ;
--border : var ( - - border - color ) ; --border-soft : var ( - - border - color ) ; --text : var ( - - text - primary ) ; --muted : var ( - - text - tertiary ) ;
--accent : var ( - - primary - active ) ; --accent-strong : var ( - - primary - color ) ; --accent-soft : var ( - - bg - tertiary ) ;
--success : var ( - - success - color ) ; --danger : var ( - - error - color ) ; --warning : var ( - - warning - color ) ;
}
* { box-sizing : border-box ; }
body { margin : 0 ; min-width : 320 px ; background : var ( - - bg ) ; color : var ( - - text ) ; }
button , input , select { font : inherit ; }
button { cursor : pointer ; }
. app-shell { min-height : 100 vh ; }
. topbar { position : sticky ; z-index : 20 ; top : 0 ; display : flex ; align-items : center ; justify-content : space-between ; gap : 20 px ; height : 58 px ; padding : 0 22 px ; border-bottom : 1 px solid var ( - - border - soft ) ; background : #0b0f17 eb ; backdrop-filter : blur ( 16 px ) ; }
. topbar { position : sticky ; z-index : 20 ; top : 0 ; display : flex ; align-items : center ; justify-content : space-between ; gap : 20 px ; height : 58 px ; padding : 0 22 px ; border-bottom : 1 px solid var ( - - border - soft ) ; background : color-mix ( in srgb , var ( - - bg - primary ) 92 % , transparent ) ; backdrop-filter : blur ( 16 px ) ; }
. brand { display : flex ; align-items : center ; gap : 10 px ; min-width : 0 ; }
. brand-mark { display : grid ; width : 28 px ; height : 28 px ; place-items : center ; border : 1 px solid #155e75 ; border-radius : 8 px ; background : linear-gradient ( 145 deg , #164e63 , #0c2534 ) ; color : #bae6fd ; font-size : 11 px ; font-weight : 800 ; letter-spacing : .04 em ; }
. brand-mark { display : grid ; width : 28 px ; height : 28 px ; place-items : center ; border : 1 px solid var ( - - border - primary ) ; border-radius : 8 px ; background : var ( - - bg - tertiary ) ; color : var ( - - text - primary ) ; font-size : 11 px ; font-weight : 800 ; letter-spacing : .04 em ; }
. brand strong { font-size : 14 px ; letter-spacing : .02 em ; }
. brand span { color : var ( - - muted ) ; font-size : 12 px ; }
. auth-box { display : flex ; align-items : center ; gap : 8 px ; }
. auth-box label { color : var ( - - muted ) ; font-size : 12 px ; }
. mode-switch { display : flex ; padding : 2 px ; border : 1 px solid var ( - - border ) ; border-radius : 8 px ; background : #080d14 ; }
. mode-switch { display : flex ; padding : 2 px ; border : 1 px solid var ( - - border ) ; border-radius : 8 px ; background : var ( - - bg - secondary ) ; }
. mode-button { border : 0 ; padding : 5 px 9 px ; background : transparent ; color : var ( - - muted ) ; }
. mode-button : hover { border-color : transparent ; background : #ffffff 08 ; }
. mode-button . active { background : #1b283a ; color : var ( - - text ) ; }
. demo-mode . mode-button [ data-mode = "demo" ] { background : #78350f ; color : #fef3c7 ; }
. demo-banner { display : flex ; align-items : center ; justify-content : space-between ; gap : 12 px ; padding : 8 px 22 px ; border-bottom : 1 px solid #92400e ; background : #451a03 ; color : #fde68a ; font-size : 11 px ; }
. demo-banner button { border-color : #92400e ; background : #78350f ; color : #fef3c7 ; }
input , select { min-width : 0 ; border : 1 px solid var ( - - border ) ; border-radius : 7 px ; padding : 8 px 10 px ; outline : none ; background : #0d1420 ; color : inherit ; transition : border-color .15 s , box-shadow .15 s ; }
input : focus , select : focus { border-color : #0e7490 ; box-shadow : 0 0 0 3 px #0891b2 1 f ; }
. mode-button : hover { border-color : transparent ; background : var ( - - bg - hover ) ; }
. mode-button . active { background : var ( - - bg - tertiary ) ; color : var ( - - text ) ; }
. demo-mode . mode-button [ data-mode = "demo" ] { background : color-mix ( in srgb , var ( - - amber - color ) 38 % , transparent ) ; color : #fcd34d ; }
. demo-banner { display : flex ; align-items : center ; justify-content : space-between ; gap : 12 px ; padding : 8 px 22 px ; border-bottom : 1 px solid color-mix ( in srgb , var ( - - amber - color ) 45 % , transparent ) ; background : color-mix ( in srgb , var ( - - amber - color ) 14 % , var ( - - bg - secondary ) ) ; color : #fcd34d ; font-size : 11 px ; }
. demo-banner button { border-color : color-mix ( in srgb , var ( - - amber - color ) 45 % , transparent ) ; background : color-mix ( in srgb , var ( - - amber - color ) 24 % , transparent ) ; color : #fcd34d ; }
input , select { min-width : 0 ; border : 1 px solid var ( - - border ) ; border-radius : 7 px ; padding : 8 px 10 px ; outline : none ; background : var ( - - bg - tertiary ) ; color : inherit ; transition : border-color .15 s , box-shadow .15 s ; }
input : focus , select : focus { border-color : var ( - - primary - active ) ; box-shadow : 0 0 0 3 px color-mix ( in srgb , var ( - - primary - color ) 20 % , transparent ) ; }
input : disabled , select : disabled { cursor : not-allowed ; opacity : .55 ; }
input [ type = "checkbox" ] { width : 15 px ; height : 15 px ; padding : 0 ; accent-color : var ( - - accent - strong ) ; }
# key { width : 188 px ; }
. workspace-nav { display : flex ; align-items : stretch ; gap : 3 px ; height : 50 px ; padding : 0 22 px ; border-bottom : 1 px solid var ( - - border - soft ) ; background : #0d121b ; overflow-x : auto ; overflow-y : hidden ; }
. workspace-nav { display : flex ; align-items : stretch ; gap : 3 px ; height : 50 px ; padding : 0 22 px ; border-bottom : 1 px solid var ( - - border - soft ) ; background : var ( - - bg - secondary ) ; overflow-x : auto ; overflow-y : hidden ; }
. nav-button { position : relative ; display : flex ; flex : 0 0 auto ; align-items : center ; height : 49 px ; border : 0 ; padding : 0 13 px ; background : transparent ; color : var ( - - muted ) ; font-size : 13 px ; line-height : 1 ; }
. nav-button : hover { color : var ( - - text ) ; }
. nav-button . active { color : var ( - - text ) ; }
@@ -52,11 +58,11 @@
h2 { margin-bottom : 10 px ; font-size : 15 px ; }
. section-heading p , . subtle { margin-bottom : 0 ; color : var ( - - muted ) ; font-size : 12 px ; }
. tools { display : flex ; align-items : center ; flex-wrap : wrap ; gap : 7 px ; }
button , summary { border : 1 px solid var ( - - border ) ; border-radius : 7 px ; padding : 8 px 11 px ; background : #151d2a ; color : var ( - - text ) ; font-size : 12 px ; }
button : hover , summary : hover { border-color : #3b4b61 ; background : #192334 ; }
button . primary { border-color : #0e7490 ; background : #0e7490 ; color : white ; }
button . primary : hover { background : #0891b2 ; }
button . danger { color : #fecdd3 ; }
button , summary { border : 1 px solid var ( - - border ) ; border-radius : 7 px ; padding : 8 px 11 px ; background : var ( - - bg - tertiary ) ; color : var ( - - text ) ; font-size : 12 px ; }
button : hover , summary : hover { border-color : var ( - - border - hover ) ; background : var ( - - bg - hover ) ; }
button . primary { border-color : var ( - - primary - color ) ; background : var ( - - primary - color ) ; color : white ; }
button . primary : hover { border-color : var ( - - primary - hover ) ; background : var ( - - primary - hover ) ; }
button . danger { color : #f1b0a6 ; }
button . small { padding : 5 px 8 px ; }
button . icon-button { width : 32 px ; height : 32 px ; padding : 0 ; font-size : 17 px ; }
details { position : relative ; }
@@ -66,10 +72,10 @@
. column-options label { display : flex ; align-items : center ; gap : 6 px ; }
. status { min-height : 20 px ; color : var ( - - muted ) ; font-size : 12 px ; }
. summary-grid { display : grid ; grid-template-columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ; gap : 9 px ; margin-bottom : 12 px ; }
. summary-card { min-height : 78 px ; padding : 13 px 14 px ; border : 1 px solid var ( - - border - soft ) ; border-radius : 9 px ; background : linear-gradient ( 145 deg , #121a27 , #101620 ) ; }
. summary-card { min-height : 78 px ; padding : 13 px 14 px ; border : 1 px solid var ( - - border - soft ) ; border-radius : 9 px ; background : var ( - - panel ) ; }
. summary-card span { color : var ( - - muted ) ; font-size : 11 px ; }
. summary-card strong { display : block ; margin-top : 5 px ; font-size : 22 px ; font-weight : 650 ; letter-spacing : -.03 em ; }
. summary-card small { display : block ; margin-top : 2 px ; color : #64748b ; font-size : 11 px ; }
. summary-card small { display : block ; margin-top : 2 px ; color : var ( - - text - quaternary ) ; font-size : 11 px ; }
. surface { border : 1 px solid var ( - - border - soft ) ; border-radius : 9 px ; background : var ( - - panel ) ; }
. surface-toolbar { display : flex ; align-items : center ; justify-content : space-between ; flex-wrap : wrap ; gap : 8 px 12 px ; min-height : 45 px ; padding : 8 px 10 px 8 px 13 px ; border-bottom : 1 px solid var ( - - border - soft ) ; }
. surface-toolbar . status { min-height : 0 ; }
@@ -80,9 +86,9 @@
. chart-card p { margin-bottom : 0 ; color : var ( - - muted ) ; font-size : 10 px ; }
. bar-chart { display : grid ; gap : 9 px ; }
. bar-row { display : grid ; grid-template-columns : minmax ( 70 px , 110 px ) minmax ( 100 px , 1 fr ) 42 px ; align-items : center ; gap : 9 px ; font-size : 10 px ; }
. bar-label { overflow : hidden ; color : #cbd5e1 ; text-overflow : ellipsis ; white-space : nowrap ; }
. bar-track { height : 7 px ; overflow : hidden ; border-radius : 999 px ; background : #202b3b ; }
. bar-value { display : block ; height : 100 % ; min-width : 3 px ; border-radius : inherit ; background : linear-gradient ( 90 deg , #0e7490 , #38bdf8 ) ; }
. bar-label { overflow : hidden ; color : var ( - - text - secondary ) ; text-overflow : ellipsis ; white-space : nowrap ; }
. bar-track { height : 7 px ; overflow : hidden ; border-radius : 999 px ; background : var ( - - bg - tertiary ) ; }
. bar-value { display : block ; height : 100 % ; min-width : 3 px ; border-radius : inherit ; background : var ( - - primary - active ) ; }
. bar-count { color : var ( - - muted ) ; text-align : right ; }
. user-usage { display : grid ; overflow-x : auto ; }
. user-usage-row { display : grid ; grid-template-columns : minmax ( 90 px , 1.2 fr ) 68 px minmax ( 90 px , .8 fr ) 78 px minmax ( 120 px , 1 fr ) ; align-items : center ; gap : 10 px ; min-width : 570 px ; min-height : 31 px ; border-bottom : 1 px solid var ( - - border - soft ) ; font-size : 10 px ; }
@@ -95,15 +101,15 @@
. price-sidebar-header { display : flex ; align-items : center ; justify-content : space-between ; gap : 10 px ; margin-bottom : 10 px ; }
. price-sidebar-header h2 { margin : 0 ; font-size : 12 px ; }
. price-list { display : grid ; gap : 6 px ; }
. price-row { display : grid ; grid-template-columns : minmax ( 0 , 1 fr ) auto ; align-items : center ; gap : 8 px ; padding : 9 px 9 px 9 px 11 px ; border : 1 px solid var ( - - border ) ; border-radius : 7 px ; background : #111925 ; }
. price-row : hover { border-color : #3b4b61 ; background : #151f2d ; }
. price-row { display : grid ; grid-template-columns : minmax ( 0 , 1 fr ) auto ; align-items : center ; gap : 8 px ; padding : 9 px 9 px 9 px 11 px ; border : 1 px solid var ( - - border ) ; border-radius : 7 px ; background : var ( - - bg - primary ) ; }
. price-row : hover { border-color : var ( - - border - hover ) ; background : var ( - - bg - hover ) ; }
. price-row . price-name { overflow : hidden ; font-size : 11 px ; font-weight : 600 ; text-overflow : ellipsis ; }
. price-row small { display : block ; margin-top : 3 px ; overflow : hidden ; color : var ( - - muted ) ; font-size : 9 px ; text-overflow : ellipsis ; white-space : nowrap ; }
. price-row . key-actions { flex-wrap : nowrap ; }
. price-editor { padding : 16 px ; }
. price-editor-header { display : flex ; align-items : center ; justify-content : space-between ; gap : 12 px ; margin-bottom : 12 px ; }
. price-editor-header h2 { margin : 0 ; }
. price-section { margin-bottom : 10 px ; padding : 13 px ; border : 1 px solid var ( - - border - soft ) ; border-radius : 8 px ; background : #0e1520 ; }
. price-section { margin-bottom : 10 px ; padding : 13 px ; border : 1 px solid var ( - - border - soft ) ; border-radius : 8 px ; background : var ( - - bg - quinary ) ; }
. price-section-header { display : flex ; align-items : center ; justify-content : space-between ; gap : 12 px ; margin-bottom : 11 px ; }
. price-section-header h3 { margin : 0 ; font-size : 11 px ; }
. price-section-header . inline-check { color : var ( - - text ) ; }
@@ -116,15 +122,18 @@
. price-actions { display : flex ; justify-content : flex-end ; gap : 8 px ; padding-top : 3 px ; }
. inline-check { display : flex ; align-items : center ; gap : 6 px ; color : var ( - - muted ) ; font-size : 12 px ; }
. key-identity { display : flex ; align-items : center ; gap : 9 px ; }
. avatar { display : grid ; flex : 0 0 auto ; width : 28 px ; height : 28 px ; place-items : center ; border : 1 px solid #285266 ; border-radius : 8 px ; background : var ( - - accent - soft ) ; color : #bae6fd ; font-size : 11 px ; font-weight : 700 ; }
. avatar { display : grid ; flex : 0 0 auto ; width : 28 px ; height : 28 px ; place-items : center ; border : 1 px solid var ( - - border - primary ) ; border-radius : 8 px ; background : var ( - - accent - soft ) ; color : var ( - - text - secondary ) ; font-size : 11 px ; font-weight : 700 ; }
. key-identity strong { display : block ; font-size : 12 px ; }
. key-identity small { display : block ; max-width : 160 px ; overflow : hidden ; color : #64748b ; font-size : 10 px ; text-overflow : ellipsis ; }
. credential { display : flex ; align-items : center ; gap : 6 px ; }
code { color : #cbd5e1 ; font : 11 px ui-monospace , SFMono-Regular , Menlo , Consolas , monospace ; }
. key-identity small { display : block ; max-width : 160 px ; overflow : hidden ; color : var ( - - text - quaternary ) ; font-size : 10 px ; text-overflow : ellipsis ; }
. credential { display : flex ; align-items : center ; }
. credential-copy { cursor : copy ; border-radius : 4 px ; padding : 3 px 4 px ; }
. credential-copy : hover { background : var ( - - bg - hover ) ; color : var ( - - text - primary ) ; }
. credential-copy : focus-visible { outline : 2 px solid var ( - - primary - active ) ; outline-offset : 2 px ; }
code { color : var ( - - text - secondary ) ; font : 11 px ui-monospace , SFMono-Regular , Menlo , Consolas , monospace ; }
. badge { display : inline-flex ; align-items : center ; gap : 5 px ; border : 1 px solid var ( - - border ) ; border-radius : 999 px ; padding : 3 px 7 px ; color : var ( - - muted ) ; font-size : 10 px ; }
. badge :: before { width : 5 px ; height : 5 px ; border-radius : 50 % ; background : currentColor ; content : "" ; }
. badge . active { border-color : #14532d ; background : #052e1b ; color : var ( - - success ) ; }
. badge . disabled , . badge . archived { color : #94a3b8 ; }
. badge . active { border-color : #059669 ; background : #064e3b 4 d ; color : #6ee7b7 ; }
. badge . disabled , . badge . archived { color : var ( - - text - tertiary ) ; }
. route-main { font-size : 12 px ; }
. route-sub { margin-top : 2 px ; max-width : 220 px ; overflow : hidden ; color : var ( - - muted ) ; font-size : 10 px ; text-overflow : ellipsis ; }
. key-actions { display : flex ; gap : 6 px ; }
@@ -132,29 +141,29 @@
. table-wrap { overflow : auto ; }
table { width : 100 % ; border-collapse : collapse ; white-space : nowrap ; font-size : 13 px ; }
th , td { padding : 8 px 10 px ; border-bottom : 1 px solid var ( - - border - soft ) ; text-align : right ; }
th { position : sticky ; top : 0 ; z-index : 1 ; background : #141c29 ; color : var ( - - muted ) ; font-size : 10 px ; font-weight : 650 ; letter-spacing : .03 em ; text-transform : uppercase ; }
th { position : sticky ; top : 0 ; z-index : 1 ; background : var ( - - bg - quinary ) ; color : var ( - - muted ) ; font-size : 10 px ; font-weight : 650 ; letter-spacing : .03 em ; text-transform : uppercase ; }
th . left , td . left { text-align : left ; }
tr : last-child td { border-bottom : 0 ; }
tbody tr : hover { background : #ffffff 05 ; }
. ok { color : #34d399 ; }
. failed { color : #f87171 ; }
. drawer-overlay { position : fixed ; z-index : 40 ; inset : 0 ; background : #020617 a6 ; backdrop-filter : blur ( 2 px ) ; }
. drawer { position : fixed ; z-index : 50 ; top : 0 ; right : 0 ; display : flex ; flex-direction : column ; width : min ( 540 px , 100 vw ) ; height : 100 vh ; border-left : 1 px solid var ( - - border ) ; background : #0f1621 ; box-shadow : -20 px 0 60 px #0008 ; }
tbody tr : hover { background : color-mix ( in srgb , var ( - - text - primary ) 3 % , transparent ) ; }
. ok { color : var ( - - success - color ) ; }
. failed { color : var ( - - error - color ) ; }
. drawer-overlay { position : fixed ; z-index : 40 ; inset : 0 ; background : #000000 a6 ; backdrop-filter : blur ( 2 px ) ; }
. drawer { position : fixed ; z-index : 50 ; top : 0 ; right : 0 ; display : flex ; flex-direction : column ; width : min ( 540 px , 100 vw ) ; height : 100 vh ; border-left : 1 px solid var ( - - border ) ; background : var ( - - bg - primary ) ; box-shadow : -20 px 0 60 px #0008 ; }
. drawer-header { display : flex ; align-items : flex-start ; justify-content : space-between ; gap : 14 px ; padding : 17 px 18 px 13 px ; border-bottom : 1 px solid var ( - - border - soft ) ; }
. drawer-header h2 { margin-bottom : 3 px ; }
. drawer-header p { margin-bottom : 0 ; color : var ( - - muted ) ; font-size : 11 px ; }
. drawer-body { flex : 1 ; overflow : auto ; padding : 16 px 18 px ; }
. drawer-form { display : grid ; gap : 13 px ; }
. drawer-section { display : grid ; gap : 10 px ; padding : 12 px ; border : 1 px solid var ( - - border - soft ) ; border-radius : 8 px ; background : #0c131e ; }
. drawer-section { display : grid ; gap : 10 px ; padding : 12 px ; border : 1 px solid var ( - - border - soft ) ; border-radius : 8 px ; background : var ( - - bg - secondary ) ; }
. drawer-section h3 { margin : 0 ; font-size : 11 px ; }
. field-hint { margin : -2 px 0 0 ; color : #64748b ; font-size : 9 px ; }
. field-hint { margin : -2 px 0 0 ; color : var ( - - text - quaternary ) ; font-size : 9 px ; }
. billing-preview { display : grid ; grid-template-columns : repeat ( 3 , 1 fr ) ; gap : 7 px ; }
. billing-preview div { padding : 9 px ; border : 1 px solid var ( - - border - soft ) ; border-radius : 7 px ; background : #111925 ; }
. billing-preview div { padding : 9 px ; border : 1 px solid var ( - - border - soft ) ; border-radius : 7 px ; background : var ( - - bg - primary ) ; }
. billing-preview span { display : block ; color : var ( - - muted ) ; font-size : 9 px ; }
. billing-preview strong { display : block ; margin-top : 3 px ; font-size : 14 px ; }
. billing-tools { display : flex ; align-items : center ; justify-content : space-between ; gap : 8 px ; }
. ledger-list { display : grid ; max-height : 210 px ; overflow : auto ; gap : 5 px ; }
. ledger-item { display : grid ; grid-template-columns : 1 fr auto ; gap : 3 px 10 px ; padding : 8 px ; border : 1 px solid var ( - - border - soft ) ; border-radius : 7 px ; background : #101824 ; }
. ledger-item { display : grid ; grid-template-columns : 1 fr auto ; gap : 3 px 10 px ; padding : 8 px ; border : 1 px solid var ( - - border - soft ) ; border-radius : 7 px ; background : var ( - - bg - primary ) ; }
. ledger-item strong { font-size : 10 px ; }
. ledger-item . positive { color : var ( - - success ) ; }
. ledger-item . negative { color : var ( - - danger ) ; }
@@ -174,7 +183,7 @@
. pagination { display : flex ; align-items : center ; justify-content : flex-end ; gap : 10 px ; padding : 0 ; }
. page-buttons { display : flex ; align-items : center ; gap : 4 px ; }
. page-buttons button { min-width : 29 px ; height : 27 px ; padding : 3 px 7 px ; }
. page-buttons button . active { border-color : #0e7490 ; background : #0e7490 ; color : white ; }
. page-buttons button . active { border-color : var ( - - primary - color ) ; background : var ( - - primary - color ) ; color : white ; }
. page-buttons button : disabled { cursor : default ; opacity : .4 ; }
. page-ellipsis { width : 22 px ; color : var ( - - muted ) ; text-align : center ; font-size : 10 px ; }
. usage-filters { display : grid ; grid-template-columns : repeat ( 6 , minmax ( 120 px , 1 fr ) ) ; gap : 9 px ; margin-bottom : 10 px ; padding : 11 px ; }
@@ -209,7 +218,7 @@
< / div >
< div class = "chart-grid" > < article class = "chart-card" > < header > < div > < h2 > 今日用户用量< / h2 > < / div > < / header > < div id = "user-usage" class = "user-usage" > < / div > < / article > < article class = "chart-card" > < header > < div > < h2 > 近 7 日 Token< / h2 > < / div > < / header > < div id = "daily-chart" class = "bar-chart" > < / div > < / article > < / div >
< div class = "surface" >
< div class = "surface-toolbar" > < span id = "key-status" class = "status" > < / span > < span class = "subtle" > 点击“管理”修改访问、路由与额度 < / span > < / div >
< div class = "surface-toolbar" > < span id = "key-status" class = "status" > < / span > < / div >
< div class = "table-wrap" > < table class = "key-table" > < thead > < tr > < th class = "left" > 用户< / th > < th class = "left" > 完整 Key< / th > < th class = "left" > 状态< / th > < th class = "left" > 余额 / 额度< / th > < th class = "left" > 并发< / th > < th class = "left" > 路由< / th > < th class = "left" > 允许模型< / th > < th class = "left" > 操作< / th > < / tr > < / thead > < tbody id = "key-rows" > < / tbody > < / table > < / div >
< / div >
< / section >
@@ -257,10 +266,10 @@
< div class = "drawer-body" >
< div id = "key-editor" class = "drawer-form" >
< div class = "drawer-section" > < h3 > 用户< / h3 > < label class = "field" > 名称< input id = "editor-name" placeholder = "例如 alice" > < / label >
< label class = "field" > 完整 Key< input id = "editor-secret" class = "secret" placeholder = "留空自动生成" > < / label > < / div >
< label class = "field" > 完整 Key< input id = "editor-secret" class = "secret" minlength = "6" maxlength = "256" placeholder = "留空自动生成" > < / label > < / div >
< div id = "managed-fields" class = "drawer-form" >
< div class = "drawer-section" > < h3 > 访问与路由< / h3 >
< div class = "field-row" > < label class = "field" > 状态< select id = "editor-status" > < option value = "active" > 启用< / option > < option value = "disabled" > 禁用< / option > < option value = "archived" > 已归档< / option > < / select > < / label > < label class = "field" > 路由< select id = "editor-route" > < option value = "auto" > 自动 选择< / option > < option value = "strict" > 指定账号< / option > < / select > < / label > < / div >
< div class = "field-row" > < label class = "field" > 状态< select id = "editor-status" > < option value = "active" > 启用< / option > < option value = "disabled" > 禁用< / option > < option value = "archived" > 已归档< / option > < / select > < / label > < label class = "field" > 路由< select id = "editor-route" > < option value = "auto" > 自由 选择< / option > < option value = "strict" > 指定账号< / option > < / select > < / label > < / div >
< label class = "field" > 上游账号< select id = "editor-upstream" > < / select > < / label >
< label class = "inline-check" > < input id = "editor-all-models" type = "checkbox" > 允许全部模型< / label >
< label class = "field" > 允许模型规则< input id = "editor-models" placeholder = "deepseek-*, gpt-5.6-sol" > < / label > < p class = "field-hint" > 多个规则用逗号分隔;* 匹配任意字符,未匹配的模型会被拒绝。< / p > < / div >
@@ -675,9 +684,14 @@
}
function routeLabel ( key ) {
if ( key . route _mode !== "strict" ) return { main : "自动 选择" , sub : "由 CPA 调度" } ;
if ( key . route _mode !== "strict" ) return "自由 选择" ;
const account = upstreamAccounts . find ( item => item . id === key . upstream _account _id ) ;
return { main : "指定账号" , sub : upstreamLabel ( account ) } ;
return account ? account . display _name || account . cpa _auth _id || account . id : "未 指定" ;
}
function maskedSecret ( value ) {
const secret = String ( value || "" ) ;
return secret . slice ( 0 , 2 ) + "******" + secret . slice ( - 4 ) ;
}
function copyText ( value , button ) {
@@ -760,12 +774,12 @@
const identity = document . createElement ( "div" ) ; identity . className = "key-identity" ;
const avatar = document . createElement ( "span" ) ; avatar . className = "avatar" ; avatar . textContent = ( key . name || "K" ) . slice ( 0 , 2 ) . toUpperCase ( ) ;
const identityText = document . createElement ( "div" ) ; const name = document . createElement ( "strong" ) ; name . textContent = key . name ; const id = document . createElement ( "small" ) ; id . textContent = key . id ; identityText . append ( name , id ) ; identity . append ( avatar , identityText ) ;
const credential = document . createElement ( "div" ) ; credential . className = "credential" ; const secret = document . createElement ( "code" ) ; secret . textContent = key . secret ;
const copy = document . createElement ( "button" ) ; copy . type = "butto n"; copy . className = "small" ; copy . textContent = "复制 " ; copy . addEventListener ( "click" , ( ) => copyText ( key . secret , copy ) ) ; credential . append ( secret , copy ) ;
const credential = document . createElement ( "div" ) ; credential . className = "credential" ; const secret = document . createElement ( "code" ) ; secret . className = "credential-copy" ; secret . textContent = maskedSecret ( key . secret ) ; secret . tabIndex = 0 ; secret . setAttribute ( "role" , "button" ) ; secret . setAttribute ( "aria-label" , "复制完整 Key" ) ; secret . title = "点击复制完整 Key" ;
secret . addEventListener ( "click" , ( ) => copyText ( key . secret , secret ) ) ; secret . addEventListener ( "keydow n", event => { if ( event . key === "Enter" || event . key == = " " ) { event . preventDefault ( ) ; copyText ( key . secret , secret ) ; } } ) ; credential . append ( secret ) ;
const status = document . createElement ( "span" ) ; status . className = ` badge ${ key . status } ` ; status . textContent = key . status === "active" ? "启用" : key . status === "disabled" ? "禁用" : "已归档" ;
const billing = key . billing || { } ; const balance = document . createElement ( "div" ) ; const balanceMain = document . createElement ( "div" ) ; balanceMain . className = "route-main" ; balanceMain . textContent = money ( billing . balance _usd ) ; const balanceSub = document . createElement ( "div" ) ; balanceSub . className = "route-sub" ; balanceSub . textContent = ` 额度 ${ money ( billing . quota _usd ) } ${ billing . reset _period && billing . reset _period !== "none" ? " · " + ( { daily : "每天" , weekly : "每周" , monthly : "每月" } [billing.reset_period] || billing.reset_period) : ""} ` ; balance . append ( balanceMain , balanceSub ) ;
const concurrency = document . createElement ( "span" ) ; concurrency . textContent = ` ${ billing . active _requests || 0 } / ${ billing . max _concurrency || 4 } ` ;
const route = routeLabel ( key ) ; const routeNode = document . createElement ( "div " ) ; const routeMain = document . createElement ( "div" ) ; routeMain . className = "route-main" ; routeMain . textContent = route . main ; const routeSub = document . createElement ( "div" ) ; routeSub . className = "route-sub" ; routeSub . textContent = route . sub ; routeNode . append ( routeMain , routeSub ) ;
const routeNode = document . createElement ( "span " ) ; routeNode . className = "route-main" ; routeNode . textContent = routeLabel ( key ) ;
const models = document . createElement ( "span" ) ; models . textContent = key . all _models ? "全部模型" : ( key . models || [ ] ) . join ( ", " ) || "未配置" ;
const actions = document . createElement ( "div" ) ; actions . className = "key-actions" ;
const stats = document . createElement ( "button" ) ; stats . type = "button" ; stats . className = "small" ; stats . textContent = "统计" ; stats . addEventListener ( "click" , ( ) => loadKeyStats ( key ) ) ;
@@ -841,6 +855,7 @@
try {
if ( testMode ) {
if ( ! name ) throw new Error ( "请输入名称" ) ;
if ( secret && secret . length < 6 ) throw new Error ( "Key 至少需要 6 个字符" ) ;
if ( demoState . keys . some ( key => key . name . toLowerCase ( ) === name . toLowerCase ( ) ) ) throw new Error ( "名称已经存在" ) ;
const created = { id : "demo-key-" + Date . now ( ) , ... payload , secret : secret || ` demo- ${ Date . now ( ) } -000000 ` , status : "active" , billing : { ... payload . billing , spent _usd : "0" , balance _usd : payload . billing . quota _usd , active _requests : 0 , cycle _started _at : new Date ( ) . toISOString ( ) } } ;
demoState . keys . push ( created ) ; saveDemoState ( ) ; closeDrawer ( ) ; await loadKeyManagement ( ) ;