grabbit 13ce6ae442 feat: implement complete edge device registration system
- 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>
2025-08-13 08:46:25 +08:00

339 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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>
);
}