應用 APP 功能實作
This commit is contained in:
@@ -1,6 +1,39 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
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"
|
||||
@@ -29,6 +62,7 @@ import {
|
||||
ExternalLink,
|
||||
AlertTriangle,
|
||||
X,
|
||||
XCircle,
|
||||
Check,
|
||||
TrendingDown,
|
||||
Link,
|
||||
@@ -52,6 +86,7 @@ import {
|
||||
Shield,
|
||||
Settings,
|
||||
Lightbulb,
|
||||
Users
|
||||
} from "lucide-react"
|
||||
|
||||
// Add available icons array after imports
|
||||
@@ -83,12 +118,34 @@ const availableIcons = [
|
||||
const mockApps: any[] = []
|
||||
|
||||
export function AppManagement() {
|
||||
const [apps, setApps] = useState(mockApps)
|
||||
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,
|
||||
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)
|
||||
@@ -99,26 +156,163 @@ export function AppManagement() {
|
||||
name: "",
|
||||
type: "文字處理",
|
||||
department: "HQBU",
|
||||
creator: "",
|
||||
creator_id: "",
|
||||
description: "",
|
||||
appUrl: "",
|
||||
icon: "Bot",
|
||||
iconColor: "from-blue-500 to-purple-500",
|
||||
})
|
||||
|
||||
const filteredApps = apps.filter((app) => {
|
||||
const matchesSearch =
|
||||
app.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
app.creator.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
const matchesType = selectedType === "all" || app.type === selectedType
|
||||
const matchesStatus = selectedStatus === "all" || app.status === selectedStatus
|
||||
|
||||
return matchesSearch && matchesType && matchesStatus
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
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) => {
|
||||
@@ -127,7 +321,7 @@ export function AppManagement() {
|
||||
name: app.name,
|
||||
type: app.type,
|
||||
department: app.department,
|
||||
creator: app.creator,
|
||||
creator_id: app.creator_id || "",
|
||||
description: app.description,
|
||||
appUrl: app.appUrl,
|
||||
icon: app.icon || "Bot",
|
||||
@@ -141,25 +335,74 @@ export function AppManagement() {
|
||||
setShowDeleteConfirm(true)
|
||||
}
|
||||
|
||||
const confirmDeleteApp = () => {
|
||||
if (selectedApp) {
|
||||
setApps(apps.filter((app) => app.id !== selectedApp.id))
|
||||
setShowDeleteConfirm(false)
|
||||
setSelectedApp(null)
|
||||
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 = (appId: string) => {
|
||||
setApps(
|
||||
apps.map((app) =>
|
||||
app.id === appId
|
||||
? {
|
||||
...app,
|
||||
status: app.status === "published" ? "draft" : "published",
|
||||
}
|
||||
: app,
|
||||
),
|
||||
)
|
||||
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") => {
|
||||
@@ -187,45 +430,116 @@ export function AppManagement() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddApp = () => {
|
||||
const app = {
|
||||
id: Date.now().toString(),
|
||||
...newApp,
|
||||
status: "pending",
|
||||
createdAt: new Date().toISOString().split("T")[0],
|
||||
views: 0,
|
||||
likes: 0,
|
||||
rating: 0,
|
||||
reviews: 0,
|
||||
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",
|
||||
})
|
||||
}
|
||||
setApps([...apps, app])
|
||||
setNewApp({
|
||||
name: "",
|
||||
type: "文字處理",
|
||||
department: "HQBU",
|
||||
creator: "",
|
||||
description: "",
|
||||
appUrl: "",
|
||||
icon: "Bot",
|
||||
iconColor: "from-blue-500 to-purple-500",
|
||||
})
|
||||
setShowAddApp(false)
|
||||
}
|
||||
|
||||
const handleUpdateApp = () => {
|
||||
if (selectedApp) {
|
||||
setApps(
|
||||
apps.map((app) =>
|
||||
app.id === selectedApp.id
|
||||
? {
|
||||
...app,
|
||||
...newApp,
|
||||
}
|
||||
: app,
|
||||
),
|
||||
)
|
||||
setShowEditApp(false)
|
||||
setSelectedApp(null)
|
||||
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",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,10 +630,10 @@ export function AppManagement() {
|
||||
<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">{apps.filter((a) => a.status === "pending").length}</p>
|
||||
<p className="text-sm text-gray-600">已下架</p>
|
||||
<p className="text-2xl font-bold">{apps.filter((a) => a.status === "draft").length}</p>
|
||||
</div>
|
||||
<Clock className="w-8 h-8 text-yellow-600" />
|
||||
<XCircle className="w-8 h-8 text-red-600" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -344,11 +658,19 @@ export function AppManagement() {
|
||||
<SelectValue placeholder="類型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部類型</SelectItem>
|
||||
<SelectItem value="文字處理">文字處理</SelectItem>
|
||||
<SelectItem value="圖像生成">圖像生成</SelectItem>
|
||||
<SelectItem value="語音辨識">語音辨識</SelectItem>
|
||||
<SelectItem value="推薦系統">推薦系統</SelectItem>
|
||||
<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>
|
||||
|
||||
@@ -451,8 +773,8 @@ export function AppManagement() {
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Star className="w-4 h-4 text-yellow-500" />
|
||||
<span className="font-medium">{app.rating}</span>
|
||||
<span className="text-sm text-gray-500">({app.reviews})</span>
|
||||
<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>
|
||||
@@ -507,7 +829,7 @@ export function AppManagement() {
|
||||
)}
|
||||
<DropdownMenuItem className="text-red-600" onClick={() => handleDeleteApp(app)}>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
刪除應用
|
||||
永久刪除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -540,12 +862,25 @@ export function AppManagement() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="creator">創建者 *</Label>
|
||||
<Input
|
||||
id="creator"
|
||||
value={newApp.creator}
|
||||
onChange={(e) => setNewApp({ ...newApp, creator: e.target.value })}
|
||||
placeholder="輸入創建者姓名"
|
||||
/>
|
||||
<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>
|
||||
|
||||
@@ -557,10 +892,19 @@ export function AppManagement() {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="文字處理">文字處理</SelectItem>
|
||||
<SelectItem value="圖像生成">圖像生成</SelectItem>
|
||||
<SelectItem value="語音辨識">語音辨識</SelectItem>
|
||||
<SelectItem value="推薦系統">推薦系統</SelectItem>
|
||||
<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>
|
||||
@@ -644,7 +988,7 @@ export function AppManagement() {
|
||||
<Button variant="outline" onClick={() => setShowAddApp(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleAddApp} disabled={!newApp.name || !newApp.creator || !newApp.description}>
|
||||
<Button onClick={handleAddApp} disabled={!newApp.name || !newApp.creator_id || !newApp.description}>
|
||||
創建應用
|
||||
</Button>
|
||||
</div>
|
||||
@@ -673,12 +1017,25 @@ export function AppManagement() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-creator">創建者 *</Label>
|
||||
<Input
|
||||
id="edit-creator"
|
||||
value={newApp.creator}
|
||||
onChange={(e) => setNewApp({ ...newApp, creator: e.target.value })}
|
||||
placeholder="輸入創建者姓名"
|
||||
/>
|
||||
<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>
|
||||
|
||||
@@ -690,10 +1047,18 @@ export function AppManagement() {
|
||||
<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>
|
||||
<SelectItem value="數據分析">數據分析</SelectItem>
|
||||
<SelectItem value="設計工具">設計工具</SelectItem>
|
||||
<SelectItem value="語音技術">語音技術</SelectItem>
|
||||
<SelectItem value="教育工具">教育工具</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -775,7 +1140,7 @@ export function AppManagement() {
|
||||
<Button variant="outline" onClick={() => setShowEditApp(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleUpdateApp} disabled={!newApp.name || !newApp.creator || !newApp.description}>
|
||||
<Button onClick={handleUpdateApp} disabled={!newApp.name || !newApp.creator_id || !newApp.description}>
|
||||
更新應用
|
||||
</Button>
|
||||
</div>
|
||||
@@ -791,7 +1156,7 @@ export function AppManagement() {
|
||||
<AlertTriangle className="w-5 h-5 text-red-500" />
|
||||
<span>確認刪除</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription>您確定要刪除應用「{selectedApp?.name}」嗎?此操作無法復原。</DialogDescription>
|
||||
<DialogDescription>您確定要永久刪除應用「{selectedApp?.name}」嗎?此操作無法復原,應用將從資料庫中完全移除。</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-end space-x-3">
|
||||
<Button variant="outline" onClick={() => setShowDeleteConfirm(false)}>
|
||||
@@ -878,7 +1243,7 @@ export function AppManagement() {
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<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")}>
|
||||
@@ -939,11 +1304,14 @@ export function AppManagement() {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="stats" className="space-y-4">
|
||||
{/* 基本統計數據 */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-blue-600">{selectedApp.views}</p>
|
||||
<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>
|
||||
@@ -951,7 +1319,9 @@ export function AppManagement() {
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-red-600">{selectedApp.likes}</p>
|
||||
<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>
|
||||
@@ -959,7 +1329,9 @@ export function AppManagement() {
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-yellow-600">{selectedApp.rating}</p>
|
||||
<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>
|
||||
@@ -967,20 +1339,139 @@ export function AppManagement() {
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-green-600">{selectedApp.reviews}</p>
|
||||
<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="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 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>
|
||||
)}
|
||||
|
Reference in New Issue
Block a user