新增後台查看詳細

This commit is contained in:
2025-10-07 15:33:14 +08:00
parent 7069784fd1
commit 98e4f16a15
2 changed files with 534 additions and 2 deletions

View File

@@ -6,6 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import {
Download,
Eye,
@@ -31,7 +32,17 @@ import {
ChevronUp,
Shield,
EyeOff,
HelpCircle
HelpCircle,
X,
Calendar,
User,
Mail,
Tag,
Star,
Image as ImageIcon,
Clock,
MapPin,
Monitor
} from "lucide-react"
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
@@ -106,6 +117,12 @@ export default function AdminPage() {
const [isExportingExcel, setIsExportingExcel] = useState(false)
const [showCategoryGuide, setShowCategoryGuide] = useState(false)
const [showPrivacyDetails, setShowPrivacyDetails] = useState(false)
const [selectedWish, setSelectedWish] = useState<WishData | null>(null)
const [showWishDetails, setShowWishDetails] = useState(false)
const [wishDetails, setWishDetails] = useState<any>(null)
const [loadingDetails, setLoadingDetails] = useState(false)
const [showImageModal, setShowImageModal] = useState(false)
const [selectedImage, setSelectedImage] = useState<any>(null)
// 分頁狀態
const [currentPage, setCurrentPage] = useState(1)
@@ -465,6 +482,35 @@ export default function AdminPage() {
}
}
// 查看詳細資訊
const viewWishDetails = async (wish: WishData) => {
try {
setLoadingDetails(true)
setSelectedWish(wish)
const response = await fetch(`/api/admin/wishes/${wish.id}`)
const result = await response.json()
if (result.success) {
setWishDetails(result.data)
setShowWishDetails(true)
} else {
throw new Error(result.error || 'Failed to fetch details')
}
} catch (error) {
console.error('獲取詳細資訊失敗:', error)
alert('獲取詳細資訊失敗,請稍後再試')
} finally {
setLoadingDetails(false)
}
}
// 查看大圖
const viewImage = (image: any) => {
setSelectedImage(image)
setShowImageModal(true)
}
useEffect(() => {
fetchData()
}, [])
@@ -740,9 +786,15 @@ export default function AdminPage() {
<Button
size="sm"
variant="outline"
onClick={() => viewWishDetails(wish)}
disabled={loadingDetails}
className="text-blue-200 border-slate-600/50 hover:bg-slate-700/50 hover:text-white hover:border-cyan-400/50"
>
{loadingDetails ? (
<RefreshCw className="w-4 h-4 mr-1 animate-spin" />
) : (
<Eye className="w-4 h-4 mr-1" />
)}
</Button>
</td>
@@ -1031,6 +1083,386 @@ export default function AdminPage() {
</Tabs>
</div>
</main>
{/* 詳細資訊對話框 */}
<Dialog open={showWishDetails} onOpenChange={setShowWishDetails}>
<DialogContent className="w-[95vw] max-w-6xl h-[95vh] bg-slate-800 border-slate-700 flex flex-col p-0 z-[60]">
<DialogHeader className="px-6 py-4 border-b border-slate-600/50 flex-shrink-0">
<DialogTitle className="text-white flex items-center gap-2">
<Eye className="w-5 h-5 text-cyan-400" />
</DialogTitle>
<DialogDescription className="text-blue-200">
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-y-auto px-6 py-4">
{wishDetails && (
<div className="space-y-6">
{/* 基本信息 */}
<Card className="bg-slate-700/50 border-slate-600/50">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<FileText className="w-5 h-5 text-green-400" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center gap-2">
<Tag className="w-4 h-4 text-blue-400" />
<span className="text-blue-200">ID:</span>
<span className="text-white font-mono">{wishDetails.id}</span>
</div>
<div className="flex items-center gap-2">
<Star className="w-4 h-4 text-yellow-400" />
<span className="text-blue-200">:</span>
<Badge variant="outline" className="text-white border-slate-500">
{wishDetails.priority}
</Badge>
</div>
<div className="flex items-center gap-2">
<Eye className="w-4 h-4 text-cyan-400" />
<span className="text-blue-200">:</span>
<Badge
variant={wishDetails.isPublic ? "default" : "outline"}
className={wishDetails.isPublic
? "bg-blue-600/20 text-blue-300 border-blue-500/30"
: "bg-orange-600/20 text-orange-300 border-orange-500/30"
}
>
{wishDetails.isPublic ? '公開' : '私密'}
</Badge>
</div>
<div className="flex items-center gap-2">
<Target className="w-4 h-4 text-green-400" />
<span className="text-blue-200">:</span>
<Badge
variant={wishDetails.status === 'active' ? "default" : "secondary"}
className={wishDetails.status === 'active'
? "bg-green-600/20 text-green-300 border-green-500/30"
: "bg-slate-600/20 text-slate-300 border-slate-500/30"
}
>
{wishDetails.status === 'active' ? '活躍' : '非活躍'}
</Badge>
</div>
</div>
<div>
<h4 className="text-blue-200 font-semibold mb-2"></h4>
<p className="text-white bg-slate-800/50 p-3 rounded-lg border border-slate-600/30">
{wishDetails.title}
</p>
</div>
{wishDetails.category && (
<div>
<h4 className="text-blue-200 font-semibold mb-2"></h4>
<Badge variant="outline" className="text-white border-slate-500">
{wishDetails.category}
</Badge>
</div>
)}
</CardContent>
</Card>
{/* 困擾內容 */}
<Card className="bg-slate-700/50 border-slate-600/50">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Users className="w-5 h-5 text-red-400" />
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-white bg-slate-800/50 p-4 rounded-lg border border-slate-600/30 whitespace-pre-wrap">
{wishDetails.currentPain}
</p>
</CardContent>
</Card>
{/* 期望解決方式 */}
<Card className="bg-slate-700/50 border-slate-600/50">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Target className="w-5 h-5 text-green-400" />
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-white bg-slate-800/50 p-4 rounded-lg border border-slate-600/30 whitespace-pre-wrap">
{wishDetails.expectedSolution}
</p>
</CardContent>
</Card>
{/* 預期效果 */}
{wishDetails.expectedEffect && (
<Card className="bg-slate-700/50 border-slate-600/50">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Star className="w-5 h-5 text-yellow-400" />
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-white bg-slate-800/50 p-4 rounded-lg border border-slate-600/30 whitespace-pre-wrap">
{wishDetails.expectedEffect}
</p>
</CardContent>
</Card>
)}
{/* 圖片 */}
{wishDetails.images && Array.isArray(wishDetails.images) && wishDetails.images.length > 0 && (
<Card className="bg-slate-700/50 border-slate-600/50">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<ImageIcon className="w-5 h-5 text-purple-400" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{wishDetails.images.map((image: any, index: number) => (
<div key={index} className="bg-slate-800/50 rounded-lg border border-slate-600/30 overflow-hidden">
{/* 圖片顯示 */}
<div className="aspect-video bg-slate-900/50 flex items-center justify-center relative group cursor-pointer" onClick={() => viewImage(image)}>
{(image.url || image.base64 || image.storage_path || image.public_url) ? (
<>
<img
src={image.public_url || image.url || image.base64 || image.storage_path}
alt={image.name || `圖片 ${index + 1}`}
className="max-w-full max-h-full object-contain rounded-lg transition-transform group-hover:scale-105"
onLoad={(e) => {
const target = e.target as HTMLImageElement;
target.nextElementSibling?.classList.add('hidden');
}}
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
target.nextElementSibling?.classList.remove('hidden');
}}
/>
{/* 載入中指示器 */}
<div className="absolute inset-0 flex flex-col items-center justify-center text-slate-400 p-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-cyan-400 mb-2"></div>
<span className="text-sm">...</span>
</div>
{/* 放大圖標 */}
<div className="absolute top-2 right-2 bg-black/50 backdrop-blur-sm rounded-full p-2 opacity-0 group-hover:opacity-100 transition-opacity">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7" />
</svg>
</div>
</>
) : (
<div className="flex flex-col items-center justify-center text-slate-400 p-8">
<ImageIcon className="w-12 h-12 mb-2 text-slate-500" />
<span className="text-sm"></span>
</div>
)}
</div>
{/* 圖片資訊 */}
<div className="p-3 border-t border-slate-600/30">
<div className="flex items-center justify-between">
<div className="text-sm text-white font-medium truncate">
{image.name || `圖片 ${index + 1}`}
</div>
<div className="text-xs text-cyan-400 opacity-0 group-hover:opacity-100 transition-opacity">
</div>
</div>
{image.size && (
<div className="text-xs text-slate-400 mt-1">
: {typeof image.size === 'number' ? `${(image.size / 1024).toFixed(1)} KB` : image.size}
</div>
)}
{image.type && (
<div className="text-xs text-slate-400">
: {image.type}
</div>
)}
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* 用戶資訊 */}
<Card className="bg-slate-700/50 border-slate-600/50">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<User className="w-5 h-5 text-cyan-400" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-blue-400" />
<span className="text-blue-200"> ID:</span>
<span className="text-white font-mono text-sm">{wishDetails.userSession}</span>
</div>
{wishDetails.email && (
<div className="flex items-center gap-2">
<Mail className="w-4 h-4 text-green-400" />
<span className="text-blue-200">:</span>
<span className="text-white">{wishDetails.email}</span>
</div>
)}
</CardContent>
</Card>
{/* 時間資訊 */}
<Card className="bg-slate-700/50 border-slate-600/50">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Clock className="w-5 h-5 text-orange-400" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-blue-400" />
<span className="text-blue-200">:</span>
<span className="text-white">
{new Date(wishDetails.createdAt).toLocaleString('zh-TW')}
</span>
</div>
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-green-400" />
<span className="text-blue-200">:</span>
<span className="text-white">
{new Date(wishDetails.updatedAt).toLocaleString('zh-TW')}
</span>
</div>
</CardContent>
</Card>
{/* 點讚記錄 */}
<Card className="bg-slate-700/50 border-slate-600/50">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Heart className="w-5 h-5 text-pink-400" />
({wishDetails.likes?.length || 0} )
</CardTitle>
</CardHeader>
<CardContent>
{wishDetails.likes && wishDetails.likes.length > 0 ? (
<div className="space-y-3 max-h-48 overflow-y-auto scrollbar-thin scrollbar-thumb-slate-600 scrollbar-track-slate-800">
{wishDetails.likes.map((like: any, index: number) => (
<div key={like.id} className="bg-slate-800/50 p-3 rounded-lg border border-slate-600/30">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Heart className="w-4 h-4 text-pink-400" />
<span className="text-blue-200"> #{index + 1}</span>
</div>
<span className="text-slate-400 text-sm">
{new Date(like.createdAt).toLocaleString('zh-TW')}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
<div className="flex items-center gap-2">
<User className="w-3 h-3 text-blue-400" />
<span className="text-blue-200">:</span>
<span className="text-white font-mono text-xs">{like.userSession}</span>
</div>
{like.ipAddress && (
<div className="flex items-center gap-2">
<MapPin className="w-3 h-3 text-green-400" />
<span className="text-blue-200">IP:</span>
<span className="text-white font-mono text-xs">{like.ipAddress}</span>
</div>
)}
</div>
{like.userAgent && (
<div className="mt-2 flex items-start gap-2">
<Monitor className="w-3 h-3 text-purple-400 mt-0.5" />
<span className="text-blue-200 text-xs">:</span>
<span className="text-slate-300 text-xs break-all">{like.userAgent}</span>
</div>
)}
</div>
))}
</div>
) : (
<div className="text-center py-8">
<Heart className="w-8 h-8 text-slate-500 mx-auto mb-2" />
<p className="text-slate-400"></p>
</div>
)}
</CardContent>
</Card>
</div>
)}
</div>
<div className="flex justify-end px-6 py-4 border-t border-slate-600/50 flex-shrink-0">
<Button
onClick={() => setShowWishDetails(false)}
className="bg-slate-700 hover:bg-slate-600 text-white"
>
<X className="w-4 h-4 mr-2" />
</Button>
</div>
</DialogContent>
</Dialog>
{/* 圖片放大對話框 */}
<Dialog open={showImageModal} onOpenChange={setShowImageModal}>
<DialogContent className="max-w-[95vw] max-h-[95vh] bg-black border-slate-700 p-0 overflow-hidden">
<DialogHeader className="px-6 py-4 border-b border-slate-600/50 flex-shrink-0">
<DialogTitle className="text-white flex items-center gap-2">
<ImageIcon className="w-5 h-5 text-purple-400" />
{selectedImage?.name || '圖片預覽'}
</DialogTitle>
<DialogDescription className="text-blue-200">
ESC
</DialogDescription>
</DialogHeader>
<div className="flex-1 flex items-center justify-center p-4 bg-black relative">
{selectedImage && (
<>
<img
src={selectedImage.public_url || selectedImage.url || selectedImage.base64 || selectedImage.storage_path}
alt={selectedImage.name || '圖片預覽'}
className="max-w-full max-h-full object-contain"
onClick={(e) => e.stopPropagation()}
/>
{/* 圖片載入失敗處理 */}
<div className="hidden flex-col items-center justify-center text-slate-400">
<ImageIcon className="w-16 h-16 mb-4 text-slate-500" />
<span className="text-lg"></span>
</div>
</>
)}
</div>
<div className="flex justify-between items-center px-6 py-4 border-t border-slate-600/50 flex-shrink-0 bg-slate-800/50">
<div className="text-sm text-slate-300">
{selectedImage?.size && (
<span>: {typeof selectedImage.size === 'number' ? `${(selectedImage.size / 1024).toFixed(1)} KB` : selectedImage.size}</span>
)}
{selectedImage?.type && (
<span className="ml-4">: {selectedImage.type}</span>
)}
</div>
<Button
onClick={() => setShowImageModal(false)}
className="bg-slate-700 hover:bg-slate-600 text-white"
>
<X className="w-4 h-4 mr-2" />
</Button>
</div>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -0,0 +1,100 @@
import { NextRequest, NextResponse } from 'next/server'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const wishId = parseInt(params.id)
if (isNaN(wishId)) {
return NextResponse.json(
{ success: false, error: 'Invalid wish ID' },
{ status: 400 }
)
}
console.log(`🔍 後台管理 - 獲取困擾案例詳細資訊: ID=${wishId}`)
// 獲取 wish 詳細資訊
const wish = await prisma.wish.findUnique({
where: { id: wishId },
include: {
likes: {
select: {
id: true,
userSession: true,
ipAddress: true,
userAgent: true,
createdAt: true
}
}
}
})
if (!wish) {
return NextResponse.json(
{ success: false, error: 'Wish not found' },
{ status: 404 }
)
}
// 處理圖片數據
let images = []
if (wish.images && Array.isArray(wish.images)) {
images = wish.images.map((img: any) => ({
id: img.id,
name: img.name,
size: img.size,
type: img.type,
url: img.public_url || img.base64 || img.url || img.storage_path,
base64: img.base64,
storage_path: img.storage_path,
public_url: img.public_url,
uploaded_at: img.uploaded_at
}))
}
// 轉換數據格式
const wishDetails = {
id: Number(wish.id),
title: wish.title,
currentPain: wish.currentPain,
expectedSolution: wish.expectedSolution,
expectedEffect: wish.expectedEffect,
isPublic: wish.isPublic,
email: wish.email,
images: images,
userSession: wish.userSession,
status: wish.status,
category: wish.category,
priority: Number(wish.priority),
createdAt: wish.createdAt.toISOString(),
updatedAt: wish.updatedAt.toISOString(),
likes: wish.likes.map(like => ({
id: Number(like.id),
userSession: like.userSession,
ipAddress: like.ipAddress,
userAgent: like.userAgent,
createdAt: like.createdAt.toISOString()
}))
}
console.log(`✅ 成功獲取困擾案例詳細資訊: ${wishDetails.title}`)
return NextResponse.json({
success: true,
data: wishDetails
})
} catch (error) {
console.error('獲取困擾案例詳細資訊失敗:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch wish details' },
{ status: 500 }
)
}
}