优化扫码登录

This commit is contained in:
mtvpls
2026-05-30 21:18:41 +08:00
parent 56d0de288f
commit 8d0e0fb779
2 changed files with 51 additions and 36 deletions
+16 -9
View File
@@ -1,6 +1,6 @@
'use client';
import { Loader2, RefreshCw, Smartphone } from 'lucide-react';
import { Loader2, Smartphone } from 'lucide-react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';
@@ -20,7 +20,11 @@ export default function TVLoginPage() {
setStatus('正在生成二维码...');
const res = await fetch('/api/auth/qr/create', { method: 'POST' });
const data = await res.json();
setQr(data);
const qrUrl =
typeof window === 'undefined'
? data.qrUrl
: `${window.location.origin}/qr-login?token=${encodeURIComponent(data.token)}`;
setQr({ ...data, qrUrl });
setLeft(Math.max(0, Math.ceil((data.expiresAt - Date.now()) / 1000)));
setStatus('请使用手机扫码登录');
}, []);
@@ -29,9 +33,16 @@ export default function TVLoginPage() {
useEffect(() => {
if (!qr) return;
const timer = window.setInterval(() => setLeft(Math.max(0, Math.ceil((qr.expiresAt - Date.now()) / 1000))), 1000);
const timer = window.setInterval(() => {
const remaining = Math.max(0, Math.ceil((qr.expiresAt - Date.now()) / 1000));
setLeft(remaining);
if (remaining <= 0) {
window.clearInterval(timer);
create();
}
}, 1000);
return () => window.clearInterval(timer);
}, [qr]);
}, [create, qr]);
useEffect(() => {
if (!qr) return;
@@ -53,21 +64,17 @@ export default function TVLoginPage() {
const qrImg = qr ? `/api/auth/qr/image?data=${encodeURIComponent(qr.qrUrl)}` : '';
return (
<TVLayout>
<TVLayout showNav={false}>
<section className='mx-auto grid max-w-6xl grid-cols-[1fr_430px] gap-10 rounded-[42px] border border-white/10 bg-slate-950/75 p-12 shadow-2xl shadow-black/60'>
<div className='flex flex-col justify-center'>
<div className='inline-flex w-fit items-center gap-3 rounded-full bg-rose-600 px-5 py-2 text-xl font-bold text-white'><Smartphone className='h-6 w-6' /> · </div>
<h1 className='mt-7 text-7xl font-black tracking-tight'></h1>
<p className='mt-6 max-w-2xl text-3xl leading-relaxed text-slate-300'></p>
<p className='mt-8 text-2xl font-bold text-rose-300'>{status}</p>
<button onClick={create} className='mt-10 flex w-fit cursor-pointer items-center gap-3 rounded-3xl bg-white px-8 py-5 text-2xl font-black text-black outline-none transition hover:bg-slate-200 focus-visible:ring-4 focus-visible:ring-rose-500/70'>
<RefreshCw className='h-7 w-7' />
</button>
</div>
<div className='rounded-[36px] border border-white/10 bg-white p-7 text-center text-black shadow-2xl shadow-black/60'>
{qrImg ? <img src={qrImg} alt='扫码登录二维码' className='mx-auto h-[360px] w-[360px]' /> : <div className='flex h-[360px] items-center justify-center'><Loader2 className='h-12 w-12 animate-spin' /></div>}
<div className='mt-5 text-2xl font-black'> {left} </div>
<div className='mt-2 break-all text-sm text-slate-500'>{qr?.qrUrl}</div>
</div>
</section>
</TVLayout>
+35 -27
View File
@@ -27,38 +27,46 @@ const navItems = [
{ label: '私人影库', href: '/tv/private', icon: Heart },
];
export default function TVLayout({ children }: { children: ReactNode }) {
export default function TVLayout({
children,
showNav = true,
}: {
children: ReactNode;
showNav?: boolean;
}) {
const pathname = usePathname();
return (
<main className='min-h-screen overflow-x-hidden bg-black text-slate-50'>
<div className='fixed inset-0 pointer-events-none bg-[radial-gradient(circle_at_20%_0%,rgba(225,29,72,0.22),transparent_34%),radial-gradient(circle_at_90%_10%,rgba(79,70,229,0.24),transparent_30%),linear-gradient(180deg,#05050b_0%,#000_55%)]' />
<header className='fixed left-6 right-6 top-5 z-40 rounded-[28px] border border-white/10 bg-slate-950/78 px-5 py-3 shadow-2xl shadow-black/60 backdrop-blur-xl'>
<nav className='flex items-center justify-center gap-3 overflow-x-auto overscroll-x-contain px-4 py-3 [scrollbar-width:none]'>
{navItems.map((item) => {
const active =
item.href === '/tv'
? pathname === '/tv'
: pathname === item.href || pathname.startsWith(`${item.href}/`);
const Icon = item.icon;
return (
<Link
key={item.href}
href={item.href}
className={`group flex shrink-0 cursor-pointer items-center gap-2 rounded-2xl px-5 py-3 text-xl font-semibold outline-none transition duration-200 tv-focusable ${
active
? 'bg-rose-600 text-white shadow-lg shadow-rose-950/40'
: 'bg-white/5 text-slate-300 hover:bg-white/10 hover:text-white focus:bg-white/12'
}`}
>
<Icon className='h-6 w-6' />
<span>{item.label}</span>
</Link>
);
})}
</nav>
</header>
<div className='relative z-10 px-8 pb-16 pt-32'>{children}</div>
{showNav && (
<header className='fixed left-6 right-6 top-5 z-40 rounded-[28px] border border-white/10 bg-slate-950/78 px-5 py-3 shadow-2xl shadow-black/60 backdrop-blur-xl'>
<nav className='flex items-center justify-center gap-3 overflow-x-auto overscroll-x-contain px-4 py-3 [scrollbar-width:none]'>
{navItems.map((item) => {
const active =
item.href === '/tv'
? pathname === '/tv'
: pathname === item.href || pathname.startsWith(`${item.href}/`);
const Icon = item.icon;
return (
<Link
key={item.href}
href={item.href}
className={`group flex shrink-0 cursor-pointer items-center gap-2 rounded-2xl px-5 py-3 text-xl font-semibold outline-none transition duration-200 tv-focusable ${
active
? 'bg-rose-600 text-white shadow-lg shadow-rose-950/40'
: 'bg-white/5 text-slate-300 hover:bg-white/10 hover:text-white focus:bg-white/12'
}`}
>
<Icon className='h-6 w-6' />
<span>{item.label}</span>
</Link>
);
})}
</nav>
</header>
)}
<div className={`relative z-10 px-8 pb-16 ${showNav ? 'pt-32' : 'pt-16'}`}>{children}</div>
<TVVirtualRemote />
</main>
);