- Add hardware fingerprinting with cross-platform support - Implement secure device registration flow with X.509 certificates - Add WebSocket real-time communication for device status - Create comprehensive device management dashboard - Establish zero-trust security architecture with multi-layer protection - Add database migrations for device registration entities - Implement Rust edge client with hardware identification - Add certificate management and automated provisioning system 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
339 lines
14 KiB
TypeScript
339 lines
14 KiB
TypeScript
'use client';
|
||
|
||
import React, { useState, useEffect } from 'react';
|
||
import { BarChart2, Clock, Star, Map, Camera, List, Calendar, Eye } from 'lucide-react';
|
||
import { StatCard } from '@/components/ui/stat-card';
|
||
import { LoadingSpinner } from '@/components/ui/loading-spinner';
|
||
import { AppLayout } from '@/components/layout/app-layout';
|
||
import { PageLoading } from '@/components/ui/page-loading';
|
||
import { MeteorTypePieChart } from '@/components/charts/meteor-type-pie-chart';
|
||
import { StationDistributionChart } from '@/components/charts/station-distribution-chart';
|
||
import { TimeDistributionChart } from '@/components/charts/time-distribution-chart';
|
||
import { BrightnessDistributionChart } from '@/components/charts/brightness-distribution-chart';
|
||
import {
|
||
getStatisticsSummary,
|
||
getTimeDistribution,
|
||
getBrightnessAnalysis,
|
||
getRegionalDistribution,
|
||
getCameraCorrelation,
|
||
getMeteorEvents,
|
||
type AnalysisData,
|
||
type TimeDistributionData,
|
||
type BrightnessAnalysisData
|
||
} from '@/services/analysis';
|
||
|
||
type ActiveTab = 'time' | 'brightness' | 'regional' | 'correlation' | 'events';
|
||
|
||
export default function AnalysisPage() {
|
||
const [activeTab, setActiveTab] = useState<ActiveTab>('time');
|
||
const [timeFrame, setTimeFrame] = useState<'hour' | 'day' | 'month'>('month');
|
||
const [loading, setLoading] = useState(true);
|
||
const [summaryData, setSummaryData] = useState<AnalysisData | null>(null);
|
||
const [timeDistData, setTimeDistData] = useState<TimeDistributionData | null>(null);
|
||
const [brightnessData, setBrightnessData] = useState<BrightnessAnalysisData | null>(null);
|
||
const [regionalData, setRegionalData] = useState<any>(null);
|
||
|
||
// 获取分析数据
|
||
const fetchAnalysisData = async () => {
|
||
setLoading(true);
|
||
|
||
try {
|
||
const [summaryResponse, timeResponse, brightnessResponse, regionalResponse] = await Promise.all([
|
||
getStatisticsSummary(),
|
||
getTimeDistribution(timeFrame),
|
||
getBrightnessAnalysis(),
|
||
getRegionalDistribution()
|
||
]);
|
||
|
||
setSummaryData(summaryResponse);
|
||
setTimeDistData(timeResponse);
|
||
setBrightnessData(brightnessResponse);
|
||
setRegionalData(regionalResponse);
|
||
} catch (error) {
|
||
console.error('获取分析数据失败:', error);
|
||
// 如果API调用失败,使用空数据
|
||
setSummaryData(null);
|
||
setTimeDistData(null);
|
||
setBrightnessData(null);
|
||
setRegionalData(null);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
fetchAnalysisData();
|
||
}, []);
|
||
|
||
// 渲染统计摘要卡片
|
||
const renderStatCards = () => {
|
||
if (!summaryData) return null;
|
||
|
||
return (
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||
<StatCard
|
||
title="总流星数"
|
||
value={summaryData.totalDetections}
|
||
icon={BarChart2}
|
||
iconBg="bg-blue-500"
|
||
/>
|
||
<StatCard
|
||
title="平均亮度"
|
||
value={summaryData.averageBrightness?.toFixed(1) || '--'}
|
||
suffix="等"
|
||
icon={Star}
|
||
iconBg="bg-purple-500"
|
||
/>
|
||
<StatCard
|
||
title="最活跃月份"
|
||
value={summaryData.mostActiveMonth?.month || '--'}
|
||
description={summaryData.mostActiveMonth ? `${summaryData.mostActiveMonth.shower} (${summaryData.mostActiveMonth.count} 颗)` : ''}
|
||
icon={Calendar}
|
||
iconBg="bg-green-500"
|
||
/>
|
||
<StatCard
|
||
title="最活跃站点"
|
||
value={summaryData.topStations?.[0]?.region || '--'}
|
||
description={summaryData.topStations?.[0] ? `${summaryData.topStations[0].count} 次观测` : ''}
|
||
icon={Camera}
|
||
iconBg="bg-orange-500"
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 渲染时间分布视图
|
||
const renderTimeDistributionView = () => {
|
||
const distribution = timeFrame === 'hour' ? timeDistData?.hourly :
|
||
timeFrame === 'month' ? timeDistData?.monthly :
|
||
timeDistData?.yearly;
|
||
|
||
if (!timeDistData || !distribution || !Array.isArray(distribution)) {
|
||
return (
|
||
<div className="flex justify-center items-center h-64">
|
||
<p className="text-gray-500 dark:text-gray-400">暂无数据</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<div className="mb-4 flex flex-wrap gap-2">
|
||
<button
|
||
className={`px-3 py-1 text-sm rounded-md ${timeFrame === 'hour' ? 'bg-blue-600 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'}`}
|
||
onClick={() => setTimeFrame('hour')}
|
||
>
|
||
按小时
|
||
</button>
|
||
<button
|
||
className={`px-3 py-1 text-sm rounded-md ${timeFrame === 'day' ? 'bg-blue-600 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'}`}
|
||
onClick={() => setTimeFrame('day')}
|
||
>
|
||
按天
|
||
</button>
|
||
<button
|
||
className={`px-3 py-1 text-sm rounded-md ${timeFrame === 'month' ? 'bg-blue-600 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'}`}
|
||
onClick={() => setTimeFrame('month')}
|
||
>
|
||
按月
|
||
</button>
|
||
</div>
|
||
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">流星时间分布</h3>
|
||
<TimeDistributionChart data={distribution} timeFrame={timeFrame} />
|
||
<div className="mt-4 text-center text-sm text-gray-500 dark:text-gray-400">
|
||
总计: {distribution.reduce((acc, item) => acc + item.count, 0).toLocaleString()} 颗流星
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
};
|
||
|
||
// 渲染亮度分析视图
|
||
const renderBrightnessView = () => {
|
||
if (!brightnessData || !brightnessData.brightnessDistribution || !brightnessData.brightnessStats) {
|
||
return (
|
||
<div className="flex justify-center items-center h-64">
|
||
<p className="text-gray-500 dark:text-gray-400">暂无数据</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">亮度分布</h3>
|
||
<BrightnessDistributionChart data={brightnessData.brightnessDistribution || []} />
|
||
</div>
|
||
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">亮度统计</h3>
|
||
|
||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||
<div className="p-3 bg-gray-100 dark:bg-gray-750 rounded-lg">
|
||
<p className="text-sm text-gray-500 dark:text-gray-400">平均亮度</p>
|
||
<p className="text-xl font-bold text-gray-900 dark:text-white">
|
||
{brightnessData.brightnessStats?.average?.toFixed(1) || '--'} <span className="text-sm font-normal">等</span>
|
||
</p>
|
||
</div>
|
||
|
||
<div className="p-3 bg-gray-100 dark:bg-gray-750 rounded-lg">
|
||
<p className="text-sm text-gray-500 dark:text-gray-400">中位数亮度</p>
|
||
<p className="text-xl font-bold text-gray-900 dark:text-white">
|
||
{brightnessData.brightnessStats?.median?.toFixed(1) || '--'} <span className="text-sm font-normal">等</span>
|
||
</p>
|
||
</div>
|
||
|
||
<div className="p-3 bg-gray-100 dark:bg-gray-750 rounded-lg">
|
||
<p className="text-sm text-gray-500 dark:text-gray-400">最亮流星</p>
|
||
<p className="text-xl font-bold text-gray-900 dark:text-white">
|
||
{brightnessData.brightnessStats?.brightest?.toFixed(1) || '--'} <span className="text-sm font-normal">等</span>
|
||
</p>
|
||
</div>
|
||
|
||
<div className="p-3 bg-gray-100 dark:bg-gray-750 rounded-lg">
|
||
<p className="text-sm text-gray-500 dark:text-gray-400">最暗流星</p>
|
||
<p className="text-xl font-bold text-gray-900 dark:text-white">
|
||
{brightnessData.brightnessStats?.dimmest?.toFixed(1) || '--'} <span className="text-sm font-normal">等</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||
<h4 className="font-medium mb-2">亮度等级说明</h4>
|
||
<p className="mb-1">视星等解释:</p>
|
||
<ul className="list-disc list-inside space-y-1">
|
||
<li>-6及以下: 超级火球</li>
|
||
<li>-6到-4: 火球</li>
|
||
<li>-4到-2: 很亮流星</li>
|
||
<li>-2到0: 亮流星</li>
|
||
<li>0到2: 普通流星</li>
|
||
<li>2到4: 暗流星</li>
|
||
<li>4及以上: 很暗流星</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
if (loading) {
|
||
return <PageLoading text="加载分析数据中..." />;
|
||
}
|
||
|
||
return (
|
||
<AppLayout>
|
||
<div className="p-4 md:p-6">
|
||
<div className="mb-6">
|
||
<h2 className="text-xl md:text-2xl font-bold mb-2 text-gray-900 dark:text-white">数据分析</h2>
|
||
<p className="text-sm text-gray-600 dark:text-gray-400">深入分析流星观测数据的各项指标和趋势</p>
|
||
</div>
|
||
|
||
{/* 统计摘要卡片 */}
|
||
{renderStatCards()}
|
||
|
||
{/* 分析标签页 */}
|
||
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
|
||
<div className="flex flex-wrap -mb-px">
|
||
<button
|
||
className={`mr-2 inline-block py-2 px-4 text-sm font-medium ${
|
||
activeTab === 'time'
|
||
? 'text-blue-600 border-b-2 border-blue-600'
|
||
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||
}`}
|
||
onClick={() => setActiveTab('time')}
|
||
>
|
||
<Clock size={16} className="inline mr-1 mb-1" />
|
||
时间分布
|
||
</button>
|
||
<button
|
||
className={`mr-2 inline-block py-2 px-4 text-sm font-medium ${
|
||
activeTab === 'brightness'
|
||
? 'text-blue-600 border-b-2 border-blue-600'
|
||
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||
}`}
|
||
onClick={() => setActiveTab('brightness')}
|
||
>
|
||
<Star size={16} className="inline mr-1 mb-1" />
|
||
亮度分析
|
||
</button>
|
||
<button
|
||
className={`mr-2 inline-block py-2 px-4 text-sm font-medium ${
|
||
activeTab === 'regional'
|
||
? 'text-blue-600 border-b-2 border-blue-600'
|
||
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||
}`}
|
||
onClick={() => setActiveTab('regional')}
|
||
>
|
||
<Map size={16} className="inline mr-1 mb-1" />
|
||
区域分布
|
||
</button>
|
||
<button
|
||
className={`mr-2 inline-block py-2 px-4 text-sm font-medium ${
|
||
activeTab === 'correlation'
|
||
? 'text-blue-600 border-b-2 border-blue-600'
|
||
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||
}`}
|
||
onClick={() => setActiveTab('correlation')}
|
||
>
|
||
<Camera size={16} className="inline mr-1 mb-1" />
|
||
相机关联
|
||
</button>
|
||
<button
|
||
className={`mr-2 inline-block py-2 px-4 text-sm font-medium ${
|
||
activeTab === 'events'
|
||
? 'text-blue-600 border-b-2 border-blue-600'
|
||
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||
}`}
|
||
onClick={() => setActiveTab('events')}
|
||
>
|
||
<List size={16} className="inline mr-1 mb-1" />
|
||
流星事件
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 内容区域 */}
|
||
<div>
|
||
{activeTab === 'time' && renderTimeDistributionView()}
|
||
{activeTab === 'brightness' && renderBrightnessView()}
|
||
{activeTab === 'regional' && (
|
||
<div className="space-y-6">
|
||
{regionalData && regionalData.regions && Array.isArray(regionalData.regions) ? (
|
||
<>
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">流星类型分布</h3>
|
||
<MeteorTypePieChart data={regionalData.meteorTypes || []} />
|
||
</div>
|
||
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">监测站分布</h3>
|
||
<StationDistributionChart data={regionalData.regions || []} />
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className="text-center py-16">
|
||
<Map className="text-gray-400 mb-4 mx-auto" size={48} />
|
||
<p className="text-gray-500 dark:text-gray-400">正在加载区域分布数据...</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
{activeTab === 'correlation' && (
|
||
<div className="text-center py-16">
|
||
<Camera className="text-gray-400 mb-4 mx-auto" size={48} />
|
||
<p className="text-gray-500 dark:text-gray-400">相机关联分析开发中...</p>
|
||
</div>
|
||
)}
|
||
{activeTab === 'events' && (
|
||
<div className="text-center py-16">
|
||
<List className="text-gray-400 mb-4 mx-auto" size={48} />
|
||
<p className="text-gray-500 dark:text-gray-400">流星事件列表开发中...</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</AppLayout>
|
||
);
|
||
} |