實作個人收藏、個人活動紀錄
This commit is contained in:
@@ -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>
|
||||
|
Reference in New Issue
Block a user