From 179fc73c8fa2f540101a7a039eaf8204dace3a10 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Thu, 4 Dec 2025 22:14:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E5=8E=BB=E5=B9=BF=E5=91=8A=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/admin/page.tsx | 3597 ++++++++++++++++++++----------- src/app/api/ad-filter/route.ts | 28 + src/app/api/admin/site/route.ts | 10 +- src/app/play/page.tsx | 75 + src/lib/admin.types.ts | 3 + 5 files changed, 2477 insertions(+), 1236 deletions(-) create mode 100644 src/app/api/ad-filter/route.ts diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index a4ed081..03bc9db 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -50,35 +50,53 @@ import PageLayout from '@/components/PageLayout'; // 统一按钮样式系统 const buttonStyles = { // 主要操作按钮(蓝色)- 用于配置、设置、确认等 - primary: 'px-3 py-1.5 text-sm font-medium bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded-lg transition-colors', + primary: + 'px-3 py-1.5 text-sm font-medium bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded-lg transition-colors', // 成功操作按钮(绿色)- 用于添加、启用、保存等 - success: 'px-3 py-1.5 text-sm font-medium bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700 text-white rounded-lg transition-colors', + success: + 'px-3 py-1.5 text-sm font-medium bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700 text-white rounded-lg transition-colors', // 危险操作按钮(红色)- 用于删除、禁用、重置等 - danger: 'px-3 py-1.5 text-sm font-medium bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700 text-white rounded-lg transition-colors', + danger: + 'px-3 py-1.5 text-sm font-medium bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700 text-white rounded-lg transition-colors', // 次要操作按钮(灰色)- 用于取消、关闭等 - secondary: 'px-3 py-1.5 text-sm font-medium bg-gray-600 hover:bg-gray-700 dark:bg-gray-600 dark:hover:bg-gray-700 text-white rounded-lg transition-colors', + secondary: + 'px-3 py-1.5 text-sm font-medium bg-gray-600 hover:bg-gray-700 dark:bg-gray-600 dark:hover:bg-gray-700 text-white rounded-lg transition-colors', // 警告操作按钮(黄色)- 用于批量禁用等 - warning: 'px-3 py-1.5 text-sm font-medium bg-yellow-600 hover:bg-yellow-700 dark:bg-yellow-600 dark:hover:bg-yellow-700 text-white rounded-lg transition-colors', + warning: + 'px-3 py-1.5 text-sm font-medium bg-yellow-600 hover:bg-yellow-700 dark:bg-yellow-600 dark:hover:bg-yellow-700 text-white rounded-lg transition-colors', // 小尺寸主要按钮 - primarySmall: 'px-2 py-1 text-xs font-medium bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded-md transition-colors', + primarySmall: + 'px-2 py-1 text-xs font-medium bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded-md transition-colors', // 小尺寸成功按钮 - successSmall: 'px-2 py-1 text-xs font-medium bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700 text-white rounded-md transition-colors', + successSmall: + 'px-2 py-1 text-xs font-medium bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700 text-white rounded-md transition-colors', // 小尺寸危险按钮 - dangerSmall: 'px-2 py-1 text-xs font-medium bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700 text-white rounded-md transition-colors', + dangerSmall: + 'px-2 py-1 text-xs font-medium bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700 text-white rounded-md transition-colors', // 小尺寸次要按钮 - secondarySmall: 'px-2 py-1 text-xs font-medium bg-gray-600 hover:bg-gray-700 dark:bg-gray-600 dark:hover:bg-gray-700 text-white rounded-md transition-colors', + secondarySmall: + 'px-2 py-1 text-xs font-medium bg-gray-600 hover:bg-gray-700 dark:bg-gray-600 dark:hover:bg-gray-700 text-white rounded-md transition-colors', // 小尺寸警告按钮 - warningSmall: 'px-2 py-1 text-xs font-medium bg-yellow-600 hover:bg-yellow-700 dark:bg-yellow-600 dark:hover:bg-yellow-700 text-white rounded-md transition-colors', + warningSmall: + 'px-2 py-1 text-xs font-medium bg-yellow-600 hover:bg-yellow-700 dark:bg-yellow-600 dark:hover:bg-yellow-700 text-white rounded-md transition-colors', // 圆角小按钮(用于表格操作) - roundedPrimary: 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 hover:bg-blue-200 dark:bg-blue-900/40 dark:hover:bg-blue-900/60 dark:text-blue-200 transition-colors', - roundedSuccess: 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-green-100 text-green-800 hover:bg-green-200 dark:bg-green-900/40 dark:hover:bg-green-900/60 dark:text-green-200 transition-colors', - roundedDanger: 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-red-100 text-red-800 hover:bg-red-200 dark:bg-red-900/40 dark:hover:bg-red-900/60 dark:text-red-200 transition-colors', - roundedSecondary: 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 hover:bg-gray-200 dark:bg-gray-700/40 dark:hover:bg-gray-700/60 dark:text-gray-200 transition-colors', - roundedWarning: 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 hover:bg-yellow-200 dark:bg-yellow-900/40 dark:hover:bg-yellow-900/60 dark:text-yellow-200 transition-colors', - roundedPurple: 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 hover:bg-purple-200 dark:bg-purple-900/40 dark:hover:bg-purple-900/60 dark:text-purple-200 transition-colors', + roundedPrimary: + 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 hover:bg-blue-200 dark:bg-blue-900/40 dark:hover:bg-blue-900/60 dark:text-blue-200 transition-colors', + roundedSuccess: + 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-green-100 text-green-800 hover:bg-green-200 dark:bg-green-900/40 dark:hover:bg-green-900/60 dark:text-green-200 transition-colors', + roundedDanger: + 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-red-100 text-red-800 hover:bg-red-200 dark:bg-red-900/40 dark:hover:bg-red-900/60 dark:text-red-200 transition-colors', + roundedSecondary: + 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 hover:bg-gray-200 dark:bg-gray-700/40 dark:hover:bg-gray-700/60 dark:text-gray-200 transition-colors', + roundedWarning: + 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 hover:bg-yellow-200 dark:bg-yellow-900/40 dark:hover:bg-yellow-900/60 dark:text-yellow-200 transition-colors', + roundedPurple: + 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 hover:bg-purple-200 dark:bg-purple-900/40 dark:hover:bg-purple-900/60 dark:text-purple-200 transition-colors', // 禁用状态 - disabled: 'px-3 py-1.5 text-sm font-medium bg-gray-400 dark:bg-gray-600 cursor-not-allowed text-white rounded-lg transition-colors', - disabledSmall: 'px-2 py-1 text-xs font-medium bg-gray-400 dark:bg-gray-600 cursor-not-allowed text-white rounded-md transition-colors', + disabled: + 'px-3 py-1.5 text-sm font-medium bg-gray-400 dark:bg-gray-600 cursor-not-allowed text-white rounded-lg transition-colors', + disabledSmall: + 'px-2 py-1 text-xs font-medium bg-gray-400 dark:bg-gray-600 cursor-not-allowed text-white rounded-md transition-colors', // 开关按钮样式 toggleOn: 'bg-green-600 dark:bg-green-600', toggleOff: 'bg-gray-200 dark:bg-gray-700', @@ -86,7 +104,8 @@ const buttonStyles = { toggleThumbOn: 'translate-x-6', toggleThumbOff: 'translate-x-1', // 快速操作按钮样式 - quickAction: 'px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-md transition-colors', + quickAction: + 'px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-md transition-colors', }; // 通用弹窗组件 @@ -107,7 +126,7 @@ const AlertModal = ({ title, message, timer, - showConfirm = false + showConfirm = false, }: AlertModalProps) => { const [isVisible, setIsVisible] = useState(false); @@ -129,11 +148,11 @@ const AlertModal = ({ const getIcon = () => { switch (type) { case 'success': - return ; + return ; case 'error': - return ; + return ; case 'warning': - return ; + return ; default: return null; } @@ -153,21 +172,25 @@ const AlertModal = ({ }; return createPortal( -
-
-
-
- {getIcon()} -
+
+
+
+
{getIcon()}
-

