kol overview frontend

This commit is contained in:
2025-03-14 12:40:57 +08:00
parent 18276652f3
commit 6964eb75cc
2 changed files with 427 additions and 260 deletions

View File

@@ -56,7 +56,7 @@
- 条形长度直观反映各平台占比 - 条形长度直观反映各平台占比
- 帮助团队了解哪些平台效果更好 - 帮助团队了解哪些平台效果更好
# 审核状态分布 # 审核状态分布 [先不做]
- 环形图展示内容审核状态的分布情况 - 环形图展示内容审核状态的分布情况
- 包括三种状态:已核准、待审核、已拒绝 - 包括三种状态:已核准、待审核、已拒绝

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { import {
TrendingUp, TrendingUp,
TrendingDown,
PieChart, PieChart,
MessageSquare, MessageSquare,
Facebook, Facebook,
@@ -21,7 +22,8 @@ import {
Download, Download,
AlertTriangle, AlertTriangle,
Save, Save,
Eye Eye,
Twitter
} from 'lucide-react'; } from 'lucide-react';
// Define interfaces for analytics data // Define interfaces for analytics data
@@ -64,23 +66,20 @@ interface EngagementData {
shares: number; shares: number;
} }
// 更新KOL数据接口与API接口结构匹配
interface KOLData { interface KOLData {
id: string; influencer_id: string;
name: string; name: string;
platform: string; platform: string;
followers: number; profile_url: string;
engagement: number; followers_count: number;
posts: number; followers_change: number;
sentiment: SentimentData; followers_change_percentage: number | null | string;
avatar?: string; likes_change: number;
platforms?: string[]; likes_change_percentage: number | null | string;
postCount?: number; follows_change: number;
likeCount?: number; follows_change_percentage: number | null | string;
commentCount?: number; avatar?: string; // 兼容旧代码
engagementRate?: number;
engagementTrend?: number;
sentimentScore?: number;
officialInteractions?: number;
} }
interface FunnelData { interface FunnelData {
@@ -105,8 +104,20 @@ interface Project {
description?: string; description?: string;
} }
// 添加API响应接口
interface KOLOverviewResponse {
success: boolean;
data: KOLData[];
pagination: {
limit: number;
offset: number;
total: number;
};
error?: string;
}
const Analytics: React.FC = () => { const Analytics: React.FC = () => {
const [timeRange, setTimeRange] = useState('7days'); const [timeRange, setTimeRange] = useState('30'); // 修改默认值为'30'与API匹配
const [selectedKOL, setSelectedKOL] = useState('all'); const [selectedKOL, setSelectedKOL] = useState('all');
const [selectedPlatform, setSelectedPlatform] = useState('all'); const [selectedPlatform, setSelectedPlatform] = useState('all');
const [platformData, setPlatformData] = useState<AnalyticsData[]>([]); const [platformData, setPlatformData] = useState<AnalyticsData[]>([]);
@@ -121,7 +132,9 @@ const Analytics: React.FC = () => {
const [kolData, setKolData] = useState<KOLData[]>([]); const [kolData, setKolData] = useState<KOLData[]>([]);
const [funnelData, setFunnelData] = useState<FunnelData[]>([]); const [funnelData, setFunnelData] = useState<FunnelData[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [kolLoading, setKolLoading] = useState(true); // 新增KOL数据加载状态
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [kolError, setKolError] = useState<string | null>(null); // 新增KOL数据错误状态
const [filteredEngagementData, setFilteredEngagementData] = useState<EngagementData[]>([]); const [filteredEngagementData, setFilteredEngagementData] = useState<EngagementData[]>([]);
// 添加项目相关状态 // 添加项目相关状态
@@ -131,7 +144,16 @@ const Analytics: React.FC = () => {
{ id: '3', name: '项目 3', description: '示例项目 3' }, { id: '3', name: '项目 3', description: '示例项目 3' },
{ id: '550e8400-e29b-41d4-a716-446655440000', name: 'UUID格式项目', description: 'UUID格式的项目ID示例' } { id: '550e8400-e29b-41d4-a716-446655440000', name: 'UUID格式项目', description: 'UUID格式的项目ID示例' }
]); ]);
const [selectedProject, setSelectedProject] = useState<string>('1'); const [selectedProject, setSelectedProject] = useState<string>('all');
// 添加分页状态
const [kolPage, setKolPage] = useState(0);
const [kolPageSize, setKolPageSize] = useState(20);
const [kolTotal, setKolTotal] = useState(0);
// 添加排序状态
const [kolSortBy, setKolSortBy] = useState('followers_change');
const [kolSortOrder, setKolSortOrder] = useState('desc');
// Add new state for influencer tracking // Add new state for influencer tracking
const [showTrackingForm, setShowTrackingForm] = useState(false); const [showTrackingForm, setShowTrackingForm] = useState(false);
@@ -145,7 +167,78 @@ const Analytics: React.FC = () => {
const [trackingSuccess, setTrackingSuccess] = useState<string | null>(null); const [trackingSuccess, setTrackingSuccess] = useState<string | null>(null);
const [trackingError, setTrackingError] = useState<string | null>(null); const [trackingError, setTrackingError] = useState<string | null>(null);
// 获取KOL概览数据
const fetchKolOverviewData = async () => {
setKolLoading(true);
setKolError(null);
try {
// 构建API URL包含所有查询参数
let url = `http://localhost:4000/api/analytics/kol-overview?timeRange=${timeRange}&sortBy=${kolSortBy}&sortOrder=${kolSortOrder}&limit=${kolPageSize}&offset=${kolPage * kolPageSize}`;
if (selectedProject !== 'all') {
url += `&projectId=${selectedProject}`;
}
// 获取认证令牌 (实际项目中应从身份验证上下文中获取)
const authToken = 'eyJhbGciOiJIUzI1NiIsImtpZCI6Inl3blNGYnRBOGtBUnl4UmUiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3h0cWhsdXpvcm5hemxta29udWNyLnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiI1YjQzMThiZi0yMWE4LTQ3YWMtOGJmYS0yYThmOGVmOWMwZmIiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzQxNjI3ODkyLCJpYXQiOjE3NDE2MjQyOTIsImVtYWlsIjoidml0YWxpdHltYWlsZ0BnbWFpbC5jb20iLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7ImVtYWlsX3ZlcmlmaWVkIjp0cnVlfSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTc0MTYyNDI5Mn1dLCJzZXNzaW9uX2lkIjoiODlmYjg0YzktZmEzYy00YmVlLTk0MDQtNjI1MjE0OGIyMzVlIiwiaXNfYW5vbnltb3VzIjpmYWxzZX0.VuUX2yhqN-FZseKL8fQG91i1cohfRqW2m1Z8CIWhZuk';
// 发送请求
const response = await fetch(url, {
headers: {
'accept': 'application/json',
'Authorization': `Bearer ${authToken}`
}
});
// 处理响应
if (response.ok) {
const result: KOLOverviewResponse = await response.json();
if (result.success) {
console.log('成功获取KOL概览数据:', result);
// 更新状态
setKolData(result.data);
setKolTotal(result.pagination.total);
// 处理模拟数据标志
if ('is_mock_data' in result && result.is_mock_data) {
console.info('注意: 使用的是模拟数据');
}
} else {
// API返回了成功状态码但标记为不成功
console.error('API返回错误:', result.error);
setKolError(result.error || '无法获取KOL数据');
// 保留现有数据或清空
// setKolData([]);
}
} else {
// HTTP错误
console.error('获取KOL数据失败HTTP状态:', response.status);
const errorText = await response.text();
setKolError(`获取失败 (${response.status}): ${errorText}`);
// 保留现有数据或清空
// setKolData([]);
}
} catch (error) {
// 网络或其他错误
console.error('获取KOL数据时发生错误:', error);
setKolError(`获取KOL数据时发生错误: ${error instanceof Error ? error.message : String(error)}`);
// 保留现有数据或清空
// setKolData([]);
} finally {
setKolLoading(false);
}
};
useEffect(() => { useEffect(() => {
// 获取KOL概览数据
fetchKolOverviewData();
const fetchAnalyticsData = async () => { const fetchAnalyticsData = async () => {
try { try {
setLoading(true); setLoading(true);
@@ -175,9 +268,9 @@ const Analytics: React.FC = () => {
// Set mock status data // Set mock status data
setStatusData([ setStatusData([
{ name: 'Approved', value: 45, color: '#10B981' }, { name: 'approved', value: 45, color: '#10B981' },
{ name: 'Pending', value: 30, color: '#F59E0B' }, { name: 'pending', value: 30, color: '#F59E0B' },
{ name: 'Rejected', value: 25, color: '#EF4444' } { name: 'rejected', value: 25, color: '#EF4444' }
]); ]);
// Set mock popular articles // Set mock popular articles
@@ -189,64 +282,6 @@ const Analytics: React.FC = () => {
{ id: '5', title: 'Content Creation Tips', views: 620, engagement: 57, platform: 'YouTube' } { id: '5', title: 'Content Creation Tips', views: 620, engagement: 57, platform: 'YouTube' }
]); ]);
// Set mock KOL data
setKolData([
{
id: '1',
name: 'John Smith',
platform: 'Instagram',
followers: 120000,
engagement: 3.5,
posts: 450,
sentiment: { positive: 75, neutral: 15, negative: 10 },
avatar: 'https://randomuser.me/api/portraits/men/1.jpg',
platforms: ['Instagram', 'TikTok'],
postCount: 120,
likeCount: 45000,
commentCount: 2800,
engagementRate: 3.8,
engagementTrend: 0.5,
sentimentScore: 4.2,
officialInteractions: 12
},
{
id: '2',
name: 'Sarah Johnson',
platform: 'YouTube',
followers: 250000,
engagement: 4.2,
posts: 320,
sentiment: { positive: 82, neutral: 10, negative: 8 },
avatar: 'https://randomuser.me/api/portraits/women/2.jpg',
platforms: ['YouTube', 'Instagram'],
postCount: 85,
likeCount: 120000,
commentCount: 8500,
engagementRate: 4.5,
engagementTrend: 1.2,
sentimentScore: 4.5,
officialInteractions: 8
},
{
id: '3',
name: 'David Lee',
platform: 'Twitter',
followers: 180000,
engagement: 2.8,
posts: 1200,
sentiment: { positive: 68, neutral: 22, negative: 10 },
avatar: 'https://randomuser.me/api/portraits/men/3.jpg',
platforms: ['Twitter', 'LinkedIn'],
postCount: 350,
likeCount: 28000,
commentCount: 4200,
engagementRate: 2.5,
engagementTrend: -0.3,
sentimentScore: 3.8,
officialInteractions: 5
}
]);
// 尝试从API获取漏斗数据 // 尝试从API获取漏斗数据
try { try {
// 使用选中的项目ID // 使用选中的项目ID
@@ -318,15 +353,20 @@ const Analytics: React.FC = () => {
}; };
fetchAnalyticsData(); fetchAnalyticsData();
}, [timeRange, selectedProject]); // 添加selectedProject作为依赖项 }, [timeRange, selectedProject, kolSortBy, kolSortOrder, kolPage, kolPageSize]); // 更新依赖项
// 当排序或筛选条件变化时,重置页码
useEffect(() => {
setKolPage(0);
}, [timeRange, selectedProject, kolSortBy, kolSortOrder]);
// Filter KOLs based on selected platform // Filter KOLs based on selected platform
const filteredKOLs = selectedPlatform === 'all' const filteredKOLs = selectedPlatform === 'all'
? kolData ? kolData
: kolData.filter(kol => kol.platform === selectedPlatform); : kolData.filter(kol => kol.platform.toLowerCase() === selectedPlatform.toLowerCase());
// Sort KOLs by followers count // 不需要额外排序API已经排序
const sortedKOLs = [...filteredKOLs].sort((a, b) => b.followers - a.followers); const sortedKOLs = filteredKOLs;
// Update filtered engagement data when KOL selection changes // Update filtered engagement data when KOL selection changes
useEffect(() => { useEffect(() => {
@@ -338,11 +378,14 @@ const Analytics: React.FC = () => {
}, [selectedKOL]); }, [selectedKOL]);
const getPlatformIcon = (platform: string) => { const getPlatformIcon = (platform: string) => {
switch (platform) { const platformLower = platform.toLowerCase();
switch (platformLower) {
case 'facebook': case 'facebook':
return <Facebook className="w-5 h-5 text-blue-600" />; return <Facebook className="w-5 h-5 text-blue-600" />;
case 'threads': case 'threads':
return <Hash className="w-5 h-5 text-black" />; return <Hash className="w-5 h-5 text-black" />;
case 'twitter':
return <Twitter className="w-5 h-5 text-blue-400" />;
case 'instagram': case 'instagram':
return <Instagram className="w-5 h-5 text-pink-500" />; return <Instagram className="w-5 h-5 text-pink-500" />;
case 'linkedin': case 'linkedin':
@@ -355,6 +398,43 @@ const Analytics: React.FC = () => {
return null; return null;
} }
}; };
// 帮助函数:获取表示变化趋势的图标和样式
const getTrendingIcon = (change: number | null | string) => {
// 如果是字符串(可能是"待计算"或者null返回中性图标
if (typeof change !== 'number') {
return {
icon: <div className="w-4 h-4" />,
colorClass: 'text-gray-500'
};
}
if (change > 0) {
return {
icon: <TrendingUp className="w-4 h-4" />,
colorClass: 'text-green-500'
};
} else if (change < 0) {
return {
icon: <TrendingDown className="w-4 h-4" />,
colorClass: 'text-red-500'
};
} else {
return {
icon: <div className="w-4 h-4" />,
colorClass: 'text-gray-500'
};
}
};
// 帮助函数:格式化百分比变化
const formatPercentageChange = (change: number | null | string) => {
if (change === null || typeof change === 'string') {
return '0%';
}
return `${change > 0 ? '+' : ''}${change.toFixed(1)}%`;
};
const getStatusIcon = (status: string) => { const getStatusIcon = (status: string) => {
switch (status) { switch (status) {
@@ -602,6 +682,7 @@ const Analytics: React.FC = () => {
onChange={(e) => setSelectedProject(e.target.value)} onChange={(e) => setSelectedProject(e.target.value)}
className="block w-full py-2 pl-3 pr-10 text-base border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" className="block w-full py-2 pl-3 pr-10 text-base border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
> >
<option value="all"></option>
{projects.map((project) => ( {projects.map((project) => (
<option key={project.id} value={project.id}> <option key={project.id} value={project.id}>
{project.name} {project.name}
@@ -619,8 +700,6 @@ const Analytics: React.FC = () => {
<div className="p-6"> <div className="p-6">
<h1 className="mb-6 text-2xl font-bold">Analytics Dashboard</h1> <h1 className="mb-6 text-2xl font-bold">Analytics Dashboard</h1>
{/* 删除这里的项目选择器 */}
{loading ? ( {loading ? (
<div className="flex flex-col items-center justify-center h-80"> <div className="flex flex-col items-center justify-center h-80">
<div className="w-16 h-16 border-4 border-blue-500 rounded-full border-t-transparent animate-spin"></div> <div className="w-16 h-16 border-4 border-blue-500 rounded-full border-t-transparent animate-spin"></div>
@@ -651,10 +730,9 @@ const Analytics: React.FC = () => {
value={timeRange} value={timeRange}
onChange={(e) => setTimeRange(e.target.value)} onChange={(e) => setTimeRange(e.target.value)}
> >
<option value="7days"> 7 </option> <option value="7"> 7 </option>
<option value="30days"> 30 </option> <option value="30"> 30 </option>
<option value="90days"> 90 </option> <option value="90"> 90 </option>
<option value="1year"> 1 </option>
</select> </select>
<select <select
@@ -664,7 +742,7 @@ const Analytics: React.FC = () => {
> >
<option value="all"> KOL</option> <option value="all"> KOL</option>
{kolData.map(kol => ( {kolData.map(kol => (
<option key={kol.id} value={kol.id}>{kol.name}</option> <option key={kol.influencer_id} value={kol.influencer_id}>{kol.name}</option>
))} ))}
</select> </select>
@@ -676,7 +754,7 @@ const Analytics: React.FC = () => {
<option value="all"></option> <option value="all"></option>
<option value="facebook">Facebook</option> <option value="facebook">Facebook</option>
<option value="instagram">Instagram</option> <option value="instagram">Instagram</option>
<option value="threads">Threads</option> <option value="twitter">Twitter</option>
<option value="youtube">YouTube</option> <option value="youtube">YouTube</option>
<option value="xiaohongshu"></option> <option value="xiaohongshu"></option>
</select> </select>
@@ -689,30 +767,198 @@ const Analytics: React.FC = () => {
</div> </div>
</div> </div>
{/* KOL 表現概覽 */} {/* KOL 表現概覽 - 使用API数据 */}
<div className="p-6 mb-8 bg-white rounded-lg shadow"> <div className="p-6 mb-8 bg-white rounded-lg shadow">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-medium text-gray-800">KOL </h3> <h3 className="text-lg font-medium text-gray-800">KOL </h3>
<div className="flex items-center text-sm text-blue-600">
<span className="mr-1"></span> <div className="flex items-center space-x-4">
<ArrowRight className="w-4 h-4" /> {/* 排序控制 */}
<div className="flex items-center">
<span className="mr-2 text-sm text-gray-600">:</span>
<select
className="px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={kolSortBy}
onChange={(e) => setKolSortBy(e.target.value)}
>
<option value="followers_change"></option>
<option value="likes_change"></option>
<option value="follows_change"></option>
<option value="followers_count"></option>
</select>
<button
className="flex items-center justify-center w-8 h-8 ml-2 text-gray-500 bg-gray-100 rounded-md hover:bg-gray-200"
onClick={() => setKolSortOrder(prev => prev === 'asc' ? 'desc' : 'asc')}
>
{kolSortOrder === 'asc' ? '↑' : '↓'}
</button>
</div>
<div className="flex items-center text-sm text-blue-600 cursor-pointer">
<span className="mr-1"></span>
<ArrowRight className="w-4 h-4" />
</div>
</div> </div>
</div> </div>
<div className="overflow-hidden border border-gray-200 rounded-lg shadow-sm"> {kolLoading ? (
<div className="p-6 text-center"> <div className="flex flex-col items-center justify-center h-64">
<div className="mb-4 text-gray-500"> <div className="w-12 h-12 border-4 border-blue-500 rounded-full border-t-transparent animate-spin"></div>
<MessageSquare className="inline-block w-12 h-12" /> <p className="mt-4 text-gray-600">KOL数据中...</p>
</div>
<h3 className="mb-2 text-lg font-medium text-gray-900"></h3>
<p className="text-gray-500"></p>
</div> </div>
</div> ) : kolError ? (
<div className="p-6 text-center border border-red-200 rounded-lg bg-red-50">
<div className="mb-4 text-red-500">
<AlertTriangle className="inline-block w-12 h-12" />
</div>
<h3 className="mb-2 text-lg font-medium text-red-700">KOL数据</h3>
<p className="text-red-600">{kolError}</p>
<button
onClick={fetchKolOverviewData}
className="px-4 py-2 mt-4 text-white bg-blue-600 rounded-md hover:bg-blue-700"
>
</button>
</div>
) : sortedKOLs.length === 0 ? (
<div className="p-6 text-center border border-gray-200 rounded-lg">
<div className="mb-4 text-gray-400">
<Users className="inline-block w-12 h-12" />
</div>
<h3 className="mb-2 text-lg font-medium text-gray-700">KOL数据</h3>
<p className="text-gray-500">KOL数据</p>
</div>
) : (
<>
{/* KOL卡片网格 */}
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{sortedKOLs.map((kol) => (
<div key={kol.influencer_id} className="overflow-hidden transition-shadow duration-300 bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md">
<div className="p-4">
{/* KOL头部信息 */}
<div className="flex items-center mb-4">
<div className="w-12 h-12 mr-3 overflow-hidden bg-gray-200 rounded-full">
{/* 如果有头像则使用,否则显示占位符 */}
{kol.avatar ? (
<img src={kol.avatar} alt={kol.name} className="object-cover w-full h-full" />
) : (
<div className="flex items-center justify-center w-full h-full text-xl font-semibold text-gray-500">
{kol.name.charAt(0).toUpperCase()}
</div>
)}
</div>
<div>
<h4 className="text-base font-semibold text-gray-900">{kol.name}</h4>
<div className="flex items-center text-sm text-gray-500">
{getPlatformIcon(kol.platform)}
<span className="ml-1">{kol.platform}</span>
</div>
</div>
</div>
{/* KOL指标 */}
<div className="space-y-3">
{/* 粉丝增长 */}
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">:</span>
<div className="flex items-center">
<span className="mr-2 text-base font-semibold">{kol.followers_change.toLocaleString()}</span>
<div className={getTrendingIcon(kol.followers_change_percentage).colorClass}>
{getTrendingIcon(kol.followers_change_percentage).icon}
<span className="ml-1 text-xs">
{formatPercentageChange(kol.followers_change_percentage)}
</span>
</div>
</div>
</div>
{/* 新增点赞 */}
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">:</span>
<div className="flex items-center">
<span className="mr-2 text-base font-semibold">{kol.likes_change.toLocaleString()}</span>
<div className={getTrendingIcon(kol.likes_change_percentage).colorClass}>
{getTrendingIcon(kol.likes_change_percentage).icon}
<span className="ml-1 text-xs">
{formatPercentageChange(kol.likes_change_percentage)}
</span>
</div>
</div>
</div>
{/* 新增关注 */}
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">:</span>
<div className="flex items-center">
<span className="mr-2 text-base font-semibold">{kol.follows_change.toLocaleString()}</span>
<div className={getTrendingIcon(kol.follows_change_percentage).colorClass}>
{getTrendingIcon(kol.follows_change_percentage).icon}
<span className="ml-1 text-xs">
{formatPercentageChange(kol.follows_change_percentage)}
</span>
</div>
</div>
</div>
{/* 粉丝总数 */}
<div className="flex items-center justify-between pt-2 mt-2 border-t border-gray-100">
<span className="text-sm text-gray-600">:</span>
<span className="text-base font-semibold">{kol.followers_count.toLocaleString()}</span>
</div>
</div>
</div>
{/* 链接到详情 */}
<div className="py-2 text-center text-blue-600 bg-blue-50 hover:bg-blue-100">
<a href={kol.profile_url} target="_blank" rel="noopener noreferrer" className="text-sm">
</a>
</div>
</div>
))}
</div>
{/* 分页控制 */}
{kolTotal > kolPageSize && (
<div className="flex items-center justify-between mt-6">
<div className="text-sm text-gray-500">
{kolPage * kolPageSize + 1} - {Math.min((kolPage + 1) * kolPageSize, kolTotal)} {kolTotal}
</div>
<div className="flex space-x-2">
<button
onClick={() => setKolPage(prev => Math.max(0, prev - 1))}
disabled={kolPage === 0}
className={`px-3 py-1 text-sm border rounded ${
kolPage === 0
? 'text-gray-400 border-gray-200 cursor-not-allowed'
: 'text-blue-600 border-blue-200 hover:bg-blue-50'
}`}
>
</button>
<button
onClick={() => setKolPage(prev => prev + 1)}
disabled={(kolPage + 1) * kolPageSize >= kolTotal}
className={`px-3 py-1 text-sm border rounded ${
(kolPage + 1) * kolPageSize >= kolTotal
? 'text-gray-400 border-gray-200 cursor-not-allowed'
: 'text-blue-600 border-blue-200 hover:bg-blue-50'
}`}
>
</button>
</div>
</div>
)}
</>
)}
</div> </div>
{/* 轉換漏斗 */} {/* 转换漏斗 */}
<div className="p-6 mb-8 bg-white rounded-lg shadow"> <div className="p-6 mb-8 bg-white rounded-lg shadow">
<h3 className="mb-6 text-lg font-medium text-gray-800">KOL </h3> <h3 className="mb-6 text-lg font-medium text-gray-800">KOL </h3>
<div className="flex justify-center"> <div className="flex justify-center">
<div className="w-full max-w-3xl"> <div className="w-full max-w-3xl">
{funnelData.map((stage, index) => ( {funnelData.map((stage, index) => (
@@ -730,7 +976,7 @@ const Analytics: React.FC = () => {
<div className="flex justify-center my-1"> <div className="flex justify-center my-1">
<div className="flex items-center text-sm text-gray-500"> <div className="flex items-center text-sm text-gray-500">
<ArrowRight className="w-4 h-4 mr-1" /> <ArrowRight className="w-4 h-4 mr-1" />
: {((funnelData[index + 1].count / stage.count) * 100).toFixed(1)}% : {((funnelData[index + 1].count / stage.count) * 100).toFixed(1)}%
</div> </div>
</div> </div>
)} )}
@@ -740,34 +986,34 @@ const Analytics: React.FC = () => {
</div> </div>
<div className="grid grid-cols-1 gap-4 mt-6 md:grid-cols-3"> <div className="grid grid-cols-1 gap-4 mt-6 md:grid-cols-3">
<div className="p-4 rounded-lg bg-gray-50"> <div className="p-4 rounded-lg bg-gray-50">
<h4 className="mb-2 text-sm font-medium text-gray-700"></h4> <h4 className="mb-2 text-sm font-medium text-gray-700"></h4>
<p className="text-2xl font-bold text-blue-600"> <p className="text-2xl font-bold text-blue-600">
{((funnelData[funnelData.length - 1].count / funnelData[0].count) * 100).toFixed(1)}% {((funnelData[funnelData.length - 1].count / funnelData[0].count) * 100).toFixed(1)}%
</p> </p>
<p className="mt-1 text-xs text-gray-500"></p> <p className="mt-1 text-xs text-gray-500"></p>
</div> </div>
<div className="p-4 rounded-lg bg-gray-50"> <div className="p-4 rounded-lg bg-gray-50">
<h4 className="mb-2 text-sm font-medium text-gray-700"></h4> <h4 className="mb-2 text-sm font-medium text-gray-700"></h4>
<p className="text-2xl font-bold text-green-600"> </p> <p className="text-2xl font-bold text-green-600"> </p>
<p className="mt-1 text-xs text-gray-500"> 15%</p> <p className="mt-1 text-xs text-gray-500"> 15%</p>
</div> </div>
<div className="p-4 rounded-lg bg-gray-50"> <div className="p-4 rounded-lg bg-gray-50">
<h4 className="mb-2 text-sm font-medium text-gray-700"></h4> <h4 className="mb-2 text-sm font-medium text-gray-700"></h4>
<p className="text-2xl font-bold text-red-600"> </p> <p className="text-2xl font-bold text-red-600"> </p>
<p className="mt-1 text-xs text-gray-500"> 23%</p> <p className="mt-1 text-xs text-gray-500"> 23%</p>
</div> </div>
</div> </div>
</div> </div>
{/* KOL 文表 */} {/* KOL 文表 */}
<div className="p-6 mb-8 bg-white rounded-lg shadow"> <div className="p-6 mb-8 bg-white rounded-lg shadow">
<h3 className="mb-6 text-lg font-medium text-gray-800">KOL </h3> <h3 className="mb-6 text-lg font-medium text-gray-800">KOL </h3>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200"> <table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase"> <th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th> </th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase"> <th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
KOL KOL
@@ -776,22 +1022,22 @@ const Analytics: React.FC = () => {
</th> </th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase"> <th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th> </th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase"> <th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th> </th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase"> <th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th> </th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase"> <th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th> </th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase"> <th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th> </th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase"> <th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -810,13 +1056,13 @@ const Analytics: React.FC = () => {
<div className="flex items-center"> <div className="flex items-center">
<div className="w-8 h-8 mr-2 overflow-hidden rounded-full"> <div className="w-8 h-8 mr-2 overflow-hidden rounded-full">
<img <img
src={kolData.find(k => k.id === post.kolId)?.avatar || ''} src={kolData.find(k => k.influencer_id === post.kolId)?.avatar || ''}
alt={kolData.find(k => k.id === post.kolId)?.name || ''} alt={kolData.find(k => k.influencer_id === post.kolId)?.name || ''}
className="object-cover w-full h-full" className="object-cover w-full h-full"
/> />
</div> </div>
<div className="text-sm text-gray-900"> <div className="text-sm text-gray-900">
{kolData.find(k => k.id === post.kolId)?.name || ''} {kolData.find(k => k.influencer_id === post.kolId)?.name || ''}
</div> </div>
</div> </div>
</td> </td>
@@ -824,7 +1070,7 @@ const Analytics: React.FC = () => {
<div className="flex items-center"> <div className="flex items-center">
{getPlatformIcon(post.platform)} {getPlatformIcon(post.platform)}
<span className="ml-2 text-sm text-gray-900"> <span className="ml-2 text-sm text-gray-900">
{post.platform === 'xiaohongshu' ? '小紅書' : post.platform} {post.platform === 'xiaohongshu' ? '小红书' : post.platform}
</span> </span>
</div> </div>
</td> </td>
@@ -875,29 +1121,29 @@ const Analytics: React.FC = () => {
</div> </div>
</div> </div>
{/* 概卡片 */} {/* 概卡片 */}
<div className="grid grid-cols-1 gap-6 mb-8 md:grid-cols-3"> <div className="grid grid-cols-1 gap-6 mb-8 md:grid-cols-3">
<div className="p-6 bg-white rounded-lg shadow"> <div className="p-6 bg-white rounded-lg shadow">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-800"></h3> <h3 className="text-lg font-medium text-gray-800"></h3>
<MessageSquare className="w-6 h-6 text-blue-600" /> <MessageSquare className="w-6 h-6 text-blue-600" />
</div> </div>
<p className="mb-2 text-3xl font-bold text-gray-900">{platformData.reduce((sum, item) => sum + item.value, 0)}</p> <p className="mb-2 text-3xl font-bold text-gray-900">{platformData.reduce((sum, item) => sum + item.value, 0)}</p>
<div className="flex items-center text-sm"> <div className="flex items-center text-sm">
<TrendingUp className="w-4 h-4 mr-1 text-green-500" /> <TrendingUp className="w-4 h-4 mr-1 text-green-500" />
<span className="text-green-500"> 12% </span> <span className="text-green-500"> 12% </span>
</div> </div>
</div> </div>
<div className="p-6 bg-white rounded-lg shadow"> <div className="p-6 bg-white rounded-lg shadow">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-800"></h3> <h3 className="text-lg font-medium text-gray-800"></h3>
<Users className="w-6 h-6 text-blue-600" /> <Users className="w-6 h-6 text-blue-600" />
</div> </div>
<p className="mb-2 text-3xl font-bold text-gray-900">4.8%</p> <p className="mb-2 text-3xl font-bold text-gray-900">4.8%</p>
<div className="flex items-center text-sm"> <div className="flex items-center text-sm">
<TrendingUp className="w-4 h-4 mr-1 text-green-500" /> <TrendingUp className="w-4 h-4 mr-1 text-green-500" />
<span className="text-green-500"> 0.5% </span> <span className="text-green-500"> 0.5% </span>
</div> </div>
</div> </div>
@@ -909,14 +1155,14 @@ const Analytics: React.FC = () => {
<p className="mb-2 text-3xl font-bold text-gray-900">{sentimentData.positive}% </p> <p className="mb-2 text-3xl font-bold text-gray-900">{sentimentData.positive}% </p>
<div className="flex items-center text-sm"> <div className="flex items-center text-sm">
<TrendingUp className="w-4 h-4 mr-1 text-green-500" /> <TrendingUp className="w-4 h-4 mr-1 text-green-500" />
<span className="text-green-500"> 5% </span> <span className="text-green-500"> 5% </span>
</div> </div>
</div> </div>
</div> </div>
{/* 留言趨勢圖 */} {/* 留言趋势图 */}
<div className="p-6 mb-8 bg-white rounded-lg shadow"> <div className="p-6 mb-8 bg-white rounded-lg shadow">
<h3 className="mb-4 text-lg font-medium text-gray-800"></h3> <h3 className="mb-4 text-lg font-medium text-gray-800"></h3>
<div className="h-64"> <div className="h-64">
<div className="flex items-end space-x-2 h-52"> <div className="flex items-end space-x-2 h-52">
{timelineData.map((item, index) => ( {timelineData.map((item, index) => (
@@ -940,9 +1186,9 @@ const Analytics: React.FC = () => {
</div> </div>
<div className="grid grid-cols-1 gap-6 mb-8 lg:grid-cols-2"> <div className="grid grid-cols-1 gap-6 mb-8 lg:grid-cols-2">
{/* 平台分 */} {/* 平台分 */}
<div className="p-6 bg-white rounded-lg shadow"> <div className="p-6 bg-white rounded-lg shadow">
<h3 className="mb-4 text-lg font-medium text-gray-800"></h3> <h3 className="mb-4 text-lg font-medium text-gray-800"></h3>
<div className="space-y-4"> <div className="space-y-4">
{platformData.map((item, index) => ( {platformData.map((item, index) => (
<div key={index}> <div key={index}>
@@ -950,11 +1196,11 @@ const Analytics: React.FC = () => {
<div className="flex items-center"> <div className="flex items-center">
{getPlatformIcon(item.name)} {getPlatformIcon(item.name)}
<span className="ml-2 text-sm font-medium text-gray-700"> <span className="ml-2 text-sm font-medium text-gray-700">
{item.name === 'xiaohongshu' ? '小紅書' : item.name} {item.name === 'xiaohongshu' ? '小红书' : item.name}
</span> </span>
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<span className="mr-2 text-sm text-gray-500">{item.value} </span> <span className="mr-2 text-sm text-gray-500">{item.value} </span>
<span className="text-sm font-medium text-gray-700">{item.percentage}%</span> <span className="text-sm font-medium text-gray-700">{item.percentage}%</span>
</div> </div>
</div> </div>
@@ -969,13 +1215,13 @@ const Analytics: React.FC = () => {
</div> </div>
</div> </div>
{/* 審核狀態分佈 */} {/* 审核状态分布 */}
<div className="p-6 bg-white rounded-lg shadow"> <div className="p-6 bg-white rounded-lg shadow">
<h3 className="mb-4 text-lg font-medium text-gray-800"></h3> <h3 className="mb-4 text-lg font-medium text-gray-800"></h3>
<div className="flex justify-center mb-6"> <div className="flex justify-center mb-6">
<div className="relative w-48 h-48 rounded-full"> <div className="relative w-48 h-48 rounded-full">
{statusData.map((item, index) => { {statusData.map((item, index) => {
// 算每扇形的起始角度和束角度 // 算每扇形的起始角度和束角度
const startAngle = index === 0 ? 0 : statusData.slice(0, index).reduce((sum, i) => sum + i.percentage, 0) * 3.6; const startAngle = index === 0 ? 0 : statusData.slice(0, index).reduce((sum, i) => sum + i.percentage, 0) * 3.6;
const endAngle = startAngle + item.percentage * 3.6; const endAngle = startAngle + item.percentage * 3.6;
@@ -1003,7 +1249,7 @@ const Analytics: React.FC = () => {
<span className="ml-2 text-sm font-medium text-gray-700">{getStatusName(item.name)}</span> <span className="ml-2 text-sm font-medium text-gray-700">{getStatusName(item.name)}</span>
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<span className="mr-2 text-sm text-gray-500">{item.value} </span> <span className="mr-2 text-sm text-gray-500">{item.value} </span>
<span className="text-sm font-medium text-gray-700">{item.percentage}%</span> <span className="text-sm font-medium text-gray-700">{item.percentage}%</span>
</div> </div>
</div> </div>
@@ -1012,128 +1258,49 @@ const Analytics: React.FC = () => {
</div> </div>
</div> </div>
<div className="grid grid-cols-1 gap-6 mb-8 lg:grid-cols-2"> {/* 情感分析详情 */}
{/* 情感分析詳情 */} <div className="p-6 bg-white rounded-lg shadow">
<div className="p-6 bg-white rounded-lg shadow"> <h3 className="mb-4 text-lg font-medium text-gray-800"></h3>
<h3 className="mb-4 text-lg font-medium text-gray-800"></h3> <div className="flex justify-center mb-6">
<div className="flex justify-center mb-6"> <div className="relative w-48 h-12 rounded-lg bg-gradient-to-r from-red-500 via-yellow-400 to-green-500">
<div className="relative w-48 h-12 rounded-lg bg-gradient-to-r from-red-500 via-yellow-400 to-green-500"> <div
<div className="absolute top-0 w-1 h-full transform -translate-x-1/2 bg-black border-2 border-white rounded-full"
className="absolute top-0 w-1 h-full transform -translate-x-1/2 bg-black border-2 border-white rounded-full" style={{ left: `${sentimentData.positive}%` }}
style={{ left: `${sentimentData.positive}%` }} ></div>
></div>
</div>
</div>
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p className="text-lg font-bold text-red-500">{sentimentData.negative}%</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p className="text-lg font-bold text-yellow-500">{sentimentData.neutral}%</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p className="text-lg font-bold text-green-500">{sentimentData.positive}%</p>
</div>
</div> </div>
</div> </div>
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p className="text-lg font-bold text-red-500">{sentimentData.negative}%</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p className="text-lg font-bold text-yellow-500">{sentimentData.neutral}%</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p className="text-lg font-bold text-green-500">{sentimentData.positive}%</p>
</div>
</div>
</div>
{/* 熱門文章 */} {/* 热门文章 */}
<div className="p-6 bg-white rounded-lg shadow"> <div className="p-6 bg-white rounded-lg shadow">
<h3 className="mb-4 text-lg font-medium text-gray-800"></h3> <h3 className="mb-4 text-lg font-medium text-gray-800"></h3>
<div className="space-y-4"> <div className="space-y-4">
{popularArticles.map((article: any, index: number) => ( {popularArticles.map((article: any, index: number) => (
<div key={index} className="pb-3 border-b border-gray-200 last:border-0 last:pb-0"> <div key={index} className="pb-3 border-b border-gray-200 last:border-0 last:pb-0">
<p className="mb-1 text-sm font-medium text-gray-800">{article.title}</p> <p className="mb-1 text-sm font-medium text-gray-800">{article.title}</p>
<div className="flex items-center justify-between text-xs text-gray-500"> <div className="flex items-center justify-between text-xs text-gray-500">
<span>{article.count} </span> <span>{article.count} </span>
<div className="flex items-center"> <div className="flex items-center">
<div className="w-2 h-2 mr-1 bg-green-500 rounded-full"></div> <div className="w-2 h-2 mr-1 bg-green-500 rounded-full"></div>
<span></span> <span></span>
</div>
</div> </div>
</div> </div>
))}
</div>
</div>
</div>
{/* 關鍵字雲 */}
<div className="p-6 mb-8 bg-white rounded-lg shadow">
<h3 className="mb-4 text-lg font-medium text-gray-800"></h3>
<div className="flex flex-wrap justify-center gap-3 py-4">
<span className="px-4 py-2 text-lg text-blue-800 bg-blue-100 rounded-full"></span>
<span className="px-6 py-3 text-xl text-green-800 bg-green-100 rounded-full"></span>
<span className="px-3 py-1 text-base text-yellow-800 bg-yellow-100 rounded-full"></span>
<span className="px-5 py-2 text-lg text-purple-800 bg-purple-100 rounded-full"></span>
<span className="py-3 text-2xl text-red-800 bg-red-100 rounded-full px-7"></span>
<span className="px-3 py-1 text-base text-indigo-800 bg-indigo-100 rounded-full"></span>
<span className="px-4 py-2 text-lg text-pink-800 bg-pink-100 rounded-full"></span>
<span className="px-5 py-2 text-lg text-blue-800 bg-blue-100 rounded-full"></span>
<span className="px-3 py-1 text-base text-green-800 bg-green-100 rounded-full">便</span>
<span className="px-6 py-3 text-xl text-yellow-800 bg-yellow-100 rounded-full"></span>
<span className="px-4 py-2 text-lg text-purple-800 bg-purple-100 rounded-full"></span>
<span className="px-3 py-1 text-base text-red-800 bg-red-100 rounded-full"></span>
</div>
</div>
{/* 用戶互動時間分析 */}
<div className="p-6 bg-white rounded-lg shadow">
<h3 className="mb-4 text-lg font-medium text-gray-800"></h3>
<div className="grid h-40 grid-cols-12 gap-1">
{Array.from({ length: 24 }).map((_, hour) => {
// 模擬不同時段的活躍度
let height = '20%';
if (hour >= 9 && hour <= 11) height = '60%';
if (hour >= 12 && hour <= 14) height = '40%';
if (hour >= 19 && hour <= 22) height = '80%';
return (
<div key={hour} className="flex flex-col items-center justify-end">
<div
className="w-full transition-all bg-blue-500 rounded-t-sm hover:bg-blue-600"
style={{ height }}
></div>
<span className="mt-1 text-xs">{hour}</span>
</div>
);
})}
</div>
<div className="mt-2 text-sm text-center text-gray-500">
<p> (24)</p>
</div>
</div>
{/* 內容表現分析 */}
<div className="p-6 bg-white rounded-lg shadow-sm">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-medium text-gray-800"></h3>
<div className="flex space-x-2">
<select
className="px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={selectedPlatform}
onChange={(e) => setSelectedPlatform(e.target.value)}
>
<option value="all"></option>
<option value="facebook">Facebook</option>
<option value="instagram">Instagram</option>
<option value="twitter">Twitter</option>
<option value="linkedin">LinkedIn</option>
<option value="youtube">YouTube</option>
</select>
</div>
</div>
<div className="overflow-hidden border border-gray-200 rounded-lg shadow-sm">
<div className="p-6 text-center">
<div className="mb-4 text-gray-500">
<MessageSquare className="inline-block w-12 h-12" />
</div> </div>
<h3 className="mb-2 text-lg font-medium text-gray-900"></h3> ))}
<p className="text-gray-500"></p>
</div>
</div> </div>
</div> </div>
</> </>