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