This commit is contained in:
2025-04-01 19:43:30 +08:00
parent 53822f1087
commit 1b901bda90
14 changed files with 577 additions and 580 deletions

View File

@@ -1,68 +1,52 @@
"use client";
import { useEffect, useRef } from 'react';
import { DeviceAnalytics as DeviceAnalyticsType } from '@/app/api/types';
import { Chart, PieController, ArcElement, Tooltip, Legend } from 'chart.js';
interface CategoryItem {
name: string;
count: number;
percentage: number;
}
interface DeviceAnalyticsProps {
data: DeviceAnalyticsType;
}
function StatCard({ title, items }: { title: string; items: { name: string; count: number; percentage: number }[] }) {
// 安全地格式化数字
const formatNumber = (value: number | string | undefined | null): string => {
if (value === undefined || value === null) return '0';
return typeof value === 'number' ? value.toLocaleString() : String(value);
};
// 安全地格式化百分比
const formatPercent = (value: number | undefined | null): string => {
if (value === undefined || value === null) return '0';
return value.toFixed(1);
};
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">{title}</h3>
export default function DeviceAnalytics({ data }: DeviceAnalyticsProps) {
const renderCategory = (items: CategoryItem[], title: string) => (
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">{title}</h3>
<div className="space-y-4">
{items.map((item, index) => (
<div key={index}>
<div className="flex justify-between text-sm text-gray-600 dark:text-gray-400 mb-1">
<span>{item.name || 'Unknown'}</span>
<span>{formatNumber(item.count)} ({formatPercent(item.percentage)}%)</span>
<div className="flex justify-between text-sm text-gray-600 mb-1">
<span>{item.name}</span>
<span>{item.percentage.toFixed(1)}% ({item.count})</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
className="bg-blue-500 h-2 rounded-full"
style={{ width: `${item.percentage || 0}%` }}
/>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${item.percentage}%` }}
></div>
</div>
</div>
))}
</div>
</div>
);
}
export default function DeviceAnalytics({ data }: DeviceAnalyticsProps) {
// Prepare device types data
const deviceItems = data.deviceTypes.map(item => ({
name: item.type || 'Unknown',
count: item.count,
percentage: item.percentage
}));
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<StatCard
title="Device Types"
items={(data.deviceTypes || []).map(item => ({
name: item.type ? (item.type.charAt(0).toUpperCase() + item.type.slice(1)) : 'Unknown',
count: item.count,
percentage: item.percentage
}))}
/>
<StatCard
title="Browsers"
items={data.browsers || []}
/>
<StatCard
title="Operating Systems"
items={data.operatingSystems || []}
/>
{renderCategory(deviceItems, 'Device Types')}
{renderCategory(data.browsers, 'Browsers')}
{renderCategory(data.operatingSystems, 'Operating Systems')}
</div>
);
}

View File

@@ -1,8 +1,6 @@
"use client";
import { useEffect, useRef } from 'react';
import { GeoData } from '@/app/api/types';
import { Chart, PieController, ArcElement, Tooltip, Legend } from 'chart.js';
interface GeoAnalyticsProps {
data: GeoData[];
@@ -10,57 +8,59 @@ interface GeoAnalyticsProps {
export default function GeoAnalytics({ data }: GeoAnalyticsProps) {
// 安全地格式化数字
const formatNumber = (value: any): string => {
const formatNumber = (value: number | undefined | null): string => {
if (value === undefined || value === null) return '0';
return typeof value === 'number' ? value.toLocaleString() : String(value);
return value.toLocaleString();
};
// 安全地格式化百分比
const formatPercent = (value: any): string => {
const formatPercent = (value: number | undefined | null): string => {
if (value === undefined || value === null) return '0';
return typeof value === 'number' ? value.toFixed(2) : String(value);
return value.toFixed(1);
};
const sortedData = [...data].sort((a, b) => (b.visits || 0) - (a.visits || 0));
return (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Location
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Visits
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Unique Visitors
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Percentage
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
% of Total
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
{data.map((item, index) => (
<tr key={index} className={index % 2 === 0 ? 'bg-white dark:bg-gray-900' : 'bg-gray-50 dark:bg-gray-800'}>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
{item.city ? `${item.city}, ${item.region}, ${item.country}` : item.region ? `${item.region}, ${item.country}` : item.country || item.location || 'Unknown'}
<tbody className="bg-white divide-y divide-gray-200">
{sortedData.map((item, index) => (
<tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{item.location || 'Unknown'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{formatNumber(item.visits)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
{formatNumber(item.uniqueVisitors || item.visitors)}
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{formatNumber(item.visitors)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<div className="flex items-center">
<span className="mr-2">{formatPercent(item.percentage)}%</span>
<div className="w-24 bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div className="w-24 bg-gray-200 rounded-full h-2">
<div
className="bg-blue-500 h-2 rounded-full"
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${item.percentage || 0}%` }}
/>
</div>
<span className="ml-2">{formatPercent(item.percentage)}%</span>
</div>
</td>
</tr>