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