feat: add admin page loading ui

This commit is contained in:
shinya
2025-08-25 02:02:38 +08:00
parent 905544b6fa
commit b8604f3c8d
+192 -108
View File
@@ -228,6 +228,33 @@ const showSuccess = (message: string, showAlert?: (config: any) => void) => {
} }
}; };
// 通用加载状态管理系统
interface LoadingState {
[key: string]: boolean;
}
const useLoadingState = () => {
const [loadingStates, setLoadingStates] = useState<LoadingState>({});
const setLoading = (key: string, loading: boolean) => {
setLoadingStates(prev => ({ ...prev, [key]: loading }));
};
const isLoading = (key: string) => loadingStates[key] || false;
const withLoading = async (key: string, operation: () => Promise<any>): Promise<any> => {
setLoading(key, true);
try {
const result = await operation();
return result;
} finally {
setLoading(key, false);
}
};
return { loadingStates, setLoading, isLoading, withLoading };
};
// 新增站点配置类型 // 新增站点配置类型
interface SiteConfig { interface SiteConfig {
SiteName: string; SiteName: string;
@@ -320,6 +347,7 @@ interface UserConfigProps {
const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
const { alertModal, showAlert, hideAlert } = useAlertModal(); const { alertModal, showAlert, hideAlert } = useAlertModal();
const { isLoading, withLoading } = useLoadingState();
const [showAddUserForm, setShowAddUserForm] = useState(false); const [showAddUserForm, setShowAddUserForm] = useState(false);
const [showChangePasswordForm, setShowChangePasswordForm] = useState(false); const [showChangePasswordForm, setShowChangePasswordForm] = useState(false);
const [showAddUserGroupForm, setShowAddUserGroupForm] = useState(false); const [showAddUserGroupForm, setShowAddUserGroupForm] = useState(false);
@@ -390,6 +418,7 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
groupName: string, groupName: string,
enabledApis?: string[] enabledApis?: string[]
) => { ) => {
return withLoading(`userGroup_${action}_${groupName}`, async () => {
try { try {
const res = await fetch('/api/admin/user', { const res = await fetch('/api/admin/user', {
method: 'POST', method: 'POST',
@@ -420,7 +449,9 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
showSuccess(action === 'add' ? '用户组添加成功' : action === 'edit' ? '用户组更新成功' : '用户组删除成功', showAlert); showSuccess(action === 'add' ? '用户组添加成功' : action === 'edit' ? '用户组更新成功' : '用户组删除成功', showAlert);
} catch (err) { } catch (err) {
showError(err instanceof Error ? err.message : '操作失败', showAlert); showError(err instanceof Error ? err.message : '操作失败', showAlert);
throw err;
} }
});
}; };
const handleAddUserGroup = () => { const handleAddUserGroup = () => {
@@ -466,6 +497,7 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
// 为用户分配用户组 // 为用户分配用户组
const handleAssignUserGroup = async (username: string, userGroups: string[]) => { const handleAssignUserGroup = async (username: string, userGroups: string[]) => {
return withLoading(`assignUserGroup_${username}`, async () => {
try { try {
const res = await fetch('/api/admin/user', { const res = await fetch('/api/admin/user', {
method: 'POST', method: 'POST',
@@ -486,34 +518,39 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
showSuccess('用户组分配成功', showAlert); showSuccess('用户组分配成功', showAlert);
} catch (err) { } catch (err) {
showError(err instanceof Error ? err.message : '操作失败', showAlert); showError(err instanceof Error ? err.message : '操作失败', showAlert);
throw err;
} }
});
}; };
const handleBanUser = async (uname: string) => { const handleBanUser = async (uname: string) => {
await handleUserAction('ban', uname); await withLoading(`banUser_${uname}`, () => handleUserAction('ban', uname));
}; };
const handleUnbanUser = async (uname: string) => { const handleUnbanUser = async (uname: string) => {
await handleUserAction('unban', uname); await withLoading(`unbanUser_${uname}`, () => handleUserAction('unban', uname));
}; };
const handleSetAdmin = async (uname: string) => { const handleSetAdmin = async (uname: string) => {
await handleUserAction('setAdmin', uname); await withLoading(`setAdmin_${uname}`, () => handleUserAction('setAdmin', uname));
}; };
const handleRemoveAdmin = async (uname: string) => { const handleRemoveAdmin = async (uname: string) => {
await handleUserAction('cancelAdmin', uname); await withLoading(`removeAdmin_${uname}`, () => handleUserAction('cancelAdmin', uname));
}; };
const handleAddUser = async () => { const handleAddUser = async () => {
if (!newUser.username || !newUser.password) return; 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: '' }); setNewUser({ username: '', password: '', userGroup: '' });
setShowAddUserForm(false); setShowAddUserForm(false);
});
}; };
const handleChangePassword = async () => { const handleChangePassword = async () => {
if (!changePasswordUser.username || !changePasswordUser.password) return; if (!changePasswordUser.username || !changePasswordUser.password) return;
await withLoading(`changePassword_${changePasswordUser.username}`, async () => {
await handleUserAction( await handleUserAction(
'changePassword', 'changePassword',
changePasswordUser.username, changePasswordUser.username,
@@ -521,6 +558,7 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
); );
setChangePasswordUser({ username: '', password: '' }); setChangePasswordUser({ username: '', password: '' });
setShowChangePasswordForm(false); setShowChangePasswordForm(false);
});
}; };
const handleShowChangePasswordForm = (username: string) => { const handleShowChangePasswordForm = (username: string) => {
@@ -557,6 +595,7 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
const handleSaveUserGroups = async () => { const handleSaveUserGroups = async () => {
if (!selectedUserForGroup) return; if (!selectedUserForGroup) return;
await withLoading(`saveUserGroups_${selectedUserForGroup.username}`, async () => {
try { try {
await handleAssignUserGroup(selectedUserForGroup.username, selectedUserGroups); await handleAssignUserGroup(selectedUserForGroup.username, selectedUserGroups);
setShowConfigureUserGroupModal(false); setShowConfigureUserGroupModal(false);
@@ -565,6 +604,7 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
} catch (err) { } catch (err) {
// 错误处理已在 handleAssignUserGroup 中处理 // 错误处理已在 handleAssignUserGroup 中处理
} }
});
}; };
// 处理用户选择 // 处理用户选择
@@ -599,6 +639,7 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
const handleBatchSetUserGroup = async (userGroup: string) => { const handleBatchSetUserGroup = async (userGroup: string) => {
if (selectedUsers.size === 0) return; if (selectedUsers.size === 0) return;
await withLoading('batchSetUserGroup', async () => {
try { try {
const res = await fetch('/api/admin/user', { const res = await fetch('/api/admin/user', {
method: 'POST', method: 'POST',
@@ -625,7 +666,9 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
await refreshConfig(); await refreshConfig();
} catch (err) { } catch (err) {
showError('批量设置用户组失败', showAlert); showError('批量设置用户组失败', showAlert);
throw err;
} }
});
}; };
@@ -644,6 +687,7 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
const handleSaveUserApis = async () => { const handleSaveUserApis = async () => {
if (!selectedUser) return; if (!selectedUser) return;
await withLoading(`saveUserApis_${selectedUser.username}`, async () => {
try { try {
const res = await fetch('/api/admin/user', { const res = await fetch('/api/admin/user', {
method: 'POST', method: 'POST',
@@ -667,7 +711,9 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
setSelectedApis([]); setSelectedApis([]);
} catch (err) { } catch (err) {
showError(err instanceof Error ? err.message : '操作失败', showAlert); showError(err instanceof Error ? err.message : '操作失败', showAlert);
throw err;
} }
});
}; };
// 通用请求函数 // 通用请求函数
@@ -711,6 +757,7 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
const handleConfirmDeleteUser = async () => { const handleConfirmDeleteUser = async () => {
if (!deletingUser) return; if (!deletingUser) return;
await withLoading(`deleteUser_${deletingUser}`, async () => {
try { try {
await handleUserAction('deleteUser', deletingUser); await handleUserAction('deleteUser', deletingUser);
setShowDeleteUserModal(false); setShowDeleteUserModal(false);
@@ -718,6 +765,7 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
} catch (err) { } catch (err) {
// 错误处理已在 handleUserAction 中处理 // 错误处理已在 handleUserAction 中处理
} }
});
}; };
if (!config) { if (!config) {
@@ -801,7 +849,8 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
<td className='px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2'> <td className='px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2'>
<button <button
onClick={() => handleStartEditUserGroup(group)} onClick={() => handleStartEditUserGroup(group)}
className={buttonStyles.roundedPrimary} disabled={isLoading(`userGroup_edit_${group.name}`)}
className={`${buttonStyles.roundedPrimary} ${isLoading(`userGroup_edit_${group.name}`) ? 'opacity-50 cursor-not-allowed' : ''}`}
> >
</button> </button>
@@ -911,10 +960,10 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
<div className='flex justify-end'> <div className='flex justify-end'>
<button <button
onClick={handleAddUser} onClick={handleAddUser}
disabled={!newUser.username || !newUser.password} disabled={!newUser.username || !newUser.password || isLoading('addUser')}
className={!newUser.username || !newUser.password ? buttonStyles.disabled : buttonStyles.success} className={!newUser.username || !newUser.password || isLoading('addUser') ? buttonStyles.disabled : buttonStyles.success}
> >
{isLoading('addUser') ? '添加中...' : '添加'}
</button> </button>
</div> </div>
</div> </div>
@@ -949,10 +998,10 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
/> />
<button <button
onClick={handleChangePassword} onClick={handleChangePassword}
disabled={!changePasswordUser.password} disabled={!changePasswordUser.password || isLoading(`changePassword_${changePasswordUser.username}`)}
className={`w-full sm:w-auto ${!changePasswordUser.password ? buttonStyles.disabled : buttonStyles.primary}`} className={`w-full sm:w-auto ${!changePasswordUser.password || isLoading(`changePassword_${changePasswordUser.username}`) ? buttonStyles.disabled : buttonStyles.primary}`}
> >
{isLoading(`changePassword_${changePasswordUser.username}`) ? '修改中...' : '修改密码'}
</button> </button>
<button <button
onClick={() => { onClick={() => {
@@ -1177,7 +1226,8 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
{user.role === 'user' && ( {user.role === 'user' && (
<button <button
onClick={() => handleSetAdmin(user.username)} onClick={() => handleSetAdmin(user.username)}
className={buttonStyles.roundedPurple} disabled={isLoading(`setAdmin_${user.username}`)}
className={`${buttonStyles.roundedPurple} ${isLoading(`setAdmin_${user.username}`) ? 'opacity-50 cursor-not-allowed' : ''}`}
> >
</button> </button>
@@ -1187,7 +1237,8 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
onClick={() => onClick={() =>
handleRemoveAdmin(user.username) handleRemoveAdmin(user.username)
} }
className={buttonStyles.roundedSecondary} disabled={isLoading(`removeAdmin_${user.username}`)}
className={`${buttonStyles.roundedSecondary} ${isLoading(`removeAdmin_${user.username}`) ? 'opacity-50 cursor-not-allowed' : ''}`}
> >
</button> </button>
@@ -1196,7 +1247,8 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
(!user.banned ? ( (!user.banned ? (
<button <button
onClick={() => handleBanUser(user.username)} onClick={() => handleBanUser(user.username)}
className={buttonStyles.roundedDanger} disabled={isLoading(`banUser_${user.username}`)}
className={`${buttonStyles.roundedDanger} ${isLoading(`banUser_${user.username}`) ? 'opacity-50 cursor-not-allowed' : ''}`}
> >
</button> </button>
@@ -1205,7 +1257,8 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
onClick={() => onClick={() =>
handleUnbanUser(user.username) handleUnbanUser(user.username)
} }
className={buttonStyles.roundedSuccess} disabled={isLoading(`unbanUser_${user.username}`)}
className={`${buttonStyles.roundedSuccess} ${isLoading(`unbanUser_${user.username}`) ? 'opacity-50 cursor-not-allowed' : ''}`}
> >
</button> </button>
@@ -1350,9 +1403,10 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
</button> </button>
<button <button
onClick={handleSaveUserApis} onClick={handleSaveUserApis}
className={`px-6 py-2.5 text-sm font-medium ${buttonStyles.primary}`} 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> </button>
</div> </div>
</div> </div>
@@ -1476,10 +1530,10 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
</button> </button>
<button <button
onClick={handleAddUserGroup} onClick={handleAddUserGroup}
disabled={!newUserGroup.name.trim()} disabled={!newUserGroup.name.trim() || isLoading('userGroup_add_new')}
className={`px-6 py-2.5 text-sm font-medium ${!newUserGroup.name.trim() ? buttonStyles.disabled : buttonStyles.primary}`} 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> </button>
</div> </div>
</div> </div>
@@ -1588,9 +1642,10 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
</button> </button>
<button <button
onClick={handleEditUserGroup} onClick={handleEditUserGroup}
className={`px-6 py-2.5 text-sm font-medium ${buttonStyles.primary}`} 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> </button>
</div> </div>
</div> </div>
@@ -1684,9 +1739,10 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
</button> </button>
<button <button
onClick={handleSaveUserGroups} onClick={handleSaveUserGroups}
className={`px-6 py-2.5 text-sm font-medium ${buttonStyles.primary}`} 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> </button>
</div> </div>
</div> </div>
@@ -1783,9 +1839,10 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
</button> </button>
<button <button
onClick={handleConfirmDeleteUserGroup} onClick={handleConfirmDeleteUserGroup}
className={`px-6 py-2.5 text-sm font-medium ${buttonStyles.danger}`} 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> </button>
</div> </div>
</div> </div>
@@ -1934,9 +1991,10 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
</button> </button>
<button <button
onClick={() => handleBatchSetUserGroup(selectedUserGroup)} onClick={() => handleBatchSetUserGroup(selectedUserGroup)}
className={`px-6 py-2.5 text-sm font-medium ${buttonStyles.primary}`} disabled={isLoading('batchSetUserGroup')}
className={`px-6 py-2.5 text-sm font-medium ${isLoading('batchSetUserGroup') ? buttonStyles.disabled : buttonStyles.primary}`}
> >
{isLoading('batchSetUserGroup') ? '设置中...' : '确认设置'}
</button> </button>
</div> </div>
</div> </div>
@@ -1970,6 +2028,7 @@ const VideoSourceConfig = ({
refreshConfig: () => Promise<void>; refreshConfig: () => Promise<void>;
}) => { }) => {
const { alertModal, showAlert, hideAlert } = useAlertModal(); const { alertModal, showAlert, hideAlert } = useAlertModal();
const { isLoading, withLoading } = useLoadingState();
const [sources, setSources] = useState<DataSource[]>([]); const [sources, setSources] = useState<DataSource[]>([]);
const [showAddForm, setShowAddForm] = useState(false); const [showAddForm, setShowAddForm] = useState(false);
const [orderChanged, setOrderChanged] = useState(false); const [orderChanged, setOrderChanged] = useState(false);
@@ -2069,27 +2128,27 @@ const VideoSourceConfig = ({
const target = sources.find((s) => s.key === key); const target = sources.find((s) => s.key === key);
if (!target) return; if (!target) return;
const action = target.disabled ? 'enable' : 'disable'; const action = target.disabled ? 'enable' : 'disable';
callSourceApi({ action, key }).catch(() => { withLoading(`toggleSource_${key}`, () => callSourceApi({ action, key })).catch(() => {
console.error('操作失败', action, key); console.error('操作失败', action, key);
}); });
}; };
const handleDelete = (key: string) => { const handleDelete = (key: string) => {
callSourceApi({ action: 'delete', key }).catch(() => { withLoading(`deleteSource_${key}`, () => callSourceApi({ action: 'delete', key })).catch(() => {
console.error('操作失败', 'delete', key); console.error('操作失败', 'delete', key);
}); });
}; };
const handleAddSource = () => { const handleAddSource = () => {
if (!newSource.name || !newSource.key || !newSource.api) return; if (!newSource.name || !newSource.key || !newSource.api) return;
callSourceApi({ withLoading('addSource', async () => {
await callSourceApi({
action: 'add', action: 'add',
key: newSource.key, key: newSource.key,
name: newSource.name, name: newSource.name,
api: newSource.api, api: newSource.api,
detail: newSource.detail, detail: newSource.detail,
}) });
.then(() => {
setNewSource({ setNewSource({
name: '', name: '',
key: '', key: '',
@@ -2099,8 +2158,7 @@ const VideoSourceConfig = ({
from: 'custom', from: 'custom',
}); });
setShowAddForm(false); setShowAddForm(false);
}) }).catch(() => {
.catch(() => {
console.error('操作失败', 'add', newSource); console.error('操作失败', 'add', newSource);
}); });
}; };
@@ -2116,7 +2174,7 @@ const VideoSourceConfig = ({
const handleSaveOrder = () => { const handleSaveOrder = () => {
const order = sources.map((s) => s.key); const order = sources.map((s) => s.key);
callSourceApi({ action: 'sort', order }) withLoading('saveSourceOrder', () => callSourceApi({ action: 'sort', order }))
.then(() => { .then(() => {
setOrderChanged(false); setOrderChanged(false);
}) })
@@ -2132,6 +2190,7 @@ const VideoSourceConfig = ({
return; return;
} }
await withLoading('validateSources', async () => {
setIsValidating(true); setIsValidating(true);
setValidationResults([]); // 清空之前的结果 setValidationResults([]); // 清空之前的结果
setShowValidationModal(false); // 立即关闭弹窗 setShowValidationModal(false); // 立即关闭弹窗
@@ -2216,7 +2275,9 @@ const VideoSourceConfig = ({
} catch (error) { } catch (error) {
setIsValidating(false); setIsValidating(false);
showAlert({ type: 'error', title: '验证失败', message: error instanceof Error ? error.message : '未知错误' }); showAlert({ type: 'error', title: '验证失败', message: error instanceof Error ? error.message : '未知错误' });
throw error;
} }
});
}; };
// 获取有效性状态显示 // 获取有效性状态显示
@@ -2338,17 +2399,19 @@ const VideoSourceConfig = ({
<td className='px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2'> <td className='px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2'>
<button <button
onClick={() => handleToggleEnable(source.key)} 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 className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${!source.disabled
? buttonStyles.roundedDanger ? buttonStyles.roundedDanger
: buttonStyles.roundedSuccess : buttonStyles.roundedSuccess
} transition-colors`} } transition-colors ${isLoading(`toggleSource_${source.key}`) ? 'opacity-50 cursor-not-allowed' : ''}`}
> >
{!source.disabled ? '禁用' : '启用'} {!source.disabled ? '禁用' : '启用'}
</button> </button>
{source.from !== 'config' && ( {source.from !== 'config' && (
<button <button
onClick={() => handleDelete(source.key)} onClick={() => handleDelete(source.key)}
className={buttonStyles.roundedSecondary} disabled={isLoading(`deleteSource_${source.key}`)}
className={`${buttonStyles.roundedSecondary} ${isLoading(`deleteSource_${source.key}`) ? 'opacity-50 cursor-not-allowed' : ''}`}
> >
</button> </button>
@@ -2414,7 +2477,7 @@ const VideoSourceConfig = ({
message: confirmMessage, message: confirmMessage,
onConfirm: async () => { onConfirm: async () => {
try { try {
await callSourceApi({ action, keys }); await withLoading(`batchSource_${action}`, () => callSourceApi({ action, keys }));
showAlert({ type: 'success', title: `${actionName}成功`, message: `${actionName}${keys.length} 个视频源`, timer: 2000 }); showAlert({ type: 'success', title: `${actionName}成功`, message: `${actionName}${keys.length} 个视频源`, timer: 2000 });
// 重置选择状态 // 重置选择状态
setSelectedSources(new Set()); setSelectedSources(new Set());
@@ -2454,21 +2517,24 @@ const VideoSourceConfig = ({
</span> </span>
<button <button
onClick={() => handleBatchOperation('batch_enable')} onClick={() => handleBatchOperation('batch_enable')}
className={`px-3 py-1 text-sm ${buttonStyles.success}`} disabled={isLoading('batchSource_batch_enable')}
className={`px-3 py-1 text-sm ${isLoading('batchSource_batch_enable') ? buttonStyles.disabled : buttonStyles.success}`}
> >
{isLoading('batchSource_batch_enable') ? '启用中...' : '批量启用'}
</button> </button>
<button <button
onClick={() => handleBatchOperation('batch_disable')} onClick={() => handleBatchOperation('batch_disable')}
className={`px-3 py-1 text-sm ${buttonStyles.warning}`} disabled={isLoading('batchSource_batch_disable')}
className={`px-3 py-1 text-sm ${isLoading('batchSource_batch_disable') ? buttonStyles.disabled : buttonStyles.warning}`}
> >
{isLoading('batchSource_batch_disable') ? '禁用中...' : '批量禁用'}
</button> </button>
<button <button
onClick={() => handleBatchOperation('batch_delete')} onClick={() => handleBatchOperation('batch_delete')}
className={`px-3 py-1 text-sm ${buttonStyles.danger}`} disabled={isLoading('batchSource_batch_delete')}
className={`px-3 py-1 text-sm ${isLoading('batchSource_batch_delete') ? buttonStyles.disabled : buttonStyles.danger}`}
> >
{isLoading('batchSource_batch_delete') ? '删除中...' : '批量删除'}
</button> </button>
</div> </div>
<div className='w-px h-6 bg-gray-300 dark:bg-gray-600'></div> <div className='w-px h-6 bg-gray-300 dark:bg-gray-600'></div>
@@ -2543,10 +2609,10 @@ const VideoSourceConfig = ({
<div className='flex justify-end'> <div className='flex justify-end'>
<button <button
onClick={handleAddSource} onClick={handleAddSource}
disabled={!newSource.name || !newSource.key || !newSource.api} 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 ? buttonStyles.disabled : buttonStyles.success}`} 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> </button>
</div> </div>
</div> </div>
@@ -2617,9 +2683,10 @@ const VideoSourceConfig = ({
<div className='flex justify-end'> <div className='flex justify-end'>
<button <button
onClick={handleSaveOrder} onClick={handleSaveOrder}
className={`px-3 py-1.5 text-sm ${buttonStyles.primary}`} disabled={isLoading('saveSourceOrder')}
className={`px-3 py-1.5 text-sm ${isLoading('saveSourceOrder') ? buttonStyles.disabled : buttonStyles.primary}`}
> >
{isLoading('saveSourceOrder') ? '保存中...' : '保存排序'}
</button> </button>
</div> </div>
)} )}
@@ -2652,10 +2719,10 @@ const VideoSourceConfig = ({
</button> </button>
<button <button
onClick={handleValidateSources} onClick={handleValidateSources}
disabled={isValidating || !searchKeyword.trim()} disabled={!searchKeyword.trim()}
className={`px-4 py-2 ${isValidating || !searchKeyword.trim() ? buttonStyles.disabled : buttonStyles.primary}`} className={`px-4 py-2 ${!searchKeyword.trim() ? buttonStyles.disabled : buttonStyles.primary}`}
> >
{isValidating ? `检测中... (${validationResults.length}/${sources.length})` : '开始检测'}
</button> </button>
</div> </div>
</div> </div>
@@ -2710,9 +2777,10 @@ const VideoSourceConfig = ({
</button> </button>
<button <button
onClick={confirmModal.onConfirm} onClick={confirmModal.onConfirm}
className={`px-4 py-2 text-sm font-medium ${buttonStyles.primary}`} 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> </button>
</div> </div>
</div> </div>
@@ -2733,6 +2801,7 @@ const CategoryConfig = ({
refreshConfig: () => Promise<void>; refreshConfig: () => Promise<void>;
}) => { }) => {
const { alertModal, showAlert, hideAlert } = useAlertModal(); const { alertModal, showAlert, hideAlert } = useAlertModal();
const { isLoading, withLoading } = useLoadingState();
const [categories, setCategories] = useState<CustomCategory[]>([]); const [categories, setCategories] = useState<CustomCategory[]>([]);
const [showAddForm, setShowAddForm] = useState(false); const [showAddForm, setShowAddForm] = useState(false);
const [orderChanged, setOrderChanged] = useState(false); const [orderChanged, setOrderChanged] = useState(false);
@@ -2794,26 +2863,26 @@ const CategoryConfig = ({
const target = categories.find((c) => c.query === query && c.type === type); const target = categories.find((c) => c.query === query && c.type === type);
if (!target) return; if (!target) return;
const action = target.disabled ? 'enable' : 'disable'; const action = target.disabled ? 'enable' : 'disable';
callCategoryApi({ action, query, type }).catch(() => { withLoading(`toggleCategory_${query}_${type}`, () => callCategoryApi({ action, query, type })).catch(() => {
console.error('操作失败', action, query, type); console.error('操作失败', action, query, type);
}); });
}; };
const handleDelete = (query: string, type: 'movie' | 'tv') => { const handleDelete = (query: string, type: 'movie' | 'tv') => {
callCategoryApi({ action: 'delete', query, type }).catch(() => { withLoading(`deleteCategory_${query}_${type}`, () => callCategoryApi({ action: 'delete', query, type })).catch(() => {
console.error('操作失败', 'delete', query, type); console.error('操作失败', 'delete', query, type);
}); });
}; };
const handleAddCategory = () => { const handleAddCategory = () => {
if (!newCategory.name || !newCategory.query) return; if (!newCategory.name || !newCategory.query) return;
callCategoryApi({ withLoading('addCategory', async () => {
await callCategoryApi({
action: 'add', action: 'add',
name: newCategory.name, name: newCategory.name,
type: newCategory.type, type: newCategory.type,
query: newCategory.query, query: newCategory.query,
}) });
.then(() => {
setNewCategory({ setNewCategory({
name: '', name: '',
type: 'movie', type: 'movie',
@@ -2822,8 +2891,7 @@ const CategoryConfig = ({
from: 'custom', from: 'custom',
}); });
setShowAddForm(false); setShowAddForm(false);
}) }).catch(() => {
.catch(() => {
console.error('操作失败', 'add', newCategory); console.error('操作失败', 'add', newCategory);
}); });
}; };
@@ -2843,7 +2911,7 @@ const CategoryConfig = ({
const handleSaveOrder = () => { const handleSaveOrder = () => {
const order = categories.map((c) => `${c.query}:${c.type}`); const order = categories.map((c) => `${c.query}:${c.type}`);
callCategoryApi({ action: 'sort', order }) withLoading('saveCategoryOrder', () => callCategoryApi({ action: 'sort', order }))
.then(() => { .then(() => {
setOrderChanged(false); setOrderChanged(false);
}) })
@@ -2909,17 +2977,19 @@ const CategoryConfig = ({
onClick={() => onClick={() =>
handleToggleEnable(category.query, category.type) 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 className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${!category.disabled
? buttonStyles.roundedDanger ? buttonStyles.roundedDanger
: buttonStyles.roundedSuccess : buttonStyles.roundedSuccess
} transition-colors`} } transition-colors ${isLoading(`toggleCategory_${category.query}_${category.type}`) ? 'opacity-50 cursor-not-allowed' : ''}`}
> >
{!category.disabled ? '禁用' : '启用'} {!category.disabled ? '禁用' : '启用'}
</button> </button>
{category.from !== 'config' && ( {category.from !== 'config' && (
<button <button
onClick={() => handleDelete(category.query, category.type)} onClick={() => handleDelete(category.query, category.type)}
className={buttonStyles.roundedSecondary} disabled={isLoading(`deleteCategory_${category.query}_${category.type}`)}
className={`${buttonStyles.roundedSecondary} ${isLoading(`deleteCategory_${category.query}_${category.type}`) ? 'opacity-50 cursor-not-allowed' : ''}`}
> >
</button> </button>
@@ -2990,10 +3060,10 @@ const CategoryConfig = ({
<div className='flex justify-end'> <div className='flex justify-end'>
<button <button
onClick={handleAddCategory} onClick={handleAddCategory}
disabled={!newCategory.name || !newCategory.query} disabled={!newCategory.name || !newCategory.query || isLoading('addCategory')}
className={`w-full sm:w-auto px-4 py-2 ${!newCategory.name || !newCategory.query ? buttonStyles.disabled : buttonStyles.success}`} className={`w-full sm:w-auto px-4 py-2 ${!newCategory.name || !newCategory.query || isLoading('addCategory') ? buttonStyles.disabled : buttonStyles.success}`}
> >
{isLoading('addCategory') ? '添加中...' : '添加'}
</button> </button>
</div> </div>
</div> </div>
@@ -3051,9 +3121,10 @@ const CategoryConfig = ({
<div className='flex justify-end'> <div className='flex justify-end'>
<button <button
onClick={handleSaveOrder} onClick={handleSaveOrder}
className={`px-3 py-1.5 text-sm ${buttonStyles.primary}`} disabled={isLoading('saveCategoryOrder')}
className={`px-3 py-1.5 text-sm ${isLoading('saveCategoryOrder') ? buttonStyles.disabled : buttonStyles.primary}`}
> >
{isLoading('saveCategoryOrder') ? '保存中...' : '保存排序'}
</button> </button>
</div> </div>
)} )}
@@ -3075,6 +3146,7 @@ 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 { alertModal, showAlert, hideAlert } = useAlertModal();
const { isLoading, withLoading } = useLoadingState();
const [configContent, setConfigContent] = useState(''); const [configContent, setConfigContent] = useState('');
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [subscriptionUrl, setSubscriptionUrl] = useState(''); const [subscriptionUrl, setSubscriptionUrl] = useState('');
@@ -3104,8 +3176,8 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
return; return;
} }
await withLoading('fetchConfig', async () => {
try { try {
setFetching(true);
const resp = await fetch('/api/admin/config_subscription/fetch', { const resp = await fetch('/api/admin/config_subscription/fetch', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -3129,15 +3201,15 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
} }
} catch (err) { } catch (err) {
showError(err instanceof Error ? err.message : '拉取失败', showAlert); showError(err instanceof Error ? err.message : '拉取失败', showAlert);
} finally { throw err;
setFetching(false);
} }
});
}; };
// 保存配置文件 // 保存配置文件
const handleSave = async () => { const handleSave = async () => {
await withLoading('saveConfig', async () => {
try { try {
setSaving(true);
const resp = await fetch('/api/admin/config_file', { const resp = await fetch('/api/admin/config_file', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -3158,9 +3230,9 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
await refreshConfig(); await refreshConfig();
} catch (err) { } catch (err) {
showError(err instanceof Error ? err.message : '保存失败', showAlert); showError(err instanceof Error ? err.message : '保存失败', showAlert);
} finally { throw err;
setSaving(false);
} }
});
}; };
@@ -3209,13 +3281,13 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
<div className='pt-2'> <div className='pt-2'>
<button <button
onClick={handleFetchConfig} onClick={handleFetchConfig}
disabled={fetching || !subscriptionUrl.trim()} disabled={isLoading('fetchConfig') || !subscriptionUrl.trim()}
className={`w-full px-6 py-3 rounded-lg font-medium transition-all duration-200 ${fetching || !subscriptionUrl.trim() className={`w-full px-6 py-3 rounded-lg font-medium transition-all duration-200 ${isLoading('fetchConfig') || !subscriptionUrl.trim()
? buttonStyles.disabled ? buttonStyles.disabled
: buttonStyles.success : buttonStyles.success
}`} }`}
> >
{fetching ? ( {isLoading('fetchConfig') ? (
<div className='flex items-center justify-center gap-2'> <div className='flex items-center justify-center gap-2'>
<div className='w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin'></div> <div className='w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin'></div>
@@ -3280,13 +3352,13 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
</div> </div>
<button <button
onClick={handleSave} onClick={handleSave}
disabled={saving} disabled={isLoading('saveConfig')}
className={`px-4 py-2 rounded-lg transition-colors ${saving className={`px-4 py-2 rounded-lg transition-colors ${isLoading('saveConfig')
? buttonStyles.disabled ? buttonStyles.disabled
: buttonStyles.success : buttonStyles.success
}`} }`}
> >
{saving ? '保存中…' : '保存'} {isLoading('saveConfig') ? '保存中…' : '保存'}
</button> </button>
</div> </div>
</div> </div>
@@ -3308,6 +3380,7 @@ 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 { alertModal, showAlert, hideAlert } = useAlertModal();
const { isLoading, withLoading } = useLoadingState();
const [siteSettings, setSiteSettings] = useState<SiteConfig>({ const [siteSettings, setSiteSettings] = useState<SiteConfig>({
SiteName: '', SiteName: '',
Announcement: '', Announcement: '',
@@ -3447,8 +3520,8 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
// 保存站点配置 // 保存站点配置
const handleSave = async () => { const handleSave = async () => {
await withLoading('saveSiteConfig', async () => {
try { try {
setSaving(true);
const resp = await fetch('/api/admin/site', { const resp = await fetch('/api/admin/site', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -3464,9 +3537,9 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
await refreshConfig(); await refreshConfig();
} catch (err) { } catch (err) {
showError(err instanceof Error ? err.message : '保存失败', showAlert); showError(err instanceof Error ? err.message : '保存失败', showAlert);
} finally { throw err;
setSaving(false);
} }
});
}; };
if (!config) { if (!config) {
@@ -3847,13 +3920,13 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig |
<div className='flex justify-end'> <div className='flex justify-end'>
<button <button
onClick={handleSave} onClick={handleSave}
disabled={saving} disabled={isLoading('saveSiteConfig')}
className={`px-4 py-2 ${saving className={`px-4 py-2 ${isLoading('saveSiteConfig')
? buttonStyles.disabled ? buttonStyles.disabled
: buttonStyles.success : buttonStyles.success
} rounded-lg transition-colors`} } rounded-lg transition-colors`}
> >
{saving ? '保存中…' : '保存'} {isLoading('saveSiteConfig') ? '保存中…' : '保存'}
</button> </button>
</div> </div>
@@ -3880,6 +3953,7 @@ const LiveSourceConfig = ({
refreshConfig: () => Promise<void>; refreshConfig: () => Promise<void>;
}) => { }) => {
const { alertModal, showAlert, hideAlert } = useAlertModal(); const { alertModal, showAlert, hideAlert } = useAlertModal();
const { isLoading, withLoading } = useLoadingState();
const [liveSources, setLiveSources] = useState<LiveDataSource[]>([]); const [liveSources, setLiveSources] = useState<LiveDataSource[]>([]);
const [showAddForm, setShowAddForm] = useState(false); const [showAddForm, setShowAddForm] = useState(false);
const [orderChanged, setOrderChanged] = useState(false); const [orderChanged, setOrderChanged] = useState(false);
@@ -3944,13 +4018,13 @@ const LiveSourceConfig = ({
const target = liveSources.find((s) => s.key === key); const target = liveSources.find((s) => s.key === key);
if (!target) return; if (!target) return;
const action = target.disabled ? 'enable' : 'disable'; const action = target.disabled ? 'enable' : 'disable';
callLiveSourceApi({ action, key }).catch(() => { withLoading(`toggleLiveSource_${key}`, () => callLiveSourceApi({ action, key })).catch(() => {
console.error('操作失败', action, key); console.error('操作失败', action, key);
}); });
}; };
const handleDelete = (key: string) => { const handleDelete = (key: string) => {
callLiveSourceApi({ action: 'delete', key }).catch(() => { withLoading(`deleteLiveSource_${key}`, () => callLiveSourceApi({ action: 'delete', key })).catch(() => {
console.error('操作失败', 'delete', key); console.error('操作失败', 'delete', key);
}); });
}; };
@@ -3959,6 +4033,7 @@ const LiveSourceConfig = ({
const handleRefreshLiveSources = async () => { const handleRefreshLiveSources = async () => {
if (isRefreshing) return; if (isRefreshing) return;
await withLoading('refreshLiveSources', async () => {
setIsRefreshing(true); setIsRefreshing(true);
try { try {
const response = await fetch('/api/admin/live/refresh', { const response = await fetch('/api/admin/live/refresh', {
@@ -3976,22 +4051,24 @@ const LiveSourceConfig = ({
showAlert({ type: 'success', title: '刷新成功', message: '直播源已刷新', timer: 2000 }); showAlert({ type: 'success', title: '刷新成功', message: '直播源已刷新', timer: 2000 });
} catch (err) { } catch (err) {
showError(err instanceof Error ? err.message : '刷新失败', showAlert); showError(err instanceof Error ? err.message : '刷新失败', showAlert);
throw err;
} finally { } finally {
setIsRefreshing(false); setIsRefreshing(false);
} }
});
}; };
const handleAddLiveSource = () => { const handleAddLiveSource = () => {
if (!newLiveSource.name || !newLiveSource.key || !newLiveSource.url) return; if (!newLiveSource.name || !newLiveSource.key || !newLiveSource.url) return;
callLiveSourceApi({ withLoading('addLiveSource', async () => {
await callLiveSourceApi({
action: 'add', action: 'add',
key: newLiveSource.key, key: newLiveSource.key,
name: newLiveSource.name, name: newLiveSource.name,
url: newLiveSource.url, url: newLiveSource.url,
ua: newLiveSource.ua, ua: newLiveSource.ua,
epg: newLiveSource.epg, epg: newLiveSource.epg,
}) });
.then(() => {
setNewLiveSource({ setNewLiveSource({
name: '', name: '',
key: '', key: '',
@@ -4002,8 +4079,7 @@ const LiveSourceConfig = ({
from: 'custom', from: 'custom',
}); });
setShowAddForm(false); setShowAddForm(false);
}) }).catch(() => {
.catch(() => {
console.error('操作失败', 'add', newLiveSource); console.error('操作失败', 'add', newLiveSource);
}); });
}; };
@@ -4019,7 +4095,7 @@ const LiveSourceConfig = ({
const handleSaveOrder = () => { const handleSaveOrder = () => {
const order = liveSources.map((s) => s.key); const order = liveSources.map((s) => s.key);
callLiveSourceApi({ action: 'sort', order }) withLoading('saveLiveSourceOrder', () => callLiveSourceApi({ action: 'sort', order }))
.then(() => { .then(() => {
setOrderChanged(false); setOrderChanged(false);
}) })
@@ -4092,17 +4168,19 @@ const LiveSourceConfig = ({
<td className='px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2'> <td className='px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2'>
<button <button
onClick={() => handleToggleEnable(liveSource.key)} 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 className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${!liveSource.disabled
? buttonStyles.roundedDanger ? buttonStyles.roundedDanger
: buttonStyles.roundedSuccess : buttonStyles.roundedSuccess
} transition-colors`} } transition-colors ${isLoading(`toggleLiveSource_${liveSource.key}`) ? 'opacity-50 cursor-not-allowed' : ''}`}
> >
{!liveSource.disabled ? '禁用' : '启用'} {!liveSource.disabled ? '禁用' : '启用'}
</button> </button>
{liveSource.from !== 'config' && ( {liveSource.from !== 'config' && (
<button <button
onClick={() => handleDelete(liveSource.key)} onClick={() => handleDelete(liveSource.key)}
className={buttonStyles.roundedSecondary} disabled={isLoading(`deleteLiveSource_${liveSource.key}`)}
className={`${buttonStyles.roundedSecondary} ${isLoading(`deleteLiveSource_${liveSource.key}`) ? 'opacity-50 cursor-not-allowed' : ''}`}
> >
</button> </button>
@@ -4130,13 +4208,13 @@ const LiveSourceConfig = ({
<div className='flex items-center space-x-2'> <div className='flex items-center space-x-2'>
<button <button
onClick={handleRefreshLiveSources} onClick={handleRefreshLiveSources}
disabled={isRefreshing} disabled={isRefreshing || isLoading('refreshLiveSources')}
className={`px-3 py-1.5 text-sm font-medium flex items-center space-x-2 ${isRefreshing 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-gray-400 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' : 'bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded-lg transition-colors'
}`} }`}
> >
<span>{isRefreshing ? '刷新中...' : '刷新直播源'}</span> <span>{isRefreshing || isLoading('refreshLiveSources') ? '刷新中...' : '刷新直播源'}</span>
</button> </button>
<button <button
onClick={() => setShowAddForm(!showAddForm)} onClick={() => setShowAddForm(!showAddForm)}
@@ -4200,10 +4278,10 @@ const LiveSourceConfig = ({
<div className='flex justify-end'> <div className='flex justify-end'>
<button <button
onClick={handleAddLiveSource} onClick={handleAddLiveSource}
disabled={!newLiveSource.name || !newLiveSource.key || !newLiveSource.url} 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 ? buttonStyles.disabled : buttonStyles.success}`} 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> </button>
</div> </div>
</div> </div>
@@ -4267,9 +4345,10 @@ const LiveSourceConfig = ({
<div className='flex justify-end'> <div className='flex justify-end'>
<button <button
onClick={handleSaveOrder} onClick={handleSaveOrder}
className={`px-3 py-1.5 text-sm ${buttonStyles.primary}`} disabled={isLoading('saveLiveSourceOrder')}
className={`px-3 py-1.5 text-sm ${isLoading('saveLiveSourceOrder') ? buttonStyles.disabled : buttonStyles.primary}`}
> >
{isLoading('saveLiveSourceOrder') ? '保存中...' : '保存排序'}
</button> </button>
</div> </div>
)} )}
@@ -4292,6 +4371,7 @@ const LiveSourceConfig = ({
function AdminPageClient() { function AdminPageClient() {
const { alertModal, showAlert, hideAlert } = useAlertModal(); const { alertModal, showAlert, hideAlert } = useAlertModal();
const { isLoading, withLoading } = useLoadingState();
const [config, setConfig] = useState<AdminConfig | null>(null); const [config, setConfig] = useState<AdminConfig | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -4355,6 +4435,7 @@ function AdminPageClient() {
}; };
const handleConfirmResetConfig = async () => { const handleConfirmResetConfig = async () => {
await withLoading('resetConfig', async () => {
try { try {
const response = await fetch(`/api/admin/reset`); const response = await fetch(`/api/admin/reset`);
if (!response.ok) { if (!response.ok) {
@@ -4365,7 +4446,9 @@ function AdminPageClient() {
setShowResetConfigModal(false); setShowResetConfigModal(false);
} catch (err) { } catch (err) {
showError(err instanceof Error ? err.message : '重置失败', showAlert); showError(err instanceof Error ? err.message : '重置失败', showAlert);
throw err;
} }
});
}; };
if (loading) { if (loading) {
@@ -4578,9 +4661,10 @@ function AdminPageClient() {
</button> </button>
<button <button
onClick={handleConfirmResetConfig} onClick={handleConfirmResetConfig}
className={`px-6 py-2.5 text-sm font-medium ${buttonStyles.danger}`} disabled={isLoading('resetConfig')}
className={`px-6 py-2.5 text-sm font-medium ${isLoading('resetConfig') ? buttonStyles.disabled : buttonStyles.danger}`}
> >
{isLoading('resetConfig') ? '重置中...' : '确认重置'}
</button> </button>
</div> </div>
</div> </div>