1577 lines
61 KiB
TypeScript
1577 lines
61 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect } from "react"
|
||
|
||
// 輔助函數:安全地格式化數字
|
||
const formatNumber = (value: any, decimals: number = 0): string => {
|
||
if (value == null) {
|
||
return decimals > 0 ? '0.' + '0'.repeat(decimals) : '0'
|
||
}
|
||
|
||
// 處理字符串和數字類型
|
||
const numValue = typeof value === 'string' ? parseFloat(value) : value
|
||
|
||
if (isNaN(numValue)) {
|
||
return decimals > 0 ? '0.' + '0'.repeat(decimals) : '0'
|
||
}
|
||
|
||
return decimals > 0 ? numValue.toFixed(decimals) : numValue.toString()
|
||
}
|
||
|
||
const formatLocaleNumber = (value: any): string => {
|
||
if (value == null) {
|
||
return '0'
|
||
}
|
||
|
||
// 處理字符串和數字類型
|
||
const numValue = typeof value === 'string' ? parseFloat(value) : value
|
||
|
||
if (isNaN(numValue)) {
|
||
return '0'
|
||
}
|
||
|
||
return numValue.toLocaleString()
|
||
}
|
||
import { useAuth } from "@/contexts/auth-context"
|
||
import { useToast } from "@/hooks/use-toast"
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||
import { Label } from "@/components/ui/label"
|
||
import { Textarea } from "@/components/ui/textarea"
|
||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||
import {
|
||
Search,
|
||
Plus,
|
||
MoreHorizontal,
|
||
Edit,
|
||
Trash2,
|
||
Eye,
|
||
Star,
|
||
Heart,
|
||
TrendingUp,
|
||
Bot,
|
||
CheckCircle,
|
||
Clock,
|
||
MessageSquare,
|
||
ExternalLink,
|
||
AlertTriangle,
|
||
X,
|
||
XCircle,
|
||
Check,
|
||
TrendingDown,
|
||
Link,
|
||
Zap,
|
||
Brain,
|
||
Mic,
|
||
ImageIcon,
|
||
FileText,
|
||
BarChart3,
|
||
Camera,
|
||
Music,
|
||
Video,
|
||
Code,
|
||
Database,
|
||
Globe,
|
||
Smartphone,
|
||
Monitor,
|
||
Headphones,
|
||
Palette,
|
||
Calculator,
|
||
Shield,
|
||
Settings,
|
||
Lightbulb,
|
||
Users
|
||
} from "lucide-react"
|
||
|
||
// Add available icons array after imports
|
||
const availableIcons = [
|
||
{ name: "Bot", icon: Bot, color: "from-blue-500 to-purple-500" },
|
||
{ name: "Brain", icon: Brain, color: "from-purple-500 to-pink-500" },
|
||
{ name: "Zap", icon: Zap, color: "from-yellow-500 to-orange-500" },
|
||
{ name: "Mic", icon: Mic, color: "from-green-500 to-teal-500" },
|
||
{ name: "ImageIcon", icon: ImageIcon, color: "from-pink-500 to-rose-500" },
|
||
{ name: "FileText", icon: FileText, color: "from-blue-500 to-cyan-500" },
|
||
{ name: "BarChart3", icon: BarChart3, color: "from-emerald-500 to-green-500" },
|
||
{ name: "Camera", icon: Camera, color: "from-indigo-500 to-purple-500" },
|
||
{ name: "Music", icon: Music, color: "from-violet-500 to-purple-500" },
|
||
{ name: "Video", icon: Video, color: "from-red-500 to-pink-500" },
|
||
{ name: "Code", icon: Code, color: "from-gray-500 to-slate-500" },
|
||
{ name: "Database", icon: Database, color: "from-cyan-500 to-blue-500" },
|
||
{ name: "Globe", icon: Globe, color: "from-blue-500 to-indigo-500" },
|
||
{ name: "Smartphone", icon: Smartphone, color: "from-slate-500 to-gray-500" },
|
||
{ name: "Monitor", icon: Monitor, color: "from-gray-600 to-slate-600" },
|
||
{ name: "Headphones", icon: Headphones, color: "from-purple-500 to-violet-500" },
|
||
{ name: "Palette", icon: Palette, color: "from-pink-500 to-purple-500" },
|
||
{ name: "Calculator", icon: Calculator, color: "from-orange-500 to-red-500" },
|
||
{ name: "Shield", icon: Shield, color: "from-green-500 to-emerald-500" },
|
||
{ name: "Settings", icon: Settings, color: "from-gray-500 to-zinc-500" },
|
||
{ name: "Lightbulb", icon: Lightbulb, color: "from-yellow-500 to-amber-500" },
|
||
]
|
||
|
||
// App data - empty for production
|
||
const mockApps: any[] = []
|
||
|
||
export function AppManagement() {
|
||
const { user } = useAuth()
|
||
const { toast } = useToast()
|
||
const [apps, setApps] = useState<any[]>([])
|
||
const [isLoading, setIsLoading] = useState(true)
|
||
const [searchTerm, setSearchTerm] = useState("")
|
||
const [selectedType, setSelectedType] = useState("all")
|
||
const [selectedStatus, setSelectedStatus] = useState("all")
|
||
const [selectedApp, setSelectedApp] = useState<any>(null)
|
||
const [showAppDetail, setShowAppDetail] = useState(false)
|
||
const [appStats, setAppStats] = useState({
|
||
basic: {
|
||
views: 0,
|
||
likes: 0,
|
||
favorites: 0,
|
||
rating: 0,
|
||
reviewCount: 0
|
||
},
|
||
usage: {
|
||
dailyUsers: 0,
|
||
weeklyUsers: 0,
|
||
monthlyUsers: 0,
|
||
totalSessions: 0,
|
||
topDepartments: [],
|
||
trendData: []
|
||
}
|
||
})
|
||
const [appReviews, setAppReviews] = useState<any[]>([])
|
||
const [isLoadingStats, setIsLoadingStats] = useState(false)
|
||
const [isLoadingReviews, setIsLoadingReviews] = useState(false)
|
||
const [showAddApp, setShowAddApp] = useState(false)
|
||
const [showEditApp, setShowEditApp] = useState(false)
|
||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||
const [showApprovalDialog, setShowApprovalDialog] = useState(false)
|
||
const [approvalAction, setApprovalAction] = useState<"approve" | "reject">("approve")
|
||
const [approvalReason, setApprovalReason] = useState("")
|
||
const [newApp, setNewApp] = useState({
|
||
name: "",
|
||
type: "文字處理",
|
||
department: "HQBU",
|
||
creator_id: "",
|
||
description: "",
|
||
appUrl: "",
|
||
icon: "Bot",
|
||
iconColor: "from-blue-500 to-purple-500",
|
||
})
|
||
const [pagination, setPagination] = useState({
|
||
page: 1,
|
||
limit: 5,
|
||
total: 0,
|
||
totalPages: 0
|
||
})
|
||
const [stats, setStats] = useState({
|
||
totalApps: 0,
|
||
activeApps: 0,
|
||
inactiveApps: 0,
|
||
pendingApps: 0,
|
||
totalViews: 0,
|
||
totalLikes: 0,
|
||
newThisMonth: 0
|
||
})
|
||
const [users, setUsers] = useState<any[]>([])
|
||
const [isLoadingUsers, setIsLoadingUsers] = useState(false)
|
||
|
||
// 載入應用數據
|
||
const loadApps = async () => {
|
||
try {
|
||
setIsLoading(true)
|
||
const params = new URLSearchParams({
|
||
page: pagination.page.toString(),
|
||
limit: pagination.limit.toString(),
|
||
search: searchTerm,
|
||
type: selectedType,
|
||
status: selectedStatus
|
||
})
|
||
|
||
const response = await fetch(`/api/admin/apps?${params}`)
|
||
const data = await response.json()
|
||
|
||
if (data.success) {
|
||
setApps(data.data.apps)
|
||
setPagination(data.data.pagination)
|
||
setStats(data.data.stats)
|
||
}
|
||
} catch (error) {
|
||
console.error('載入應用數據錯誤:', error)
|
||
} finally {
|
||
setIsLoading(false)
|
||
}
|
||
}
|
||
|
||
// 載入用戶列表
|
||
const loadUsers = async () => {
|
||
try {
|
||
setIsLoadingUsers(true)
|
||
const response = await fetch('/api/admin/users/list')
|
||
const data = await response.json()
|
||
|
||
if (data.success) {
|
||
setUsers(data.data.users)
|
||
}
|
||
} catch (error) {
|
||
console.error('載入用戶列表錯誤:', error)
|
||
} finally {
|
||
setIsLoadingUsers(false)
|
||
}
|
||
}
|
||
|
||
// 初始載入
|
||
useEffect(() => {
|
||
loadApps()
|
||
loadUsers()
|
||
}, [pagination.page, searchTerm, selectedType, selectedStatus])
|
||
|
||
const filteredApps = apps
|
||
|
||
const handleViewApp = (app: any) => {
|
||
setSelectedApp(app)
|
||
setShowAppDetail(true)
|
||
loadAppStats(app.id)
|
||
loadAppReviews(app.id)
|
||
}
|
||
|
||
// 載入應用統計數據
|
||
const loadAppStats = async (appId: string) => {
|
||
setIsLoadingStats(true)
|
||
try {
|
||
const response = await fetch(`/api/admin/apps/${appId}/stats`)
|
||
const data = await response.json()
|
||
|
||
if (data.success) {
|
||
setAppStats(data.data)
|
||
}
|
||
} catch (error) {
|
||
console.error('載入應用統計數據錯誤:', error)
|
||
} finally {
|
||
setIsLoadingStats(false)
|
||
}
|
||
}
|
||
|
||
// 載入應用評價
|
||
const loadAppReviews = async (appId: string) => {
|
||
setIsLoadingReviews(true)
|
||
try {
|
||
const response = await fetch(`/api/admin/apps/${appId}/reviews`)
|
||
const data = await response.json()
|
||
|
||
if (data.success) {
|
||
setAppReviews(data.data.reviews)
|
||
}
|
||
} catch (error) {
|
||
console.error('載入應用評價錯誤:', error)
|
||
} finally {
|
||
setIsLoadingReviews(false)
|
||
}
|
||
}
|
||
|
||
// 刪除評價
|
||
const handleDeleteReview = async (reviewId: string) => {
|
||
if (!selectedApp) return
|
||
|
||
try {
|
||
const response = await fetch(`/api/admin/apps/${selectedApp.id}/reviews`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ reviewId })
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (data.success) {
|
||
toast({
|
||
title: "刪除成功",
|
||
description: "評價已成功刪除",
|
||
variant: "success",
|
||
})
|
||
// 重新載入評價列表
|
||
loadAppReviews(selectedApp.id)
|
||
// 重新載入統計數據
|
||
loadAppStats(selectedApp.id)
|
||
} else {
|
||
toast({
|
||
title: "刪除失敗",
|
||
description: data.error || '刪除評價失敗',
|
||
variant: "destructive",
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.error('刪除評價錯誤:', error)
|
||
toast({
|
||
title: "錯誤",
|
||
description: '刪除評價時發生錯誤',
|
||
variant: "destructive",
|
||
})
|
||
}
|
||
}
|
||
|
||
const handleEditApp = (app: any) => {
|
||
setSelectedApp(app)
|
||
setNewApp({
|
||
name: app.name,
|
||
type: app.type,
|
||
department: app.department,
|
||
creator_id: app.creator_id || "",
|
||
description: app.description,
|
||
appUrl: app.appUrl,
|
||
icon: app.icon || "Bot",
|
||
iconColor: app.iconColor || "from-blue-500 to-purple-500",
|
||
})
|
||
setShowEditApp(true)
|
||
}
|
||
|
||
const handleDeleteApp = (app: any) => {
|
||
setSelectedApp(app)
|
||
setShowDeleteConfirm(true)
|
||
}
|
||
|
||
const confirmDeleteApp = async () => {
|
||
try {
|
||
if (!selectedApp) return
|
||
|
||
const response = await fetch(`/api/admin/apps/${selectedApp.id}`, {
|
||
method: 'DELETE'
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (data.success) {
|
||
await loadApps()
|
||
setShowDeleteConfirm(false)
|
||
setSelectedApp(null)
|
||
toast({
|
||
title: "刪除成功",
|
||
description: `應用「${selectedApp.name}」已永久刪除`,
|
||
variant: "success",
|
||
})
|
||
} else {
|
||
toast({
|
||
title: "刪除失敗",
|
||
description: data.error || '刪除應用失敗',
|
||
variant: "destructive",
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.error('刪除應用錯誤:', error)
|
||
toast({
|
||
title: "錯誤",
|
||
description: '刪除應用時發生錯誤',
|
||
variant: "destructive",
|
||
})
|
||
}
|
||
}
|
||
|
||
const handleToggleAppStatus = async (appId: string) => {
|
||
try {
|
||
const response = await fetch(`/api/admin/apps/${appId}/toggle-status`, {
|
||
method: 'POST'
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (data.success) {
|
||
await loadApps()
|
||
// 根據新狀態顯示不同的成功訊息
|
||
const newStatus = data.data?.app?.is_active ? '發布' : '下架'
|
||
toast({
|
||
title: "狀態更新成功",
|
||
description: `應用已成功${newStatus}`,
|
||
variant: "success",
|
||
})
|
||
} else {
|
||
toast({
|
||
title: "狀態更新失敗",
|
||
description: data.error || '切換應用狀態失敗',
|
||
variant: "destructive",
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.error('切換應用狀態錯誤:', error)
|
||
toast({
|
||
title: "錯誤",
|
||
description: '切換應用狀態時發生錯誤',
|
||
variant: "destructive",
|
||
})
|
||
}
|
||
}
|
||
|
||
const handleApprovalAction = (app: any, action: "approve" | "reject") => {
|
||
setSelectedApp(app)
|
||
setApprovalAction(action)
|
||
setApprovalReason("")
|
||
setShowApprovalDialog(true)
|
||
}
|
||
|
||
const confirmApproval = () => {
|
||
if (selectedApp) {
|
||
setApps(
|
||
apps.map((app) =>
|
||
app.id === selectedApp.id
|
||
? {
|
||
...app,
|
||
status: approvalAction === "approve" ? "published" : "rejected",
|
||
}
|
||
: app,
|
||
),
|
||
)
|
||
setShowApprovalDialog(false)
|
||
setSelectedApp(null)
|
||
setApprovalReason("")
|
||
}
|
||
}
|
||
|
||
const handleAddApp = async () => {
|
||
try {
|
||
if (!newApp.creator_id) {
|
||
toast({
|
||
title: "驗證失敗",
|
||
description: '請選擇創建者',
|
||
variant: "destructive",
|
||
})
|
||
return
|
||
}
|
||
|
||
const response = await fetch('/api/admin/apps', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
name: newApp.name,
|
||
description: newApp.description,
|
||
creator_id: newApp.creator_id,
|
||
category: newApp.type, // 使用 type 作為 category
|
||
type: newApp.type,
|
||
app_url: newApp.appUrl,
|
||
icon: newApp.icon,
|
||
icon_color: newApp.iconColor
|
||
})
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (data.success) {
|
||
// 重新載入應用列表
|
||
await loadApps()
|
||
setNewApp({
|
||
name: "",
|
||
type: "文字處理",
|
||
department: "HQBU",
|
||
creator_id: "",
|
||
description: "",
|
||
appUrl: "",
|
||
icon: "Bot",
|
||
iconColor: "from-blue-500 to-purple-500",
|
||
})
|
||
setShowAddApp(false)
|
||
} else {
|
||
toast({
|
||
title: "創建失敗",
|
||
description: data.error || '創建應用失敗',
|
||
variant: "destructive",
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.error('創建應用錯誤:', error)
|
||
toast({
|
||
title: "錯誤",
|
||
description: '創建應用時發生錯誤',
|
||
variant: "destructive",
|
||
})
|
||
}
|
||
}
|
||
|
||
const handleUpdateApp = async () => {
|
||
try {
|
||
if (!selectedApp) return
|
||
|
||
if (!newApp.creator_id) {
|
||
toast({
|
||
title: "驗證失敗",
|
||
description: '請選擇創建者',
|
||
variant: "destructive",
|
||
})
|
||
return
|
||
}
|
||
|
||
const response = await fetch(`/api/admin/apps/${selectedApp.id}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
name: newApp.name,
|
||
description: newApp.description,
|
||
category: newApp.type,
|
||
type: newApp.type,
|
||
app_url: newApp.appUrl,
|
||
icon: newApp.icon,
|
||
icon_color: newApp.iconColor
|
||
})
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (data.success) {
|
||
await loadApps()
|
||
setShowEditApp(false)
|
||
setSelectedApp(null)
|
||
} else {
|
||
toast({
|
||
title: "更新失敗",
|
||
description: data.error || '更新應用失敗',
|
||
variant: "destructive",
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.error('更新應用錯誤:', error)
|
||
toast({
|
||
title: "錯誤",
|
||
description: '更新應用時發生錯誤',
|
||
variant: "destructive",
|
||
})
|
||
}
|
||
}
|
||
|
||
const getStatusColor = (status: string) => {
|
||
switch (status) {
|
||
case "published":
|
||
return "bg-green-100 text-green-800 border-green-200"
|
||
case "pending":
|
||
return "bg-yellow-100 text-yellow-800 border-yellow-200"
|
||
case "draft":
|
||
return "bg-gray-100 text-gray-800 border-gray-200"
|
||
case "rejected":
|
||
return "bg-red-100 text-red-800 border-red-200"
|
||
default:
|
||
return "bg-gray-100 text-gray-800 border-gray-200"
|
||
}
|
||
}
|
||
|
||
const getTypeColor = (type: string) => {
|
||
const colors = {
|
||
文字處理: "bg-blue-100 text-blue-800 border-blue-200",
|
||
圖像生成: "bg-purple-100 text-purple-800 border-purple-200",
|
||
語音辨識: "bg-green-100 text-green-800 border-green-200",
|
||
推薦系統: "bg-orange-100 text-orange-800 border-orange-200",
|
||
}
|
||
return colors[type as keyof typeof colors] || "bg-gray-100 text-gray-800 border-gray-200"
|
||
}
|
||
|
||
const getStatusText = (status: string) => {
|
||
switch (status) {
|
||
case "published":
|
||
return "已發布"
|
||
case "pending":
|
||
return "待審核"
|
||
case "draft":
|
||
return "草稿"
|
||
case "rejected":
|
||
return "已拒絕"
|
||
default:
|
||
return status
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-3xl font-bold text-gray-900">應用管理</h1>
|
||
<p className="text-gray-600">管理平台上的所有 AI 應用</p>
|
||
</div>
|
||
<Button
|
||
onClick={() => setShowAddApp(true)}
|
||
className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
|
||
>
|
||
<Plus className="w-4 h-4 mr-2" />
|
||
新增應用
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Stats Cards */}
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-600">總應用數</p>
|
||
<p className="text-2xl font-bold">{stats.totalApps}</p>
|
||
</div>
|
||
<Bot className="w-8 h-8 text-blue-600" />
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-600">已發布</p>
|
||
<p className="text-2xl font-bold">{stats.activeApps}</p>
|
||
</div>
|
||
<CheckCircle className="w-8 h-8 text-green-600" />
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-600">已下架</p>
|
||
<p className="text-2xl font-bold">{stats.inactiveApps}</p>
|
||
</div>
|
||
<XCircle className="w-8 h-8 text-red-600" />
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* Filters */}
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<div className="flex flex-col lg:flex-row gap-4 items-center">
|
||
<div className="flex-1 relative">
|
||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||
<Input
|
||
placeholder="搜尋應用名稱或創建者..."
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
className="pl-10"
|
||
/>
|
||
</div>
|
||
<div className="flex gap-3">
|
||
<Select value={selectedType} onValueChange={setSelectedType}>
|
||
<SelectTrigger className="w-32">
|
||
<SelectValue placeholder="類型" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="all">全部類型</SelectItem>
|
||
<SelectItem value="文字處理">文字處理</SelectItem>
|
||
<SelectItem value="圖像生成">圖像生成</SelectItem>
|
||
<SelectItem value="語音辨識">語音辨識</SelectItem>
|
||
<SelectItem value="推薦系統">推薦系統</SelectItem>
|
||
<SelectItem value="音樂生成">音樂生成</SelectItem>
|
||
<SelectItem value="程式開發">程式開發</SelectItem>
|
||
<SelectItem value="影像處理">影像處理</SelectItem>
|
||
<SelectItem value="對話系統">對話系統</SelectItem>
|
||
<SelectItem value="數據分析">數據分析</SelectItem>
|
||
<SelectItem value="設計工具">設計工具</SelectItem>
|
||
<SelectItem value="語音技術">語音技術</SelectItem>
|
||
<SelectItem value="教育工具">教育工具</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
|
||
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
|
||
<SelectTrigger className="w-32">
|
||
<SelectValue placeholder="狀態" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="all">全部狀態</SelectItem>
|
||
<SelectItem value="published">已發布</SelectItem>
|
||
<SelectItem value="pending">待審核</SelectItem>
|
||
<SelectItem value="draft">草稿</SelectItem>
|
||
<SelectItem value="rejected">已拒絕</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Apps Table */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>應用列表 ({pagination.total})</CardTitle>
|
||
<CardDescription>管理所有 AI 應用</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>應用名稱</TableHead>
|
||
<TableHead>類型</TableHead>
|
||
<TableHead>創建者</TableHead>
|
||
<TableHead>狀態</TableHead>
|
||
<TableHead>統計</TableHead>
|
||
<TableHead>評分</TableHead>
|
||
<TableHead>創建日期</TableHead>
|
||
<TableHead>操作</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{filteredApps.map((app) => (
|
||
<TableRow key={app.id}>
|
||
<TableCell>
|
||
<div className="flex items-center space-x-3">
|
||
<div
|
||
className={`w-8 h-8 bg-gradient-to-r ${app.iconColor} rounded-lg flex items-center justify-center`}
|
||
>
|
||
{(() => {
|
||
const IconComponent = availableIcons.find((icon) => icon.name === app.icon)?.icon || Bot
|
||
return <IconComponent className="w-4 h-4 text-white" />
|
||
})()}
|
||
</div>
|
||
<div>
|
||
<div className="flex items-center space-x-2">
|
||
<p className="font-medium">{app.name}</p>
|
||
{app.appUrl && (
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="h-6 w-6 p-0"
|
||
onClick={() => window.open(app.appUrl, "_blank")}
|
||
title="開啟應用"
|
||
>
|
||
<ExternalLink className="w-3 h-3" />
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</TableCell>
|
||
<TableCell>
|
||
<Badge variant="outline" className={getTypeColor(app.type)}>
|
||
{app.type}
|
||
</Badge>
|
||
</TableCell>
|
||
<TableCell>
|
||
<div>
|
||
<p className="font-medium">{app.creator}</p>
|
||
<p className="text-sm text-gray-500">{app.department}</p>
|
||
</div>
|
||
</TableCell>
|
||
<TableCell>
|
||
<Badge variant="outline" className={getStatusColor(app.status)}>
|
||
{getStatusText(app.status)}
|
||
</Badge>
|
||
</TableCell>
|
||
<TableCell>
|
||
<div className="text-sm">
|
||
<div className="flex items-center space-x-1">
|
||
<Eye className="w-3 h-3" />
|
||
<span>{app.views}</span>
|
||
</div>
|
||
<div className="flex items-center space-x-1">
|
||
<Heart className="w-3 h-3" />
|
||
<span>{app.likes}</span>
|
||
</div>
|
||
</div>
|
||
</TableCell>
|
||
<TableCell>
|
||
<div className="flex items-center space-x-1">
|
||
<Star className="w-4 h-4 text-yellow-500" />
|
||
<span className="font-medium">{formatNumber(app.rating, 1)}</span>
|
||
<span className="text-sm text-gray-500">({app.reviewCount || 0})</span>
|
||
</div>
|
||
</TableCell>
|
||
<TableCell className="text-sm text-gray-600">{app.createdAt}</TableCell>
|
||
<TableCell>
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" size="sm">
|
||
<MoreHorizontal className="w-4 h-4" />
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="end">
|
||
<DropdownMenuItem onClick={() => handleViewApp(app)}>
|
||
<Eye className="w-4 h-4 mr-2" />
|
||
查看詳情
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem onClick={() => handleEditApp(app)}>
|
||
<Edit className="w-4 h-4 mr-2" />
|
||
編輯應用
|
||
</DropdownMenuItem>
|
||
{app.appUrl && (
|
||
<DropdownMenuItem onClick={() => window.open(app.appUrl, "_blank")}>
|
||
<ExternalLink className="w-4 h-4 mr-2" />
|
||
開啟應用
|
||
</DropdownMenuItem>
|
||
)}
|
||
{app.status === "pending" && (
|
||
<>
|
||
<DropdownMenuItem onClick={() => handleApprovalAction(app, "approve")}>
|
||
<Check className="w-4 h-4 mr-2" />
|
||
批准發布
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem onClick={() => handleApprovalAction(app, "reject")}>
|
||
<X className="w-4 h-4 mr-2" />
|
||
拒絕申請
|
||
</DropdownMenuItem>
|
||
</>
|
||
)}
|
||
{app.status !== "pending" && (
|
||
<DropdownMenuItem onClick={() => handleToggleAppStatus(app.id)}>
|
||
{app.status === "published" ? (
|
||
<>
|
||
<TrendingDown className="w-4 h-4 mr-2" />
|
||
下架應用
|
||
</>
|
||
) : (
|
||
<>
|
||
<TrendingUp className="w-4 h-4 mr-2" />
|
||
發布應用
|
||
</>
|
||
)}
|
||
</DropdownMenuItem>
|
||
)}
|
||
<DropdownMenuItem className="text-red-600" onClick={() => handleDeleteApp(app)}>
|
||
<Trash2 className="w-4 h-4 mr-2" />
|
||
永久刪除
|
||
</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 分頁控制元件 */}
|
||
{pagination.totalPages > 1 && (
|
||
<div className="flex flex-col items-center space-y-4 mt-6">
|
||
<div className="text-sm text-gray-600">
|
||
第 {pagination.page} 頁,共 {pagination.totalPages} 頁 (共 {pagination.total} 個應用)
|
||
</div>
|
||
<div className="flex items-center space-x-2">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => setPagination(prev => ({ ...prev, page: Math.max(1, prev.page - 1) }))}
|
||
disabled={pagination.page === 1}
|
||
className="flex items-center space-x-1"
|
||
>
|
||
<span>‹</span>
|
||
<span>上一頁</span>
|
||
</Button>
|
||
|
||
<div className="flex items-center space-x-1">
|
||
{Array.from({ length: Math.min(pagination.totalPages, 5) }, (_, i) => {
|
||
let page;
|
||
if (pagination.totalPages <= 5) {
|
||
page = i + 1;
|
||
} else if (pagination.page <= 3) {
|
||
page = i + 1;
|
||
} else if (pagination.page >= pagination.totalPages - 2) {
|
||
page = pagination.totalPages - 4 + i;
|
||
} else {
|
||
page = pagination.page - 2 + i;
|
||
}
|
||
|
||
return (
|
||
<Button
|
||
key={page}
|
||
variant={pagination.page === page ? "default" : "outline"}
|
||
size="sm"
|
||
onClick={() => setPagination(prev => ({ ...prev, page }))}
|
||
className="w-10 h-10 p-0"
|
||
>
|
||
{page}
|
||
</Button>
|
||
)
|
||
})}
|
||
</div>
|
||
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => setPagination(prev => ({ ...prev, page: Math.min(prev.totalPages, prev.page + 1) }))}
|
||
disabled={pagination.page === pagination.totalPages}
|
||
className="flex items-center space-x-1"
|
||
>
|
||
<span>下一頁</span>
|
||
<span>›</span>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Add App Dialog */}
|
||
<Dialog open={showAddApp} onOpenChange={setShowAddApp}>
|
||
<DialogContent className="max-w-4xl max-h-[85vh] overflow-y-auto">
|
||
<DialogHeader>
|
||
<DialogTitle>新增 AI 應用</DialogTitle>
|
||
<DialogDescription>創建一個新的 AI 應用</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<div className="space-y-4">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="name">應用名稱 *</Label>
|
||
<Input
|
||
id="name"
|
||
value={newApp.name}
|
||
onChange={(e) => setNewApp({ ...newApp, name: e.target.value })}
|
||
placeholder="輸入應用名稱"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="creator">創建者 *</Label>
|
||
<Select
|
||
value={newApp.creator_id}
|
||
onValueChange={(value) => setNewApp({ ...newApp, creator_id: value })}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="選擇創建者" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{isLoadingUsers ? (
|
||
<SelectItem value="" disabled>載入中...</SelectItem>
|
||
) : (
|
||
users.map((user) => (
|
||
<SelectItem key={user.id} value={user.id}>
|
||
{user.name} ({user.email})
|
||
</SelectItem>
|
||
))
|
||
)}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="type">應用類型</Label>
|
||
<Select value={newApp.type} onValueChange={(value) => setNewApp({ ...newApp, type: value })}>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="all">全部類型</SelectItem>
|
||
<SelectItem value="文字處理">文字處理</SelectItem>
|
||
<SelectItem value="圖像生成">圖像生成</SelectItem>
|
||
<SelectItem value="語音辨識">語音辨識</SelectItem>
|
||
<SelectItem value="推薦系統">推薦系統</SelectItem>
|
||
<SelectItem value="音樂生成">音樂生成</SelectItem>
|
||
<SelectItem value="程式開發">程式開發</SelectItem>
|
||
<SelectItem value="影像處理">影像處理</SelectItem>
|
||
<SelectItem value="對話系統">對話系統</SelectItem>
|
||
<SelectItem value="數據分析">數據分析</SelectItem>
|
||
<SelectItem value="設計工具">設計工具</SelectItem>
|
||
<SelectItem value="語音技術">語音技術</SelectItem>
|
||
<SelectItem value="教育工具">教育工具</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="department">所屬部門</Label>
|
||
<Select
|
||
value={newApp.department}
|
||
onValueChange={(value) => setNewApp({ ...newApp, department: value })}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="ACBU">ACBU</SelectItem>
|
||
<SelectItem value="AUBU">AUBU</SelectItem>
|
||
<SelectItem value="FAB3">FAB3</SelectItem>
|
||
<SelectItem value="FNBU">FNBU</SelectItem>
|
||
<SelectItem value="HQBU">HQBU</SelectItem>
|
||
<SelectItem value="HRBU">HRBU</SelectItem>
|
||
<SelectItem value="IBU">IBU</SelectItem>
|
||
<SelectItem value="ITBU">ITBU</SelectItem>
|
||
<SelectItem value="MBU1">MBU1</SelectItem>
|
||
<SelectItem value="PBU">PBU</SelectItem>
|
||
<SelectItem value="SBG">SBG</SelectItem>
|
||
<SelectItem value="SBU">SBU</SelectItem>
|
||
<SelectItem value="法務室">法務室</SelectItem>
|
||
<SelectItem value="關係企業發展">關係企業發展</SelectItem>
|
||
<SelectItem value="稽核室">稽核室</SelectItem>
|
||
<SelectItem value="總經理室">總經理室</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="icon">應用圖示</Label>
|
||
<div className="grid grid-cols-7 gap-2 p-4 border rounded-lg max-h-60 overflow-y-auto bg-gray-50">
|
||
{availableIcons.map((iconOption) => {
|
||
const IconComponent = iconOption.icon
|
||
return (
|
||
<button
|
||
key={iconOption.name}
|
||
type="button"
|
||
onClick={() => {
|
||
setNewApp({
|
||
...newApp,
|
||
icon: iconOption.name,
|
||
iconColor: iconOption.color,
|
||
})
|
||
}}
|
||
className={`w-12 h-12 rounded-lg flex items-center justify-center transition-all hover:scale-105 ${
|
||
newApp.icon === iconOption.name
|
||
? `bg-gradient-to-r ${iconOption.color} shadow-lg ring-2 ring-blue-500`
|
||
: `bg-gradient-to-r ${iconOption.color} opacity-70 hover:opacity-100`
|
||
}`}
|
||
title={iconOption.name}
|
||
>
|
||
<IconComponent className="w-6 h-6 text-white" />
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
<p className="text-xs text-gray-500">選擇一個代表您應用的圖示</p>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="appUrl">應用連結</Label>
|
||
<div className="relative">
|
||
<Link className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||
<Input
|
||
id="appUrl"
|
||
value={newApp.appUrl}
|
||
onChange={(e) => setNewApp({ ...newApp, appUrl: e.target.value })}
|
||
placeholder="https://your-app.example.com"
|
||
className="pl-10"
|
||
/>
|
||
</div>
|
||
<p className="text-xs text-gray-500">用戶點擊應用時將跳轉到此連結</p>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="description">應用描述 *</Label>
|
||
<Textarea
|
||
id="description"
|
||
value={newApp.description}
|
||
onChange={(e) => setNewApp({ ...newApp, description: e.target.value })}
|
||
placeholder="描述應用的功能和特色"
|
||
rows={3}
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex justify-end space-x-3">
|
||
<Button variant="outline" onClick={() => setShowAddApp(false)}>
|
||
取消
|
||
</Button>
|
||
<Button onClick={handleAddApp} disabled={!newApp.name || !newApp.creator_id || !newApp.description}>
|
||
創建應用
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* Edit App Dialog */}
|
||
<Dialog open={showEditApp} onOpenChange={setShowEditApp}>
|
||
<DialogContent className="max-w-4xl max-h-[85vh] overflow-y-auto">
|
||
<DialogHeader>
|
||
<DialogTitle>編輯應用</DialogTitle>
|
||
<DialogDescription>修改應用資訊</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<div className="space-y-4">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="edit-name">應用名稱 *</Label>
|
||
<Input
|
||
id="edit-name"
|
||
value={newApp.name}
|
||
onChange={(e) => setNewApp({ ...newApp, name: e.target.value })}
|
||
placeholder="輸入應用名稱"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="edit-creator">創建者 *</Label>
|
||
<Select
|
||
value={newApp.creator_id}
|
||
onValueChange={(value) => setNewApp({ ...newApp, creator_id: value })}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="選擇創建者" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{isLoadingUsers ? (
|
||
<SelectItem value="" disabled>載入中...</SelectItem>
|
||
) : (
|
||
users.map((user) => (
|
||
<SelectItem key={user.id} value={user.id}>
|
||
{user.name} ({user.email})
|
||
</SelectItem>
|
||
))
|
||
)}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="edit-type">應用類型</Label>
|
||
<Select value={newApp.type} onValueChange={(value) => setNewApp({ ...newApp, type: value })}>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="文字處理">文字處理</SelectItem>
|
||
<SelectItem value="圖像生成">圖像生成</SelectItem>
|
||
<SelectItem value="語音辨識">語音辨識</SelectItem>
|
||
<SelectItem value="推薦系統">推薦系統</SelectItem>
|
||
<SelectItem value="音樂生成">音樂生成</SelectItem>
|
||
<SelectItem value="程式開發">程式開發</SelectItem>
|
||
<SelectItem value="影像處理">影像處理</SelectItem>
|
||
<SelectItem value="對話系統">對話系統</SelectItem>
|
||
<SelectItem value="數據分析">數據分析</SelectItem>
|
||
<SelectItem value="設計工具">設計工具</SelectItem>
|
||
<SelectItem value="語音技術">語音技術</SelectItem>
|
||
<SelectItem value="教育工具">教育工具</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="edit-department">所屬部門</Label>
|
||
<Select
|
||
value={newApp.department}
|
||
onValueChange={(value) => setNewApp({ ...newApp, department: value })}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="ACBU">ACBU</SelectItem>
|
||
<SelectItem value="AUBU">AUBU</SelectItem>
|
||
<SelectItem value="FAB3">FAB3</SelectItem>
|
||
<SelectItem value="FNBU">FNBU</SelectItem>
|
||
<SelectItem value="HQBU">HQBU</SelectItem>
|
||
<SelectItem value="HRBU">HRBU</SelectItem>
|
||
<SelectItem value="IBU">IBU</SelectItem>
|
||
<SelectItem value="ITBU">ITBU</SelectItem>
|
||
<SelectItem value="MBU1">MBU1</SelectItem>
|
||
<SelectItem value="PBU">PBU</SelectItem>
|
||
<SelectItem value="SBG">SBG</SelectItem>
|
||
<SelectItem value="SBU">SBU</SelectItem>
|
||
<SelectItem value="法務室">法務室</SelectItem>
|
||
<SelectItem value="關係企業發展">關係企業發展</SelectItem>
|
||
<SelectItem value="稽核室">稽核室</SelectItem>
|
||
<SelectItem value="總經理室">總經理室</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="icon">應用圖示</Label>
|
||
<div className="grid grid-cols-7 gap-2 p-4 border rounded-lg max-h-60 overflow-y-auto bg-gray-50">
|
||
{availableIcons.map((iconOption) => {
|
||
const IconComponent = iconOption.icon
|
||
return (
|
||
<button
|
||
key={iconOption.name}
|
||
type="button"
|
||
onClick={() => {
|
||
setNewApp({
|
||
...newApp,
|
||
icon: iconOption.name,
|
||
iconColor: iconOption.color,
|
||
})
|
||
}}
|
||
className={`w-12 h-12 rounded-lg flex items-center justify-center transition-all hover:scale-105 ${
|
||
newApp.icon === iconOption.name
|
||
? `bg-gradient-to-r ${iconOption.color} shadow-lg ring-2 ring-blue-500`
|
||
: `bg-gradient-to-r ${iconOption.color} opacity-70 hover:opacity-100`
|
||
}`}
|
||
title={iconOption.name}
|
||
>
|
||
<IconComponent className="w-6 h-6 text-white" />
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="edit-appUrl">應用連結</Label>
|
||
<div className="relative">
|
||
<Link className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||
<Input
|
||
id="edit-appUrl"
|
||
value={newApp.appUrl}
|
||
onChange={(e) => setNewApp({ ...newApp, appUrl: e.target.value })}
|
||
placeholder="https://your-app.example.com"
|
||
className="pl-10"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="edit-description">應用描述 *</Label>
|
||
<Textarea
|
||
id="edit-description"
|
||
value={newApp.description}
|
||
onChange={(e) => setNewApp({ ...newApp, description: e.target.value })}
|
||
placeholder="描述應用的功能和特色"
|
||
rows={3}
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex justify-end space-x-3">
|
||
<Button variant="outline" onClick={() => setShowEditApp(false)}>
|
||
取消
|
||
</Button>
|
||
<Button onClick={handleUpdateApp} disabled={!newApp.name || !newApp.creator_id || !newApp.description}>
|
||
更新應用
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* Delete Confirmation Dialog */}
|
||
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
||
<DialogContent>
|
||
<DialogHeader>
|
||
<DialogTitle className="flex items-center space-x-2">
|
||
<AlertTriangle className="w-5 h-5 text-red-500" />
|
||
<span>確認刪除</span>
|
||
</DialogTitle>
|
||
<DialogDescription>您確定要永久刪除應用「{selectedApp?.name}」嗎?此操作無法復原,應用將從資料庫中完全移除。</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="flex justify-end space-x-3">
|
||
<Button variant="outline" onClick={() => setShowDeleteConfirm(false)}>
|
||
取消
|
||
</Button>
|
||
<Button variant="destructive" onClick={confirmDeleteApp}>
|
||
確認刪除
|
||
</Button>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* Approval Dialog */}
|
||
<Dialog open={showApprovalDialog} onOpenChange={setShowApprovalDialog}>
|
||
<DialogContent>
|
||
<DialogHeader>
|
||
<DialogTitle className="flex items-center space-x-2">
|
||
{approvalAction === "approve" ? (
|
||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||
) : (
|
||
<X className="w-5 h-5 text-red-500" />
|
||
)}
|
||
<span>{approvalAction === "approve" ? "批准應用" : "拒絕應用"}</span>
|
||
</DialogTitle>
|
||
<DialogDescription>
|
||
{approvalAction === "approve"
|
||
? `確認批准應用「${selectedApp?.name}」並發布到平台?`
|
||
: `確認拒絕應用「${selectedApp?.name}」的發布申請?`}
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="space-y-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="approval-reason">
|
||
{approvalAction === "approve" ? "批准備註(可選)" : "拒絕原因 *"}
|
||
</Label>
|
||
<Textarea
|
||
id="approval-reason"
|
||
value={approvalReason}
|
||
onChange={(e) => setApprovalReason(e.target.value)}
|
||
placeholder={approvalAction === "approve" ? "輸入批准備註..." : "請說明拒絕原因,以便開發者了解並改進"}
|
||
rows={3}
|
||
/>
|
||
</div>
|
||
<div className="flex justify-end space-x-3">
|
||
<Button variant="outline" onClick={() => setShowApprovalDialog(false)}>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
onClick={confirmApproval}
|
||
variant={approvalAction === "approve" ? "default" : "destructive"}
|
||
disabled={approvalAction === "reject" && !approvalReason.trim()}
|
||
>
|
||
{approvalAction === "approve" ? "確認批准" : "確認拒絕"}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* App Detail Dialog */}
|
||
<Dialog open={showAppDetail} onOpenChange={setShowAppDetail}>
|
||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||
<DialogHeader>
|
||
<DialogTitle>應用詳情</DialogTitle>
|
||
<DialogDescription>查看應用的詳細資訊和統計數據</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
{selectedApp && (
|
||
<Tabs defaultValue="info" className="w-full">
|
||
<TabsList className="grid w-full grid-cols-3">
|
||
<TabsTrigger value="info">基本資訊</TabsTrigger>
|
||
<TabsTrigger value="stats">統計數據</TabsTrigger>
|
||
<TabsTrigger value="reviews">評價管理</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent value="info" className="space-y-4">
|
||
<div className="flex items-start space-x-4">
|
||
<div
|
||
className={`w-16 h-16 bg-gradient-to-r ${selectedApp.iconColor} rounded-xl flex items-center justify-center`}
|
||
>
|
||
{(() => {
|
||
const IconComponent = availableIcons.find((icon) => icon.name === selectedApp.icon)?.icon || Bot
|
||
return <IconComponent className="w-8 h-8 text-white" />
|
||
})()}
|
||
</div>
|
||
<div className="flex-1">
|
||
<div className="flex items-center space-x-2 mb-2 justify-between">
|
||
<h3 className="text-xl font-semibold">{selectedApp.name}</h3>
|
||
{selectedApp.appUrl && (
|
||
<Button variant="outline" size="sm" onClick={() => window.open(selectedApp.appUrl, "_blank")}>
|
||
<ExternalLink className="w-4 h-4 mr-2" />
|
||
開啟應用
|
||
</Button>
|
||
)}
|
||
</div>
|
||
<p className="text-gray-600 mb-2">{selectedApp.description}</p>
|
||
<div className="flex flex-wrap gap-2">
|
||
<Badge variant="outline" className={getTypeColor(selectedApp.type)}>
|
||
{selectedApp.type}
|
||
</Badge>
|
||
<Badge variant="outline" className="bg-gray-100 text-gray-700">
|
||
{selectedApp.department}
|
||
</Badge>
|
||
<Badge variant="outline" className={getStatusColor(selectedApp.status)}>
|
||
{getStatusText(selectedApp.status)}
|
||
</Badge>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<p className="text-sm text-gray-500">創建者</p>
|
||
<p className="font-medium">{selectedApp.creator}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-gray-500">創建日期</p>
|
||
<p className="font-medium">{selectedApp.createdAt}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-gray-500">應用ID</p>
|
||
<p className="font-medium">{selectedApp.id}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-gray-500">所屬部門</p>
|
||
<p className="font-medium">{selectedApp.department}</p>
|
||
</div>
|
||
</div>
|
||
|
||
{selectedApp.appUrl && (
|
||
<div>
|
||
<p className="text-sm text-gray-500">應用連結</p>
|
||
<div className="flex items-center space-x-2">
|
||
<p className="font-medium text-blue-600">{selectedApp.appUrl}</p>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => navigator.clipboard.writeText(selectedApp.appUrl)}
|
||
>
|
||
複製
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</TabsContent>
|
||
|
||
<TabsContent value="stats" className="space-y-4">
|
||
{/* 基本統計數據 */}
|
||
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4">
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<div className="text-center">
|
||
<p className="text-2xl font-bold text-blue-600">
|
||
{isLoadingStats ? '...' : (appStats?.basic?.views || 0)}
|
||
</p>
|
||
<p className="text-sm text-gray-600">總瀏覽量</p>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<div className="text-center">
|
||
<p className="text-2xl font-bold text-red-600">
|
||
{isLoadingStats ? '...' : (appStats?.basic?.likes || 0)}
|
||
</p>
|
||
<p className="text-sm text-gray-600">按讚數</p>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<div className="text-center">
|
||
<p className="text-2xl font-bold text-purple-600">
|
||
{isLoadingStats ? '...' : (appStats?.basic?.favorites || 0)}
|
||
</p>
|
||
<p className="text-sm text-gray-600">收藏數</p>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<div className="text-center">
|
||
<p className="text-2xl font-bold text-yellow-600">
|
||
{isLoadingStats ? '...' : formatNumber(appStats?.basic?.rating, 1)}
|
||
</p>
|
||
<p className="text-sm text-gray-600">平均評分</p>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<div className="text-center">
|
||
<p className="text-2xl font-bold text-green-600">
|
||
{isLoadingStats ? '...' : (appStats?.basic?.reviewCount || 0)}
|
||
</p>
|
||
<p className="text-sm text-gray-600">評價數量</p>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* 使用趨勢 */}
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<Card>
|
||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
<CardTitle className="text-sm font-medium">今日使用者</CardTitle>
|
||
<Users className="h-4 w-4 text-blue-500" />
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="text-2xl font-bold">
|
||
{isLoadingStats ? '...' : (appStats?.usage?.dailyUsers || 0)}
|
||
</div>
|
||
<p className="text-xs text-muted-foreground">活躍用戶數</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>
|
||
<TrendingUp className="h-4 w-4 text-green-500" />
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="text-2xl font-bold">
|
||
{isLoadingStats ? '...' : (appStats?.usage?.weeklyUsers || 0)}
|
||
</div>
|
||
<p className="text-xs text-muted-foreground">週活躍用戶</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>
|
||
<BarChart3 className="h-4 w-4 text-purple-500" />
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="text-2xl font-bold">
|
||
{isLoadingStats ? '...' : formatLocaleNumber(appStats?.usage?.totalSessions)}
|
||
</div>
|
||
<p className="text-xs text-muted-foreground">累計使用次數</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* 部門使用統計 */}
|
||
{appStats?.usage?.topDepartments && appStats.usage.topDepartments.length > 0 && (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>部門使用統計</CardTitle>
|
||
<CardDescription>使用該應用最多的部門</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="space-y-2">
|
||
{appStats.usage.topDepartments.map((dept: any, index: number) => (
|
||
<div key={index} className="flex items-center justify-between">
|
||
<span className="text-sm font-medium">{dept.department}</span>
|
||
<span className="text-sm text-gray-500">{dept.count} 次</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</TabsContent>
|
||
|
||
<TabsContent value="reviews" className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<h3 className="text-lg font-semibold">評價管理</h3>
|
||
<div className="text-sm text-gray-500">
|
||
{isLoadingReviews ? '載入中...' : `共 ${appReviews.length} 條評價`}
|
||
</div>
|
||
</div>
|
||
|
||
{isLoadingReviews ? (
|
||
<div className="text-center py-8">
|
||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||
<p className="text-gray-500 mt-2">載入評價中...</p>
|
||
</div>
|
||
) : appReviews.length === 0 ? (
|
||
<div className="text-center py-8">
|
||
<MessageSquare className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||
<h3 className="text-lg font-semibold text-gray-600 mb-2">暫無評價</h3>
|
||
<p className="text-gray-500">該應用目前還沒有用戶評價</p>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-4">
|
||
{appReviews.map((review) => (
|
||
<Card key={review.id}>
|
||
<CardContent className="p-4">
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex-1">
|
||
<div className="flex items-center space-x-2 mb-2">
|
||
<div className="flex items-center space-x-1">
|
||
{[...Array(5)].map((_, i) => (
|
||
<Star
|
||
key={i}
|
||
className={`w-4 h-4 ${
|
||
i < review.rating
|
||
? 'text-yellow-400 fill-current'
|
||
: 'text-gray-300'
|
||
}`}
|
||
/>
|
||
))}
|
||
</div>
|
||
<span className="text-sm font-medium">{review.userName}</span>
|
||
<span className="text-xs text-gray-500">({review.userDepartment})</span>
|
||
</div>
|
||
<p className="text-gray-700 mb-2">{review.review}</p>
|
||
<p className="text-xs text-gray-500">
|
||
{review.ratedAt ? new Date(review.ratedAt).toLocaleString('zh-TW') : '未知時間'}
|
||
</p>
|
||
</div>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleDeleteReview(review.id)}
|
||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
)}
|
||
</TabsContent>
|
||
</Tabs>
|
||
)}
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
)
|
||
}
|