diff --git a/package.json b/package.json index d19a4cc..c93ad26 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "react-dom": "^18.2.0", "react-icons": "^5.4.0", "redis": "^4.6.7", - "sweetalert2": "^11.11.0", "swiper": "^11.2.8", "tailwind-merge": "^2.6.0", "vidstack": "^0.6.15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd4c437..846452f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,9 +83,6 @@ importers: redis: specifier: ^4.6.7 version: 4.7.1 - sweetalert2: - specifier: ^11.11.0 - version: 11.22.2 swiper: specifier: ^11.2.8 version: 11.2.8 @@ -4639,9 +4636,6 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - sweetalert2@11.22.2: - resolution: {integrity: sha512-GFQGzw8ZXF23PO79WMAYXLl4zYmLiaKqYJwcp5eBF07wiI5BYPbZtKi2pcvVmfUQK+FqL1risJAMxugcPbGIyg==} - swiper@11.2.8: resolution: {integrity: sha512-S5FVf6zWynPWooi7pJ7lZhSUe2snTzqLuUzbd5h5PHUOhzgvW0bLKBd2wv0ixn6/5o9vwc/IkQT74CRcLJQzeg==} engines: {node: '>= 4.7.0'} @@ -10404,8 +10398,6 @@ snapshots: csso: 5.0.5 picocolors: 1.1.1 - sweetalert2@11.22.2: {} - swiper@11.2.8: {} symbol-tree@3.2.4: {} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 2d13d95..e7be48a 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-explicit-any, no-console, @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/no-explicit-any, no-console, @typescript-eslint/no-non-null-assertion,react-hooks/exhaustive-deps */ 'use client'; @@ -22,7 +22,10 @@ import { } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { + AlertCircle, + AlertTriangle, Check, + CheckCircle, ChevronDown, ChevronUp, Database, @@ -36,7 +39,6 @@ import { import { GripVertical } from 'lucide-react'; import { Suspense, useCallback, useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; -import Swal from 'sweetalert2'; import { AdminConfig, AdminConfigResult } from '@/lib/admin.types'; import { getAuthInfoFromBrowserCookie } from '@/lib/auth'; @@ -44,19 +46,145 @@ import { getAuthInfoFromBrowserCookie } from '@/lib/auth'; import DataMigration from '@/components/DataMigration'; import PageLayout from '@/components/PageLayout'; -// 统一弹窗方法(必须在首次使用前定义) -const showError = (message: string) => - Swal.fire({ icon: 'error', title: '错误', text: message }); +// 通用弹窗组件 +interface AlertModalProps { + isOpen: boolean; + onClose: () => void; + type: 'success' | 'error' | 'warning'; + title: string; + message?: string; + timer?: number; + showConfirm?: boolean; +} -const showSuccess = (message: string) => - Swal.fire({ - icon: 'success', - title: '成功', - text: message, - timer: 2000, - showConfirmButton: false, +const AlertModal = ({ + isOpen, + onClose, + type, + title, + message, + timer, + showConfirm = false +}: AlertModalProps) => { + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + if (isOpen) { + setIsVisible(true); + if (timer) { + setTimeout(() => { + onClose(); + }, timer); + } + } else { + setIsVisible(false); + } + }, [isOpen, timer, onClose]); + + if (!isOpen) return null; + + const getIcon = () => { + switch (type) { + case 'success': + return ; + case 'error': + return ; + case 'warning': + return ; + default: + return null; + } + }; + + const getBgColor = () => { + switch (type) { + case 'success': + return 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800'; + case 'error': + return 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800'; + case 'warning': + return 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800'; + default: + return 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800'; + } + }; + + return createPortal( +
+
+
+
+ {getIcon()} +
+ +

+ {title} +

