From 48bd53105c0511b09476188122e82f410f946722 Mon Sep 17 00:00:00 2001 From: alikia2x Date: Sun, 21 Dec 2025 23:05:06 +0800 Subject: [PATCH] fix: add zero-value snapshot at video creation - Adds a "virtual" record with all zeros to ensure the chart starts from the origin. - Fixes various linting issues. --- .idea/biome.xml | 1 + .idea/data_source_mapping.xml | 1 + .idea/prettier.xml | 2 +- package.json | 5 +- packages/core/net/delegate.ts | 12 +- .../app/components/SearchResults.tsx | 8 +- .../app/routes/song/[id]/info/index.tsx | 324 +++--------------- .../app/routes/song/[id]/info/lib.ts | 3 +- .../routes/song/[id]/info/snapshotsView.tsx | 251 ++++++++++++++ .../app/routes/song/[id]/info/views-chart.tsx | 19 +- turbo.json | 15 +- 11 files changed, 337 insertions(+), 304 deletions(-) create mode 100644 packages/temp_frontend/app/routes/song/[id]/info/snapshotsView.tsx diff --git a/.idea/biome.xml b/.idea/biome.xml index f400d0c..c5fed51 100644 --- a/.idea/biome.xml +++ b/.idea/biome.xml @@ -2,6 +2,7 @@ \ No newline at end of file diff --git a/.idea/data_source_mapping.xml b/.idea/data_source_mapping.xml index 7ba958c..73510c6 100644 --- a/.idea/data_source_mapping.xml +++ b/.idea/data_source_mapping.xml @@ -6,5 +6,6 @@ + \ No newline at end of file diff --git a/.idea/prettier.xml b/.idea/prettier.xml index 01f3c56..3a5991f 100644 --- a/.idea/prettier.xml +++ b/.idea/prettier.xml @@ -1,7 +1,7 @@ - \ No newline at end of file diff --git a/package.json b/package.json index 09532a8..fd47115 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,10 @@ "scripts": { "biome:format": "bunx --bun biome format --write", "biome:lint": "bunx --bun biome lint", - "biome:check": "bunx --bun biome check --write" + "dev": "turbo run dev --filter=temp_frontend --filter=backend", + "db": "turbo run drizzle:pull", + "biome": "turbo run biome", + "test": "turbo run test" }, "workspaces": { "packages": [ diff --git a/packages/core/net/delegate.ts b/packages/core/net/delegate.ts index 581147e..849c316 100644 --- a/packages/core/net/delegate.ts +++ b/packages/core/net/delegate.ts @@ -364,8 +364,6 @@ class IPPoolManager { this.isRefreshing = true; try { - logger.debug("Refreshing IP pool", "net", "IPPoolManager.refreshPool"); - const extractedIPs = await this.config.extractor(); const newIPs = extractedIPs.slice(0, this.config.maxPoolSize - this.pool.length); @@ -378,12 +376,6 @@ class IPPoolManager { }; this.pool.push(ipEntry); } - - logger.debug( - `IP pool refreshed. Pool size: ${this.pool.length}`, - "net", - "IPPoolManager.refreshPool" - ); } catch (error) { logger.error(error as Error, "net", "IPPoolManager.refreshPool"); } finally { @@ -477,7 +469,9 @@ export class NetworkDelegate { const boundProxies = new Set(); for (const [_taskName, taskImpl] of Object.entries(this.tasks)) { if (taskImpl.provider === providerName) { - taskImpl.proxies.forEach((p) => boundProxies.add(p)); + taskImpl.proxies.forEach((p) => { + boundProxies.add(p); + }); } } diff --git a/packages/temp_frontend/app/components/SearchResults.tsx b/packages/temp_frontend/app/components/SearchResults.tsx index fdcb3b4..76c117b 100644 --- a/packages/temp_frontend/app/components/SearchResults.tsx +++ b/packages/temp_frontend/app/components/SearchResults.tsx @@ -6,15 +6,16 @@ interface SearchResultsProps { query: string; } -export const formatDateTime = (date: Date): string => { +export const formatDateTime = (date: Date, showYear = true, showSec = false): string => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); // 月份从0开始,补0 const day = String(date.getDate()).padStart(2, "0"); const hour = String(date.getHours()).padStart(2, "0"); const minute = String(date.getMinutes()).padStart(2, "0"); const second = String(date.getSeconds()).padStart(2, "0"); - - return `${year}-${month}-${day} ${hour}:${minute}:${second}`; + const yearStr = showYear ? ` ${year}-` : ""; + const secStr = showSec ? `:${second}` : ""; + return `${yearStr}${month}-${day} ${hour}:${minute}${secStr}`; }; const biliIDSchema = z.union([z.string().regex(/BV1[0-9A-Za-z]{9}/), z.string().regex(/av[0-9]+/)]); @@ -65,6 +66,7 @@ export function SearchResults({ results, query }: SearchResultsProps) {

{results.data.map((result, index) => (
(import.meta.env.VITE_API_URL!); +const app = treaty(import.meta.env.VITE_API_URL || "https://api.projectcvsa.com/"); type SongInfo = Awaited["info"]["get"]>>["data"]; -type EtaInfo = Awaited["eta"]["get"]>>["data"]; +export type EtaInfo = Awaited["eta"]["get"]>>["data"]; export type Snapshots = Awaited< ReturnType["snapshots"]["get"]> >["data"]; @@ -53,6 +40,7 @@ type SnapshotsError = Awaited< >["error"]; type EtaInfoError = Awaited["eta"]["get"]>>["error"]; +// noinspection JSUnusedGlobalSymbols export async function clientLoader({ params }: Route.LoaderArgs) { return { id: params.id }; } @@ -70,217 +58,6 @@ export function addHoursToNow(hours: number): string { return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d.getDate().toString().padStart(2, "0")} ${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`; } -const StatsTable = ({ snapshots }: { snapshots: Snapshots | null }) => { - if (!snapshots || snapshots.length === 0) { - return null; - } - - const tableData: Snapshot[] = snapshots.map((snapshot) => ({ - createdAt: snapshot.createdAt, - views: snapshot.views, - likes: snapshot.likes || 0, - favorites: snapshot.favorites || 0, - coins: snapshot.coins || 0, - danmakus: snapshot.danmakus || 0, - shares: snapshot.shares || 0, - })); - - return ; -}; - -const getMileStoneName = (views: number) => { - if (views < 100000) return "殿堂"; - if (views < 1000000) return "传说"; - return "神话"; -}; - -const SnapshotsView = ({ - snapshots, - etaData, - publishedAt, -}: { - snapshots: Snapshots | null; - etaData: EtaInfo | null; - publishedAt?: string; -}) => { - const [timeRange, setTimeRange] = useState("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 [...snapshots] - .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) - .map((s) => ({ - ...s, - timestamp: new Date(s.createdAt).getTime(), - })); - }, [snapshots]); - - 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; - }, [snapshots, timeRangeHours, timeOffsetHours]); - - const canGoForward = useMemo(() => { - if (!sortedSnapshots || !timeRangeHours || timeRangeHours <= 0 || timeOffsetHours <= 0) - return false; - return true; - }, [snapshots, timeRangeHours, timeOffsetHours]); - - const processedData = useMemo( - () => processSnapshots(sortedSnapshots, timeRangeHours, timeOffsetHours), - [snapshots, timeRangeHours, timeOffsetHours] - ); - - if (!snapshots) { - return ; - } - - if (snapshots.length === 0) { - return ( -
-

暂无数据

-
- ); - } - - 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 ( -
-

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

- {etaData && ( - <> - {etaData!.views <= 10000000 && ( -

- 下一个成就:{getMileStoneName(etaData!.views)} - - {" "} - 预计 {formatHours(etaData!.eta)} 后({addHoursToNow(etaData!.eta)} - )达成 - -

- )} - - {milestoneAchievements.length > 0 && ( -
-

成就达成时间:

- {milestoneAchievements.map((achievement) => ( -

- {achievement.milestoneName}( - {achievement.milestone.toLocaleString()}) -{" "} - {formatDateTime(new Date(achievement.achievedAt))} - {achievement.timeTaken && ` - 用时 ${achievement.timeTaken}`} -

- ))} -
- )} - - )} - - -
-

数据

- - 图表 - 表格 - -
- - -
-
- - - -
- -
-
- - - -
-
- ); -}; - -const MemoizedSnapshotsView = memo(SnapshotsView); - export default function SongInfo({ loaderData }: Route.ComponentProps) { const [songInfo, setData] = useState(null); const [snapshots, setSnapshots] = useState(null); @@ -292,25 +69,25 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) { const [isDeleting, setIsDeleting] = useState(false); const [isSaving, setIsSaving] = useState(false); - const getEta = async () => { + const getEta = useCallback(async () => { const { data, error } = await app.song({ id: loaderData.id }).eta.get(); if (error) { console.log(error); return; } setEtaData(data); - }; + }, [loaderData.id]); - const getSnapshots = async () => { + const getSnapshots = useCallback(async () => { const { data, error } = await app.song({ id: loaderData.id }).snapshots.get(); if (error) { console.log(error); return; } setSnapshots(data); - }; + }, [loaderData.id]); - const getInfo = async () => { + const getInfo = useCallback(async () => { const { data, error } = await app.song({ id: loaderData.id }).info.get(); if (error) { console.log(error); @@ -318,13 +95,13 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) { return; } setData(data); - }; + }, [loaderData.id]); useEffect(() => { - getInfo(); - getSnapshots(); - getEta(); - }, []); + getInfo().then(() => {}); + getSnapshots().then(() => {}); + getEta().then(() => {}); + }, [getEta, getInfo, getSnapshots]); useEffect(() => { if (songInfo?.name) { @@ -368,6 +145,11 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) { return ; } + // Type guard: songInfo is guaranteed to be non-null at this point + if (!songInfo) { + throw new Error("Invariant violation: songInfo should not be null here"); + } + const formatDuration = (duration: number) => { return `${Math.floor(duration / 60)}:${(duration % 60).toString().padStart(2, "0")}`; }; @@ -388,14 +170,14 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) { if (error || !data) { toast.error(`无法更新:${error.value.message || "未知错误"}`); } - getInfo(); + getInfo().then(() => {}); }; const handleDeleteSong = async () => { if (!songInfo) return; setIsDeleting(true); try { - const { data, error } = await app.song({ id: songInfo.id }).delete(undefined, { + const { error } = await app.song({ id: songInfo.id }).delete(undefined, { headers: { Authorization: `Bearer ${localStorage.getItem("sessionID") || ""}`, }, @@ -411,7 +193,7 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) { setTimeout(() => { window.location.href = "/"; }, 1000); - } catch (err) { + } catch { toast.error("删除失败:网络错误"); } finally { setIsDeleting(false); @@ -421,17 +203,18 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) { return ( - - {songInfo!.cover && ( + <Title title={songInfo.name ? songInfo.name : "未知歌曲名"} /> + <When condition={songInfo.cover}> <img - src={songInfo!.cover} + src={songInfo?.cover} + alt="封面图片" referrerPolicy="no-referrer" className="w-full aspect-video object-cover rounded-lg mt-6" /> - )} + </When> <div className="mt-6 flex items-center gap-2"> <h1 className="text-4xl font-medium" onDoubleClick={() => setIsDialogOpen(true)}> - {songInfo!.name ? songInfo!.name : "未知歌曲名"} + {songInfo.name ? songInfo.name : "未知歌曲名"} </h1> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <DialogContent> @@ -459,42 +242,35 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) { </div> <div className="flex justify-between mt-3 stat-num"> <div> - <If condition={songInfo!.aid}> - <Then> - <p> - <span>av{songInfo!.aid}</span> ·{" "} - <span>{av2bv(songInfo!.aid!)}</span> - </p> - </Then> - </If> + <When condition={songInfo.aid}> + <p> + <span>av{songInfo.aid}</span> · <span>{av2bv(songInfo.aid || 0)}</span> + </p> + </When> <p> - <If condition={songInfo!.duration}> - <Then> - <span> - 时长: - {formatDuration(songInfo!.duration!)} - </span> - </Then> - </If> + <When condition={songInfo.duration}> + <span> + 时长: + {formatDuration(songInfo.duration || 0)} + </span> + </When> <span> · </span> - <If condition={songInfo!.publishedAt}> - <Then> - <span> - 发布于 {formatDateTime(new Date(songInfo!.publishedAt!))} - </span> - </Then> - </If> + <When condition={songInfo.publishedAt}> + <span> + 发布于 {formatDateTime(new Date(songInfo.publishedAt || 0))} + </span> + </When> </p> <span> P主: - {songInfo!.producer ? songInfo!.producer : "未知P主"} + {songInfo.producer ? songInfo.producer : "未知P主"} </span> </div> <div className="flex flex-col gap-3"> - {songInfo!.aid && ( + {songInfo.aid && ( <Button className="bg-pink-400"> - <a href={`https://www.bilibili.com/video/${av2bv(songInfo!.aid)}`}> + <a href={`https://www.bilibili.com/video/${av2bv(songInfo.aid)}`}> 哔哩哔哩 </a> </Button> @@ -527,7 +303,7 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) { <MemoizedSnapshotsView snapshots={snapshots} etaData={etaData} - publishedAt={songInfo!.publishedAt || undefined} + publishedAt={songInfo.publishedAt || ""} /> </Layout> ); diff --git a/packages/temp_frontend/app/routes/song/[id]/info/lib.ts b/packages/temp_frontend/app/routes/song/[id]/info/lib.ts index a3f4a3e..f185c2a 100644 --- a/packages/temp_frontend/app/routes/song/[id]/info/lib.ts +++ b/packages/temp_frontend/app/routes/song/[id]/info/lib.ts @@ -14,7 +14,7 @@ const getDataIntervalMins = (interval: number, timeRangeHours?: number) => { return 24 * 60; } else if (interval >= 6 * HOUR) { return 6 * 60; - } else if (interval >= 1 * HOUR) { + } else if (interval >= HOUR) { return 60; } else if (interval >= 15 * MINUTE) { return 15; @@ -175,7 +175,6 @@ export const detectMilestoneAchievements = ( const milestones = [100000, 1000000, 10000000]; const milestoneNames = ["殿堂", "传说", "神话"]; - const achievements: MilestoneAchievement[] = []; const earliestAchievements = new Map<number, MilestoneAchievement>(); diff --git a/packages/temp_frontend/app/routes/song/[id]/info/snapshotsView.tsx b/packages/temp_frontend/app/routes/song/[id]/info/snapshotsView.tsx new file mode 100644 index 0000000..f56ba37 --- /dev/null +++ b/packages/temp_frontend/app/routes/song/[id]/info/snapshotsView.tsx @@ -0,0 +1,251 @@ +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) => ({ + createdAt: snapshot.createdAt, + views: snapshot.views, + likes: snapshot.likes || 0, + favorites: snapshot.favorites || 0, + coins: snapshot.coins || 0, + danmakus: snapshot.danmakus || 0, + shares: snapshot.shares || 0, + })); + + 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 [ + { + id: 0, + createdAt: publishedAt, + views: 0, + coins: 0, + likes: 0, + favorites: 0, + shares: 0, + danmakus: 0, + aid: 0, + replies: 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))} + {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); diff --git a/packages/temp_frontend/app/routes/song/[id]/info/views-chart.tsx b/packages/temp_frontend/app/routes/song/[id]/info/views-chart.tsx index 1d6f29a..c92ead2 100644 --- a/packages/temp_frontend/app/routes/song/[id]/info/views-chart.tsx +++ b/packages/temp_frontend/app/routes/song/[id]/info/views-chart.tsx @@ -2,6 +2,7 @@ import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"; import { useDarkMode } from "usehooks-ts"; +import { formatDateTime } from "@/components/SearchResults"; import { type ChartConfig, ChartContainer, @@ -34,29 +35,21 @@ interface ChartData { views: number; } -function formatDate(dateStr: string, showYear = false): string { - const date = new Date(dateStr); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const day = String(date.getDate()).padStart(2, "0"); - const hours = String(date.getHours()).padStart(2, "0"); - const minutes = String(date.getMinutes()).padStart(2, "0"); - const yearStr = showYear ? ` ${year}-` : ""; - return `${yearStr}${month}-${day} ${hours}:${minutes}`; -} +const formatDate = (dateStr: string, showYear = false) => + formatDateTime(new Date(dateStr), showYear); const formatYAxisLabel = (value: number) => { if (value >= 1000000) { - return (value / 10000).toPrecision(4) + "万"; + return `${(value / 10000).toPrecision(4)}万`; } else if (value >= 10000) { - return (value / 10000).toPrecision(3) + "万"; + return `${(value / 10000).toPrecision(3)}万`; } return value.toLocaleString(); }; export function ViewsChart({ chartData }: { chartData: ChartData[] }) { const { isDarkMode } = useDarkMode(); - if (!chartData || chartData.length === 0) return <></>; + if (!chartData || chartData.length === 0) return; return ( <ChartContainer config={isDarkMode ? chartConfigDark : chartConfigLight} diff --git a/turbo.json b/turbo.json index 9c43e1e..c5da32e 100644 --- a/turbo.json +++ b/turbo.json @@ -9,6 +9,19 @@ "//#biome:lint": {}, "//#lint": { "dependsOn": ["biome:lint"] - } + }, + "dev": { + "persistent": true, + "cache": false + }, + "drizzle:pull": { + "cache": false + }, + "test": {}, + "deploy": { + "cache": false, + "dependsOn": ["temp_frontend#build"] + }, + "build": {} } }