import type { Route } from "./+types/info"; import { treaty } from "@elysiajs/eden"; import type { App } from "@elysia/src"; import { useEffect, useState } from "react"; import { Skeleton } from "@/components/ui/skeleton"; import { TriangleAlert } from "lucide-react"; import { Title } from "@/components/Title"; import { toast } from "sonner"; import { Error } from "@/components/Error"; import { Layout } from "@/components/Layout"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { formatDateTime } from "@/components/SearchResults"; // @ts-expect-error anyway... const app = treaty(import.meta.env.VITE_API_URL!); type SongInfo = Awaited["info"]["get"]>>["data"]; type Snapshots = Awaited["snapshots"]["get"]>>["data"]; type SongInfoError = Awaited["info"]["get"]>>["error"]; type SnapshotsError = Awaited["snapshots"]["get"]>>["error"]; export async function clientLoader({ params }: Route.LoaderArgs) { return { id: params.id }; } const SnapshotsView = ({ snapshots }: { snapshots: Snapshots | null }) => { if (!snapshots) { return ( <> {/*

历史快照

*/} ); } return (

播放: {snapshots[0].views.toLocaleString()},更新于{formatDateTime(new Date(snapshots[0].createdAt))}

{/*

历史快照

{snapshots.map((snapshot: Exclude[number]) => ( ))}
日期 播放量 弹幕数 点赞数 收藏数 硬币数
{new Date(snapshot.createdAt).toLocaleDateString()} {snapshot.views} {snapshot.danmakus} {snapshot.likes} {snapshot.favorites} {snapshot.coins}
*/}
); }; export default function SongInfo({ loaderData }: Route.ComponentProps) { const [data, setData] = useState(null); const [snapshots, setSnapshots] = useState(null); const [error, setError] = useState(null); const [isDialogOpen, setIsDialogOpen] = useState(false); const [songName, setSongName] = useState(""); const getSnapshots = async (aid: number) => { const { data, error } = await app.video({ id: `av${aid}` }).snapshots.get(); if (error) { console.log(error); setError(error); return; } setSnapshots(data); }; const getInfo = async () => { const { data, error } = await app.song({ id: loaderData.id }).info.get(); if (error) { console.log(error); setError(error); return; } setData(data); }; useEffect(() => { getInfo(); }, []); useEffect(() => { if (!data) return; const aid = data.aid; if (!aid) return; getSnapshots(aid); }, [data]); // Update local song name when data changes useEffect(() => { if (data?.name) { setSongName(data.name); } }, [data?.name]); if (!data && !error) { return ( <Skeleton className="mt-6 w-full aspect-video rounded-lg" /> <div className="mt-6 flex justify-between items-baseline"> <Skeleton className="w-60 h-10 rounded-sm" /> <Skeleton className="w-25 h-10 rounded-sm" /> </div> </Layout> ); } if (error?.status === 404) { return ( <div className="w-screen min-h-screen flex items-center justify-center"> <Title title="未找到曲目" /> <div className="max-w-md w-full bg-gray-100 dark:bg-neutral-900 rounded-2xl shadow-lg p-6 flex flex-col gap-4 items-center text-center"> <div className="w-16 h-16 flex items-center justify-center rounded-full bg-red-500 text-white text-3xl"> <TriangleAlert size={34} className="-translate-y-0.5" /> </div> <h1 className="text-3xl font-semibold text-neutral-900 dark:text-neutral-100">无法找到曲目</h1> <a href={`/song/${loaderData.id}/add`} className="text-secondary-foreground"> 点此收录 </a> </div> </div> ); } if (error) { return <Error error={error} />; } const formatDuration = (duration: number) => { return `${Math.floor(duration / 60)}:${(duration % 60).toString().padStart(2, "0")}`; }; const handleSongNameChange = async () => { if (songName.trim() === "") return; const { data, error } = await app.song({ id: loaderData.id }).info.patch( { name: songName }, { headers: { Authorization: `Bearer ${localStorage.getItem("sessionID") || ""}`, }, }, ); setIsDialogOpen(false); // Refresh the data to show the updated name if (error || !data) { toast.error(`无法更新:${error.value.message || "未知错误"}`); } getInfo(); }; return ( <Layout> <Title title={data!.name ? data!.name : "未知歌曲名"} /> {data!.cover && ( <img src={data!.cover} referrerPolicy="no-referrer" className="w-full aspect-video object-cover rounded-lg mt-6" /> )} <div className="mt-6 flex justify-between"> <div className="flex items-center gap-2"> <h1 className="text-4xl font-medium" onDoubleClick={() => setIsDialogOpen(true)}> {data!.name ? data!.name : "未知歌曲名"} </h1> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <DialogContent> <DialogHeader> <DialogTitle>编辑歌曲名称</DialogTitle> </DialogHeader> <div className="space-y-4"> <Input value={songName} onChange={(e) => setSongName(e.target.value)} placeholder="请输入歌曲名称" className="w-full" /> <div className="flex justify-end gap-2"> <Button variant="outline" onClick={() => setIsDialogOpen(false)}> 取消 </Button> <Button onClick={handleSongNameChange}>保存</Button> </div> </div> </DialogContent> </Dialog> </div> <div className="flex flex-col items-end h-10 whitespace-nowrap"> <span className="leading-5 text-neutral-800 dark:text-neutral-200"> {data!.duration ? formatDuration(data!.duration) : "未知时长"} </span> <span className="text-lg leading-5 text-neutral-800 dark:text-neutral-200 font-bold"> {data!.producer ? data!.producer : "未知P主"} </span> </div> </div> <SnapshotsView snapshots={snapshots} /> </Layout> ); }