+ + {message && ( +

+ {message} +

+ )} + + {showConfirm && ( + + )} +
+
+
, + document.body + ); +}; + +// 弹窗状态管理 +const useAlertModal = () => { + const [alertModal, setAlertModal] = useState<{ + isOpen: boolean; + type: 'success' | 'error' | 'warning'; + title: string; + message?: string; + timer?: number; + showConfirm?: boolean; + }>({ + isOpen: false, + type: 'success', + title: '', }); + const showAlert = (config: Omit) => { + setAlertModal({ ...config, isOpen: true }); + }; + + const hideAlert = () => { + setAlertModal(prev => ({ ...prev, isOpen: false })); + }; + + return { alertModal, showAlert, hideAlert }; +}; + +// 统一弹窗方法(必须在首次使用前定义) +const showError = (message: string, showAlert?: (config: any) => void) => { + if (showAlert) { + showAlert({ type: 'error', title: '错误', message, showConfirm: true }); + } else { + console.error(message); + } +}; + +const showSuccess = (message: string, showAlert?: (config: any) => void) => { + if (showAlert) { + showAlert({ type: 'success', title: '成功', message, timer: 2000 }); + } else { + console.log(message); + } +}; + // 新增站点配置类型 interface SiteConfig { SiteName: string; @@ -136,6 +264,7 @@ interface UserConfigProps { } const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { + const { alertModal, showAlert, hideAlert } = useAlertModal(); const [showAddUserForm, setShowAddUserForm] = useState(false); const [showChangePasswordForm, setShowChangePasswordForm] = useState(false); const [showAddUserGroupForm, setShowAddUserGroupForm] = useState(false); @@ -165,6 +294,20 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { tags?: string[]; } | null>(null); const [selectedApis, setSelectedApis] = useState([]); + const [showConfigureUserGroupModal, setShowConfigureUserGroupModal] = useState(false); + const [selectedUserForGroup, setSelectedUserForGroup] = useState<{ + username: string; + role: 'user' | 'admin' | 'owner'; + tags?: string[]; + } | null>(null); + const [selectedUserGroups, setSelectedUserGroups] = useState([]); + const [showDeleteUserGroupModal, setShowDeleteUserGroupModal] = useState(false); + const [deletingUserGroup, setDeletingUserGroup] = useState<{ + name: string; + affectedUsers: Array<{ username: string; role: 'user' | 'admin' | 'owner' }>; + } | null>(null); + const [showDeleteUserModal, setShowDeleteUserModal] = useState(false); + const [deletingUser, setDeletingUser] = useState(null); // 当前登录用户名 const currentUsername = getAuthInfoFromBrowserCookie()?.username || null; @@ -205,9 +348,9 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { setShowEditUserGroupForm(false); } - showSuccess(action === 'add' ? '用户组添加成功' : action === 'edit' ? '用户组更新成功' : '用户组删除成功'); + showSuccess(action === 'add' ? '用户组添加成功' : action === 'edit' ? '用户组更新成功' : '用户组删除成功', showAlert); } catch (err) { - showError(err instanceof Error ? err.message : '操作失败'); + showError(err instanceof Error ? err.message : '操作失败', showAlert); } }; @@ -221,51 +364,29 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { handleUserGroupAction('edit', editingUserGroup.name, editingUserGroup.enabledApis); }; - const handleDeleteUserGroup = async (groupName: string) => { + const handleDeleteUserGroup = (groupName: string) => { // 计算会受影响的用户数量 const affectedUsers = config?.UserConfig?.Users?.filter(user => user.tags && user.tags.includes(groupName) ) || []; - const affectedCount = affectedUsers.length; - const affectedUserNames = affectedUsers.map(u => u.username).join(', '); - - const { isConfirmed } = await Swal.fire({ - title: '确认删除用户组', - html: ` -
-

删除用户组 ${groupName} 将影响所有使用该组的用户,此操作不可恢复!

