@@ -8,11 +8,13 @@ import {
88 DatabaseOutlined ,
99 DeleteOutlined ,
1010 DesktopOutlined ,
11+ EditOutlined ,
1112 EyeInvisibleOutlined ,
1213 FileTextOutlined ,
1314 InfoCircleOutlined ,
1415 MoreOutlined ,
1516 PoweroffOutlined ,
17+ PlusOutlined ,
1618 RollbackOutlined ,
1719 SaveOutlined ,
1820 SwapOutlined ,
@@ -49,7 +51,6 @@ import appLogo from "../../assets/aigate_1024_1024.png";
4951import { UpdateCard } from "../updates/UpdateCard" ;
5052
5153const { Text, Title } = Typography ;
52- const { TextArea } = Input ;
5354
5455type SettingsTabKey = "general" | "proxy" | "advanced" | "about" ;
5556
@@ -98,6 +99,20 @@ function triggerTextDownload(filename: string, content: string) {
9899 URL . revokeObjectURL ( url ) ;
99100}
100101
102+ function parseWhitelistEntries ( raw : string ) : string [ ] {
103+ return raw
104+ . split ( / [ \n \r , ; ] + / )
105+ . map ( ( entry ) => entry . trim ( ) )
106+ . filter ( ( entry ) => entry . length > 0 ) ;
107+ }
108+
109+ function stringifyWhitelistEntries ( entries : string [ ] ) : string {
110+ return entries
111+ . map ( ( entry ) => entry . trim ( ) )
112+ . filter ( ( entry ) => entry . length > 0 )
113+ . join ( "\n" ) ;
114+ }
115+
101116function validateExchangePayload ( raw : string ) : void {
102117 let payload : { format ?: string ; version ?: number } ;
103118 try {
@@ -176,6 +191,9 @@ export function SettingsPage({
176191 const [ importingSQL , setImportingSQL ] = useState ( false ) ;
177192 const [ importModalOpen , setImportModalOpen ] = useState ( false ) ;
178193 const [ importFile , setImportFile ] = useState < File | null > ( null ) ;
194+ const [ whitelistModalOpen , setWhitelistModalOpen ] = useState ( false ) ;
195+ const [ whitelistModalValue , setWhitelistModalValue ] = useState ( "" ) ;
196+ const [ editingWhitelistIndex , setEditingWhitelistIndex ] = useState < number | null > ( null ) ;
179197 const [ activeTab , setActiveTab ] = useState < SettingsTabKey > ( initialTab ) ;
180198
181199 useEffect ( ( ) => {
@@ -185,6 +203,7 @@ export function SettingsPage({
185203 account_pricing : initialSettings . account_pricing ?? { } ,
186204 usage_request_timeout_seconds : initialSettings . usage_request_timeout_seconds ?? 15 ,
187205 lan_share_enabled : initialSettings . lan_share_enabled ?? false ,
206+ lan_share_whitelist_enabled : initialSettings . lan_share_whitelist_enabled ?? false ,
188207 lan_share_ip_whitelist : initialSettings . lan_share_ip_whitelist ?? "" ,
189208 upstream_proxy_mode : initialSettings . upstream_proxy_mode ?? "system" ,
190209 upstream_proxy_url : initialSettings . upstream_proxy_url ?? "" ,
@@ -242,6 +261,71 @@ export function SettingsPage({
242261 } ) ) ;
243262 }
244263
264+ const whitelistEntries = parseWhitelistEntries ( draftSettings . lan_share_ip_whitelist ) ;
265+ const whitelistEnabled = draftSettings . lan_share_whitelist_enabled ?? false ;
266+
267+ function updateWhitelistEntries ( entries : string [ ] ) {
268+ updateDraft ( { lan_share_ip_whitelist : stringifyWhitelistEntries ( entries ) } ) ;
269+ }
270+
271+ function openCreateWhitelistModal ( ) {
272+ setEditingWhitelistIndex ( null ) ;
273+ setWhitelistModalValue ( "" ) ;
274+ setWhitelistModalOpen ( true ) ;
275+ }
276+
277+ function openEditWhitelistModal ( index : number ) {
278+ setEditingWhitelistIndex ( index ) ;
279+ setWhitelistModalValue ( whitelistEntries [ index ] ?? "" ) ;
280+ setWhitelistModalOpen ( true ) ;
281+ }
282+
283+ function closeWhitelistModal ( ) {
284+ setWhitelistModalOpen ( false ) ;
285+ setWhitelistModalValue ( "" ) ;
286+ setEditingWhitelistIndex ( null ) ;
287+ }
288+
289+ function submitWhitelistModal ( ) {
290+ const nextValue = whitelistModalValue . trim ( ) ;
291+ if ( ! nextValue ) {
292+ void messageApi . error ( t ( "请输入白名单 IP 或 CIDR" ) ) ;
293+ return ;
294+ }
295+
296+ const nextEntries = [ ...whitelistEntries ] ;
297+ const duplicateIndex = nextEntries . findIndex ( ( entry , index ) => entry === nextValue && index !== editingWhitelistIndex ) ;
298+ if ( duplicateIndex >= 0 ) {
299+ void messageApi . error ( t ( "该白名单条目已存在" ) ) ;
300+ return ;
301+ }
302+
303+ if ( editingWhitelistIndex === null ) {
304+ nextEntries . push ( nextValue ) ;
305+ } else {
306+ nextEntries [ editingWhitelistIndex ] = nextValue ;
307+ }
308+ updateWhitelistEntries ( nextEntries ) ;
309+ closeWhitelistModal ( ) ;
310+ }
311+
312+ function handleDeleteWhitelistEntry ( index : number ) {
313+ const value = whitelistEntries [ index ] ;
314+ if ( ! value ) {
315+ return ;
316+ }
317+ Modal . confirm ( {
318+ title : t ( "删除白名单" ) ,
319+ content : t ( "确认删除该白名单条目?" ) ,
320+ okText : t ( "删除" ) ,
321+ cancelText : t ( "取消" ) ,
322+ okButtonProps : { danger : true , "aria-label" : t ( "确认删除白名单" ) } as any ,
323+ onOk : ( ) => {
324+ updateWhitelistEntries ( whitelistEntries . filter ( ( _ , itemIndex ) => itemIndex !== index ) ) ;
325+ } ,
326+ } ) ;
327+ }
328+
245329 function updateProviderPricing ( providerType : string , field : "input_per_million" | "output_per_million" , value : number | null ) {
246330 setDraftSettings ( ( current ) => ( {
247331 ...current ,
@@ -585,22 +669,66 @@ export function SettingsPage({
585669 className = "settings-number"
586670 />
587671 </ label >
588- < label className = "settings-field" >
589- < span className = "settings-field-label" > { t ( "IP 白名单" ) } </ span >
590- < TextArea
591- aria-label = { t ( "IP 白名单" ) }
592- value = { draftSettings . lan_share_ip_whitelist }
593- onChange = { ( event ) => updateDraft ( { lan_share_ip_whitelist : event . target . value } ) }
594- placeholder = { `192.168.1.10\n192.168.1.0/24` }
595- autoSize = { { minRows : 4 , maxRows : 6 } }
596- />
597- < Text type = "secondary" >
598- { t ( "每行一个 IP 或 CIDR。留空表示允许所有局域网来源;本机 127.0.0.1 / ::1 始终放行。" ) }
599- </ Text >
600- </ label >
601672 </ div >
602673 </ Card >
603674
675+ { draftSettings . lan_share_enabled ? (
676+ < Card className = "settings-card" variant = "borderless" >
677+ < SectionHeader
678+ icon = { < ControlOutlined /> }
679+ title = { t ( "白名单" ) }
680+ description = { t ( "仅控制局域网共享的远端访问来源;本机 127.0.0.1 / ::1 始终放行。" ) }
681+ actions = {
682+ whitelistEnabled ? (
683+ < Button icon = { < PlusOutlined /> } aria-label = { t ( "新增白名单" ) } onClick = { openCreateWhitelistModal } >
684+ { t ( "新增" ) }
685+ </ Button >
686+ ) : null
687+ }
688+ />
689+ < div className = "settings-stack" >
690+ < ToggleRow
691+ icon = { < ControlOutlined /> }
692+ title = { t ( "是否开启白名单" ) }
693+ description = { t ( "关闭时允许所有局域网来源;开启后只允许列表中的 IP。" ) }
694+ label = { t ( "是否开启白名单" ) }
695+ checked = { whitelistEnabled }
696+ onChange = { ( checked ) => updateDraft ( { lan_share_whitelist_enabled : checked } ) }
697+ />
698+ </ div >
699+ { whitelistEnabled ? (
700+ whitelistEntries . length > 0 ? (
701+ < div className = "settings-whitelist-list" >
702+ { whitelistEntries . map ( ( entry , index ) => (
703+ < div key = { `${ entry } -${ index } ` } className = "settings-whitelist-item" >
704+ < div className = "settings-whitelist-value" > { entry } </ div >
705+ < div className = "settings-whitelist-actions" >
706+ < Button
707+ type = "text"
708+ icon = { < EditOutlined /> }
709+ aria-label = { `${ t ( "编辑白名单" ) } ${ entry } ` }
710+ onClick = { ( ) => openEditWhitelistModal ( index ) }
711+ />
712+ < Button
713+ type = "text"
714+ danger
715+ icon = { < DeleteOutlined /> }
716+ aria-label = { `${ t ( "删除白名单" ) } ${ entry } ` }
717+ onClick = { ( ) => handleDeleteWhitelistEntry ( index ) }
718+ />
719+ </ div >
720+ </ div >
721+ ) ) }
722+ </ div >
723+ ) : (
724+ < div className = "settings-empty" > { t ( "当前未配置任何 IP,保存后将拒绝所有非本机局域网访问。" ) } </ div >
725+ )
726+ ) : (
727+ < Text type = "secondary" > { t ( "关闭白名单时,所有局域网来源都可访问;已配置条目会保留但不会生效。" ) } </ Text >
728+ ) }
729+ </ Card >
730+ ) : null }
731+
604732 < Card className = "settings-card" variant = "borderless" >
605733 < SectionHeader
606734 icon = { < DesktopOutlined /> }
@@ -986,6 +1114,29 @@ export function SettingsPage({
9861114 < Input type = "file" accept = ".json,application/json,text/plain" onChange = { ( event ) => setImportFile ( event . target . files ?. [ 0 ] || null ) } />
9871115 </ div >
9881116 </ Modal >
1117+ < Modal
1118+ open = { whitelistModalOpen }
1119+ title = { editingWhitelistIndex === null ? t ( "新增白名单" ) : t ( "编辑白名单" ) }
1120+ onCancel = { closeWhitelistModal }
1121+ footer = { [
1122+ < Button key = "cancel" onClick = { closeWhitelistModal } >
1123+ { t ( "取消" ) }
1124+ </ Button > ,
1125+ < Button key = "ok" type = "primary" aria-label = { t ( "确认白名单弹窗" ) } onClick = { submitWhitelistModal } >
1126+ { t ( "确认" ) }
1127+ </ Button > ,
1128+ ] }
1129+ >
1130+ < label className = "settings-field" >
1131+ < span className = "settings-field-label" > { t ( "白名单 IP" ) } </ span >
1132+ < Input
1133+ aria-label = { t ( "白名单 IP" ) }
1134+ value = { whitelistModalValue }
1135+ onChange = { ( event ) => setWhitelistModalValue ( event . target . value ) }
1136+ placeholder = "192.168.1.10 或 192.168.1.0/24"
1137+ />
1138+ </ label >
1139+ </ Modal >
9891140 </ div >
9901141 ) ;
9911142}
0 commit comments