diff --git a/app/(app)/analytics/devices/page.tsx b/app/(app)/analytics/devices/page.tsx deleted file mode 100644 index 4f8ab74..0000000 --- a/app/(app)/analytics/devices/page.tsx +++ /dev/null @@ -1,308 +0,0 @@ -"use client"; - -import { useState, useEffect, useRef } from 'react'; -import { DeviceAnalytics } from '@/app/api/types'; -import { Chart, PieController, ArcElement, Tooltip, Legend, CategoryScale, LinearScale } from 'chart.js'; - -// 注册Chart.js组件 -Chart.register(PieController, ArcElement, Tooltip, Legend, CategoryScale, LinearScale); - -export default function DeviceAnalyticsPage() { - const [deviceData, setDeviceData] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [dateRange, setDateRange] = useState({ - from: new Date('2024-02-01'), - to: new Date('2025-03-05') - }); - - // 创建图表引用 - const deviceTypesChartRef = useRef(null); - const browsersChartRef = useRef(null); - const osChartRef = useRef(null); - - // 图表实例引用 - const deviceTypesChartInstance = useRef(null); - const browsersChartInstance = useRef(null); - const osChartInstance = useRef(null); - - // 颜色配置 - const COLORS = { - deviceTypes: ['rgba(59, 130, 246, 0.8)', 'rgba(96, 165, 250, 0.8)', 'rgba(147, 197, 253, 0.8)', 'rgba(191, 219, 254, 0.8)', 'rgba(219, 234, 254, 0.8)'], - browsers: ['rgba(16, 185, 129, 0.8)', 'rgba(52, 211, 153, 0.8)', 'rgba(110, 231, 183, 0.8)', 'rgba(167, 243, 208, 0.8)', 'rgba(209, 250, 229, 0.8)'], - os: ['rgba(239, 68, 68, 0.8)', 'rgba(248, 113, 113, 0.8)', 'rgba(252, 165, 165, 0.8)', 'rgba(254, 202, 202, 0.8)', 'rgba(254, 226, 226, 0.8)'] - }; - - useEffect(() => { - const fetchDeviceData = async () => { - try { - setIsLoading(true); - setError(null); - - const response = await fetch(`/api/events/devices?startTime=${dateRange.from.toISOString().split('T')[0]}T00:00:00Z&endTime=${dateRange.to.toISOString().split('T')[0]}T23:59:59Z`); - if (!response.ok) throw new Error('Failed to fetch device data'); - - const data = await response.json(); - setDeviceData(data.data); - } catch (err) { - setError(err instanceof Error ? err.message : 'An error occurred'); - } finally { - setIsLoading(false); - } - }; - - fetchDeviceData(); - }, [dateRange]); - - // 初始化和更新图表 - useEffect(() => { - if (!deviceData || isLoading) return; - - // 销毁旧的图表实例 - if (deviceTypesChartInstance.current) { - deviceTypesChartInstance.current.destroy(); - } - if (browsersChartInstance.current) { - browsersChartInstance.current.destroy(); - } - if (osChartInstance.current) { - osChartInstance.current.destroy(); - } - - // 创建设备类型图表 - if (deviceTypesChartRef.current && deviceData.deviceTypes.length > 0) { - const ctx = deviceTypesChartRef.current.getContext('2d'); - if (ctx) { - deviceTypesChartInstance.current = new Chart(ctx, { - type: 'pie', - data: { - labels: deviceData.deviceTypes.map(item => item.type), - datasets: [{ - data: deviceData.deviceTypes.map(item => item.count), - backgroundColor: COLORS.deviceTypes, - borderColor: COLORS.deviceTypes.map(color => color.replace('0.8', '1')), - borderWidth: 1 - }] - }, - options: { - responsive: true, - plugins: { - legend: { - position: 'bottom', - labels: { - color: 'currentColor' - } - }, - tooltip: { - callbacks: { - label: function(context) { - const label = context.label || ''; - const value = context.raw as number; - const total = (context.chart.data.datasets[0].data as number[]).reduce((a, b) => (a as number) + (b as number), 0); - const percentage = Math.round((value * 100) / total); - return `${label}: ${value} (${percentage}%)`; - } - } - } - } - } - }); - } - } - - // 创建浏览器图表 - if (browsersChartRef.current && deviceData.browsers.length > 0) { - const ctx = browsersChartRef.current.getContext('2d'); - if (ctx) { - browsersChartInstance.current = new Chart(ctx, { - type: 'pie', - data: { - labels: deviceData.browsers.map(item => item.name), - datasets: [{ - data: deviceData.browsers.map(item => item.count), - backgroundColor: COLORS.browsers, - borderColor: COLORS.browsers.map(color => color.replace('0.8', '1')), - borderWidth: 1 - }] - }, - options: { - responsive: true, - plugins: { - legend: { - position: 'bottom', - labels: { - color: 'currentColor' - } - }, - tooltip: { - callbacks: { - label: function(context) { - const label = context.label || ''; - const value = context.raw as number; - const total = (context.chart.data.datasets[0].data as number[]).reduce((a, b) => (a as number) + (b as number), 0); - const percentage = Math.round((value * 100) / total); - return `${label}: ${value} (${percentage}%)`; - } - } - } - } - } - }); - } - } - - // 创建操作系统图表 - if (osChartRef.current && deviceData.operatingSystems.length > 0) { - const ctx = osChartRef.current.getContext('2d'); - if (ctx) { - osChartInstance.current = new Chart(ctx, { - type: 'pie', - data: { - labels: deviceData.operatingSystems.map(item => item.name), - datasets: [{ - data: deviceData.operatingSystems.map(item => item.count), - backgroundColor: COLORS.os, - borderColor: COLORS.os.map(color => color.replace('0.8', '1')), - borderWidth: 1 - }] - }, - options: { - responsive: true, - plugins: { - legend: { - position: 'bottom', - labels: { - color: 'currentColor' - } - }, - tooltip: { - callbacks: { - label: function(context) { - const label = context.label || ''; - const value = context.raw as number; - const total = (context.chart.data.datasets[0].data as number[]).reduce((a, b) => (a as number) + (b as number), 0); - const percentage = Math.round((value * 100) / total); - return `${label}: ${value} (${percentage}%)`; - } - } - } - } - } - }); - } - } - - // 清理函数 - return () => { - if (deviceTypesChartInstance.current) { - deviceTypesChartInstance.current.destroy(); - } - if (browsersChartInstance.current) { - browsersChartInstance.current.destroy(); - } - if (osChartInstance.current) { - osChartInstance.current.destroy(); - } - }; - }, [deviceData, isLoading]); - - return ( -
- {/* 页面标题 */} -
-

