修正 vercal 佈署失敗問題
This commit is contained in:
@@ -3,22 +3,31 @@
|
|||||||
import type React from "react"
|
import type React from "react"
|
||||||
|
|
||||||
import { Analytics } from "@vercel/analytics/next"
|
import { Analytics } from "@vercel/analytics/next"
|
||||||
import { useSearchParams } from "next/navigation"
|
|
||||||
import { Suspense } from "react"
|
import { Suspense } from "react"
|
||||||
import { Toaster } from "@/components/ui/toaster"
|
import { Toaster } from "@/components/ui/toaster"
|
||||||
|
|
||||||
|
function ClientLayoutContent({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<Toaster />
|
||||||
|
<Analytics />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function ClientLayout({
|
export default function ClientLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}>) {
|
}>) {
|
||||||
const searchParams = useSearchParams()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
|
<ClientLayoutContent>{children}</ClientLayoutContent>
|
||||||
<Toaster />
|
</Suspense>
|
||||||
<Analytics />
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
"use client"
|
import { Suspense } from "react"
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
|
||||||
import { useSearchParams } from "next/navigation"
|
|
||||||
import { Sidebar } from "@/components/sidebar"
|
import { Sidebar } from "@/components/sidebar"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
@@ -28,164 +25,10 @@ import {
|
|||||||
import { Download, Share2, TrendingUp, AlertCircle, CheckCircle, Star } from "lucide-react"
|
import { Download, Share2, TrendingUp, AlertCircle, CheckCircle, Star } from "lucide-react"
|
||||||
import { useToast } from "@/hooks/use-toast"
|
import { useToast } from "@/hooks/use-toast"
|
||||||
import { ShareModal } from "@/components/share-modal"
|
import { ShareModal } from "@/components/share-modal"
|
||||||
|
import ResultsContent from "./results-content"
|
||||||
|
|
||||||
// 模擬評分結果數據 - 使用您提到的例子:8, 7, 6, 8, 4,平均 = 6.6
|
// Loading component
|
||||||
const mockCriteria = [
|
function ResultsLoading() {
|
||||||
{
|
|
||||||
name: "內容品質",
|
|
||||||
score: 8,
|
|
||||||
maxScore: 10,
|
|
||||||
weight: 25,
|
|
||||||
weightedScore: 20,
|
|
||||||
feedback: "內容結構清晰,資訊豐富且準確。建議增加更多實際案例來支撐論點。",
|
|
||||||
strengths: ["邏輯清晰", "資料準確", "結構完整"],
|
|
||||||
improvements: ["增加案例", "深化分析"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "視覺設計",
|
|
||||||
score: 7,
|
|
||||||
maxScore: 10,
|
|
||||||
weight: 20,
|
|
||||||
weightedScore: 14,
|
|
||||||
feedback: "整體設計風格統一,色彩搭配合理。部分頁面文字密度過高,影響閱讀體驗。",
|
|
||||||
strengths: ["風格統一", "色彩協調", "版面整潔"],
|
|
||||||
improvements: ["減少文字密度", "增加視覺元素"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "邏輯結構",
|
|
||||||
score: 6,
|
|
||||||
maxScore: 10,
|
|
||||||
weight: 20,
|
|
||||||
weightedScore: 12,
|
|
||||||
feedback: "邏輯架構清晰,各章節銜接自然,論述層次分明。",
|
|
||||||
strengths: ["邏輯清晰", "結構完整", "銜接自然"],
|
|
||||||
improvements: ["可增加總結回顧"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "創新性",
|
|
||||||
score: 8,
|
|
||||||
maxScore: 10,
|
|
||||||
weight: 15,
|
|
||||||
weightedScore: 12,
|
|
||||||
feedback: "內容具有一定創新性,但可以更大膽地提出獨特觀點和解決方案。",
|
|
||||||
strengths: ["思路新穎", "角度獨特"],
|
|
||||||
improvements: ["增加創新元素", "提出獨特見解"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "實用性",
|
|
||||||
score: 4,
|
|
||||||
maxScore: 10,
|
|
||||||
weight: 20,
|
|
||||||
weightedScore: 8,
|
|
||||||
feedback: "內容實用性有待提升,提供的解決方案需要更具可操作性。",
|
|
||||||
strengths: ["實用性強", "可操作性好", "價值明確"],
|
|
||||||
improvements: ["增加實施步驟"],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 計算 mock 數據的 overview - 基於平均分作為閾值
|
|
||||||
const calculateMockOverview = (criteria: any[]) => {
|
|
||||||
if (!criteria || criteria.length === 0) {
|
|
||||||
return {
|
|
||||||
excellentItems: 0,
|
|
||||||
improvementItems: 0,
|
|
||||||
overallPerformance: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 計算所有項目的平均分數(不考慮權重)
|
|
||||||
const totalScore = criteria.reduce((sum, item) => sum + item.score, 0);
|
|
||||||
const averageScore = totalScore / criteria.length;
|
|
||||||
|
|
||||||
// 以平均分作為閾值
|
|
||||||
// ≥ 平均分 = 優秀項目,< 平均分 = 待改進項目
|
|
||||||
const excellentItems = criteria.filter(item => item.score >= averageScore).length;
|
|
||||||
const improvementItems = criteria.filter(item => item.score < averageScore).length;
|
|
||||||
|
|
||||||
// 整體表現:基於權重的加權平均分數
|
|
||||||
const overallPerformance = Math.round(criteria.reduce((sum, item) => sum + (item.score / item.maxScore) * item.weight, 0));
|
|
||||||
|
|
||||||
return {
|
|
||||||
excellentItems,
|
|
||||||
improvementItems,
|
|
||||||
overallPerformance
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockResults = {
|
|
||||||
projectTitle: "產品介紹簡報",
|
|
||||||
overallScore: 82,
|
|
||||||
totalPossible: 100,
|
|
||||||
grade: "B+",
|
|
||||||
analysisDate: "2024-01-15",
|
|
||||||
criteria: mockCriteria,
|
|
||||||
overview: calculateMockOverview(mockCriteria),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 圖表數據將在組件內部動態生成
|
|
||||||
|
|
||||||
const COLORS = ["#0891b2", "#6366f1", "#f59e0b", "#dc2626", "#10b981"]
|
|
||||||
|
|
||||||
export default function ResultsPage() {
|
|
||||||
const [activeTab, setActiveTab] = useState("overview")
|
|
||||||
const [evaluationData, setEvaluationData] = useState(null)
|
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
const [isShareModalOpen, setIsShareModalOpen] = useState(false)
|
|
||||||
const { toast } = useToast()
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadEvaluationData = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true)
|
|
||||||
setError(null)
|
|
||||||
|
|
||||||
// 檢查是否有 URL 參數中的評審 ID
|
|
||||||
const evaluationId = searchParams.get('id')
|
|
||||||
|
|
||||||
if (evaluationId) {
|
|
||||||
// 從 API 獲取評審數據
|
|
||||||
console.log(`📊 從 API 獲取評審數據: ID=${evaluationId}`)
|
|
||||||
const response = await fetch(`/api/evaluation/${evaluationId}`)
|
|
||||||
const result = await response.json()
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
setEvaluationData(result.data)
|
|
||||||
console.log('✅ 成功載入評審數據:', result.data.projectTitle)
|
|
||||||
} else {
|
|
||||||
throw new Error(result.error || '獲取評審數據失敗')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 回退到從 localStorage 獲取評審結果
|
|
||||||
console.log('📊 從 localStorage 獲取評審結果')
|
|
||||||
const storedData = localStorage.getItem('evaluationResult')
|
|
||||||
if (storedData) {
|
|
||||||
const data = JSON.parse(storedData)
|
|
||||||
setEvaluationData(data)
|
|
||||||
console.log('✅ 成功載入 localStorage 數據')
|
|
||||||
} else {
|
|
||||||
throw new Error('沒有找到評審結果')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('載入評審結果失敗:', error)
|
|
||||||
setError((error as Error).message)
|
|
||||||
toast({
|
|
||||||
title: "載入失敗",
|
|
||||||
description: (error as Error).message || "無法載入評審結果,請重新進行評審",
|
|
||||||
variant: "destructive",
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadEvaluationData()
|
|
||||||
}, [searchParams, toast])
|
|
||||||
|
|
||||||
// 如果正在載入,顯示載入狀態
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
@@ -201,477 +44,12 @@ export default function ResultsPage() {
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果沒有數據或發生錯誤,顯示錯誤狀態
|
export default function ResultsPage() {
|
||||||
if (!evaluationData || error) {
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<Suspense fallback={<ResultsLoading />}>
|
||||||
<Sidebar />
|
<ResultsContent />
|
||||||
<main className="md:ml-64 p-6">
|
</Suspense>
|
||||||
<div className="max-w-6xl mx-auto">
|
|
||||||
<div className="flex items-center justify-center h-64">
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-muted-foreground mb-4">
|
|
||||||
{error ? `載入失敗: ${error}` : '沒有找到評審結果'}
|
|
||||||
</p>
|
|
||||||
<div className="space-x-2">
|
|
||||||
<Button onClick={() => window.location.href = '/upload'}>
|
|
||||||
重新進行評審
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" onClick={() => window.location.href = '/history'}>
|
|
||||||
查看歷史記錄
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用真實數據或回退到模擬數據
|
|
||||||
const results = (evaluationData as any)?.evaluation?.fullData || evaluationData || mockResults
|
|
||||||
|
|
||||||
// 計算統計數據 - 基於 criteria_items 的平均分作為閾值
|
|
||||||
const calculateOverview = (criteria: any[]): any => {
|
|
||||||
if (!criteria || criteria.length === 0) {
|
|
||||||
return {
|
|
||||||
excellentItems: 0,
|
|
||||||
improvementItems: 0,
|
|
||||||
overallPerformance: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 計算所有項目的平均分數(不考慮權重)
|
|
||||||
const totalScore = criteria.reduce((sum, item) => sum + item.score, 0);
|
|
||||||
const averageScore = totalScore / criteria.length;
|
|
||||||
|
|
||||||
// 以平均分作為閾值
|
|
||||||
// ≥ 平均分 = 優秀項目,< 平均分 = 待改進項目
|
|
||||||
const excellentItems = criteria.filter(item => item.score >= averageScore).length;
|
|
||||||
const improvementItems = criteria.filter(item => item.score < averageScore).length;
|
|
||||||
|
|
||||||
// 整體表現:基於權重的加權平均分數
|
|
||||||
const overallPerformance = Math.round(criteria.reduce((sum, item) => sum + (item.score / item.maxScore) * item.weight, 0));
|
|
||||||
|
|
||||||
return {
|
|
||||||
excellentItems,
|
|
||||||
improvementItems,
|
|
||||||
overallPerformance
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 確保所有必要的數據結構存在
|
|
||||||
const safeResults = {
|
|
||||||
...results,
|
|
||||||
overview: results.overview || calculateOverview(results.criteria || []),
|
|
||||||
chartData: results.chartData || {
|
|
||||||
barChart: [],
|
|
||||||
pieChart: [],
|
|
||||||
radarChart: []
|
|
||||||
},
|
|
||||||
improvementSuggestions: results.improvementSuggestions || {
|
|
||||||
overallSuggestions: '暫無改進建議',
|
|
||||||
maintainStrengths: [],
|
|
||||||
keyImprovements: [],
|
|
||||||
actionPlan: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadReport = async () => {
|
|
||||||
try {
|
|
||||||
// 顯示載入提示
|
|
||||||
toast({
|
|
||||||
title: "報告下載中",
|
|
||||||
description: "評審報告 PDF 正在生成,請稍候...",
|
|
||||||
});
|
|
||||||
|
|
||||||
// 獲取評審 ID
|
|
||||||
const evaluationId = searchParams.get('id');
|
|
||||||
|
|
||||||
if (!evaluationId) {
|
|
||||||
throw new Error('無法獲取評審 ID');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 調用下載 API
|
|
||||||
const response = await fetch(`/api/evaluation/${evaluationId}/download`);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json();
|
|
||||||
throw new Error(errorData.error || '下載失敗');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 獲取 PDF Blob
|
|
||||||
const pdfBlob = await response.blob();
|
|
||||||
|
|
||||||
// 創建下載連結
|
|
||||||
const url = window.URL.createObjectURL(pdfBlob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
|
|
||||||
// 從 Content-Disposition 標頭獲取檔案名稱,或使用預設名稱
|
|
||||||
const contentDisposition = response.headers.get('Content-Disposition');
|
|
||||||
let fileName = `評審報告_${safeResults.projectTitle}_${safeResults.analysisDate}.pdf`;
|
|
||||||
|
|
||||||
if (contentDisposition) {
|
|
||||||
const fileNameMatch = contentDisposition.match(/filename="(.+)"/);
|
|
||||||
if (fileNameMatch) {
|
|
||||||
fileName = decodeURIComponent(fileNameMatch[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
link.download = fileName;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
// 顯示成功提示
|
|
||||||
toast({
|
|
||||||
title: "下載完成",
|
|
||||||
description: "評審報告已成功下載",
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('下載失敗:', error);
|
|
||||||
toast({
|
|
||||||
title: "下載失敗",
|
|
||||||
description: error instanceof Error ? error.message : '下載過程中發生錯誤',
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const shareResults = () => {
|
|
||||||
setIsShareModalOpen(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getScoreColor = (score: number, maxScore: number) => {
|
|
||||||
const percentage = (score / maxScore) * 100
|
|
||||||
if (percentage >= 90) return "text-green-600"
|
|
||||||
if (percentage >= 80) return "text-blue-600"
|
|
||||||
if (percentage >= 70) return "text-yellow-600"
|
|
||||||
if (percentage >= 60) return "text-orange-600"
|
|
||||||
return "text-red-600"
|
|
||||||
}
|
|
||||||
|
|
||||||
const getGradeColor = (grade: string) => {
|
|
||||||
if (grade.startsWith("A")) return "bg-green-100 text-green-800"
|
|
||||||
if (grade.startsWith("B")) return "bg-blue-100 text-blue-800"
|
|
||||||
if (grade.startsWith("C")) return "bg-yellow-100 text-yellow-800"
|
|
||||||
return "bg-red-100 text-red-800"
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-background">
|
|
||||||
<Sidebar />
|
|
||||||
|
|
||||||
<main className="md:ml-64 p-6">
|
|
||||||
<div className="max-w-6xl mx-auto">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8 pt-8 md:pt-0">
|
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold text-foreground mb-2 font-[var(--font-playfair)]">評審結果</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
{safeResults.projectTitle} - 分析完成於 {safeResults.analysisDate}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button onClick={shareResults} variant="outline">
|
|
||||||
<Share2 className="h-4 w-4 mr-2" />
|
|
||||||
分享
|
|
||||||
</Button>
|
|
||||||
<Button onClick={downloadReport}>
|
|
||||||
<Download className="h-4 w-4 mr-2" />
|
|
||||||
下載報告
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Overall Score */}
|
|
||||||
<Card className="mb-8">
|
|
||||||
<CardContent className="pt-6">
|
|
||||||
<div className="grid md:grid-cols-4 gap-6">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-4xl font-bold text-primary mb-2">{safeResults.overallScore}</div>
|
|
||||||
<div className="text-sm text-muted-foreground">總分 / {safeResults.totalPossible}</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<Badge className={`text-lg px-4 py-2 ${getGradeColor(safeResults.grade)}`}>{safeResults.grade}</Badge>
|
|
||||||
<div className="text-sm text-muted-foreground mt-2">等級評定</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="flex items-center justify-center mb-2">
|
|
||||||
<TrendingUp className="h-6 w-6 text-green-600" />
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-muted-foreground">{safeResults.performanceStatus}</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="flex items-center justify-center mb-2">
|
|
||||||
{Array.from({ length: safeResults.recommendedStars || 0 }, (_, i) => (
|
|
||||||
<Star key={i} className="h-6 w-6 text-yellow-500 fill-current" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-muted-foreground">推薦等級</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Detailed Results */}
|
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
|
||||||
<TabsList className="grid w-full grid-cols-4">
|
|
||||||
<TabsTrigger value="overview">總覽</TabsTrigger>
|
|
||||||
<TabsTrigger value="detailed">詳細分析</TabsTrigger>
|
|
||||||
<TabsTrigger value="charts">圖表分析</TabsTrigger>
|
|
||||||
<TabsTrigger value="suggestions">改進建議</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="overview" className="space-y-6">
|
|
||||||
{/* Score Breakdown */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>評分明細</CardTitle>
|
|
||||||
<CardDescription>各項評分標準的得分情況</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{safeResults.criteria.map((item: any, index: number) => (
|
|
||||||
<div key={index} className="flex items-center justify-between p-4 bg-muted rounded-lg">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-3 mb-2">
|
|
||||||
<h4 className="font-medium">{item.name}</h4>
|
|
||||||
<Badge variant="outline">{item.weight}% 權重</Badge>
|
|
||||||
</div>
|
|
||||||
<Progress value={(item.score / item.maxScore) * 100} className="h-2" />
|
|
||||||
</div>
|
|
||||||
<div className="text-right ml-4">
|
|
||||||
<div className={`text-2xl font-bold ${getScoreColor(item.score, item.maxScore)}`}>
|
|
||||||
{item.score}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-muted-foreground">/ {item.maxScore}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Quick Stats */}
|
|
||||||
<div className="grid md:grid-cols-3 gap-6">
|
|
||||||
<Card>
|
|
||||||
<CardContent className="pt-6 text-center">
|
|
||||||
<CheckCircle className="h-8 w-8 text-green-600 mx-auto mb-2" />
|
|
||||||
<div className="text-2xl font-bold text-green-600 mb-1">{safeResults.overview.excellentItems}</div>
|
|
||||||
<div className="text-sm text-muted-foreground">優秀項目</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardContent className="pt-6 text-center">
|
|
||||||
<AlertCircle className="h-8 w-8 text-yellow-600 mx-auto mb-2" />
|
|
||||||
<div className="text-2xl font-bold text-yellow-600 mb-1">{safeResults.overview.improvementItems}</div>
|
|
||||||
<div className="text-sm text-muted-foreground">待改進項目</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardContent className="pt-6 text-center">
|
|
||||||
<TrendingUp className="h-8 w-8 text-blue-600 mx-auto mb-2" />
|
|
||||||
<div className="text-2xl font-bold text-blue-600 mb-1">{safeResults.overview.overallPerformance}%</div>
|
|
||||||
<div className="text-sm text-muted-foreground">整體表現</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="detailed" className="space-y-6">
|
|
||||||
{safeResults.criteria.map((item: any, index: number) => (
|
|
||||||
<Card key={index}>
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
{item.name}
|
|
||||||
<Badge variant="outline">{item.weight}% 權重</Badge>
|
|
||||||
</CardTitle>
|
|
||||||
<div className={`text-2xl font-bold ${getScoreColor(item.score, item.maxScore)}`}>
|
|
||||||
{item.score}/{item.maxScore}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">AI 評語</h4>
|
|
||||||
<p className="text-muted-foreground leading-relaxed">{item.feedback}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2 text-green-700">優點</h4>
|
|
||||||
<ul className="space-y-1">
|
|
||||||
{item.strengths.map((strength: any, i: number) => (
|
|
||||||
<li key={i} className="flex items-center gap-2 text-sm">
|
|
||||||
<CheckCircle className="h-4 w-4 text-green-600" />
|
|
||||||
{strength}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2 text-orange-700">改進建議</h4>
|
|
||||||
<ul className="space-y-1">
|
|
||||||
{item.improvements.map((improvement: any, i: number) => (
|
|
||||||
<li key={i} className="flex items-center gap-2 text-sm">
|
|
||||||
<AlertCircle className="h-4 w-4 text-orange-600" />
|
|
||||||
{improvement}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="charts" className="space-y-6">
|
|
||||||
<div className="grid lg:grid-cols-2 gap-6">
|
|
||||||
{/* Bar Chart */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>各項目得分</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
|
||||||
<BarChart data={safeResults.chartData.barChart}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="name" angle={-45} textAnchor="end" height={80} />
|
|
||||||
<YAxis domain={[0, 10]} />
|
|
||||||
<Tooltip />
|
|
||||||
<Bar dataKey="score" fill="#0891b2" />
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Pie Chart */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>權重分布</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
|
||||||
<PieChart>
|
|
||||||
<Pie
|
|
||||||
data={safeResults.chartData.pieChart}
|
|
||||||
cx="50%"
|
|
||||||
cy="50%"
|
|
||||||
labelLine={false}
|
|
||||||
label={({ name, weight }) => `${name} (${weight}%)`}
|
|
||||||
outerRadius={80}
|
|
||||||
fill="#8884d8"
|
|
||||||
dataKey="value"
|
|
||||||
>
|
|
||||||
{safeResults.chartData.pieChart.map((entry: any, index: number) => (
|
|
||||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
<Tooltip />
|
|
||||||
</PieChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Radar Chart */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>能力雷達圖</CardTitle>
|
|
||||||
<CardDescription>各項能力的綜合表現分析</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
|
||||||
<RadarChart data={safeResults.chartData.radarChart}>
|
|
||||||
<PolarGrid />
|
|
||||||
<PolarAngleAxis dataKey="subject" />
|
|
||||||
<PolarRadiusAxis domain={[0, 10]} />
|
|
||||||
<Radar name="得分" dataKey="score" stroke="#0891b2" fill="#0891b2" fillOpacity={0.3} />
|
|
||||||
</RadarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="suggestions" className="space-y-6">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>整體改進建議</CardTitle>
|
|
||||||
<CardDescription>{safeResults.improvementSuggestions.overallSuggestions}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-3 text-green-700">繼續保持的優勢</h4>
|
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
|
||||||
{safeResults.improvementSuggestions.maintainStrengths.map((strength: any, index: number) => (
|
|
||||||
<div key={index} className="p-4 bg-green-50 rounded-lg">
|
|
||||||
<h5 className="font-medium mb-2">{strength.title}</h5>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{strength.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-3 text-orange-700">重點改進方向</h4>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{safeResults.improvementSuggestions.keyImprovements.map((improvement: any, index: number) => (
|
|
||||||
<div key={index} className="p-4 bg-orange-50 rounded-lg">
|
|
||||||
<h5 className="font-medium mb-2">{improvement.title}</h5>
|
|
||||||
<p className="text-sm text-muted-foreground mb-3">{improvement.description}</p>
|
|
||||||
<ul className="text-sm space-y-1">
|
|
||||||
{improvement.suggestions.map((suggestion: any, sIndex: number) => (
|
|
||||||
<li key={sIndex}>• {suggestion}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-3 text-blue-700">下一步行動計劃</h4>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{safeResults.improvementSuggestions.actionPlan.map((action: any, index: number) => (
|
|
||||||
<div key={index} className="flex items-start gap-3 p-3 bg-blue-50 rounded-lg">
|
|
||||||
<div className="w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-bold">
|
|
||||||
{index + 1}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h5 className="font-medium">{action.phase}</h5>
|
|
||||||
<p className="text-sm text-muted-foreground">{action.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{/* Share Modal */}
|
|
||||||
<ShareModal
|
|
||||||
isOpen={isShareModalOpen}
|
|
||||||
onClose={() => setIsShareModalOpen(false)}
|
|
||||||
evaluationId={searchParams.get('id') || undefined}
|
|
||||||
projectTitle={safeResults.projectTitle}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
677
app/results/results-content.tsx
Normal file
677
app/results/results-content.tsx
Normal file
@@ -0,0 +1,677 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { useSearchParams } from "next/navigation"
|
||||||
|
import { Sidebar } from "@/components/sidebar"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Progress } from "@/components/ui/progress"
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
|
import {
|
||||||
|
BarChart,
|
||||||
|
Bar,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
ResponsiveContainer,
|
||||||
|
PieChart,
|
||||||
|
Pie,
|
||||||
|
Cell,
|
||||||
|
RadarChart,
|
||||||
|
PolarGrid,
|
||||||
|
PolarAngleAxis,
|
||||||
|
PolarRadiusAxis,
|
||||||
|
Radar,
|
||||||
|
} from "recharts"
|
||||||
|
import { Download, Share2, TrendingUp, AlertCircle, CheckCircle, Star } from "lucide-react"
|
||||||
|
import { useToast } from "@/hooks/use-toast"
|
||||||
|
import { ShareModal } from "@/components/share-modal"
|
||||||
|
|
||||||
|
// 模擬評分結果數據 - 使用您提到的例子:8, 7, 6, 8, 4,平均 = 6.6
|
||||||
|
const mockCriteria = [
|
||||||
|
{
|
||||||
|
name: "內容品質",
|
||||||
|
score: 8,
|
||||||
|
maxScore: 10,
|
||||||
|
weight: 25,
|
||||||
|
weightedScore: 20,
|
||||||
|
feedback: "內容結構清晰,資訊豐富且準確。建議增加更多實際案例來支撐論點。",
|
||||||
|
strengths: ["邏輯清晰", "資料準確", "結構完整"],
|
||||||
|
improvements: ["增加案例", "深化分析"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "視覺設計",
|
||||||
|
score: 7,
|
||||||
|
maxScore: 10,
|
||||||
|
weight: 20,
|
||||||
|
weightedScore: 14,
|
||||||
|
feedback: "整體設計風格統一,色彩搭配合理。部分頁面文字密度過高,影響閱讀體驗。",
|
||||||
|
strengths: ["風格統一", "色彩協調", "版面整潔"],
|
||||||
|
improvements: ["減少文字密度", "增加視覺元素"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "邏輯結構",
|
||||||
|
score: 6,
|
||||||
|
maxScore: 10,
|
||||||
|
weight: 20,
|
||||||
|
weightedScore: 12,
|
||||||
|
feedback: "邏輯架構清晰,各章節銜接自然,論述層次分明。",
|
||||||
|
strengths: ["邏輯清晰", "結構完整", "銜接自然"],
|
||||||
|
improvements: ["可增加總結回顧"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "創新性",
|
||||||
|
score: 8,
|
||||||
|
maxScore: 10,
|
||||||
|
weight: 15,
|
||||||
|
weightedScore: 12,
|
||||||
|
feedback: "內容具有一定創新性,但可以更大膽地提出獨特觀點和解決方案。",
|
||||||
|
strengths: ["思路新穎", "角度獨特"],
|
||||||
|
improvements: ["增加創新元素", "提出獨特見解"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "實用性",
|
||||||
|
score: 4,
|
||||||
|
maxScore: 10,
|
||||||
|
weight: 20,
|
||||||
|
weightedScore: 8,
|
||||||
|
feedback: "內容實用性有待提升,提供的解決方案需要更具可操作性。",
|
||||||
|
strengths: ["實用性強", "可操作性好", "價值明確"],
|
||||||
|
improvements: ["增加實施步驟"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 計算 mock 數據的 overview - 基於平均分作為閾值
|
||||||
|
const calculateMockOverview = (criteria: any[]) => {
|
||||||
|
if (!criteria || criteria.length === 0) {
|
||||||
|
return {
|
||||||
|
excellentItems: 0,
|
||||||
|
improvementItems: 0,
|
||||||
|
overallPerformance: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 計算所有項目的平均分數(不考慮權重)
|
||||||
|
const totalScore = criteria.reduce((sum, item) => sum + item.score, 0);
|
||||||
|
const averageScore = totalScore / criteria.length;
|
||||||
|
|
||||||
|
// 以平均分作為閾值
|
||||||
|
// ≥ 平均分 = 優秀項目,< 平均分 = 待改進項目
|
||||||
|
const excellentItems = criteria.filter(item => item.score >= averageScore).length;
|
||||||
|
const improvementItems = criteria.filter(item => item.score < averageScore).length;
|
||||||
|
|
||||||
|
// 整體表現:基於權重的加權平均分數
|
||||||
|
const overallPerformance = Math.round(criteria.reduce((sum, item) => sum + (item.score / item.maxScore) * item.weight, 0));
|
||||||
|
|
||||||
|
return {
|
||||||
|
excellentItems,
|
||||||
|
improvementItems,
|
||||||
|
overallPerformance
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockResults = {
|
||||||
|
projectTitle: "產品介紹簡報",
|
||||||
|
overallScore: 82,
|
||||||
|
totalPossible: 100,
|
||||||
|
grade: "B+",
|
||||||
|
analysisDate: "2024-01-15",
|
||||||
|
criteria: mockCriteria,
|
||||||
|
overview: calculateMockOverview(mockCriteria),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 圖表數據將在組件內部動態生成
|
||||||
|
|
||||||
|
const COLORS = ["#0891b2", "#6366f1", "#f59e0b", "#dc2626", "#10b981"]
|
||||||
|
|
||||||
|
export default function ResultsContent() {
|
||||||
|
const [activeTab, setActiveTab] = useState("overview")
|
||||||
|
const [evaluationData, setEvaluationData] = useState(null)
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [isShareModalOpen, setIsShareModalOpen] = useState(false)
|
||||||
|
const { toast } = useToast()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadEvaluationData = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
// 檢查是否有 URL 參數中的評審 ID
|
||||||
|
const evaluationId = searchParams.get('id')
|
||||||
|
|
||||||
|
if (evaluationId) {
|
||||||
|
// 從 API 獲取評審數據
|
||||||
|
console.log(`📊 從 API 獲取評審數據: ID=${evaluationId}`)
|
||||||
|
const response = await fetch(`/api/evaluation/${evaluationId}`)
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setEvaluationData(result.data)
|
||||||
|
console.log('✅ 成功載入評審數據:', result.data.projectTitle)
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || '獲取評審數據失敗')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 回退到從 localStorage 獲取評審結果
|
||||||
|
console.log('📊 從 localStorage 獲取評審結果')
|
||||||
|
const storedData = localStorage.getItem('evaluationResult')
|
||||||
|
if (storedData) {
|
||||||
|
const data = JSON.parse(storedData)
|
||||||
|
setEvaluationData(data)
|
||||||
|
console.log('✅ 成功載入 localStorage 數據')
|
||||||
|
} else {
|
||||||
|
throw new Error('沒有找到評審結果')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('載入評審結果失敗:', error)
|
||||||
|
setError((error as Error).message)
|
||||||
|
toast({
|
||||||
|
title: "載入失敗",
|
||||||
|
description: (error as Error).message || "無法載入評審結果,請重新進行評審",
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadEvaluationData()
|
||||||
|
}, [searchParams, toast])
|
||||||
|
|
||||||
|
// 如果正在載入,顯示載入狀態
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<Sidebar />
|
||||||
|
<main className="md:ml-64 p-6">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
|
||||||
|
<p className="text-muted-foreground">載入評審結果中...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果沒有數據或發生錯誤,顯示錯誤狀態
|
||||||
|
if (!evaluationData || error) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<Sidebar />
|
||||||
|
<main className="md:ml-64 p-6">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
{error ? `載入失敗: ${error}` : '沒有找到評審結果'}
|
||||||
|
</p>
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Button onClick={() => window.location.href = '/upload'}>
|
||||||
|
重新進行評審
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={() => window.location.href = '/history'}>
|
||||||
|
查看歷史記錄
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用真實數據或回退到模擬數據
|
||||||
|
const results = (evaluationData as any)?.evaluation?.fullData || evaluationData || mockResults
|
||||||
|
|
||||||
|
// 計算統計數據 - 基於 criteria_items 的平均分作為閾值
|
||||||
|
const calculateOverview = (criteria: any[]): any => {
|
||||||
|
if (!criteria || criteria.length === 0) {
|
||||||
|
return {
|
||||||
|
excellentItems: 0,
|
||||||
|
improvementItems: 0,
|
||||||
|
overallPerformance: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 計算所有項目的平均分數(不考慮權重)
|
||||||
|
const totalScore = criteria.reduce((sum, item) => sum + item.score, 0);
|
||||||
|
const averageScore = totalScore / criteria.length;
|
||||||
|
|
||||||
|
// 以平均分作為閾值
|
||||||
|
// ≥ 平均分 = 優秀項目,< 平均分 = 待改進項目
|
||||||
|
const excellentItems = criteria.filter(item => item.score >= averageScore).length;
|
||||||
|
const improvementItems = criteria.filter(item => item.score < averageScore).length;
|
||||||
|
|
||||||
|
// 整體表現:基於權重的加權平均分數
|
||||||
|
const overallPerformance = Math.round(criteria.reduce((sum, item) => sum + (item.score / item.maxScore) * item.weight, 0));
|
||||||
|
|
||||||
|
return {
|
||||||
|
excellentItems,
|
||||||
|
improvementItems,
|
||||||
|
overallPerformance
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 確保所有必要的數據結構存在
|
||||||
|
const safeResults = {
|
||||||
|
...results,
|
||||||
|
overview: results.overview || calculateOverview(results.criteria || []),
|
||||||
|
chartData: results.chartData || {
|
||||||
|
barChart: [],
|
||||||
|
pieChart: [],
|
||||||
|
radarChart: []
|
||||||
|
},
|
||||||
|
improvementSuggestions: results.improvementSuggestions || {
|
||||||
|
overallSuggestions: '暫無改進建議',
|
||||||
|
maintainStrengths: [],
|
||||||
|
keyImprovements: [],
|
||||||
|
actionPlan: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadReport = async () => {
|
||||||
|
try {
|
||||||
|
// 顯示載入提示
|
||||||
|
toast({
|
||||||
|
title: "報告下載中",
|
||||||
|
description: "評審報告 PDF 正在生成,請稍候...",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 獲取評審 ID
|
||||||
|
const evaluationId = searchParams.get('id');
|
||||||
|
|
||||||
|
if (!evaluationId) {
|
||||||
|
throw new Error('無法獲取評審 ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 調用下載 API
|
||||||
|
const response = await fetch(`/api/evaluation/${evaluationId}/download`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || '下載失敗');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取 PDF Blob
|
||||||
|
const pdfBlob = await response.blob();
|
||||||
|
|
||||||
|
// 創建下載連結
|
||||||
|
const url = window.URL.createObjectURL(pdfBlob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
|
||||||
|
// 從 Content-Disposition 標頭獲取檔案名稱,或使用預設名稱
|
||||||
|
const contentDisposition = response.headers.get('Content-Disposition');
|
||||||
|
let fileName = `評審報告_${safeResults.projectTitle}_${safeResults.analysisDate}.pdf`;
|
||||||
|
|
||||||
|
if (contentDisposition) {
|
||||||
|
const fileNameMatch = contentDisposition.match(/filename="(.+)"/);
|
||||||
|
if (fileNameMatch) {
|
||||||
|
fileName = decodeURIComponent(fileNameMatch[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
link.download = fileName;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
// 顯示成功提示
|
||||||
|
toast({
|
||||||
|
title: "下載完成",
|
||||||
|
description: "評審報告已成功下載",
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下載失敗:', error);
|
||||||
|
toast({
|
||||||
|
title: "下載失敗",
|
||||||
|
description: error instanceof Error ? error.message : '下載過程中發生錯誤',
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shareResults = () => {
|
||||||
|
setIsShareModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getScoreColor = (score: number, maxScore: number) => {
|
||||||
|
const percentage = (score / maxScore) * 100
|
||||||
|
if (percentage >= 90) return "text-green-600"
|
||||||
|
if (percentage >= 80) return "text-blue-600"
|
||||||
|
if (percentage >= 70) return "text-yellow-600"
|
||||||
|
if (percentage >= 60) return "text-orange-600"
|
||||||
|
return "text-red-600"
|
||||||
|
}
|
||||||
|
|
||||||
|
const getGradeColor = (grade: string) => {
|
||||||
|
if (grade.startsWith("A")) return "bg-green-100 text-green-800"
|
||||||
|
if (grade.startsWith("B")) return "bg-blue-100 text-blue-800"
|
||||||
|
if (grade.startsWith("C")) return "bg-yellow-100 text-yellow-800"
|
||||||
|
return "bg-red-100 text-red-800"
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<Sidebar />
|
||||||
|
|
||||||
|
<main className="md:ml-64 p-6">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-8 pt-8 md:pt-0">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-foreground mb-2 font-[var(--font-playfair)]">評審結果</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{safeResults.projectTitle} - 分析完成於 {safeResults.analysisDate}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={shareResults} variant="outline">
|
||||||
|
<Share2 className="h-4 w-4 mr-2" />
|
||||||
|
分享
|
||||||
|
</Button>
|
||||||
|
<Button onClick={downloadReport}>
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
下載報告
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Overall Score */}
|
||||||
|
<Card className="mb-8">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="grid md:grid-cols-4 gap-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-4xl font-bold text-primary mb-2">{safeResults.overallScore}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">總分 / {safeResults.totalPossible}</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<Badge className={`text-lg px-4 py-2 ${getGradeColor(safeResults.grade)}`}>{safeResults.grade}</Badge>
|
||||||
|
<div className="text-sm text-muted-foreground mt-2">等級評定</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="flex items-center justify-center mb-2">
|
||||||
|
<TrendingUp className="h-6 w-6 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">{safeResults.performanceStatus}</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="flex items-center justify-center mb-2">
|
||||||
|
{Array.from({ length: safeResults.recommendedStars || 0 }, (_, i) => (
|
||||||
|
<Star key={i} className="h-6 w-6 text-yellow-500 fill-current" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">推薦等級</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Detailed Results */}
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
|
<TabsList className="grid w-full grid-cols-4">
|
||||||
|
<TabsTrigger value="overview">總覽</TabsTrigger>
|
||||||
|
<TabsTrigger value="detailed">詳細分析</TabsTrigger>
|
||||||
|
<TabsTrigger value="charts">圖表分析</TabsTrigger>
|
||||||
|
<TabsTrigger value="suggestions">改進建議</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="overview" className="space-y-6">
|
||||||
|
{/* Score Breakdown */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>評分明細</CardTitle>
|
||||||
|
<CardDescription>各項評分標準的得分情況</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{safeResults.criteria.map((item: any, index: number) => (
|
||||||
|
<div key={index} className="flex items-center justify-between p-4 bg-muted rounded-lg">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<h4 className="font-medium">{item.name}</h4>
|
||||||
|
<Badge variant="outline">{item.weight}% 權重</Badge>
|
||||||
|
</div>
|
||||||
|
<Progress value={(item.score / item.maxScore) * 100} className="h-2" />
|
||||||
|
</div>
|
||||||
|
<div className="text-right ml-4">
|
||||||
|
<div className={`text-2xl font-bold ${getScoreColor(item.score, item.maxScore)}`}>
|
||||||
|
{item.score}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">/ {item.maxScore}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Quick Stats */}
|
||||||
|
<div className="grid md:grid-cols-3 gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6 text-center">
|
||||||
|
<CheckCircle className="h-8 w-8 text-green-600 mx-auto mb-2" />
|
||||||
|
<div className="text-2xl font-bold text-green-600 mb-1">{safeResults.overview.excellentItems}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">優秀項目</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6 text-center">
|
||||||
|
<AlertCircle className="h-8 w-8 text-yellow-600 mx-auto mb-2" />
|
||||||
|
<div className="text-2xl font-bold text-yellow-600 mb-1">{safeResults.overview.improvementItems}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">待改進項目</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6 text-center">
|
||||||
|
<TrendingUp className="h-8 w-8 text-blue-600 mx-auto mb-2" />
|
||||||
|
<div className="text-2xl font-bold text-blue-600 mb-1">{safeResults.overview.overallPerformance}%</div>
|
||||||
|
<div className="text-sm text-muted-foreground">整體表現</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="detailed" className="space-y-6">
|
||||||
|
{safeResults.criteria.map((item: any, index: number) => (
|
||||||
|
<Card key={index}>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
{item.name}
|
||||||
|
<Badge variant="outline">{item.weight}% 權重</Badge>
|
||||||
|
</CardTitle>
|
||||||
|
<div className={`text-2xl font-bold ${getScoreColor(item.score, item.maxScore)}`}>
|
||||||
|
{item.score}/{item.maxScore}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">AI 評語</h4>
|
||||||
|
<p className="text-muted-foreground leading-relaxed">{item.feedback}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2 text-green-700">優點</h4>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{item.strengths.map((strength: any, i: number) => (
|
||||||
|
<li key={i} className="flex items-center gap-2 text-sm">
|
||||||
|
<CheckCircle className="h-4 w-4 text-green-600" />
|
||||||
|
{strength}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2 text-orange-700">改進建議</h4>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{item.improvements.map((improvement: any, i: number) => (
|
||||||
|
<li key={i} className="flex items-center gap-2 text-sm">
|
||||||
|
<AlertCircle className="h-4 w-4 text-orange-600" />
|
||||||
|
{improvement}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="charts" className="space-y-6">
|
||||||
|
<div className="grid lg:grid-cols-2 gap-6">
|
||||||
|
{/* Bar Chart */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>各項目得分</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<BarChart data={safeResults.chartData.barChart}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" angle={-45} textAnchor="end" height={80} />
|
||||||
|
<YAxis domain={[0, 10]} />
|
||||||
|
<Tooltip />
|
||||||
|
<Bar dataKey="score" fill="#0891b2" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Pie Chart */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>權重分布</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={safeResults.chartData.pieChart}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
labelLine={false}
|
||||||
|
label={({ name, weight }) => `${name} (${weight}%)`}
|
||||||
|
outerRadius={80}
|
||||||
|
fill="#8884d8"
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{safeResults.chartData.pieChart.map((entry: any, index: number) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Radar Chart */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>能力雷達圖</CardTitle>
|
||||||
|
<CardDescription>各項能力的綜合表現分析</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ResponsiveContainer width="100%" height={400}>
|
||||||
|
<RadarChart data={safeResults.chartData.radarChart}>
|
||||||
|
<PolarGrid />
|
||||||
|
<PolarAngleAxis dataKey="subject" />
|
||||||
|
<PolarRadiusAxis domain={[0, 10]} />
|
||||||
|
<Radar name="得分" dataKey="score" stroke="#0891b2" fill="#0891b2" fillOpacity={0.3} />
|
||||||
|
</RadarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="suggestions" className="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>整體改進建議</CardTitle>
|
||||||
|
<CardDescription>{safeResults.improvementSuggestions.overallSuggestions}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-3 text-green-700">繼續保持的優勢</h4>
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
{safeResults.improvementSuggestions.maintainStrengths.map((strength: any, index: number) => (
|
||||||
|
<div key={index} className="p-4 bg-green-50 rounded-lg">
|
||||||
|
<h5 className="font-medium mb-2">{strength.title}</h5>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{strength.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-3 text-orange-700">重點改進方向</h4>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{safeResults.improvementSuggestions.keyImprovements.map((improvement: any, index: number) => (
|
||||||
|
<div key={index} className="p-4 bg-orange-50 rounded-lg">
|
||||||
|
<h5 className="font-medium mb-2">{improvement.title}</h5>
|
||||||
|
<p className="text-sm text-muted-foreground mb-3">{improvement.description}</p>
|
||||||
|
<ul className="text-sm space-y-1">
|
||||||
|
{improvement.suggestions.map((suggestion: any, sIndex: number) => (
|
||||||
|
<li key={sIndex}>• {suggestion}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-3 text-blue-700">下一步行動計劃</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{safeResults.improvementSuggestions.actionPlan.map((action: any, index: number) => (
|
||||||
|
<div key={index} className="flex items-start gap-3 p-3 bg-blue-50 rounded-lg">
|
||||||
|
<div className="w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-bold">
|
||||||
|
{index + 1}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 className="font-medium">{action.phase}</h5>
|
||||||
|
<p className="text-sm text-muted-foreground">{action.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Share Modal */}
|
||||||
|
<ShareModal
|
||||||
|
isOpen={isShareModalOpen}
|
||||||
|
onClose={() => setIsShareModalOpen(false)}
|
||||||
|
evaluationId={searchParams.get('id') || undefined}
|
||||||
|
projectTitle={safeResults.projectTitle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,7 +3,13 @@ import { dbConfig } from './config';
|
|||||||
|
|
||||||
// 建立連接池
|
// 建立連接池
|
||||||
const pool = mysql.createPool({
|
const pool = mysql.createPool({
|
||||||
...dbConfig,
|
host: dbConfig.host,
|
||||||
|
port: dbConfig.port,
|
||||||
|
user: dbConfig.user,
|
||||||
|
password: dbConfig.password,
|
||||||
|
database: dbConfig.database,
|
||||||
|
charset: dbConfig.charset,
|
||||||
|
timezone: dbConfig.timezone,
|
||||||
waitForConnections: true,
|
waitForConnections: true,
|
||||||
connectionLimit: 10,
|
connectionLimit: 10,
|
||||||
queueLimit: 0,
|
queueLimit: 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user