dashboard page good
This commit is contained in:
@@ -7,6 +7,18 @@ interface DeviceAnalyticsProps {
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -14,13 +26,13 @@ function StatCard({ title, items }: { title: string; items: { name: string; coun
|
||||
{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}</span>
|
||||
<span>{item.count.toLocaleString()} ({item.percentage.toFixed(1)}%)</span>
|
||||
<span>{item.name || 'Unknown'}</span>
|
||||
<span>{formatNumber(item.count)} ({formatPercent(item.percentage)}%)</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}%` }}
|
||||
style={{ width: `${item.percentage || 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,19 +47,19 @@ export default function DeviceAnalytics({ data }: DeviceAnalyticsProps) {
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<StatCard
|
||||
title="Device Types"
|
||||
items={data.deviceTypes.map(item => ({
|
||||
name: item.type.charAt(0).toUpperCase() + item.type.slice(1),
|
||||
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}
|
||||
items={data.browsers || []}
|
||||
/>
|
||||
<StatCard
|
||||
title="Operating Systems"
|
||||
items={data.operatingSystems}
|
||||
items={data.operatingSystems || []}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,18 @@ interface GeoAnalyticsProps {
|
||||
}
|
||||
|
||||
export default function GeoAnalytics({ data }: GeoAnalyticsProps) {
|
||||
// 安全地格式化数字
|
||||
const formatNumber = (value: any): string => {
|
||||
if (value === undefined || value === null) return '0';
|
||||
return typeof value === 'number' ? value.toLocaleString() : String(value);
|
||||
};
|
||||
|
||||
// 安全地格式化百分比
|
||||
const formatPercent = (value: any): string => {
|
||||
if (value === undefined || value === null) return '0';
|
||||
return typeof value === 'number' ? value.toFixed(2) : String(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
@@ -30,21 +42,21 @@ export default function GeoAnalytics({ data }: GeoAnalyticsProps) {
|
||||
{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.city ? `${item.city}, ${item.region}, ${item.country}` : item.region ? `${item.region}, ${item.country}` : item.country || item.location || 'Unknown'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{item.visits.toLocaleString()}
|
||||
{formatNumber(item.visits)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{item.uniqueVisitors.toLocaleString()}
|
||||
{formatNumber(item.uniqueVisitors || item.visitors)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">{item.percentage.toFixed(2)}%</span>
|
||||
<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="bg-blue-500 h-2 rounded-full"
|
||||
style={{ width: `${item.percentage}%` }}
|
||||
style={{ width: `${item.percentage || 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user