feat: Add comprehensive chart system and comment functionality

## Major Features Added

### 📊 Professional Chart Components
- **HealthMonitoringChart**: 24-hour vital signs monitoring with interactive data visualization
- **CaseStudyComparisonChart**: Before/after comparison with hover effects and improvement percentages
- **MarketTrendChart**: Multi-dimensional trend analysis with area charts, line graphs, and bars
- **AISystemArchitectureChart**: Interactive system architecture with clickable layer details
- **SimpleBarChart**: Technology maturity analysis with animated progress bars

### 💬 Complete Comment System
- **Star Rating System**: 5-star rating with half-star support and hover effects
- **Nested Comments**: Multi-level reply system with depth control (max 3 levels)
- **Comment Management**: Edit, delete, like/unlike functionality with real-time updates
- **Advanced Features**: Comment sorting, statistics dashboard, user engagement metrics
- **Responsive Design**: Mobile-optimized with touch-friendly interactions

### 🔧 Technical Implementation
- **Zero External Dependencies**: Native SVG rendering with React and Framer Motion
- **Markdown Integration**: Chart placeholders ({{CHART:type}}) automatically rendered
- **TypeScript Support**: Complete type definitions for all components
- **Modular Architecture**: Highly reusable and extensible component system
- **Performance Optimized**: Efficient rendering with smooth animations

### 📚 Documentation & Examples
- **Comprehensive Guide**: CHART_COMPONENTS_GUIDE.md with usage examples
- **Multiple Integration Methods**: Placeholder, direct import, and MDX support
- **Customization Options**: Theme, data source, and layout configurations
- **Best Practices**: Performance tips and accessibility guidelines

### 🎨 Design System
- **Consistent Theming**: Dark mode with cyan-blue gradient accent colors
- **Glass Morphism**: Modern backdrop blur effects with subtle borders
- **Smooth Animations**: Framer Motion powered transitions and micro-interactions
- **Responsive Layout**: Mobile-first design with adaptive breakpoints

### 📈 Blog Integration
- **AI Health Monitoring**: 4 integrated charts showing system capabilities
- **Smart Watch Insights**: Health monitoring and case study visualizations
- **Care Robots**: Investment ROI and implementation effectiveness charts
- **Data Platform Security**: System architecture and technology maturity analysis