+

{title}

{message && ( -

- {message} -

+

{message}

)} {showConfirm && ( @@ -205,7 +228,7 @@ const useAlertModal = () => { }; const hideAlert = () => { - setAlertModal(prev => ({ ...prev, isOpen: false })); + setAlertModal((prev) => ({ ...prev, isOpen: false })); }; return { alertModal, showAlert, hideAlert }; @@ -237,12 +260,15 @@ const useLoadingState = () => { const [loadingStates, setLoadingStates] = useState({}); const setLoading = (key: string, loading: boolean) => { - setLoadingStates(prev => ({ ...prev, [key]: loading })); + setLoadingStates((prev) => ({ ...prev, [key]: loading })); }; const isLoading = (key: string) => loadingStates[key] || false; - const withLoading = async (key: string, operation: () => Promise): Promise => { + const withLoading = async ( + key: string, + operation: () => Promise + ): Promise => { setLoading(key, true); try { const result = await operation(); @@ -380,7 +406,8 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { tags?: string[]; } | null>(null); const [selectedApis, setSelectedApis] = useState([]); - const [showConfigureUserGroupModal, setShowConfigureUserGroupModal] = useState(false); + const [showConfigureUserGroupModal, setShowConfigureUserGroupModal] = + useState(false); const [selectedUserForGroup, setSelectedUserForGroup] = useState<{ username: string; role: 'user' | 'admin' | 'owner'; @@ -390,10 +417,14 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { const [selectedUsers, setSelectedUsers] = useState>(new Set()); const [showBatchUserGroupModal, setShowBatchUserGroupModal] = useState(false); const [selectedUserGroup, setSelectedUserGroup] = useState(''); - const [showDeleteUserGroupModal, setShowDeleteUserGroupModal] = useState(false); + const [showDeleteUserGroupModal, setShowDeleteUserGroupModal] = + useState(false); const [deletingUserGroup, setDeletingUserGroup] = useState<{ name: string; - affectedUsers: Array<{ username: string; role: 'user' | 'admin' | 'owner' }>; + affectedUsers: Array<{ + username: string; + role: 'user' | 'admin' | 'owner'; + }>; } | null>(null); const [showDeleteUserModal, setShowDeleteUserModal] = useState(false); const [deletingUser, setDeletingUser] = useState(null); @@ -403,12 +434,13 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { // 使用 useMemo 计算全选状态,避免每次渲染都重新计算 const selectAllUsers = useMemo(() => { - const selectableUserCount = config?.UserConfig?.Users?.filter(user => - (role === 'owner' || - (role === 'admin' && - (user.role === 'user' || - user.username === currentUsername))) - ).length || 0; + const selectableUserCount = + config?.UserConfig?.Users?.filter( + (user) => + role === 'owner' || + (role === 'admin' && + (user.role === 'user' || user.username === currentUsername)) + ).length || 0; return selectedUsers.size === selectableUserCount && selectedUsers.size > 0; }, [selectedUsers.size, config?.UserConfig?.Users, role, currentUsername]); @@ -449,7 +481,14 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { setShowEditUserGroupForm(false); } - showSuccess(action === 'add' ? '用户组添加成功' : action === 'edit' ? '用户组更新成功' : '用户组删除成功', showAlert); + showSuccess( + action === 'add' + ? '用户组添加成功' + : action === 'edit' + ? '用户组更新成功' + : '用户组删除成功', + showAlert + ); } catch (err) { showError(err instanceof Error ? err.message : '操作失败', showAlert); throw err; @@ -464,18 +503,26 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { const handleEditUserGroup = () => { if (!editingUserGroup?.name.trim()) return; - handleUserGroupAction('edit', editingUserGroup.name, editingUserGroup.enabledApis); + handleUserGroupAction( + 'edit', + editingUserGroup.name, + editingUserGroup.enabledApis + ); }; const handleDeleteUserGroup = (groupName: string) => { // 计算会受影响的用户数量 - const affectedUsers = config?.UserConfig?.Users?.filter(user => - user.tags && user.tags.includes(groupName) - ) || []; + const affectedUsers = + config?.UserConfig?.Users?.filter( + (user) => user.tags && user.tags.includes(groupName) + ) || []; setDeletingUserGroup({ name: groupName, - affectedUsers: affectedUsers.map(u => ({ username: u.username, role: u.role })) + affectedUsers: affectedUsers.map((u) => ({ + username: u.username, + role: u.role, + })), }); setShowDeleteUserGroupModal(true); }; @@ -492,14 +539,20 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { } }; - const handleStartEditUserGroup = (group: { name: string; enabledApis: string[] }) => { + const handleStartEditUserGroup = (group: { + name: string; + enabledApis: string[]; + }) => { setEditingUserGroup({ ...group }); setShowEditUserGroupForm(true); setShowAddUserGroupForm(false); }; // 为用户分配用户组 - const handleAssignUserGroup = async (username: string, userGroups: string[]) => { + const handleAssignUserGroup = async ( + username: string, + userGroups: string[] + ) => { return withLoading(`assignUserGroup_${username}`, async () => { try { const res = await fetch('/api/admin/user', { @@ -531,21 +584,32 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { }; const handleUnbanUser = async (uname: string) => { - await withLoading(`unbanUser_${uname}`, () => handleUserAction('unban', uname)); + await withLoading(`unbanUser_${uname}`, () => + handleUserAction('unban', uname) + ); }; const handleSetAdmin = async (uname: string) => { - await withLoading(`setAdmin_${uname}`, () => handleUserAction('setAdmin', uname)); + await withLoading(`setAdmin_${uname}`, () => + handleUserAction('setAdmin', uname) + ); }; const handleRemoveAdmin = async (uname: string) => { - await withLoading(`removeAdmin_${uname}`, () => handleUserAction('cancelAdmin', uname)); + await withLoading(`removeAdmin_${uname}`, () => + handleUserAction('cancelAdmin', uname) + ); }; const handleAddUser = async () => { if (!newUser.username || !newUser.password) return; await withLoading('addUser', async () => { - await handleUserAction('add', newUser.username, newUser.password, newUser.userGroup); + await handleUserAction( + 'add', + newUser.username, + newUser.password, + newUser.userGroup + ); setNewUser({ username: '', password: '', userGroup: '' }); setShowAddUserForm(false); }); @@ -553,15 +617,18 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { const handleChangePassword = async () => { if (!changePasswordUser.username || !changePasswordUser.password) return; - await withLoading(`changePassword_${changePasswordUser.username}`, async () => { - await handleUserAction( - 'changePassword', - changePasswordUser.username, - changePasswordUser.password - ); - setChangePasswordUser({ username: '', password: '' }); - setShowChangePasswordForm(false); - }); + await withLoading( + `changePassword_${changePasswordUser.username}`, + async () => { + await handleUserAction( + 'changePassword', + changePasswordUser.username, + changePasswordUser.password + ); + setChangePasswordUser({ username: '', password: '' }); + setShowChangePasswordForm(false); + } + ); }; const handleShowChangePasswordForm = (username: string) => { @@ -598,21 +665,27 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { const handleSaveUserGroups = async () => { if (!selectedUserForGroup) return; - await withLoading(`saveUserGroups_${selectedUserForGroup.username}`, async () => { - try { - await handleAssignUserGroup(selectedUserForGroup.username, selectedUserGroups); - setShowConfigureUserGroupModal(false); - setSelectedUserForGroup(null); - setSelectedUserGroups([]); - } catch (err) { - // 错误处理已在 handleAssignUserGroup 中处理 + await withLoading( + `saveUserGroups_${selectedUserForGroup.username}`, + async () => { + try { + await handleAssignUserGroup( + selectedUserForGroup.username, + selectedUserGroups + ); + setShowConfigureUserGroupModal(false); + setSelectedUserForGroup(null); + setSelectedUserGroups([]); + } catch (err) { + // 错误处理已在 handleAssignUserGroup 中处理 + } } - }); + ); }; // 处理用户选择 const handleSelectUser = useCallback((username: string, checked: boolean) => { - setSelectedUsers(prev => { + setSelectedUsers((prev) => { const newSelectedUsers = new Set(prev); if (checked) { newSelectedUsers.add(username); @@ -623,20 +696,24 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { }); }, []); - const handleSelectAllUsers = useCallback((checked: boolean) => { - if (checked) { - // 只选择自己有权限操作的用户 - const selectableUsernames = config?.UserConfig?.Users?.filter(user => - (role === 'owner' || - (role === 'admin' && - (user.role === 'user' || - user.username === currentUsername))) - ).map(u => u.username) || []; - setSelectedUsers(new Set(selectableUsernames)); - } else { - setSelectedUsers(new Set()); - } - }, [config?.UserConfig?.Users, role, currentUsername]); + const handleSelectAllUsers = useCallback( + (checked: boolean) => { + if (checked) { + // 只选择自己有权限操作的用户 + const selectableUsernames = + config?.UserConfig?.Users?.filter( + (user) => + role === 'owner' || + (role === 'admin' && + (user.role === 'user' || user.username === currentUsername)) + ).map((u) => u.username) || []; + setSelectedUsers(new Set(selectableUsernames)); + } else { + setSelectedUsers(new Set()); + } + }, + [config?.UserConfig?.Users, role, currentUsername] + ); // 批量设置用户组 const handleBatchSetUserGroup = async (userGroup: string) => { @@ -663,7 +740,10 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { setSelectedUsers(new Set()); setShowBatchUserGroupModal(false); setSelectedUserGroup(''); - showSuccess(`已为 ${userCount} 个用户设置用户组: ${userGroup}`, showAlert); + showSuccess( + `已为 ${userCount} 个用户设置用户组: ${userGroup}`, + showAlert + ); // 刷新配置 await refreshConfig(); @@ -674,8 +754,6 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { }); }; - - // 提取URL域名的辅助函数 const extractDomain = (url: string): string => { try { @@ -796,8 +874,6 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
- - {/* 用户组管理 */}
@@ -812,7 +888,11 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { setEditingUserGroup(null); } }} - className={showAddUserGroupForm ? buttonStyles.secondary : buttonStyles.primary} + className={ + showAddUserGroupForm + ? buttonStyles.secondary + : buttonStyles.primary + } > {showAddUserGroupForm ? '取消' : '添加用户组'} @@ -836,7 +916,10 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { {userGroups.map((group) => ( - + {group.name} @@ -853,7 +936,11 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { @@ -868,7 +955,10 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { ))} {userGroups.length === 0 && ( - + 暂无用户组,请添加用户组来管理用户权限 @@ -910,7 +1000,9 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { setChangePasswordUser({ username: '', password: '' }); } }} - className={showAddUserForm ? buttonStyles.secondary : buttonStyles.success} + className={ + showAddUserForm ? buttonStyles.secondary : buttonStyles.success + } > {showAddUserForm ? '取消' : '添加用户'} @@ -927,7 +1019,10 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { placeholder='用户名' value={newUser.username} onChange={(e) => - setNewUser((prev) => ({ ...prev, username: e.target.value })) + setNewUser((prev) => ({ + ...prev, + username: e.target.value, + })) } className='px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent' /> @@ -936,7 +1031,10 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { placeholder='密码' value={newUser.password} onChange={(e) => - setNewUser((prev) => ({ ...prev, password: e.target.value })) + setNewUser((prev) => ({ + ...prev, + password: e.target.value, + })) } className='px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent' /> @@ -948,14 +1046,21 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { @@ -963,8 +1068,18 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
@@ -1001,10 +1116,20 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { />