mirror of
https://github.com/stackblitz/bolt.new
synced 2025-06-26 18:17:50 +00:00
feat: 重构用户菜单组件,引入新的对话框组件以替代链接跳转方式,优化用户交互体验。
This commit is contained in:
parent
df93f0d897
commit
26fa196bdb
38
app/components/auth/ProfileDialog.tsx
Normal file
38
app/components/auth/ProfileDialog.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { Dialog, DialogTitle, DialogDescription, DialogRoot } from '~/components/ui/Dialog';
|
||||
import { useAuth } from '~/hooks/useAuth';
|
||||
|
||||
interface ProfileDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ProfileDialog({ isOpen, onClose }: ProfileDialogProps) {
|
||||
const { user } = useAuth();
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<DialogRoot open={isOpen}>
|
||||
<Dialog onBackdrop={onClose} onClose={onClose}>
|
||||
<DialogTitle>个人信息</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-bolt-elements-textPrimary">
|
||||
昵称
|
||||
</label>
|
||||
<p className="mt-1 text-bolt-elements-textSecondary">{user.nickname}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-bolt-elements-textPrimary">
|
||||
手机号
|
||||
</label>
|
||||
<p className="mt-1 text-bolt-elements-textSecondary">{user.phone}</p>
|
||||
</div>
|
||||
{/* 可以根据需要添加更多用户信息 */}
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</Dialog>
|
||||
</DialogRoot>
|
||||
);
|
||||
}
|
||||
24
app/components/auth/SubscriptionDialog.tsx
Normal file
24
app/components/auth/SubscriptionDialog.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { Dialog, DialogTitle, DialogDescription, DialogRoot } from '~/components/ui/Dialog';
|
||||
|
||||
interface SubscriptionDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps) {
|
||||
return (
|
||||
<DialogRoot open={isOpen}>
|
||||
<Dialog onBackdrop={onClose} onClose={onClose}>
|
||||
<DialogTitle>订阅信息</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div className="space-y-4">
|
||||
<p className="text-bolt-elements-textSecondary">
|
||||
这里显示用户的订阅信息。您可以根据实际需求添加更多详细内容。
|
||||
</p>
|
||||
{/* 可以添加更多订阅相关的信息 */}
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</Dialog>
|
||||
</DialogRoot>
|
||||
);
|
||||
}
|
||||
@ -1,74 +1,82 @@
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import { Fragment } from 'react';
|
||||
import { Fragment, useState } from 'react';
|
||||
import { useAuth } from '~/hooks/useAuth';
|
||||
import { Avatar } from '~/components/ui/Avatar';
|
||||
import { Link } from '@remix-run/react';
|
||||
import { ProfileDialog } from '~/components/auth/ProfileDialog';
|
||||
import { SubscriptionDialog } from '~/components/auth/SubscriptionDialog';
|
||||
|
||||
export function UserMenu() {
|
||||
const { user, logout } = useAuth();
|
||||
const [isProfileOpen, setIsProfileOpen] = useState(false);
|
||||
const [isSubscriptionOpen, setIsSubscriptionOpen] = useState(false);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="inline-flex items-center gap-x-1.5 rounded-md bg-bolt-elements-background-depth-2 px-3 py-2 text-sm font-medium text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-primary-background">
|
||||
<Avatar src={user.avatarUrl} alt={user.nickname} />
|
||||
<span className="ml-2">{user.nickname}</span>
|
||||
<div className="i-ph:caret-down-fill text-bolt-elements-textSecondary" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<>
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="inline-flex items-center gap-x-1.5 rounded-md bg-bolt-elements-background-depth-1 px-3 py-2 text-sm font-medium text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-2 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-primary-background">
|
||||
<Avatar src={user.avatarUrl} alt={user.nickname} />
|
||||
<span className="ml-2">{user.nickname}</span>
|
||||
<div className="i-ph:caret-down-fill text-bolt-elements-textSecondary" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-bolt-elements-background-depth-2 shadow-lg ring-1 ring-bolt-elements-borderColor focus:outline-none">
|
||||
<div className="py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<Link
|
||||
to="/profile"
|
||||
className={`${
|
||||
active ? 'bg-bolt-elements-button-primary-background text-bolt-elements-button-primary-text' : 'text-bolt-elements-textSecondary'
|
||||
} block px-4 py-2 text-sm transition-colors duration-150 ease-in-out`}
|
||||
>
|
||||
个人信息
|
||||
</Link>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<Link
|
||||
to="/subscription"
|
||||
className={`${
|
||||
active ? 'bg-bolt-elements-button-primary-background text-bolt-elements-button-primary-text' : 'text-bolt-elements-textSecondary'
|
||||
} block px-4 py-2 text-sm transition-colors duration-150 ease-in-out`}
|
||||
>
|
||||
订阅信息
|
||||
</Link>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={logout}
|
||||
className={`${
|
||||
active ? 'bg-bolt-elements-button-primary-background text-bolt-elements-button-primary-text' : 'text-bolt-elements-textSecondary'
|
||||
} block w-full text-left px-4 py-2 text-sm transition-colors duration-150 ease-in-out`}
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-bolt-elements-background-depth-1 shadow-lg ring-1 ring-bolt-elements-borderColor focus:outline-none">
|
||||
<div className="py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={() => setIsProfileOpen(true)}
|
||||
className={`${
|
||||
active ? 'bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary' : 'text-bolt-elements-textSecondary'
|
||||
} block w-full text-left px-4 py-2 text-sm transition-colors duration-150 ease-in-out`}
|
||||
>
|
||||
个人信息
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={() => setIsSubscriptionOpen(true)}
|
||||
className={`${
|
||||
active ? 'bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary' : 'text-bolt-elements-textSecondary'
|
||||
} block w-full text-left px-4 py-2 text-sm transition-colors duration-150 ease-in-out`}
|
||||
>
|
||||
订阅信息
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={logout}
|
||||
className={`${
|
||||
active ? 'bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary' : 'text-bolt-elements-textSecondary'
|
||||
} block w-full text-left px-4 py-2 text-sm transition-colors duration-150 ease-in-out`}
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
|
||||
<ProfileDialog isOpen={isProfileOpen} onClose={() => setIsProfileOpen(false)} />
|
||||
<SubscriptionDialog isOpen={isSubscriptionOpen} onClose={() => setIsSubscriptionOpen(false)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -15,13 +15,25 @@ export function useAuth() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (token && storedUser) {
|
||||
setIsAuthenticated(true);
|
||||
setUser(JSON.parse(storedUser));
|
||||
}
|
||||
setIsLoading(false);
|
||||
const checkAuth = () => {
|
||||
const token = localStorage.getItem('token');
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (token && storedUser) {
|
||||
setIsAuthenticated(true);
|
||||
setUser(JSON.parse(storedUser));
|
||||
} else {
|
||||
setIsAuthenticated(false);
|
||||
setUser(null);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
window.addEventListener('storage', checkAuth);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('storage', checkAuth);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const login = (token: string, userData: User) => {
|
||||
@ -36,6 +48,7 @@ export function useAuth() {
|
||||
localStorage.removeItem('user');
|
||||
setIsAuthenticated(false);
|
||||
setUser(null);
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
return { isAuthenticated, isLoading, user, login, logout };
|
||||
|
||||
Loading…
Reference in New Issue
Block a user