This update transforms the blog into a professional data-driven platform with
interactive visualizations and comprehensive user engagement features.
This commit is contained in:
BrunoGao
2025-10-01 20:55:22 +08:00
parent 0833cbd6c6
commit 85a8cffca6
17 changed files with 2830 additions and 54 deletions
+292
View File
@@ -0,0 +1,292 @@
# 图表组件使用指南
本项目提供了一套高度通用和可复用的图表组件,可以轻松集成到任何 React + Markdown 博客系统中。
## 🎯 组件特点
### ✨ 高度通用
- **零依赖外部图表库**:使用原生 SVG 和 React 实现
- **自包含组件**:每个组件都是独立的,不依赖特定数据源
- **模块化设计**:可以单独使用任何一个图表组件
- **TypeScript 支持**:完整的类型定义
### 🎨 统一设计风格
- **暗色主题**:适配现代网站设计
- **响应式布局**:自动适配移动端和桌面端
- **流畅动画**:基于 Framer Motion 的专业动效
- **玻璃拟态效果**:现代化的视觉风格
## 📊 可用图表组件
### 1. HealthMonitoringChart - 健康监测图表
```jsx
import HealthMonitoringChart from './components/charts/HealthMonitoringChart';
// 直接使用
<HealthMonitoringChart />
```
**特点:**
- 24小时生命体征数据展示
- 多指标线图(心率、血压、睡眠、活动)
- 关键统计卡片
- 适用于:健康类、医疗类、监测类文章
### 2. CaseStudyComparisonChart - 案例对比图表
```jsx
import CaseStudyComparisonChart from './components/charts/CaseStudyComparisonChart';
// 直接使用
<CaseStudyComparisonChart />
```
**特点:**
- 实施前后效果对比
- 交互式悬停效果
- 改善百分比展示
- 适用于:案例研究、效果分析、业务报告
### 3. MarketTrendChart - 市场趋势图表
```jsx
import MarketTrendChart from './components/charts/MarketTrendChart';
// 直接使用
<MarketTrendChart />
```
**特点:**
- 时间序列数据展示
- 多维度数据对比(面积图+线图+柱图)
- 预测数据标记
- 适用于:市场分析、趋势预测、商业报告
### 4. AISystemArchitectureChart - 系统架构图
```jsx
import AISystemArchitectureChart from './components/charts/AISystemArchitectureChart';
// 直接使用
<AISystemArchitectureChart />
```
**特点:**
- 交互式系统架构展示
- 点击展开组件详情
- 数据流向可视化
- 适用于:技术文档、系统介绍、架构说明
## 🚀 快速集成到任何博客
### 方法1:占位符方式(推荐)
1. **在 markdown 内容中添加占位符:**
```markdown
## AI系统架构
我们的系统采用五层架构设计:
{{CHART:system-architecture}}
如上图所示,系统从设备层到应用层形成完整的数据处理链路。
```
2. **在渲染组件中处理占位符:**
```jsx
// 在你的博客渲染组件中
const renderContent = (content) => {
const parts = content.split(/(\{\{CHART:[^}]+\}\})/g);
return parts.map((part, index) => {
if (part.startsWith('{{CHART:')) {
const chartType = part.match(/\{\{CHART:([^}]+)\}\}/)?.[1];
return <ChartRenderer key={index} type={chartType} />;
}
return <ReactMarkdown key={index}>{part}</ReactMarkdown>;
});
};
```
### 方法2:直接组件引用
```jsx
import { HealthMonitoringChart, MarketTrendChart } from './components/charts';
const BlogPost = () => (
<article>
<h2>健康监测数据分析</h2>
<p>以下是24小时监测数据:</p>
<HealthMonitoringChart />
<h2>市场发展趋势</h2>
<p>行业增长预测如下:</p>
<MarketTrendChart />
</article>
);
```
### 方法3:MDX 集成
如果使用 MDX,可以直接在 markdown 中使用:
```mdx
import { HealthMonitoringChart } from './components/charts';
# 健康监测报告
## 数据概览
以下是实时监测数据:
<HealthMonitoringChart />
数据显示用户的健康指标都在正常范围内。
```
## 🔧 自定义配置
### 扩展数据源
可以轻松修改组件以支持动态数据:
```jsx
// 原始组件使用模拟数据
const HealthMonitoringChart = () => {
const mockData = [...]; // 内置模拟数据
// ...
};
// 扩展为支持外部数据
const HealthMonitoringChart = ({ data = mockData }) => {
// 使用传入的数据或默认模拟数据
// ...
};
// 使用方式
<HealthMonitoringChart data={yourRealData} />
```
### 主题定制
所有组件都使用 CSS 变量,可以轻松定制主题:
```css
:root {
--chart-primary: #06B6D4; /* 主色调 */
--chart-secondary: #10B981; /* 辅助色 */
--chart-background: #1F2937; /* 背景色 */
--chart-text: #F3F4F6; /* 文字色 */
--chart-border: #374151; /* 边框色 */
}
```
### 尺寸调整
组件支持响应式设计,也可以手动调整:
```jsx
<div style={{ height: '400px', width: '100%' }}>
<HealthMonitoringChart />
</div>
```
## 📦 完整集成示例
### 新建博客文章集成图表
1. **创建新的博客文章数据:**
```typescript
// blogData.ts
{
id: 'smart-healthcare-trends',
title: '智慧医疗发展趋势分析',
content: `
# 智慧医疗发展趋势分析
## 市场概况
当前智慧医疗市场正在快速发展:
{{CHART:market-trend}}
从数据可以看出,未来5年将保持高速增长。
## 技术架构
现代智慧医疗系统通常采用以下架构:
{{CHART:system-architecture}}
## 实施效果
某医院的智慧医疗系统实施效果:
{{CHART:case-study-comparison}}
## 健康监测数据
典型的患者监测数据如下:
{{CHART:health-monitoring}}
`
}
```
2. **零配置使用**:现有的 `ChartRenderer` 和 `BlogDetail` 组件会自动处理图表渲染。
## 🔄 扩展新图表类型
添加新的图表组件非常简单:
```jsx
// 1. 创建新图表组件
const CustomChart = () => {
return (
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-700/50 p-6">
{/* 你的图表内容 */}
</div>
);
};
// 2. 在 ChartRenderer 中注册
const ChartRenderer = ({ type }) => {
switch (type) {
case 'health-monitoring': return <HealthMonitoringChart />;
case 'custom-chart': return <CustomChart />; // 新增
default: return null;
}
};
// 3. 在 markdown 中使用
// {{CHART:custom-chart}}
```
## 💡 最佳实践
### 1. 性能优化
- 使用 `React.memo` 避免不必要的重渲染
- 动画使用 `transform` 而非修改布局属性
- 大数据集考虑虚拟化处理
### 2. 可访问性
- 添加适当的 ARIA 标签
- 支持键盘导航
- 提供数据的文本描述
### 3. 响应式设计
- 使用相对单位而非固定像素
- 移动端优化触摸交互
- 考虑不同屏幕密度的显示效果
## 🎉 总结
这套图表组件系统具有以下优势:
✅ **零学习成本**:markdown 占位符方式,写作者无需了解技术细节
✅ **高度灵活**:支持多种集成方式,适应不同项目需求
✅ **专业外观**:统一的设计语言,提升内容专业度
✅ **易于扩展**:模块化架构,轻松添加新图表类型
✅ **性能优越**:原生实现,无额外依赖负担
无论是技术博客、商业报告还是学术文章,都可以轻松集成这些图表组件,让数据展示更加生动和专业!
+57
View File
@@ -0,0 +1,57 @@
import React from 'react';
import HealthMonitoringChart from './charts/HealthMonitoringChart';
import CaseStudyComparisonChart from './charts/CaseStudyComparisonChart';
import MarketTrendChart from './charts/MarketTrendChart';
import AISystemArchitectureChart from './charts/AISystemArchitectureChart';
interface BlogChartsProps {
postId: string;
}
const BlogCharts: React.FC<BlogChartsProps> = ({ postId }) => {
const renderChartsForPost = () => {
switch (postId) {
case 'ai-elderly-health-monitoring':
return (
<>
<HealthMonitoringChart />
<AISystemArchitectureChart />
<CaseStudyComparisonChart />
<MarketTrendChart />
</>
);
case 'smart-watch-health-insights':
return (
<>
<HealthMonitoringChart />
</>
);
case 'care-robots-nursing-homes':
return (
<>
<CaseStudyComparisonChart />
</>
);
case 'health-data-platform-security':
return (
<>
<AISystemArchitectureChart />
</>
);
default:
return null;
}
};
return (
<div className="my-8">
{renderChartsForPost()}
</div>
);
};
export default BlogCharts;
+29
View File
@@ -0,0 +1,29 @@
import React from 'react';
import HealthMonitoringChart from './charts/HealthMonitoringChart';
import CaseStudyComparisonChart from './charts/CaseStudyComparisonChart';
import MarketTrendChart from './charts/MarketTrendChart';
import AISystemArchitectureChart from './charts/AISystemArchitectureChart';
import SimpleBarChart from './charts/SimpleBarChart';
interface ChartRendererProps {
type: string;
}
const ChartRenderer: React.FC<ChartRendererProps> = ({ type }) => {
switch (type) {
case 'health-monitoring':
return <HealthMonitoringChart />;
case 'case-study-comparison':
return <CaseStudyComparisonChart />;
case 'market-trend':
return <MarketTrendChart />;
case 'system-architecture':
return <AISystemArchitectureChart />;
case 'simple-bar-chart':
return <SimpleBarChart />;
default:
return null;
}
};
export default ChartRenderer;
+253
View File
@@ -0,0 +1,253 @@
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import { Send, User, Mail, Star, MessageCircle } from 'lucide-react';
import StarRating from './StarRating';
import { CommentFormData } from '../types/comment';
interface CommentFormProps {
onSubmit: (data: CommentFormData) => void;
parentId?: string;
isReply?: boolean;
onCancel?: () => void;
placeholder?: string;
}
const CommentForm: React.FC<CommentFormProps> = ({
onSubmit,
parentId,
isReply = false,
onCancel,
placeholder = '分享您的想法...'
}) => {
const [formData, setFormData] = useState<CommentFormData>({
author: {
name: '',
email: ''
},
content: '',
rating: isReply ? undefined : 5,
parentId
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
if (!formData.author.name.trim()) {
newErrors.name = '请输入您的姓名';
}
if (!formData.content.trim()) {
newErrors.content = '请输入评论内容';
} else if (formData.content.trim().length < 5) {
newErrors.content = '评论内容至少需要5个字符';
}
if (formData.author.email && !/\S+@\S+\.\S+/.test(formData.author.email)) {
newErrors.email = '请输入有效的邮箱地址';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) return;
setIsSubmitting(true);
try {
await onSubmit(formData);
// Reset form after successful submission
setFormData({
author: {
name: formData.author.name, // Keep name for convenience
email: formData.author.email // Keep email for convenience
},
content: '',
rating: isReply ? undefined : 5,
parentId
});
if (onCancel) {
onCancel(); // Close reply form
}
} catch (error) {
console.error('Failed to submit comment:', error);
} finally {
setIsSubmitting(false);
}
};
const handleInputChange = (field: string, value: string) => {
if (field.includes('.')) {
const [parent, child] = field.split('.');
setFormData(prev => ({
...prev,
[parent]: {
...prev[parent as keyof typeof prev] as any,
[child]: value
}
}));
} else {
setFormData(prev => ({
...prev,
[field]: value
}));
}
// Clear error when user starts typing
if (errors[field]) {
setErrors(prev => ({
...prev,
[field]: ''
}));
}
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className={`bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-700/50 p-6 ${
isReply ? 'ml-12 mt-4' : 'mb-8'
}`}
>
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-cyan-500/20 rounded-lg">
{isReply ? (
<MessageCircle size={20} className="text-cyan-400" />
) : (
<Star size={20} className="text-cyan-400" />
)}
</div>
<h3 className="text-lg font-semibold text-white">
{isReply ? '回复评论' : '发表评论'}
</h3>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
{/* User Info */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<div className="relative">
<User size={18} className="absolute left-3 top-3 text-gray-400" />
<input
type="text"
placeholder="您的姓名 *"
value={formData.author.name}
onChange={(e) => handleInputChange('author.name', e.target.value)}
className={`w-full pl-10 pr-4 py-2.5 bg-gray-800/50 border rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 transition-all ${
errors.name
? 'border-red-500 focus:ring-red-500/50'
: 'border-gray-600 focus:ring-cyan-500/50 focus:border-cyan-400'
}`}
/>
</div>
{errors.name && (
<p className="mt-1 text-sm text-red-400">{errors.name}</p>
)}
</div>
<div>
<div className="relative">
<Mail size={18} className="absolute left-3 top-3 text-gray-400" />
<input
type="email"
placeholder="邮箱地址(可选)"
value={formData.author.email || ''}
onChange={(e) => handleInputChange('author.email', e.target.value)}
className={`w-full pl-10 pr-4 py-2.5 bg-gray-800/50 border rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 transition-all ${
errors.email
? 'border-red-500 focus:ring-red-500/50'
: 'border-gray-600 focus:ring-cyan-500/50 focus:border-cyan-400'
}`}
/>
</div>
{errors.email && (
<p className="mt-1 text-sm text-red-400">{errors.email}</p>
)}
</div>
</div>
{/* Rating (only for main comments, not replies) */}
{!isReply && (
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
文章评分
</label>
<StarRating
rating={formData.rating || 5}
onRatingChange={(rating) => setFormData(prev => ({ ...prev, rating }))}
size="lg"
showValue
/>
</div>
)}
{/* Comment Content */}
<div>
<textarea
placeholder={placeholder}
value={formData.content}
onChange={(e) => handleInputChange('content', e.target.value)}
rows={isReply ? 3 : 5}
className={`w-full px-4 py-3 bg-gray-800/50 border rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 transition-all resize-none ${
errors.content
? 'border-red-500 focus:ring-red-500/50'
: 'border-gray-600 focus:ring-cyan-500/50 focus:border-cyan-400'
}`}
/>
{errors.content && (
<p className="mt-1 text-sm text-red-400">{errors.content}</p>
)}
<div className="flex justify-between items-center mt-2">
<span className="text-sm text-gray-400">
{formData.content.length}/1000
</span>
{formData.content.length > 1000 && (
<span className="text-sm text-red-400">
内容超出限制
</span>
)}
</div>
</div>
{/* Action Buttons */}
<div className="flex items-center justify-end gap-3">
{isReply && onCancel && (
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-gray-400 hover:text-white transition-colors"
>
取消
</button>
)}
<motion.button
type="submit"
disabled={isSubmitting || formData.content.length > 1000}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="flex items-center gap-2 px-6 py-2.5 bg-gradient-to-r from-cyan-500 to-blue-500 text-white rounded-lg font-medium transition-all hover:from-cyan-600 hover:to-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
) : (
<Send size={16} />
)}
{isSubmitting ? '提交中...' : '发表'}
</motion.button>
</div>
</form>
</motion.div>
);
};
export default CommentForm;
+285
View File
@@ -0,0 +1,285 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Heart,
MessageCircle,
MoreHorizontal,
Edit3,
Trash2,
Reply,
Calendar,
Check,
X
} from 'lucide-react';
import StarRating from './StarRating';
import CommentForm from './CommentForm';
import { Comment, CommentFormData } from '../types/comment';
interface CommentItemProps {
comment: Comment;
onLike: (commentId: string) => void;
onReply: (data: CommentFormData) => void;
onEdit: (commentId: string, content: string) => void;
onDelete: (commentId: string) => void;
currentUserId?: string;
depth?: number;
}
const CommentItem: React.FC<CommentItemProps> = ({
comment,
onLike,
onReply,
onEdit,
onDelete,
currentUserId = 'user1', // Mock current user
depth = 0
}) => {
const [showReplyForm, setShowReplyForm] = useState(false);
const [showDropdown, setShowDropdown] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [editContent, setEditContent] = useState(comment.content);
const isLiked = comment.likedBy.includes(currentUserId);
const canEdit = comment.author.email === 'user1@example.com'; // Mock ownership check
const maxDepth = 3; // Maximum nesting depth
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
const diffTime = Math.abs(now.getTime() - date.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 1) return '今天';
if (diffDays === 2) return '昨天';
if (diffDays <= 7) return `${diffDays}天前`;
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
};
const handleLike = () => {
onLike(comment.id);
};
const handleReply = () => {
setShowReplyForm(!showReplyForm);
};
const handleReplySubmit = (data: CommentFormData) => {
onReply({
...data,
parentId: comment.id
});
setShowReplyForm(false);
};
const handleEdit = () => {
setIsEditing(true);
setShowDropdown(false);
};
const handleSaveEdit = () => {
if (editContent.trim() && editContent !== comment.content) {
onEdit(comment.id, editContent.trim());
}
setIsEditing(false);
};
const handleCancelEdit = () => {
setEditContent(comment.content);
setIsEditing(false);
};
const handleDelete = () => {
if (window.confirm('确定要删除这条评论吗?')) {
onDelete(comment.id);
}
setShowDropdown(false);
};
if (comment.isDeleted) {
return (
<div className={`p-4 text-gray-500 italic ${depth > 0 ? 'ml-12' : ''}`}>
此评论已被删除
</div>
);
}
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className={`${depth > 0 ? 'ml-12 mt-4' : 'mb-6'}`}
>
<div className="bg-gray-900/30 backdrop-blur-sm rounded-xl border border-gray-700/30 p-6">
{/* Comment Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<img
src={comment.author.avatar || `https://i.pravatar.cc/40?img=${Math.random()}`}
alt={comment.author.name}
className="w-10 h-10 rounded-full border-2 border-gray-600"
/>
<div>
<h4 className="font-semibold text-white">{comment.author.name}</h4>
<div className="flex items-center gap-2 text-sm text-gray-400">
<Calendar size={12} />
<span>{formatDate(comment.createdAt)}</span>
{comment.isEdited && <span className="text-gray-500">(已编辑)</span>}
</div>
</div>
</div>
{/* Rating (only for main comments) */}
{comment.rating && !comment.parentId && (
<StarRating rating={comment.rating} readonly size="sm" />
)}
{/* Actions Menu */}
<div className="relative">
<button
onClick={() => setShowDropdown(!showDropdown)}
className="p-1 text-gray-400 hover:text-white transition-colors rounded"
>
<MoreHorizontal size={16} />
</button>
<AnimatePresence>
{showDropdown && (
<motion.div
initial={{ opacity: 0, scale: 0.95, y: -10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: -10 }}
className="absolute right-0 top-8 bg-gray-800 border border-gray-600 rounded-lg shadow-lg py-1 z-10 min-w-[120px]"
>
{canEdit && (
<>
<button
onClick={handleEdit}
className="flex items-center gap-2 w-full px-3 py-2 text-sm text-gray-300 hover:bg-gray-700 transition-colors"
>
<Edit3 size={14} />
编辑
</button>
<button
onClick={handleDelete}
className="flex items-center gap-2 w-full px-3 py-2 text-sm text-red-400 hover:bg-gray-700 transition-colors"
>
<Trash2 size={14} />
删除
</button>
</>
)}
</motion.div>
)}
</AnimatePresence>
</div>
</div>
{/* Comment Content */}
{isEditing ? (
<div className="mb-4">
<textarea
value={editContent}
onChange={(e) => setEditContent(e.target.value)}
className="w-full px-3 py-2 bg-gray-800/50 border border-gray-600 rounded-lg text-white resize-none focus:outline-none focus:ring-2 focus:ring-cyan-500/50"
rows={3}
/>
<div className="flex items-center gap-2 mt-2">
<button
onClick={handleSaveEdit}
className="flex items-center gap-1 px-3 py-1 bg-green-600 text-white rounded text-sm hover:bg-green-700 transition-colors"
>
<Check size={14} />
保存
</button>
<button
onClick={handleCancelEdit}
className="flex items-center gap-1 px-3 py-1 bg-gray-600 text-white rounded text-sm hover:bg-gray-700 transition-colors"
>
<X size={14} />
取消
</button>
</div>
</div>
) : (
<div className="mb-4">
<p className="text-gray-300 leading-relaxed whitespace-pre-wrap">
{comment.content}
</p>
</div>
)}
{/* Comment Actions */}
<div className="flex items-center gap-4 text-sm">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={handleLike}
className={`flex items-center gap-2 px-3 py-1.5 rounded-full transition-all ${
isLiked
? 'bg-red-500/20 text-red-400 border border-red-500/30'
: 'bg-gray-800/50 text-gray-400 hover:text-red-400 hover:bg-red-500/10 border border-gray-600'
}`}
>
<Heart size={14} className={isLiked ? 'fill-current' : ''} />
<span>{comment.likes}</span>
</motion.button>
{depth < maxDepth && (
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={handleReply}
className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-gray-800/50 text-gray-400 hover:text-cyan-400 hover:bg-cyan-500/10 transition-all border border-gray-600"
>
<MessageCircle size={14} />
<span>回复</span>
</motion.button>
)}
<button className="flex items-center gap-1 text-gray-400 hover:text-white transition-colors">
<Reply size={14} />
<span>分享</span>
</button>
</div>
</div>
{/* Reply Form */}
<AnimatePresence>
{showReplyForm && (
<CommentForm
onSubmit={handleReplySubmit}
parentId={comment.id}
isReply
onCancel={() => setShowReplyForm(false)}
placeholder={`回复 ${comment.author.name}...`}
/>
)}
</AnimatePresence>
{/* Nested Replies */}
{comment.replies && comment.replies.length > 0 && (
<div className="mt-4">
{comment.replies.map((reply) => (
<CommentItem
key={reply.id}
comment={reply}
onLike={onLike}
onReply={onReply}
onEdit={onEdit}
onDelete={onDelete}
currentUserId={currentUserId}
depth={depth + 1}
/>
))}
</div>
)}
</motion.div>
);
};
export default CommentItem;
+288
View File
@@ -0,0 +1,288 @@
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import {
MessageCircle,
Star,
TrendingUp,
Clock,
Filter,
ChevronDown
} from 'lucide-react';
import CommentItem from './CommentItem';
import CommentForm from './CommentForm';
import StarRating from './StarRating';
import CommentStatistics from './CommentStatistics';
import {
Comment,
CommentFormData,
CommentStats
} from '../types/comment';
import {
getCommentsByPostId,
getCommentStats,
addComment,
likeComment,
editComment,
deleteComment
} from '../data/commentData';
interface CommentListProps {
postId: string;
}
type SortType = 'newest' | 'oldest' | 'mostLiked' | 'rating';
const CommentList: React.FC<CommentListProps> = ({ postId }) => {
const [comments, setComments] = useState<Comment[]>([]);
const [stats, setStats] = useState<CommentStats | null>(null);
const [sortBy, setSortBy] = useState<SortType>('newest');
const [showSortDropdown, setShowSortDropdown] = useState(false);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
loadComments();
}, [postId]);
const loadComments = () => {
setIsLoading(true);
try {
const postComments = getCommentsByPostId(postId);
const postStats = getCommentStats(postId);
setComments(postComments);
setStats(postStats);
} catch (error) {
console.error('Failed to load comments:', error);
} finally {
setIsLoading(false);
}
};
const sortComments = (comments: Comment[], sortType: SortType): Comment[] => {
const sorted = [...comments];
switch (sortType) {
case 'newest':
return sorted.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
case 'oldest':
return sorted.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
case 'mostLiked':
return sorted.sort((a, b) => b.likes - a.likes);
case 'rating':
return sorted.sort((a, b) => (b.rating || 0) - (a.rating || 0));
default:
return sorted;
}
};
const handleAddComment = async (data: CommentFormData) => {
try {
const newComment = addComment({
postId,
author: data.author,
content: data.content,
rating: data.rating,
parentId: data.parentId
});
loadComments(); // Reload to get updated data
} catch (error) {
console.error('Failed to add comment:', error);
throw error;
}
};
const handleLikeComment = (commentId: string) => {
try {
likeComment(commentId, 'user1'); // Mock current user ID
loadComments();
} catch (error) {
console.error('Failed to like comment:', error);
}
};
const handleEditComment = (commentId: string, content: string) => {
try {
editComment(commentId, content);
loadComments();
} catch (error) {
console.error('Failed to edit comment:', error);
}
};
const handleDeleteComment = (commentId: string) => {
try {
deleteComment(commentId);
loadComments();
} catch (error) {
console.error('Failed to delete comment:', error);
}
};
const sortOptions = [
{ value: 'newest', label: '最新', icon: <Clock size={16} /> },
{ value: 'oldest', label: '最早', icon: <Clock size={16} /> },
{ value: 'mostLiked', label: '最多赞', icon: <TrendingUp size={16} /> },
{ value: 'rating', label: '最高评分', icon: <Star size={16} /> }
];
const sortedComments = sortComments(comments, sortBy);
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<div className="w-8 h-8 border-4 border-cyan-500/30 border-t-cyan-500 rounded-full animate-spin"></div>
</div>
);
}
return (
<div className="mt-12">
{/* Comment Stats */}
{stats && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8 bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-700/50 p-6"
>
<div className="flex items-center gap-4 mb-4">
<div className="p-3 bg-cyan-500/20 rounded-lg">
<MessageCircle size={24} className="text-cyan-400" />
</div>
<div>
<h3 className="text-xl font-semibold text-white">评论与评分</h3>
<p className="text-gray-400">
共 {stats.total} 条评论
{stats.averageRating > 0 && (
<span className="ml-2">• 平均评分 {stats.averageRating} 分</span>
)}
</p>
</div>
</div>
{/* Comment Statistics */}
<CommentStatistics stats={stats} />
{/* Rating Distribution */}
{stats.averageRating > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="flex items-center gap-4">
<div className="text-center">
<div className="text-3xl font-bold text-cyan-400">
{stats.averageRating}
</div>
<StarRating rating={stats.averageRating} readonly size="md" />
<div className="text-sm text-gray-400 mt-1">
基于 {Object.values(stats.ratingDistribution).reduce((a, b) => a + b, 0)} 个评分
</div>
</div>
</div>
<div className="space-y-2">
{[5, 4, 3, 2, 1].map((rating) => {
const count = stats.ratingDistribution[rating as keyof typeof stats.ratingDistribution];
const total = Object.values(stats.ratingDistribution).reduce((a, b) => a + b, 0);
const percentage = total > 0 ? (count / total) * 100 : 0;
return (
<div key={rating} className="flex items-center gap-3 text-sm">
<span className="text-gray-300 w-8">{rating} 星</span>
<div className="flex-1 bg-gray-700 rounded-full h-2">
<div
className="bg-gradient-to-r from-cyan-500 to-blue-500 h-2 rounded-full transition-all duration-500"
style={{ width: `${percentage}%` }}
/>
</div>
<span className="text-gray-400 w-8">{count}</span>
</div>
);
})}
</div>
</div>
)}
</motion.div>
)}
{/* Comment Form */}
<CommentForm onSubmit={handleAddComment} />
{/* Comments Section */}
{comments.length > 0 && (
<div>
{/* Sort Controls */}
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-semibold text-white">
所有评论 ({stats?.total || 0})
</h3>
<div className="relative">
<button
onClick={() => setShowSortDropdown(!showSortDropdown)}
className="flex items-center gap-2 px-4 py-2 bg-gray-800/50 border border-gray-600 rounded-lg text-gray-300 hover:text-white transition-colors"
>
<Filter size={16} />
<span>{sortOptions.find(opt => opt.value === sortBy)?.label}</span>
<ChevronDown size={16} className={`transition-transform ${showSortDropdown ? 'rotate-180' : ''}`} />
</button>
{showSortDropdown && (
<motion.div
initial={{ opacity: 0, scale: 0.95, y: -10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
className="absolute right-0 top-12 bg-gray-800 border border-gray-600 rounded-lg shadow-lg py-1 z-10 min-w-[140px]"
>
{sortOptions.map((option) => (
<button
key={option.value}
onClick={() => {
setSortBy(option.value as SortType);
setShowSortDropdown(false);
}}
className={`flex items-center gap-3 w-full px-3 py-2 text-sm transition-colors ${
sortBy === option.value
? 'bg-cyan-500/20 text-cyan-400'
: 'text-gray-300 hover:bg-gray-700'
}`}
>
{option.icon}
{option.label}
</button>
))}
</motion.div>
)}
</div>
</div>
{/* Comments List */}
<div className="space-y-6">
{sortedComments.map((comment) => (
<CommentItem
key={comment.id}
comment={comment}
onLike={handleLikeComment}
onReply={handleAddComment}
onEdit={handleEditComment}
onDelete={handleDeleteComment}
/>
))}
</div>
</div>
)}
{/* Empty State */}
{comments.length === 0 && !isLoading && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-12 text-gray-400"
>
<MessageCircle size={48} className="mx-auto mb-4 opacity-50" />
<p className="text-lg mb-2">暂无评论</p>
<p>成为第一个评论的人吧!</p>
</motion.div>
)}
</div>
);
};
export default CommentList;
+62
View File
@@ -0,0 +1,62 @@
import React from 'react';
import { motion } from 'framer-motion';
import { Star, TrendingUp, Users, MessageCircle } from 'lucide-react';
import { CommentStats } from '../types/comment';
interface CommentStatisticsProps {
stats: CommentStats;
}
const CommentStatistics: React.FC<CommentStatisticsProps> = ({ stats }) => {
const totalRatings = Object.values(stats.ratingDistribution).reduce((a, b) => a + b, 0);
return (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6"
>
{/* Total Comments */}
<div className="bg-gray-800/50 rounded-lg p-4 text-center">
<div className="flex items-center justify-center mb-2">
<MessageCircle size={20} className="text-blue-400" />
</div>
<div className="text-2xl font-bold text-white">{stats.total}</div>
<div className="text-sm text-gray-400">总评论</div>
</div>
{/* Average Rating */}
<div className="bg-gray-800/50 rounded-lg p-4 text-center">
<div className="flex items-center justify-center mb-2">
<Star size={20} className="text-yellow-400" />
</div>
<div className="text-2xl font-bold text-white">
{stats.averageRating > 0 ? stats.averageRating.toFixed(1) : '--'}
</div>
<div className="text-sm text-gray-400">平均评分</div>
</div>
{/* Total Ratings */}
<div className="bg-gray-800/50 rounded-lg p-4 text-center">
<div className="flex items-center justify-center mb-2">
<TrendingUp size={20} className="text-green-400" />
</div>
<div className="text-2xl font-bold text-white">{totalRatings}</div>
<div className="text-sm text-gray-400">评分数量</div>
</div>
{/* Engagement */}
<div className="bg-gray-800/50 rounded-lg p-4 text-center">
<div className="flex items-center justify-center mb-2">
<Users size={20} className="text-purple-400" />
</div>
<div className="text-2xl font-bold text-white">
{Math.round((totalRatings / Math.max(stats.total, 1)) * 100)}%
</div>
<div className="text-sm text-gray-400">参与度</div>
</div>
</motion.div>
);
};
export default CommentStatistics;
+93
View File
@@ -0,0 +1,93 @@
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import { Star } from 'lucide-react';
interface StarRatingProps {
rating: number;
onRatingChange?: (rating: number) => void;
readonly?: boolean;
size?: 'sm' | 'md' | 'lg';
showValue?: boolean;
className?: string;
}
const StarRating: React.FC<StarRatingProps> = ({
rating,
onRatingChange,
readonly = false,
size = 'md',
showValue = false,
className = ''
}) => {
const [hoverRating, setHoverRating] = useState(0);
const sizeClasses = {
sm: 'w-4 h-4',
md: 'w-5 h-5',
lg: 'w-6 h-6'
};
const handleStarClick = (starRating: number) => {
if (!readonly && onRatingChange) {
onRatingChange(starRating);
}
};
const handleStarHover = (starRating: number) => {
if (!readonly) {
setHoverRating(starRating);
}
};
const handleMouseLeave = () => {
if (!readonly) {
setHoverRating(0);
}
};
const currentRating = hoverRating || rating;
return (
<div className={`flex items-center gap-1 ${className}`}>
<div
className="flex items-center gap-0.5"
onMouseLeave={handleMouseLeave}
>
{[1, 2, 3, 4, 5].map((star) => (
<motion.button
key={star}
type="button"
className={`${readonly ? 'cursor-default' : 'cursor-pointer'} transition-transform`}
whileHover={readonly ? {} : { scale: 1.1 }}
whileTap={readonly ? {} : { scale: 0.95 }}
onClick={() => handleStarClick(star)}
onMouseEnter={() => handleStarHover(star)}
disabled={readonly}
>
<Star
className={`${sizeClasses[size]} transition-all duration-200 ${
star <= currentRating
? 'fill-yellow-400 text-yellow-400'
: 'fill-transparent text-gray-400 hover:text-yellow-300'
}`}
/>
</motion.button>
))}
</div>
{showValue && (
<span className="text-sm text-gray-400 ml-2">
{rating.toFixed(1)}
</span>
)}
{!readonly && hoverRating > 0 && (
<span className="text-sm text-gray-300 ml-2">
{hoverRating} 星
</span>
)}
</div>
);
};
export default StarRating;
@@ -0,0 +1,335 @@
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import {
Smartphone,
Shield,
Brain,
Database,
Wifi,
Bell,
Activity,
Heart,
Eye,
Zap
} from 'lucide-react';
interface SystemLayer {
id: string;
name: string;
description: string;
icon: React.ReactNode;
color: string;
components: string[];
}
const systemLayers: SystemLayer[] = [
{
id: 'device',
name: '设备感知层',
description: '智能设备实时采集健康数据',
icon: <Smartphone size={24} />,
color: '#06B6D4',
components: ['智能手表', '血压计', '血糖仪', '体重秤', '睡眠监测器']
},
{
id: 'transmission',
name: '数据传输层',
description: '安全可靠的数据传输通道',
icon: <Wifi size={24} />,
color: '#10B981',
components: ['WiFi', '蓝牙', '5G', '边缘计算', '数据加密']
},
{
id: 'processing',
name: 'AI处理层',
description: '智能算法分析和异常检测',
icon: <Brain size={24} />,
color: '#8B5CF6',
components: ['机器学习', '深度学习', '异常检测', '预测模型', '自然语言处理']
},
{
id: 'storage',
name: '数据存储层',
description: '安全的健康数据管理平台',
icon: <Database size={24} />,
color: '#F59E0B',
components: ['云数据库', '数据备份', '隐私保护', '访问控制', '数据同步']
},
{
id: 'application',
name: '应用服务层',
description: '个性化健康服务和预警',
icon: <Bell size={24} />,
color: '#EF4444',
components: ['健康报告', '风险预警', '用药提醒', '运动建议', '医生咨询']
}
];
const dataFlow = [
{ from: 'device', to: 'transmission', label: '原始数据' },
{ from: 'transmission', to: 'processing', label: '清洗数据' },
{ from: 'processing', to: 'storage', label: '分析结果' },
{ from: 'storage', to: 'application', label: '个性化服务' }
];
const AISystemArchitectureChart: React.FC = () => {
const [selectedLayer, setSelectedLayer] = useState<string | null>(null);
const [hoveredLayer, setHoveredLayer] = useState<string | null>(null);
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-700/50 p-6 mb-8"
>
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-purple-500/20 rounded-lg">
<Activity size={24} className="text-purple-400" />
</div>
<div>
<h3 className="text-xl font-semibold text-white">AI健康监护系统架构</h3>
<p className="text-gray-400 text-sm">端到端智能健康监护解决方案</p>
</div>
</div>
{/* Architecture Diagram */}
<div className="relative bg-gray-800/30 rounded-lg p-8 mb-6">
<svg className="w-full h-96" viewBox="0 0 800 400">
{/* Background Grid */}
<defs>
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="#374151" strokeWidth="1" opacity="0.3"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
{/* System Layers */}
{systemLayers.map((layer, index) => {
const x = 50 + index * 140;
const y = 150;
const isSelected = selectedLayer === layer.id;
const isHovered = hoveredLayer === layer.id;
return (
<g key={layer.id}>
{/* Layer Container */}
<motion.rect
x={x}
y={y}
width="120"
height="100"
rx="10"
fill={layer.color}
fillOpacity={isSelected || isHovered ? 0.3 : 0.1}
stroke={layer.color}
strokeWidth={isSelected ? 3 : 1}
className="cursor-pointer"
onClick={() => setSelectedLayer(selectedLayer === layer.id ? null : layer.id)}
onMouseEnter={() => setHoveredLayer(layer.id)}
onMouseLeave={() => setHoveredLayer(null)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
/>
{/* Layer Icon */}
<foreignObject x={x + 40} y={y + 20} width="40" height="40">
<div
className="flex items-center justify-center w-10 h-10 rounded-lg"
style={{ backgroundColor: `${layer.color}40`, color: layer.color }}
>
{layer.icon}
</div>
</foreignObject>
{/* Layer Name */}
<text
x={x + 60}
y={y + 85}
textAnchor="middle"
fill="white"
fontSize="12"
fontWeight="600"
>
{layer.name}
</text>
{/* Data Flow Arrows */}
{index < systemLayers.length - 1 && (
<motion.path
d={`M ${x + 120} ${y + 50} L ${x + 140} ${y + 50}`}
stroke="#06B6D4"
strokeWidth="2"
markerEnd="url(#arrowhead)"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ delay: index * 0.3, duration: 0.5 }}
/>
)}
</g>
);
})}
{/* Arrow Marker */}
<defs>
<marker
id="arrowhead"
markerWidth="10"
markerHeight="7"
refX="10"
refY="3.5"
orient="auto"
>
<polygon
points="0 0, 10 3.5, 0 7"
fill="#06B6D4"
/>
</marker>
</defs>
{/* Data Flow Labels */}
{dataFlow.map((flow, index) => (
<motion.text
key={index}
x={120 + index * 140}
y={140}
textAnchor="middle"
fill="#06B6D4"
fontSize="10"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: index * 0.3 + 0.5 }}
>
{flow.label}
</motion.text>
))}
{/* Health Data Types */}
<motion.g
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1 }}
>
<rect x="50" y="50" width="700" height="60" rx="8" fill="#1F2937" stroke="#374151" />
<text x="400" y="70" textAnchor="middle" fill="white" fontSize="14" fontWeight="600">
健康数据类型
</text>
{[
{ icon: <Heart size={16} />, label: '心率', color: '#EF4444' },
{ icon: <Activity size={16} />, label: '血压', color: '#F59E0B' },
{ icon: <Eye size={16} />, label: '睡眠', color: '#8B5CF6' },
{ icon: <Zap size={16} />, label: '活动', color: '#10B981' }
].map((item, index) => (
<g key={index}>
<foreignObject x={100 + index * 150} y={80} width="20" height="20">
<div style={{ color: item.color }}>
{item.icon}
</div>
</foreignObject>
<text x={125 + index * 150} y={95} fill="#9CA3AF" fontSize="10">
{item.label}
</text>
</g>
))}
</motion.g>
{/* Security Shield */}
<motion.g
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 1.5 }}
>
<foreignObject x="350" y="300" width="100" height="60">
<div className="flex flex-col items-center">
<div className="p-3 bg-green-500/20 rounded-lg border border-green-500/30">
<Shield size={24} className="text-green-400" />
</div>
<div className="text-xs text-green-400 mt-1 text-center">安全防护</div>
</div>
</foreignObject>
</motion.g>
</svg>
</div>
{/* Layer Details */}
{selectedLayer && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="bg-gray-800/50 rounded-lg border border-gray-700/50 p-4 mb-4"
>
{(() => {
const layer = systemLayers.find(l => l.id === selectedLayer);
if (!layer) return null;
return (
<div>
<div className="flex items-center gap-3 mb-3">
<div
className="p-2 rounded-lg"
style={{ backgroundColor: `${layer.color}20`, color: layer.color }}
>
{layer.icon}
</div>
<div>
<h4 className="text-lg font-semibold text-white">{layer.name}</h4>
<p className="text-gray-400 text-sm">{layer.description}</p>
</div>
</div>
<div>
<h5 className="text-sm font-medium text-gray-300 mb-2">核心组件:</h5>
<div className="flex flex-wrap gap-2">
{layer.components.map((component, index) => (
<span
key={index}
className="px-3 py-1 bg-gray-700/50 rounded-full text-xs text-gray-300 border border-gray-600"
>
{component}
</span>
))}
</div>
</div>
</div>
);
})()}
</motion.div>
)}
{/* Key Features */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<motion.div
className="bg-cyan-500/10 border border-cyan-500/20 rounded-lg p-4"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 }}
>
<h4 className="font-semibold text-cyan-400 mb-2">实时监测</h4>
<p className="text-sm text-gray-300">24/7不间断健康数据采集与分析</p>
</motion.div>
<motion.div
className="bg-purple-500/10 border border-purple-500/20 rounded-lg p-4"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.6 }}
>
<h4 className="font-semibold text-purple-400 mb-2">智能预警</h4>
<p className="text-sm text-gray-300">AI算法驱动的异常检测与风险预测</p>
</motion.div>
<motion.div
className="bg-green-500/10 border border-green-500/20 rounded-lg p-4"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.7 }}
>
<h4 className="font-semibold text-green-400 mb-2">隐私保护</h4>
<p className="text-sm text-gray-300">端到端加密确保数据安全与隐私</p>
</motion.div>
</div>
</motion.div>
);
};
export default AISystemArchitectureChart;
@@ -0,0 +1,218 @@
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import { BarChart3, TrendingUp, Clock, Shield, Heart } from 'lucide-react';
interface ComparisonData {
metric: string;
before: number;
after: number;
unit: string;
improvement: number;
icon: React.ReactNode;
color: string;
}
const comparisonData: ComparisonData[] = [
{
metric: '急救响应时间',
before: 15,
after: 3,
unit: '分钟',
improvement: 80,
icon: <Clock size={20} />,
color: '#EF4444'
},
{
metric: '跌倒事故率',
before: 8.5,
after: 2.1,
unit: '%',
improvement: 75,
icon: <Shield size={20} />,
color: '#F59E0B'
},
{
metric: '用药准确率',
before: 85,
after: 98,
unit: '%',
improvement: 15,
icon: <Heart size={20} />,
color: '#10B981'
},
{
metric: '家属满意度',
before: 75,
after: 95,
unit: '%',
improvement: 27,
icon: <TrendingUp size={20} />,
color: '#8B5CF6'
}
];
const CaseStudyComparisonChart: React.FC = () => {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-700/50 p-6 mb-8"
>
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-green-500/20 rounded-lg">
<BarChart3 size={24} className="text-green-400" />
</div>
<div>
<h3 className="text-xl font-semibold text-white">案例研究:实施效果对比</h3>
<p className="text-gray-400 text-sm">AI健康监护系统实施前后对比分析</p>
</div>
</div>
{/* Chart Container */}
<div className="space-y-6">
{comparisonData.map((data, index) => (
<motion.div
key={index}
className="relative"
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
>
{/* Metric Header */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<div
className="p-2 rounded-lg"
style={{ backgroundColor: `${data.color}20` }}
>
<div style={{ color: data.color }}>
{data.icon}
</div>
</div>
<div>
<h4 className="font-semibold text-white">{data.metric}</h4>
<p className="text-sm text-gray-400">改善率: +{data.improvement}%</p>
</div>
</div>
<div className="text-right">
<div className="flex items-center gap-2 text-sm text-gray-400">
<span>实施前</span>
<div className="w-4 h-4 bg-gray-600 rounded"></div>
<span>实施后</span>
<div
className="w-4 h-4 rounded"
style={{ backgroundColor: data.color }}
></div>
</div>
</div>
</div>
{/* Bar Chart */}
<div className="relative h-16 bg-gray-800/30 rounded-lg overflow-hidden">
{/* Before Bar */}
<motion.div
className="absolute top-2 left-2 h-5 bg-gray-600 rounded flex items-center justify-end pr-2"
initial={{ width: 0 }}
animate={{ width: `${(data.before / Math.max(data.before, data.after)) * 45}%` }}
transition={{ duration: 1, delay: index * 0.2 }}
>
<span className="text-xs text-white font-medium">
{data.before}{data.unit}
</span>
</motion.div>
{/* After Bar */}
<motion.div
className="absolute bottom-2 left-2 h-5 rounded flex items-center justify-end pr-2"
style={{ backgroundColor: data.color }}
initial={{ width: 0 }}
animate={{ width: `${(data.after / Math.max(data.before, data.after)) * 45}%` }}
transition={{ duration: 1, delay: index * 0.2 + 0.3 }}
>
<span className="text-xs text-white font-medium">
{data.after}{data.unit}
</span>
</motion.div>
{/* Improvement Badge */}
<motion.div
className="absolute right-2 top-1/2 transform -translate-y-1/2 bg-cyan-500/20 border border-cyan-500/30 rounded-full px-3 py-1"
initial={{ scale: 0, opacity: 0 }}
animate={{
scale: hoveredIndex === index ? 1.1 : 1,
opacity: 1
}}
transition={{ duration: 0.3, delay: index * 0.2 + 0.6 }}
>
<span className="text-xs font-bold text-cyan-400">
↑{data.improvement}%
</span>
</motion.div>
</div>
{/* Detailed Comparison */}
{hoveredIndex === index && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="mt-3 p-3 bg-gray-800/50 rounded-lg border border-gray-700/50"
>
<div className="grid grid-cols-3 gap-4 text-sm">
<div className="text-center">
<div className="text-gray-400">实施前</div>
<div className="text-white font-semibold">{data.before}{data.unit}</div>
</div>
<div className="text-center">
<div className="text-gray-400">实施后</div>
<div className="text-white font-semibold">{data.after}{data.unit}</div>
</div>
<div className="text-center">
<div className="text-gray-400">改善幅度</div>
<div className="text-green-400 font-semibold">+{data.improvement}%</div>
</div>
</div>
</motion.div>
)}
</motion.div>
))}
</div>
{/* Summary Statistics */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1 }}
className="mt-8 p-4 bg-gradient-to-r from-cyan-500/10 to-blue-500/10 rounded-lg border border-cyan-500/20"
>
<div className="text-center">
<h4 className="text-lg font-semibold text-white mb-2">总体改善效果</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<div className="text-2xl font-bold text-cyan-400">6个月</div>
<div className="text-gray-400">实施周期</div>
</div>
<div>
<div className="text-2xl font-bold text-green-400">200人</div>
<div className="text-gray-400">覆盖人群</div>
</div>
<div>
<div className="text-2xl font-bold text-yellow-400">49%</div>
<div className="text-gray-400">平均改善率</div>
</div>
<div>
<div className="text-2xl font-bold text-purple-400">95%</div>
<div className="text-gray-400">用户满意度</div>
</div>
</div>
</div>
</motion.div>
</motion.div>
);
};
export default CaseStudyComparisonChart;
@@ -0,0 +1,212 @@
import React from 'react';
import { motion } from 'framer-motion';
import { Activity, Heart, Moon, TrendingUp } from 'lucide-react';
interface HealthData {
time: string;
heartRate: number;
bloodPressure: number;
sleepQuality: number;
activity: number;
}
const mockHealthData: HealthData[] = [
{ time: '00:00', heartRate: 65, bloodPressure: 120, sleepQuality: 85, activity: 0 },
{ time: '02:00', heartRate: 58, bloodPressure: 115, sleepQuality: 90, activity: 0 },
{ time: '04:00', heartRate: 55, bloodPressure: 110, sleepQuality: 95, activity: 0 },
{ time: '06:00', heartRate: 62, bloodPressure: 118, sleepQuality: 88, activity: 10 },
{ time: '08:00', heartRate: 72, bloodPressure: 125, sleepQuality: 75, activity: 30 },
{ time: '10:00', heartRate: 78, bloodPressure: 130, sleepQuality: 60, activity: 65 },
{ time: '12:00', heartRate: 85, bloodPressure: 135, sleepQuality: 50, activity: 80 },
{ time: '14:00', heartRate: 82, bloodPressure: 132, sleepQuality: 45, activity: 75 },
{ time: '16:00', heartRate: 88, bloodPressure: 138, sleepQuality: 40, activity: 90 },
{ time: '18:00', heartRate: 75, bloodPressure: 128, sleepQuality: 55, activity: 60 },
{ time: '20:00', heartRate: 70, bloodPressure: 125, sleepQuality: 70, activity: 35 },
{ time: '22:00', heartRate: 68, bloodPressure: 122, sleepQuality: 80, activity: 15 },
];
const HealthMonitoringChart: React.FC = () => {
const maxHeartRate = Math.max(...mockHealthData.map(d => d.heartRate));
const maxBloodPressure = Math.max(...mockHealthData.map(d => d.bloodPressure));
const maxActivity = Math.max(...mockHealthData.map(d => d.activity));
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-700/50 p-6 mb-8"
>
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-cyan-500/20 rounded-lg">
<Activity size={24} className="text-cyan-400" />
</div>
<div>
<h3 className="text-xl font-semibold text-white">24小时健康监测数据</h3>
<p className="text-gray-400 text-sm">实时生命体征监测与分析</p>
</div>
</div>
{/* Legend */}
<div className="flex flex-wrap gap-6 mb-6 text-sm">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-red-500 rounded"></div>
<span className="text-gray-300">心率 (BPM)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-blue-500 rounded"></div>
<span className="text-gray-300">血压 (mmHg)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-purple-500 rounded"></div>
<span className="text-gray-300">睡眠质量 (%)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-green-500 rounded"></div>
<span className="text-gray-300">活动量 (%)</span>
</div>
</div>
{/* Chart */}
<div className="relative h-80 bg-gray-800/30 rounded-lg p-4">
<svg className="w-full h-full" viewBox="0 0 800 300">
{/* Grid lines */}
{[0, 1, 2, 3, 4].map((i) => (
<line
key={i}
x1="50"
y1={50 + i * 50}
x2="750"
y2={50 + i * 50}
stroke="#374151"
strokeWidth="1"
strokeDasharray="3,3"
/>
))}
{/* Y-axis labels */}
<text x="20" y="55" fill="#9CA3AF" fontSize="10">150</text>
<text x="20" y="105" fill="#9CA3AF" fontSize="10">112</text>
<text x="20" y="155" fill="#9CA3AF" fontSize="10">75</text>
<text x="20" y="205" fill="#9CA3AF" fontSize="10">37</text>
<text x="20" y="255" fill="#9CA3AF" fontSize="10">0</text>
{/* Heart Rate Line */}
<motion.path
d={`M ${mockHealthData.map((d, i) =>
`${50 + (i * 700 / (mockHealthData.length - 1))} ${250 - (d.heartRate / maxHeartRate) * 200}`
).join(' L ')}`}
fill="none"
stroke="#EF4444"
strokeWidth="3"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 2, ease: "easeInOut" }}
/>
{/* Blood Pressure Line */}
<motion.path
d={`M ${mockHealthData.map((d, i) =>
`${50 + (i * 700 / (mockHealthData.length - 1))} ${250 - (d.bloodPressure / maxBloodPressure) * 200}`
).join(' L ')}`}
fill="none"
stroke="#3B82F6"
strokeWidth="3"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 2, delay: 0.3, ease: "easeInOut" }}
/>
{/* Sleep Quality Line */}
<motion.path
d={`M ${mockHealthData.map((d, i) =>
`${50 + (i * 700 / (mockHealthData.length - 1))} ${250 - (d.sleepQuality / 100) * 200}`
).join(' L ')}`}
fill="none"
stroke="#8B5CF6"
strokeWidth="3"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 2, delay: 0.6, ease: "easeInOut" }}
/>
{/* Activity Line */}
<motion.path
d={`M ${mockHealthData.map((d, i) =>
`${50 + (i * 700 / (mockHealthData.length - 1))} ${250 - (d.activity / maxActivity) * 200}`
).join(' L ')}`}
fill="none"
stroke="#10B981"
strokeWidth="3"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 2, delay: 0.9, ease: "easeInOut" }}
/>
{/* Data points */}
{mockHealthData.map((d, i) => (
<g key={i}>
<motion.circle
cx={50 + (i * 700 / (mockHealthData.length - 1))}
cy={250 - (d.heartRate / maxHeartRate) * 200}
r="4"
fill="#EF4444"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 2 + i * 0.1 }}
/>
<motion.circle
cx={50 + (i * 700 / (mockHealthData.length - 1))}
cy={250 - (d.bloodPressure / maxBloodPressure) * 200}
r="4"
fill="#3B82F6"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 2.3 + i * 0.1 }}
/>
</g>
))}
{/* X-axis labels */}
{mockHealthData.filter((_, i) => i % 2 === 0).map((d, i) => (
<text
key={i}
x={50 + (i * 2 * 700 / (mockHealthData.length - 1))}
y="280"
fill="#9CA3AF"
fontSize="10"
textAnchor="middle"
>
{d.time}
</text>
))}
</svg>
</div>
{/* Key Insights */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-6">
<div className="bg-gray-800/30 rounded-lg p-4 text-center">
<Heart size={20} className="text-red-400 mx-auto mb-2" />
<div className="text-lg font-semibold text-white">72 BPM</div>
<div className="text-sm text-gray-400">平均心率</div>
</div>
<div className="bg-gray-800/30 rounded-lg p-4 text-center">
<TrendingUp size={20} className="text-blue-400 mx-auto mb-2" />
<div className="text-lg font-semibold text-white">125/80</div>
<div className="text-sm text-gray-400">平均血压</div>
</div>
<div className="bg-gray-800/30 rounded-lg p-4 text-center">
<Moon size={20} className="text-purple-400 mx-auto mb-2" />
<div className="text-lg font-semibold text-white">7.5h</div>
<div className="text-sm text-gray-400">睡眠时长</div>
</div>
<div className="bg-gray-800/30 rounded-lg p-4 text-center">
<Activity size={20} className="text-green-400 mx-auto mb-2" />
<div className="text-lg font-semibold text-white">8,547</div>
<div className="text-sm text-gray-400">步数</div>
</div>
</div>
</motion.div>
);
};
export default HealthMonitoringChart;
+271
View File
@@ -0,0 +1,271 @@
import React from 'react';
import { motion } from 'framer-motion';
import { TrendingUp, DollarSign, Users, Globe } from 'lucide-react';
interface MarketData {
year: string;
marketSize: number;
userAdoption: number;
investment: number;
}
const marketData: MarketData[] = [
{ year: '2020', marketSize: 2.5, userAdoption: 15, investment: 1.2 },
{ year: '2021', marketSize: 3.8, userAdoption: 25, investment: 2.1 },
{ year: '2022', marketSize: 5.6, userAdoption: 38, investment: 3.4 },
{ year: '2023', marketSize: 8.2, userAdoption: 52, investment: 5.8 },
{ year: '2024', marketSize: 12.1, userAdoption: 68, investment: 8.9 },
{ year: '2025*', marketSize: 18.5, userAdoption: 82, investment: 13.2 },
{ year: '2026*', marketSize: 26.8, userAdoption: 92, investment: 19.1 },
{ year: '2027*', marketSize: 35.2, userAdoption: 98, investment: 25.6 }
];
const MarketTrendChart: React.FC = () => {
const maxMarketSize = Math.max(...marketData.map(d => d.marketSize));
const maxUserAdoption = 100;
const maxInvestment = Math.max(...marketData.map(d => d.investment));
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-700/50 p-6 mb-8"
>
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-purple-500/20 rounded-lg">
<TrendingUp size={24} className="text-purple-400" />
</div>
<div>
<h3 className="text-xl font-semibold text-white">AI健康监护市场前景</h3>
<p className="text-gray-400 text-sm">市场规模、用户接受度与投资趋势分析</p>
</div>
</div>
{/* Legend */}
<div className="flex flex-wrap gap-6 mb-6 text-sm">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-cyan-500 rounded"></div>
<span className="text-gray-300">市场规模 (十亿美元)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-green-500 rounded"></div>
<span className="text-gray-300">用户接受度 (%)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-yellow-500 rounded"></div>
<span className="text-gray-300">投资金额 (十亿美元)</span>
</div>
</div>
{/* Chart */}
<div className="relative h-80 bg-gray-800/30 rounded-lg p-4 mb-6">
<svg className="w-full h-full" viewBox="0 0 800 300">
{/* Grid lines */}
{[0, 1, 2, 3, 4].map((i) => (
<line
key={i}
x1="60"
y1={50 + i * 50}
x2="750"
y2={50 + i * 50}
stroke="#374151"
strokeWidth="1"
strokeDasharray="3,3"
/>
))}
{/* Y-axis labels */}
<text x="20" y="55" fill="#9CA3AF" fontSize="10">100</text>
<text x="20" y="105" fill="#9CA3AF" fontSize="10">75</text>
<text x="20" y="155" fill="#9CA3AF" fontSize="10">50</text>
<text x="20" y="205" fill="#9CA3AF" fontSize="10">25</text>
<text x="20" y="255" fill="#9CA3AF" fontSize="10">0</text>
{/* Market Size Area Chart */}
<motion.path
d={`M 60 250 L ${marketData.map((d, i) =>
`${60 + (i * 690 / (marketData.length - 1))} ${250 - (d.marketSize / maxMarketSize) * 200}`
).join(' L ')} L ${60 + (690)} 250 Z`}
fill="url(#marketGradient)"
initial={{ opacity: 0 }}
animate={{ opacity: 0.3 }}
transition={{ duration: 1.5 }}
/>
{/* Market Size Line */}
<motion.path
d={`M ${marketData.map((d, i) =>
`${60 + (i * 690 / (marketData.length - 1))} ${250 - (d.marketSize / maxMarketSize) * 200}`
).join(' L ')}`}
fill="none"
stroke="#06B6D4"
strokeWidth="3"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 2, ease: "easeInOut" }}
/>
{/* User Adoption Line */}
<motion.path
d={`M ${marketData.map((d, i) =>
`${60 + (i * 690 / (marketData.length - 1))} ${250 - (d.userAdoption / maxUserAdoption) * 200}`
).join(' L ')}`}
fill="none"
stroke="#10B981"
strokeWidth="3"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 2, delay: 0.3, ease: "easeInOut" }}
/>
{/* Investment Bars */}
{marketData.map((d, i) => (
<motion.rect
key={i}
x={55 + (i * 690 / (marketData.length - 1))}
y={250 - (d.investment / maxInvestment) * 200}
width="10"
height={(d.investment / maxInvestment) * 200}
fill="#F59E0B"
opacity="0.7"
initial={{ height: 0, y: 250 }}
animate={{
height: (d.investment / maxInvestment) * 200,
y: 250 - (d.investment / maxInvestment) * 200
}}
transition={{ duration: 1, delay: 0.6 + i * 0.1 }}
/>
))}
{/* Data points */}
{marketData.map((d, i) => (
<g key={i}>
<motion.circle
cx={60 + (i * 690 / (marketData.length - 1))}
cy={250 - (d.marketSize / maxMarketSize) * 200}
r="5"
fill="#06B6D4"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 2 + i * 0.1 }}
/>
<motion.circle
cx={60 + (i * 690 / (marketData.length - 1))}
cy={250 - (d.userAdoption / maxUserAdoption) * 200}
r="5"
fill="#10B981"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 2.3 + i * 0.1 }}
/>
</g>
))}
{/* X-axis labels */}
{marketData.map((d, i) => (
<text
key={i}
x={60 + (i * 690 / (marketData.length - 1))}
y="280"
fill="#9CA3AF"
fontSize="10"
textAnchor="middle"
>
{d.year}
</text>
))}
{/* Gradient Definitions */}
<defs>
<linearGradient id="marketGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#06B6D4" stopOpacity="0.6"/>
<stop offset="100%" stopColor="#06B6D4" stopOpacity="0.1"/>
</linearGradient>
</defs>
</svg>
{/* Prediction Label */}
<div className="absolute top-4 right-4 bg-yellow-500/20 border border-yellow-500/30 rounded-lg px-3 py-1">
<span className="text-xs text-yellow-400 font-medium">* 预测数据</span>
</div>
</div>
{/* Key Statistics */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<motion.div
className="bg-cyan-500/10 border border-cyan-500/20 rounded-lg p-4 text-center"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.5 }}
>
<DollarSign size={24} className="text-cyan-400 mx-auto mb-2" />
<div className="text-2xl font-bold text-white">$35.2B</div>
<div className="text-sm text-gray-400">2027年预期市场规模</div>
</motion.div>
<motion.div
className="bg-green-500/10 border border-green-500/20 rounded-lg p-4 text-center"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.6 }}
>
<Users size={24} className="text-green-400 mx-auto mb-2" />
<div className="text-2xl font-bold text-white">98%</div>
<div className="text-sm text-gray-400">预期用户接受度</div>
</motion.div>
<motion.div
className="bg-yellow-500/10 border border-yellow-500/20 rounded-lg p-4 text-center"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.7 }}
>
<TrendingUp size={24} className="text-yellow-400 mx-auto mb-2" />
<div className="text-2xl font-bold text-white">300%</div>
<div className="text-sm text-gray-400">5年复合增长率</div>
</motion.div>
<motion.div
className="bg-purple-500/10 border border-purple-500/20 rounded-lg p-4 text-center"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.8 }}
>
<Globe size={24} className="text-purple-400 mx-auto mb-2" />
<div className="text-2xl font-bold text-white">45+</div>
<div className="text-sm text-gray-400">覆盖国家数量</div>
</motion.div>
</div>
{/* Market Insights */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1 }}
className="mt-6 p-4 bg-gray-800/30 rounded-lg"
>
<h4 className="text-lg font-semibold text-white mb-3">市场洞察</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-gray-300">
<div>
<strong className="text-cyan-400">驱动因素:</strong>
<ul className="mt-1 space-y-1 pl-4">
<li>• 人口老龄化加速</li>
<li>• AI技术成熟度提升</li>
<li>• 医疗成本控制需求</li>
</ul>
</div>
<div>
<strong className="text-green-400">发展机遇:</strong>
<ul className="mt-1 space-y-1 pl-4">
<li>• 政策支持力度加大</li>
<li>• 5G网络普及应用</li>
<li>• 消费者接受度提高</li>
</ul>
</div>
</div>
</motion.div>
</motion.div>
);
};
export default MarketTrendChart;
+92
View File
@@ -0,0 +1,92 @@
import React from 'react';
import { motion } from 'framer-motion';
import { BarChart3 } from 'lucide-react';
interface BarData {
label: string;
value: number;
color: string;
}
// 示例数据 - 可以轻松替换为动态数据
const sampleData: BarData[] = [
{ label: '人工智能', value: 85, color: '#06B6D4' },
{ label: '物联网', value: 72, color: '#10B981' },
{ label: '大数据', value: 68, color: '#8B5CF6' },
{ label: '云计算', value: 91, color: '#F59E0B' },
{ label: '区块链', value: 45, color: '#EF4444' }
];
const SimpleBarChart: React.FC = () => {
const maxValue = Math.max(...sampleData.map(d => d.value));
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-700/50 p-6 mb-8"
>
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-blue-500/20 rounded-lg">
<BarChart3 size={24} className="text-blue-400" />
</div>
<div>
<h3 className="text-xl font-semibold text-white">技术成熟度分析</h3>
<p className="text-gray-400 text-sm">各项技术的市场成熟度评估</p>
</div>
</div>
<div className="space-y-4">
{sampleData.map((item, index) => (
<motion.div
key={index}
className="space-y-2"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
>
<div className="flex justify-between items-center">
<span className="text-gray-300 font-medium">{item.label}</span>
<span className="text-white font-bold">{item.value}%</span>
</div>
<div className="relative h-3 bg-gray-800 rounded-full overflow-hidden">
<motion.div
className="absolute top-0 left-0 h-full rounded-full"
style={{ backgroundColor: item.color }}
initial={{ width: 0 }}
animate={{ width: `${(item.value / maxValue) * 100}%` }}
transition={{ duration: 1, delay: index * 0.1 + 0.5 }}
/>
{/* Glow effect */}
<motion.div
className="absolute top-0 left-0 h-full rounded-full opacity-50 blur-sm"
style={{ backgroundColor: item.color }}
initial={{ width: 0 }}
animate={{ width: `${(item.value / maxValue) * 100}%` }}
transition={{ duration: 1, delay: index * 0.1 + 0.5 }}
/>
</div>
</motion.div>
))}
</div>
{/* Summary */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1 }}
className="mt-6 p-4 bg-gray-800/30 rounded-lg"
>
<h4 className="text-sm font-semibold text-gray-300 mb-2">分析总结</h4>
<p className="text-sm text-gray-400">
云计算技术成熟度最高(91%),已进入广泛应用阶段;人工智能紧随其后(85%),正在快速发展;
区块链技术相对较新(45%),仍在探索应用场景。
</p>
</motion.div>
</motion.div>
);
};
export default SimpleBarChart;
+36
View File
@@ -48,6 +48,10 @@ AI算法能够分析历史健康数据,预测潜在的健康风险:
- 营养状况评估
- 认知功能退化预测
{{CHART:health-monitoring}}
通过上图可以看到,AI系统能够实时监测老年人的各项生命体征,包括心率、血压、睡眠质量和活动量。系统会自动分析这些数据的变化趋势,及时发现异常情况。
### 3. 个性化护理方案
基于大数据分析,AI可以为每位老年人定制专属的护理计划:
@@ -79,6 +83,10 @@ def detect_anomaly(vital_signs):
- **黄色预警**:中度风险,通知护理人员
- **红色预警**:紧急情况,立即呼叫救援
{{CHART:system-architecture}}
上图展示了完整的AI健康监护系统架构,从设备感知层到应用服务层,构建了一个端到端的智能健康监护解决方案。
## 案例研究:智慧养老院实践
### 背景介绍
@@ -101,6 +109,10 @@ def detect_anomaly(vital_signs):
3. **持续优化**:根据反馈不断改进算法
4. **隐私保护**:严格的数据安全措施
{{CHART:case-study-comparison}}
从上述对比数据可以清晰地看到,AI健康监护系统在各个关键指标上都取得了显著的改善效果。
## 面临的挑战与解决方案
### 挑战1:老年人对技术的接受度
@@ -140,6 +152,10 @@ def detect_anomaly(vital_signs):
根据市场研究,AI健康监护市场预计将在未来5年内增长300%,这为技术创新和应用提供了广阔的空间。
{{CHART:market-trend}}
市场趋势分析显示,AI健康监护领域正处于快速发展期,预计到2027年市场规模将超过350亿美元。
## 结论
AI技术正在深刻改变老年人健康监护的方式。通过智能化、个性化的解决方案,我们不仅能够提高护理质量,还能让老年人享受更有尊严、更独立的生活。随着技术的不断进步,我们相信AI将在老年人健康管理领域发挥更大的作用。
@@ -183,6 +199,10 @@ PPG传感器通过LED光源和光电探测器,能够精确测量:
- 血氧饱和度
- 血压趋势
{{CHART:health-monitoring}}
上图展示了智能手表24小时监测的典型健康数据,可以清楚地看到不同时段的生理指标变化。
### 2. 生物电阻抗分析(BIA)
通过微弱电流测量身体组成:
@@ -240,6 +260,10 @@ function calculateHealthScore(data) {
通过持续监测和AI分析,系统为用户提供个性化的睡眠改善建议,平均睡眠质量提升30%。
{{CHART:case-study-comparison}}
以上数据展示了智能手表在各种应用场景下的显著改善效果。
## 数据安全与隐私保护
### 加密技术
@@ -431,6 +455,10 @@ for metric, values in efficiency_data.items():
- 年度节约:200万元
- 预计回收期:2.5年
{{CHART:case-study-comparison}}
投资回报分析显示,康养机器人系统在经济效益和社会效益方面都取得了显著成果。
## 未来规划
### 技术升级
@@ -517,6 +545,10 @@ security_layers:
- 微分段网络
- 持续监控和验证
{{CHART:system-architecture}}
上图展示了我们的安全架构设计,每一层都有相应的安全防护措施。
## HIPAA合规实施
### 技术保障措施
@@ -725,6 +757,10 @@ const dashboardConfig = {
- **联邦学习**:隐私保护的机器学习
- **量子加密**:下一代加密技术
{{CHART:simple-bar-chart}}
各项新兴技术的成熟度分析显示,云计算和人工智能已经达到较高的实用化水平。
### 持续改进
- 自动化安全测试
+235
View File
@@ -0,0 +1,235 @@
import { Comment, CommentStats } from '../types/comment';
// Mock comment data for demonstration
export const mockComments: Comment[] = [
{
id: 'comment-1',
postId: 'ai-elderly-health-monitoring',
author: {
name: '张医生',
avatar: 'https://i.pravatar.cc/150?img=1',
email: 'doctor.zhang@example.com'
},
content: '这篇文章对AI在老年护理中的应用分析得非常深入!特别是关于预测性健康分析的部分,让我看到了技术在医疗领域的巨大潜力。我们医院正在考虑引入类似的系统。',
rating: 5,
createdAt: '2024-03-16T09:30:00Z',
likes: 12,
likedBy: ['user1', 'user2', 'user3'],
replies: [
{
id: 'reply-1',
postId: 'ai-elderly-health-monitoring',
author: {
name: 'Dr. Zhang Wei',
avatar: 'https://i.pravatar.cc/150?img=33'
},
content: '感谢您的关注!如果您的医院需要技术咨询或试点合作,欢迎随时联系我们团队。我们很乐意分享更多实践经验。',
createdAt: '2024-03-16T14:20:00Z',
likes: 8,
likedBy: ['user1', 'user4'],
parentId: 'comment-1',
replies: [],
isEdited: false,
isDeleted: false
}
],
isEdited: false,
isDeleted: false
},
{
id: 'comment-2',
postId: 'ai-elderly-health-monitoring',
author: {
name: '李护士长',
avatar: 'https://i.pravatar.cc/150?img=5'
},
content: '从实际护理工作的角度来看,AI系统确实能大大提高工作效率。但是我担心过度依赖技术会影响护理人员与老人之间的情感交流。希望在技术发展的同时,不要忽视人文关怀的重要性。',
rating: 4,
createdAt: '2024-03-16T11:45:00Z',
likes: 18,
likedBy: ['user2', 'user3', 'user5'],
replies: [
{
id: 'reply-2',
postId: 'ai-elderly-health-monitoring',
author: {
name: '王家属',
avatar: 'https://i.pravatar.cc/150?img=8'
},
content: '非常同意护士长的观点。我父亲在养老院,我最希望看到的还是护理人员能给予他足够的关爱和陪伴。技术应该是辅助,而不是替代。',
createdAt: '2024-03-16T16:10:00Z',
likes: 5,
likedBy: ['user3', 'user6'],
parentId: 'comment-2',
replies: [],
isEdited: false,
isDeleted: false
}
],
isEdited: false,
isDeleted: false
},
{
id: 'comment-3',
postId: 'ai-elderly-health-monitoring',
author: {
name: '科技爱好者小明',
avatar: 'https://i.pravatar.cc/150?img=15'
},
content: '技术细节介绍得很详细,特别是异常检测算法的代码示例很有参考价值。不过我想了解更多关于数据隐私保护的措施,毕竟老年人的健康数据非常敏感。',
rating: 4,
createdAt: '2024-03-17T08:20:00Z',
likes: 7,
likedBy: ['user4', 'user7'],
replies: [],
isEdited: false,
isDeleted: false
},
{
id: 'comment-4',
postId: 'smart-watch-health-insights',
author: {
name: '健身达人Amy',
avatar: 'https://i.pravatar.cc/150?img=20'
},
content: '作为一个智能手表的重度用户,这篇文章让我对手表的健康监测功能有了更深的理解。特别是PPG传感器的工作原理,之前一直很好奇。期待未来能有更多突破性的功能!',
rating: 5,
createdAt: '2024-03-13T10:15:00Z',
likes: 15,
likedBy: ['user5', 'user8', 'user9'],
replies: [
{
id: 'reply-3',
postId: 'smart-watch-health-insights',
author: {
name: 'Sarah Chen',
avatar: 'https://i.pravatar.cc/150?img=44'
},
content: '很高兴文章对您有帮助!无创血糖监测功能我们正在紧锣密鼓地开发中,预计明年会有重大突破。请持续关注我们的产品更新。',
createdAt: '2024-03-13T15:30:00Z',
likes: 6,
likedBy: ['user5', 'user10'],
parentId: 'comment-4',
replies: [],
isEdited: false,
isDeleted: false
}
],
isEdited: false,
isDeleted: false
}
];
// Comment management functions
export const getCommentsByPostId = (postId: string): Comment[] => {
return mockComments.filter(comment => comment.postId === postId && !comment.parentId);
};
export const getCommentStats = (postId: string): CommentStats => {
const comments = mockComments.filter(comment => comment.postId === postId);
const ratingsOnly = comments.filter(comment => comment.rating !== undefined);
const ratingDistribution = {
1: ratingsOnly.filter(c => c.rating === 1).length,
2: ratingsOnly.filter(c => c.rating === 2).length,
3: ratingsOnly.filter(c => c.rating === 3).length,
4: ratingsOnly.filter(c => c.rating === 4).length,
5: ratingsOnly.filter(c => c.rating === 5).length
};
const averageRating = ratingsOnly.length > 0
? ratingsOnly.reduce((sum, comment) => sum + (comment.rating || 0), 0) / ratingsOnly.length
: 0;
return {
total: comments.length,
averageRating: Math.round(averageRating * 10) / 10,
ratingDistribution
};
};
export const addComment = (comment: Omit<Comment, 'id' | 'createdAt' | 'likes' | 'likedBy' | 'replies' | 'isEdited' | 'isDeleted'>): Comment => {
const newComment: Comment = {
...comment,
id: `comment-${Date.now()}`,
createdAt: new Date().toISOString(),
likes: 0,
likedBy: [],
replies: [],
isEdited: false,
isDeleted: false
};
if (comment.parentId) {
// This is a reply - add to parent's replies array
const parentComment = mockComments.find(c => c.id === comment.parentId);
if (parentComment) {
parentComment.replies.push(newComment);
}
} else {
// This is a top-level comment
mockComments.push(newComment);
}
return newComment;
};
export const likeComment = (commentId: string, userId: string): boolean => {
const findAndLike = (comments: Comment[]): boolean => {
for (const comment of comments) {
if (comment.id === commentId) {
if (!comment.likedBy.includes(userId)) {
comment.likedBy.push(userId);
comment.likes++;
return true;
} else {
// Unlike
comment.likedBy = comment.likedBy.filter(id => id !== userId);
comment.likes--;
return false;
}
}
// Check replies
const replyResult = findAndLike(comment.replies);
if (replyResult !== undefined) return replyResult;
}
return false;
};
return findAndLike(mockComments);
};
export const editComment = (commentId: string, newContent: string): boolean => {
const findAndEdit = (comments: Comment[]): boolean => {
for (const comment of comments) {
if (comment.id === commentId) {
comment.content = newContent;
comment.updatedAt = new Date().toISOString();
comment.isEdited = true;
return true;
}
// Check replies
if (findAndEdit(comment.replies)) return true;
}
return false;
};
return findAndEdit(mockComments);
};
export const deleteComment = (commentId: string): boolean => {
const findAndDelete = (comments: Comment[]): boolean => {
for (const comment of comments) {
if (comment.id === commentId) {
comment.isDeleted = true;
comment.content = '[此评论已被删除]';
return true;
}
// Check replies
if (findAndDelete(comment.replies)) return true;
}
return false;
};
return findAndDelete(mockComments);
};
+31 -54
View File
@@ -11,6 +11,8 @@ import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { getBlogPostById, getRelatedPosts, BlogPost } from '../data/blogData';
import CommentList from '../components/CommentList';
import ChartRenderer from '../components/ChartRenderer';
const BlogDetail: React.FC = () => {
const { id } = useParams<{ id: string }>();
@@ -420,10 +422,26 @@ const BlogDetail: React.FC = () => {
);
} else {
const markdownContent = part.content;
// Split content by chart placeholders
const contentParts = markdownContent.split(/(\{\{CHART:[^}]+\}\})/g);
return (
<ReactMarkdown
key={index}
components={{
<div key={index}>
{contentParts.map((contentPart, partIndex) => {
// Check if this is a chart placeholder
if (contentPart.startsWith('{{CHART:')) {
const chartType = contentPart.match(/\{\{CHART:([^}]+)\}\}/)?.[1];
if (chartType) {
return <ChartRenderer key={partIndex} type={chartType} />;
}
return null;
} else if (contentPart.trim()) {
// Regular markdown content
return (
<ReactMarkdown
key={partIndex}
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
@@ -514,9 +532,14 @@ const BlogDetail: React.FC = () => {
</a>
),
}}
>
{markdownContent}
</ReactMarkdown>
>
{contentPart}
</ReactMarkdown>
);
}
return null;
})}
</div>
);
}
})}
@@ -611,55 +634,9 @@ const BlogDetail: React.FC = () => {
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="mt-16 p-8 bg-gray-900/50 backdrop-blur-sm rounded-2xl border border-gray-800"
className="mt-16"
>
<h2 className="text-2xl font-bold text-white mb-6 flex items-center gap-2">
<MessageCircle size={24} />
评论 ({post.comments})
</h2>
{/* Comment Input */}
<div className="mb-8">
<textarea
placeholder="分享你的想法..."
className="w-full p-4 bg-gray-800 border border-gray-700 rounded-xl text-white placeholder-gray-500 focus:border-cyan-500 focus:outline-none resize-none"
rows={4}
/>
<div className="flex justify-end mt-4">
<button className="px-6 py-2 bg-cyan-500 text-black font-semibold rounded-lg hover:bg-cyan-400 transition-colors">
发表评论
</button>
</div>
</div>
{/* Sample Comments */}
<div className="space-y-6">
{[1, 2, 3].map((i) => (
<div key={i} className="flex gap-4">
<img
src={`https://i.pravatar.cc/40?img=${i + 10}`}
alt="User"
className="w-10 h-10 rounded-full"
/>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="font-semibold text-white">用户{i}</span>
<span className="text-xs text-gray-500">2小时前</span>
</div>
<p className="text-gray-400 text-sm mb-2">
这篇文章非常有见地,特别是关于AI在健康监护中的应用部分,让我对未来充满期待。
</p>
<div className="flex items-center gap-4 text-xs text-gray-500">
<button className="flex items-center gap-1 hover:text-cyan-400 transition-colors">
<ThumbsUp size={14} />
<span>赞</span>
</button>
<button className="hover:text-cyan-400 transition-colors">回复</button>
</div>
</div>
</div>
))}
</div>
<CommentList postId={post.id} />
</motion.div>
</div>
</div>
+41
View File
@@ -0,0 +1,41 @@
export interface Comment {
id: string;
postId: string;
author: {
name: string;
avatar?: string;
email?: string;
};
content: string;
rating?: number; // 1-5 stars, optional for regular comments
createdAt: string;
updatedAt?: string;
likes: number;
likedBy: string[]; // array of user IDs who liked this comment
parentId?: string; // for reply comments
replies: Comment[]; // nested replies
isEdited: boolean;
isDeleted: boolean;
}
export interface CommentFormData {
author: {
name: string;
email?: string;
};
content: string;
rating?: number;
parentId?: string;
}
export interface CommentStats {
total: number;
averageRating: number;
ratingDistribution: {
1: number;
2: number;
3: number;
4: number;
5: number;
};
}