1
0
cvsa/packages/temp_frontend/app/routes/song/[id]/info/snapshotsView.tsx
2025-12-31 21:18:27 +08:00

252 lines
6.9 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.

import { HOUR } from "@core/lib";
import { memo, useMemo, useState } from "react";
import { When } from "react-if";
import { formatDateTime } from "@/components/SearchResults";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { columns, type Snapshot } from "@/routes/song/[id]/info/columns";
import { DataTable } from "@/routes/song/[id]/info/data-table";
import {
addHoursToNow,
type EtaInfo,
formatHours,
type Snapshots,
} from "@/routes/song/[id]/info/index";
import { detectMilestoneAchievements, processSnapshots } from "@/routes/song/[id]/info/lib";
import { ViewsChart } from "@/routes/song/[id]/info/views-chart";
const StatsTable = ({ snapshots }: { snapshots: Snapshots | null }) => {
if (!snapshots || snapshots.length === 0) {
return null;
}
const tableData: Snapshot[] = snapshots.map((snapshot) => ({
coins: snapshot.coins || 0,
createdAt: snapshot.createdAt,
danmakus: snapshot.danmakus || 0,
favorites: snapshot.favorites || 0,
likes: snapshot.likes || 0,
shares: snapshot.shares || 0,
views: snapshot.views,
}));
return <DataTable columns={columns} data={tableData} />;
};
const getMileStoneName = (views: number) => {
if (views < 100000) return "殿堂";
if (views < 1000000) return "传说";
return "神话";
};
export const SnapshotsView = ({
snapshots,
etaData,
publishedAt,
}: {
snapshots: Snapshots | null;
etaData: EtaInfo | null;
publishedAt: string;
}) => {
const [timeRange, setTimeRange] = useState<string>("7d");
const [timeOffsetHours, setTimeOffsetHours] = useState(0);
// Calculate time range in hours
const timeRangeHours = useMemo(() => {
switch (timeRange) {
case "6h":
return 6;
case "24h":
return 24;
case "7d":
return 7 * 24;
case "14d":
return 14 * 24;
case "30d":
return 30 * 24;
case "90d":
return 90 * 24;
case "365d":
return 365 * 24;
default:
return undefined; // "all"
}
}, [timeRange]);
const sortedSnapshots = useMemo(() => {
if (!snapshots) return null;
return [
{
aid: 0,
coins: 0,
createdAt: publishedAt,
danmakus: 0,
favorites: 0,
id: 0,
likes: 0,
replies: 0,
shares: 0,
views: 0,
},
...snapshots,
]
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
.map((s) => ({
...s,
timestamp: new Date(s.createdAt).getTime(),
}));
}, [snapshots, publishedAt]);
const canGoBack = useMemo(() => {
if (!sortedSnapshots || !timeRangeHours || timeRangeHours <= 0) return false;
const oldestTimestamp = sortedSnapshots[0].timestamp;
const newestTimestamp = sortedSnapshots[sortedSnapshots.length - 1].timestamp;
const timeDiff = newestTimestamp - oldestTimestamp;
return timeOffsetHours * HOUR + timeRangeHours * HOUR < timeDiff;
}, [timeRangeHours, timeOffsetHours, sortedSnapshots]);
const canGoForward = useMemo(() => {
return !(
!sortedSnapshots ||
!timeRangeHours ||
timeRangeHours <= 0 ||
timeOffsetHours <= 0
);
}, [timeRangeHours, timeOffsetHours, sortedSnapshots]);
const processedData = useMemo(
() => processSnapshots(sortedSnapshots, timeRangeHours, timeOffsetHours),
[timeRangeHours, timeOffsetHours, sortedSnapshots]
);
if (!snapshots) {
return <Skeleton className="w-full h-50 rounded-lg mt-4" />;
}
if (snapshots.length === 0) {
return (
<div className="mt-4">
<p></p>
</div>
);
}
const milestoneAchievements = detectMilestoneAchievements(snapshots, publishedAt);
const handleBack = () => {
if (timeRangeHours && timeRangeHours > 0) {
setTimeOffsetHours((prev) => prev + timeRangeHours);
}
};
const handleForward = () => {
if (timeRangeHours && timeRangeHours > 0) {
setTimeOffsetHours((prev) => Math.max(0, prev - timeRangeHours));
}
};
return (
<div className="mt-4 stat-num">
<p>
: {snapshots[0].views.toLocaleString()}
<span className="text-secondary-foreground">
{" "}
{formatDateTime(new Date(snapshots[0].createdAt))}
</span>
</p>
<When condition={etaData != null}>
<When condition={etaData?.views && etaData.views <= 10000000}>
<p>
{getMileStoneName(etaData?.views || 0)}
<span className="text-secondary-foreground">
{" "}
{formatHours(etaData?.eta || 0)}
{addHoursToNow(etaData?.eta || 0)}
</span>
</p>
</When>
<When condition={milestoneAchievements.length > 0}>
<div className="mt-2">
<p className="text-sm text-secondary-foreground"></p>
{milestoneAchievements.map((achievement) => (
<p
key={achievement.milestone}
className="text-sm text-secondary-foreground ml-2"
>
{achievement.milestoneName}
{achievement.milestone.toLocaleString()} -{" "}
{formatDateTime(new Date(achievement.achievedAt), true, false)}
{achievement.timeTaken && ` - 用时 ${achievement.timeTaken}`}
</p>
))}
</div>
</When>
</When>
<Tabs defaultValue="chart" className="mt-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-medium"></h2>
<TabsList>
<TabsTrigger value="chart"></TabsTrigger>
<TabsTrigger value="table"></TabsTrigger>
</TabsList>
</div>
<TabsContent value="chart">
<div className="flex flex-col gap-2">
<div className="flex justify-between items-center">
<Button
variant="outline"
size="sm"
onClick={handleBack}
disabled={!canGoBack}
>
</Button>
<Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger className="w-32">
<SelectValue placeholder="时间范围" />
</SelectTrigger>
<SelectContent>
<SelectItem value="6h">6</SelectItem>
<SelectItem value="24h">24</SelectItem>
<SelectItem value="7d">7</SelectItem>
<SelectItem value="14d">14</SelectItem>
<SelectItem value="30d">30</SelectItem>
<SelectItem value="90d">90</SelectItem>
<SelectItem value="365d">1</SelectItem>
<SelectItem value="all"></SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
onClick={handleForward}
disabled={!canGoForward}
>
</Button>
</div>
<ViewsChart chartData={processedData} />
</div>
</TabsContent>
<TabsContent value="table">
<StatsTable snapshots={snapshots} />
</TabsContent>
</Tabs>
</div>
);
};
export const MemoizedSnapshotsView = memo(SnapshotsView);