@@ -50,35 +50,53 @@ import PageLayout from '@/components/PageLayout';
// 统一按钮样式系统
const buttonStyles = {
// 主要操作按钮(蓝色)- 用于配置、设置、确认等
primary : 'px-3 py-1.5 text-sm font-medium bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded-lg transition-colors' ,
primary :
'px-3 py-1.5 text-sm font-medium bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded-lg transition-colors' ,
// 成功操作按钮(绿色)- 用于添加、启用、保存等
success : 'px-3 py-1.5 text-sm font-medium bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700 text-white rounded-lg transition-colors' ,
success :
'px-3 py-1.5 text-sm font-medium bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700 text-white rounded-lg transition-colors' ,
// 危险操作按钮(红色)- 用于删除、禁用、重置等
danger : 'px-3 py-1.5 text-sm font-medium bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700 text-white rounded-lg transition-colors' ,
danger :
'px-3 py-1.5 text-sm font-medium bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700 text-white rounded-lg transition-colors' ,
// 次要操作按钮(灰色)- 用于取消、关闭等
secondary : 'px-3 py-1.5 text-sm font-medium bg-gray-600 hover:bg-gray-700 dark:bg-gray-600 dark:hover:bg-gray-700 text-white rounded-lg transition-colors' ,
secondary :
'px-3 py-1.5 text-sm font-medium bg-gray-600 hover:bg-gray-700 dark:bg-gray-600 dark:hover:bg-gray-700 text-white rounded-lg transition-colors' ,
// 警告操作按钮(黄色)- 用于批量禁用等
warning : 'px-3 py-1.5 text-sm font-medium bg-yellow-600 hover:bg-yellow-700 dark:bg-yellow-600 dark:hover:bg-yellow-700 text-white rounded-lg transition-colors' ,
warning :
'px-3 py-1.5 text-sm font-medium bg-yellow-600 hover:bg-yellow-700 dark:bg-yellow-600 dark:hover:bg-yellow-700 text-white rounded-lg transition-colors' ,
// 小尺寸主要按钮
primarySmall : 'px-2 py-1 text-xs font-medium bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded-md transition-colors' ,
primarySmall :
'px-2 py-1 text-xs font-medium bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded-md transition-colors' ,
// 小尺寸成功按钮
successSmall : 'px-2 py-1 text-xs font-medium bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700 text-white rounded-md transition-colors' ,
successSmall :
'px-2 py-1 text-xs font-medium bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700 text-white rounded-md transition-colors' ,
// 小尺寸危险按钮
dangerSmall : 'px-2 py-1 text-xs font-medium bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700 text-white rounded-md transition-colors' ,
dangerSmall :
'px-2 py-1 text-xs font-medium bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700 text-white rounded-md transition-colors' ,
// 小尺寸次要按钮
secondarySmall : 'px-2 py-1 text-xs font-medium bg-gray-600 hover:bg-gray-700 dark:bg-gray-600 dark:hover:bg-gray-700 text-white rounded-md transition-colors' ,
secondarySmall :
'px-2 py-1 text-xs font-medium bg-gray-600 hover:bg-gray-700 dark:bg-gray-600 dark:hover:bg-gray-700 text-white rounded-md transition-colors' ,
// 小尺寸警告按钮
warningSmall : 'px-2 py-1 text-xs font-medium bg-yellow-600 hover:bg-yellow-700 dark:bg-yellow-600 dark:hover:bg-yellow-700 text-white rounded-md transition-colors' ,
warningSmall :
'px-2 py-1 text-xs font-medium bg-yellow-600 hover:bg-yellow-700 dark:bg-yellow-600 dark:hover:bg-yellow-700 text-white rounded-md transition-colors' ,
// 圆角小按钮(用于表格操作)
roundedPrimary : 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 hover:bg-blue-200 dark:bg-blue-900/40 dark:hover:bg-blue-900/60 dark:text-blue-200 transition-colors' ,
roundedSuccess : 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-green -100 text-green -800 hover:bg-green -200 dark:bg-green -900/40 dark:hover:bg-green -900/60 dark:text-green -200 transition-colors' ,
roundedDanger : 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-red-100 text-red-800 hover:bg-red-200 dark:bg-red-900/40 dark:hover:bg-red-900/60 dark:text-red-200 transition-colors' ,
roundedSecondary : 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-gray -100 text-gray -800 hover:bg-gray -200 dark:bg-gray-7 00/40 dark:hover:bg-gray-7 00/60 dark:text-gray -200 transition-colors' ,
roundedWarning : 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 hover:bg-yellow-200 dark:bg-yellow-900/40 dark:hover:bg-yellow-900/60 dark:text-yellow-200 transition-colors' ,
roundedPurple : 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-purple -100 text-purple -800 hover:bg-purple -200 dark:bg-purple -900/40 dark:hover:bg-purple -900/60 dark:text-purple -200 transition-colors' ,
roundedPrimary :
'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-blue -100 text-blue -800 hover:bg-blue -200 dark:bg-blue -900/40 dark:hover:bg-blue -900/60 dark:text-blue -200 transition-colors' ,
roundedSuccess :
'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-green -100 text-green -800 hover:bg-green -200 dark:bg-green-9 00/40 dark:hover:bg-green-9 00/60 dark:text-green -200 transition-colors' ,
roundedDanger :
'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-red -100 text-red -800 hover:bg-red -200 dark:bg-red -900/40 dark:hover:bg-red -900/60 dark:text-red -200 transition-colors' ,
roundedSecondary :
'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 hover:bg-gray-200 dark:bg-gray-700/40 dark:hover:bg-gray-700/60 dark:text-gray-200 transition-colors' ,
roundedWarning :
'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 hover:bg-yellow-200 dark:bg-yellow-900/40 dark:hover:bg-yellow-900/60 dark:text-yellow-200 transition-colors' ,
roundedPurple :
'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 hover:bg-purple-200 dark:bg-purple-900/40 dark:hover:bg-purple-900/60 dark:text-purple-200 transition-colors' ,
// 禁用状态
disabled : 'px-3 py-1.5 text-sm font-medium bg-gray-400 dark:bg-gray-600 cursor-not-allowed text-white rounded-lg transition-colors' ,
disabledSmall : 'px-2 py-1 text-x s font-medium bg-gray-400 dark:bg-gray-600 cursor-not-allowed text-white rounded-md transition-colors' ,
disabled :
'px-3 py-1.5 text-sm font-medium bg-gray-400 dark:bg-gray-600 cursor-not-allowed text-white rounded-lg transition-colors' ,
disabledSmall :
'px-2 py-1 text-xs font-medium bg-gray-400 dark:bg-gray-600 cursor-not-allowed text-white rounded-md transition-colors' ,
// 开关按钮样式
toggleOn : 'bg-green-600 dark:bg-green-600' ,
toggleOff : 'bg-gray-200 dark:bg-gray-700' ,
@@ -86,7 +104,8 @@ const buttonStyles = {
toggleThumbOn : 'translate-x-6' ,
toggleThumbOff : 'translate-x-1' ,
// 快速操作按钮样式
quickAction : 'px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-md transition-colors' ,
quickAction :
'px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-md transition-colors' ,
} ;
// 通用弹窗组件
@@ -107,7 +126,7 @@ const AlertModal = ({
title ,
message ,
timer ,
showConfirm = false
showConfirm = false ,
} : AlertModalProps ) = > {
const [ isVisible , setIsVisible ] = useState ( false ) ;
@@ -129,11 +148,11 @@ const AlertModal = ({
const getIcon = ( ) = > {
switch ( type ) {
case 'success' :
return < CheckCircle className = " w-8 h-8 text-green-500" / > ;
return < CheckCircle className = ' w-8 h-8 text-green-500' / > ;
case 'error' :
return < AlertCircle className = " w-8 h-8 text-red-500" / > ;
return < AlertCircle className = ' w-8 h-8 text-red-500' / > ;
case 'warning' :
return < AlertTriangle className = " w-8 h-8 text-yellow-500" / > ;
return < AlertTriangle className = ' w-8 h-8 text-yellow-500' / > ;
default :
return null ;
}
@@ -153,21 +172,25 @@ const AlertModal = ({
} ;
return createPortal (
< div className = { ` fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4 transition-opacity duration-200 ${ isVisible ? 'opacity-100' : 'opacity-0' } ` } >
< div className = { ` bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-sm w-full border ${ getBgColor ( ) } transition-all duration-200 ${ isVisible ? 'scale-100' : 'scale-95' } ` } >
< div className = "p-6 text-center" >
< div className = "flex justify-center mb-4" >
{ getIcon ( ) }
< / div >
< div
className = { ` fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4 transition-opacity duration-200 ${
isVisible ? 'opacity-100' : 'opacity-0'
} ` }
>
< div
className = { ` bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-sm w-full border ${ getBgColor ( ) } transition-all duration-200 ${
isVisible ? 'scale-100' : 'scale-95'
} ` }
>
< div className = 'p-6 text-center' >
< div className = 'flex justify-center mb-4' > { getIcon ( ) } < / div >
< h3 className = " text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2" >
< h3 className = ' text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2' >
{ title }
< / h3 >
{ message && (
< p className = " text-gray-600 dark:text-gray-400 mb-4" >
{ message }
< / p >
< p className = ' text-gray-600 dark:text-gray-400 mb-4' > { message } < / p >
) }
{ showConfirm && (
@@ -205,7 +228,7 @@ const useAlertModal = () => {
} ;
const hideAlert = ( ) = > {
setAlertModal ( prev = > ( { . . . prev , isOpen : false } ) ) ;
setAlertModal ( ( prev ) = > ( { . . . prev , isOpen : false } ) ) ;
} ;
return { alertModal , showAlert , hideAlert } ;
@@ -237,12 +260,15 @@ const useLoadingState = () => {
const [ loadingStates , setLoadingStates ] = useState < LoadingState > ( { } ) ;
const setLoading = ( key : string , loading : boolean ) = > {
setLoadingStates ( prev = > ( { . . . prev , [ key ] : loading } ) ) ;
setLoadingStates ( ( prev ) = > ( { . . . prev , [ key ] : loading } ) ) ;
} ;
const isLoading = ( key : string ) = > loadingStates [ key ] || false ;
const withLoading = async ( key : string , operation : ( ) = > Promise < any > ) : Promise < any > = > {
const withLoading = async (
key : string ,
operation : ( ) = > Promise < any >
) : Promise < any > = > {
setLoading ( key , true ) ;
try {
const result = await operation ( ) ;
@@ -380,7 +406,8 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
tags? : string [ ] ;
} | null > ( null ) ;
const [ selectedApis , setSelectedApis ] = useState < string [ ] > ( [ ] ) ;
const [ showConfigureUserGroupModal , setShowConfigureUserGroupModal ] = useState ( false ) ;
const [ showConfigureUserGroupModal , setShowConfigureUserGroupModal ] =
useState ( false ) ;
const [ selectedUserForGroup , setSelectedUserForGroup ] = useState < {
username : string ;
role : 'user' | 'admin' | 'owner' ;
@@ -390,10 +417,14 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
const [ selectedUsers , setSelectedUsers ] = useState < Set < string > > ( new Set ( ) ) ;
const [ showBatchUserGroupModal , setShowBatchUserGroupModal ] = useState ( false ) ;
const [ selectedUserGroup , setSelectedUserGroup ] = useState < string > ( '' ) ;
const [ showDeleteUserGroupModal , setShowDeleteUserGroupModal ] = useState ( false ) ;
const [ showDeleteUserGroupModal , setShowDeleteUserGroupModal ] =
useState ( false ) ;
const [ deletingUserGroup , setDeletingUserGroup ] = useState < {
name : string ;
affectedUsers : Array < { username : string ; role : 'user' | 'admin' | 'owner' } > ;
affectedUsers : Array < {
username : string ;
role : 'user' | 'admin' | 'owner' ;
} > ;
} | null > ( null ) ;
const [ showDeleteUserModal , setShowDeleteUserModal ] = useState ( false ) ;
const [ deletingUser , setDeletingUser ] = useState < string | null > ( null ) ;
@@ -403,12 +434,13 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
// 使用 useMemo 计算全选状态,避免每次渲染都重新计算
const selectAllUsers = useMemo ( ( ) = > {
const selectableUserCount = config ? . UserConfig ? . Users ? . filter ( user = >
( role === 'owner' ||
( role === 'admin' &&
( user . role === 'us er' ||
user . username === currentUsername ) ) )
) . length || 0 ;
const selectableUserCount =
config ? . UserConfig ? . Users ? . filter (
( user ) = >
role === 'own er' ||
( role === 'admin' &&
( user . role === 'user' || user . username === currentUsername ) )
) . length || 0 ;
return selectedUsers . size === selectableUserCount && selectedUsers . size > 0 ;
} , [ selectedUsers . size , config ? . UserConfig ? . Users , role , currentUsername ] ) ;
@@ -449,7 +481,14 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
setShowEditUserGroupForm ( false ) ;
}
showSuccess ( action === 'add' ? '用户组添加成功' : action === 'edit' ? '用户组更新成功' : '用户组删除成功' , showAlert ) ;
showSuccess (
action === 'add'
? '用户组添加成功'
: action === 'edit'
? '用户组更新成功'
: '用户组删除成功' ,
showAlert
) ;
} catch ( err ) {
showError ( err instanceof Error ? err . message : '操作失败' , showAlert ) ;
throw err ;
@@ -464,18 +503,26 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
const handleEditUserGroup = ( ) = > {
if ( ! editingUserGroup ? . name . trim ( ) ) return ;
handleUserGroupAction ( 'edit' , editingUserGroup . name , editingUserGroup . enabledApis ) ;
handleUserGroupAction (
'edit' ,
editingUserGroup . name ,
editingUserGroup . enabledApis
) ;
} ;
const handleDeleteUserGroup = ( groupName : string ) = > {
// 计算会受影响的用户数量
const affectedUsers = config ? . UserConfig ? . Users ? . filter ( user = >
user . tags && user . tags . includes ( groupName )
) || [ ] ;
const affectedUsers =
config ? . UserConfig ? . Users ? . filter (
( user ) = > user . tags && user . tags . includes ( groupName )
) || [ ] ;
setDeletingUserGroup ( {
name : groupName ,
affectedUsers : affectedUsers.map ( u = > ( { username : u.username , role : u.role } ) )
affectedUsers : affectedUsers.map ( ( u ) = > ( {
username : u.username ,
role : u.role ,
} ) ) ,
} ) ;
setShowDeleteUserGroupModal ( true ) ;
} ;
@@ -492,14 +539,20 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
}
} ;
const handleStartEditUserGroup = ( group : { name : string ; enabledApis : string [ ] } ) = > {
const handleStartEditUserGroup = ( group : {
name : string ;
enabledApis : string [ ] ;
} ) = > {
setEditingUserGroup ( { . . . group } ) ;
setShowEditUserGroupForm ( true ) ;
setShowAddUserGroupForm ( false ) ;
} ;
// 为用户分配用户组
const handleAssignUserGroup = async ( username : string , userGroups : string [ ] ) = > {
const handleAssignUserGroup = async (
username : string ,
userGroups : string [ ]
) = > {
return withLoading ( ` assignUserGroup_ ${ username } ` , async ( ) = > {
try {
const res = await fetch ( '/api/admin/user' , {
@@ -531,21 +584,32 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
} ;
const handleUnbanUser = async ( uname : string ) = > {
await withLoading ( ` unbanUser_ ${ uname } ` , ( ) = > handleUserAction ( 'unban' , uname ) ) ;
await withLoading ( ` unbanUser_ ${ uname } ` , ( ) = >
handleUserAction ( 'unban' , uname )
) ;
} ;
const handleSetAdmin = async ( uname : string ) = > {
await withLoading ( ` setAdmin_ ${ uname } ` , ( ) = > handleUserAction ( 'setAdmin' , uname ) ) ;
await withLoading ( ` setAdmin_ ${ uname } ` , ( ) = >
handleUserAction ( 'setAdmin' , uname )
) ;
} ;
const handleRemoveAdmin = async ( uname : string ) = > {
await withLoading ( ` removeAdmin_ ${ uname } ` , ( ) = > handleUserAction ( 'cancelAdmin' , uname ) ) ;
await withLoading ( ` removeAdmin_ ${ uname } ` , ( ) = >
handleUserAction ( 'cancelAdmin' , uname )
) ;
} ;
const handleAddUser = async ( ) = > {
if ( ! newUser . username || ! newUser . password ) return ;
await withLoading ( 'addUser' , async ( ) = > {
await handleUserAction ( 'add' , newUser . username , newUser . password , newUser . userGroup ) ;
await handleUserAction (
'add' ,
newUser . username ,
newUser . password ,
newUser . userGroup
) ;
setNewUser ( { username : '' , password : '' , userGroup : '' } ) ;
setShowAddUserForm ( false ) ;
} ) ;
@@ -553,15 +617,18 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
const handleChangePassword = async ( ) = > {
if ( ! changePasswordUser . username || ! changePasswordUser . password ) return ;
await withLoading ( ` changePassword_ ${ changePasswordUser . username } ` , async ( ) = > {
await handleUserAction (
'changePassword' ,
changePasswordUser . username ,
changePasswordUser . password
) ;
setChangePasswordUser ( { username : '' , password : '' } ) ;
setShowChangePasswordForm ( false ) ;
} ) ;
await withLoading (
` changePassword_ ${ changePasswordUser . username } ` ,
async ( ) = > {
await handleUserAction (
'changePassword' ,
changePasswordUser . username ,
changePasswordUser . password
) ;
setChangePasswordUser ( { username : '' , password : '' } ) ;
setShowChangePasswordForm ( false ) ;
}
) ;
} ;
const handleShowChangePasswordForm = ( username : string ) = > {
@@ -598,21 +665,27 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
const handleSaveUserGroups = async ( ) = > {
if ( ! selectedUserForGroup ) return ;
await withLoading ( ` saveUserGroups_ ${ selectedUserForGroup . username } ` , async ( ) = > {
try {
await handleAssignUserGroup ( selectedUserForGroup . username , selectedUserGroups ) ;
setShowConfigureUserGroupModal ( false ) ;
setSelectedUserForGroup ( null ) ;
setSelectedUserGroups ( [ ] ) ;
} catch ( err ) {
// 错误处理已在 handleAssignUserGroup 中处理
await withLoading (
` saveUserGroups_ ${ selectedUserForGroup . username } ` ,
async ( ) = > {
try {
await handleAssignUserGroup (
selectedUserForGroup . username ,
selectedUserGroups
) ;
setShowConfigureUserGroupModal ( false ) ;
setSelectedUserForGroup ( null ) ;
setSelectedUserGroups ( [ ] ) ;
} catch ( err ) {
// 错误处理已在 handleAssignUserGroup 中处理
}
}
} ) ;
) ;
} ;
// 处理用户选择
const handleSelectUser = useCallback ( ( username : string , checked : boolean ) = > {
setSelectedUsers ( prev = > {
setSelectedUsers ( ( prev ) = > {
const newSelectedUsers = new Set ( prev ) ;
if ( checked ) {
newSelectedUsers . add ( username ) ;
@@ -623,20 +696,24 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
} ) ;
} , [ ] ) ;
const handleSelectAllUsers = useCallback ( ( checked : boolean ) = > {
if ( checked ) {
// 只选择自己有权限操作的用户
const selectableUsernames = config ? . UserConfig ? . Users ? . filter ( user = >
( role === 'owner' ||
( role === 'admin' &&
( user . role === 'user' ||
user . username === currentUsername ) ) )
) . map ( u = > u . username ) || [ ] ;
setSelectedUsers ( new Set ( selectableUsernames ) ) ;
} else {
setSelectedUsers ( new Set ( ) ) ;
}
} , [ config ? . UserConfig ? . Users , role , currentUsername ] ) ;
const handleSelectAllUsers = useCallback (
( checked : boolean ) = > {
if ( checked ) {
// 只选择自己有权限操作的用户
const selectableUsernames =
config ? . UserConfig ? . Users ? . filter (
( user ) = >
role === 'owner' ||
( role === 'admin' &&
( user . role === 'user' || user . username === currentUsername ) )
) . map ( ( u ) = > u . username ) || [ ] ;
setSelectedUsers ( new Set ( selectableUsernames ) ) ;
} else {
setSelectedUsers ( new Set ( ) ) ;
}
} ,
[ config ? . UserConfig ? . Users , role , currentUsername ]
) ;
// 批量设置用户组
const handleBatchSetUserGroup = async ( userGroup : string ) = > {
@@ -663,7 +740,10 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
setSelectedUsers ( new Set ( ) ) ;
setShowBatchUserGroupModal ( false ) ;
setSelectedUserGroup ( '' ) ;
showSuccess ( ` 已为 ${ userCount } 个用户设置用户组: ${ userGroup } ` , showAlert ) ;
showSuccess (
` 已为 ${ userCount } 个用户设置用户组: ${ userGroup } ` ,
showAlert
) ;
// 刷新配置
await refreshConfig ( ) ;
@@ -674,8 +754,6 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
} ) ;
} ;
// 提取URL域名的辅助函数
const extractDomain = ( url : string ) : string = > {
try {
@@ -796,8 +874,6 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
< / div >
< / div >
{ /* 用户组管理 */ }
< div >
< div className = 'flex items-center justify-between mb-3' >
@@ -812,7 +888,11 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
setEditingUserGroup ( null ) ;
}
} }
className = { showAddUserGroupForm ? buttonStyles.secondary : buttonStyles.primary }
className = {
showAddUserGroupForm
? buttonStyles.secondary
: buttonStyles.primary
}
>
{ showAddUserGroupForm ? '取消' : '添加用户组' }
< / button >
@@ -836,7 +916,10 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
< / thead >
< tbody className = 'divide-y divide-gray-200 dark:divide-gray-700' >
{ userGroups . map ( ( group ) = > (
< tr key = { group . name } className = 'hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors' >
< tr
key = { group . name }
className = 'hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors'
>
< td className = 'px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100' >
{ group . name }
< / td >
@@ -853,7 +936,11 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
< button
onClick = { ( ) = > handleStartEditUserGroup ( group ) }
disabled = { isLoading ( ` userGroup_edit_ ${ group . name } ` ) }
className = { ` ${ buttonStyles . roundedPrimary } ${ isLoading ( ` userGroup_edit_ ${ group . name } ` ) ? 'opacity-50 cursor-not-allowed' : '' } ` }
className = { ` ${ buttonStyles . roundedPrimary } ${
isLoading ( ` userGroup_edit_ ${ group . name } ` )
? 'opacity-50 cursor-not-allowed'
: ''
} ` }
>
编 辑
< / button >
@@ -868,7 +955,10 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
) ) }
{ userGroups . length === 0 && (
< tr >
< td colSpan = { 3 } className = 'px-6 py-8 text-center text-sm text-gray-500 dark:text-gray-400' >
< td
colSpan = { 3 }
className = 'px-6 py-8 text-center text-sm text-gray-500 dark:text-gray-400'
>
暂 无 用 户 组 , 请 添 加 用 户 组 来 管 理 用 户 权 限
< / td >
< / tr >
@@ -910,7 +1000,9 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
setChangePasswordUser ( { username : '' , password : '' } ) ;
}
} }
className = { showAddUserForm ? buttonStyles.secondary : buttonStyles.success }
className = {
showAddUserForm ? buttonStyles.secondary : buttonStyles.success
}
>
{ showAddUserForm ? '取消' : '添加用户' }
< / button >
@@ -927,7 +1019,10 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
placeholder = '用户名'
value = { newUser . username }
onChange = { ( e ) = >
setNewUser ( ( prev ) = > ( { . . . prev , username : e.target.value } ) )
setNewUser ( ( prev ) = > ( {
. . . prev ,
username : e.target.value ,
} ) )
}
className = 'px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
/ >
@@ -936,7 +1031,10 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
placeholder = '密码'
value = { newUser . password }
onChange = { ( e ) = >
setNewUser ( ( prev ) = > ( { . . . prev , password : e.target.value } ) )
setNewUser ( ( prev ) = > ( {
. . . prev ,
password : e.target.value ,
} ) )
}
className = 'px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
/ >
@@ -948,14 +1046,21 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
< select
value = { newUser . userGroup }
onChange = { ( e ) = >
setNewUser ( ( prev ) = > ( { . . . prev , userGroup : e.target.value } ) )
setNewUser ( ( prev ) = > ( {
. . . prev ,
userGroup : e.target.value ,
} ) )
}
className = 'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
>
< option value = '' > 无 用 户 组 ( 无 限 制 ) < / option >
{ userGroups . map ( ( group ) = > (
< option key = { group . name } value = { group . name } >
{ group . name } ( { group . enabledApis && group . enabledApis . length > 0 ? ` ${ group . enabledApis . length } 个源 ` : '无限制' } )
{ group . name } (
{ group . enabledApis && group . enabledApis . length > 0
? ` ${ group . enabledApis . length } 个源 `
: '无限制' }
)
< / option >
) ) }
< / select >
@@ -963,8 +1068,18 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
< div className = 'flex justify-end' >
< button
onClick = { handleAddUser }
disabled = { ! newUser . username || ! newUser . password || isLoading ( 'addUser' ) }
className = { ! newUser . username || ! newUser . password || isLoading ( 'addUser' ) ? buttonStyles.disabled : buttonStyles.success }
disabled = {
! newUser . username ||
! newUser . password ||
isLoading ( 'addUser' )
}
className = {
! newUser . username ||
! newUser . password ||
isLoading ( 'addUser' )
? buttonStyles.disabled
: buttonStyles.success
}
>
{ isLoading ( 'addUser' ) ? '添加中...' : '添加' }
< / button >
@@ -1001,10 +1116,20 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
/ >
< button
onClick = { handleChangePassword }
disabled = { ! changePasswordUser . password || isLoading ( ` changePassword_ ${ changePasswordUser . username } ` ) }
className = { ` w-full sm:w-auto ${ ! changePasswordUser . password || isLoading ( ` changePassword_ ${ changePasswordUser . username } ` ) ? buttonStyles.disabled : buttonStyles.primary } ` }
disabled = {
! changePasswordUser . password ||
isLoading ( ` changePassword_ ${ changePasswordUser . username } ` )
}
className = { ` w-full sm:w-auto ${
! changePasswordUser . password ||
isLoading ( ` changePassword_ ${ changePasswordUser . username } ` )
? buttonStyles.disabled
: buttonStyles.primary
} ` }
>
{ isLoading ( ` changePassword_ ${ changePasswordUser . username } ` ) ? '修改中...' : '修改密码' }
{ isLoading ( ` changePassword_ ${ changePasswordUser . username } ` )
? '修改中...'
: '修改密码' }
< / button >
< button
onClick = { ( ) = > {
@@ -1020,7 +1145,10 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
) }
{ /* 用户列表 */ }
< div className = 'border border-gray-200 dark:border-gray-700 rounded-lg max-h-[28rem] overflow-y-auto overflow-x-auto relative' data-table = "user-list" >
< div
className = 'border border-gray-200 dark:border-gray-700 rounded-lg max-h-[28rem] overflow-y-auto overflow-x-auto relative'
data-table = 'user-list'
>
< table className = 'min-w-full divide-y divide-gray-200 dark:divide-gray-700' >
< thead className = 'bg-gray-50 dark:bg-gray-900 sticky top-0 z-10' >
< tr >
@@ -1028,11 +1156,12 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
< th className = 'w-10 px-1 py-3 text-center' >
{ ( ( ) = > {
// 检查是否有权限操作任何用户
const hasAnyPermission = config ? . UserConfig ? . Users ? . some ( user = >
( role === 'owner' ||
( role === 'admin ' &&
( user . role === 'user ' ||
user . username === currentUsername ) ) )
const hasAnyPermission = config ? . UserConfig ? . Users ? . some (
( user ) = >
role === 'owner ' ||
( role === 'admin ' &&
( user . role === 'user' ||
user . username === currentUsername ) )
) ;
return hasAnyPermission ? (
@@ -1126,14 +1255,19 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
>
< td className = 'w-4' / >
< td className = 'w-10 px-1 py-3 text-center' >
{ ( role === 'owner' ||
( role === 'admin' &&
( user . role === 'user' ||
user . username === currentUsername ) ) ) ? (
{ role === 'owner' ||
( role === 'admin' &&
( user . role === 'user' ||
user . username === currentUsername ) ) ? (
< input
type = 'checkbox'
checked = { selectedUsers . has ( user . username ) }
onChange = { ( e ) = > handleSelectUser ( user . username , e . target . checked ) }
onChange = { ( e ) = >
handleSelectUser (
user . username ,
e . target . checked
)
}
className = 'w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600'
/ >
) : (
@@ -1145,26 +1279,28 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
< / td >
< td className = 'px-6 py-4 whitespace-nowrap' >
< span
className = { ` px-2 py-1 text-xs rounded-full ${ user . role === 'owner'
? 'bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300 '
: user . role === 'admin '
className = { ` px-2 py-1 text-xs rounded-full ${
user . role === 'owner '
? 'bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300 '
: user . role === 'admin'
? 'bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-300'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
} ` }
} ` }
>
{ user . role === 'owner'
? '站长'
: user . role === 'admin'
? '管理员'
: '普通用户' }
? '管理员'
: '普通用户' }
< / span >
< / td >
< td className = 'px-6 py-4 whitespace-nowrap' >
< span
className = { ` px-2 py-1 text-xs rounded-full ${ ! user . banned
? 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300'
: 'bg-red -100 dark:bg-red -900/20 text-red -800 dark:text-red -300'
} ` }
className = { ` px-2 py-1 text-xs rounded-full ${
! user . banned
? 'bg-g reen -100 dark:bg-g reen -900/20 text-g reen -800 dark:text-g reen -300'
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300'
} ` }
>
{ ! user . banned ? '正常' : '已封禁' }
< / span >
@@ -1181,13 +1317,13 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
( role === 'admin' &&
( user . role === 'user' ||
user . username === currentUsername ) ) ) && (
< button
onClick = { ( ) = > handleConfigureUserGroup ( user ) }
className = { buttonStyles . roundedPrimary }
>
配 置
< / button >
) }
< button
onClick = { ( ) = > handleConfigureUserGroup ( user ) }
className = { buttonStyles . roundedPrimary }
>
配 置
< / button >
) }
< / div >
< / td >
< td className = 'px-6 py-4 whitespace-nowrap' >
@@ -1202,13 +1338,13 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
( role === 'admin' &&
( user . role === 'user' ||
user . username === currentUsername ) ) ) && (
< button
onClick = { ( ) = > handleConfigureUserApis ( user ) }
className = { buttonStyles . roundedPrimary }
>
配 置
< / button >
) }
< button
onClick = { ( ) = > handleConfigureUserApis ( user ) }
className = { buttonStyles . roundedPrimary }
>
配 置
< / button >
) }
< / div >
< / td >
< td className = 'px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2' >
@@ -1229,8 +1365,14 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
{ user . role === 'user' && (
< button
onClick = { ( ) = > handleSetAdmin ( user . username ) }
disabled = { isLoading ( ` setAdmin_ ${ user . username } ` ) }
className = { ` ${ buttonStyles . roundedPurple } ${ isLoading ( ` setAdmin_ ${ user . username } ` ) ? 'opacity-50 cursor-not-allowed' : '' } ` }
disabled = { isLoading (
` setAdmin_ ${ user . username } `
) }
className = { ` ${ buttonStyles . roundedPurple } ${
isLoading ( ` setAdmin_ ${ user . username } ` )
? 'opacity-50 cursor-not-allowed'
: ''
} ` }
>
设 为 管 理
< / button >
@@ -1240,8 +1382,16 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
onClick = { ( ) = >
handleRemoveAdmin ( user . username )
}
disabled = { isLoading ( ` removeAdmin_ ${ user . username } ` ) }
className = { ` ${ buttonStyles . roundedSecondary } ${ isLoading ( ` removeAdmin_ ${ user . username } ` ) ? 'opacity-50 cursor-not-allowed' : '' } ` }
disabled = { isLoading (
` removeAdmin_ ${ user . username } `
) }
className = { ` ${
buttonStyles . roundedSecondary
} ${
isLoading ( ` removeAdmin_ ${ user . username } ` )
? 'opacity-50 cursor-not-allowed'
: ''
} ` }
>
取 消 管 理
< / button >
@@ -1250,8 +1400,14 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
( ! user . banned ? (
< button
onClick = { ( ) = > handleBanUser ( user . username ) }
disabled = { isLoading ( ` banUser_ ${ user . username } ` ) }
className = { ` ${ buttonStyles . roundedDanger } ${ isLoading ( ` banUser_ ${ user . username } ` ) ? 'opacity-50 cursor-not-allowed' : '' } ` }
disabled = { isLoading (
` banUser_ ${ user . username } `
) }
className = { ` ${ buttonStyles . roundedDanger } ${
isLoading ( ` banUser_ ${ user . username } ` )
? 'opacity-50 cursor-not-allowed'
: ''
} ` }
>
封 禁
< / button >
@@ -1260,8 +1416,16 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
onClick = { ( ) = >
handleUnbanUser ( user . username )
}
disabled = { isLoading ( ` unbanUser_ ${ user . username } ` ) }
className = { ` ${ buttonStyles . roundedSuccess } ${ isLoading ( ` unbanUser_ ${ user . username } ` ) ? 'opacity-50 cursor-not-allowed' : '' } ` }
disabled = { isLoading (
` unbanUser_ ${ user . username } `
) }
className = { ` ${
buttonStyles . roundedSuccess
} ${
isLoading ( ` unbanUser_ ${ user . username } ` )
? 'opacity-50 cursor-not-allowed'
: ''
} ` }
>
解 封
< / button >
@@ -1289,199 +1453,97 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
< / div >
{ /* 配置用户采集源权限弹窗 */ }
{ showConfigureApisModal && selectedUser && createPortal (
< div className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4' onClick = { ( ) = > {
setShowConfigureApisModal ( false ) ;
setSelectedUser ( null ) ;
setSelectedApis ( [ ] ) ;
} } >
< div className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[80vh] overflow-y-auto' onClick = { ( e ) = > e . stopPropagation ( ) } >
< div className = 'p-6' >
< div className = 'flex items-center justify-between mb-6' >
< h3 className = 'text-xl font-semibold text-gray-900 dark:text-gray-100' >
配 置 用 户 采 集 源 权 限 - { selectedUser . username }
< / h3 >
< button
onClick = { ( ) = > {
setShowConfigureApisModal ( false ) ;
setSelectedUser ( null ) ;
setSelectedApis ( [ ] ) ;
} }
className = 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
< svg className = 'w-6 h-6' fill = 'none' stroke = 'currentColor' viewBox = '0 0 24 24' >
< path strokeLinecap = 'round' strokeLinejoin = 'round' strokeWidth = { 2 } d = 'M6 18L18 6M6 6l12 12' / >
< / svg >
< / button >
< / div >
< div className = 'mb-6' >
< div className = 'bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4' >
< div className = 'flex items-center space-x-2 mb-2' >
< svg className = 'w-5 h-5 text-blue-600 dark:text-blue-400' fill = 'none' stroke = 'currentColor' viewBox = '0 0 24 24' >
< path strokeLinecap = 'round' strokeLinejoin = 'round' strokeWidth = { 2 } d = 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' / >
< / svg >
< span className = 'text-sm font-medium text-blue-800 dark:text-blue-300' >
配 置 说 明
< / span >
< / div >
< p className = 'text-sm text-blue-700 dark:text-blue-400 mt-1' >
提 示 : 全 不 选 为 无 限 制 , 选 中 的 采 集 源 将 限 制 用 户 只 能 访 问 这 些 源
< / p >
< / div >
< / div >
{ /* 采集源选择 - 多列布局 */ }
< div className = 'mb-6' >
< h4 className = 'text-sm font-medium text-gray-700 dark:text-gray-300 mb-4' >
选 择 可 用 的 采 集 源 :
< / h4 >
< div className = 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4' >
{ config ? . SourceConfig ? . map ( ( source ) = > (
< label key = { source . key } className = 'flex items-center space-x-3 p-3 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer transition-colors' >
< input
type = 'checkbox'
checked = { selectedApis . includes ( source . key ) }
onChange = { ( e ) = > {
if ( e . target . checked ) {
setSelectedApis ( [ . . . selectedApis , source . key ] ) ;
} else {
setSelectedApis ( selectedApis . filter ( api = > api !== source . key ) ) ;
}
} }
className = 'rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700'
/ >
< div className = 'flex-1 min-w-0' >
< div className = 'text-sm font-medium text-gray-900 dark:text-gray-100 truncate' >
{ source . name }
< / div >
{ source . api && (
< div className = 'text-xs text-gray-500 dark:text-gray-400 truncate' >
{ extractDomain ( source . api ) }
< / div >
) }
< / div >
< / label >
) ) }
< / div >
< / div >
{ /* 快速操作按钮 */ }
< div className = 'flex flex-wrap items-center justify-between mb-6 p-4 bg-gray-50 dark:bg-gray-900 rounded-lg' >
< div className = 'flex space-x-2' >
< button
onClick = { ( ) = > setSelectedApis ( [ ] ) }
className = { buttonStyles . quickAction }
>
全 不 选 ( 无 限 制 )
< / button >
{ showConfigureApisModal &&
selectedUser &&
createPortal (
< div
className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'
onClick = { ( ) = > {
setShowConfigureApisModal ( false ) ;
setSelectedUser ( null ) ;
setSelectedApis ( [ ] ) ;
} }
>
< div
className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[80vh] overflow-y-auto'
onClick = { ( e ) = > e . stopPropagation ( ) }
>
< div className = 'p-6' >
< div className = 'flex items-center justify-between mb-6' >
< h3 className = 'text-xl font-semibold text-gray-900 dark:text-gray-100' >
配 置 用 户 采 集 源 权 限 - { selectedUser . username }
< / h3 >
< button
onClick = { ( ) = > {
const allApis = config ? . SourceConfig ? . filter ( source = > ! source . disabled ) . map ( s = > s . key ) || [ ] ;
setSelectedApis ( allApis ) ;
setShowConfigureApisModal ( false ) ;
setSelectedUser ( null ) ;
setSelectedApis ( [ ] ) ;
} }
className = { buttonStyles . quickAction }
className = 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
全 选
< svg
className = 'w-6 h-6'
fill = 'none'
stroke = 'currentColor'
viewBox = '0 0 24 24'
>
< path
strokeLinecap = 'round'
strokeLinejoin = 'round'
strokeWidth = { 2 }
d = 'M6 18L18 6M6 6l12 12'
/ >
< / svg >
< / button >
< / div >
< div className = 'text-sm text-gray-600 dark:text-gray-400' >
已 选 择 : < span className = 'font-medium text-blue-600 dark:text-blue-400' >
{ selectedApis . length > 0 ? ` ${ selectedApis . length } 个源 ` : '无限制' }
< / span >
< / div >
< / div >
{ /* 操作按钮 */ }
< div className = 'flex justify-end space-x-3 ' >
< button
onClick = { ( ) = > {
setShowConfigureApisModal ( false ) ;
setSelectedUser ( null ) ;
setSelectedApis ( [ ] ) ;
} }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . secondary } ` }
>
取 消
< / button >
< button
onClick = { handleSaveUserApis }
disabled = { isLoading ( ` saveUserApis_ ${ selectedUser ? . username } ` ) }
className = { ` px-6 py-2.5 text-sm font-medium ${ isLoading ( ` saveUserApis_ ${ selectedUser ? . username } ` ) ? buttonStyles.disabled : buttonStyles.primary } ` }
>
{ isLoading ( ` saveUserApis_ ${ selectedUser ? . username } ` ) ? '配置中...' : '确认配置' }
< / button >
< / div >
< / div >
< / div >
< / div > ,
document . body
) }
{ /* 添加用户组弹窗 */ }
{ showAddUserGroupForm && createPortal (
< div className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4' onClick = { ( ) = > {
setShowAddUserGroupForm ( false ) ;
setNewUserGroup ( { name : '' , enabledApis : [ ] } ) ;
} } >
< div className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[80vh] overflow-y-auto' onClick = { ( e ) = > e . stopPropagation ( ) } >
< div className = 'p-6' >
< div className = 'flex items-center justify-between mb-6' >
< h3 className = 'text-xl font-semibold text-gray-900 dark:text-gray-100' >
添 加 新 用 户 组
< / h3 >
< button
onClick = { ( ) = > {
setShowAddUserGroupForm ( false ) ;
setNewUserGroup ( { name : '' , enabledApis : [ ] } ) ;
} }
className = 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
< svg className = 'w-6 h-6' fill = 'none' stroke = 'currentColor' viewBox = '0 0 24 24' >
< path strokeLinecap = 'round' strokeLinejoin = 'round' strokeWidth = { 2 } d = 'M6 18L18 6M6 6l12 12' / >
< / svg >
< / button >
< / div >
< div className = 'space-y-6' >
{ /* 用户组名称 */ }
< div >
< label className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2' >
用 户 组 名 称
< / label >
< input
type = 'text'
placeholder = '请输入用户组名称'
value = { newUserGroup . name }
onChange = { ( e ) = >
setNewUserGroup ( ( prev ) = > ( { . . . prev , name : e.target.value } ) )
}
className = 'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent'
/ >
< div className = 'mb-6' >
< div className = 'bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 ' >
< div className = 'flex items-center space-x-2 mb-2' >
< svg
className = 'w-5 h-5 text-blue-600 dark:text-blue-400'
fill = 'none'
stroke = 'currentColor'
viewBox = '0 0 24 24'
>
< path
strokeLinecap = 'round'
strokeLinejoin = 'round'
strokeWidth = { 2 }
d = 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z'
/ >
< / svg >
< span className = 'text-sm font-medium text-blue-800 dark:text-blue-300' >
配 置 说 明
< / span >
< / div >
< p className = 'text-sm text-blue-700 dark:text-blue-400 mt-1' >
提 示 : 全 不 选 为 无 限 制 , 选 中 的 采 集 源 将 限 制 用 户 只 能 访 问 这 些 源
< / p >
< / div >
< / div >
{ /* 可用视频源 */ }
< div >
< label className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-4' >
可 用 视 频 源
< / label >
< div className = 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 ' >
{ /* 采集源选择 - 多列布局 */ }
< div className = 'mb-6' >
< h4 className = 'text-sm font-medium text-gray-700 dark:text-gray-300 mb-4' >
选 择 可 用 的 采 集 源 :
< / h4 >
< div className = 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 ' >
{ config ? . SourceConfig ? . map ( ( source ) = > (
< label key = { source . key } className = 'flex items-center space-x-3 p-3 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer transition-colors' >
< label
key = { source . key }
className = 'flex items-center space-x-3 p-3 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer transition-colors'
>
< input
type = 'checkbox'
checked = { newUserGroup . enabledApis . includes ( source . key ) }
checked = { selectedApis . includes ( source . key ) }
onChange = { ( e ) = > {
if ( e . target . checked ) {
setNewUserGroup ( prev = > ( {
. . . prev ,
enabledApis : [ . . . prev . enabledApis , source . key ]
} ) ) ;
setSelectedApis ( [ . . . selectedApis , source . key ] ) ;
} else {
setNewUserGroup ( prev = > ( {
. . . prev ,
enabledApis : prev.enabledApis.filter ( api = > api !== source . key )
} ) ) ;
setSelectedApis (
selectedApis . filter ( ( api ) = > api !== source . key )
) ;
}
} }
className = 'rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700'
@@ -1499,479 +1561,511 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
< / label >
) ) }
< / div >
< / div >
{ /* 快速操作按钮 */ }
< div className = 'mt-4 flex space-x-2 ' >
{ /* 快速操作按钮 */ }
< div className = 'flex flex-wrap items-center justify-between mb-6 p-4 bg-gray-50 dark:bg-gray-900 rounded-lg ' >
< div className = 'flex space-x-2' >
< button
onClick = { ( ) = > setNewUserGroup ( prev = > ( { . . . prev , enabledApis : [ ] } ) ) }
onClick = { ( ) = > setSelectedApis ( [ ] ) }
className = { buttonStyles . quickAction }
>
全 不 选 ( 无 限 制 )
< / button >
< button
onClick = { ( ) = > {
const allApis = config ? . SourceConfig ? . filter ( source = > ! source . disabled ) . map ( s = > s . key ) || [ ] ;
setNewUserGroup ( prev = > ( { . . . prev , enabledApis : allApis } ) ) ;
const allApis =
config ? . SourceConfig ? . filter (
( source ) = > ! source . disabled
) . map ( ( s ) = > s . key ) || [ ] ;
setSelectedApis ( allApis ) ;
} }
className = { buttonStyles . quickAction }
>
全 选
< / button >
< / div >
< / div >
{ /* 操作按钮 */ }
< div className = 'flex justify-end space-x-3 pt-4 border-t border-gray-200 dark:border-gray-700' >
< button
onClick = { ( ) = > {
setShowAddUserGroupForm ( false ) ;
setNewUserGroup ( { name : '' , enabledApis : [ ] } ) ;
} }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . secondary } ` }
>
取 消
< / button >
< button
onClick = { handleAddUserGroup }
disabled = { ! newUserGroup . name . trim ( ) || isLoading ( 'userGroup_add_new' ) }
className = { ` px-6 py-2.5 text-sm font-medium ${ ! newUserGroup . name . trim ( ) || isLoading ( 'userGroup_add_new' ) ? buttonStyles.disabled : buttonStyles.primary } ` }
>
{ isLoading ( 'userGroup_add_new' ) ? '添加中...' : '添加用户组' }
< / button >
< / div >
< / div >
< / div >
< / div >
< / div > ,
document . body
) }
{ /* 编辑用户组弹窗 */ }
{ showEditUserGroupForm && editingUserGroup && createPortal (
< div className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4' onClick = { ( ) = > {
setShowEditUserGroupForm ( false ) ;
setEditingUserGroup ( null ) ;
} } >
< div className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[80vh] overflow-y-auto' onClick = { ( e ) = > e . stopPropagation ( ) } >
< div className = 'p-6' >
< div className = 'flex items-center justify-between mb-6' >
< h3 className = 'text-xl font-semibold text-gray-900 dark:text-gray-100' >
编 辑 用 户 组 - { editingUserGroup . name }
< / h3 >
< button
onClick = { ( ) = > {
setShowEditUserGroupForm ( false ) ;
setEditingUserGroup ( null ) ;
} }
className = 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
< svg className = 'w-6 h-6' fill = 'none' stroke = 'currentColor' viewBox = '0 0 24 24' >
< path strokeLinecap = 'round' strokeLinejoin = 'round' strokeWidth = { 2 } d = 'M6 18L18 6M6 6l12 12' / >
< / svg >
< / button >
< / div >
< div className = 'space-y-6' >
{ /* 可用视频源 */ }
< div >
< label className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-4' >
可 用 视 频 源
< / label >
< div className = 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3' >
{ config ? . SourceConfig ? . map ( ( source ) = > (
< label key = { source . key } className = 'flex items-center space-x-3 p-3 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer transition-colors' >
< input
type = 'checkbox'
checked = { editingUserGroup . enabledApis . includes ( source . key ) }
onChange = { ( e ) = > {
if ( e . target . checked ) {
setEditingUserGroup ( prev = > prev ? {
. . . prev ,
enabledApis : [ . . . prev . enabledApis , source . key ]
} : null ) ;
} else {
setEditingUserGroup ( prev = > prev ? {
. . . prev ,
enabledApis : prev.enabledApis.filter ( api = > api !== source . key )
} : null ) ;
}
} }
className = 'rounded border-gray-300 text-purple-600 focus:ring-purple-500 dark:border-gray-600 dark:bg-gray-700'
/ >
< div className = 'flex-1 min-w-0' >
< div className = 'text-sm font-medium text-gray-900 dark:text-gray-100 truncate' >
{ source . name }
< / div >
{ source . api && (
< div className = 'text-xs text-gray-500 dark:text-gray-400 truncate' >
{ extractDomain ( source . api ) }
< / div >
) }
< / div >
< / label >
) ) }
< / div >
{ /* 快速操作按钮 */ }
< div className = 'mt-4 flex space-x-2' >
< button
onClick = { ( ) = > setEditingUserGroup ( prev = > prev ? { . . . prev , enabledApis : [ ] } : null ) }
className = { buttonStyles . quickAction }
>
全 不 选 ( 无 限 制 )
< / button >
< button
onClick = { ( ) = > {
const allApis = config ? . SourceConfig ? . filter ( source = > ! source . disabled ) . map ( s = > s . key ) || [ ] ;
setEditingUserGroup ( prev = > prev ? { . . . prev , enabledApis : allApis } : null ) ;
} }
className = { buttonStyles . quickAction }
>
全 选
< / button >
< / div >
< / div >
{ /* 操作按钮 */ }
< div className = 'flex justify-end space-x-3 pt-4 border-t border-gray-200 dark:border-gray-700' >
< button
onClick = { ( ) = > {
setShowEditUserGroupForm ( false ) ;
setEditingUserGroup ( null ) ;
} }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . secondary } ` }
>
取 消
< / button >
< button
onClick = { handleEditUserGroup }
disabled = { isLoading ( ` userGroup_edit_ ${ editingUserGroup ? . name } ` ) }
className = { ` px-6 py-2.5 text-sm font-medium ${ isLoading ( ` userGroup_edit_ ${ editingUserGroup ? . name } ` ) ? buttonStyles.disabled : buttonStyles.primary } ` }
>
{ isLoading ( ` userGroup_edit_ ${ editingUserGroup ? . name } ` ) ? '保存中...' : '保存修改' }
< / button >
< / div >
< / div >
< / div >
< / div >
< / div > ,
document . body
) }
{ /* 配置用户组弹窗 */ }
{ showConfigureUserGroupModal && selectedUserForGroup && createPortal (
< div className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4' onClick = { ( ) = > {
setShowConfigureUserGroupModal ( false ) ;
setSelectedUserForGroup ( null ) ;
setSelectedUserGroups ( [ ] ) ;
} } >
< div className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[80vh] overflow-y-auto' onClick = { ( e ) = > e . stopPropagation ( ) } >
< div className = 'p-6' >
< div className = 'flex items-center justify-between mb-6' >
< h3 className = 'text-xl font-semibold text-gray-900 dark:text-gray-100' >
配 置 用 户 组 - { selectedUserForGroup . username }
< / h3 >
< button
onClick = { ( ) = > {
setShowConfigureUserGroupModal ( false ) ;
setSelectedUserForGroup ( null ) ;
setSelectedUserGroups ( [ ] ) ;
} }
className = 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
< svg className = 'w-6 h-6' fill = 'none' stroke = 'currentColor' viewBox = '0 0 24 24' >
< path strokeLinecap = 'round' strokeLinejoin = 'round' strokeWidth = { 2 } d = 'M6 18L18 6M6 6l12 12' / >
< / svg >
< / button >
< / div >
< div className = 'mb-6' >
< div className = 'bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4' >
< div className = 'flex items-center space-x-2 mb-2' >
< svg className = 'w-5 h-5 text-blue-600 dark:text-blue-400' fill = 'none' stroke = 'currentColor' viewBox = '0 0 24 24' >
< path strokeLinecap = 'round' strokeLinejoin = 'round' strokeWidth = { 2 } d = 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' / >
< / svg >
< span className = 'text-sm font-medium text-blue-800 dark:text-blue-300' >
配 置 说 明
< div className = 'text-sm text-gray-600 dark:text-gray-400' >
已 选 择 :
< span className = 'font-medium text-blue-600 dark:text-blue-400' >
{ selectedApis . length > 0
? ` ${ selectedApis . length } 个源 `
: '无限制' }
< / span >
< / div >
< p className = 'text-sm text-blue-700 dark:text-blue-400 mt-1' >
提 示 : 选 择 "无用户组" 为 无 限 制 , 选 择 特 定 用 户 组 将 限 制 用 户 只 能 访 问 该 用 户 组 允 许 的 采 集 源
< / p >
< / div >
< / div >
{ /* 用户组选择 - 下拉选择器 */ }
< div className = 'mb-6' >
< label className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2' >
选 择 用 户 组 :
< / label >
< select
value = { selectedUserGroups . length > 0 ? selectedUserGroups [ 0 ] : '' }
onChange = { ( e ) = > {
const value = e . target . value ;
setSelectedUserGroups ( value ? [ value ] : [ ] ) ;
} }
className = 'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors'
>
< option value = '' > 无 用 户 组 ( 无 限 制 ) < / option >
{ userGroups . map ( ( group ) = > (
< option key = { group . name } value = { group . name } >
{ group . name } { group . enabledApis && group . enabledApis . length > 0 ? ` ( ${ group . enabledApis . length } 个源) ` : '' }
< / option >
) ) }
< / select >
< p className = 'mt-2 text-xs text-gray-500 dark:text-gray-400' >
选 择 "无用户组" 为 无 限 制 , 选 择 特 定 用 户 组 将 限 制 用 户 只 能 访 问 该 用 户 组 允 许 的 采 集 源
< / p >
< / div >
{ /* 操作按钮 */ }
< div className = 'flex justify-end space-x-3' >
< button
onClick = { ( ) = > {
setShowConfigureUserGroupModal ( false ) ;
setSelectedUserForGroup ( null ) ;
setSelectedUserGroups ( [ ] ) ;
} }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . secondary } ` }
>
取 消
< / button >
< button
onClick = { handleSaveUserGroups }
disabled = { isLoading ( ` saveUserGroups_ ${ selectedUserForGroup ? . username } ` ) }
className = { ` px-6 py-2.5 text-sm font-medium ${ isLoading ( ` saveUserGroups_ ${ selectedUserForGroup ? . username } ` ) ? buttonStyles.disabled : buttonStyles.primary } ` }
>
{ isLoading ( ` saveUserGroups_ ${ selectedUserForGroup ? . username } ` ) ? '配置中...' : '确认配置' }
< / button >
< / div >
< / div >
< / div >
< / div > ,
document . body
) }
{ /* 删除用户组确认弹窗 */ }
{ showDeleteUserGroupModal && deletingUserGroup && createPortal (
< div className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4' onClick = { ( ) = > {
setShowDeleteUserGroupModal ( false ) ;
setDeletingUserGroup ( null ) ;
} } >
< div className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full' onClick = { ( e ) = > e . stopPropagation ( ) } >
< div className = 'p-6' >
< div className = 'flex items-center justify-between mb-6' >
< h3 className = 'text-xl font-semibold text-gray-900 dark:text-gray-100' >
确 认 删 除 用 户 组
< / h3 >
< button
onClick = { ( ) = > {
setShowDeleteUserGroupModal ( false ) ;
setDeletingUserGroup ( null ) ;
} }
className = 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
< svg className = 'w-6 h-6' fill = 'none' stroke = 'currentColor' viewBox = '0 0 24 24' >
< path strokeLinecap = 'round' strokeLinejoin = 'round' strokeWidth = { 2 } d = 'M6 18L18 6M6 6l12 12' / >
< / svg >
< / button >
< / div >
< div className = 'mb-6' >
< div className = 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-4' >
< div className = 'flex items-center space-x-2 mb-2' >
< svg className = 'w-5 h-5 text-red-600 dark:text-red-400' fill = 'none' stroke = 'currentColor' viewBox = '0 0 24 24' >
< path strokeLinecap = 'round' strokeLinejoin = 'round' strokeWidth = { 2 } d = 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z' / >
< / svg >
< span className = 'text-sm font-medium text-red-800 dark:text-red-300' >
危 险 操 作 警 告
< / span >
< / div >
< p className = 'text-sm text-red-700 dark:text-red-400' >
删 除 用 户 组 < strong > { deletingUserGroup . name } < / strong > 将 影 响 所 有 使 用 该 组 的 用 户 , 此 操 作 不 可 恢 复 !
< / p >
< / div >
{ deletingUserGroup . affectedUsers . length > 0 ? (
< div className = 'bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4' >
< div className = 'flex items-center space-x-2 mb-2' >
< svg className = 'w-5 h-5 text-yellow-600 dark:text-yellow-400' fill = 'none' stroke = 'currentColor' viewBox = '0 0 24 24' >
< path strokeLinecap = 'round' strokeLinejoin = 'round' strokeWidth = { 2 } d = 'M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' / >
< / svg >
< span className = 'text-sm font-medium text-yellow-800 dark:text-yellow-300' >
⚠ ️ 将 影 响 { deletingUserGroup . affectedUsers . length } 个 用 户 :
< / span >
< / div >
< div className = 'space-y-1' >
{ deletingUserGroup . affectedUsers . map ( ( user , index ) = > (
< div key = { index } className = 'text-sm text-yellow-700 dark:text-yellow-300' >
• { user . username } ( { user . role } )
< / div >
) ) }
< / div >
< p className = 'text-xs text-yellow-600 dark:text-yellow-400 mt-2' >
这 些 用 户 的 用 户 组 将 被 自 动 移 除
< / p >
< / div >
) : (
< div className = 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4' >
< div className = 'flex items-center space-x-2' >
< svg className = 'w-5 h-5 text-green-600 dark:text-green-400' fill = 'none' stroke = 'currentColor' viewBox = '0 0 24 24' >
< path strokeLinecap = 'round' strokeLinejoin = 'round' strokeWidth = { 2 } d = 'M5 13l4 4L19 7' / >
< / svg >
< span className = 'text-sm font-medium text-green-800 dark:text-green-300' >
✅ 当 前 没 有 用 户 使 用 此 用 户 组
< / span >
< / div >
< / div >
) }
< / div >
{ /* 操作按钮 */ }
< div className = 'flex justify-end space-x-3' >
< button
onClick = { ( ) = > {
setShowDeleteUserGroupModal ( false ) ;
setDeletingUserGroup ( null ) ;
} }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . secondary } ` }
>
取 消
< / button >
< button
onClick = { handleConfirmDeleteUserGroup }
disabled = { isLoading ( ` userGroup_delete_ ${ deletingUserGroup ? . name } ` ) }
className = { ` px-6 py-2.5 text-sm font-medium ${ isLoading ( ` userGroup_delete_ ${ deletingUserGroup ? . name } ` ) ? buttonStyles.disabled : buttonStyles.danger } ` }
>
{ isLoading ( ` userGroup_delete_ ${ deletingUserGroup ? . name } ` ) ? '删除中...' : '确认删除' }
< / button >
< / div >
< / div >
< / div >
< / div > ,
document . body
) }
{ /* 删除用户确认弹窗 */ }
{ showDeleteUserModal && deletingUser && createPortal (
< div className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4' onClick = { ( ) = > {
setShowDeleteUserModal ( false ) ;
setDeletingUser ( null ) ;
} } >
< div className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full' onClick = { ( e ) = > e . stopPropagation ( ) } >
< div className = 'p-6' >
< div className = 'flex items-center justify-between mb-6' >
< h3 className = 'text-xl font-semibold text-gray-900 dark:text-gray-100' >
确 认 删 除 用 户
< / h3 >
< button
onClick = { ( ) = > {
setShowDeleteUserModal ( false ) ;
setDeletingUser ( null ) ;
} }
className = 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
< svg className = 'w-6 h-6' fill = 'none' stroke = 'currentColor' viewBox = '0 0 24 24' >
< path strokeLinecap = 'round' strokeLinejoin = 'round' strokeWidth = { 2 } d = 'M6 18L18 6M6 6l12 12' / >
< / svg >
< / button >
< / div >
< div className = 'mb-6' >
< div className = 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-4' >
< div className = 'flex items-center space-x-2 mb-2' >
< svg className = 'w-5 h-5 text-red-600 dark:text-red-400' fill = 'none' stroke = 'currentColor' viewBox = '0 0 24 24' >
< path strokeLinecap = 'round' strokeLinejoin = 'round' strokeWidth = { 2 } d = 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z' / >
< / svg >
< span className = 'text-sm font-medium text-red-800 dark:text-red-300' >
危 险 操 作 警 告
< / span >
< / div >
< p className = 'text-sm text-red-700 dark:text-red-400' >
删 除 用 户 < strong > { deletingUser } < / strong > 将 同 时 删 除 其 搜 索 历 史 、 播 放 记 录 和 收 藏 夹 , 此 操 作 不 可 恢 复 !
< / p >
< / div >
{ /* 操作按钮 */ }
< div className = 'flex justify-end space-x-3' >
< button
onClick = { ( ) = > {
setShowDeleteUserModal ( false ) ;
setDeletingUser ( null ) ;
setShowConfigureApisModal ( false ) ;
setSelectedUser ( null ) ;
setSelectedApis ( [ ] ) ;
} }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . secondary } ` }
>
取 消
< / button >
< button
onClick = { handleConfirmDeleteUser }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . danger } ` }
onClick = { handleSaveUserApis }
disabled = { isLoading (
` saveUserApis_ ${ selectedUser ? . username } `
) }
className = { ` px-6 py-2.5 text-sm font-medium ${
isLoading ( ` saveUserApis_ ${ selectedUser ? . username } ` )
? buttonStyles.disabled
: buttonStyles.primary
} ` }
>
确 认 删 除
{ isLoading ( ` saveUserApis_ ${ selectedUser ? . username } ` )
? '配置中...'
: '确认配置' }
< / button >
< / div >
< / div >
< / div >
< / div >
< / div > ,
document . body
) }
< / div > ,
document . body
) }
{ /* 批量设置 用户组弹窗 */ }
{ showBatchUserGroupModal && createPortal (
< div className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4' onClick = { ( ) = > {
setShowBatchUserGroupModal ( false ) ;
setSelectedUserGroup ( '' ) ;
} } >
< div className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full' onClick = { ( e ) = > e . stopPropagation ( ) } >
< div className = 'p-6' >
< div className = 'flex items-center justify-between mb-6' >
< h3 className = 'text-xl font-semibold text-gray-900 dark:text-gray-100' >
批 量 设 置 用 户 组
< / h3 >
< button
onClick = { ( ) = > {
setShowBatchUserGroupModal ( false ) ;
setSelectedUserGroup ( '' ) ;
} }
className = 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
< svg className = 'w-6 h-6' fill = 'none' stroke = 'currentColor' viewBox = '0 0 24 24' >
< path strokeLinecap = 'round' strokeLinejoin = 'round' strokeWidth = { 2 } d = 'M6 18L18 6M6 6l12 12' / >
< / svg >
< / button >
< / div >
< div className = 'mb-6' >
< div className = 'bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-4' >
< div className = 'flex items-center space-x-2 mb-2' >
< svg className = 'w-5 h-5 text-blue-600 dark:text-blue-400' fill = 'none' stroke = 'currentColor' viewBox = '0 0 24 24' >
< path strokeLinecap = 'round' strokeLinejoin = 'round' strokeWidth = { 2 } d = 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' / >
{ /* 添加 用户组弹窗 */ }
{ showAddUserGroupForm &&
createPortal (
< div
className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'
onClick = { ( ) = > {
setShowAddUserGroupForm ( false ) ;
setNewUserGroup ( { name : '' , enabledApis : [ ] } ) ;
} }
>
< div
className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[80vh] overflow-y-auto'
onClick = { ( e ) = > e . stopPropagation ( ) }
>
< div className = 'p-6' >
< div className = 'flex items-center justify-between mb-6' >
< h3 className = 'text-xl font-semibold text-gray-900 dark:text-gray-100' >
添 加 新 用 户 组
< / h3 >
< button
onClick = { ( ) = > {
setShowAddUserGroupForm ( false ) ;
setNewUserGroup ( { name : '' , enabledApis : [ ] } ) ;
} }
className = 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
< svg
className = 'w-6 h-6'
fill = 'none'
stroke = 'currentColor'
viewBox = '0 0 24 24'
>
< path
strokeLinecap = 'round'
strokeLinejoin = 'round'
strokeWidth = { 2 }
d = 'M6 18L18 6M6 6l12 12'
/ >
< / svg >
< span className = 'text-sm font-medium text-blue-800 dark:text-blue-300' >
批 量 操 作 说 明
< / span >
< / div >
< p className = 'text-sm text-blue-700 dark:text-blue-400' >
将 为 选 中 的 < strong > { selectedUsers . size } 个 用 户 < / strong > 设 置 用 户 组 , 选 择 "无用户组" 为 无 限 制
< / p >
< / button >
< / div >
< div >
< div className = 'space-y-6' >
{ /* 用户组名称 */ }
< div >
< label className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2' >
用 户 组 名 称
< / label >
< input
type = 'text'
placeholder = '请输入用户组名称'
value = { newUserGroup . name }
onChange = { ( e ) = >
setNewUserGroup ( ( prev ) = > ( {
. . . prev ,
name : e.target.value ,
} ) )
}
className = 'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent'
/ >
< / div >
{ /* 可用视频源 */ }
< div >
< label className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-4' >
可 用 视 频 源
< / label >
< div className = 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3' >
{ config ? . SourceConfig ? . map ( ( source ) = > (
< label
key = { source . key }
className = 'flex items-center space-x-3 p-3 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer transition-colors'
>
< input
type = 'checkbox'
checked = { newUserGroup . enabledApis . includes (
source . key
) }
onChange = { ( e ) = > {
if ( e . target . checked ) {
setNewUserGroup ( ( prev ) = > ( {
. . . prev ,
enabledApis : [
. . . prev . enabledApis ,
source . key ,
] ,
} ) ) ;
} else {
setNewUserGroup ( ( prev ) = > ( {
. . . prev ,
enabledApis : prev.enabledApis.filter (
( api ) = > api !== source . key
) ,
} ) ) ;
}
} }
className = 'rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700'
/ >
< div className = 'flex-1 min-w-0' >
< div className = 'text-sm font-medium text-gray-900 dark:text-gray-100 truncate' >
{ source . name }
< / div >
{ source . api && (
< div className = 'text-xs text-gray-500 dark:text-gray-400 truncate' >
{ extractDomain ( source . api ) }
< / div >
) }
< / div >
< / label >
) ) }
< / div >
{ /* 快速操作按钮 */ }
< div className = 'mt-4 flex space-x-2' >
< button
onClick = { ( ) = >
setNewUserGroup ( ( prev ) = > ( {
. . . prev ,
enabledApis : [ ] ,
} ) )
}
className = { buttonStyles . quickAction }
>
全 不 选 ( 无 限 制 )
< / button >
< button
onClick = { ( ) = > {
const allApis =
config ? . SourceConfig ? . filter (
( source ) = > ! source . disabled
) . map ( ( s ) = > s . key ) || [ ] ;
setNewUserGroup ( ( prev ) = > ( {
. . . prev ,
enabledApis : allApis ,
} ) ) ;
} }
className = { buttonStyles . quickAction }
>
全 选
< / button >
< / div >
< / div >
{ /* 操作按钮 */ }
< div className = 'flex justify-end space-x-3 pt-4 border-t border-gray-200 dark:border-gray-700' >
< button
onClick = { ( ) = > {
setShowAddUserGroupForm ( false ) ;
setNewUserGroup ( { name : '' , enabledApis : [ ] } ) ;
} }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . secondary } ` }
>
取 消
< / button >
< button
onClick = { handleAddUserGroup }
disabled = {
! newUserGroup . name . trim ( ) ||
isLoading ( 'userGroup_add_new' )
}
className = { ` px-6 py-2.5 text-sm font-medium ${
! newUserGroup . name . trim ( ) ||
isLoading ( 'userGroup_add_new' )
? buttonStyles.disabled
: buttonStyles.primary
} ` }
>
{ isLoading ( 'userGroup_add_new' )
? '添加中...'
: '添加用户组' }
< / button >
< / div >
< / div >
< / div >
< / div >
< / div > ,
document . body
) }
{ /* 编辑用户组弹窗 */ }
{ showEditUserGroupForm &&
editingUserGroup &&
createPortal (
< div
className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'
onClick = { ( ) = > {
setShowEditUserGroupForm ( false ) ;
setEditingUserGroup ( null ) ;
} }
>
< div
className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[80vh] overflow-y-auto'
onClick = { ( e ) = > e . stopPropagation ( ) }
>
< div className = 'p-6' >
< div className = 'flex items-center justify-between mb-6' >
< h3 className = 'text-xl font-semibold text-gray-900 dark:text-gray-100' >
编 辑 用 户 组 - { editingUserGroup . name }
< / h3 >
< button
onClick = { ( ) = > {
setShowEditUserGroupForm ( false ) ;
setEditingUserGroup ( null ) ;
} }
className = 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
< svg
className = 'w-6 h-6'
fill = 'none'
stroke = 'currentColor'
viewBox = '0 0 24 24'
>
< path
strokeLinecap = 'round'
strokeLinejoin = 'round'
strokeWidth = { 2 }
d = 'M6 18L18 6M6 6l12 12'
/ >
< / svg >
< / button >
< / div >
< div className = 'space-y-6' >
{ /* 可用视频源 */ }
< div >
< label className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-4' >
可 用 视 频 源
< / label >
< div className = 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3' >
{ config ? . SourceConfig ? . map ( ( source ) = > (
< label
key = { source . key }
className = 'flex items-center space-x-3 p-3 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer transition-colors'
>
< input
type = 'checkbox'
checked = { editingUserGroup . enabledApis . includes (
source . key
) }
onChange = { ( e ) = > {
if ( e . target . checked ) {
setEditingUserGroup ( ( prev ) = >
prev
? {
. . . prev ,
enabledApis : [
. . . prev . enabledApis ,
source . key ,
] ,
}
: null
) ;
} else {
setEditingUserGroup ( ( prev ) = >
prev
? {
. . . prev ,
enabledApis : prev.enabledApis.filter (
( api ) = > api !== source . key
) ,
}
: null
) ;
}
} }
className = 'rounded border-gray-300 text-purple-600 focus:ring-purple-500 dark:border-gray-600 dark:bg-gray-700'
/ >
< div className = 'flex-1 min-w-0' >
< div className = 'text-sm font-medium text-gray-900 dark:text-gray-100 truncate' >
{ source . name }
< / div >
{ source . api && (
< div className = 'text-xs text-gray-500 dark:text-gray-400 truncate' >
{ extractDomain ( source . api ) }
< / div >
) }
< / div >
< / label >
) ) }
< / div >
{ /* 快速操作按钮 */ }
< div className = 'mt-4 flex space-x-2' >
< button
onClick = { ( ) = >
setEditingUserGroup ( ( prev ) = >
prev ? { . . . prev , enabledApis : [ ] } : null
)
}
className = { buttonStyles . quickAction }
>
全 不 选 ( 无 限 制 )
< / button >
< button
onClick = { ( ) = > {
const allApis =
config ? . SourceConfig ? . filter (
( source ) = > ! source . disabled
) . map ( ( s ) = > s . key ) || [ ] ;
setEditingUserGroup ( ( prev ) = >
prev ? { . . . prev , enabledApis : allApis } : null
) ;
} }
className = { buttonStyles . quickAction }
>
全 选
< / button >
< / div >
< / div >
{ /* 操作按钮 */ }
< div className = 'flex justify-end space-x-3 pt-4 border-t border-gray-200 dark:border-gray-700' >
< button
onClick = { ( ) = > {
setShowEditUserGroupForm ( false ) ;
setEditingUserGroup ( null ) ;
} }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . secondary } ` }
>
取 消
< / button >
< button
onClick = { handleEditUserGroup }
disabled = { isLoading (
` userGroup_edit_ ${ editingUserGroup ? . name } `
) }
className = { ` px-6 py-2.5 text-sm font-medium ${
isLoading ( ` userGroup_edit_ ${ editingUserGroup ? . name } ` )
? buttonStyles.disabled
: buttonStyles.primary
} ` }
>
{ isLoading ( ` userGroup_edit_ ${ editingUserGroup ? . name } ` )
? '保存中...'
: '保存修改' }
< / button >
< / div >
< / div >
< / div >
< / div >
< / div > ,
document . body
) }
{ /* 配置用户组弹窗 */ }
{ showConfigureUserGroupModal &&
selectedUserForGroup &&
createPortal (
< div
className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'
onClick = { ( ) = > {
setShowConfigureUserGroupModal ( false ) ;
setSelectedUserForGroup ( null ) ;
setSelectedUserGroups ( [ ] ) ;
} }
>
< div
className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[80vh] overflow-y-auto'
onClick = { ( e ) = > e . stopPropagation ( ) }
>
< div className = 'p-6' >
< div className = 'flex items-center justify-between mb-6' >
< h3 className = 'text-xl font-semibold text-gray-900 dark:text-gray-100' >
配 置 用 户 组 - { selectedUserForGroup . username }
< / h3 >
< button
onClick = { ( ) = > {
setShowConfigureUserGroupModal ( false ) ;
setSelectedUserForGroup ( null ) ;
setSelectedUserGroups ( [ ] ) ;
} }
className = 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
< svg
className = 'w-6 h-6'
fill = 'none'
stroke = 'currentColor'
viewBox = '0 0 24 24'
>
< path
strokeLinecap = 'round'
strokeLinejoin = 'round'
strokeWidth = { 2 }
d = 'M6 18L18 6M6 6l12 12'
/ >
< / svg >
< / button >
< / div >
< div className = 'mb-6' >
< div className = 'bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4' >
< div className = 'flex items-center space-x-2 mb-2' >
< svg
className = 'w-5 h-5 text-blue-600 dark:text-blue-400'
fill = 'none'
stroke = 'currentColor'
viewBox = '0 0 24 24'
>
< path
strokeLinecap = 'round'
strokeLinejoin = 'round'
strokeWidth = { 2 }
d = 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z'
/ >
< / svg >
< span className = 'text-sm font-medium text-blue-800 dark:text-blue-300' >
配 置 说 明
< / span >
< / div >
< p className = 'text-sm text-blue-700 dark:text-blue-400 mt-1' >
提 示 : 选 择 "无用户组" 为 无 限 制 , 选 择 特 定 用 户 组 将 限 制 用 户 只 能 访 问 该 用 户 组 允 许 的 采 集 源
< / p >
< / div >
< / div >
{ /* 用户组选择 - 下拉选择器 */ }
< div className = 'mb-6' >
< label className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2' >
选 择 用 户 组 :
< / label >
< select
onChange = { ( e ) = > setSelectedUserGroup ( e . target . value ) }
value = {
selectedUserGroups . length > 0 ? selectedUserGroups [ 0 ] : ''
}
onChange = { ( e ) = > {
const value = e . target . value ;
setSelectedUserGroups ( value ? [ value ] : [ ] ) ;
} }
className = 'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors'
value = { selectedUserGroup }
>
< option value = '' > 无 用 户 组 ( 无 限 制 ) < / option >
{ userGroups . map ( ( group ) = > (
< option key = { group . name } value = { group . name } >
{ group . name } { group . enabledApis && group . enabledApis . length > 0 ? ` ( ${ group . enabledApis . length } 个源) ` : '' }
{ group . name } { ' ' }
{ group . enabledApis && group . enabledApis . length > 0
? ` ( ${ group . enabledApis . length } 个源) `
: '' }
< / option >
) ) }
< / select >
@@ -1979,32 +2073,421 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
选 择 "无用户组" 为 无 限 制 , 选 择 特 定 用 户 组 将 限 制 用 户 只 能 访 问 该 用 户 组 允 许 的 采 集 源
< / p >
< / div >
< / div >
{ /* 操作按钮 */ }
< div className = 'flex justify-end space-x-3' >
< button
onClick = { ( ) = > {
setShowBatchUserGroupModal ( false ) ;
setSelectedUserGroup ( '' ) ;
} }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . secondary } ` }
>
取 消
< / button >
< button
onClick = { ( ) = > handleBatchSetUserGroup ( selectedUserGroup ) }
disabled = { isLoading ( 'batchSetUserGroup' ) }
className = { ` px-6 py-2.5 text-sm font-medium ${ isLoading ( 'batchSetUserGroup' ) ? buttonStyles.disabled : buttonStyles.primary } ` }
>
{ isLoading ( 'batchSetUserGroup' ) ? '设置中...' : '确认设置' }
< / button >
{ /* 操作按钮 */ }
< div className = 'flex justify-end space-x-3' >
< button
onClick = { ( ) = > {
setShowConfigureUserGroupModal ( false ) ;
setSelectedUserForGroup ( null ) ;
setSelectedUserGroups ( [ ] ) ;
} }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . secondary } ` }
>
取 消
< / button >
< button
onClick = { handleSaveUserGroups }
disabled = { isLoading (
` saveUserGroups_ ${ selectedUserForGroup ? . username } `
) }
className = { ` px-6 py-2.5 text-sm font-medium ${
isLoading (
` saveUserGroups_ ${ selectedUserForGroup ? . username } `
)
? buttonStyles.disabled
: buttonStyles.primary
} ` }
>
{ isLoading (
` saveUserGroups_ ${ selectedUserForGroup ? . username } `
)
? '配置中...'
: '确认配置' }
< / button >
< / div >
< / div >
< / div >
< / div >
< / div > ,
document . body
) }
< / div > ,
document . body
) }
{ /* 删除用户组确认弹窗 */ }
{ showDeleteUserGroupModal &&
deletingUserGroup &&
createPortal (
< div
className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'
onClick = { ( ) = > {
setShowDeleteUserGroupModal ( false ) ;
setDeletingUserGroup ( null ) ;
} }
>
< div
className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full'
onClick = { ( e ) = > e . stopPropagation ( ) }
>
< div className = 'p-6' >
< div className = 'flex items-center justify-between mb-6' >
< h3 className = 'text-xl font-semibold text-gray-900 dark:text-gray-100' >
确 认 删 除 用 户 组
< / h3 >
< button
onClick = { ( ) = > {
setShowDeleteUserGroupModal ( false ) ;
setDeletingUserGroup ( null ) ;
} }
className = 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
< svg
className = 'w-6 h-6'
fill = 'none'
stroke = 'currentColor'
viewBox = '0 0 24 24'
>
< path
strokeLinecap = 'round'
strokeLinejoin = 'round'
strokeWidth = { 2 }
d = 'M6 18L18 6M6 6l12 12'
/ >
< / svg >
< / button >
< / div >
< div className = 'mb-6' >
< div className = 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-4' >
< div className = 'flex items-center space-x-2 mb-2' >
< svg
className = 'w-5 h-5 text-red-600 dark:text-red-400'
fill = 'none'
stroke = 'currentColor'
viewBox = '0 0 24 24'
>
< path
strokeLinecap = 'round'
strokeLinejoin = 'round'
strokeWidth = { 2 }
d = 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z'
/ >
< / svg >
< span className = 'text-sm font-medium text-red-800 dark:text-red-300' >
危 险 操 作 警 告
< / span >
< / div >
< p className = 'text-sm text-red-700 dark:text-red-400' >
删 除 用 户 组 < strong > { deletingUserGroup . name } < / strong > { ' ' }
将 影 响 所 有 使 用 该 组 的 用 户 , 此 操 作 不 可 恢 复 !
< / p >
< / div >
{ deletingUserGroup . affectedUsers . length > 0 ? (
< div className = 'bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4' >
< div className = 'flex items-center space-x-2 mb-2' >
< svg
className = 'w-5 h-5 text-yellow-600 dark:text-yellow-400'
fill = 'none'
stroke = 'currentColor'
viewBox = '0 0 24 24'
>
< path
strokeLinecap = 'round'
strokeLinejoin = 'round'
strokeWidth = { 2 }
d = 'M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z'
/ >
< / svg >
< span className = 'text-sm font-medium text-yellow-800 dark:text-yellow-300' >
⚠ ️ 将 影 响 { deletingUserGroup . affectedUsers . length } { ' ' }
个 用 户 :
< / span >
< / div >
< div className = 'space-y-1' >
{ deletingUserGroup . affectedUsers . map ( ( user , index ) = > (
< div
key = { index }
className = 'text-sm text-yellow-700 dark:text-yellow-300'
>
• { user . username } ( { user . role } )
< / div >
) ) }
< / div >
< p className = 'text-xs text-yellow-600 dark:text-yellow-400 mt-2' >
这 些 用 户 的 用 户 组 将 被 自 动 移 除
< / p >
< / div >
) : (
< div className = 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4' >
< div className = 'flex items-center space-x-2' >
< svg
className = 'w-5 h-5 text-green-600 dark:text-green-400'
fill = 'none'
stroke = 'currentColor'
viewBox = '0 0 24 24'
>
< path
strokeLinecap = 'round'
strokeLinejoin = 'round'
strokeWidth = { 2 }
d = 'M5 13l4 4L19 7'
/ >
< / svg >
< span className = 'text-sm font-medium text-green-800 dark:text-green-300' >
✅ 当 前 没 有 用 户 使 用 此 用 户 组
< / span >
< / div >
< / div >
) }
< / div >
{ /* 操作按钮 */ }
< div className = 'flex justify-end space-x-3' >
< button
onClick = { ( ) = > {
setShowDeleteUserGroupModal ( false ) ;
setDeletingUserGroup ( null ) ;
} }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . secondary } ` }
>
取 消
< / button >
< button
onClick = { handleConfirmDeleteUserGroup }
disabled = { isLoading (
` userGroup_delete_ ${ deletingUserGroup ? . name } `
) }
className = { ` px-6 py-2.5 text-sm font-medium ${
isLoading ( ` userGroup_delete_ ${ deletingUserGroup ? . name } ` )
? buttonStyles.disabled
: buttonStyles.danger
} ` }
>
{ isLoading ( ` userGroup_delete_ ${ deletingUserGroup ? . name } ` )
? '删除中...'
: '确认删除' }
< / button >
< / div >
< / div >
< / div >
< / div > ,
document . body
) }
{ /* 删除用户确认弹窗 */ }
{ showDeleteUserModal &&
deletingUser &&
createPortal (
< div
className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'
onClick = { ( ) = > {
setShowDeleteUserModal ( false ) ;
setDeletingUser ( null ) ;
} }
>
< div
className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full'
onClick = { ( e ) = > e . stopPropagation ( ) }
>
< div className = 'p-6' >
< div className = 'flex items-center justify-between mb-6' >
< h3 className = 'text-xl font-semibold text-gray-900 dark:text-gray-100' >
确 认 删 除 用 户
< / h3 >
< button
onClick = { ( ) = > {
setShowDeleteUserModal ( false ) ;
setDeletingUser ( null ) ;
} }
className = 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
< svg
className = 'w-6 h-6'
fill = 'none'
stroke = 'currentColor'
viewBox = '0 0 24 24'
>
< path
strokeLinecap = 'round'
strokeLinejoin = 'round'
strokeWidth = { 2 }
d = 'M6 18L18 6M6 6l12 12'
/ >
< / svg >
< / button >
< / div >
< div className = 'mb-6' >
< div className = 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-4' >
< div className = 'flex items-center space-x-2 mb-2' >
< svg
className = 'w-5 h-5 text-red-600 dark:text-red-400'
fill = 'none'
stroke = 'currentColor'
viewBox = '0 0 24 24'
>
< path
strokeLinecap = 'round'
strokeLinejoin = 'round'
strokeWidth = { 2 }
d = 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z'
/ >
< / svg >
< span className = 'text-sm font-medium text-red-800 dark:text-red-300' >
危 险 操 作 警 告
< / span >
< / div >
< p className = 'text-sm text-red-700 dark:text-red-400' >
删 除 用 户 < strong > { deletingUser } < / strong > { ' ' }
将 同 时 删 除 其 搜 索 历 史 、 播 放 记 录 和 收 藏 夹 , 此 操 作 不 可 恢 复 !
< / p >
< / div >
{ /* 操作按钮 */ }
< div className = 'flex justify-end space-x-3' >
< button
onClick = { ( ) = > {
setShowDeleteUserModal ( false ) ;
setDeletingUser ( null ) ;
} }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . secondary } ` }
>
取 消
< / button >
< button
onClick = { handleConfirmDeleteUser }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . danger } ` }
>
确 认 删 除
< / button >
< / div >
< / div >
< / div >
< / div >
< / div > ,
document . body
) }
{ /* 批量设置用户组弹窗 */ }
{ showBatchUserGroupModal &&
createPortal (
< div
className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'
onClick = { ( ) = > {
setShowBatchUserGroupModal ( false ) ;
setSelectedUserGroup ( '' ) ;
} }
>
< div
className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full'
onClick = { ( e ) = > e . stopPropagation ( ) }
>
< div className = 'p-6' >
< div className = 'flex items-center justify-between mb-6' >
< h3 className = 'text-xl font-semibold text-gray-900 dark:text-gray-100' >
批 量 设 置 用 户 组
< / h3 >
< button
onClick = { ( ) = > {
setShowBatchUserGroupModal ( false ) ;
setSelectedUserGroup ( '' ) ;
} }
className = 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
< svg
className = 'w-6 h-6'
fill = 'none'
stroke = 'currentColor'
viewBox = '0 0 24 24'
>
< path
strokeLinecap = 'round'
strokeLinejoin = 'round'
strokeWidth = { 2 }
d = 'M6 18L18 6M6 6l12 12'
/ >
< / svg >
< / button >
< / div >
< div className = 'mb-6' >
< div className = 'bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-4' >
< div className = 'flex items-center space-x-2 mb-2' >
< svg
className = 'w-5 h-5 text-blue-600 dark:text-blue-400'
fill = 'none'
stroke = 'currentColor'
viewBox = '0 0 24 24'
>
< path
strokeLinecap = 'round'
strokeLinejoin = 'round'
strokeWidth = { 2 }
d = 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z'
/ >
< / svg >
< span className = 'text-sm font-medium text-blue-800 dark:text-blue-300' >
批 量 操 作 说 明
< / span >
< / div >
< p className = 'text-sm text-blue-700 dark:text-blue-400' >
将 为 选 中 的 < strong > { selectedUsers . size } 个 用 户 < / strong > { ' ' }
设 置 用 户 组 , 选 择 "无用户组" 为 无 限 制
< / p >
< / div >
< div >
< label className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2' >
选 择 用 户 组 :
< / label >
< select
onChange = { ( e ) = > setSelectedUserGroup ( e . target . value ) }
className = 'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors'
value = { selectedUserGroup }
>
< option value = '' > 无 用 户 组 ( 无 限 制 ) < / option >
{ userGroups . map ( ( group ) = > (
< option key = { group . name } value = { group . name } >
{ group . name } { ' ' }
{ group . enabledApis && group . enabledApis . length > 0
? ` ( ${ group . enabledApis . length } 个源) `
: '' }
< / option >
) ) }
< / select >
< p className = 'mt-2 text-xs text-gray-500 dark:text-gray-400' >
选 择 "无用户组" 为 无 限 制 , 选 择 特 定 用 户 组 将 限 制 用 户 只 能 访 问 该 用 户 组 允 许 的 采 集 源
< / p >
< / div >
< / div >
{ /* 操作按钮 */ }
< div className = 'flex justify-end space-x-3' >
< button
onClick = { ( ) = > {
setShowBatchUserGroupModal ( false ) ;
setSelectedUserGroup ( '' ) ;
} }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . secondary } ` }
>
取 消
< / button >
< button
onClick = { ( ) = > handleBatchSetUserGroup ( selectedUserGroup ) }
disabled = { isLoading ( 'batchSetUserGroup' ) }
className = { ` px-6 py-2.5 text-sm font-medium ${
isLoading ( 'batchSetUserGroup' )
? buttonStyles.disabled
: buttonStyles.primary
} ` }
>
{ isLoading ( 'batchSetUserGroup' ) ? '设置中...' : '确认设置' }
< / button >
< / div >
< / div >
< / div >
< / div > ,
document . body
) }
{ /* 通用弹窗组件 */ }
< AlertModal
@@ -2016,11 +2499,9 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
timer = { alertModal . timer }
showConfirm = { alertModal . showConfirm }
/ >
< / div >
) ;
}
} ;
// 视频源配置组件
const VideoSourceConfig = ( {
@@ -2045,7 +2526,9 @@ const VideoSourceConfig = ({
} ) ;
// 批量操作相关状态
const [ selectedSources , setSelectedSources ] = useState < Set < string > > ( new Set ( ) ) ;
const [ selectedSources , setSelectedSources ] = useState < Set < string > > (
new Set ( )
) ;
// 使用 useMemo 计算全选状态,避免每次渲染都重新计算
const selectAll = useMemo ( ( ) = > {
@@ -2063,21 +2546,23 @@ const VideoSourceConfig = ({
isOpen : false ,
title : '' ,
message : '' ,
onConfirm : ( ) = > { } ,
onCancel : ( ) = > { }
onConfirm : ( ) = > { } ,
onCancel : ( ) = > { } ,
} ) ;
// 有效性检测相关状态
const [ showValidationModal , setShowValidationModal ] = useState ( false ) ;
const [ searchKeyword , setSearchKeyword ] = useState ( '' ) ;
const [ isValidating , setIsValidating ] = useState ( false ) ;
const [ validationResults , setValidationResults ] = useState < Array < {
key : string ;
name : string ;
status : 'valid' | 'no_results' | 'invalid' | 'validating' ;
message : string ;
resultCount : number ;
} > > ( [ ] ) ;
const [ validationResults , setValidationResults ] = useState <
Array < {
key : string ;
name : string ;
status : 'valid' | 'no_results' | 'invalid' | 'validating' ;
message : string ;
resultCount : number ;
} >
> ( [ ] ) ;
// dnd-kit 传感器
const sensors = useSensors (
@@ -2131,13 +2616,17 @@ const VideoSourceConfig = ({
const target = sources . find ( ( s ) = > s . key === key ) ;
if ( ! target ) return ;
const action = target . disabled ? 'enable' : 'disable' ;
withLoading ( ` toggleSource_ ${ key } ` , ( ) = > callSourceApi ( { action , key } ) ) . catch ( ( ) = > {
withLoading ( ` toggleSource_ ${ key } ` , ( ) = >
callSourceApi ( { action , key } )
) . catch ( ( ) = > {
console . error ( '操作失败' , action , key ) ;
} ) ;
} ;
const handleDelete = ( key : string ) = > {
withLoading ( ` deleteSource_ ${ key } ` , ( ) = > callSourceApi ( { action : 'delete' , key } ) ) . catch ( ( ) = > {
withLoading ( ` deleteSource_ ${ key } ` , ( ) = >
callSourceApi ( { action : 'delete' , key } )
) . catch ( ( ) = > {
console . error ( '操作失败' , 'delete' , key ) ;
} ) ;
} ;
@@ -2177,7 +2666,9 @@ const VideoSourceConfig = ({
const handleSaveOrder = ( ) = > {
const order = sources . map ( ( s ) = > s . key ) ;
withLoading ( 'saveSourceOrder' , ( ) = > callSourceApi ( { action : 'sort' , order } ) )
withLoading ( 'saveSourceOrder' , ( ) = >
callSourceApi ( { action : 'sort' , order } )
)
. then ( ( ) = > {
setOrderChanged ( false ) ;
} )
@@ -2189,7 +2680,11 @@ const VideoSourceConfig = ({
// 有效性检测函数
const handleValidateSources = async ( ) = > {
if ( ! searchKeyword . trim ( ) ) {
showAlert ( { type : 'warning' , title : '请输入搜索关键词' , message : '搜索关键词不能为空' } ) ;
showAlert ( {
type : 'warning' ,
title : '请输入搜索关键词' ,
message : '搜索关键词不能为空' ,
} ) ;
return ;
}
@@ -2199,18 +2694,22 @@ const VideoSourceConfig = ({
setShowValidationModal ( false ) ; // 立即关闭弹窗
// 初始化所有视频源为检测中状态
const initialResults = sources . map ( source = > ( {
const initialResults = sources . map ( ( source ) = > ( {
key : source.key ,
name : source.name ,
status : 'validating' as const ,
message : '检测中...' ,
resultCount : 0
resultCount : 0 ,
} ) ) ;
setValidationResults ( initialResults ) ;
try {
// 使用EventSource接收流式数据
const eventSource = new EventSource ( ` /api/admin/source/validate?q= ${ encodeURIComponent ( searchKeyword . trim ( ) ) } ` ) ;
const eventSource = new EventSource (
` /api/admin/source/validate?q= ${ encodeURIComponent (
searchKeyword . trim ( )
) } `
) ;
eventSource . onmessage = ( event ) = > {
try {
@@ -2224,32 +2723,53 @@ const VideoSourceConfig = ({
case 'source_result' :
case 'source_error' :
// 更新验证结果
setValidationResults ( prev = > {
const existing = prev . find ( r = > r . key === data . source ) ;
setValidationResults ( ( prev ) = > {
const existing = prev . find ( ( r ) = > r . key === data . source ) ;
if ( existing ) {
return prev . map ( r = > r . key === data . source ? {
key : data.source ,
name : sources.find ( s = > s . key === data . source ) ? . name || data . source ,
status : data.status ,
message : data.status === 'valid' ? '搜索正常' :
data . status === 'no_results' ? '无法搜索到结果' : '连接失败' ,
resultCount : data.status === 'valid' ? 1 : 0
} : r ) ;
return prev . map ( ( r ) = >
r . key === data . source
? {
key : data.source ,
name :
sources.find ( ( s ) = > s . key === data . source )
? . name || data . source ,
status : data.status ,
message :
data.status === 'valid'
? '搜索正常'
: data . status === 'no_results'
? '无法搜索到结果'
: '连接失败' ,
resultCount : data.status === 'valid' ? 1 : 0 ,
}
: r
) ;
} else {
return [ . . . prev , {
key : data.source ,
name : sources.find ( s = > s . key === data . source ) ? . name || data . source ,
status : data.status ,
message : data.status === 'valid' ? '搜索正常' :
data . status === 'no_results' ? '无法搜索到结果' : '连接失败' ,
resultCount : data.status === 'valid' ? 1 : 0
} ] ;
return [
. . . prev ,
{
key : data.source ,
name :
sources.find ( ( s ) = > s . key === data . source ) ? . name ||
data . source ,
status : data.status ,
message :
data.status === 'valid'
? '搜索正常'
: data . status === 'no_results'
? '无法搜索到结果'
: '连接失败' ,
resultCount : data.status === 'valid' ? 1 : 0 ,
} ,
] ;
}
} ) ;
break ;
case 'complete' :
console . log ( ` 检测完成,共检测 ${ data . completedSources } 个视频源 ` ) ;
console . log (
` 检测完成,共检测 ${ data . completedSources } 个视频源 `
) ;
eventSource . close ( ) ;
setIsValidating ( false ) ;
break ;
@@ -2263,7 +2783,11 @@ const VideoSourceConfig = ({
console . error ( 'EventSource错误:' , error ) ;
eventSource . close ( ) ;
setIsValidating ( false ) ;
showAlert ( { type : 'error' , title : '验证失败' , message : '连接错误,请重试' } ) ;
showAlert ( {
type : 'error' ,
title : '验证失败' ,
message : '连接错误,请重试' ,
} ) ;
} ;
// 设置超时,防止长时间等待
@@ -2271,13 +2795,20 @@ const VideoSourceConfig = ({
if ( eventSource . readyState === EventSource . OPEN ) {
eventSource . close ( ) ;
setIsValidating ( false ) ;
showAlert ( { type : 'warning' , title : '验证超时' , message : '检测超时,请重试' } ) ;
showAlert ( {
type : 'warning' ,
title : '验证超时' ,
message : '检测超时,请重试' ,
} ) ;
}
} , 60000 ) ; // 60秒超时
} catch ( error ) {
setIsValidating ( false ) ;
showAlert ( { type : 'error' , title : '验证失败' , message : error instanceof Error ? error . message : '未知错误' } ) ;
showAlert ( {
type : 'error' ,
title : '验证失败' ,
message : error instanceof Error ? error . message : '未知错误' ,
} ) ;
throw error ;
}
} ) ;
@@ -2285,37 +2816,41 @@ const VideoSourceConfig = ({
// 获取有效性状态显示
const getValidationStatus = ( sourceKey : string ) = > {
const result = validationResults . find ( r = > r . key === sourceKey ) ;
const result = validationResults . find ( ( r ) = > r . key === sourceKey ) ;
if ( ! result ) return null ;
switch ( result . status ) {
case 'validating' :
return {
text : '检测中' ,
className : 'bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300' ,
className :
'bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300' ,
icon : '⟳' ,
message : result.message
message : result.message ,
} ;
case 'valid' :
return {
text : '有效' ,
className : 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300' ,
className :
'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300' ,
icon : '✓' ,
message : result.message
message : result.message ,
} ;
case 'no_results' :
return {
text : '无法搜索' ,
className : 'bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300' ,
className :
'bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300' ,
icon : '⚠' ,
message : result.message
message : result.message ,
} ;
case 'invalid' :
return {
text : '无效' ,
className : 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300' ,
className :
'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300' ,
icon : '✗' ,
message : result.message
message : result.message ,
} ;
default :
return null ;
@@ -2374,10 +2909,11 @@ const VideoSourceConfig = ({
< / td >
< td className = 'px-6 py-4 whitespace-nowrap max-w-[1rem]' >
< span
className = { ` px-2 py-1 text-xs rounded-full ${ ! source . disabled
? 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300'
: 'bg-red -100 dark:bg-red -900/20 text-red -800 dark:text-red -300'
} ` }
className = { ` px-2 py-1 text-xs rounded-full ${
! source . disabled
? 'bg-g reen -100 dark:bg-g reen -900/20 text-g reen -800 dark:text-g reen -300'
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300'
} ` }
>
{ ! source . disabled ? '启用中' : '已禁用' }
< / span >
@@ -2393,7 +2929,10 @@ const VideoSourceConfig = ({
) ;
}
return (
< span className = { ` px-2 py-1 text-xs rounded-full ${ status . className } ` } title = { status . message } >
< span
className = { ` px-2 py-1 text-xs rounded-full ${ status . className } ` }
title = { status . message }
>
{ status . icon } { status . text }
< / span >
) ;
@@ -2403,10 +2942,15 @@ const VideoSourceConfig = ({
< button
onClick = { ( ) = > handleToggleEnable ( source . key ) }
disabled = { isLoading ( ` toggleSource_ ${ source . key } ` ) }
className = { ` inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${ ! source . disabled
? buttonStyles.roundedDanger
: buttonStyles.roundedSuccess
} transition-colors ${ isLoading ( ` toggleSource_ ${ source . key } ` ) ? 'opacity-50 cursor-not-allowed' : '' } ` }
className = { ` inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${
! source . disabled
? buttonStyles.roundedDanger
: buttonStyles.roundedSuccess
} transition-colors ${
isLoading ( ` toggleSource_ ${ source . key } ` )
? 'opacity-50 cursor-not-allowed'
: ''
} ` }
>
{ ! source . disabled ? '禁用' : '启用' }
< / button >
@@ -2414,7 +2958,11 @@ const VideoSourceConfig = ({
< button
onClick = { ( ) = > handleDelete ( source . key ) }
disabled = { isLoading ( ` deleteSource_ ${ source . key } ` ) }
className = { ` ${ buttonStyles . roundedSecondary } ${ isLoading ( ` deleteSource_ ${ source . key } ` ) ? 'opacity-50 cursor-not-allowed' : '' } ` }
className = { ` ${ buttonStyles . roundedSecondary } ${
isLoading ( ` deleteSource_ ${ source . key } ` )
? 'opacity-50 cursor-not-allowed'
: ''
} ` }
>
删 除
< / button >
@@ -2425,18 +2973,21 @@ const VideoSourceConfig = ({
} ;
// 全选/取消全选
const handleSelectAll = useCallback ( ( checked : boolean ) = > {
if ( checked ) {
const allKeys = sources . map ( s = > s . key ) ;
setSelectedSources ( new Set ( allKeys ) ) ;
} else {
setSelectedSources ( new Set ( ) ) ;
}
} , [ sources ] ) ;
const handleSelectAll = useCallback (
( checked : boolean ) = > {
if ( checked ) {
const allKeys = sources . map ( ( s ) = > s . key ) ;
setSelectedSources ( new Set ( allKeys ) ) ;
} else {
setSelectedSources ( new Set ( ) ) ;
}
} ,
[ sources ]
) ;
// 单个选择
const handleSelectSource = useCallback ( ( key : string , checked : boolean ) = > {
setSelectedSources ( prev = > {
setSelectedSources ( ( prev ) = > {
const newSelected = new Set ( prev ) ;
if ( checked ) {
newSelected . add ( key ) ;
@@ -2448,9 +2999,15 @@ const VideoSourceConfig = ({
} , [ ] ) ;
// 批量操作
const handleBatchOperation = async ( action : 'batch_enable' | 'batch_disable' | 'batch_delete' ) = > {
const handleBatchOperation = async (
action : 'batch_enable' | 'batch_disable' | 'batch_delete'
) = > {
if ( selectedSources . size === 0 ) {
showAlert ( { type : 'warning' , title : '请先选择要操作的视频源' , message : '请选择至少一个视频源' } ) ;
showAlert ( {
type : 'warning' ,
title : '请先选择要操作的视频源' ,
message : '请选择至少一个视频源' ,
} ) ;
return ;
}
@@ -2480,18 +3037,41 @@ const VideoSourceConfig = ({
message : confirmMessage ,
onConfirm : async ( ) = > {
try {
await withLoading ( ` batchSource_ ${ action } ` , ( ) = > callSourceApi ( { action , keys } ) ) ;
showAlert ( { type : 'success' , title : ` ${ actionName } 成功 ` , message : ` ${ actionName } 了 ${ keys . length } 个视频源 ` , timer : 2000 } ) ;
await withLoading ( ` batchSource_ ${ action } ` , ( ) = >
callSourceApi ( { action , keys } )
) ;
showAlert ( {
type : 'success' ,
title : ` ${ actionName } 成功 ` ,
message : ` ${ actionName } 了 ${ keys . length } 个视频源 ` ,
timer : 2000 ,
} ) ;
// 重置选择状态
setSelectedSources ( new Set ( ) ) ;
} catch ( err ) {
showAlert ( { type : 'error' , title : ` ${ actionName } 失败 ` , message : err instanceof Error ? err . message : '操作失败' } ) ;
showAlert ( {
type : 'error' ,
title : ` ${ actionName } 失败 ` ,
message : err instanceof Error ? err . message : '操作失败' ,
} ) ;
}
setConfirmModal ( { isOpen : false , title : '' , message : '' , onConfirm : ( ) = > { } , onCancel : ( ) = > { } } ) ;
setConfirmModal ( {
isOpen : false ,
title : '' ,
message : '' ,
onConfirm : ( ) = > { } ,
onCancel : ( ) = > { } ,
} ) ;
} ,
onCancel : ( ) = > {
setConfirmModal ( { isOpen : false , title : '' , message : '' , onConfirm : ( ) = > { } , onCancel : ( ) = > { } } ) ;
}
setConfirmModal ( {
isOpen : false ,
title : '' ,
message : '' ,
onConfirm : ( ) = > { } ,
onCancel : ( ) = > { } ,
} ) ;
} ,
} ) ;
} ;
@@ -2517,28 +3097,48 @@ const VideoSourceConfig = ({
< div className = 'flex flex-wrap items-center gap-3 order-2 sm:order-1' >
< span className = 'text-sm text-gray-600 dark:text-gray-400' >
< span className = 'sm:hidden' > 已 选 { selectedSources . size } < / span >
< span className = 'hidden sm:inline' > 已 选 择 { selectedSources . size } 个 视 频 源 < / span >
< span className = 'hidden sm:inline' >
已 选 择 { selectedSources . size } 个 视 频 源
< / span >
< / span >
< button
onClick = { ( ) = > handleBatchOperation ( 'batch_enable' ) }
disabled = { isLoading ( 'batchSource_batch_enable' ) }
className = { ` px-3 py-1 text-sm ${ isLoading ( 'batchSource_batch_enable' ) ? buttonStyles.disabled : buttonStyles.success } ` }
className = { ` px-3 py-1 text-sm ${
isLoading ( 'batchSource_batch_enable' )
? buttonStyles.disabled
: buttonStyles.success
} ` }
>
{ isLoading ( 'batchSource_batch_enable' ) ? '启用中...' : '批量启用' }
{ isLoading ( 'batchSource_batch_enable' )
? '启用中...'
: '批量启用' }
< / button >
< button
onClick = { ( ) = > handleBatchOperation ( 'batch_disable' ) }
disabled = { isLoading ( 'batchSource_batch_disable' ) }
className = { ` px-3 py-1 text-sm ${ isLoading ( 'batchSource_batch_disable' ) ? buttonStyles.disabled : buttonStyles.warning } ` }
className = { ` px-3 py-1 text-sm ${
isLoading ( 'batchSource_batch_disable' )
? buttonStyles.disabled
: buttonStyles.warning
} ` }
>
{ isLoading ( 'batchSource_batch_disable' ) ? '禁用中...' : '批量禁用' }
{ isLoading ( 'batchSource_batch_disable' )
? '禁用中...'
: '批量禁用' }
< / button >
< button
onClick = { ( ) = > handleBatchOperation ( 'batch_delete' ) }
disabled = { isLoading ( 'batchSource_batch_delete' ) }
className = { ` px-3 py-1 text-sm ${ isLoading ( 'batchSource_batch_delete' ) ? buttonStyles.disabled : buttonStyles.danger } ` }
className = { ` px-3 py-1 text-sm ${
isLoading ( 'batchSource_batch_delete' )
? buttonStyles.disabled
: buttonStyles.danger
} ` }
>
{ isLoading ( 'batchSource_batch_delete' ) ? '删除中...' : '批量删除' }
{ isLoading ( 'batchSource_batch_delete' )
? '删除中...'
: '批量删除' }
< / button >
< / div >
< div className = 'hidden sm:block w-px h-6 bg-gray-300 dark:bg-gray-600 order-2' > < / div >
@@ -2548,10 +3148,9 @@ const VideoSourceConfig = ({
< button
onClick = { ( ) = > setShowValidationModal ( true ) }
disabled = { isValidating }
className = { ` px-3 py-1 text-sm rounded-lg transition-colors flex items-center space-x-1 ${ isValidating
? buttonStyles.disabled
: buttonStyles.primary
} ` }
className = { ` px-3 py-1 text-sm rounded-lg transition-colors flex items-center space-x-1 ${
isValidating ? buttonStyles.disabled : buttonStyles.primary
} ` }
>
{ isValidating ? (
< >
@@ -2564,7 +3163,9 @@ const VideoSourceConfig = ({
< / button >
< button
onClick = { ( ) = > setShowAddForm ( ! showAddForm ) }
className = { showAddForm ? buttonStyles.secondary : buttonStyles.success }
className = {
showAddForm ? buttonStyles.secondary : buttonStyles.success
}
>
{ showAddForm ? '取消' : '添加视频源' }
< / button >
@@ -2615,8 +3216,20 @@ const VideoSourceConfig = ({
< div className = 'flex justify-end' >
< button
onClick = { handleAddSource }
disabled = { ! newSource . name || ! newSource . key || ! newSource . api || isLoading ( 'addSource' ) }
className = { ` w-full sm:w-auto px-4 py-2 ${ ! newSource . name || ! newSource . key || ! newSource . api || isLoading ( 'addSource' ) ? buttonStyles.disabled : buttonStyles.success } ` }
disabled = {
! newSource . name ||
! newSource . key ||
! newSource . api ||
isLoading ( 'addSource' )
}
className = { ` w-full sm:w-auto px-4 py-2 ${
! newSource . name ||
! newSource . key ||
! newSource . api ||
isLoading ( 'addSource' )
? buttonStyles.disabled
: buttonStyles.success
} ` }
>
{ isLoading ( 'addSource' ) ? '添加中...' : '添加' }
< / button >
@@ -2624,10 +3237,11 @@ const VideoSourceConfig = ({
< / div >
) }
{ /* 视频源表格 */ }
< div className = 'border border-gray-200 dark:border-gray-700 rounded-lg max-h-[28rem] overflow-y-auto overflow-x-auto relative' data-table = "source-list" >
< div
className = 'border border-gray-200 dark:border-gray-700 rounded-lg max-h-[28rem] overflow-y-auto overflow-x-auto relative'
data-table = 'source-list'
>
< table className = 'min-w-full divide-y divide-gray-200 dark:divide-gray-700' >
< thead className = 'bg-gray-50 dark:bg-gray-900 sticky top-0 z-10' >
< tr >
@@ -2690,7 +3304,11 @@ const VideoSourceConfig = ({
< button
onClick = { handleSaveOrder }
disabled = { isLoading ( 'saveSourceOrder' ) }
className = { ` px-3 py-1.5 text-sm ${ isLoading ( 'saveSourceOrder' ) ? buttonStyles.disabled : buttonStyles.primary } ` }
className = { ` px-3 py-1.5 text-sm ${
isLoading ( 'saveSourceOrder' )
? buttonStyles.disabled
: buttonStyles.primary
} ` }
>
{ isLoading ( 'saveSourceOrder' ) ? '保存中...' : '保存排序' }
< / button >
@@ -2698,44 +3316,57 @@ const VideoSourceConfig = ({
) }
{ /* 有效性检测弹窗 */ }
{ showValidationModal && createPortal (
< div className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50' onClick = { ( ) = > setShowValidationModal ( false ) } >
< div className = 'bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-md mx-4' onClick = { ( e ) = > e . stopPropagation ( ) } >
< h3 className = 'text-lg font-medium text-gray-900 dark:text-gray-100 mb-4' >
视 频 源 有 效 性 检 测
< / h3 >
< p className = 'text-sm text-gray-600 dark:text-gray-400 mb-4' >
请 输 入 检 测 用 的 搜 索 关 键 词
< / p >
< div className = 'space-y-4' >
< input
type = 'text'
placeholder = '请输入搜索关键词'
value = { searchKeyword }
onChange = { ( e ) = > setSearchKeyword ( e . target . value ) }
className = 'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
onKeyPress = { ( e ) = > e . key === 'Enter' && handleValidateSources ( ) }
/ >
< div className = 'flex justify-end space-x-3' >
< button
onClick = { ( ) = > setShowValidationModal ( false ) }
className = 'px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 transition-colors'
>
取 消
< / button >
< button
onClick = { handleValidateSources }
disabled = { ! searchKeyword . trim ( ) }
className = { ` px-4 py-2 ${ ! searchKeyword . trim ( ) ? buttonStyles.disabled : buttonStyles.primary } ` }
>
开 始 检 测
< / button >
{ showValidationModal &&
createPortal (
< div
className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'
onClick = { ( ) = > setShowValidationModal ( false ) }
>
< div
className = 'bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-md mx-4'
onClick = { ( e ) = > e . stopPropagation ( ) }
>
< h3 className = 'text-lg font-medium text-gray-900 dark:text-gray-100 mb-4' >
视 频 源 有 效 性 检 测
< / h3 >
< p className = 'text-sm text-gray-600 dark:text-gray-400 mb-4' >
请 输 入 检 测 用 的 搜 索 关 键 词
< / p >
< div className = 'space-y-4' >
< input
type = 'text'
placeholder = '请输入搜索关键词'
value = { searchKeyword }
onChange = { ( e ) = > setSearchKeyword ( e . target . value ) }
className = 'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
onKeyPress = { ( e ) = >
e . key === 'Enter' && handleValidateSources ( )
}
/ >
< div className = 'flex justify-end space-x-3' >
< button
onClick = { ( ) = > setShowValidationModal ( false ) }
className = 'px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 transition-colors'
>
取 消
< / button >
< button
onClick = { handleValidateSources }
disabled = { ! searchKeyword . trim ( ) }
className = { ` px-4 py-2 ${
! searchKeyword . trim ( )
? buttonStyles.disabled
: buttonStyles.primary
} ` }
>
开 始 检 测
< / button >
< / div >
< / div >
< / div >
< / div >
< / div > ,
document . body
) }
< / div > ,
document . body
) }
{ /* 通用弹窗组件 */ }
< AlertModal
@@ -2749,51 +3380,82 @@ const VideoSourceConfig = ({
/ >
{ /* 批量操作确认弹窗 */ }
{ confirmModal . isOpen && createPortal (
< div className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4' onClick = { confirmModal . onCancel } >
< div className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full' onClick = { ( e ) = > e . stopPropagation ( ) } >
< div className = 'p-6' >
< div className = 'flex items-center justify-between mb-4' >
< h3 className = 'text-lg font-semibold text-gray-900 dark:text-gray-100' >
{ confirmModal . title }
< / h3 >
< button
onClick = { confirmModal . onCancel }
className = 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
< svg className = 'w-5 h-5' fill = 'none' stroke = 'currentColor' viewBox = '0 0 24 24 '>
< path strokeLinecap = 'round' strokeLinejoin = 'round' strokeWidth = { 2 } d = 'M6 18L18 6M6 6l12 12' / >
< / svg >
< / button >
< / div >
{ confirmModal . isOpen &&
createPortal (
< div
className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'
onClick = { confirmModal . onCancel }
>
< div
className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full'
onClick = { ( e ) = > e . stopPropagation ( ) }
>
< div className = 'p-6' >
< div className = 'flex items-center justify-between mb-4' >
< h3 className = 'text-lg font-semibold text-gray-900 dark:text-gray-100 ' >
{ confirmModal . title }
< / h3 >
< button
onClick = { confirmModal . onCancel }
className = 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
< svg
className = 'w-5 h-5'
fill = 'none'
stroke = 'currentColor'
viewBox = '0 0 24 24'
>
< path
strokeLinecap = 'round'
strokeLinejoin = 'round'
strokeWidth = { 2 }
d = 'M6 18L18 6M6 6l12 12'
/ >
< / svg >
< / button >
< / div >
< div className = 'mb-6' >
< p className = 'text-sm text-gray-600 dark:text-gray-400' >
{ confirmModal . message }
< / p >
< / div >
< div className = 'mb-6' >
< p className = 'text-sm text-gray-600 dark:text-gray-400' >
{ confirmModal . message }
< / p >
< / div >
{ /* 操作按钮 */ }
< div className = 'flex justify-end space-x-3' >
< button
onClick = { confirmModal . onCancel }
className = { ` px-4 py-2 text-sm font-medium ${ buttonStyles . secondary } ` }
>
取 消
< / button >
< button
onClick = { confirmModal . onConfirm }
disabled = { isLoading ( 'batchSource_batch_enable' ) || isLoading ( 'batchSource_batch_disable' ) || isLoading ( 'batchSource_batch_delete' ) }
className = { ` px-4 py-2 text-sm font-medium ${ isLoading ( 'batchSource_batch_enable' ) || isLoading ( 'batchSource_batch_disable' ) || isLoading ( 'batchSource_batch_delete' ) ? buttonStyles.disabled : buttonStyles.primary } ` }
>
{ isLoading ( 'batchSource_batch_enable' ) || isLoading ( 'batchSource_batch_disable' ) || isLoading ( 'batchSource_batch_delete' ) ? '操作中...' : '确认' }
< / button >
{ /* 操作按钮 */ }
< div className = 'flex justify-end space-x-3' >
< button
onClick = { confirmModal . onCancel }
className = { ` px-4 py-2 text-sm font-medium ${ buttonStyles . secondary } ` }
>
取 消
< / button >
< button
onClick = { confirmModal . onConfirm }
disabled = {
isLoading ( 'batchSource_batch_enable' ) ||
isLoading ( 'batchSource_batch_disable' ) ||
isLoading ( 'batchSource_batch_delete' )
}
className = { ` px-4 py-2 text-sm font-medium ${
isLoading ( 'batchSource_batch_enable' ) ||
isLoading ( 'batchSource_batch_disable' ) ||
isLoading ( 'batchSource_batch_delete' )
? buttonStyles.disabled
: buttonStyles.primary
} ` }
>
{ isLoading ( 'batchSource_batch_enable' ) ||
isLoading ( 'batchSource_batch_disable' ) ||
isLoading ( 'batchSource_batch_delete' )
? '操作中...'
: '确认' }
< / button >
< / div >
< / div >
< / div >
< / div >
< / div > ,
document . body
) }
< / div > ,
document . body
) }
< / div >
) ;
} ;
@@ -2869,13 +3531,17 @@ const CategoryConfig = ({
const target = categories . find ( ( c ) = > c . query === query && c . type === type ) ;
if ( ! target ) return ;
const action = target . disabled ? 'enable' : 'disable' ;
withLoading ( ` toggleCategory_ ${ query } _ ${ type } ` , ( ) = > callCategoryApi ( { action , query , type } ) ) . catch ( ( ) = > {
withLoading ( ` toggleCategory_ ${ query } _ ${ type } ` , ( ) = >
callCategoryApi ( { action , query , type } )
) . catch ( ( ) = > {
console . error ( '操作失败' , action , query , type ) ;
} ) ;
} ;
const handleDelete = ( query : string , type : 'movie' | 'tv' ) = > {
withLoading ( ` deleteCategory_ ${ query } _ ${ type } ` , ( ) = > callCategoryApi ( { action : 'delete' , query , type } ) ) . catch ( ( ) = > {
withLoading ( ` deleteCategory_ ${ query } _ ${ type } ` , ( ) = >
callCategoryApi ( { action : 'delete' , query , type } )
) . catch ( ( ) = > {
console . error ( '操作失败' , 'delete' , query , type ) ;
} ) ;
} ;
@@ -2917,7 +3583,9 @@ const CategoryConfig = ({
const handleSaveOrder = ( ) = > {
const order = categories . map ( ( c ) = > ` ${ c . query } : ${ c . type } ` ) ;
withLoading ( 'saveCategoryOrder' , ( ) = > callCategoryApi ( { action : 'sort' , order } ) )
withLoading ( 'saveCategoryOrder' , ( ) = >
callCategoryApi ( { action : 'sort' , order } )
)
. then ( ( ) = > {
setOrderChanged ( false ) ;
} )
@@ -2943,7 +3611,7 @@ const CategoryConfig = ({
className = 'hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors select-none'
>
< td
className = " px-2 py-4 cursor-grab text-gray-400"
className = ' px-2 py-4 cursor-grab text-gray-400'
style = { { touchAction : 'none' } }
{ ... { ...attributes , ...listeners }}
>
@@ -2954,10 +3622,11 @@ const CategoryConfig = ({
< / td >
< td className = 'px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100' >
< span
className = { ` px-2 py-1 text-xs rounded-full ${ category . type === 'movie'
? 'bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300 '
: 'bg-purpl e-100 dark:bg-purpl e-900/20 text-purpl e-800 dark:text-purpl e-300'
} ` }
className = { ` px-2 py-1 text-xs rounded-full ${
category . type === 'movie '
? 'bg-blu e-100 dark:bg-blu e-900/20 text-blu e-800 dark:text-blu e-300'
: 'bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-300'
} ` }
>
{ category . type === 'movie' ? '电影' : '电视剧' }
< / span >
@@ -2970,32 +3639,44 @@ const CategoryConfig = ({
< / td >
< td className = 'px-6 py-4 whitespace-nowrap max-w-[1rem]' >
< span
className = { ` px-2 py-1 text-xs rounded-full ${ ! category . disabled
? 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300'
: 'bg-red -100 dark:bg-red -900/20 text-red -800 dark:text-red -300'
} ` }
className = { ` px-2 py-1 text-xs rounded-full ${
! category . disabled
? 'bg-g reen -100 dark:bg-g reen -900/20 text-g reen -800 dark:text-g reen -300'
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300'
} ` }
>
{ ! category . disabled ? '启用中' : '已禁用' }
< / span >
< / td >
< td className = 'px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2' >
< button
onClick = { ( ) = >
handleToggleEnable ( category . query , category . type )
}
disabled = { isLoading ( ` toggleCategory_ ${ category . query } _ ${ category . type } ` ) }
className = { ` inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${ ! category . disabled
? buttonStyles.roundedDanger
: buttonStyles.roundedSuccess
} transition-colors ${ isLoading ( ` toggleCategory_ ${ category . query } _ ${ category . type } ` ) ? 'opacity-50 cursor-not-allowed' : '' } ` }
onClick = { ( ) = > handleToggleEnable ( category . query , category . type ) }
disabled = { isLoading (
` toggleCategory_ ${ category . query } _ ${ category . type } `
) }
className = { ` inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${
! category . disabled
? buttonStyles.roundedDanger
: buttonStyles.roundedSuccess
} transition-colors ${
isLoading ( ` toggleCategory_ ${ category . query } _ ${ category . type } ` )
? 'opacity-50 cursor-not-allowed'
: ''
} ` }
>
{ ! category . disabled ? '禁用' : '启用' }
< / button >
{ category . from !== 'config' && (
< button
onClick = { ( ) = > handleDelete ( category . query , category . type ) }
disabled = { isLoading ( ` deleteCategory_ ${ category . query } _ ${ category . type } ` ) }
className = { ` ${ buttonStyles . roundedSecondary } ${ isLoading ( ` deleteCategory_ ${ category . query } _ ${ category . type } ` ) ? 'opacity-50 cursor-not-allowed' : '' } ` }
disabled = { isLoading (
` deleteCategory_ ${ category . query } _ ${ category . type } `
) }
className = { ` ${ buttonStyles . roundedSecondary } ${
isLoading ( ` deleteCategory_ ${ category . query } _ ${ category . type } ` )
? 'opacity-50 cursor-not-allowed'
: ''
} ` }
>
删 除
< / button >
@@ -3022,7 +3703,9 @@ const CategoryConfig = ({
< / h4 >
< button
onClick = { ( ) = > setShowAddForm ( ! showAddForm ) }
className = { ` px-3 py-1 text-sm rounded-lg transition-colors ${ showAddForm ? buttonStyles.secondary : buttonStyles.success } ` }
className = { ` px-3 py-1 text-sm rounded-lg transition-colors ${
showAddForm ? buttonStyles.secondary : buttonStyles.success
} ` }
>
{ showAddForm ? '取消' : '添加分类' }
< / button >
@@ -3066,8 +3749,18 @@ const CategoryConfig = ({
< div className = 'flex justify-end' >
< button
onClick = { handleAddCategory }
disabled = { ! newCategory . name || ! newCategory . query || isLoading ( 'addCategory' ) }
className = { ` w-full sm:w-auto px-4 py-2 ${ ! newCategory . name || ! newCategory . query || isLoading ( 'addCategory' ) ? buttonStyles.disabled : buttonStyles.success } ` }
disabled = {
! newCategory . name ||
! newCategory . query ||
isLoading ( 'addCategory' )
}
className = { ` w-full sm:w-auto px-4 py-2 ${
! newCategory . name ||
! newCategory . query ||
isLoading ( 'addCategory' )
? buttonStyles.disabled
: buttonStyles.success
} ` }
>
{ isLoading ( 'addCategory' ) ? '添加中...' : '添加' }
< / button >
@@ -3128,7 +3821,11 @@ const CategoryConfig = ({
< button
onClick = { handleSaveOrder }
disabled = { isLoading ( 'saveCategoryOrder' ) }
className = { ` px-3 py-1.5 text-sm ${ isLoading ( 'saveCategoryOrder' ) ? buttonStyles.disabled : buttonStyles.primary } ` }
className = { ` px-3 py-1.5 text-sm ${
isLoading ( 'saveCategoryOrder' )
? buttonStyles.disabled
: buttonStyles.primary
} ` }
>
{ isLoading ( 'saveCategoryOrder' ) ? '保存中...' : '保存排序' }
< / button >
@@ -3150,7 +3847,13 @@ const CategoryConfig = ({
} ;
// 新增配置文件组件
const ConfigFileComponent = ( { config , refreshConfig } : { config : AdminConfig | null ; refreshConfig : ( ) = > Promise < void > } ) = > {
const ConfigFileComponent = ( {
config ,
refreshConfig ,
} : {
config : AdminConfig | null ;
refreshConfig : ( ) = > Promise < void > ;
} ) = > {
const { alertModal , showAlert , hideAlert } = useAlertModal ( ) ;
const { isLoading , withLoading } = useLoadingState ( ) ;
const [ configContent , setConfigContent ] = useState ( '' ) ;
@@ -3158,8 +3861,6 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
const [ autoUpdate , setAutoUpdate ] = useState ( false ) ;
const [ lastCheckTime , setLastCheckTime ] = useState < string > ( '' ) ;
useEffect ( ( ) = > {
if ( config ? . ConfigFile ) {
setConfigContent ( config . ConfigFile ) ;
@@ -3171,8 +3872,6 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
}
} , [ config ] ) ;
// 拉取订阅配置
const handleFetchConfig = async ( ) = > {
if ( ! subscriptionUrl . trim ( ) ) {
@@ -3211,7 +3910,9 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
} ;
// 处理文件上传
const handleFileUpload = async ( event : React.ChangeEvent < HTMLInputElement > ) = > {
const handleFileUpload = async (
event : React.ChangeEvent < HTMLInputElement >
) = > {
const file = event . target . files ? . [ 0 ] ;
if ( ! file ) return ;
@@ -3224,7 +3925,7 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
await withLoading ( 'uploadConfig' , async ( ) = > {
try {
const fileContent = await file . text ( ) ;
// 验证JSON格式
let parsedConfig ;
try {
@@ -3241,9 +3942,11 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
}
// 根据api字段进行去重
const existingConfig = configContent ? JSON . parse ( configContent ) : { api_site : { } } ;
const existingConfig = configContent
? JSON . parse ( configContent )
: { api_site : { } } ;
const existingApis = new Set ( ) ;
// 收集现有配置中的所有api
Object . values ( existingConfig . api_site || { } ) . forEach ( ( site : any ) = > {
if ( site . api ) {
@@ -3254,31 +3957,36 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
// 合并新配置,去重处理
const mergedApiSite = { . . . existingConfig . api_site } ;
let duplicateCount = 0 ;
Object . entries ( parsedConfig . api_site || { } ) . forEach ( ( [ key , site ] : [ string , any ] ) = > {
if ( site . api && existingApis . has ( site . api ) ) {
duplicateCount ++ ;
// 跳过重复的api
return ;
Object . entries ( parsedConfig . api_site || { } ) . forEach (
( [ key , site ] : [ string , any ] ) = > {
if ( site . api && existingApis . has ( site . api ) ) {
duplicateCount ++ ;
// 跳过重复的api
return ;
}
mergedApiSite [ key ] = site ;
}
mergedApiSite [ key ] = site ;
} ) ;
) ;
const mergedConfig = {
. . . parsedConfig ,
api_site : mergedApiSite
api_site : mergedApiSite ,
} ;
// 更新配置内容
setConfigContent ( JSON . stringify ( mergedConfig , null , 2 ) ) ;
const message = duplicateCount > 0
? ` 配置上传成功,跳过了 ${ duplicateCount } 个重复的API `
: '配置上传成功' ;
showSuccess ( message , showAlert ) ;
const message =
duplicateCount > 0
? ` 配置上传成功,跳过了 ${ duplicateCount } 个重复的API `
: '配置上传成功' ;
showSuccess ( message , showAlert ) ;
} catch ( err ) {
showError ( err instanceof Error ? err . message : '文件上传失败' , showAlert ) ;
showError (
err instanceof Error ? err . message : '文件上传失败' ,
showAlert
) ;
throw err ;
}
} ) ;
@@ -3298,7 +4006,7 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
configFile : configContent ,
subscriptionUrl ,
autoUpdate ,
lastCheckTime : lastCheckTime || new Date ( ) . toISOString ( )
lastCheckTime : lastCheckTime || new Date ( ) . toISOString ( ) ,
} ) ,
} ) ;
@@ -3316,8 +4024,6 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
} ) ;
} ;
if ( ! config ) {
return (
< div className = 'text-center text-gray-500 dark:text-gray-400' >
@@ -3335,7 +4041,10 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
配 置 订 阅
< / h3 >
< div className = 'text-sm text-gray-500 dark:text-gray-400 px-3 py-1.5 rounded-full' >
最 后 更 新 : { lastCheckTime ? new Date ( lastCheckTime ) . toLocaleString ( 'zh-CN' ) : '从未更新 '}
最 后 更 新 : { ' '}
{ lastCheckTime
? new Date ( lastCheckTime ) . toLocaleString ( 'zh-CN' )
: '从未更新' }
< / div >
< / div >
@@ -3363,10 +4072,11 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
< button
onClick = { handleFetchConfig }
disabled = { isLoading ( 'fetchConfig' ) || ! subscriptionUrl . trim ( ) }
className = { ` w-full px-6 py-3 rounded-lg font-medium transition-all duration-200 ${ isLoading ( 'fetchConfig' ) || ! subscriptionUrl . trim ( )
? buttonStyles.disabled
: buttonStyles.success
} ` }
className = { ` w-full px-6 py-3 rounded-lg font-medium transition-all duration-200 ${
isLoading ( 'fetchConfig' ) || ! subscriptionUrl . trim ( )
? buttonStyles.disabled
: buttonStyles.success
} ` }
>
{ isLoading ( 'fetchConfig' ) ? (
< div className = 'flex items-center justify-center gap-2' >
@@ -3393,16 +4103,18 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
type = 'button'
onClick = { ( ) = > setAutoUpdate ( ! autoUpdate ) }
disabled = { false }
className = { ` relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${ autoUpdate
? buttonStyles.toggleOn
: buttonStyles.toggleOff
} ` }
className = { ` relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
autoUpdate ? buttonStyles.toggleOn : buttonStyles.toggleOff
} ` }
>
< span
className = { ` inline-block h-4 w-4 transform rounded-full ${ buttonStyles . toggleThumb } transition-transform ${ autoUpdate
? buttonStyles.toggleThumbOn
: buttonStyles.toggleThumbOff
} ` }
className = { ` inline-block h-4 w-4 transform rounded-full ${
buttonStyles . toggleThumb
} transition-transform ${
autoUpdate
? buttonStyles.toggleThumbOn
: buttonStyles.toggleThumbOff
} ` }
/ >
< / button >
< / div >
@@ -3420,7 +4132,8 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
disabled = { false }
className = 'w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono text-sm leading-relaxed resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 hover:border-gray-400 dark:hover:border-gray-500'
style = { {
fontFamily : 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace'
fontFamily :
'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace' ,
} }
spellCheck = { false }
data-gramm = { false }
@@ -3458,12 +4171,24 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
{ isLoading ( 'uploadConfig' ) ? (
< >
< div className = 'w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full animate-spin' > < / div >
< span className = 'text-sm text-gray-600 dark:text-gray-400' > 上 传 中 . . . < / span >
< span className = 'text-sm text-gray-600 dark:text-gray-400' >
上 传 中 . . .
< / span >
< / >
) : (
< >
< svg className = 'w-5 h-5 text-gray-400' fill = 'none' stroke = 'currentColor' viewBox = '0 0 24 24' >
< path strokeLinecap = 'round' strokeLinejoin = 'round' strokeWidth = { 2 } d = 'M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12' / >
< svg
className = 'w-5 h-5 text-gray-400'
fill = 'none'
stroke = 'currentColor'
viewBox = '0 0 24 24'
>
< path
strokeLinecap = 'round'
strokeLinejoin = 'round'
strokeWidth = { 2 }
d = 'M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12'
/ >
< / svg >
< span className = 'text-sm text-gray-600 dark:text-gray-400' >
点 击 选 择 JSON文件或拖拽到此处
@@ -3485,10 +4210,11 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
< button
onClick = { handleSave }
disabled = { isLoading ( 'saveConfig' ) }
className = { ` px-4 py-2 rounded-lg transition-colors ${ isLoading ( 'saveConfig' )
? buttonStyles.disabled
: buttonStyles.success
} ` }
className = { ` px-4 py-2 rounded-lg transition-colors ${
isLoading ( 'saveConfig' )
? buttonStyles.disabled
: buttonStyles.success
} ` }
>
{ isLoading ( 'saveConfig' ) ? '保存中…' : '保存' }
< / button >
@@ -3510,7 +4236,13 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
} ;
// 新增站点配置组件
const SiteConfigComponent = ( { config , refreshConfig } : { config : AdminConfig | null ; refreshConfig : ( ) = > Promise < void > } ) = > {
const SiteConfigComponent = ( {
config ,
refreshConfig ,
} : {
config : AdminConfig | null ;
refreshConfig : ( ) = > Promise < void > ;
} ) = > {
const { alertModal , showAlert , hideAlert } = useAlertModal ( ) ;
const { isLoading , withLoading } = useLoadingState ( ) ;
const [ showEnableCommentsModal , setShowEnableCommentsModal ] = useState ( false ) ;
@@ -3583,14 +4315,16 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
if ( config ? . SiteConfig ) {
setSiteSettings ( {
. . . config . SiteConfig ,
DoubanProxyType : config.SiteConfig.DoubanProxyType || 'cmliussss-cdn-tencent' ,
DoubanProxyType :
config.SiteConfig.DoubanProxyType || 'cmliussss-cdn-tencent' ,
DoubanProxy : config.SiteConfig.DoubanProxy || '' ,
DoubanImageProxyType :
config.SiteConfig.DoubanImageProxyType || 'cmliussss-cdn-tencent' ,
DoubanImageProxy : config.SiteConfig.DoubanImageProxy || '' ,
DisableYellowFilter : config.SiteConfig.DisableYellowFilter || false ,
FluidSearch : config.SiteConfig.FluidSearch || true ,
DanmakuApiBase : config.SiteConfig.DanmakuApiBase || 'http://localhost:9321' ,
DanmakuApiBase :
config.SiteConfig.DanmakuApiBase || 'http://localhost:9321' ,
DanmakuApiToken : config.SiteConfig.DanmakuApiToken || '87654321' ,
EnableComments : config.SiteConfig.EnableComments || false ,
} ) ;
@@ -3707,9 +4441,7 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
< div className = 'space-y-6' >
{ /* 站点名称 */ }
< div >
< label
className = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
< label className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2' >
站 点 名 称
< / label >
< input
@@ -3718,15 +4450,13 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
onChange = { ( e ) = >
setSiteSettings ( ( prev ) = > ( { . . . prev , SiteName : e.target.value } ) )
}
className = " w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent"
className = ' w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
/ >
< / div >
{ /* 站点公告 */ }
< div >
< label
className = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
< label className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2' >
站 点 公 告
< / label >
< textarea
@@ -3738,16 +4468,14 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
} ) )
}
rows = { 3 }
className = " w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent"
className = ' w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
/ >
< / div >
{ /* 豆瓣数据源设置 */ }
< div className = 'space-y-3' >
< div >
< label
className = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
< label className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2' >
豆 瓣 数 据 代 理
< / label >
< div className = 'relative' data-dropdown = 'douban-datasource' >
@@ -3755,7 +4483,7 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
< button
type = 'button'
onClick = { ( ) = > setIsDoubanDropdownOpen ( ! isDoubanDropdownOpen ) }
className = " w-full px-3 py-2.5 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 text-left"
className = ' w-full px-3 py-2.5 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 text-left'
>
{
doubanDataSourceOptions . find (
@@ -3767,8 +4495,9 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
{ /* 下拉箭头 */ }
< div className = 'absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none' >
< ChevronDown
className = { ` w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${ isDoubanDropdownOpen ? 'rotate-180' : ''
} ` }
className = { ` w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${
isDoubanDropdownOpen ? 'rotate-180' : ''
} ` }
/ >
< / div >
@@ -3783,10 +4512,11 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
handleDoubanDataSourceChange ( option . value ) ;
setIsDoubanDropdownOpen ( false ) ;
} }
className = { ` w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${ siteSettings . DoubanProxyType === option . value
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
: ' text-gray-9 00 dark:text-gray-1 00'
} ` }
className = { ` w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${
siteSettings . DoubanProxyType === option . value
? 'bg-green-50 dark:bg-green-900/20 text-green-6 00 dark:text-green-4 00'
: 'text-gray-900 dark:text-gray-100'
} ` }
>
< span className = 'truncate' > { option . label } < / span >
{ siteSettings . DoubanProxyType === option . value && (
@@ -3826,9 +4556,7 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
{ /* 豆瓣代理地址设置 - 仅在选择自定义代理时显示 */ }
{ siteSettings . DoubanProxyType === 'custom' && (
< div >
< label
className = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
< label className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2' >
豆 瓣 代 理 地 址
< / label >
< input
@@ -3841,7 +4569,7 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
DoubanProxy : e.target.value ,
} ) )
}
className = " w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 shadow-sm hover:border-gray-400 dark:hover:border-gray-500"
className = ' w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 shadow-sm hover:border-gray-400 dark:hover:border-gray-500'
/ >
< p className = 'mt-1 text-xs text-gray-500 dark:text-gray-400' >
自 定 义 代 理 服 务 器 地 址
@@ -3853,9 +4581,7 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
{ /* 豆瓣图片代理设置 */ }
< div className = 'space-y-3' >
< div >
< label
className = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
< label className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2' >
豆 瓣 图 片 代 理
< / label >
< div className = 'relative' data-dropdown = 'douban-image-proxy' >
@@ -3867,7 +4593,7 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
! isDoubanImageProxyDropdownOpen
)
}
className = " w-full px-3 py-2.5 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 text-left"
className = ' w-full px-3 py-2.5 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 text-left'
>
{
doubanImageProxyTypeOptions . find (
@@ -3879,8 +4605,9 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
{ /* 下拉箭头 */ }
< div className = 'absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none' >
< ChevronDown
className = { ` w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${ isDoubanImageProxyDropdownOpen ? 'rotate-180' : ''
} ` }
className = { ` w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${
isDoubanImageProxyDropdownOpen ? 'rotate-180' : ''
} ` }
/ >
< / div >
@@ -3895,10 +4622,11 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
handleDoubanImageProxyChange ( option . value ) ;
setIsDoubanImageProxyDropdownOpen ( false ) ;
} }
className = { ` w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${ siteSettings . DoubanImageProxyType === option . value
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
: ' text-gray-9 00 dark:text-gray-1 00'
} ` }
className = { ` w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${
siteSettings . DoubanImageProxyType === option . value
? 'bg-green-50 dark:bg-green-900/20 text-green-6 00 dark:text-green-4 00'
: 'text-gray-900 dark:text-gray-100'
} ` }
>
< span className = 'truncate' > { option . label } < / span >
{ siteSettings . DoubanImageProxyType === option . value && (
@@ -3938,9 +4666,7 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
{ /* 豆瓣代理地址设置 - 仅在选择自定义代理时显示 */ }
{ siteSettings . DoubanImageProxyType === 'custom' && (
< div >
< label
className = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
< label className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2' >
豆 瓣 图 片 代 理 地 址
< / label >
< input
@@ -3953,7 +4679,7 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
DoubanImageProxy : e.target.value ,
} ) )
}
className = " w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 shadow-sm hover:border-gray-400 dark:hover:border-gray-500"
className = ' w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 shadow-sm hover:border-gray-400 dark:hover:border-gray-500'
/ >
< p className = 'mt-1 text-xs text-gray-500 dark:text-gray-400' >
自 定 义 图 片 代 理 服 务 器 地 址
@@ -4003,9 +4729,7 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
{ /* 禁用黄色过滤器 */ }
< div >
< div className = 'flex items-center justify-between' >
< label
className = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
< label className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2' >
禁 用 黄 色 过 滤 器
< / label >
< button
@@ -4016,16 +4740,20 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
DisableYellowFilter : ! prev . DisableYellowFilter ,
} ) )
}
className = { ` relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${ siteSettings . DisableYellowFilter
? buttonStyles.toggleOn
: buttonStyles.toggleOff
} ` }
className = { ` relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
siteSettings . DisableYellowFilter
? buttonStyles.toggleOn
: buttonStyles.toggleOff
} ` }
>
< span
className = { ` inline-block h-4 w-4 transform rounded-full ${ buttonStyles . toggleThumb } transition-transform ${ siteSettings . DisableYellowFilter
? buttonStyles.toggleThumbOn
: buttonStyles.toggleThumbOff
} ` }
className = { ` inline-block h-4 w-4 transform rounded-full ${
buttonStyles . toggleThumb
} transition-transform ${
siteSettings . DisableYellowFilter
? buttonStyles.toggleThumbOn
: buttonStyles.toggleThumbOff
} ` }
/ >
< / button >
< / div >
@@ -4037,9 +4765,7 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
{ /* 流式搜索 */ }
< div >
< div className = 'flex items-center justify-between' >
< label
className = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
< label className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2' >
启 用 流 式 搜 索
< / label >
< button
@@ -4050,16 +4776,20 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
FluidSearch : ! prev . FluidSearch ,
} ) )
}
className = { ` relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${ siteSettings . FluidSearch
? buttonStyles.toggleOn
: buttonStyles.toggleOff
} ` }
className = { ` relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
siteSettings . FluidSearch
? buttonStyles.toggleOn
: buttonStyles.toggleOff
} ` }
>
< span
className = { ` inline-block h-4 w-4 transform rounded-full ${ buttonStyles . toggleThumb } transition-transform ${ siteSettings . FluidSearch
? buttonStyles.toggleThumbOn
: buttonStyles.toggleThumbOff
} ` }
className = { ` inline-block h-4 w-4 transform rounded-full ${
buttonStyles . toggleThumb
} transition-transform ${
siteSettings . FluidSearch
? buttonStyles.toggleThumbOn
: buttonStyles.toggleThumbOff
} ` }
/ >
< / button >
< / div >
@@ -4128,22 +4858,26 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
{ /* 开启评论 */ }
< div >
< div className = 'flex items-center justify-between' >
< label className = " block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2" >
< label className = ' block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2' >
开 启 评 论
< / label >
< button
type = 'button'
onClick = { ( ) = > handleCommentsToggle ( ! siteSettings . EnableComments ) }
className = { ` relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${ siteSettings . EnableComments
? buttonStyles.toggleOn
: buttonStyles.toggleOff
} ` }
className = { ` relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
siteSettings . EnableComments
? buttonStyles.toggleOn
: buttonStyles.toggleOff
} ` }
>
< span
className = { ` inline-block h-4 w-4 transform rounded-full ${ buttonStyles . toggleThumb } transition-transform ${ siteSettings . EnableComments
? buttonStyles.toggleThumbOn
: buttonStyles.toggleThumbOff
} ` }
className = { ` inline-block h-4 w-4 transform rounded-full ${
buttonStyles . toggleThumb
} transition-transform ${
siteSettings . EnableComments
? buttonStyles.toggleThumbOn
: buttonStyles.toggleThumbOff
} ` }
/ >
< / button >
< / div >
@@ -4158,10 +4892,11 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
< button
onClick = { handleSave }
disabled = { isLoading ( 'saveSiteConfig' ) }
className = { ` px-4 py-2 ${ isLoading ( 'saveSiteConfig' )
? buttonStyles.disabled
: buttonStyles.success
} rounded-lg transition-colors ` }
className = { ` px-4 py-2 ${
isLoading ( 'saveSiteConfig' )
? buttonStyles.disabled
: buttonStyles.success
} rounded-lg transition-colors ` }
>
{ isLoading ( 'saveSiteConfig' ) ? '保存中…' : '保存' }
< / button >
@@ -4179,58 +4914,317 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
/ >
{ /* 开启评论确认弹窗 */ }
{ showEnableCommentsModal && createPortal (
< div className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4' onClick = { ( ) = > setShowEnableCommentsModal ( false ) } >
< div className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full' onClick = { ( e ) = > e . stopPropagation ( ) } >
< div className = 'p-6' >
< div className = 'flex items-center justify-between mb-6' >
< h3 className = 'text-xl font-semibold text-gray-900 dark:text-gray-100' >
开 启 评 论 功 能
< / h3 >
< button
onClick = { ( ) = > setShowEnableCommentsModal ( false ) }
className = 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
< svg className = 'w-6 h-6' fill = 'none' stroke = 'currentColor' viewBox = '0 0 24 24 '>
< path strokeLinecap = 'round' strokeLinejoin = 'round' strokeWidth = { 2 } d = 'M6 18L18 6M6 6l12 12' / >
< / svg >
< / button >
< / div >
{ showEnableCommentsModal &&
createPortal (
< div
className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'
onClick = { ( ) = > setShowEnableCommentsModal ( false ) }
>
< div
className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full'
onClick = { ( e ) = > e . stopPropagation ( ) }
>
< div className = 'p-6' >
< div className = 'flex items-center justify-between mb-6' >
< h3 className = 'text-xl font-semibold text-gray-900 dark:text-gray-100 ' >
开 启 评 论 功 能
< / h3 >
< button
onClick = { ( ) = > setShowEnableCommentsModal ( false ) }
className = 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
< svg
className = 'w-6 h-6'
fill = 'none'
stroke = 'currentColor'
viewBox = '0 0 24 24'
>
< path
strokeLinecap = 'round'
strokeLinejoin = 'round'
strokeWidth = { 2 }
d = 'M6 18L18 6M6 6l12 12'
/ >
< / svg >
< / button >
< / div >
< div className = 'mb-6' >
< div className = 'bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4' >
< div className = 'flex items-center space-x-2 mb-2' >
< AlertTriangle className = 'w-5 h-5 text-yellow-600 dark:text-yellow-400' / >
< span className = 'text-sm font-medium text-yellow-800 dark:text-yellow-300' >
重 要 提 示
< / span >
< div className = 'mb-6' >
< div className = 'bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4' >
< div className = 'flex items-center space-x-2 mb-2' >
< AlertTriangle className = 'w-5 h-5 text-yellow-600 dark:text-yellow-400' / >
< span className = 'text-sm font-medium text-yellow-800 dark:text-yellow-300' >
重 要 提 示
< / span >
< / div >
< p className = 'text-sm text-yellow-700 dark:text-yellow-400' >
评 论 功 能 为 逆 向 抓 取 豆 瓣 评 论 数 据 , 此 功 能 仅 供 学 习 , 开 启 后 请 自 行 承 担 相 关 责 任 和 风 险 。
< / p >
< / div >
< p className = 'text-sm text-yellow-700 dark:text-yellow-400' >
评 论 功 能 为 逆 向 抓 取 豆 瓣 评 论 数 据 , 此 功 能 仅 供 学 习 , 开 启 后 请 自 行 承 担 相 关 责 任 和 风 险 。
< / p >
< / div >
{ /* 操作按钮 */ }
< div className = 'flex justify-end space-x-3' >
< button
onClick = { ( ) = > setShowEnableCommentsModal ( false ) }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . secondary } ` }
>
取 消
< / button >
< button
onClick = { handleConfirmEnableComments }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . primary } ` }
>
我 已 知 晓 , 确 认 开 启
< / button >
< / div >
< / div >
{ /* 操作按钮 */ }
< div className = 'flex justify-end space-x-3' >
< button
onClick = { ( ) = > setShowEnableCommentsModal ( false ) }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . secondary } ` }
>
取 消
< / button >
< button
onClick = { handleConfirmEnableComments }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . primary } ` }
>
我 已 知 晓 , 确 认 开 启
< / button >
< / div >
< / div >
< / div > ,
document . body
) }
< / div >
) ;
} ;
// 自定义去广告配置组件
const CustomAdFilterConfig = ( {
config ,
refreshConfig ,
} : {
config : AdminConfig | null ;
refreshConfig : ( ) = > Promise < void > ;
} ) = > {
const { alertModal , showAlert , hideAlert } = useAlertModal ( ) ;
const { isLoading , withLoading } = useLoadingState ( ) ;
const [ adFilterCode , setAdFilterCode ] = useState ( '' ) ;
// 默认去广告代码
const defaultAdFilterCode = ` function filterAdsFromM3U8(type: string, m3u8Content: string): string {
if (!m3u8Content) return '';
// 按行分割M3U8内容
const lines = m3u8Content.split(' \\ n');
const filteredLines = [];
let nextdelete = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 只过滤#EXT-X-DISCONTINUITY标识
if (!line.includes('#EXT-X-DISCONTINUITY')) {
filteredLines.push(line);
}
}
return filteredLines.join(' \\ n');
} ` ;
useEffect ( ( ) = > {
// 从数据库配置读取自定义去广告代码
if ( config ? . SiteConfig ? . CustomAdFilterCode ) {
setAdFilterCode ( config . SiteConfig . CustomAdFilterCode ) ;
} else {
// 如果数据库没有保存的代码,使用默认代码
setAdFilterCode ( defaultAdFilterCode ) ;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
} , [ config ] ) ;
// 移除 TypeScript 类型注解,转换为纯 JavaScript
const removeTypeAnnotations = ( code : string ) : string = > {
return (
code
// 移除函数参数的类型注解:name: type
. replace (
/(\w+)\s*:\s*(string|number|boolean|any|void|never|unknown|object)\s*([,)])/g ,
'$1$3'
)
// 移除函数返回值类型注解:): type {
. replace (
/\)\s*:\s*(string|number|boolean|any|void|never|unknown|object)\s*\{/g ,
') {'
)
// 移除变量声明的类型注解:const name: type =
. replace (
/(const|let|var)\s+(\w+)\s*:\s*(string|number|boolean|any|void|never|unknown|object)\s*=/g ,
'$1 $2 ='
)
) ;
} ;
// 保存自定义去广告代码
const handleSave = async ( ) = > {
await withLoading ( 'saveAdFilterCode' , async ( ) = > {
try {
// 验证代码语法
try {
// 移除类型注解后验证
const jsCode = removeTypeAnnotations ( adFilterCode ) ;
// 使用 Function 构造器验证代码是否可以解析
new Function (
'type' ,
'm3u8Content' ,
jsCode + '\nreturn filterAdsFromM3U8(type, m3u8Content);'
) ;
} catch ( parseError ) {
console . error ( '代码验证失败:' , parseError ) ;
showError (
'代码语法错误:' +
( parseError instanceof Error
? parseError . message
: '请检查代码格式' ) ,
showAlert
) ;
return ;
}
// 更新配置到数据库
if ( ! config ) {
showError ( '配置未加载' , showAlert ) ;
return ;
}
// 准备更新的站点配置,包含自定义去广告代码
const updatedSiteConfig = {
. . . config . SiteConfig ,
CustomAdFilterCode : adFilterCode ,
CustomAdFilterVersion : Date.now ( ) , // 使用时间戳作为版本号
} ;
const response = await fetch ( '/api/admin/site' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON.stringify ( updatedSiteConfig ) ,
} ) ;
if ( ! response . ok ) {
const errorData = await response . json ( ) ;
throw new Error ( errorData . error || '保存配置失败' ) ;
}
// 刷新配置
await refreshConfig ( ) ;
showSuccess ( '去广告代码保存成功,刷新后生效' , showAlert ) ;
} catch ( err ) {
showError ( err instanceof Error ? err . message : '保存失败' , showAlert ) ;
throw err ;
}
} ) ;
} ;
// 重置为默认代码
const handleReset = ( ) = > {
setAdFilterCode ( defaultAdFilterCode ) ;
showSuccess ( '已重置为默认代码' , showAlert ) ;
} ;
if ( ! config ) {
return (
< div className = 'text-center text-gray-500 dark:text-gray-400' >
加 载 中 . . .
< / div >
) ;
}
return (
< div className = 'space-y-4' >
{ /* 说明区域 */ }
< div className = 'bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4' >
< div className = 'flex items-center space-x-2 mb-2' >
< svg
className = 'w-5 h-5 text-blue-600 dark:text-blue-400'
fill = 'none'
stroke = 'currentColor'
viewBox = '0 0 24 24'
>
< path
strokeLinecap = 'round'
strokeLinejoin = 'round'
strokeWidth = { 2 }
d = 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z'
/ >
< / svg >
< span className = 'text-sm font-medium text-blue-800 dark:text-blue-300' >
使 用 说 明
< / span >
< / div >
< div className = 'text-sm text-blue-700 dark:text-blue-400 space-y-1' >
< p > • 此 功 能 用 于 自 定 义 M3U8 播 放 列 表 的 去 广 告 逻 辑 < / p >
< p > • 配 置 保 存 到 数 据 库 , 对 全 平 台 所 有 用 户 生 效 < / p >
< p >
• 客 户 端 会 自 动 缓 存 代 码 , 只 在 版 本 更 新 时 重 新 获 取 , 不 会 频 繁 请 求 服 务 器
< / p >
< p >
• 函 数 签 名 必 须 为 : { ' ' }
< code className = 'bg-blue-100 dark:bg-blue-900/40 px-1 rounded' >
filterAdsFromM3U8 ( type , m3u8Content )
< / code >
< / p >
< p > • type 参 数 为 视 频 源 类 型 , m3u8Content 为 播 放 列 表 内 容 < / p >
< p > • 函 数 需 要 返 回 处 理 后 的 M3U8 内 容 < / p >
< p > • 支 持 TypeScript 类 型 注 解 , 保 存 时 会 自 动 转 换 为 JavaScript < / p >
< / div >
< / div >
{ /* 代码编辑区域 */ }
< div className = 'space-y-3' >
< div className = 'flex items-center justify-between' >
< label className = 'text-sm font-medium text-gray-700 dark:text-gray-300' >
自 定 义 去 广 告 代 码
< / label >
< button
onClick = { handleReset }
className = { ` ${ buttonStyles . secondarySmall } ` }
>
重 置 为 默 认
< / button >
< / div >
< div className = 'relative' >
< textarea
value = { adFilterCode }
onChange = { ( e ) = > setAdFilterCode ( e . target . value ) }
rows = { 25 }
placeholder = '请输入自定义去广告代码...'
className = 'w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono text-sm leading-relaxed resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 hover:border-gray-400 dark:hover:border-gray-500'
style = { {
fontFamily :
'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace' ,
} }
spellCheck = { false }
data-gramm = { false }
/ >
< / div >
< div className = 'flex items-center justify-between' >
< div className = 'text-xs text-gray-500 dark:text-gray-400' >
修 改 后 需 保 存 才 能 生 效 , 保 存 前 会 进 行 语 法 验 证
< / div >
< / div > ,
document . body
) }
< button
onClick = { handleSave }
disabled = { isLoading ( 'saveAdFilterCode' ) }
className = { ` px-4 py-2 rounded-lg transition-colors ${
isLoading ( 'saveAdFilterCode' )
? buttonStyles.disabled
: buttonStyles.success
} ` }
>
{ isLoading ( 'saveAdFilterCode' ) ? '保存中…' : '保存' }
< / button >
< / div >
< / div >
{ /* 通用弹窗组件 */ }
< AlertModal
isOpen = { alertModal . isOpen }
onClose = { hideAlert }
type = { alertModal . type }
title = { alertModal . title }
message = { alertModal . message }
timer = { alertModal . timer }
showConfirm = { alertModal . showConfirm }
/ >
< / div >
) ;
} ;
@@ -4247,7 +5241,8 @@ const LiveSourceConfig = ({
const { isLoading , withLoading } = useLoadingState ( ) ;
const [ liveSources , setLiveSources ] = useState < LiveDataSource [ ] > ( [ ] ) ;
const [ showAddForm , setShowAddForm ] = useState ( false ) ;
const [ editingLiveSource , setEditingLiveSource ] = useState < LiveDataSource | null > ( null ) ;
const [ editingLiveSource , setEditingLiveSource ] =
useState < LiveDataSource | null > ( null ) ;
const [ orderChanged , setOrderChanged ] = useState ( false ) ;
const [ isRefreshing , setIsRefreshing ] = useState ( false ) ;
const [ newLiveSource , setNewLiveSource ] = useState < LiveDataSource > ( {
@@ -4310,13 +5305,17 @@ const LiveSourceConfig = ({
const target = liveSources . find ( ( s ) = > s . key === key ) ;
if ( ! target ) return ;
const action = target . disabled ? 'enable' : 'disable' ;
withLoading ( ` toggleLiveSource_ ${ key } ` , ( ) = > callLiveSourceApi ( { action , key } ) ) . catch ( ( ) = > {
withLoading ( ` toggleLiveSource_ ${ key } ` , ( ) = >
callLiveSourceApi ( { action , key } )
) . catch ( ( ) = > {
console . error ( '操作失败' , action , key ) ;
} ) ;
} ;
const handleDelete = ( key : string ) = > {
withLoading ( ` deleteLiveSource_ ${ key } ` , ( ) = > callLiveSourceApi ( { action : 'delete' , key } ) ) . catch ( ( ) = > {
withLoading ( ` deleteLiveSource_ ${ key } ` , ( ) = >
callLiveSourceApi ( { action : 'delete' , key } )
) . catch ( ( ) = > {
console . error ( '操作失败' , 'delete' , key ) ;
} ) ;
} ;
@@ -4340,7 +5339,12 @@ const LiveSourceConfig = ({
// 刷新成功后重新获取配置
await refreshConfig ( ) ;
showAlert ( { type : 'success' , title : '刷新成功' , message : '直播源已刷新' , timer : 2000 } ) ;
showAlert ( {
type : 'success' ,
title : '刷新成功' ,
message : '直播源已刷新' ,
timer : 2000 ,
} ) ;
} catch ( err ) {
showError ( err instanceof Error ? err . message : '刷新失败' , showAlert ) ;
throw err ;
@@ -4377,7 +5381,8 @@ const LiveSourceConfig = ({
} ;
const handleEditLiveSource = ( ) = > {
if ( ! editingLiveSource || ! editingLiveSource . name || ! editingLiveSource . url ) return ;
if ( ! editingLiveSource || ! editingLiveSource . name || ! editingLiveSource . url )
return ;
withLoading ( 'editLiveSource' , async ( ) = > {
await callLiveSourceApi ( {
action : 'edit' ,
@@ -4408,7 +5413,9 @@ const LiveSourceConfig = ({
const handleSaveOrder = ( ) = > {
const order = liveSources . map ( ( s ) = > s . key ) ;
withLoading ( 'saveLiveSourceOrder' , ( ) = > callLiveSourceApi ( { action : 'sort' , order } ) )
withLoading ( 'saveLiveSourceOrder' , ( ) = >
callLiveSourceApi ( { action : 'sort' , order } )
)
. then ( ( ) = > {
setOrderChanged ( false ) ;
} )
@@ -4466,14 +5473,17 @@ const LiveSourceConfig = ({
{ liveSource . ua || '-' }
< / td >
< td className = 'px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100 text-center' >
{ liveSource . channelNumber && liveSource . channelNumber > 0 ? liveSource . channelNumber : '-' }
{ liveSource . channelNumber && liveSource . channelNumber > 0
? liveSource . channelNumber
: '-' }
< / td >
< td className = 'px-6 py-4 whitespace-nowrap max-w-[1rem]' >
< span
className = { ` px-2 py-1 text-xs rounded-full ${ ! liveSource . disabled
? 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300'
: 'bg-red -100 dark:bg-red -900/20 text-red -800 dark:text-red -300'
} ` }
className = { ` px-2 py-1 text-xs rounded-full ${
! liveSource . disabled
? 'bg-g reen -100 dark:bg-g reen -900/20 text-g reen -800 dark:text-g reen -300'
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300'
} ` }
>
{ ! liveSource . disabled ? '启用中' : '已禁用' }
< / span >
@@ -4482,10 +5492,15 @@ const LiveSourceConfig = ({
< button
onClick = { ( ) = > handleToggleEnable ( liveSource . key ) }
disabled = { isLoading ( ` toggleLiveSource_ ${ liveSource . key } ` ) }
className = { ` inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${ ! liveSource . disabled
? buttonStyles.roundedDanger
: buttonStyles.roundedSuccess
} transition-colors ${ isLoading ( ` toggleLiveSource_ ${ liveSource . key } ` ) ? 'opacity-50 cursor-not-allowed' : '' } ` }
className = { ` inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${
! liveSource . disabled
? buttonStyles.roundedDanger
: buttonStyles.roundedSuccess
} transition-colors ${
isLoading ( ` toggleLiveSource_ ${ liveSource . key } ` )
? 'opacity-50 cursor-not-allowed'
: ''
} ` }
>
{ ! liveSource . disabled ? '禁用' : '启用' }
< / button >
@@ -4494,14 +5509,22 @@ const LiveSourceConfig = ({
< button
onClick = { ( ) = > setEditingLiveSource ( liveSource ) }
disabled = { isLoading ( ` editLiveSource_ ${ liveSource . key } ` ) }
className = { ` ${ buttonStyles . roundedPrimary } ${ isLoading ( ` editLiveSource_ ${ liveSource . key } ` ) ? 'opacity-50 cursor-not-allowed' : '' } ` }
className = { ` ${ buttonStyles . roundedPrimary } ${
isLoading ( ` editLiveSource_ ${ liveSource . key } ` )
? 'opacity-50 cursor-not-allowed'
: ''
} ` }
>
编 辑
< / button >
< button
onClick = { ( ) = > handleDelete ( liveSource . key ) }
disabled = { isLoading ( ` deleteLiveSource_ ${ liveSource . key } ` ) }
className = { ` ${ buttonStyles . roundedSecondary } ${ isLoading ( ` deleteLiveSource_ ${ liveSource . key } ` ) ? 'opacity-50 cursor-not-allowed' : '' } ` }
className = { ` ${ buttonStyles . roundedSecondary } ${
isLoading ( ` deleteLiveSource_ ${ liveSource . key } ` )
? 'opacity-50 cursor-not-allowed'
: ''
} ` }
>
删 除
< / button >
@@ -4531,16 +5554,23 @@ const LiveSourceConfig = ({
< button
onClick = { handleRefreshLiveSources }
disabled = { isRefreshing || isLoading ( 'refreshLiveSources' ) }
className = { ` px-3 py-1.5 text-sm font-medium flex items-center space-x-2 ${ isRefreshing || isLoading ( 'refreshLiveSources' )
? 'bg-gray-400 dark:bg-gray-600 cursor-not-allowed text-white rounded-lg'
: 'bg-blue-600 hover:bg-blue-7 00 dark:bg-blue -600 dark:hover:bg-blue-700 text-white rounded-lg transition-colors '
} ` }
className = { ` px-3 py-1.5 text-sm font-medium flex items-center space-x-2 ${
isRefreshing || isLoading ( 'refreshLiveSources' )
? 'bg-gray-4 00 dark:bg-gray -600 cursor-not-allowed text-white rounded-lg'
: 'bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded-lg transition-colors'
} ` }
>
< span > { isRefreshing || isLoading ( 'refreshLiveSources' ) ? '刷新中...' : '刷新直播源' } < / span >
< span >
{ isRefreshing || isLoading ( 'refreshLiveSources' )
? '刷新中...'
: '刷新直播源' }
< / span >
< / button >
< button
onClick = { ( ) = > setShowAddForm ( ! showAddForm ) }
className = { showAddForm ? buttonStyles.secondary : buttonStyles.success }
className = {
showAddForm ? buttonStyles.secondary : buttonStyles.success
}
>
{ showAddForm ? '取消' : '添加直播源' }
< / button >
@@ -4595,13 +5625,24 @@ const LiveSourceConfig = ({
}
className = 'px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
/ >
< / div >
< div className = 'flex justify-end' >
< button
onClick = { handleAddLiveSource }
disabled = { ! newLiveSource . name || ! newLiveSource . key || ! newLiveSource . url || isLoading ( 'addLiveSource' ) }
className = { ` w-full sm:w-auto px-4 py-2 ${ ! newLiveSource . name || ! newLiveSource . key || ! newLiveSource . url || isLoading ( 'addLiveSource' ) ? buttonStyles.disabled : buttonStyles.success } ` }
disabled = {
! newLiveSource . name ||
! newLiveSource . key ||
! newLiveSource . url ||
isLoading ( 'addLiveSource' )
}
className = { ` w-full sm:w-auto px-4 py-2 ${
! newLiveSource . name ||
! newLiveSource . key ||
! newLiveSource . url ||
isLoading ( 'addLiveSource' )
? buttonStyles.disabled
: buttonStyles.success
} ` }
>
{ isLoading ( 'addLiveSource' ) ? '添加中...' : '添加' }
< / button >
@@ -4632,7 +5673,9 @@ const LiveSourceConfig = ({
type = 'text'
value = { editingLiveSource . name }
onChange = { ( e ) = >
setEditingLiveSource ( ( prev ) = > prev ? ( { . . . prev , name : e.target.value } ) : null )
setEditingLiveSource ( ( prev ) = >
prev ? { . . . prev , name : e.target.value } : null
)
}
className = 'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
/ >
@@ -4656,7 +5699,9 @@ const LiveSourceConfig = ({
type = 'text'
value = { editingLiveSource . url }
onChange = { ( e ) = >
setEditingLiveSource ( ( prev ) = > prev ? ( { . . . prev , url : e.target.value } ) : null )
setEditingLiveSource ( ( prev ) = >
prev ? { . . . prev , url : e.target.value } : null
)
}
className = 'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
/ >
@@ -4669,7 +5714,9 @@ const LiveSourceConfig = ({
type = 'text'
value = { editingLiveSource . epg }
onChange = { ( e ) = >
setEditingLiveSource ( ( prev ) = > prev ? ( { . . . prev , epg : e.target.value } ) : null )
setEditingLiveSource ( ( prev ) = >
prev ? { . . . prev , epg : e.target.value } : null
)
}
className = 'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
/ >
@@ -4682,7 +5729,9 @@ const LiveSourceConfig = ({
type = 'text'
value = { editingLiveSource . ua }
onChange = { ( e ) = >
setEditingLiveSource ( ( prev ) = > prev ? ( { . . . prev , ua : e.target.value } ) : null )
setEditingLiveSource ( ( prev ) = >
prev ? { . . . prev , ua : e.target.value } : null
)
}
className = 'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
/ >
@@ -4697,8 +5746,18 @@ const LiveSourceConfig = ({
< / button >
< button
onClick = { handleEditLiveSource }
disabled = { ! editingLiveSource . name || ! editingLiveSource . url || isLoading ( 'editLiveSource' ) }
className = { ` ${ ! editingLiveSource . name || ! editingLiveSource . url || isLoading ( 'editLiveSource' ) ? buttonStyles.disabled : buttonStyles.success } ` }
disabled = {
! editingLiveSource . name ||
! editingLiveSource . url ||
isLoading ( 'editLiveSource' )
}
className = { ` ${
! editingLiveSource . name ||
! editingLiveSource . url ||
isLoading ( 'editLiveSource' )
? buttonStyles.disabled
: buttonStyles.success
} ` }
>
{ isLoading ( 'editLiveSource' ) ? '保存中...' : '保存' }
< / button >
@@ -4707,7 +5766,10 @@ const LiveSourceConfig = ({
) }
{ /* 直播源表格 */ }
< div className = 'border border-gray-200 dark:border-gray-700 rounded-lg max-h-[28rem] overflow-y-auto overflow-x-auto relative' data-table = "live-source-list" >
< div
className = 'border border-gray-200 dark:border-gray-700 rounded-lg max-h-[28rem] overflow-y-auto overflow-x-auto relative'
data-table = 'live-source-list'
>
< table className = 'min-w-full divide-y divide-gray-200 dark:divide-gray-700' >
< thead className = 'bg-gray-50 dark:bg-gray-900 sticky top-0 z-10' >
< tr >
@@ -4765,7 +5827,11 @@ const LiveSourceConfig = ({
< button
onClick = { handleSaveOrder }
disabled = { isLoading ( 'saveLiveSourceOrder' ) }
className = { ` px-3 py-1.5 text-sm ${ isLoading ( 'saveLiveSourceOrder' ) ? buttonStyles.disabled : buttonStyles.primary } ` }
className = { ` px-3 py-1.5 text-sm ${
isLoading ( 'saveLiveSourceOrder' )
? buttonStyles.disabled
: buttonStyles.primary
} ` }
>
{ isLoading ( 'saveLiveSourceOrder' ) ? '保存中...' : '保存排序' }
< / button >
@@ -4782,8 +5848,6 @@ const LiveSourceConfig = ({
timer = { alertModal . timer }
showConfirm = { alertModal . showConfirm }
/ >
< / div >
) ;
} ;
@@ -4804,6 +5868,7 @@ function AdminPageClient() {
categoryConfig : false ,
configFile : false ,
dataMigration : false ,
customAdFilter : false ,
} ) ;
// 获取管理员配置
@@ -4929,7 +5994,10 @@ function AdminPageClient() {
isExpanded = { expandedTabs . configFile }
onToggle = { ( ) = > toggleTab ( 'configFile' ) }
>
< ConfigFileComponent config = { config } refreshConfig = { fetchConfig } / >
< ConfigFileComponent
config = { config }
refreshConfig = { fetchConfig }
/ >
< / CollapsibleTab >
) }
@@ -5004,6 +6072,34 @@ function AdminPageClient() {
< CategoryConfig config = { config } refreshConfig = { fetchConfig } / >
< / CollapsibleTab >
{ /* 自定义去广告标签 */ }
< CollapsibleTab
title = '自定义去广告'
icon = {
< svg
width = '20'
height = '20'
viewBox = '0 0 24 24'
fill = 'none'
stroke = 'currentColor'
strokeWidth = '2'
strokeLinecap = 'round'
strokeLinejoin = 'round'
className = 'text-gray-600 dark:text-gray-400'
>
< path d = 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z' / >
< path d = 'M8 12h8' / >
< / svg >
}
isExpanded = { expandedTabs . customAdFilter }
onToggle = { ( ) = > toggleTab ( 'customAdFilter' ) }
>
< CustomAdFilterConfig
config = { config }
refreshConfig = { fetchConfig }
/ >
< / CollapsibleTab >
{ /* 数据迁移标签 - 仅站长可见 */ }
{ role === 'owner' && (
< CollapsibleTab
@@ -5036,61 +6132,92 @@ function AdminPageClient() {
/ >
{ /* 重置配置确认弹窗 */ }
{ showResetConfigModal && createPortal (
< div className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4' onClick = { ( ) = > setShowResetConfigModal ( false ) } >
< div className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full' onClick = { ( e ) = > e . stopPropagation ( ) } >
< div className = 'p-6' >
< div className = 'flex items-center justify-between mb-6' >
< h3 className = 'text-xl font-semibold text-gray-900 dark:text-gray-100' >
确 认 重 置 配 置
< / h3 >
< button
onClick = { ( ) = > setShowResetConfigModal ( false ) }
className = 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
< svg className = 'w-6 h-6' fill = 'none' stroke = 'currentColor' viewBox = '0 0 24 24 '>
< path strokeLinecap = 'round' strokeLinejoin = 'round' strokeWidth = { 2 } d = 'M6 18L18 6M6 6l12 12' / >
< / svg >
< / button >
< / div >
< div className = 'mb-6' >
< div className = 'bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4 mb-4' >
< div className = 'flex items-center space-x-2 mb-2' >
< svg className = 'w-5 h-5 text-yellow-600 dark:text-yellow-400' fill = 'none' stroke = 'currentColor' viewBox = '0 0 24 24' >
< path strokeLinecap = 'round' strokeLinejoin = 'round' strokeWidth = { 2 } d = 'M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' / >
{ showResetConfigModal &&
createPortal (
< div
className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'
onClick = { ( ) = > setShowResetConfigModal ( false ) }
>
< div
className = 'bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full'
onClick = { ( e ) = > e . stopPropagation ( ) }
>
< div className = 'p-6' >
< div className = 'flex items-center justify-between mb-6' >
< h3 className = 'text-xl font-semibold text-gray-900 dark:text-gray-100 ' >
确 认 重 置 配 置
< / h3 >
< button
onClick = { ( ) = > setShowResetConfigModal ( false ) }
className = 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
< svg
className = 'w-6 h-6'
fill = 'none'
stroke = 'currentColor'
viewBox = '0 0 24 24'
>
< path
strokeLinecap = 'round'
strokeLinejoin = 'round'
strokeWidth = { 2 }
d = 'M6 18L18 6M6 6l12 12'
/ >
< / svg >
< span className = 'text-sm font-medium text-yellow-800 dark:text-yellow-300' >
⚠ ️ 危 险 操 作 警 告
< / span >
< / button >
< / div >
< div className = 'mb-6' >
< div className = 'bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4 mb-4' >
< div className = 'flex items-center space-x-2 mb-2' >
< svg
className = 'w-5 h-5 text-yellow-600 dark:text-yellow-400'
fill = 'none'
stroke = 'currentColor'
viewBox = '0 0 24 24'
>
< path
strokeLinecap = 'round'
strokeLinejoin = 'round'
strokeWidth = { 2 }
d = 'M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z'
/ >
< / svg >
< span className = 'text-sm font-medium text-yellow-800 dark:text-yellow-300' >
⚠ ️ 危 险 操 作 警 告
< / span >
< / div >
< p className = 'text-sm text-yellow-700 dark:text-yellow-400' >
此 操 作 将 重 置 用 户 封 禁 和 管 理 员 设 置 、 自 定 义 视 频 源 , 站 点 配 置 将 重 置 为 默 认 值 , 是 否 继 续 ?
< / p >
< / div >
< p className = 'text-sm text-yellow-700 dark:text-yellow-400' >
此 操 作 将 重 置 用 户 封 禁 和 管 理 员 设 置 、 自 定 义 视 频 源 , 站 点 配 置 将 重 置 为 默 认 值 , 是 否 继 续 ?
< / p >
< / div >
{ /* 操作按钮 */ }
< div className = 'flex justify-end space-x-3' >
< button
onClick = { ( ) = > setShowResetConfigModal ( false ) }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . secondary } ` }
>
取 消
< / button >
< button
onClick = { handleConfirmResetConfig }
disabled = { isLoading ( 'resetConfig' ) }
className = { ` px-6 py-2.5 text-sm font-medium ${
isLoading ( 'resetConfig' )
? buttonStyles.disabled
: buttonStyles.danger
} ` }
>
{ isLoading ( 'resetConfig' ) ? '重置中...' : '确认重置' }
< / button >
< / div >
< / div >
{ /* 操作按钮 */ }
< div className = 'flex justify-end space-x-3' >
< button
onClick = { ( ) = > setShowResetConfigModal ( false ) }
className = { ` px-6 py-2.5 text-sm font-medium ${ buttonStyles . secondary } ` }
>
取 消
< / button >
< button
onClick = { handleConfirmResetConfig }
disabled = { isLoading ( 'resetConfig' ) }
className = { ` px-6 py-2.5 text-sm font-medium ${ isLoading ( 'resetConfig' ) ? buttonStyles.disabled : buttonStyles.danger } ` }
>
{ isLoading ( 'resetConfig' ) ? '重置中...' : '确认重置' }
< / button >
< / div >
< / div >
< / div >
< / div > ,
document . body
) }
< / div > ,
document . body
) }
< / PageLayout >
) ;
}