|
|
@@ -1,376 +1,760 @@
|
|
|
---
|
|
|
-description: "Shadcn-ui 数据大屏开发指令"
|
|
|
+description: "Shadcn-ui 全屏酷炫数据大屏开发指令"
|
|
|
---
|
|
|
|
|
|
## 概述
|
|
|
|
|
|
-基于 `src/client/admin-shadcn/pages/Dashboard.tsx` 中数据大屏的实现,提取可复用的数据可视化开发模式和最佳实践,适用于基于 Shadcn-ui 的数据大屏和仪表板开发。
|
|
|
+基于现代Web技术栈的全屏酷炫数据大屏开发指令,适用于展示大型数据可视化、实时监控、企业级仪表板等场景。采用深色主题、3D效果、霓虹光效、动态数据流等设计元素,打造专业级数据展示体验。
|
|
|
|
|
|
-## 核心特性
|
|
|
+## 设计特点
|
|
|
|
|
|
-### 1. 响应式布局系统
|
|
|
-- **网格布局**:使用 CSS Grid 和 Flexbox 实现响应式卡片布局
|
|
|
-- **断点设计**:支持 sm, md, lg, xl, 2xl 五个断点
|
|
|
-- **自适应卡片**:卡片宽度根据屏幕尺寸自动调整
|
|
|
+### 1. 沉浸式全屏体验
|
|
|
+- **无边框设计**:100%视口高度和宽度
|
|
|
+- **深色主题**:宇宙黑背景配合霓虹光效
|
|
|
+- **3D景深**:视差滚动和3D变换效果
|
|
|
+- **动态背景**:粒子系统和流动光效
|
|
|
|
|
|
-### 2. 数据可视化组件
|
|
|
-- **图表集成**:基于 Recharts 的图表组件封装
|
|
|
-- **统计卡片**:标准化的 KPI 展示卡片
|
|
|
-- **实时数据**:支持数据实时更新和刷新
|
|
|
+### 2. 未来科技风格
|
|
|
+- **霓虹光效**:发光边框、脉冲动画
|
|
|
+- **玻璃拟态**:毛玻璃效果配合渐变
|
|
|
+- **网格系统**:科技感网格背景
|
|
|
+- **全息投影**:半透明层叠效果
|
|
|
|
|
|
-### 3. 主题一致性
|
|
|
-- **深色/浅色主题**:自动适配系统主题
|
|
|
-- **色彩规范**:使用 Tailwind CSS 色彩系统
|
|
|
-- **动画效果**:平滑的过渡和加载动画
|
|
|
+### 3. 实时数据流
|
|
|
+- **动态计数器**:数字滚动动画
|
|
|
+- **数据流动**:连接线动画效果
|
|
|
+- **状态指示器**:脉冲心跳效果
|
|
|
+- **实时更新**:无缝数据刷新
|
|
|
|
|
|
-## 开发模板
|
|
|
+## 核心架构
|
|
|
|
|
|
-### 基础结构模板
|
|
|
+### 基础布局模板
|
|
|
|
|
|
```typescript
|
|
|
-// 1. 核心导入
|
|
|
+// 全屏数据大屏入口
|
|
|
import { useState, useEffect } from 'react';
|
|
|
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
|
|
|
-import { Button } from '@/client/components/ui/button';
|
|
|
-import { RefreshCw, TrendingUp, TrendingDown } from 'lucide-react';
|
|
|
-import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
|
|
-
|
|
|
-// 2. 数据类型定义
|
|
|
-interface DashboardData {
|
|
|
- totalUsers: number;
|
|
|
- totalRevenue: number;
|
|
|
- monthlyGrowth: number;
|
|
|
- chartData: Array<{
|
|
|
- name: string;
|
|
|
- value: number;
|
|
|
- growth: number;
|
|
|
- }>;
|
|
|
-}
|
|
|
+import { motion } from 'framer-motion';
|
|
|
+import { Canvas } from '@react-three/fiber';
|
|
|
+import { Stars } from '@react-three/drei';
|
|
|
+import { useQuery } from '@tanstack/react-query';
|
|
|
|
|
|
-// 3. 组件状态
|
|
|
-const [data, setData] = useState<DashboardData | null>(null);
|
|
|
-const [loading, setLoading] = useState(true);
|
|
|
-const [lastUpdated, setLastUpdated] = useState<Date>(new Date());
|
|
|
+const FullScreenDashboard = () => {
|
|
|
+ return (
|
|
|
+ <div className="fixed inset-0 bg-gradient-to-br from-gray-900 via-black to-gray-900 overflow-hidden">
|
|
|
+ {/* 3D背景 */}
|
|
|
+ <Canvas className="absolute inset-0">
|
|
|
+ <Stars radius={100} depth={50} count={5000} factor={4} saturation={0} fade speed={1} />
|
|
|
+ </Canvas>
|
|
|
+
|
|
|
+ {/* 网格背景 */}
|
|
|
+ <div className="absolute inset-0 bg-[linear-gradient(to_right,#ffffff08_1px,transparent_1px),linear-gradient(to_bottom,#ffffff08_1px,transparent_1px)] bg-[size:50px_50px]" />
|
|
|
+
|
|
|
+ {/* 主内容层 */}
|
|
|
+ <div className="relative z-10 h-full flex flex-col">
|
|
|
+ <DashboardHeader />
|
|
|
+ <DashboardGrid />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 动态光效 */}
|
|
|
+ <div className="absolute inset-0 bg-gradient-to-r from-cyan-500/10 via-transparent to-purple-500/10 animate-pulse" />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
```
|
|
|
|
|
|
-### 统计卡片模板
|
|
|
+### 响应式网格系统
|
|
|
|
|
|
-#### 基础统计卡片
|
|
|
```typescript
|
|
|
-<Card>
|
|
|
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
|
- <CardTitle className="text-sm font-medium">总用户数</CardTitle>
|
|
|
- <Users className="h-4 w-4 text-muted-foreground" />
|
|
|
- </CardHeader>
|
|
|
- <CardContent>
|
|
|
- <div className="text-2xl font-bold">{data?.totalUsers.toLocaleString()}</div>
|
|
|
- <p className="text-xs text-muted-foreground">
|
|
|
- {data?.monthlyGrowth > 0 ? '+' : ''}{data?.monthlyGrowth}% 较上月
|
|
|
- </p>
|
|
|
- </CardContent>
|
|
|
-</Card>
|
|
|
+const DashboardGrid = () => (
|
|
|
+ <div className="flex-1 p-4 md:p-8 grid grid-cols-12 gap-4 auto-rows-fr">
|
|
|
+ {/* 顶部标题区 */}
|
|
|
+ <motion.div
|
|
|
+ className="col-span-12 h-20"
|
|
|
+ initial={{ opacity: 0, y: -50 }}
|
|
|
+ animate={{ opacity: 1, y: 0 }}
|
|
|
+ transition={{ duration: 0.8 }}
|
|
|
+ >
|
|
|
+ <div className="h-full glass-card rounded-xl flex items-center justify-between px-6">
|
|
|
+ <h1 className="text-2xl md:text-4xl font-bold bg-gradient-to-r from-cyan-400 to-purple-400 bg-clip-text text-transparent">
|
|
|
+ 智慧城市数据监控中心
|
|
|
+ </h1>
|
|
|
+ <DigitalClock />
|
|
|
+ </div>
|
|
|
+ </motion.div>
|
|
|
+
|
|
|
+ {/* 左侧统计区 */}
|
|
|
+ <motion.div
|
|
|
+ className="col-span-12 md:col-span-3 space-y-4"
|
|
|
+ initial={{ opacity: 0, x: -50 }}
|
|
|
+ animate={{ opacity: 1, x: 0 }}
|
|
|
+ transition={{ duration: 0.8, delay: 0.2 }}
|
|
|
+ >
|
|
|
+ <KpiCard title="实时在线" value={1234567} unit="人" trend={12.5} />
|
|
|
+ <KpiCard title="今日流量" value={890123} unit="GB" trend={-5.2} />
|
|
|
+ <KpiCard title="异常事件" value={42} unit="起" trend={8.7} />
|
|
|
+ <KpiCard title="处理效率" value={98.7} unit="%" trend={2.1} />
|
|
|
+ </motion.div>
|
|
|
+
|
|
|
+ {/* 中间主图表区 */}
|
|
|
+ <motion.div
|
|
|
+ className="col-span-12 md:col-span-6"
|
|
|
+ initial={{ opacity: 0, scale: 0.9 }}
|
|
|
+ animate={{ opacity: 1, scale: 1 }}
|
|
|
+ transition={{ duration: 0.8, delay: 0.4 }}
|
|
|
+ >
|
|
|
+ <div className="h-full glass-card rounded-xl p-4">
|
|
|
+ <RealTimeMap />
|
|
|
+ </div>
|
|
|
+ </motion.div>
|
|
|
+
|
|
|
+ {/* 右侧排行榜 */}
|
|
|
+ <motion.div
|
|
|
+ className="col-span-12 md:col-span-3"
|
|
|
+ initial={{ opacity: 0, x: 50 }}
|
|
|
+ animate={{ opacity: 1, x: 0 }}
|
|
|
+ transition={{ duration: 0.8, delay: 0.6 }}
|
|
|
+ >
|
|
|
+ <div className="h-full glass-card rounded-xl p-4">
|
|
|
+ <RankingList />
|
|
|
+ </div>
|
|
|
+ </motion.div>
|
|
|
+
|
|
|
+ {/* 底部图表区 */}
|
|
|
+ <motion.div
|
|
|
+ className="col-span-12 md:col-span-4"
|
|
|
+ initial={{ opacity: 0, y: 50 }}
|
|
|
+ animate={{ opacity: 1, y: 0 }}
|
|
|
+ transition={{ duration: 0.8, delay: 0.8 }}
|
|
|
+ >
|
|
|
+ <div className="h-64 glass-card rounded-xl p-4">
|
|
|
+ <RealTimeChart type="line" title="流量趋势" />
|
|
|
+ </div>
|
|
|
+ </motion.div>
|
|
|
+
|
|
|
+ <motion.div
|
|
|
+ className="col-span-12 md:col-span-4"
|
|
|
+ initial={{ opacity: 0, y: 50 }}
|
|
|
+ animate={{ opacity: 1, y: 0 }}
|
|
|
+ transition={{ duration: 0.8, delay: 1.0 }}
|
|
|
+ >
|
|
|
+ <div className="h-64 glass-card rounded-xl p-4">
|
|
|
+ <RealTimeChart type="bar" title="设备分布" />
|
|
|
+ </div>
|
|
|
+ </motion.div>
|
|
|
+
|
|
|
+ <motion.div
|
|
|
+ className="col-span-12 md:col-span-4"
|
|
|
+ initial={{ opacity: 0, y: 50 }}
|
|
|
+ animate={{ opacity: 1, y: 0 }}
|
|
|
+ transition={{ duration: 0.8, delay: 1.2 }}
|
|
|
+ >
|
|
|
+ <div className="h-64 glass-card rounded-xl p-4">
|
|
|
+ <RealTimeChart type="pie" title="区域分布" />
|
|
|
+ </div>
|
|
|
+ </motion.div>
|
|
|
+ </div>
|
|
|
+);
|
|
|
```
|
|
|
|
|
|
-#### 带趋势指标卡片
|
|
|
+## 核心组件
|
|
|
+
|
|
|
+### 1. 玻璃拟态卡片
|
|
|
```typescript
|
|
|
-<Card>
|
|
|
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
|
- <CardTitle className="text-sm font-medium">总收入</CardTitle>
|
|
|
- <DollarSign className="h-4 w-4 text-muted-foreground" />
|
|
|
- </CardHeader>
|
|
|
- <CardContent>
|
|
|
- <div className="text-2xl font-bold">${data?.totalRevenue.toLocaleString()}</div>
|
|
|
- <div className="flex items-center mt-1">
|
|
|
- {data?.monthlyGrowth > 0 ? (
|
|
|
- <TrendingUp className="h-4 w-4 text-green-500 mr-1" />
|
|
|
- ) : (
|
|
|
- <TrendingDown className="h-4 w-4 text-red-500 mr-1" />
|
|
|
- )}
|
|
|
- <p className={`text-xs ${data?.monthlyGrowth > 0 ? 'text-green-500' : 'text-red-500'}`}>
|
|
|
- {Math.abs(data?.monthlyGrowth || 0)}% 较上月
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- </CardContent>
|
|
|
-</Card>
|
|
|
+const GlassCard = ({ children, className = '' }) => (
|
|
|
+ <div className={`
|
|
|
+ bg-white/5 backdrop-blur-md
|
|
|
+ border border-white/10 rounded-2xl
|
|
|
+ shadow-[0_8px_32px_0_rgba(31,38,135,0.37)]
|
|
|
+ hover:shadow-[0_8px_32px_0_rgba(31,38,135,0.6)]
|
|
|
+ transition-all duration-300
|
|
|
+ ${className}
|
|
|
+ `}>
|
|
|
+ {children}
|
|
|
+ </div>
|
|
|
+);
|
|
|
```
|
|
|
|
|
|
-### 图表组件模板
|
|
|
-
|
|
|
-#### 柱状图
|
|
|
+### 2. 数字计数器
|
|
|
```typescript
|
|
|
-<Card className="col-span-4">
|
|
|
- <CardHeader>
|
|
|
- <CardTitle>月度数据概览</CardTitle>
|
|
|
- <CardDescription>过去12个月的数据趋势</CardDescription>
|
|
|
- </CardHeader>
|
|
|
- <CardContent className="pl-2">
|
|
|
- <ResponsiveContainer width="100%" height={350}>
|
|
|
- <BarChart data={chartData}>
|
|
|
- <CartesianGrid strokeDasharray="3 3" />
|
|
|
- <XAxis dataKey="name" />
|
|
|
- <YAxis />
|
|
|
- <Tooltip />
|
|
|
- <Legend />
|
|
|
- <Bar dataKey="value" fill="#8884d8" />
|
|
|
- </BarChart>
|
|
|
- </ResponsiveContainer>
|
|
|
- </CardContent>
|
|
|
-</Card>
|
|
|
+import CountUp from 'react-countup';
|
|
|
+import { useInView } from 'react-intersection-observer';
|
|
|
+
|
|
|
+const AnimatedNumber = ({ value, duration = 2 }) => {
|
|
|
+ const { ref, inView } = useInView({ threshold: 0.1 });
|
|
|
+
|
|
|
+ return (
|
|
|
+ <span ref={ref} className="text-3xl md:text-5xl font-bold text-cyan-400">
|
|
|
+ <CountUp
|
|
|
+ end={inView ? value : 0}
|
|
|
+ duration={duration}
|
|
|
+ separator=","
|
|
|
+ />
|
|
|
+ </span>
|
|
|
+ );
|
|
|
+};
|
|
|
```
|
|
|
|
|
|
-#### 折线图
|
|
|
+### 3. KPI卡片组件
|
|
|
```typescript
|
|
|
-<Card className="col-span-4">
|
|
|
- <CardHeader>
|
|
|
- <CardTitle>趋势分析</CardTitle>
|
|
|
- <CardDescription>数据变化趋势</CardDescription>
|
|
|
- </CardHeader>
|
|
|
- <CardContent>
|
|
|
- <ResponsiveContainer width="100%" height={300}>
|
|
|
- <LineChart data={lineChartData}>
|
|
|
- <CartesianGrid strokeDasharray="3 3" />
|
|
|
- <XAxis dataKey="name" />
|
|
|
- <YAxis />
|
|
|
- <Tooltip />
|
|
|
- <Legend />
|
|
|
- <Line type="monotone" dataKey="users" stroke="#8884d8" strokeWidth={2} />
|
|
|
- <Line type="monotone" dataKey="revenue" stroke="#82ca9d" strokeWidth={2} />
|
|
|
- </LineChart>
|
|
|
- </ResponsiveContainer>
|
|
|
- </CardContent>
|
|
|
-</Card>
|
|
|
+const KpiCard = ({ title, value, unit, trend, color = 'cyan' }) => {
|
|
|
+ const isPositive = trend > 0;
|
|
|
+ const colorClasses = {
|
|
|
+ cyan: 'from-cyan-400 to-blue-500',
|
|
|
+ green: 'from-green-400 to-emerald-500',
|
|
|
+ red: 'from-red-400 to-pink-500',
|
|
|
+ purple: 'from-purple-400 to-indigo-500',
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <GlassCard className="p-4 md:p-6 relative overflow-hidden">
|
|
|
+ {/* 背景光效 */}
|
|
|
+ <div className={`absolute inset-0 bg-gradient-to-br ${colorClasses[color]} opacity-10`} />
|
|
|
+
|
|
|
+ <div className="relative z-10">
|
|
|
+ <h3 className="text-sm md:text-lg text-gray-300 mb-2">{title}</h3>
|
|
|
+ <div className="flex items-baseline space-x-2">
|
|
|
+ <AnimatedNumber value={value} />
|
|
|
+ <span className="text-lg text-gray-400">{unit}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="mt-2 flex items-center space-x-1">
|
|
|
+ <motion.div
|
|
|
+ animate={{ rotate: isPositive ? 0 : 180 }}
|
|
|
+ className={`w-0 h-0 border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent border-b-[8px] ${
|
|
|
+ isPositive ? 'border-b-green-400' : 'border-b-red-400'
|
|
|
+ }`}
|
|
|
+ />
|
|
|
+ <span className={`text-sm ${isPositive ? 'text-green-400' : 'text-red-400'}`}>
|
|
|
+ {Math.abs(trend)}%
|
|
|
+ </span>
|
|
|
+ <span className="text-xs text-gray-500">vs 上期</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 脉冲效果 */}
|
|
|
+ <motion.div
|
|
|
+ className="absolute top-2 right-2 w-2 h-2 bg-green-400 rounded-full"
|
|
|
+ animate={{
|
|
|
+ scale: [1, 1.5, 1],
|
|
|
+ opacity: [1, 0.5, 1],
|
|
|
+ }}
|
|
|
+ transition={{
|
|
|
+ duration: 2,
|
|
|
+ repeat: Infinity,
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </GlassCard>
|
|
|
+ );
|
|
|
+};
|
|
|
```
|
|
|
|
|
|
-#### 饼图
|
|
|
+### 4. 实时地图组件
|
|
|
```typescript
|
|
|
-<Card className="col-span-3">
|
|
|
- <CardHeader>
|
|
|
- <CardTitle>数据分布</CardTitle>
|
|
|
- <CardDescription>按类别分布</CardDescription>
|
|
|
- </CardHeader>
|
|
|
- <CardContent>
|
|
|
- <ResponsiveContainer width="100%" height={300}>
|
|
|
- <PieChart>
|
|
|
- <Pie
|
|
|
- data={pieData}
|
|
|
- cx="50%"
|
|
|
- cy="50%"
|
|
|
- labelLine={false}
|
|
|
- label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
|
|
- outerRadius={80}
|
|
|
- fill="#8884d8"
|
|
|
- dataKey="value"
|
|
|
- >
|
|
|
- {pieData.map((entry, index) => (
|
|
|
- <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
|
|
- ))}
|
|
|
- </Pie>
|
|
|
- <Tooltip />
|
|
|
- </PieChart>
|
|
|
- </ResponsiveContainer>
|
|
|
- </CardContent>
|
|
|
-</Card>
|
|
|
-```
|
|
|
+import { MapContainer, TileLayer, CircleMarker, Popup } from 'react-leaflet';
|
|
|
+import 'leaflet/dist/leaflet.css';
|
|
|
+
|
|
|
+const RealTimeMap = () => {
|
|
|
+ const [positions, setPositions] = useState([]);
|
|
|
+
|
|
|
+ // 实时数据获取
|
|
|
+ const { data: mapData } = useQuery({
|
|
|
+ queryKey: ['real-time-positions'],
|
|
|
+ queryFn: () => apiClient.map.$get(),
|
|
|
+ refetchInterval: 5000,
|
|
|
+ });
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (mapData) {
|
|
|
+ setPositions(mapData.positions);
|
|
|
+ }
|
|
|
+ }, [mapData]);
|
|
|
|
|
|
-### 响应式网格布局
|
|
|
+ return (
|
|
|
+ <div className="h-full w-full rounded-xl overflow-hidden relative">
|
|
|
+ <MapContainer
|
|
|
+ center={[39.9042, 116.4074]}
|
|
|
+ zoom={10}
|
|
|
+ className="h-full w-full"
|
|
|
+ style={{ background: 'transparent' }}
|
|
|
+ >
|
|
|
+ <TileLayer
|
|
|
+ url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
|
|
|
+ attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
|
|
+ />
|
|
|
+
|
|
|
+ {positions.map((pos, index) => (
|
|
|
+ <CircleMarker
|
|
|
+ key={index}
|
|
|
+ center={[pos.lat, pos.lng]}
|
|
|
+ radius={8}
|
|
|
+ pathOptions={{
|
|
|
+ fillColor: '#00ff88',
|
|
|
+ color: '#00ff88',
|
|
|
+ weight: 2,
|
|
|
+ opacity: 0.8,
|
|
|
+ fillOpacity: 0.6,
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Popup>
|
|
|
+ <div className="text-white">
|
|
|
+ <p>设备ID: {pos.deviceId}</p>
|
|
|
+ <p>状态: {pos.status}</p>
|
|
|
+ <p>时间: {new Date(pos.timestamp).toLocaleTimeString()}</p>
|
|
|
+ </div>
|
|
|
+ </Popup>
|
|
|
+ </CircleMarker>
|
|
|
+ ))}
|
|
|
+ </MapContainer>
|
|
|
+
|
|
|
+ {/* 地图遮罩 */}
|
|
|
+ <div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none" />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+```
|
|
|
|
|
|
-#### 基础网格
|
|
|
+### 5. 实时图表组件
|
|
|
```typescript
|
|
|
-<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
|
- {/* 统计卡片 */}
|
|
|
- <Card>...</Card>
|
|
|
- <Card>...</Card>
|
|
|
- <Card>...</Card>
|
|
|
- <Card>...</Card>
|
|
|
-</div>
|
|
|
+import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
|
|
+import { useRealtimeData } from '@/hooks/useRealtimeData';
|
|
|
+
|
|
|
+const RealTimeChart = ({ type = 'line', title, dataKey = 'value' }) => {
|
|
|
+ const { data: chartData } = useRealtimeData(dataKey, {
|
|
|
+ maxPoints: 50,
|
|
|
+ interval: 1000,
|
|
|
+ });
|
|
|
+
|
|
|
+ const CustomTooltip = ({ active, payload, label }) => {
|
|
|
+ if (active && payload && payload.length) {
|
|
|
+ return (
|
|
|
+ <GlassCard className="p-2">
|
|
|
+ <p className="text-cyan-400">{`${label}`}</p>
|
|
|
+ <p className="text-white">{`${payload[0].value}`}</p>
|
|
|
+ </GlassCard>
|
|
|
+ );
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="h-full w-full">
|
|
|
+ <h3 className="text-lg font-semibold text-cyan-400 mb-4">{title}</h3>
|
|
|
+ <ResponsiveContainer width="100%" height="80%">
|
|
|
+ <AreaChart data={chartData}>
|
|
|
+ <defs>
|
|
|
+ <linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1">
|
|
|
+ <stop offset="5%" stopColor="#00ff88" stopOpacity={0.8}/>
|
|
|
+ <stop offset="95%" stopColor="#00ff88" stopOpacity={0.1}/>
|
|
|
+ </linearGradient>
|
|
|
+ </defs>
|
|
|
+ <CartesianGrid strokeDasharray="3 3" stroke="#ffffff20" />
|
|
|
+ <XAxis
|
|
|
+ dataKey="time"
|
|
|
+ stroke="#ffffff60"
|
|
|
+ tick={{ fill: '#ffffff80' }}
|
|
|
+ />
|
|
|
+ <YAxis
|
|
|
+ stroke="#ffffff60"
|
|
|
+ tick={{ fill: '#ffffff80' }}
|
|
|
+ />
|
|
|
+ <Tooltip content={<CustomTooltip />} />
|
|
|
+ <Area
|
|
|
+ type="monotone"
|
|
|
+ dataKey={dataKey}
|
|
|
+ stroke="#00ff88"
|
|
|
+ strokeWidth={2}
|
|
|
+ fillOpacity={1}
|
|
|
+ fill="url(#colorGradient)"
|
|
|
+ />
|
|
|
+ </AreaChart>
|
|
|
+ </ResponsiveContainer>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
```
|
|
|
|
|
|
-#### 复杂网格
|
|
|
+### 6. 排行榜组件
|
|
|
```typescript
|
|
|
-<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
|
|
|
- <Card className="col-span-4">...</Card>
|
|
|
- <Card className="col-span-3">...</Card>
|
|
|
-</div>
|
|
|
+const RankingList = () => {
|
|
|
+ const { data: rankings } = useQuery({
|
|
|
+ queryKey: ['real-time-rankings'],
|
|
|
+ queryFn: () => apiClient.rankings.$get(),
|
|
|
+ refetchInterval: 10000,
|
|
|
+ });
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="h-full">
|
|
|
+ <h3 className="text-lg font-semibold text-cyan-400 mb-4">实时排行</h3>
|
|
|
+ <div className="space-y-2">
|
|
|
+ {rankings?.map((item, index) => (
|
|
|
+ <motion.div
|
|
|
+ key={item.id}
|
|
|
+ className="flex items-center space-x-3 p-3 rounded-lg bg-white/5 hover:bg-white/10 transition-colors"
|
|
|
+ initial={{ opacity: 0, x: -20 }}
|
|
|
+ animate={{ opacity: 1, x: 0 }}
|
|
|
+ transition={{ delay: index * 0.1 }}
|
|
|
+ >
|
|
|
+ <div className={`
|
|
|
+ w-8 h-8 rounded-full flex items-center justify-center font-bold
|
|
|
+ ${index === 0 ? 'bg-yellow-500 text-black' : ''}
|
|
|
+ ${index === 1 ? 'bg-gray-400 text-black' : ''}
|
|
|
+ ${index === 2 ? 'bg-orange-600 text-white' : ''}
|
|
|
+ ${index > 2 ? 'bg-gray-600 text-white' : ''}
|
|
|
+ `}>
|
|
|
+ {index + 1}
|
|
|
+ </div>
|
|
|
+ <div className="flex-1">
|
|
|
+ <p className="text-white font-medium">{item.name}</p>
|
|
|
+ <p className="text-gray-400 text-sm">{item.value}</p>
|
|
|
+ </div>
|
|
|
+ <div className="text-cyan-400 font-bold">
|
|
|
+ {item.score}
|
|
|
+ </div>
|
|
|
+ </motion.div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
```
|
|
|
|
|
|
-### 数据加载状态
|
|
|
+## 动画效果
|
|
|
|
|
|
-#### 骨架屏
|
|
|
+### 1. 粒子背景
|
|
|
```typescript
|
|
|
-import { Skeleton } from '@/client/components/ui/skeleton';
|
|
|
-
|
|
|
-const LoadingCard = () => (
|
|
|
- <Card>
|
|
|
- <CardHeader>
|
|
|
- <Skeleton className="h-4 w-[100px]" />
|
|
|
- </CardHeader>
|
|
|
- <CardContent>
|
|
|
- <Skeleton className="h-8 w-[150px] mb-2" />
|
|
|
- <Skeleton className="h-3 w-[100px]" />
|
|
|
- </CardContent>
|
|
|
- </Card>
|
|
|
-);
|
|
|
+const ParticleBackground = () => {
|
|
|
+ return (
|
|
|
+ <div className="absolute inset-0">
|
|
|
+ <Particles
|
|
|
+ id="tsparticles"
|
|
|
+ options={{
|
|
|
+ background: { color: { value: "#000" } },
|
|
|
+ fpsLimit: 60,
|
|
|
+ particles: {
|
|
|
+ color: { value: ["#00ff88", "#00ffff", "#ff00ff"] },
|
|
|
+ links: {
|
|
|
+ color: "#ffffff20",
|
|
|
+ distance: 100,
|
|
|
+ enable: true,
|
|
|
+ opacity: 0.1,
|
|
|
+ width: 1,
|
|
|
+ },
|
|
|
+ move: {
|
|
|
+ direction: "none",
|
|
|
+ enable: true,
|
|
|
+ outModes: { default: "bounce" },
|
|
|
+ random: false,
|
|
|
+ speed: 0.5,
|
|
|
+ straight: false,
|
|
|
+ },
|
|
|
+ number: { density: { enable: true, area: 800 }, value: 80 },
|
|
|
+ opacity: { value: 0.5 },
|
|
|
+ shape: { type: "circle" },
|
|
|
+ size: { value: { min: 1, max: 3 } },
|
|
|
+ },
|
|
|
+ detectRetina: true,
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
```
|
|
|
|
|
|
-#### 加载动画
|
|
|
+### 2. 扫描线效果
|
|
|
```typescript
|
|
|
-import { Loader2 } from 'lucide-react';
|
|
|
-
|
|
|
-const LoadingSpinner = () => (
|
|
|
- <div className="flex items-center justify-center h-[400px]">
|
|
|
- <Loader2 className="h-8 w-8 animate-spin" />
|
|
|
- </div>
|
|
|
+const ScanningLine = () => (
|
|
|
+ <motion.div
|
|
|
+ className="absolute inset-0 pointer-events-none"
|
|
|
+ animate={{
|
|
|
+ background: [
|
|
|
+ 'linear-gradient(transparent 0%, rgba(0,255,136,0.1) 50%, transparent 100%)',
|
|
|
+ 'linear-gradient(transparent 100%, rgba(0,255,136,0.1) 50%, transparent 0%)',
|
|
|
+ ],
|
|
|
+ }}
|
|
|
+ transition={{
|
|
|
+ duration: 2,
|
|
|
+ repeat: Infinity,
|
|
|
+ ease: "linear",
|
|
|
+ }}
|
|
|
+ style={{
|
|
|
+ backgroundSize: '100% 200%',
|
|
|
+ backgroundPosition: '0% 0%',
|
|
|
+ }}
|
|
|
+ />
|
|
|
);
|
|
|
```
|
|
|
|
|
|
-### 实时数据更新
|
|
|
+## 实时数据更新
|
|
|
|
|
|
-#### 自动刷新
|
|
|
+### 1. WebSocket集成
|
|
|
```typescript
|
|
|
-const Dashboard = () => {
|
|
|
- const [data, setData] = useState<DashboardData | null>(null);
|
|
|
- const [autoRefresh, setAutoRefresh] = useState(true);
|
|
|
+const useWebSocketData = (url: string) => {
|
|
|
+ const [data, setData] = useState(null);
|
|
|
+ const [connected, setConnected] = useState(false);
|
|
|
|
|
|
useEffect(() => {
|
|
|
- fetchData();
|
|
|
-
|
|
|
- if (autoRefresh) {
|
|
|
- const interval = setInterval(() => {
|
|
|
- fetchData();
|
|
|
- }, 30000); // 30秒刷新一次
|
|
|
-
|
|
|
- return () => clearInterval(interval);
|
|
|
- }
|
|
|
- }, [autoRefresh]);
|
|
|
-
|
|
|
- const fetchData = async () => {
|
|
|
- try {
|
|
|
- const response = await apiClient.$get();
|
|
|
- setData(response.data);
|
|
|
- } catch (error) {
|
|
|
- console.error('Failed to fetch dashboard data:', error);
|
|
|
- }
|
|
|
- };
|
|
|
+ const ws = new WebSocket(url);
|
|
|
|
|
|
- return (
|
|
|
- <div>
|
|
|
- <div className="flex justify-between items-center mb-4">
|
|
|
- <h1 className="text-3xl font-bold">数据大屏</h1>
|
|
|
- <Button
|
|
|
- variant="outline"
|
|
|
- size="sm"
|
|
|
- onClick={() => fetchData()}
|
|
|
- >
|
|
|
- <RefreshCw className="h-4 w-4 mr-2" />
|
|
|
- 刷新数据
|
|
|
- </Button>
|
|
|
- </div>
|
|
|
- {/* 数据展示 */}
|
|
|
- </div>
|
|
|
- );
|
|
|
+ ws.onopen = () => {
|
|
|
+ setConnected(true);
|
|
|
+ };
|
|
|
+
|
|
|
+ ws.onmessage = (event) => {
|
|
|
+ const newData = JSON.parse(event.data);
|
|
|
+ setData(newData);
|
|
|
+ };
|
|
|
+
|
|
|
+ ws.onclose = () => {
|
|
|
+ setConnected(false);
|
|
|
+ // 自动重连
|
|
|
+ setTimeout(() => useWebSocketData(url), 3000);
|
|
|
+ };
|
|
|
+
|
|
|
+ return () => ws.close();
|
|
|
+ }, [url]);
|
|
|
+
|
|
|
+ return { data, connected };
|
|
|
};
|
|
|
```
|
|
|
|
|
|
-## 主题配置
|
|
|
-
|
|
|
-### 颜色配置
|
|
|
+### 2. 实时数据Hook
|
|
|
```typescript
|
|
|
-const COLORS = {
|
|
|
- primary: '#8884d8',
|
|
|
- secondary: '#82ca9d',
|
|
|
- accent: '#ffc658',
|
|
|
- danger: '#ff7c7c',
|
|
|
- warning: '#ffb347',
|
|
|
- success: '#00c49f',
|
|
|
+const useRealtimeData = (dataKey: string, options = {}) => {
|
|
|
+ const { data, connected } = useWebSocketData(`ws://localhost:3001/${dataKey}`);
|
|
|
+
|
|
|
+ return {
|
|
|
+ data,
|
|
|
+ isConnected: connected,
|
|
|
+ lastUpdated: data?.timestamp ? new Date(data.timestamp) : null,
|
|
|
+ };
|
|
|
};
|
|
|
```
|
|
|
|
|
|
-### 图表主题
|
|
|
+## 主题和样式
|
|
|
+
|
|
|
+### 1. CSS变量定义
|
|
|
+```css
|
|
|
+:root {
|
|
|
+ /* 主色调 */
|
|
|
+ --primary-cyan: #00ffff;
|
|
|
+ --primary-green: #00ff88;
|
|
|
+ --primary-purple: #ff00ff;
|
|
|
+
|
|
|
+ /* 背景色 */
|
|
|
+ --bg-primary: #000000;
|
|
|
+ --bg-secondary: #0a0a0a;
|
|
|
+ --bg-glass: rgba(255, 255, 255, 0.05);
|
|
|
+
|
|
|
+ /* 文字色 */
|
|
|
+ --text-primary: #ffffff;
|
|
|
+ --text-secondary: #a0a0a0;
|
|
|
+ --text-accent: #00ffff;
|
|
|
+
|
|
|
+ /* 边框色 */
|
|
|
+ --border-glow: rgba(0, 255, 255, 0.3);
|
|
|
+ --border-subtle: rgba(255, 255, 255, 0.1);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 2. Tailwind扩展
|
|
|
```typescript
|
|
|
-const chartConfig = {
|
|
|
- users: {
|
|
|
- label: "用户数",
|
|
|
- color: "#2563eb",
|
|
|
- },
|
|
|
- revenue: {
|
|
|
- label: "收入",
|
|
|
- color: "#16a34a",
|
|
|
- },
|
|
|
- orders: {
|
|
|
- label: "订单数",
|
|
|
- color: "#9333ea",
|
|
|
+// tailwind.config.js
|
|
|
+module.exports = {
|
|
|
+ theme: {
|
|
|
+ extend: {
|
|
|
+ animation: {
|
|
|
+ 'glow': 'glow 2s ease-in-out infinite alternate',
|
|
|
+ 'scan': 'scan 3s linear infinite',
|
|
|
+ 'pulse-slow': 'pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
|
|
+ },
|
|
|
+ keyframes: {
|
|
|
+ glow: {
|
|
|
+ 'from': { boxShadow: '0 0 20px -5px rgba(0,255,255,0.3)' },
|
|
|
+ 'to': { boxShadow: '0 0 30px 5px rgba(0,255,255,0.6)' },
|
|
|
+ },
|
|
|
+ scan: {
|
|
|
+ 'from': { transform: 'translateY(-100%)' },
|
|
|
+ 'to': { transform: 'translateY(100%)' },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
},
|
|
|
-} satisfies ChartConfig;
|
|
|
+};
|
|
|
```
|
|
|
|
|
|
-## 最佳实践
|
|
|
-
|
|
|
-### 1. 性能优化
|
|
|
-- 使用 `useMemo` 缓存计算数据
|
|
|
-- 实现数据分页加载
|
|
|
-- 使用虚拟滚动处理大量数据
|
|
|
+## 性能优化
|
|
|
|
|
|
-### 2. 用户体验
|
|
|
-- 提供数据刷新功能
|
|
|
-- 显示最后更新时间
|
|
|
-- 添加数据加载状态
|
|
|
-
|
|
|
-### 3. 无障碍设计
|
|
|
-- 为图表添加描述性标签
|
|
|
-- 提供键盘导航支持
|
|
|
-- 使用高对比度颜色
|
|
|
+### 1. 虚拟滚动
|
|
|
+```typescript
|
|
|
+const VirtualizedList = ({ items, height = 400, itemHeight = 60 }) => (
|
|
|
+ <FixedSizeList
|
|
|
+ height={height}
|
|
|
+ itemCount={items.length}
|
|
|
+ itemSize={itemHeight}
|
|
|
+ width="100%"
|
|
|
+ >
|
|
|
+ {({ index, style }) => (
|
|
|
+ <div style={style}>
|
|
|
+ <RankingItem data={items[index]} index={index} />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </FixedSizeList>
|
|
|
+);
|
|
|
+```
|
|
|
|
|
|
-### 4. 响应式设计
|
|
|
+### 2. 数据缓存策略
|
|
|
```typescript
|
|
|
-// 响应式断点
|
|
|
-const breakpoints = {
|
|
|
- sm: 640,
|
|
|
- md: 768,
|
|
|
- lg: 1024,
|
|
|
- xl: 1280,
|
|
|
- 2xl: 1536,
|
|
|
+const useOptimizedData = (key, fetcher) => {
|
|
|
+ return useQuery({
|
|
|
+ queryKey: [key],
|
|
|
+ queryFn: fetcher,
|
|
|
+ staleTime: 5 * 60 * 1000, // 5分钟
|
|
|
+ cacheTime: 10 * 60 * 1000, // 10分钟
|
|
|
+ refetchInterval: 30 * 1000, // 30秒刷新
|
|
|
+ refetchOnWindowFocus: false, // 禁用窗口聚焦刷新
|
|
|
+ retry: 3,
|
|
|
+ });
|
|
|
};
|
|
|
```
|
|
|
|
|
|
## 完整示例
|
|
|
|
|
|
-### 综合数据大屏
|
|
|
+### 主入口组件
|
|
|
```typescript
|
|
|
-import { Dashboard } from '@/client/admin-shadcn/pages/Dashboard';
|
|
|
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
|
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
|
|
+import { BrowserRouter } from 'react-router-dom';
|
|
|
+
|
|
|
+const queryClient = new QueryClient({
|
|
|
+ defaultOptions: {
|
|
|
+ queries: {
|
|
|
+ staleTime: 5 * 60 * 1000,
|
|
|
+ cacheTime: 10 * 60 * 1000,
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+const App = () => (
|
|
|
+ <QueryClientProvider client={queryClient}>
|
|
|
+ <BrowserRouter>
|
|
|
+ <FullScreenDashboard />
|
|
|
+ </BrowserRouter>
|
|
|
+ <ReactQueryDevtools initialIsOpen={false} />
|
|
|
+ </QueryClientProvider>
|
|
|
+);
|
|
|
+```
|
|
|
|
|
|
-const DashboardPage = () => {
|
|
|
- return (
|
|
|
- <div className="flex-1 space-y-4 p-4 md:p-8 pt-6">
|
|
|
- <div className="flex items-center justify-between space-y-2">
|
|
|
- <h2 className="text-3xl font-bold tracking-tight">数据大屏</h2>
|
|
|
- <div className="flex items-center space-x-2">
|
|
|
- <Button variant="outline" size="sm">
|
|
|
- <RefreshCw className="h-4 w-4 mr-2" />
|
|
|
- 刷新
|
|
|
- </Button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <Dashboard />
|
|
|
- </div>
|
|
|
- );
|
|
|
-};
|
|
|
+## 技术栈
|
|
|
+
|
|
|
+### 核心依赖
|
|
|
+```json
|
|
|
+{
|
|
|
+ "dependencies": {
|
|
|
+ "react": "^18.2.0",
|
|
|
+ "framer-motion": "^10.16.4",
|
|
|
+ "@react-three/fiber": "^8.15.11",
|
|
|
+ "@react-three/drei": "^9.88.13",
|
|
|
+ "react-particles": "^2.12.2",
|
|
|
+ "tsparticles": "^2.12.0",
|
|
|
+ "react-countup": "^6.4.2",
|
|
|
+ "react-intersection-observer": "^9.5.2",
|
|
|
+ "@tanstack/react-query": "^5.8.4",
|
|
|
+ "leaflet": "^1.9.4",
|
|
|
+ "react-leaflet": "^4.2.1",
|
|
|
+ "recharts": "^2.8.0"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 部署优化
|
|
|
+
|
|
|
+### 1. 构建配置
|
|
|
+```typescript
|
|
|
+// vite.config.ts
|
|
|
+export default defineConfig({
|
|
|
+ build: {
|
|
|
+ rollupOptions: {
|
|
|
+ output: {
|
|
|
+ manualChunks: {
|
|
|
+ 'three': ['three', '@react-three/fiber', '@react-three/drei'],
|
|
|
+ 'charts': ['recharts'],
|
|
|
+ 'leaflet': ['leaflet', 'react-leaflet'],
|
|
|
+ 'particles': ['react-particles', 'tsparticles'],
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### 2. 环境变量
|
|
|
+```bash
|
|
|
+# .env
|
|
|
+VITE_WS_URL=ws://localhost:3001
|
|
|
+VITE_API_URL=http://localhost:3000/api
|
|
|
+VITE_MAP_TILE_URL=https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png
|
|
|
```
|
|
|
|
|
|
## 组件导入清单
|
|
|
|
|
|
```typescript
|
|
|
-// UI 组件
|
|
|
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
|
|
|
+// 动画库
|
|
|
+import { motion, AnimatePresence } from 'framer-motion';
|
|
|
+import { Canvas } from '@react-three/fiber';
|
|
|
+import { Stars, OrbitControls } from '@react-three/drei';
|
|
|
+import Particles from 'react-particles';
|
|
|
+import { loadFull } from "tsparticles";
|
|
|
+
|
|
|
+// 数据可视化
|
|
|
+import CountUp from 'react-countup';
|
|
|
+import { useInView } from 'react-intersection-observer';
|
|
|
+import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
|
+import { MapContainer, TileLayer, CircleMarker, Popup } from 'react-leaflet';
|
|
|
+import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
|
|
+
|
|
|
+// UI组件
|
|
|
+import { Card } from '@/client/components/ui/card';
|
|
|
import { Button } from '@/client/components/ui/button';
|
|
|
-import { Skeleton } from '@/client/components/ui/skeleton';
|
|
|
-
|
|
|
-// 图表组件
|
|
|
-import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
|
|
+import { Badge } from '@/client/components/ui/badge';
|
|
|
|
|
|
// 图标
|
|
|
-import { Users, DollarSign, TrendingUp, TrendingDown, RefreshCw, Activity } from 'lucide-react';
|
|
|
+import {
|
|
|
+ Activity, Users, DollarSign, TrendingUp, TrendingDown,
|
|
|
+ MapPin, Clock, BarChart3, PieChart, LineChart, Globe,
|
|
|
+ Wifi, WifiOff, AlertCircle, RefreshCw, Zap
|
|
|
+} from 'lucide-react';
|
|
|
|
|
|
// 工具
|
|
|
-import { useState, useEffect, useMemo } from 'react';
|
|
|
-import { format } from 'date-fns';
|
|
|
+import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
|
+import { format } from 'date-fns';
|
|
|
+```
|
|
|
+
|
|
|
+## 使用指南
|
|
|
+
|
|
|
+### 1. 快速开始
|
|
|
+```bash
|
|
|
+npm install @tanstack/react-query framer-motion @react-three/fiber recharts leaflet
|
|
|
+```
|
|
|
+
|
|
|
+### 2. 创建大屏页面
|
|
|
+```typescript
|
|
|
+// src/pages/BigScreenDashboard.tsx
|
|
|
+import { FullScreenDashboard } from '@/components/dashboard/FullScreenDashboard';
|
|
|
+
|
|
|
+export default function BigScreenPage() {
|
|
|
+ return <FullScreenDashboard />;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3. 配置路由
|
|
|
+```typescript
|
|
|
+// src/routes.tsx
|
|
|
+import { createBrowserRouter } from 'react-router-dom';
|
|
|
+import BigScreenPage from '@/pages/BigScreenDashboard';
|
|
|
+
|
|
|
+const router = createBrowserRouter([
|
|
|
+ {
|
|
|
+ path: '/bigscreen',
|
|
|
+ element: <BigScreenPage />,
|
|
|
+ },
|
|
|
+]);
|