优化扫码登录

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'; 'use client';
import { Loader2, RefreshCw, Smartphone } from 'lucide-react'; import { Loader2, Smartphone } from 'lucide-react';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
@@ -20,7 +20,11 @@ export default function TVLoginPage() {
setStatus('正在生成二维码...'); setStatus('正在生成二维码...');
const res = await fetch('/api/auth/qr/create', { method: 'POST' }); const res = await fetch('/api/auth/qr/create', { method: 'POST' });
const data = await res.json(); 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))); setLeft(Math.max(0, Math.ceil((data.expiresAt - Date.now()) / 1000)));
setStatus('请使用手机扫码登录'); setStatus('请使用手机扫码登录');
}, []); }, []);
@@ -29,9 +33,16 @@ export default function TVLoginPage() {
useEffect(() => { useEffect(() => {
if (!qr) return; 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); return () => window.clearInterval(timer);
}, [qr]); }, [create, qr]);
useEffect(() => { useEffect(() => {
if (!qr) return; if (!qr) return;
@@ -53,21 +64,17 @@ export default function TVLoginPage() {
const qrImg = qr ? `/api/auth/qr/image?data=${encodeURIComponent(qr.qrUrl)}` : ''; const qrImg = qr ? `/api/auth/qr/image?data=${encodeURIComponent(qr.qrUrl)}` : '';
return ( 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'> <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='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> <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> <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-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> <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>
<div className='rounded-[36px] border border-white/10 bg-white p-7 text-center text-black shadow-2xl shadow-black/60'> <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>} {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-5 text-2xl font-black'> {left} </div>
<div className='mt-2 break-all text-sm text-slate-500'>{qr?.qrUrl}</div>
</div> </div>
</section> </section>
</TVLayout> </TVLayout>
+35 -27
View File
@@ -27,38 +27,46 @@ const navItems = [
{ label: '私人影库', href: '/tv/private', icon: Heart }, { 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(); const pathname = usePathname();
return ( return (
<main className='min-h-screen overflow-x-hidden bg-black text-slate-50'> <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%)]' /> <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'> {showNav && (
<nav className='flex items-center justify-center gap-3 overflow-x-auto overscroll-x-contain px-4 py-3 [scrollbar-width:none]'> <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'>
{navItems.map((item) => { <nav className='flex items-center justify-center gap-3 overflow-x-auto overscroll-x-contain px-4 py-3 [scrollbar-width:none]'>
const active = {navItems.map((item) => {
item.href === '/tv' const active =
? pathname === '/tv' item.href === '/tv'
: pathname === item.href || pathname.startsWith(`${item.href}/`); ? pathname === '/tv'
const Icon = item.icon; : pathname === item.href || pathname.startsWith(`${item.href}/`);
return ( const Icon = item.icon;
<Link return (
key={item.href} <Link
href={item.href} key={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 ${ href={item.href}
active 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 ${
? 'bg-rose-600 text-white shadow-lg shadow-rose-950/40' active
: 'bg-white/5 text-slate-300 hover:bg-white/10 hover:text-white focus:bg-white/12' ? '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> <Icon className='h-6 w-6' />
</Link> <span>{item.label}</span>
); </Link>
})} );
</nav> })}
</header> </nav>
<div className='relative z-10 px-8 pb-16 pt-32'>{children}</div> </header>
)}
<div className={`relative z-10 px-8 pb-16 ${showNav ? 'pt-32' : 'pt-16'}`}>{children}</div>
<TVVirtualRemote /> <TVVirtualRemote />
</main> </main>
); );