tv页面增加“我的”

Tento commit je obsažen v:
mtvpls
2026-05-31 10:29:36 +08:00
rodič b5afe6a25b
revize 3fa206450a
2 změnil soubory, kde provedl 191 přidání a 0 odebrání
+189
Zobrazit soubor
@@ -0,0 +1,189 @@
'use client';
import {
BadgeCheck,
Clock3,
Loader2,
LogOut,
ShieldCheck,
User,
} from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react';
import { clearAuthCookie, getAuthInfoFromBrowserCookie } from '@/lib/auth';
import TVLayout from '@/components/tv/TVLayout';
type AuthInfo = {
username?: string;
role?: 'owner' | 'admin' | 'user';
timestamp?: number;
refreshExpires?: number;
};
function getRoleText(role?: AuthInfo['role']) {
switch (role) {
case 'owner':
return '站长';
case 'admin':
return '管理员';
case 'user':
return '用户';
default:
return '访客';
}
}
function formatDateTime(value?: number) {
if (!value) return '当前会话';
try {
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
}).format(new Date(value));
} catch {
return '当前会话';
}
}
export default function TVMePage() {
const router = useRouter();
const [ready, setReady] = useState(false);
const [authInfo, setAuthInfo] = useState<AuthInfo | null>(null);
const [loggingOut, setLoggingOut] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
const auth = getAuthInfoFromBrowserCookie();
setAuthInfo(auth);
setReady(true);
}, []);
useEffect(() => {
if (ready && !authInfo) {
router.replace('/tv/login?redirect=/tv/me');
}
}, [authInfo, ready, router]);
const username = authInfo?.username || 'default';
const roleText = getRoleText(authInfo?.role || 'user');
const avatarText = useMemo(
() => username.trim().charAt(0).toUpperCase() || 'D',
[username]
);
const handleLogout = async () => {
setLoggingOut(true);
setError('');
try {
await fetch('/api/logout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
} catch {
setError('登出请求失败,已清理本地登录状态。');
} finally {
clearAuthCookie();
router.replace('/tv/login');
}
};
if (!ready || !authInfo) {
return (
<TVLayout>
<section className='mx-auto max-w-5xl rounded-[42px] border border-white/10 bg-slate-950/75 p-12 text-center shadow-2xl shadow-black/60'>
<Loader2 className='mx-auto h-16 w-16 animate-spin text-rose-400' />
<h1 className='mt-6 text-5xl font-black'></h1>
<p className='mt-4 text-2xl text-slate-300'></p>
</section>
</TVLayout>
);
}
return (
<TVLayout>
<section className='mx-auto max-w-6xl overflow-hidden rounded-[42px] border border-white/10 bg-slate-950/75 shadow-2xl shadow-black/60 backdrop-blur-xl'>
<div className='relative p-10 md:p-12'>
<div className='pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_18%_10%,rgba(244,63,94,0.25),transparent_34%),radial-gradient(circle_at_85%_0%,rgba(67,56,202,0.28),transparent_30%)]' />
<div className='relative grid gap-10 lg:grid-cols-[1fr_340px]'>
<div>
<div className='inline-flex items-center gap-3 rounded-full border border-emerald-400/30 bg-emerald-500/15 px-5 py-2 text-xl font-bold text-emerald-200'>
<BadgeCheck className='h-6 w-6' />
</div>
<div className='mt-8 flex items-center gap-7'>
<div className='flex h-28 w-28 shrink-0 items-center justify-center rounded-[32px] bg-gradient-to-br from-rose-500 to-indigo-600 text-6xl font-black text-white shadow-2xl shadow-rose-950/50'>
{avatarText}
</div>
<div>
<h1 className='text-6xl font-black tracking-tight text-white md:text-7xl'>
{username}
</h1>
<p className='mt-3 text-2xl text-slate-300'>
</p>
</div>
</div>
<div className='mt-10 grid gap-4 md:grid-cols-2'>
<div className='rounded-[28px] border border-white/10 bg-white/[0.06] p-6'>
<div className='flex items-center gap-3 text-xl font-bold text-slate-300'>
<ShieldCheck className='h-6 w-6 text-rose-300' />
</div>
<div className='mt-4 text-4xl font-black text-white'>{roleText}</div>
</div>
<div className='rounded-[28px] border border-white/10 bg-white/[0.06] p-6'>
<div className='flex items-center gap-3 text-xl font-bold text-slate-300'>
<Clock3 className='h-6 w-6 text-indigo-300' />
</div>
<div className='mt-4 text-3xl font-black text-white'>
{formatDateTime(authInfo.timestamp)}
</div>
</div>
</div>
</div>
<aside className='flex flex-col justify-between rounded-[34px] border border-white/10 bg-black/35 p-7'>
<div>
<div className='flex h-16 w-16 items-center justify-center rounded-3xl bg-white/10'>
<User className='h-9 w-9 text-rose-300' />
</div>
<h2 className='mt-6 text-4xl font-black'></h2>
<p className='mt-4 text-xl leading-relaxed text-slate-300'>
</p>
</div>
<div className='mt-10'>
{error && (
<p role='alert' className='mb-4 rounded-2xl border border-red-400/40 bg-red-950/50 p-4 text-xl text-red-100'>
{error}
</p>
)}
<button
type='button'
onClick={handleLogout}
disabled={loggingOut}
className='tv-focusable flex w-full cursor-pointer items-center justify-center gap-3 rounded-3xl bg-rose-600 px-7 py-5 text-3xl font-black text-white outline-none transition duration-200 hover:bg-rose-500 focus:ring-4 focus:ring-rose-300 disabled:cursor-not-allowed disabled:opacity-70'
>
{loggingOut ? (
<Loader2 className='h-8 w-8 animate-spin' />
) : (
<LogOut className='h-8 w-8' />
)}
{loggingOut ? '正在登出' : '登出'}
</button>
</div>
</aside>
</div>
</div>
</section>
</TVLayout>
);
}
+2
Zobrazit soubor
@@ -9,6 +9,7 @@ import {
Search,
Sparkles,
Tv,
User,
} from 'lucide-react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
@@ -25,6 +26,7 @@ const navItems = [
{ label: '综艺', href: '/tv/variety', icon: MonitorPlay },
{ label: '直播', href: '/tv/live', icon: Radio },
{ label: '私人影库', href: '/tv/private', icon: Heart },
{ label: '我的', href: '/tv/me', icon: User },
];
export default function TVLayout({