first commit
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Sync to CNB / sync (push) Has been cancelled
Delete old workflow runs / del_runs (push) Has been cancelled
Upstream Sync / Sync latest commits from upstream repo (push) Has been cancelled

This commit is contained in:
2026-01-30 16:57:44 +08:00
commit 3d175d75af
119 changed files with 35834 additions and 0 deletions

View File

@@ -0,0 +1,338 @@
"use client";
import { useState, useEffect, useRef } from "react";
import {
CloudDownload,
Copy,
LogIn,
Loader2,
Info,
ExternalLink,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { toast } from "sonner";
interface StatusResponse {
status: "LoginSuccess" | "ScanSuccess" | "LoginFailed" | "QRCodeExpired" | "WaitLogin";
access_token: string;
refresh_token: string;
}
export default function AlipanTvToken() {
const [hasGenerated, setHasGenerated] = useState(false);
const [authUrl, setAuthUrl] = useState("");
const [isLoading, setIsLoading] = useState(true);
const [hasAccessToken, setHasAccessToken] = useState(false);
const [hasRefreshToken, setHasRefreshToken] = useState(false);
const [authorizing, setAuthorizing] = useState(false);
const [isNoticeOpen, setIsNoticeOpen] = useState(false);
const [accessToken, setAccessToken] = useState("");
const [refreshToken, setRefreshToken] = useState("");
const [currentSid, setCurrentSid] = useState("");
const [currentHost, setCurrentHost] = useState("");
const checkTimer = useRef<NodeJS.Timeout | null>(null);
const getCurrentHost = () => {
if (typeof window !== "undefined") {
return `${window.location.protocol}//${window.location.host}`;
}
return "";
};
async function generateAuthUrl() {
try {
setIsLoading(true);
const response = await fetch("/api/alipan-tv-token/generate_qr", {
method: "POST",
});
const data = await response.json();
setCurrentSid(data.sid);
setAuthUrl(`https://www.alipan.com/o/oauth/authorize?sid=${data.sid}`);
} catch {
toast.error("初始化失败,请检查网络");
} finally {
setIsLoading(false);
}
}
function closeNotice() {
setIsNoticeOpen(false);
}
async function checkStatus(sid: string) {
try {
const response = await fetch(`/api/alipan-tv-token/check_status/${sid}`);
const data: StatusResponse = await response.json();
if (data.status === "LoginSuccess") {
setAccessToken(data.access_token);
setRefreshToken(data.refresh_token);
setHasAccessToken(!!data.access_token);
setHasRefreshToken(!!data.refresh_token);
setAuthorizing(false);
toast.success("登录成功");
} else if (data.status === "ScanSuccess") {
checkTimer.current = setTimeout(() => checkStatus(sid), 2000);
} else if (data.status === "LoginFailed") {
setAuthorizing(false);
toast.error("登录失败,请刷新页面重试");
} else if (data.status === "QRCodeExpired") {
setAuthorizing(false);
toast.error("链接过期,请刷新页面重试");
} else {
// WaitLogin
checkTimer.current = setTimeout(() => checkStatus(sid), 2000);
}
} catch (error) {
console.error("检查状态时出错:", error);
toast.error("发生错误,请稍后重试");
}
}
const copyToClipboard = async (text: string, name: string) => {
try {
await navigator.clipboard.writeText(text);
toast.success(`${name} 已复制`);
} catch {
toast.error("复制失败");
}
};
const handleAuth = (url: string) => {
setAuthorizing(true);
window.open(url, "_blank");
if (currentSid) {
if (checkTimer.current) {
clearTimeout(checkTimer.current);
}
checkTimer.current = setTimeout(() => checkStatus(currentSid), 1000);
}
};
useEffect(() => {
setCurrentHost(getCurrentHost());
setIsNoticeOpen(true);
if (!hasGenerated) {
generateAuthUrl();
setHasGenerated(true);
}
return () => {
if (checkTimer.current) {
clearTimeout(checkTimer.current);
}
};
}, [hasGenerated]);
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Page Header */}
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-teal-500 to-teal-600 shadow-lg">
<CloudDownload className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">TV Token</h1>
<p className="text-muted-foreground">
TV端的授权令牌
</p>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
{/* Left: Tokens */}
<div className="space-y-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-base font-medium">访</CardTitle>
<Button
variant="ghost"
size="sm"
className="h-8 gap-1"
disabled={!hasAccessToken}
onClick={() => copyToClipboard(accessToken, "访问令牌")}
>
<Copy className="h-3 w-3" />
</Button>
</CardHeader>
<CardContent>
<Textarea
value={accessToken}
readOnly
rows={4}
placeholder="授权成功后,访问令牌将显示在这里..."
className="font-mono resize-none bg-muted/50 text-xs"
/>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-base font-medium"></CardTitle>
<Button
variant="ghost"
size="sm"
className="h-8 gap-1"
disabled={!hasRefreshToken}
onClick={() => copyToClipboard(refreshToken, "刷新令牌")}
>
<Copy className="h-3 w-3" />
</Button>
</CardHeader>
<CardContent>
<Textarea
value={refreshToken}
readOnly
rows={3}
placeholder="刷新令牌将显示在这里..."
className="font-mono resize-none bg-muted/50 text-xs"
/>
</CardContent>
</Card>
</div>
{/* Right: Auth Action */}
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<LogIn className="h-4 w-4" />
</CardTitle>
</CardHeader>
<CardContent>
{!hasAccessToken && !hasRefreshToken && (
<div className="py-8">
{isLoading ? (
<div className="flex flex-col items-center gap-4 text-muted-foreground">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p>...</p>
</div>
) : (
<Button
size="lg"
className="w-full gap-2 text-lg h-14"
onClick={() => handleAuth(authUrl)}
disabled={authorizing}
>
{authorizing ? (
<>
<Loader2 className="h-5 w-5 animate-spin" />
...
</>
) : (
<>
<LogIn className="h-5 w-5" />
</>
)}
</Button>
)}
</div>
)}
{(hasAccessToken || hasRefreshToken) && (
<div className="py-8 flex flex-col items-center gap-4 text-emerald-600 dark:text-emerald-400">
<div className="h-16 w-16 rounded-full bg-emerald-100 dark:bg-emerald-900/30 flex items-center justify-center">
<CloudDownload className="h-8 w-8" />
</div>
<p className="font-medium"></p>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base font-medium">API </CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<span className="text-sm font-medium">OAuth </span>
<div className="p-3 bg-muted rounded-md break-all text-xs font-mono">
{currentHost}/api/oauth/alipan/token
</div>
</CardContent>
</Card>
</div>
</div>
{/* Info Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span className="text-xl">💡</span> 使
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<h4 className="font-medium text-sm"></h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li>TV版的刷新令牌</li>
<li>TV接口可绕过三方应用权益包的速率限制</li>
<li>SVIP会员才能享受高速下载</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-medium text-sm">使</h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li>"开始授权登录"</li>
<li>使APP扫码</li>
<li></li>
<li>使</li>
</ul>
</div>
</div>
<Alert className="bg-amber-50 dark:bg-amber-950/20 text-amber-900 dark:text-amber-200 border-amber-200 dark:border-amber-800">
<Info className="h-4 w-4" />
<AlertTitle></AlertTitle>
<AlertDescription>
TV接口能绕过三方应用权益包的速率限制SVIP会员才能享受高速下载
</AlertDescription>
</Alert>
</CardContent>
</Card>
<Dialog open={isNoticeOpen} onOpenChange={setIsNoticeOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>使</DialogTitle>
</DialogHeader>
<DialogDescription className="py-4">
TV版
<br /><br />
<strong></strong> TV接口能绕过三方应用权益包的速率限制SVIP
</DialogDescription>
<DialogFooter className="flex-col sm:flex-row gap-2">
<Button
variant="outline"
className="gap-2 text-red-600 hover:text-red-700 hover:bg-red-50 border-red-200"
asChild
>
<a href="https://www.alipan.com/cpx/member?userCode=MjAyNTk2" target="_blank" rel="noopener noreferrer">
<ExternalLink className="h-4 w-4" />
</a>
</Button>
<Button onClick={closeNotice}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}