Device Analytics

-

Analyze visitor distribution by devices, browsers, and operating systems

-
- - {/* 时间范围选择器 */} -
-
-
- - setDateRange(prev => ({ ...prev, from: new Date(e.target.value) }))} - /> -
-
- - setDateRange(prev => ({ ...prev, to: new Date(e.target.value) }))} - /> -
-
-
- - {/* 设备类型分析 */} -
- {/* 设备类型 */} -
-

Device Types

- {deviceData && deviceData.deviceTypes.length > 0 ? ( -
- -
- ) : ( -
- No data available -
- )} -
- - {/* 浏览器 */} -
-

Browsers

- {deviceData && deviceData.browsers.length > 0 ? ( -
- -
- ) : ( -
- No data available -
- )} -
- - {/* 操作系统 */} -
-

Operating Systems

- {deviceData && deviceData.operatingSystems.length > 0 ? ( -
- -
- ) : ( -
- No data available -
- )} -
-
- - {/* 加载状态 */} - {isLoading && ( -
-
-
- )} - - {/* 错误状态 */} - {error && ( -
-

{error}

-
- )} - - {/* 无数据状态 */} - {!isLoading && !error && !deviceData && ( -
-

No device data available

-
- )} -
- ); -} \ No newline at end of file diff --git a/app/(app)/dashboard/page.tsx b/app/(app)/dashboard/page.tsx index 9c5f1dd..016f2df 100644 --- a/app/(app)/dashboard/page.tsx +++ b/app/(app)/dashboard/page.tsx @@ -5,7 +5,7 @@ import { format } from 'date-fns'; import { DateRangePicker } from '@/app/components/ui/DateRangePicker'; import TimeSeriesChart from '@/app/components/charts/TimeSeriesChart'; import GeoAnalytics from '@/app/components/analytics/GeoAnalytics'; -import DeviceAnalytics from '@/app/components/analytics/DeviceAnalytics'; +import DevicePieCharts from '@/app/components/charts/DevicePieCharts'; import { EventsSummary, TimeSeriesData, GeoData, DeviceAnalytics as DeviceAnalyticsType } from '@/app/api/types'; export default function DashboardPage() { @@ -128,7 +128,7 @@ export default function DashboardPage() {

Device Analytics

- {deviceData && } + {deviceData && }
diff --git a/app/components/charts/DevicePieCharts.tsx b/app/components/charts/DevicePieCharts.tsx new file mode 100644 index 0000000..d4cdda2 --- /dev/null +++ b/app/components/charts/DevicePieCharts.tsx @@ -0,0 +1,211 @@ +'use client'; + +import { useEffect, useRef } from 'react'; +import { DeviceAnalytics } from '@/app/api/types'; +import { Chart, PieController, ArcElement, Tooltip, Legend, CategoryScale, LinearScale } from 'chart.js'; + +// 注册Chart.js组件 +Chart.register(PieController, ArcElement, Tooltip, Legend, CategoryScale, LinearScale); + +interface DevicePieChartsProps { + data: DeviceAnalytics; +} + +// 颜色配置 +const COLORS = { + deviceTypes: ['rgba(59, 130, 246, 0.8)', 'rgba(96, 165, 250, 0.8)', 'rgba(147, 197, 253, 0.8)', 'rgba(191, 219, 254, 0.8)', 'rgba(219, 234, 254, 0.8)'], + browsers: ['rgba(16, 185, 129, 0.8)', 'rgba(52, 211, 153, 0.8)', 'rgba(110, 231, 183, 0.8)', 'rgba(167, 243, 208, 0.8)', 'rgba(209, 250, 229, 0.8)'], + os: ['rgba(239, 68, 68, 0.8)', 'rgba(248, 113, 113, 0.8)', 'rgba(252, 165, 165, 0.8)', 'rgba(254, 202, 202, 0.8)', 'rgba(254, 226, 226, 0.8)'] +}; + +export default function DevicePieCharts({ data }: DevicePieChartsProps) { + // 创建图表引用 + const deviceTypesChartRef = useRef(null); + const browsersChartRef = useRef(null); + const osChartRef = useRef(null); + + // 图表实例引用 + const deviceTypesChartInstance = useRef(null); + const browsersChartInstance = useRef(null); + const osChartInstance = useRef(null); + + // 初始化和更新图表 + useEffect(() => { + if (!data) return; + + // 销毁旧的图表实例 + if (deviceTypesChartInstance.current) { + deviceTypesChartInstance.current.destroy(); + } + if (browsersChartInstance.current) { + browsersChartInstance.current.destroy(); + } + if (osChartInstance.current) { + osChartInstance.current.destroy(); + } + + // 创建设备类型图表 + if (deviceTypesChartRef.current && data.deviceTypes.length > 0) { + const ctx = deviceTypesChartRef.current.getContext('2d'); + if (ctx) { + deviceTypesChartInstance.current = new Chart(ctx, { + type: 'pie', + data: { + labels: data.deviceTypes.map(item => item.type), + datasets: [{ + data: data.deviceTypes.map(item => item.count), + backgroundColor: COLORS.deviceTypes, + borderColor: COLORS.deviceTypes.map(color => color.replace('0.8', '1')), + borderWidth: 1 + }] + }, + options: { + responsive: true, + plugins: { + legend: { + position: 'bottom', + labels: { + color: 'currentColor' + } + }, + tooltip: { + callbacks: { + label: function(context) { + const label = context.label || ''; + const value = context.raw as number; + const total = (context.chart.data.datasets[0].data as number[]).reduce((a, b) => (a as number) + (b as number), 0); + const percentage = Math.round((value * 100) / total); + return `${label}: ${value} (${percentage}%)`; + } + } + } + } + } + }); + } + } + + // 创建浏览器图表 + if (browsersChartRef.current && data.browsers.length > 0) { + const ctx = browsersChartRef.current.getContext('2d'); + if (ctx) { + browsersChartInstance.current = new Chart(ctx, { + type: 'pie', + data: { + labels: data.browsers.map(item => item.name), + datasets: [{ + data: data.browsers.map(item => item.count), + backgroundColor: COLORS.browsers, + borderColor: COLORS.browsers.map(color => color.replace('0.8', '1')), + borderWidth: 1 + }] + }, + options: { + responsive: true, + plugins: { + legend: { + position: 'bottom', + labels: { + color: 'currentColor' + } + }, + tooltip: { + callbacks: { + label: function(context) { + const label = context.label || ''; + const value = context.raw as number; + const total = (context.chart.data.datasets[0].data as number[]).reduce((a, b) => (a as number) + (b as number), 0); + const percentage = Math.round((value * 100) / total); + return `${label}: ${value} (${percentage}%)`; + } + } + } + } + } + }); + } + } + + // 创建操作系统图表 + if (osChartRef.current && data.operatingSystems.length > 0) { + const ctx = osChartRef.current.getContext('2d'); + if (ctx) { + osChartInstance.current = new Chart(ctx, { + type: 'pie', + data: { + labels: data.operatingSystems.map(item => item.name), + datasets: [{ + data: data.operatingSystems.map(item => item.count), + backgroundColor: COLORS.os, + borderColor: COLORS.os.map(color => color.replace('0.8', '1')), + borderWidth: 1 + }] + }, + options: { + responsive: true, + plugins: { + legend: { + position: 'bottom', + labels: { + color: 'currentColor' + } + }, + tooltip: { + callbacks: { + label: function(context) { + const label = context.label || ''; + const value = context.raw as number; + const total = (context.chart.data.datasets[0].data as number[]).reduce((a, b) => (a as number) + (b as number), 0); + const percentage = Math.round((value * 100) / total); + return `${label}: ${value} (${percentage}%)`; + } + } + } + } + } + }); + } + } + + // 清理函数 + return () => { + if (deviceTypesChartInstance.current) { + deviceTypesChartInstance.current.destroy(); + } + if (browsersChartInstance.current) { + browsersChartInstance.current.destroy(); + } + if (osChartInstance.current) { + osChartInstance.current.destroy(); + } + }; + }, [data]); + + return ( +
+ {/* 设备类型 */} +
+

Device Types

+
+ +
+
+ + {/* 浏览器 */} +
+

Browsers

+
+ +
+
+ + {/* 操作系统 */} +
+

Operating Systems

+
+ +
+
+
+ ); +} \ No newline at end of file