實作個人收藏、個人活動紀錄

This commit is contained in:
2025-09-11 17:40:07 +08:00
parent bc2104d374
commit 9c5dceb001
29 changed files with 3781 additions and 601 deletions

View File

@@ -6,7 +6,14 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Progress } from "@/components/ui/progress"
import { BarChart3, Clock, Heart, ImageIcon, MessageSquare, FileText, TrendingUp, Trash2, RefreshCw } from "lucide-react"
import {
BarChart3, Clock, Heart, ImageIcon, MessageSquare, FileText, TrendingUp, Trash2, RefreshCw,
Bot, Mic, Settings, Zap, Star, Eye, Target, Users, Lightbulb, Search, Database, Shield, Cpu,
Globe, Layers, PieChart, Activity, Calendar, Code, Command, Compass, CreditCard, Download,
Edit, ExternalLink, Filter, Flag, Folder, Gift, Home, Info, Key, Link, Lock, Mail, MapPin,
Minus, Monitor, MoreHorizontal, MoreVertical, MousePointer, Navigation, Pause, Play, Plus,
Power, Save, Send, Share, Smartphone, Tablet, Upload, Volume2, Wifi, X, ZoomIn, ZoomOut
} from "lucide-react"
import { useState, useEffect } from "react"
interface ActivityRecordsDialogProps {
@@ -30,74 +37,47 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia
const [recentApps, setRecentApps] = useState<any[]>([])
const [categoryData, setCategoryData] = useState<any[]>([])
const [userStats, setUserStats] = useState({
totalUsage: 0,
favoriteApps: 0,
daysJoined: 0
})
const [isLoading, setIsLoading] = useState(false)
const [isResetting, setIsResetting] = useState(false)
if (!user) return null
// Calculate user statistics
const calculateUserStats = () => {
if (!user) return {
totalUsage: 0,
totalDuration: 0,
favoriteApps: 0,
daysJoined: 0
}
// Load user activity data from API
const loadUserActivity = async () => {
if (!user) return
// Calculate total usage count (views)
const totalUsage = Object.values(user.recentApps || []).length
// Calculate total duration (simplified - 5 minutes per app view)
const totalDuration = totalUsage * 5 // minutes
// Get favorite apps count
const favoriteApps = user.favoriteApps?.length || 0
// Calculate days joined
const joinDate = new Date(user.joinDate)
const now = new Date()
// Check if joinDate is valid
let daysJoined = 0
if (!isNaN(joinDate.getTime())) {
daysJoined = Math.floor((now.getTime() - joinDate.getTime()) / (1000 * 60 * 60 * 24))
}
return {
totalUsage,
totalDuration,
favoriteApps,
daysJoined: Math.max(0, daysJoined)
setIsLoading(true)
try {
const response = await fetch(`/api/user/activity?userId=${user.id}`)
const data = await response.json()
if (data.success) {
setRecentApps(data.data.recentApps || [])
setCategoryData(data.data.categoryStats || [])
setUserStats(data.data.userStats || {
totalUsage: 0,
favoriteApps: 0,
daysJoined: 0
})
}
} catch (error) {
console.error('載入用戶活動數據錯誤:', error)
} finally {
setIsLoading(false)
}
}
const stats = calculateUserStats()
// Load recent apps from user's recent apps
// Load data when dialog opens
useEffect(() => {
if (user?.recentApps) {
// Convert recent app IDs to app objects (simplified)
const recentAppsData = user.recentApps.slice(0, 10).map((appId, index) => ({
id: appId,
name: `應用 ${appId}`,
author: "系統",
category: "AI應用",
usageCount: getViewCount(appId),
timeSpent: "5分鐘",
lastUsed: `${index + 1}天前`,
icon: MessageSquare,
color: "bg-blue-500"
}))
setRecentApps(recentAppsData)
} else {
setRecentApps([])
if (open && user) {
loadUserActivity()
}
}, [user, getViewCount])
// Load category data (simplified)
useEffect(() => {
// This would normally be calculated from actual usage data
setCategoryData([])
}, [user])
}, [open, user])
// Reset user activity data
const resetActivityData = async () => {
@@ -155,38 +135,150 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia
<h3 className="text-lg font-semibold mb-2">使</h3>
<p className="text-sm text-muted-foreground mb-4"> AI </p>
{recentApps.length > 0 ? (
{isLoading ? (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-500 mx-auto mb-2"></div>
<p className="text-gray-500">...</p>
</div>
) : recentApps.length > 0 ? (
<div className="grid gap-4">
{recentApps.map((app) => {
const IconComponent = app.icon
// 圖標映射函數
const getIconComponent = (iconName: string) => {
const iconMap: { [key: string]: any } = {
'Bot': Bot,
'ImageIcon': ImageIcon,
'Mic': Mic,
'MessageSquare': MessageSquare,
'Settings': Settings,
'Zap': Zap,
'TrendingUp': TrendingUp,
'Star': Star,
'Heart': Heart,
'Eye': Eye,
'Target': Target,
'Users': Users,
'Lightbulb': Lightbulb,
'Search': Search,
'BarChart3': BarChart3,
'Database': Database,
'Shield': Shield,
'Cpu': Cpu,
'FileText': FileText,
'Globe': Globe,
'Layers': Layers,
'PieChart': PieChart,
'Activity': Activity,
'Calendar': Calendar,
'Clock': Clock,
'Code': Code,
'Command': Command,
'Compass': Compass,
'CreditCard': CreditCard,
'Download': Download,
'Edit': Edit,
'ExternalLink': ExternalLink,
'Filter': Filter,
'Flag': Flag,
'Folder': Folder,
'Gift': Gift,
'Home': Home,
'Info': Info,
'Key': Key,
'Link': Link,
'Lock': Lock,
'Mail': Mail,
'MapPin': MapPin,
'Minus': Minus,
'Monitor': Monitor,
'MoreHorizontal': MoreHorizontal,
'MoreVertical': MoreVertical,
'MousePointer': MousePointer,
'Navigation': Navigation,
'Pause': Pause,
'Play': Play,
'Plus': Plus,
'Power': Power,
'RefreshCw': RefreshCw,
'Save': Save,
'Send': Send,
'Share': Share,
'Smartphone': Smartphone,
'Tablet': Tablet,
'Trash2': Trash2,
'Upload': Upload,
'Volume2': Volume2,
'Wifi': Wifi,
'X': X,
'ZoomIn': ZoomIn,
'ZoomOut': ZoomOut,
}
return iconMap[iconName] || MessageSquare
}
const IconComponent = getIconComponent(app.icon || 'MessageSquare')
const lastUsedDate = new Date(app.lastUsed)
const now = new Date()
const diffTime = Math.abs(now.getTime() - lastUsedDate.getTime())
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
return (
<Card key={app.id} className="hover:shadow-md transition-shadow">
<CardContent className="flex items-center justify-between p-4">
<div className="flex items-center space-x-4">
<div className={`p-3 rounded-lg ${app.color}`}>
<div className={`p-3 rounded-lg bg-gradient-to-r ${app.iconColor || 'from-blue-500 to-purple-500'}`}>
<IconComponent className="w-6 h-6 text-white" />
</div>
<div>
<h4 className="font-semibold">{app.name}</h4>
<p className="text-sm text-muted-foreground">by {app.author}</p>
<p className="text-sm text-muted-foreground">by {app.creator}</p>
<div className="flex items-center gap-4 mt-1">
<Badge variant="secondary" className="text-xs">
{app.category}
{app.type}
</Badge>
<span className="text-xs text-muted-foreground flex items-center gap-1">
<BarChart3 className="w-3 h-3" />
使 {app.usageCount}
</span>
<span className="text-xs text-muted-foreground flex items-center gap-1">
<Clock className="w-3 h-3" />
{app.timeSpent}
</span>
</div>
</div>
</div>
<div className="text-right">
<p className="text-xs text-muted-foreground mb-2">{app.lastUsed}</p>
<Button size="sm" variant="outline">
<p className="text-xs text-muted-foreground mb-2">
{diffDays === 1 ? '昨天' : diffDays < 7 ? `${diffDays}天前` : `${Math.ceil(diffDays / 7)}週前`}
</p>
<Button
size="sm"
variant="outline"
onClick={() => {
// 記錄活動
if (user) {
fetch('/api/user/activity', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: user.id,
action: 'view',
resourceType: 'app',
resourceId: app.id,
details: JSON.stringify({ appName: app.name })
})
}).catch(console.error)
}
// 增加查看次數
fetch(`/api/apps/${app.id}/interactions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'view', userId: user?.id })
}).catch(console.error)
// 打開應用
if (app.appUrl && app.appUrl !== '#') {
window.open(app.appUrl, '_blank')
}
}}
>
</Button>
</div>
@@ -237,35 +329,16 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia
</div>
{/* Statistics Cards */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">使</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{isNaN(stats.totalUsage) ? 0 : stats.totalUsage}</div>
<div className="text-2xl font-bold">{isNaN(userStats.totalUsage) ? 0 : userStats.totalUsage}</div>
<p className="text-xs text-muted-foreground">
{(isNaN(stats.totalUsage) ? 0 : stats.totalUsage) > 0 ? "累計使用" : "尚未使用任何應用"}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">使</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{isNaN(stats.totalDuration) ? "0分鐘" : (
stats.totalDuration >= 60
? `${(stats.totalDuration / 60).toFixed(1)}小時`
: `${stats.totalDuration}分鐘`
)}
</div>
<p className="text-xs text-muted-foreground">
{(isNaN(stats.totalDuration) ? 0 : stats.totalDuration) > 0 ? "累計時長" : "尚未開始使用"}
{(isNaN(userStats.totalUsage) ? 0 : userStats.totalUsage) > 0 ? "累計使用" : "尚未使用任何應用"}
</p>
</CardContent>
</Card>
@@ -276,9 +349,9 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia
<Heart className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{isNaN(stats.favoriteApps) ? 0 : stats.favoriteApps}</div>
<div className="text-2xl font-bold">{isNaN(userStats.favoriteApps) ? 0 : userStats.favoriteApps}</div>
<p className="text-xs text-muted-foreground">
{(isNaN(stats.favoriteApps) ? 0 : stats.favoriteApps) > 0 ? "個人收藏" : "尚未收藏任何應用"}
{(isNaN(userStats.favoriteApps) ? 0 : userStats.favoriteApps) > 0 ? "個人收藏" : "尚未收藏任何應用"}
</p>
</CardContent>
</Card>
@@ -289,9 +362,9 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{isNaN(stats.daysJoined) ? 0 : stats.daysJoined}</div>
<div className="text-2xl font-bold">{isNaN(userStats.daysJoined) ? 0 : userStats.daysJoined}</div>
<p className="text-xs text-muted-foreground">
{(isNaN(stats.daysJoined) ? 0 : stats.daysJoined) > 0 ? "已加入平台" : "今天剛加入"}
{(isNaN(userStats.daysJoined) ? 0 : userStats.daysJoined) > 0 ? "已加入平台" : "今天剛加入"}
</p>
</CardContent>
</Card>