Vocal-rank/components/video-search.tsx
2025-02-10 09:03:50 +08:00

280 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
interface Video {
video_stat: {
view: number;
like: number;
coin: number;
favorite: number;
reply: number;
share: number;
danmaku: number;
};
video_info: {
uploader_mid: string;
uploader_name: string;
title: string;
pic: string;
pages: number;
timestamp: number;
};
video_id: {
avid: string;
bvid: string;
};
vrank_info: {
vrank_score: number;
rank: string;
rank_code: number;
progress_percentage: number;
};
video_increase: {
view: number;
like: number;
coin: number;
favorite: number;
reply: number;
share: number;
danmaku: number;
};
score_rank: number;
}
function getAchievement(views: number) {
if (views >= 10000000) return { name: "神话曲", next: null, progress: 100 };
if (views >= 1000000)
return {
name: "传说曲",
next: "神话曲",
progress: (views / 10000000) * 100,
};
if (views >= 100000)
return {
name: "殿堂曲",
next: "传说曲",
progress: (views / 1000000) * 100,
};
return { name: "未达成", next: "殿堂曲", progress: (views / 100000) * 100 };
}
export default function VideoSearch() {
const [searchTerm, setSearchTerm] = useState("");
const [videos, setVideos] = useState<Video[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSearch = async () => {
if (!searchTerm) {
setError("请输入搜索内容");
return;
}
setLoading(true);
setError(null);
try {
const response = await fetch(
`https://api.ninevocalrank.top/vocaloid_rank/v1/video/${searchTerm}`
);
const weekly_response = await fetch(
`https://api.ninevocalrank.top/vocaloid_rank/v1/video/${searchTerm}`
);
if (!response.ok || !weekly_response.ok) {
throw new Error("服务器响应错误");
}
const data = await response.json();
const weekly_data = await weekly_response.json();
// 合并 data 和 weekly_data
const combinedData = {
...data,
weekly_data: weekly_data,
};
setVideos([combinedData]);
if (videos.length === 0) {
setError("未找到匹配的视频");
}
} catch (error) {
console.error("搜索视频时出错:", error);
setError("搜索过程中出现错误");
} finally {
setLoading(false);
}
};
return (
<Card className="bg-gradient-to-br from-yellow-100 to-red-100">
<CardHeader>
<CardTitle className="text-2xl font-bold text-primary">
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex space-x-2 mb-4">
<Input
type="text"
placeholder="输入 BV/AV 号 "
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<Button onClick={handleSearch} disabled={loading}>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : "搜索"}
</Button>
</div>
{error && <p className="text-red-500 mb-4">{error}</p>}
{videos.length > 0 ? (
<ul className="space-y-8">
{videos.map((video) => {
const achievement = getAchievement(video.video_stat.view);
return (
<li
key={video.video_id.bvid}
className="border p-6 rounded-lg bg-white shadow-md"
>
<div className="flex flex-col gap-6">
<div className="w-full">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={video.video_info.pic}
className="w-full h-64 object-cover rounded-lg"
alt="视频封面"
></img>
</div>
<div className="w-full">
<h3 className="font-bold text-xl mb-2">
{video.video_info.title}
</h3>
<p className="text-gray-600">
UP主: {video.video_info.uploader_name} (UID:{" "}
{video.video_info.uploader_mid})
</p>
<p className="text-gray-600">
BV号: {video.video_id.bvid}
</p>
<p className="text-gray-600">
AV号: {video.video_id.avid}
</p>
<div className="mt-4 grid grid-cols-2 gap-2">
<p className="text-gray-600">
: {video.video_stat.view.toLocaleString()}
</p>
<p className="text-gray-600">
: {video.video_stat.like.toLocaleString()}
</p>
<p className="text-gray-600">
: {video.video_stat.coin.toLocaleString()}
</p>
<p className="text-gray-600">
: {video.video_stat.favorite.toLocaleString()}
</p>
<p className="text-gray-600">
: {video.video_stat.reply.toLocaleString()}
</p>
<p className="text-gray-600">
: {video.video_stat.share.toLocaleString()}
</p>
<p className="text-gray-600">
: {video.video_stat.danmaku.toLocaleString()}
</p>
</div>
<div className="mt-4 bg-blue-100 p-4 rounded-lg">
<h4 className="font-bold text-lg mb-2"></h4>
{video.vrank_info ? (
<>
<p className="text-gray-700">
:{" "}
{video.vrank_info.vrank_score.toFixed(2)}
</p>
<p className="text-gray-700 font-bold text-xl">
: {video.score_rank}
</p>
</>
) : (
<p className="text-gray-700"></p>
)}
{video.video_increase && (
<>
<h5 className="font-semibold mt-2"></h5>
<div className="grid grid-cols-2 gap-2">
<p className="text-gray-700">
:{" "}
{video.video_increase.view.toLocaleString()}
</p>
<p className="text-gray-700">
:{" "}
{video.video_increase.like.toLocaleString()}
</p>
<p className="text-gray-700">
:{" "}
{video.video_increase.coin.toLocaleString()}
</p>
<p className="text-gray-700">
:{" "}
{video.video_increase.favorite.toLocaleString()}
</p>
<p className="text-gray-700">
:{" "}
{video.video_increase.reply.toLocaleString()}
</p>
<p className="text-gray-700">
:{" "}
{video.video_increase.share.toLocaleString()}
</p>
<p className="text-gray-700">
:{" "}
{video.video_increase.danmaku.toLocaleString()}
</p>
</div>
</>
)}
</div>
<p className="text-gray-600 mt-2">
:{" "}
{new Date(
video.video_info.timestamp * 1000
).toLocaleString("zh-CN")}
</p>
<div className="mt-4">
<p className="font-semibold">
: {achievement.name}
</p>
{achievement.next && (
<div className="mt-2">
<p>
{achievement.next} {" "}
{(achievement.next === "殿堂曲"
? 100000
: achievement.next === "传说曲"
? 1000000
: 10000000) - video.video_stat.view}{" "}
</p>
<div className="w-full bg-gray-200 rounded-full h-2.5 mt-2">
<div
className="bg-blue-600 h-2.5 rounded-full"
style={{ width: `${achievement.progress}%` }}
></div>
</div>
</div>
)}
</div>
</div>
</div>
</li>
);
})}
</ul>
) : (
!loading && !error && <p></p>
)}
</CardContent>
</Card>
);
}