- ${affectedCount > 0 ? ` -
-

- ⚠️ 将影响 ${affectedCount} 个用户: -

-

- ${affectedUserNames} -

-

- 这些用户的用户组将被自动移除 -

-
- ` : ` -
-

- ✅ 当前没有用户使用此用户组 -

-
- `} -
- `, - icon: 'warning', - showCancelButton: true, - confirmButtonText: '确认删除', - cancelButtonText: '取消', - confirmButtonColor: '#dc2626', + setDeletingUserGroup({ + name: groupName, + affectedUsers: affectedUsers.map(u => ({ username: u.username, role: u.role })) }); + setShowDeleteUserGroupModal(true); + }; - if (!isConfirmed) return; + const handleConfirmDeleteUserGroup = async () => { + if (!deletingUserGroup) return; - await handleUserGroupAction('delete', groupName); + try { + await handleUserGroupAction('delete', deletingUserGroup.name); + setShowDeleteUserGroupModal(false); + setDeletingUserGroup(null); + } catch (err) { + // 错误处理已在 handleUserGroupAction 中处理 + } }; const handleStartEditUserGroup = (group: { name: string; enabledApis: string[] }) => { @@ -293,9 +414,9 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { } await refreshConfig(); - showSuccess('用户组分配成功'); + showSuccess('用户组分配成功', showAlert); } catch (err) { - showError(err instanceof Error ? err.message : '操作失败'); + showError(err instanceof Error ? err.message : '操作失败', showAlert); } }; @@ -339,20 +460,9 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { setShowAddUserForm(false); // 关闭添加用户表单 }; - const handleDeleteUser = async (username: string) => { - const { isConfirmed } = await Swal.fire({ - title: '确认删除用户', - text: `删除用户 ${username} 将同时删除其搜索历史、播放记录和收藏夹,此操作不可恢复!`, - icon: 'warning', - showCancelButton: true, - confirmButtonText: '确认删除', - cancelButtonText: '取消', - confirmButtonColor: '#dc2626', - }); - - if (!isConfirmed) return; - - await handleUserAction('deleteUser', username); + const handleDeleteUser = (username: string) => { + setDeletingUser(username); + setShowDeleteUserModal(true); }; const handleConfigureUserApis = (user: { @@ -365,6 +475,29 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { setShowConfigureApisModal(true); }; + const handleConfigureUserGroup = (user: { + username: string; + role: 'user' | 'admin' | 'owner'; + tags?: string[]; + }) => { + setSelectedUserForGroup(user); + setSelectedUserGroups(user.tags || []); + setShowConfigureUserGroupModal(true); + }; + + const handleSaveUserGroups = async () => { + if (!selectedUserForGroup) return; + + try { + await handleAssignUserGroup(selectedUserForGroup.username, selectedUserGroups); + setShowConfigureUserGroupModal(false); + setSelectedUserForGroup(null); + setSelectedUserGroups([]); + } catch (err) { + // 错误处理已在 handleAssignUserGroup 中处理 + } + }; + // 提取URL域名的辅助函数 const extractDomain = (url: string): string => { try { @@ -401,7 +534,7 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { setSelectedUser(null); setSelectedApis([]); } catch (err) { - showError(err instanceof Error ? err.message : '操作失败'); + showError(err instanceof Error ? err.message : '操作失败', showAlert); } }; @@ -439,7 +572,19 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { // 成功后刷新配置(无需整页刷新) await refreshConfig(); } catch (err) { - showError(err instanceof Error ? err.message : '操作失败'); + showError(err instanceof Error ? err.message : '操作失败', showAlert); + } + }; + + const handleConfirmDeleteUser = async () => { + if (!deletingUser) return; + + try { + await handleUserAction('deleteUser', deletingUser); + setShowDeleteUserModal(false); + setDeletingUser(null); + } catch (err) { + // 错误处理已在 handleUserAction 中处理 } }; @@ -795,31 +940,7 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { (user.role === 'user' || user.username === currentUsername))) && ( + + +
+
+
+ + + + + 配置说明 + +
+

+ 提示:选择"无用户组"为无限制,选择特定用户组将限制用户只能访问该用户组允许的采集源 +

+
+
+ + {/* 用户组选择 - 下拉选择器 */} +
+ + +

+ 选择"无用户组"为无限制,选择特定用户组将限制用户只能访问该用户组允许的采集源 +

+
+ + + + {/* 操作按钮 */} +
+ + +
+ + + , + document.body + )} + + {/* 删除用户组确认弹窗 */} + {showDeleteUserGroupModal && deletingUserGroup && createPortal( +
+
+
+
+

+ 确认删除用户组 +

+ +
+ +
+
+
+ + + + + 危险操作警告 + +
+

+ 删除用户组 {deletingUserGroup.name} 将影响所有使用该组的用户,此操作不可恢复! +

+
+ + {deletingUserGroup.affectedUsers.length > 0 ? ( +
+
+ + + + + ⚠️ 将影响 {deletingUserGroup.affectedUsers.length} 个用户: + +
+
+ {deletingUserGroup.affectedUsers.map((user, index) => ( +
+ • {user.username} ({user.role}) +
+ ))} +
+

+ 这些用户的用户组将被自动移除 +

+
+ ) : ( +
+
+ + + + + ✅ 当前没有用户使用此用户组 + +
+
+ )} +
+ + {/* 操作按钮 */} +
+ + +
+
+
+
, + document.body + )} + + {/* 删除用户确认弹窗 */} + {showDeleteUserModal && deletingUser && createPortal( +
+
+
+
+

+ 确认删除用户 +

+ +
+ +
+
+
+ + + + + 危险操作警告 + +
+

+ 删除用户 {deletingUser} 将同时删除其搜索历史、播放记录和收藏夹,此操作不可恢复! +

+
+ + {/* 操作按钮 */} +
+ + +
+
+
+
+
, + document.body + )} + + {/* 通用弹窗组件 */} + + + ); -}; +} // 视频源配置组件 const VideoSourceConfig = ({ @@ -1290,6 +1673,7 @@ const VideoSourceConfig = ({ config: AdminConfig | null; refreshConfig: () => Promise; }) => { + const { alertModal, showAlert, hideAlert } = useAlertModal(); const [sources, setSources] = useState([]); const [showAddForm, setShowAddForm] = useState(false); const [orderChanged, setOrderChanged] = useState(false); @@ -1343,7 +1727,7 @@ const VideoSourceConfig = ({ // 成功后刷新配置 await refreshConfig(); } catch (err) { - showError(err instanceof Error ? err.message : '操作失败'); + showError(err instanceof Error ? err.message : '操作失败', showAlert); throw err; // 向上抛出方便调用处判断 } }; @@ -1616,6 +2000,17 @@ const VideoSourceConfig = ({ )} + + {/* 通用弹窗组件 */} + ); }; @@ -1628,6 +2023,7 @@ const CategoryConfig = ({ config: AdminConfig | null; refreshConfig: () => Promise; }) => { + const { alertModal, showAlert, hideAlert } = useAlertModal(); const [categories, setCategories] = useState([]); const [showAddForm, setShowAddForm] = useState(false); const [orderChanged, setOrderChanged] = useState(false); @@ -1680,7 +2076,7 @@ const CategoryConfig = ({ // 成功后刷新配置 await refreshConfig(); } catch (err) { - showError(err instanceof Error ? err.message : '操作失败'); + showError(err instanceof Error ? err.message : '操作失败', showAlert); throw err; // 向上抛出方便调用处判断 } }; @@ -1952,12 +2348,24 @@ const CategoryConfig = ({ )} + + {/* 通用弹窗组件 */} + ); }; // 新增配置文件组件 const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig | null; refreshConfig: () => Promise }) => { + const { alertModal, showAlert, hideAlert } = useAlertModal(); const [configContent, setConfigContent] = useState(''); const [saving, setSaving] = useState(false); const [subscriptionUrl, setSubscriptionUrl] = useState(''); @@ -1983,7 +2391,7 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig | // 拉取订阅配置 const handleFetchConfig = async () => { if (!subscriptionUrl.trim()) { - showError('请输入订阅URL'); + showError('请输入订阅URL', showAlert); return; } @@ -2006,12 +2414,12 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig | // 更新本地配置的最后检查时间 const currentTime = new Date().toISOString(); setLastCheckTime(currentTime); - showSuccess('配置拉取成功'); + showSuccess('配置拉取成功', showAlert); } else { - showError('拉取失败:未获取到配置内容'); + showError('拉取失败:未获取到配置内容', showAlert); } } catch (err) { - showError(err instanceof Error ? err.message : '拉取失败'); + showError(err instanceof Error ? err.message : '拉取失败', showAlert); } finally { setFetching(false); } @@ -2037,10 +2445,10 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig | throw new Error(data.error || `保存失败: ${resp.status}`); } - showSuccess('配置文件保存成功'); + showSuccess('配置文件保存成功', showAlert); await refreshConfig(); } catch (err) { - showError(err instanceof Error ? err.message : '保存失败'); + showError(err instanceof Error ? err.message : '保存失败', showAlert); } finally { setSaving(false); } @@ -2173,12 +2581,24 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig | + + {/* 通用弹窗组件 */} + ); }; // 新增站点配置组件 const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig | null; refreshConfig: () => Promise }) => { + const { alertModal, showAlert, hideAlert } = useAlertModal(); const [siteSettings, setSiteSettings] = useState({ SiteName: '', Announcement: '', @@ -2331,10 +2751,10 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig | throw new Error(data.error || `保存失败: ${resp.status}`); } - showSuccess('保存成功, 请刷新页面'); + showSuccess('保存成功, 请刷新页面', showAlert); await refreshConfig(); } catch (err) { - showError(err instanceof Error ? err.message : '保存失败'); + showError(err instanceof Error ? err.message : '保存失败', showAlert); } finally { setSaving(false); } @@ -2727,15 +3147,28 @@ const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig | {saving ? '保存中…' : '保存'} + + {/* 通用弹窗组件 */} + ); }; function AdminPageClient() { + const { alertModal, showAlert, hideAlert } = useAlertModal(); const [config, setConfig] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [role, setRole] = useState<'owner' | 'admin' | null>(null); + const [showResetConfigModal, setShowResetConfigModal] = useState(false); const [expandedTabs, setExpandedTabs] = useState<{ [key: string]: boolean }>({ userConfig: false, videoSource: false, @@ -2765,7 +3198,7 @@ function AdminPageClient() { setRole(data.Role); } catch (err) { const msg = err instanceof Error ? err.message : '获取配置失败'; - showError(msg); + showError(msg, showAlert); setError(msg); } finally { if (showLoading) { @@ -2788,26 +3221,21 @@ function AdminPageClient() { }; // 新增: 重置配置处理函数 - const handleResetConfig = async () => { - const { isConfirmed } = await Swal.fire({ - title: '确认重置配置', - text: '此操作将重置用户封禁和管理员设置、自定义视频源,站点配置将重置为默认值,是否继续?', - icon: 'warning', - showCancelButton: true, - confirmButtonText: '确认', - cancelButtonText: '取消', - }); - if (!isConfirmed) return; + const handleResetConfig = () => { + setShowResetConfigModal(true); + }; + const handleConfirmResetConfig = async () => { try { const response = await fetch(`/api/admin/reset`); if (!response.ok) { throw new Error(`重置失败: ${response.status}`); } - showSuccess('重置成功,请刷新页面!'); + showSuccess('重置成功,请刷新页面!', showAlert); await fetchConfig(); + setShowResetConfigModal(false); } catch (err) { - showError(err instanceof Error ? err.message : '重置失败'); + showError(err instanceof Error ? err.message : '重置失败', showAlert); } }; @@ -2834,7 +3262,7 @@ function AdminPageClient() { } if (error) { - // 错误已通过 SweetAlert2 展示,此处直接返回空 + // 错误已通过弹窗展示,此处直接返回空 return null; } @@ -2952,6 +3380,73 @@ function AdminPageClient() { + + {/* 通用弹窗组件 */} + + + {/* 重置配置确认弹窗 */} + {showResetConfigModal && createPortal( +
+
+
+
+

+ 确认重置配置 +

+ +
+ +
+
+
+ + + + + ⚠️ 危险操作警告 + +
+

+ 此操作将重置用户封禁和管理员设置、自定义视频源,站点配置将重置为默认值,是否继续? +

+
+
+ + {/* 操作按钮 */} +
+ + +
+
+
+
, + document.body + )} ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ace6098..2a6582d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,7 +4,6 @@ import type { Metadata, Viewport } from 'next'; import { Inter } from 'next/font/google'; import './globals.css'; -import 'sweetalert2/dist/sweetalert2.min.css'; import { getConfig } from '@/lib/config'; diff --git a/src/components/DataMigration.tsx b/src/components/DataMigration.tsx index 2c40256..99e9b4f 100644 --- a/src/components/DataMigration.tsx +++ b/src/components/DataMigration.tsx @@ -1,30 +1,181 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client'; -import { AlertTriangle, Download, FileCheck, Lock, Upload } from 'lucide-react'; -import { useRef, useState } from 'react'; -import Swal from 'sweetalert2'; +import { AlertCircle,AlertTriangle, CheckCircle, Download, FileCheck, Lock, Upload } from 'lucide-react'; +import { useEffect,useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; interface DataMigrationProps { onRefreshConfig?: () => Promise; } +interface AlertModalProps { + isOpen: boolean; + onClose: () => void; + type: 'success' | 'error' | 'warning'; + title: string; + message?: string; + html?: string; + confirmText?: string; + onConfirm?: () => void; + showConfirm?: boolean; + timer?: number; +} + +const AlertModal = ({ + isOpen, + onClose, + type, + title, + message, + html, + confirmText = '确定', + onConfirm, + showConfirm = false, + timer +}: AlertModalProps) => { + const [isVisible, setIsVisible] = useState(false); + + // 控制动画状态 + useEffect(() => { + if (isOpen) { + setIsVisible(true); + if (timer) { + setTimeout(() => { + onClose(); + }, timer); + } + } else { + setIsVisible(false); + } + }, [isOpen, timer, onClose]); + + if (!isOpen) return null; + + const getIcon = () => { + switch (type) { + case 'success': + return ; + case 'error': + return ; + case 'warning': + return ; + default: + return null; + } + }; + + const getBgColor = () => { + switch (type) { + case 'success': + return 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800'; + case 'error': + return 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800'; + case 'warning': + return 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800'; + default: + return 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800'; + } + }; + + return createPortal( +
+
+
+
+ {getIcon()} +
+ +

+ {title} +

+ + {message && ( +

+ {message} +

+ )} + + {html && ( +
+ )} + +
+ {showConfirm && onConfirm ? ( + <> + + + + ) : ( + + )} +
+
+
+
, + document.body + ); +}; + const DataMigration = ({ onRefreshConfig }: DataMigrationProps) => { const [exportPassword, setExportPassword] = useState(''); const [importPassword, setImportPassword] = useState(''); const [selectedFile, setSelectedFile] = useState(null); const [isExporting, setIsExporting] = useState(false); const [isImporting, setIsImporting] = useState(false); + const [alertModal, setAlertModal] = useState<{ + isOpen: boolean; + type: 'success' | 'error' | 'warning'; + title: string; + message?: string; + html?: string; + confirmText?: string; + onConfirm?: () => void; + showConfirm?: boolean; + timer?: number; + }>({ + isOpen: false, + type: 'success', + title: '', + }); const fileInputRef = useRef(null); + const showAlert = (config: Omit) => { + setAlertModal({ ...config, isOpen: true }); + }; + + const hideAlert = () => { + setAlertModal(prev => ({ ...prev, isOpen: false })); + }; + // 导出数据 const handleExport = async () => { if (!exportPassword.trim()) { - Swal.fire({ - icon: 'error', + showAlert({ + type: 'error', title: '错误', - text: '请输入加密密码', - returnFocus: false, + message: '请输入加密密码', }); return; } @@ -67,22 +218,19 @@ const DataMigration = ({ onRefreshConfig }: DataMigrationProps) => { window.URL.revokeObjectURL(url); document.body.removeChild(a); - Swal.fire({ - icon: 'success', + showAlert({ + type: 'success', title: '导出成功', - text: '数据已成功导出,请妥善保管备份文件和密码', + message: '数据已成功导出,请妥善保管备份文件和密码', timer: 3000, - showConfirmButton: false, - returnFocus: false, }); setExportPassword(''); } catch (error) { - Swal.fire({ - icon: 'error', + showAlert({ + type: 'error', title: '导出失败', - text: error instanceof Error ? error.message : '导出过程中发生错误', - returnFocus: false, + message: error instanceof Error ? error.message : '导出过程中发生错误', }); } finally { setIsExporting(false); @@ -100,21 +248,19 @@ const DataMigration = ({ onRefreshConfig }: DataMigrationProps) => { // 导入数据 const handleImport = async () => { if (!selectedFile) { - Swal.fire({ - icon: 'error', + showAlert({ + type: 'error', title: '错误', - text: '请选择备份文件', - returnFocus: false, + message: '请选择备份文件', }); return; } if (!importPassword.trim()) { - Swal.fire({ - icon: 'error', + showAlert({ + type: 'error', title: '错误', - text: '请输入解密密码', - returnFocus: false, + message: '请输入解密密码', }); return; } @@ -137,8 +283,8 @@ const DataMigration = ({ onRefreshConfig }: DataMigrationProps) => { throw new Error(result.error || `导入失败: ${response.status}`); } - await Swal.fire({ - icon: 'success', + showAlert({ + type: 'success', title: '导入成功', html: `
@@ -149,30 +295,30 @@ const DataMigration = ({ onRefreshConfig }: DataMigrationProps) => {

请刷新页面以查看最新数据。

`, - confirmButtonText: '刷新页面', - returnFocus: false, + confirmText: '刷新页面', + showConfirm: true, + onConfirm: async () => { + // 清理状态 + setSelectedFile(null); + setImportPassword(''); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + + // 刷新配置 + if (onRefreshConfig) { + await onRefreshConfig(); + } + + // 刷新页面 + window.location.reload(); + }, }); - - // 清理状态 - setSelectedFile(null); - setImportPassword(''); - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } - - // 刷新配置 - if (onRefreshConfig) { - await onRefreshConfig(); - } - - // 刷新页面 - window.location.reload(); } catch (error) { - Swal.fire({ - icon: 'error', + showAlert({ + type: 'error', title: '导入失败', - text: error instanceof Error ? error.message : '导入过程中发生错误', - returnFocus: false, + message: error instanceof Error ? error.message : '导入过程中发生错误', }); } finally { setIsImporting(false); @@ -180,163 +326,179 @@ const DataMigration = ({ onRefreshConfig }: DataMigrationProps) => { }; return ( -
- {/* 简洁警告提示 */} -
- -

- 数据迁移操作请谨慎,确保已备份重要数据 -

-
- - {/* 主要操作区域 - 响应式布局 */} -
- {/* 数据导出 */} -
-
-
- -
-
-

数据导出

-

创建加密备份文件

-
-
- -
-
- {/* 密码输入 */} -
- - setExportPassword(e.target.value)} - placeholder="设置强密码保护备份文件" - className="w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" - disabled={isExporting} - /> -

- 导入时需要使用相同密码 -

-
- - {/* 备份内容列表 */} -
-

备份内容:

-
-
• 管理配置
-
• 用户数据
-
• 播放记录
-
• 收藏夹
-
-
-
- - {/* 导出按钮 */} - -
+ <> +
+ {/* 简洁警告提示 */} +
+ +

+ 数据迁移操作请谨慎,确保已备份重要数据 +

- {/* 数据导入 */} -
-
-
- + {/* 主要操作区域 - 响应式布局 */} +
+ {/* 数据导出 */} +
+
+
+ +
+
+

数据导出

+

创建加密备份文件

+
-
-

数据导入

-

⚠️ 将清空现有数据

+ +
+
+ {/* 密码输入 */} +
+ + setExportPassword(e.target.value)} + placeholder="设置强密码保护备份文件" + className="w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" + disabled={isExporting} + /> +

+ 导入时需要使用相同密码 +

+
+ + {/* 备份内容列表 */} +
+

备份内容:

+
+
• 管理配置
+
• 用户数据
+
• 播放记录
+
• 收藏夹
+
+
+
+ + {/* 导出按钮 */} +
-
-
- {/* 文件选择 */} -
- - + {/* 数据导入 */} +
+
+
+
- - {/* 密码输入 */}
- - setImportPassword(e.target.value)} - placeholder="输入导出时的加密密码" - className="w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-red-500 focus:border-red-500 transition-colors" - disabled={isImporting} - /> +

数据导入

+

⚠️ 将清空现有数据

- {/* 导入按钮 */} - +
+ + {/* 导入按钮 */} + +
-
+ + {/* 弹窗组件 */} + + ); };