diff --git a/src/components/CustomTabs.tsx b/src/components/CustomTabs.tsx index 555578a..2db8162 100644 --- a/src/components/CustomTabs.tsx +++ b/src/components/CustomTabs.tsx @@ -22,6 +22,10 @@ const presetTabs = [ name: '版本列表', key: 'versions', }, + { + name: '下载趋势', + key: 'trends', + }, ]; export default function CustomTabs({ diff --git a/src/components/RecentDownloads.tsx b/src/components/RecentDownloads.tsx index 1964add..6e1cfe0 100644 --- a/src/components/RecentDownloads.tsx +++ b/src/components/RecentDownloads.tsx @@ -1,17 +1,26 @@ -import { useRecentDownloads } from '@/hooks/useRecentDownloads'; +import { useRecentDownloads, useTotalDownloads } from '@/hooks/useRecentDownloads'; import React from 'react'; import { Empty } from 'antd'; - import { Line } from 'react-chartjs-2'; import "chart.js/auto"; -import { scales } from 'chart.js/auto'; - type RecentDownloadsContentProps = { pkgName: string; version: string; }; +type TotalDownloadsProps = { + pkgNameList: string[]; +}; + +const COLOR_LIST = [ + 'rgb(53, 162, 235, 0.7)', + 'rgb(255, 99, 132, 0.7)', + 'rgb(255, 205, 86, 0.7)', + 'rgb(75, 192, 192, 0.7)', + 'rgb(153, 102, 255, 0.7)', +]; + export function RecentDownloads({ pkgName, version }: RecentDownloadsContentProps) { const { data: res, isLoading } = useRecentDownloads(pkgName, version); if (isLoading || !res) { @@ -54,3 +63,83 @@ export function RecentDownloads({ pkgName, version }: RecentDownloadsContentProp /> ); } + +export function TotalDownloads({ pkgNameList }: TotalDownloadsProps) { + // 通过 swr 来进行缓存,由于 hook 限制,不能直接 map 循环 + // 另一种方式是在 useTotalDownloads 中自行维护 cache 逻辑 + // 由于希望添加 pkgName 时页面不额外刷新,先采用这种方式 + const [pkg1, pkg2, pkg3, pkg4, pkg5] = pkgNameList; + const {data: pkg1Data} = useTotalDownloads(pkg1); + const {data: pkg2Data} = useTotalDownloads(pkg2); + const {data: pkg3Data} = useTotalDownloads(pkg3); + const {data: pkg4Data} = useTotalDownloads(pkg4); + const {data: pkg5Data} = useTotalDownloads(pkg5); + + const res = [pkg1Data, pkg2Data, pkg3Data, pkg4Data, pkg5Data]; + + if (!res.find(_ => _?.downloads)) { + return ; + } + + return ( + _?.downloads)!.downloads?.map((_) => _.day), + datasets: res.filter(_ => _?.downloads).map((_, index) => { + return { + fill: false, + label: pkgNameList[res.indexOf(_)], + data: _!.downloads.map((_) => _.downloads), + borderColor: COLOR_LIST[index], + // 会影响到 label 展示,虽然 fill false 也一并添加 + backgroundColor: COLOR_LIST[index], + } + }) + }} + /> + ); +} diff --git a/src/hooks/useRecentDownloads.ts b/src/hooks/useRecentDownloads.ts index 7cde98c..4afa4e9 100644 --- a/src/hooks/useRecentDownloads.ts +++ b/src/hooks/useRecentDownloads.ts @@ -3,6 +3,7 @@ import dayjs from 'dayjs'; import useSwr from 'swr'; const DEFAULT_RANGE = 7; +const INIT_YEAR = 2020; type DownloadRes = { downloads: { day: string; downloads: number; }[]; @@ -20,6 +21,19 @@ function getUrl(pkgName: string, range: number) { return `${REGISTRY}/downloads/range/${lastWeekStr}:${todayStr}/${pkgName}`; }; +function getTotalUrl(pkgName: string) { + const today = dayjs(); + const todayStr = today.format('YYYY-MM-DD'); + const years = today.year() - INIT_YEAR + 1; + return new Array(years).fill(0).map((_, index) => { + const year = INIT_YEAR + index; + if (year === today.year()) { + return `${REGISTRY}/downloads/range/${year}-01-01:${todayStr}/${pkgName}`; + } + return `${REGISTRY}/downloads/range/${INIT_YEAR + index}-01-01:${INIT_YEAR + index}-12-31/${pkgName}`; + }); +}; + function normalizeRes(res: DownloadRes, version: string, range: number): DownloadRes { // 根据 range,获取最近 range 天的数据 const downloads = res.downloads.slice(-range); @@ -47,3 +61,12 @@ export const useRecentDownloads = (pkgName: string, version: string, range: numb .then(res => normalizeRes(res, version, range)); }); }; + +export const useTotalDownloads = (pkgName: string) => { + return useSwr(pkgName ? `total_downloads: ${pkgName}` : null, async () => { + const res = await Promise.all(getTotalUrl(pkgName).map((url) => fetch(url).then((res) => res.json()))); + return { downloads: res.reduce((acc, cur) => acc.concat(cur.downloads), []), versions: {} }; + }, { + refreshInterval: 0, + }); +}; diff --git a/src/pages/package/[...slug]/index.tsx b/src/pages/package/[...slug]/index.tsx index a120ef0..89efeca 100644 --- a/src/pages/package/[...slug]/index.tsx +++ b/src/pages/package/[...slug]/index.tsx @@ -2,6 +2,7 @@ import { ThemeProvider as _ThemeProvider } from 'antd-style'; import PageHome from '@/slugs/home'; import PageFiles from '@/slugs/files'; import PageVersions from '@/slugs/versions'; +import PageTrends from '@/slugs/trends'; import PageDeps from '@/slugs/deps'; import 'antd/dist/reset.css'; import CustomTabs from '@/components/CustomTabs'; @@ -65,6 +66,7 @@ const PageMap: Record JSX.Element> = { deps: PageDeps, files: PageFiles, versions: PageVersions, + trends: PageTrends, } as const; // 由于路由不支持 @scope 可选参数 diff --git a/src/slugs/trends/index.tsx b/src/slugs/trends/index.tsx new file mode 100644 index 0000000..b9c8601 --- /dev/null +++ b/src/slugs/trends/index.tsx @@ -0,0 +1,60 @@ +'use client'; +import SizeContainer from '@/components/SizeContainer'; +import { Card, Select, Typography } from 'antd'; +import React, { useState } from 'react'; +import { PageProps } from '@/pages/package/[...slug]'; +import { TotalDownloads } from '@/components/RecentDownloads'; +import { useCachedSearch } from '@/hooks/useSearch'; +import { DownOutlined } from '@ant-design/icons'; + +const MAX_COUNT = 5; + +export default function Trends({ manifest: pkg, additionalInfo: needSync, version }: PageProps) { + const [search, setSearch] = useState(''); + const [pkgs, setPkgs] = useState([pkg.name]); + const { data: searchResult, isLoading } = useCachedSearch({ + keyword: search, + page: 1, + }); + + const suffix = ( + <> + + {pkgs.length} / {MAX_COUNT} + + + + ); + + return ( + <> + +