@@ -132,6 +132,14 @@ function buildShorthandConnectionString(dialect: DbDialect, input: ShorthandConn
return ` ${ protocol } :// ${ encodeURIComponent ( user ) } : ${ encodeURIComponent ( password ) } @ ${ host } : ${ port } / ${ encodeURIComponent ( database ) } ` ;
return ` ${ protocol } :// ${ encodeURIComponent ( user ) } : ${ encodeURIComponent ( password ) } @ ${ host } : ${ port } / ${ encodeURIComponent ( database ) } ` ;
}
}
function inferUrlDialect ( connectionString : string ) : 'mysql' | 'postgres' | null {
const normalized = ( connectionString || '' ) . trim ( ) . toLowerCase ( ) ;
if ( ! normalized ) return null ;
if ( normalized . startsWith ( 'mysql://' ) ) return 'mysql' ;
if ( normalized . startsWith ( 'postgres://' ) || normalized . startsWith ( 'postgresql://' ) ) return 'postgres' ;
return null ;
}
function isRegexModelPattern ( pattern : string ) : boolean {
function isRegexModelPattern ( pattern : string ) : boolean {
return pattern . trim ( ) . toLowerCase ( ) . startsWith ( 're:' ) ;
return pattern . trim ( ) . toLowerCase ( ) . startsWith ( 're:' ) ;
}
}
@@ -311,7 +319,7 @@ export default function Settings() {
const items = Array . isArray ( res ? . items ) ? res . items : [ ] ;
const items = Array . isArray ( res ? . items ) ? res . items : [ ] ;
setDownstreamKeys ( items ) ;
setDownstreamKeys ( items ) ;
} catch ( err : any ) {
} catch ( err : any ) {
toast . error ( err ? . message || '鍔犺浇涓嬫父 API Key 澶辫触 ' ) ;
toast . error ( err ? . message || '加载下游 API Key 失败 ' ) ;
} finally {
} finally {
setDownstreamLoading ( false ) ;
setDownstreamLoading ( false ) ;
}
}
@@ -329,7 +337,7 @@ export default function Settings() {
enabled : ! ! row . enabled ,
enabled : ! ! row . enabled ,
} ) ) ) ;
} ) ) ) ;
} catch ( err : any ) {
} catch ( err : any ) {
toast . error ( err ? . message || '鍔犺浇璺敱鍒楄〃澶辫触 ' ) ;
toast . error ( err ? . message || '加载路由列表失败 ' ) ;
} finally {
} finally {
setSelectorLoading ( false ) ;
setSelectorLoading ( false ) ;
}
}
@@ -376,7 +384,8 @@ export default function Settings() {
enabled : ! ! row . enabled ,
enabled : ! ! row . enabled ,
} ) ) ) ;
} ) ) ) ;
if ( runtimeDatabaseInfo ? . active ? . dialect ) {
if ( runtimeDatabaseInfo ? . active ? . dialect ) {
setMigrationDialect ( runtimeDatabaseInfo . active . dialect as DbDialect ) ;
const preferredDialect = ( runtimeDatabaseInfo ? . saved ? . dialect || runtimeDatabaseInfo . active . dialect ) as DbDialect ;
setMigrationDialect ( preferredDialect ) ;
}
}
setRuntimeDatabaseState ( {
setRuntimeDatabaseState ( {
active : {
active : {
@@ -392,7 +401,7 @@ export default function Settings() {
restartRequired : ! ! runtimeDatabaseInfo ? . restartRequired ,
restartRequired : ! ! runtimeDatabaseInfo ? . restartRequired ,
} ) ;
} ) ;
} catch ( err : any ) {
} catch ( err : any ) {
toast . error ( err ? . message || '鍔犺浇璁剧疆澶辫触 ' ) ;
toast . error ( err ? . message || '加载设置失败 ' ) ;
} finally {
} finally {
setLoading ( false ) ;
setLoading ( false ) ;
}
}
@@ -419,7 +428,7 @@ export default function Settings() {
} ) ;
} ) ;
toast . success ( 'Schedule settings saved' ) ;
toast . success ( 'Schedule settings saved' ) ;
} catch ( err : any ) {
} catch ( err : any ) {
toast . error ( err ? . message || '淇濆瓨澶辫触 ' ) ;
toast . error ( err ? . message || '保存失败 ' ) ;
} finally {
} finally {
setSavingSchedule ( false ) ;
setSavingSchedule ( false ) ;
}
}
@@ -428,7 +437,7 @@ export default function Settings() {
const saveProxyToken = async ( ) = > {
const saveProxyToken = async ( ) = > {
const suffix = proxyTokenSuffix . trim ( ) ;
const suffix = proxyTokenSuffix . trim ( ) ;
if ( ! suffix ) {
if ( ! suffix ) {
toast . info ( tr ( '璇疯緭鍏?sk- 鍚庣殑浠ょ墝鍐呭' ) ) ;
toast . info ( '请输入 sk- 后的令牌内容' ) ;
return ;
return ;
}
}
setSavingToken ( true ) ;
setSavingToken ( true ) ;
@@ -438,7 +447,7 @@ export default function Settings() {
setProxyTokenSuffix ( '' ) ;
setProxyTokenSuffix ( '' ) ;
toast . success ( 'Proxy token updated' ) ;
toast . success ( 'Proxy token updated' ) ;
} catch ( err : any ) {
} catch ( err : any ) {
toast . error ( err ? . message || '淇濆瓨澶辫触 ' ) ;
toast . error ( err ? . message || '保存失败 ' ) ;
} finally {
} finally {
setSavingToken ( false ) ;
setSavingToken ( false ) ;
}
}
@@ -501,7 +510,7 @@ export default function Settings() {
return ;
return ;
}
}
if ( ! rawKey ) {
if ( ! rawKey ) {
toast . info ( '璇峰~鍐? API Key' ) ;
toast . info ( '请填写 API Key' ) ;
return ;
return ;
}
}
if ( ! rawKey . startsWith ( PROXY_TOKEN_PREFIX ) ) {
if ( ! rawKey . startsWith ( PROXY_TOKEN_PREFIX ) ) {
@@ -533,7 +542,7 @@ export default function Settings() {
resetDownstreamForm ( ) ;
resetDownstreamForm ( ) ;
await loadDownstreamKeys ( ) ;
await loadDownstreamKeys ( ) ;
} catch ( err : any ) {
} catch ( err : any ) {
toast . error ( err ? . message || '淇濆瓨涓嬫父 API Key 澶辫触 ' ) ;
toast . error ( err ? . message || '保存下游 API Key 失败 ' ) ;
} finally {
} finally {
setDownstreamSaving ( false ) ;
setDownstreamSaving ( false ) ;
}
}
@@ -610,7 +619,7 @@ export default function Settings() {
} ) ;
} ) ;
toast . success ( 'Routing weights saved' ) ;
toast . success ( 'Routing weights saved' ) ;
} catch ( err : any ) {
} catch ( err : any ) {
toast . error ( err ? . message || '淇濆瓨澶辫触 ' ) ;
toast . error ( err ? . message || '保存失败 ' ) ;
} finally {
} finally {
setSavingRouting ( false ) ;
setSavingRouting ( false ) ;
}
}
@@ -642,7 +651,7 @@ export default function Settings() {
} ) ) ;
} ) ) ;
toast . success ( 'Security settings saved' ) ;
toast . success ( 'Security settings saved' ) ;
} catch ( err : any ) {
} catch ( err : any ) {
toast . error ( err ? . message || '淇濆瓨澶辫触 ' ) ;
toast . error ( err ? . message || '保存失败 ' ) ;
} finally {
} finally {
setSavingSecurity ( false ) ;
setSavingSecurity ( false ) ;
}
}
@@ -650,26 +659,26 @@ export default function Settings() {
const handleClearCache = async ( ) = > {
const handleClearCache = async ( ) = > {
if ( ! window . confirm ( '纭娓呯悊妯″瀷缂撳瓨骞堕噸寤鸿矾鐢憋紵 ' ) ) return ;
if ( ! window . confirm ( '确认清理模型缓存并重建路由? ' ) ) return ;
setClearingCache ( true ) ;
setClearingCache ( true ) ;
try {
try {
const res = await api . clearRuntimeCache ( ) ;
const res = await api . clearRuntimeCache ( ) ;
toast . success ( ` 缂撳瓨宸叉竻鐞嗭紙妯″瀷缂撳瓨 ${ res . deletedModelAvailability || 0 } 鏉★級 ` ) ;
toast . success ( ` 缓存已清理(模型缓存 ${ res . deletedModelAvailability || 0 } 条) ` ) ;
} catch ( err : any ) {
} catch ( err : any ) {
toast . error ( err ? . message || '娓呯悊缂撳瓨澶辫触 ' ) ;
toast . error ( err ? . message || '清理缓存失败 ' ) ;
} finally {
} finally {
setClearingCache ( false ) ;
setClearingCache ( false ) ;
}
}
} ;
} ;
const handleClearUsage = async ( ) = > {
const handleClearUsage = async ( ) = > {
if ( ! window . confirm ( '纭娓呯悊鍗犵敤缁熻涓庝娇鐢ㄦ棩蹇楋紵 ' ) ) return ;
if ( ! window . confirm ( '确认清理占用统计与使用日志? ' ) ) return ;
setClearingUsage ( true ) ;
setClearingUsage ( true ) ;
try {
try {
const res = await api . clearUsageData ( ) ;
const res = await api . clearUsageData ( ) ;
toast . success ( ` 鍗犵敤缁熻宸叉竻鐞嗭紙鏃ュ織 ${ res . deletedProxyLogs || 0 } 鏉★級 ` ) ;
toast . success ( ` 占用统计已清理(日志 ${ res . deletedProxyLogs || 0 } 条) ` ) ;
} catch ( err : any ) {
} catch ( err : any ) {
toast . error ( err ? . message || '娓呯悊鍗犵敤澶辫触 ' ) ;
toast . error ( err ? . message || '清理占用失败 ' ) ;
} finally {
} finally {
setClearingUsage ( false ) ;
setClearingUsage ( false ) ;
}
}
@@ -681,6 +690,12 @@ export default function Settings() {
return ;
return ;
}
}
const inferredDialect = inferUrlDialect ( effectiveMigrationConnectionString ) ;
if ( migrationDialect === 'sqlite' && inferredDialect ) {
toast . error ( ` 当前选择 SQLite,但连接串是 ${ inferredDialect . toUpperCase ( ) } URL,请先切换方言 ` ) ;
return ;
}
setTestingMigrationConnection ( true ) ;
setTestingMigrationConnection ( true ) ;
try {
try {
const res = await api . testExternalDatabaseConnection ( {
const res = await api . testExternalDatabaseConnection ( {
@@ -702,6 +717,12 @@ export default function Settings() {
return ;
return ;
}
}
const inferredDialect = inferUrlDialect ( effectiveMigrationConnectionString ) ;
if ( migrationDialect === 'sqlite' && inferredDialect ) {
toast . error ( ` 当前选择 SQLite,但连接串是 ${ inferredDialect . toUpperCase ( ) } URL,请先切换方言 ` ) ;
return ;
}
const warning = migrationOverwrite
const warning = migrationOverwrite
? 'Confirm migration and overwrite existing data in target database?'
? 'Confirm migration and overwrite existing data in target database?'
: 'Confirm migration to target database? If target has data, migration may fail.' ;
: 'Confirm migration to target database? If target has data, migration may fail.' ;
@@ -729,6 +750,12 @@ export default function Settings() {
return ;
return ;
}
}
const inferredDialect = inferUrlDialect ( effectiveMigrationConnectionString ) ;
if ( migrationDialect === 'sqlite' && inferredDialect ) {
toast . error ( ` 当前选择 SQLite,但连接串是 ${ inferredDialect . toUpperCase ( ) } URL,请先切换方言 ` ) ;
return ;
}
setSavingRuntimeDatabase ( true ) ;
setSavingRuntimeDatabase ( true ) ;
try {
try {
const res = await api . updateRuntimeDatabaseConfig ( {
const res = await api . updateRuntimeDatabaseConfig ( {
@@ -768,7 +795,7 @@ export default function Settings() {
return (
return (
< div className = "animate-fade-in" >
< div className = "animate-fade-in" >
< div className = "page-header" >
< div className = "page-header" >
< h2 className = "page-title" > { tr ( '绯荤粺璁剧疆' ) } < / h2 >
< h2 className = "page-title" > 系 统 设 置 < / h2 >
< / div >
< / div >
< div style = { { maxWidth : 720 , display : 'flex' , flexDirection : 'column' , gap : 16 } } >
< div style = { { maxWidth : 720 , display : 'flex' , flexDirection : 'column' , gap : 16 } } >
@@ -777,7 +804,7 @@ export default function Settings() {
< code style = { { display : 'block' , padding : '10px 14px' , background : 'var(--color-bg)' , borderRadius : 'var(--radius-sm)' , fontSize : 13 , fontFamily : 'var(--font-mono)' , color : 'var(--color-text-secondary)' , border : '1px solid var(--color-border-light)' , marginBottom : 12 } } >
< code style = { { display : 'block' , padding : '10px 14px' , background : 'var(--color-bg)' , borderRadius : 'var(--radius-sm)' , fontSize : 13 , fontFamily : 'var(--font-mono)' , color : 'var(--color-text-secondary)' , border : '1px solid var(--color-border-light)' , marginBottom : 12 } } >
{ maskedToken || '****' }
{ maskedToken || '****' }
< / code >
< / code >
< button onClick = { ( ) = > setShowChangeKey ( true ) } className = "btn btn-primary" > 淇 敼 鐧 诲 綍 浠 ょ 墝 < / button >
< button onClick = { ( ) = > setShowChangeKey ( true ) } className = "btn btn-primary" > 修 改 登 录 令 牌 < / button >
< ChangeKeyModal
< ChangeKeyModal
open = { showChangeKey }
open = { showChangeKey }
onClose = { ( ) = > {
onClose = { ( ) = > {
@@ -788,10 +815,10 @@ export default function Settings() {
< / div >
< / div >
< div className = "card animate-slide-up stagger-2" style = { { padding : 20 } } >
< div className = "card animate-slide-up stagger-2" style = { { padding : 20 } } >
< div style = { { fontWeight : 600 , fontSize : 14 , marginBottom : 12 } } > 瀹 氭 椂 浠 诲 姟 < / div >
< div style = { { fontWeight : 600 , fontSize : 14 , marginBottom : 12 } } > 定 时 任 务 < / div >
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 12 } } >
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 12 } } >
< div >
< div >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' , marginBottom : 6 } } > 绛 惧 埌 Cron < / div >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' , marginBottom : 6 } } > 签 到 Cron < / div >
< input
< input
value = { runtime . checkinCron }
value = { runtime . checkinCron }
onChange = { ( e ) = > setRuntime ( ( prev ) = > ( { . . . prev , checkinCron : e.target.value } ) ) }
onChange = { ( e ) = > setRuntime ( ( prev ) = > ( { . . . prev , checkinCron : e.target.value } ) ) }
@@ -799,7 +826,7 @@ export default function Settings() {
/ >
/ >
< / div >
< / div >
< div >
< div >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' , marginBottom : 6 } } > 浣 欓 鍒 锋 柊 Cron < / div >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' , marginBottom : 6 } } > 余 额 刷 新 Cron < / div >
< input
< input
value = { runtime . balanceRefreshCron }
value = { runtime . balanceRefreshCron }
onChange = { ( e ) = > setRuntime ( ( prev ) = > ( { . . . prev , balanceRefreshCron : e.target.value } ) ) }
onChange = { ( e ) = > setRuntime ( ( prev ) = > ( { . . . prev , balanceRefreshCron : e.target.value } ) ) }
@@ -809,7 +836,7 @@ export default function Settings() {
< / div >
< / div >
< div style = { { marginTop : 12 } } >
< div style = { { marginTop : 12 } } >
< button onClick = { saveSchedule } disabled = { savingSchedule } className = "btn btn-primary" >
< button onClick = { saveSchedule } disabled = { savingSchedule } className = "btn btn-primary" >
{ savingSchedule ? < > < span className = "spinner spinner-sm" style = { { borderTopColor : 'white' , borderColor : 'rgba(255,255,255,0.3)' } } / > 淇 濆 瓨 涓 ? . . < / > : '淇濆瓨瀹氭椂浠诲姟 ' }
{ savingSchedule ? < > < span className = "spinner spinner-sm" style = { { borderTopColor : 'white' , borderColor : 'rgba(255,255,255,0.3)' } } / > 保 存 中 . . . < / > : '保存定时任务 ' }
< / button >
< / button >
< / div >
< / div >
< / div >
< / div >
@@ -848,7 +875,7 @@ export default function Settings() {
type = "password"
type = "password"
value = { proxyTokenSuffix }
value = { proxyTokenSuffix }
onChange = { ( e ) = > setProxyTokenSuffix ( normalizeProxyTokenSuffix ( e . target . value ) ) }
onChange = { ( e ) = > setProxyTokenSuffix ( normalizeProxyTokenSuffix ( e . target . value ) ) }
placeholder = { tr ( '璇疯緭鍏?sk- 鍚庣殑浠ょ墝鍐呭' ) }
placeholder = "请输入 sk- 后的令牌内容"
style = { {
style = { {
flex : 1 ,
flex : 1 ,
border : 'none' ,
border : 'none' ,
@@ -862,28 +889,28 @@ export default function Settings() {
/ >
/ >
< / div >
< / div >
< button onClick = { saveProxyToken } disabled = { savingToken } className = "btn btn-primary" >
< button onClick = { saveProxyToken } disabled = { savingToken } className = "btn btn-primary" >
{ savingToken ? < > < span className = "spinner spinner-sm" style = { { borderTopColor : 'white' , borderColor : 'rgba(255,255,255,0.3)' } } / > 淇 濆 瓨 涓 ? . . < / > : tr ( '鏇存柊涓嬫父璁块棶浠ょ墝' ) }
{ savingToken ? < > < span className = "spinner spinner-sm" style = { { borderTopColor : 'white' , borderColor : 'rgba(255,255,255,0.3)' } } / > 保 存 中 . . . < / > : '更新下游访问令牌' }
< / button >
< / button >
< / div >
< / div >
< div className = "card animate-slide-up stagger-4" style = { { padding : 20 } } >
< div className = "card animate-slide-up stagger-4" style = { { padding : 20 } } >
< div style = { { fontWeight : 600 , fontSize : 14 , marginBottom : 8 } } > 下 游 API Key 策 略 ( 按 项 目 / 分 组 ) < / div >
< div style = { { fontWeight : 600 , fontSize : 14 , marginBottom : 8 } } > 下 游 API Key 策 略 ( 按 项 目 / 分 组 ) < / div >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' , marginBottom : 12 } } >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' , marginBottom : 12 } } >
姣 忎 釜 涓 嬫 父 Key 鍙 嫭 绔 嬮 厤 缃 繃 鏈 熴 € 侀 搴 ︼ 紝 骞 堕 € 氳 繃 鍕 鹃 € 夌 晫 闈 ㈤ 檺 鍒 跺 彲 璁 块 棶 鐨 勬 ā 鍨 嬩 笌 缇 ょ 粍 銆 ?
每 个 下 游 Key 可 独 立 配 置 过 期 、 额 度 , 并 通 过 勾 选 界 面 限 制 可 访 问 的 模 型 与 群 组 。
< / div >
< / div >
< div style = { { display : 'flex' , gap : 8 , flexWrap : 'wrap' , marginBottom : 12 } } >
< div style = { { display : 'flex' , gap : 8 , flexWrap : 'wrap' , marginBottom : 12 } } >
< button onClick = { openCreateDownstreamModal } className = "btn btn-primary" >
< button onClick = { openCreateDownstreamModal } className = "btn btn-primary" >
+ 鏂 板 API Key
+ 新 增 API Key
< / button >
< / button >
< button onClick = { loadDownstreamKeys } disabled = { downstreamLoading } className = "btn btn-ghost" style = { { border : '1px solid var(--color-border)' } } >
< button onClick = { loadDownstreamKeys } disabled = { downstreamLoading } className = "btn btn-ghost" style = { { border : '1px solid var(--color-border)' } } >
{ downstreamLoading ? '鍒锋柊涓? ..' : '鍒锋柊鍒楄〃 ' }
{ downstreamLoading ? '刷新中. ..' : '刷新列表 ' }
< / button >
< / button >
< / div >
< / div >
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 8 } } >
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 8 } } >
{ downstreamKeys . length === 0 ? (
{ downstreamKeys . length === 0 ? (
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' } } > 鏆 傛 棤 涓 嬫 父 API Key < / div >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' } } > 暂 无 下 游 API Key < / div >
) : downstreamKeys . map ( ( item ) = > {
) : downstreamKeys . map ( ( item ) = > {
const opLoading = ! ! downstreamOps [ item . id ] ;
const opLoading = ! ! downstreamOps [ item . id ] ;
const quotaText = ` ${ item . usedRequests } ${ item . maxRequests !== null ? ` / ${ item . maxRequests } ` : '' } ` ;
const quotaText = ` ${ item . usedRequests } ${ item . maxRequests !== null ? ` / ${ item . maxRequests } ` : '' } ` ;
@@ -901,30 +928,30 @@ export default function Settings() {
background : item.enabled ? 'var(--color-success-bg)' : 'var(--color-danger-bg)' ,
background : item.enabled ? 'var(--color-success-bg)' : 'var(--color-danger-bg)' ,
color : item.enabled ? 'var(--color-success)' : 'var(--color-danger)' ,
color : item.enabled ? 'var(--color-success)' : 'var(--color-danger)' ,
} } >
} } >
{ item . enabled ? '鍚敤 ' : '绂佺敤 ' }
{ item . enabled ? '启用 ' : '禁用 ' }
< / span >
< / span >
< / div >
< / div >
< div style = { { display : 'flex' , gap : 8 , alignItems : 'center' , flexWrap : 'wrap' } } >
< div style = { { display : 'flex' , gap : 8 , alignItems : 'center' , flexWrap : 'wrap' } } >
< button onClick = { ( ) = > beginEditDownstream ( item ) } className = "btn btn-ghost" style = { { border : '1px solid var(--color-border)' } } >
< button onClick = { ( ) = > beginEditDownstream ( item ) } className = "btn btn-ghost" style = { { border : '1px solid var(--color-border)' } } >
缂 栬 緫
编 辑
< / button >
< / button >
< button onClick = { ( ) = > toggleDownstreamEnabled ( item ) } disabled = { opLoading } className = "btn btn-ghost" style = { { border : '1px solid var(--color-border)' } } >
< button onClick = { ( ) = > toggleDownstreamEnabled ( item ) } disabled = { opLoading } className = "btn btn-ghost" style = { { border : '1px solid var(--color-border)' } } >
{ item . enabled ? '绂佺敤 ' : '鍚敤 ' }
{ item . enabled ? '禁用 ' : '启用 ' }
< / button >
< / button >
< button onClick = { ( ) = > resetDownstreamUsage ( item ) } disabled = { opLoading } className = "btn btn-ghost" style = { { border : '1px solid var(--color-border)' } } >
< button onClick = { ( ) = > resetDownstreamUsage ( item ) } disabled = { opLoading } className = "btn btn-ghost" style = { { border : '1px solid var(--color-border)' } } >
閲 嶇 疆 鐢 ㄩ 噺
重 置 用 量
< / button >
< / button >
< button onClick = { ( ) = > deleteDownstreamKey ( item ) } disabled = { opLoading } className = "btn btn-link btn-link-warning" >
< button onClick = { ( ) = > deleteDownstreamKey ( item ) } disabled = { opLoading } className = "btn btn-link btn-link-warning" >
鍒 犻 櫎
删 除
< / button >
< / button >
< / div >
< / div >
< / div >
< / div >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' , marginTop : 6 , display : 'flex' , gap : 14 , flexWrap : 'wrap' } } >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' , marginTop : 6 , display : 'flex' , gap : 14 , flexWrap : 'wrap' } } >
< span > 璇 锋 眰 鐢 ㄩ 噺 : { quotaText } < / span >
< span > 请 求 用 量 : { quotaText } < / span >
< span > 璐 圭 敤 鐢 ㄩ 噺 : { costText } < / span >
< span > 费 用 用 量 : { costText } < / span >
< span > 杩 囨 湡 : { item . expiresAt ? new Date ( item . expiresAt ) . toLocaleString ( ) : '姘镐箙 ' } < / span >
< span > 过 期 : { item . expiresAt ? new Date ( item . expiresAt ) . toLocaleString ( ) : '永久 ' } < / span >
< span > 妯 ″ 瀷 瑙 勫 垯 : { item . supportedModels . length || 0 } < / span >
< span > 模 型 规 则 : { item . supportedModels . length || 0 } < / span >
< span > 缇 ょ 粍 闄 愬 埗 : { item . allowedRouteIds . length || 0 } < / span >
< span > 群 组 限 制 : { item . allowedRouteIds . length || 0 } < / span >
< / div >
< / div >
< / div >
< / div >
) ;
) ;
@@ -933,13 +960,13 @@ export default function Settings() {
< / div >
< / div >
< div className = "card animate-slide-up stagger-5" style = { { padding : 20 } } >
< div className = "card animate-slide-up stagger-5" style = { { padding : 20 } } >
< div style = { { fontWeight : 600 , fontSize : 14 , marginBottom : 10 } } > 璺 敱 绛 栫 暐 < / div >
< div style = { { fontWeight : 600 , fontSize : 14 , marginBottom : 10 } } > 路 由 策 略 < / div >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' , marginBottom : 12 } } >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' , marginBottom : 12 } } >
鍏 堥 € 夋 嫨 棰 勮 绛 栫 暐 锛 屽 彧 鏈 夐 渶 瑕 佺 簿 璋 冩 椂 鍐 嶅 睍 寮 € 楂 樼 骇 鍙 傛 暟 銆 ?
先 选 择 预 设 策 略 , 只 有 需 要 精 调 时 再 展 开 高 级 参 数 。
< / div >
< / div >
< div style = { { marginBottom : 12 , maxWidth : 280 } } >
< div style = { { marginBottom : 12 , maxWidth : 280 } } >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' , marginBottom : 6 } } >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' , marginBottom : 6 } } >
鏃 犲 疄 娴 ? 閰 嶇 疆 / 鐩 綍 浠 锋 椂 榛 樿 鍗 曚 环
无 实 测 / 配 置 / 目 录 价 时 默 认 单 价
< / div >
< / div >
< input
< input
type = "number"
type = "number"
@@ -965,7 +992,7 @@ export default function Settings() {
color : activeRoutingProfile === 'balanced' ? 'var(--color-primary)' : undefined ,
color : activeRoutingProfile === 'balanced' ? 'var(--color-primary)' : undefined ,
} }
} }
>
>
鍧 囪
均 衡
< / button >
< / button >
< button
< button
onClick = { ( ) = > applyRoutingPreset ( 'stable' ) }
onClick = { ( ) = > applyRoutingPreset ( 'stable' ) }
@@ -975,7 +1002,7 @@ export default function Settings() {
color : activeRoutingProfile === 'stable' ? 'var(--color-primary)' : undefined ,
color : activeRoutingProfile === 'stable' ? 'var(--color-primary)' : undefined ,
} }
} }
>
>
绋 冲 畾 浼 樺 厛
稳 定 优 先
< / button >
< / button >
< button
< button
onClick = { ( ) = > applyRoutingPreset ( 'cost' ) }
onClick = { ( ) = > applyRoutingPreset ( 'cost' ) }
@@ -985,14 +1012,14 @@ export default function Settings() {
color : activeRoutingProfile === 'cost' ? 'var(--color-primary)' : undefined ,
color : activeRoutingProfile === 'cost' ? 'var(--color-primary)' : undefined ,
} }
} }
>
>
鎴 愭 湰 浼 樺 厛
成 本 优 先
< / button >
< / button >
< button
< button
onClick = { ( ) = > setShowAdvancedRouting ( ( prev ) = > ! prev ) }
onClick = { ( ) = > setShowAdvancedRouting ( ( prev ) = > ! prev ) }
className = "btn btn-ghost"
className = "btn btn-ghost"
style = { { border : '1px solid var(--color-border)' } }
style = { { border : '1px solid var(--color-border)' } }
>
>
{ showAdvancedRouting ? '鏀惰捣楂樼骇鍙傛暟' : '灞曞紑楂樼骇鍙傛暟 '}
{ showAdvancedRouting ? '收起高级参数' : '展开高级参数 '}
< / button >
< / button >
< / div >
< / div >
@@ -1000,11 +1027,11 @@ export default function Settings() {
< div className = "anim-collapse-inner" style = { { paddingTop : 2 } } >
< div className = "anim-collapse-inner" style = { { paddingTop : 2 } } >
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 12 } } >
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 12 } } >
{ ( [
{ ( [
[ 'baseWeightFactor' , '鍩虹鏉冮噸鍥犲瓙 ' ] ,
[ 'baseWeightFactor' , '基础权重因子 ' ] ,
[ 'valueScoreFactor' , '浠峰€煎垎鍥犲瓙 ' ] ,
[ 'valueScoreFactor' , '价值分因子 ' ] ,
[ 'costWeight' , '鎴愭湰鏉冮噸 ' ] ,
[ 'costWeight' , '成本权重 ' ] ,
[ 'balanceWeight' , '浣欓鏉冮噸 ' ] ,
[ 'balanceWeight' , '余额权重 ' ] ,
[ 'usageWeight' , '浣跨敤棰戞鏉冮噸 ' ] ,
[ 'usageWeight' , '使用频次权重 ' ] ,
] as Array < [ keyof RoutingWeights , string ] > ) . map ( ( [ key , label ] ) = > (
] as Array < [ keyof RoutingWeights , string ] > ) . map ( ( [ key , label ] ) = > (
< div key = { key } >
< div key = { key } >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' , marginBottom : 6 } } > { label } < / div >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' , marginBottom : 6 } } > { label } < / div >
@@ -1033,7 +1060,7 @@ export default function Settings() {
< div style = { { marginTop : 12 } } >
< div style = { { marginTop : 12 } } >
< button onClick = { saveRouting } disabled = { savingRouting } className = "btn btn-primary" >
< button onClick = { saveRouting } disabled = { savingRouting } className = "btn btn-primary" >
{ savingRouting ? < > < span className = "spinner spinner-sm" style = { { borderTopColor : 'white' , borderColor : 'rgba(255,255,255,0.3)' } } / > 淇 濆 瓨 涓 ? . . < / > : '淇濆瓨璺敱绛栫暐 ' }
{ savingRouting ? < > < span className = "spinner spinner-sm" style = { { borderTopColor : 'white' , borderColor : 'rgba(255,255,255,0.3)' } } / > 保 存 中 . . . < / > : '保存路由策略 ' }
< / button >
< / button >
< / div >
< / div >
< / div >
< / div >
@@ -1198,7 +1225,7 @@ export default function Settings() {
< / div >
< / div >
< div className = "card animate-slide-up stagger-6" style = { { padding : 20 } } >
< div className = "card animate-slide-up stagger-6" style = { { padding : 20 } } >
< div style = { { fontWeight : 600 , fontSize : 14 , marginBottom : 12 } } > 缁 存 姢 宸 ュ 叿 < / div >
< div style = { { fontWeight : 600 , fontSize : 14 , marginBottom : 12 } } > 维 护 工 具 < / div >
< div style = { { display : 'flex' , gap : 8 , flexWrap : 'wrap' } } >
< div style = { { display : 'flex' , gap : 8 , flexWrap : 'wrap' } } >
< button onClick = { handleClearCache } disabled = { clearingCache } className = "btn btn-ghost" style = { { border : '1px solid var(--color-border)' } } >
< button onClick = { handleClearCache } disabled = { clearingCache } className = "btn btn-ghost" style = { { border : '1px solid var(--color-border)' } } >
{ clearingCache ? < > < span className = "spinner spinner-sm" / > 清 理 中 . . . < / > : '清除缓存并重建路由' }
{ clearingCache ? < > < span className = "spinner spinner-sm" / > 清 理 中 . . . < / > : '清除缓存并重建路由' }
@@ -1212,24 +1239,24 @@ export default function Settings() {
< div className = "card animate-slide-up stagger-7" style = { { padding : 20 } } >
< div className = "card animate-slide-up stagger-7" style = { { padding : 20 } } >
< div style = { { fontWeight : 600 , fontSize : 14 , marginBottom : 10 } } > 会 话 与 安 全 < / div >
< div style = { { fontWeight : 600 , fontSize : 14 , marginBottom : 10 } } > 会 话 与 安 全 < / div >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' , marginBottom : 12 } } >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' , marginBottom : 12 } } >
鐧 诲 綍 浼 氳 瘽 榛 樿 12 灏 忔 椂 鑷 姩 杩 囨 湡 銆 傚 彲 閫 夐 厤 缃 鐞 嗙 IP 鐧 藉 悕 鍗 曪 紙 姣 忚 涓 € 涓 ? IP锛夈 € ?
登 录 会 话 默 认 12 小 时 自 动 过 期 。 可 选 配 置 管 理 端 IP 白 名 单 ( 每 行 一 个 IP ) 。
< / div >
< / div >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' , marginBottom : 6 } } >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' , marginBottom : 6 } } >
当 前 识 别 到 的 管 理 端 IP ( 由 服 务 端 判 定 ) :
当 前 识 别 到 的 管 理 端 IP ( 由 服 务 端 判 定 ) :
< / div >
< / div >
< code style = { { display : 'block' , padding : '10px 14px' , background : 'var(--color-bg)' , borderRadius : 'var(--radius-sm)' , fontSize : 13 , fontFamily : 'var(--font-mono)' , color : 'var(--color-text-secondary)' , border : '1px solid var(--color-border-light)' , marginBottom : 10 } } >
< code style = { { display : 'block' , padding : '10px 14px' , background : 'var(--color-bg)' , borderRadius : 'var(--radius-sm)' , fontSize : 13 , fontFamily : 'var(--font-mono)' , color : 'var(--color-text-secondary)' , border : '1px solid var(--color-border-light)' , marginBottom : 10 } } >
{ runtime . currentAdminIp || tr ( '鏈煡' ) }
{ runtime . currentAdminIp || '未知' }
< / code >
< / code >
< textarea
< textarea
value = { adminIpAllowlistText }
value = { adminIpAllowlistText }
onChange = { ( e ) = > setAdminIpAllowlistText ( e . target . value ) }
onChange = { ( e ) = > setAdminIpAllowlistText ( e . target . value ) }
placeholder = { '渚嬪锛歕 n127.0.0.1\n192.168.1.10' }
placeholder = { '例如:\ n127.0.0.1\n192.168.1.10' }
rows = { 4 }
rows = { 4 }
style = { { . . . inputStyle , fontFamily : 'var(--font-mono)' , resize : 'vertical' , marginBottom : 10 } }
style = { { . . . inputStyle , fontFamily : 'var(--font-mono)' , resize : 'vertical' , marginBottom : 10 } }
/ >
/ >
< div style = { { display : 'flex' , gap : 8 , flexWrap : 'wrap' } } >
< div style = { { display : 'flex' , gap : 8 , flexWrap : 'wrap' } } >
< button onClick = { saveSecuritySettings } disabled = { savingSecurity } className = "btn btn-primary" >
< button onClick = { saveSecuritySettings } disabled = { savingSecurity } className = "btn btn-primary" >
{ savingSecurity ? < > < span className = "spinner spinner-sm" style = { { borderTopColor : 'white' , borderColor : 'rgba(255,255,255,0.3)' } } / > 淇 濆 瓨 涓 ? . . < / > : '淇濆瓨瀹夊叏璁剧疆 ' }
{ savingSecurity ? < > < span className = "spinner spinner-sm" style = { { borderTopColor : 'white' , borderColor : 'rgba(255,255,255,0.3)' } } / > 保 存 中 . . . < / > : '保存安全设置 ' }
< / button >
< / button >
< button
< button
onClick = { ( ) = > {
onClick = { ( ) = > {
@@ -1238,7 +1265,7 @@ export default function Settings() {
} }
} }
className = "btn btn-danger"
className = "btn btn-danger"
>
>
閫 € 鍑 虹 櫥 褰 ?
退 出 登 录
< / button >
< / button >
< / div >
< / div >
< / div >
< / div >
@@ -1252,7 +1279,7 @@ export default function Settings() {
onClick = { ( e ) = > e . stopPropagation ( ) }
onClick = { ( e ) = > e . stopPropagation ( ) }
>
>
< div className = "modal-header" >
< div className = "modal-header" >
{ editingDownstreamId ? '缂栬緫涓嬫父 API Key' : '鏂板涓嬫父 API Key' }
{ editingDownstreamId ? '编辑下游 API Key' : '新增下游 API Key' }
< / div >
< / div >
< div className = "modal-body" style = { { display : 'flex' , flexDirection : 'column' , gap : 12 } } >
< div className = "modal-body" style = { { display : 'flex' , flexDirection : 'column' , gap : 12 } } >
< div style = { { display : 'grid' , gridTemplateColumns : 'repeat(auto-fit, minmax(260px, 1fr))' , gap : 10 } } >
< div style = { { display : 'grid' , gridTemplateColumns : 'repeat(auto-fit, minmax(260px, 1fr))' , gap : 10 } } >
@@ -1271,7 +1298,7 @@ export default function Settings() {
< input
< input
value = { downstreamCreate . maxCost }
value = { downstreamCreate . maxCost }
onChange = { ( e ) = > setDownstreamCreate ( ( prev ) = > ( { . . . prev , maxCost : e.target.value } ) ) }
onChange = { ( e ) = > setDownstreamCreate ( ( prev ) = > ( { . . . prev , maxCost : e.target.value } ) ) }
placeholder = "鏈€澶ц垂鐢紙鍙€夛級 "
placeholder = "最大费用(可选) "
type = "number"
type = "number"
min = { 0 }
min = { 0 }
step = { 0.000001 }
step = { 0.000001 }
@@ -1280,7 +1307,7 @@ export default function Settings() {
< input
< input
value = { downstreamCreate . maxRequests }
value = { downstreamCreate . maxRequests }
onChange = { ( e ) = > setDownstreamCreate ( ( prev ) = > ( { . . . prev , maxRequests : e.target.value } ) ) }
onChange = { ( e ) = > setDownstreamCreate ( ( prev ) = > ( { . . . prev , maxRequests : e.target.value } ) ) }
placeholder = "鏈€澶ц姹傛暟锛堝彲閫夛級 "
placeholder = "最大请求数(可选) "
type = "number"
type = "number"
min = { 0 }
min = { 0 }
step = { 1 }
step = { 1 }
@@ -1290,20 +1317,20 @@ export default function Settings() {
value = { downstreamCreate . expiresAt }
value = { downstreamCreate . expiresAt }
onChange = { ( e ) = > setDownstreamCreate ( ( prev ) = > ( { . . . prev , expiresAt : e.target.value } ) ) }
onChange = { ( e ) = > setDownstreamCreate ( ( prev ) = > ( { . . . prev , expiresAt : e.target.value } ) ) }
type = "datetime-local"
type = "datetime-local"
placeholder = "杩囨湡鏃堕棿锛堝彲閫夛級 "
placeholder = "过期时间(可选) "
style = { inputStyle }
style = { inputStyle }
/ >
/ >
< input
< input
value = { downstreamCreate . description }
value = { downstreamCreate . description }
onChange = { ( e ) = > setDownstreamCreate ( ( prev ) = > ( { . . . prev , description : e.target.value } ) ) }
onChange = { ( e ) = > setDownstreamCreate ( ( prev ) = > ( { . . . prev , description : e.target.value } ) ) }
placeholder = "澶囨敞锛堝彲閫夛級 "
placeholder = "备注(可选) "
style = { inputStyle }
style = { inputStyle }
/ >
/ >
< / div >
< / div >
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 8 } } >
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 8 } } >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' } } >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' } } >
宸 查 € 夋 ā 鍨 ? { downstreamCreate . selectedModels . length } 涓 紝 宸 查 € 夌 兢 缁 ? { downstreamCreate . selectedGroupRouteIds . length } 涓 ?
已 选 模 型 { downstreamCreate . selectedModels . length } 个 , 已 选 群 组 { downstreamCreate . selectedGroupRouteIds . length } 个
< / div >
< / div >
< div style = { { display : 'flex' , gap : 8 , flexWrap : 'wrap' } } >
< div style = { { display : 'flex' , gap : 8 , flexWrap : 'wrap' } } >
< button
< button
@@ -1316,25 +1343,25 @@ export default function Settings() {
className = "btn btn-ghost"
className = "btn btn-ghost"
style = { { border : '1px solid var(--color-border)' } }
style = { { border : '1px solid var(--color-border)' } }
>
>
鍕 鹃 € 夋 ā 鍨 嬪 拰 缇 ょ 粍
勾 选 模 型 和 群 组
< / button >
< / button >
{ ( downstreamCreate . selectedModels . length > 0 || downstreamCreate . selectedGroupRouteIds . length > 0 ) && (
{ ( downstreamCreate . selectedModels . length > 0 || downstreamCreate . selectedGroupRouteIds . length > 0 ) && (
< button
< button
onClick = { ( ) = > setDownstreamCreate ( ( prev ) = > ( { . . . prev , selectedModels : [ ] , selectedGroupRouteIds : [ ] } ) ) }
onClick = { ( ) = > setDownstreamCreate ( ( prev ) = > ( { . . . prev , selectedModels : [ ] , selectedGroupRouteIds : [ ] } ) ) }
className = "btn btn-link btn-link-warning"
className = "btn btn-link btn-link-warning"
>
>
娓 呯 ┖ 閫 夋 嫨
清 空 选 择
< / button >
< / button >
) }
) }
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< div className = "modal-footer" >
< div className = "modal-footer" >
< button onClick = { closeDownstreamModal } className = "btn btn-ghost" > 鍙 栨 秷 < / button >
< button onClick = { closeDownstreamModal } className = "btn btn-ghost" > 取 消 < / button >
< button onClick = { saveDownstreamKey } disabled = { downstreamSaving } className = "btn btn-primary" >
< button onClick = { saveDownstreamKey } disabled = { downstreamSaving } className = "btn btn-primary" >
{ downstreamSaving
{ downstreamSaving
? < > < span className = "spinner spinner-sm" style = { { borderTopColor : 'white' , borderColor : 'rgba(255,255,255,0.3)' } } / > 淇 濆 瓨 涓 ? . . < / >
? < > < span className = "spinner spinner-sm" style = { { borderTopColor : 'white' , borderColor : 'rgba(255,255,255,0.3)' } } / > 保 存 中 . . . < / >
: ( editingDownstreamId ? '鏇存柊 API Key' : '鏂板 API Key' ) }
: ( editingDownstreamId ? '更新 API Key' : '新增 API Key' ) }
< / button >
< / button >
< / div >
< / div >
< / div >
< / div >
@@ -1350,28 +1377,28 @@ export default function Settings() {
style = { { maxWidth : 860 } }
style = { { maxWidth : 860 } }
onClick = { ( e ) = > e . stopPropagation ( ) }
onClick = { ( e ) = > e . stopPropagation ( ) }
>
>
< div className = "modal-header" > 鍕 鹃 € 夋 ā 鍨 嬪 拰 缇 ょ 粍 < / div >
< div className = "modal-header" > 勾 选 模 型 和 群 组 < / div >
< div className = "modal-body" style = { { display : 'flex' , flexDirection : 'column' , gap : 12 } } >
< div className = "modal-body" style = { { display : 'flex' , flexDirection : 'column' , gap : 12 } } >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' } } >
< div style = { { fontSize : 12 , color : 'var(--color-text-muted)' } } >
閫 夋 嫨 缁 撴 灉 浼 氫 繚 瀛 樺 埌 褰 撳 墠 涓 嬫 父 API Key锛氱簿纭 ā 鍨 嬬 敤 浜 庢 ā 鍨 嬬 櫧 鍚 嶅 崟 锛 岀 兢 缁 勭 敤 浜 庤 矾 鐢 辫 寖 鍥 撮 檺 鍒 躲 € ?
选 择 结 果 会 保 存 到 当 前 下 游 API Key : 精 确 模 型 用 于 模 型 白 名 单 , 群 组 用 于 路 由 范 围 限 制 。
< / div >
< / div >
{ selectorLoading ? (
{ selectorLoading ? (
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 , fontSize : 13 , color : 'var(--color-text-muted)' } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 , fontSize : 13 , color : 'var(--color-text-muted)' } } >
< span className = "spinner spinner-sm" / >
< span className = "spinner spinner-sm" / >
鍔 犺 浇 璺 敱 涓 ? . .
加 载 路 由 中 . . .
< / div >
< / div >
) : (
) : (
< div style = { { display : 'grid' , gridTemplateColumns : 'repeat(auto-fit, minmax(260px, 1fr))' , gap : 12 } } >
< div style = { { display : 'grid' , gridTemplateColumns : 'repeat(auto-fit, minmax(260px, 1fr))' , gap : 12 } } >
< div style = { { border : '1px solid var(--color-border-light)' , borderRadius : 'var(--radius-sm)' , padding : 10 } } >
< div style = { { border : '1px solid var(--color-border-light)' , borderRadius : 'var(--radius-sm)' , padding : 10 } } >
< div style = { { fontSize : 13 , fontWeight : 600 , marginBottom : 8 } } >
< div style = { { fontSize : 13 , fontWeight : 600 , marginBottom : 8 } } >
绮 剧 ‘ 妯 ″ 瀷 ( { selectorModelSearch . trim ( )
精 确 模 型 ( { selectorModelSearch . trim ( )
? ` ${ filteredExactModelOptions . length } / ${ exactModelOptions . length } `
? ` ${ filteredExactModelOptions . length } / ${ exactModelOptions . length } `
: exactModelOptions . length } )
: exactModelOptions . length } )
< / div >
< / div >
< input
< input
value = { selectorModelSearch }
value = { selectorModelSearch }
onChange = { ( e ) = > setSelectorModelSearch ( e . target . value ) }
onChange = { ( e ) = > setSelectorModelSearch ( e . target . value ) }
placeholder = "鎼滅储绮剧‘妯″瀷锛堟敮鎸佹ā绯婂尮閰嶏級 "
placeholder = "搜索精确模型(支持模糊匹配) "
style = { { . . . inputStyle , padding : '8px 10px' , fontSize : 12 , marginBottom : 8 } }
style = { { . . . inputStyle , padding : '8px 10px' , fontSize : 12 , marginBottom : 8 } }
/ >
/ >
< div style = { { maxHeight : 280 , overflowY : 'auto' , display : 'flex' , flexDirection : 'column' , gap : 8 } } >
< div style = { { maxHeight : 280 , overflowY : 'auto' , display : 'flex' , flexDirection : 'column' , gap : 8 } } >
@@ -1458,7 +1485,7 @@ export default function Settings() {
< div style = { { border : '1px solid var(--color-border-light)' , borderRadius : 'var(--radius-sm)' , padding : 10 } } >
< div style = { { border : '1px solid var(--color-border-light)' , borderRadius : 'var(--radius-sm)' , padding : 10 } } >
< div style = { { fontSize : 13 , fontWeight : 600 , marginBottom : 8 } } >
< div style = { { fontSize : 13 , fontWeight : 600 , marginBottom : 8 } } >
缇 ょ 粍 ( { selectorGroupSearch . trim ( )
群 组 ( { selectorGroupSearch . trim ( )
? ` ${ filteredGroupRouteOptions . length } / ${ groupRouteOptions . length } `
? ` ${ filteredGroupRouteOptions . length } / ${ groupRouteOptions . length } `
: groupRouteOptions . length } )
: groupRouteOptions . length } )
< / div >
< / div >
@@ -1557,7 +1584,7 @@ export default function Settings() {
color : 'var(--color-danger)' ,
color : 'var(--color-danger)' ,
} }
} }
>
>
宸 茬 鐢 ?
已 禁 用
< / span >
< / span >
) }
) }
< / code >
< / code >
@@ -1574,7 +1601,7 @@ export default function Settings() {
) }
) }
< / div >
< / div >
< div className = "modal-footer" >
< div className = "modal-footer" >
< button onClick = { closeSelectorModal } className = "btn btn-ghost" > 鍏 抽 棴 < / button >
< button onClick = { closeSelectorModal } className = "btn btn-ghost" > 关 闭 < / button >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >