新增歷史履歷
This commit is contained in:
99
app/api/history/route.ts
Normal file
99
app/api/history/route.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { ProjectService, EvaluationService, ProjectFileService } from '@/lib/services/database';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
console.log('📊 開始獲取歷史記錄數據...');
|
||||
|
||||
// 獲取所有專案及其評審結果
|
||||
const projects = await ProjectService.getAll();
|
||||
console.log(`📋 找到 ${projects.length} 個專案`);
|
||||
|
||||
const historyData = [];
|
||||
|
||||
for (const project of projects) {
|
||||
// 獲取該專案的最新評審結果
|
||||
const latestEvaluation = await EvaluationService.findByProjectId(project.id);
|
||||
|
||||
// 獲取該專案的文件信息
|
||||
const projectFiles = await ProjectFileService.findByProjectId(project.id);
|
||||
|
||||
// 判斷文件類型
|
||||
let fileType = 'Unknown';
|
||||
if (projectFiles.length > 0) {
|
||||
const fileTypeFromDB = projectFiles[0].file_type;
|
||||
if (fileTypeFromDB) {
|
||||
// 根據 file_type 判斷文件類型
|
||||
if (fileTypeFromDB.toLowerCase().includes('ppt') || fileTypeFromDB.toLowerCase().includes('powerpoint')) {
|
||||
fileType = 'PPT';
|
||||
} else if (fileTypeFromDB.toLowerCase().includes('mp4') || fileTypeFromDB.toLowerCase().includes('avi') || fileTypeFromDB.toLowerCase().includes('mov')) {
|
||||
fileType = 'Video';
|
||||
} else if (fileTypeFromDB.toLowerCase().includes('pdf')) {
|
||||
fileType = 'PDF';
|
||||
} else if (fileTypeFromDB.toLowerCase().includes('html') || fileTypeFromDB.toLowerCase().includes('htm')) {
|
||||
fileType = 'Website';
|
||||
} else {
|
||||
// 如果無法從 file_type 判斷,嘗試從檔名判斷
|
||||
const fileName = projectFiles[0].original_name.toLowerCase();
|
||||
if (fileName.includes('.ppt') || fileName.includes('.pptx')) {
|
||||
fileType = 'PPT';
|
||||
} else if (fileName.includes('.mp4') || fileName.includes('.avi') || fileName.includes('.mov')) {
|
||||
fileType = 'Video';
|
||||
} else if (fileName.includes('.pdf')) {
|
||||
fileType = 'PDF';
|
||||
} else if (fileName.includes('.html') || fileName.includes('.htm')) {
|
||||
fileType = 'Website';
|
||||
} else {
|
||||
fileType = fileTypeFromDB.toUpperCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果沒有文件記錄,嘗試從專案標題判斷
|
||||
if (project.title.includes('PPT') || project.title.includes('簡報')) {
|
||||
fileType = 'PPT';
|
||||
} else if (project.title.includes('網站') || project.title.includes('Website')) {
|
||||
fileType = 'Website';
|
||||
} else if (project.title.includes('影片') || project.title.includes('Video')) {
|
||||
fileType = 'Video';
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date: Date) => {
|
||||
return new Date(date).toISOString().split('T')[0];
|
||||
};
|
||||
|
||||
historyData.push({
|
||||
id: project.id.toString(),
|
||||
title: project.title,
|
||||
type: fileType,
|
||||
score: latestEvaluation?.overall_score || 0,
|
||||
grade: latestEvaluation?.grade || '-',
|
||||
date: formatDate(project.created_at),
|
||||
status: project.status === 'completed' ? 'completed' : 'processing',
|
||||
evaluation_id: latestEvaluation?.id || null,
|
||||
description: project.description || '',
|
||||
created_at: project.created_at,
|
||||
updated_at: project.updated_at
|
||||
});
|
||||
}
|
||||
|
||||
// 按創建時間倒序排列
|
||||
historyData.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
||||
|
||||
console.log(`✅ 成功獲取 ${historyData.length} 條歷史記錄`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: historyData
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 獲取歷史記錄失敗:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '獲取歷史記錄失敗' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
56
app/api/history/stats/route.ts
Normal file
56
app/api/history/stats/route.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { ProjectService, EvaluationService } from '@/lib/services/database';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
console.log('📊 開始獲取統計數據...');
|
||||
|
||||
// 獲取所有專案
|
||||
const projects = await ProjectService.getAll();
|
||||
console.log(`📋 找到 ${projects.length} 個專案`);
|
||||
|
||||
// 計算統計數據
|
||||
const totalProjects = projects.length;
|
||||
const completedProjects = projects.filter(p => p.status === 'completed').length;
|
||||
const processingProjects = projects.filter(p => p.status === 'analyzing' || p.status === 'pending').length;
|
||||
|
||||
// 計算平均分數
|
||||
let totalScore = 0;
|
||||
let scoredProjects = 0;
|
||||
|
||||
for (const project of projects) {
|
||||
if (project.status === 'completed') {
|
||||
const evaluation = await EvaluationService.findByProjectId(project.id);
|
||||
console.log(`專案 ${project.id} (${project.title}): 狀態=${project.status}, 評審=${evaluation ? '有' : '無'}, 分數=${evaluation?.overall_score || '無'}`);
|
||||
if (evaluation && evaluation.overall_score) {
|
||||
totalScore += evaluation.overall_score;
|
||||
scoredProjects++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const averageScore = scoredProjects > 0 ? Math.round(totalScore / scoredProjects) : 0;
|
||||
console.log(`平均分數計算: 總分=${totalScore}, 有分數的專案數=${scoredProjects}, 平均分數=${averageScore}`);
|
||||
|
||||
const stats = {
|
||||
totalProjects,
|
||||
completedProjects,
|
||||
processingProjects,
|
||||
averageScore
|
||||
};
|
||||
|
||||
console.log('✅ 統計數據:', stats);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: stats
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 獲取統計數據失敗:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '獲取統計數據失敗' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,95 +1,115 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { Sidebar } from "@/components/sidebar"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { FileText, Calendar, Search, Eye, Download, Trash2 } from "lucide-react"
|
||||
import { FileText, Calendar, Search, Eye, Download, Trash2, Loader2 } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
// 模擬歷史記錄數據
|
||||
const mockHistory = [
|
||||
{
|
||||
id: "1",
|
||||
title: "產品介紹簡報",
|
||||
type: "PPT",
|
||||
score: 82,
|
||||
grade: "B+",
|
||||
date: "2024-01-15",
|
||||
status: "completed",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "市場分析報告",
|
||||
type: "Website",
|
||||
score: 76,
|
||||
grade: "B",
|
||||
date: "2024-01-12",
|
||||
status: "completed",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "產品演示影片",
|
||||
type: "Video",
|
||||
score: 88,
|
||||
grade: "A-",
|
||||
date: "2024-01-10",
|
||||
status: "completed",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
title: "技術架構說明",
|
||||
type: "PPT",
|
||||
score: 0,
|
||||
grade: "-",
|
||||
date: "2024-01-08",
|
||||
status: "processing",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
title: "用戶體驗設計",
|
||||
type: "Website",
|
||||
score: 91,
|
||||
grade: "A",
|
||||
date: "2024-01-05",
|
||||
status: "completed",
|
||||
},
|
||||
]
|
||||
// 歷史記錄數據類型
|
||||
interface HistoryItem {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
score: number;
|
||||
grade: string;
|
||||
date: string;
|
||||
status: 'completed' | 'processing';
|
||||
evaluation_id?: number;
|
||||
description?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 統計數據類型
|
||||
interface StatsData {
|
||||
totalProjects: number;
|
||||
completedProjects: number;
|
||||
processingProjects: number;
|
||||
averageScore: number;
|
||||
}
|
||||
|
||||
export default function HistoryPage() {
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [filterType, setFilterType] = useState("all")
|
||||
const [filterStatus, setFilterStatus] = useState("all")
|
||||
|
||||
const filteredHistory = mockHistory.filter((item) => {
|
||||
const matchesSearch = item.title.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
const matchesType = filterType === "all" || item.type === filterType
|
||||
const matchesStatus = filterStatus === "all" || item.status === filterStatus
|
||||
return matchesSearch && matchesType && matchesStatus
|
||||
const [historyData, setHistoryData] = useState<HistoryItem[]>([])
|
||||
const [statsData, setStatsData] = useState<StatsData>({
|
||||
totalProjects: 0,
|
||||
completedProjects: 0,
|
||||
processingProjects: 0,
|
||||
averageScore: 0
|
||||
})
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// 載入歷史記錄數據
|
||||
useEffect(() => {
|
||||
const loadHistoryData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 並行獲取歷史記錄和統計數據
|
||||
const [historyResponse, statsResponse] = await Promise.all([
|
||||
fetch('/api/history'),
|
||||
fetch('/api/history/stats')
|
||||
]);
|
||||
|
||||
if (!historyResponse.ok || !statsResponse.ok) {
|
||||
throw new Error('獲取數據失敗');
|
||||
}
|
||||
|
||||
const historyResult = await historyResponse.json();
|
||||
const statsResult = await statsResponse.json();
|
||||
|
||||
if (historyResult.success && statsResult.success) {
|
||||
setHistoryData(historyResult.data);
|
||||
setStatsData(statsResult.data);
|
||||
} else {
|
||||
throw new Error(historyResult.error || statsResult.error || '數據載入失敗');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('載入歷史記錄失敗:', err);
|
||||
setError(err instanceof Error ? err.message : '載入數據時發生錯誤');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadHistoryData();
|
||||
}, []);
|
||||
|
||||
const filteredHistory = historyData.filter((item) => {
|
||||
const matchesSearch = item.title.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesType = filterType === "all" || item.type === filterType;
|
||||
const matchesStatus = filterStatus === "all" || item.status === filterStatus;
|
||||
return matchesSearch && matchesType && matchesStatus;
|
||||
});
|
||||
|
||||
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-gray-100 text-gray-800"
|
||||
}
|
||||
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-gray-100 text-gray-800";
|
||||
};
|
||||
|
||||
const getTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "PPT":
|
||||
return <FileText className="h-4 w-4" />
|
||||
return <FileText className="h-4 w-4" />;
|
||||
case "Video":
|
||||
return <FileText className="h-4 w-4" />
|
||||
return <FileText className="h-4 w-4" />;
|
||||
case "Website":
|
||||
return <FileText className="h-4 w-4" />
|
||||
return <FileText className="h-4 w-4" />;
|
||||
default:
|
||||
return <FileText className="h-4 w-4" />
|
||||
}
|
||||
return <FileText className="h-4 w-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
@@ -147,14 +167,14 @@ export default function HistoryPage() {
|
||||
<div className="grid md:grid-cols-4 gap-4 mb-6">
|
||||
<Card>
|
||||
<CardContent className="pt-6 text-center">
|
||||
<div className="text-2xl font-bold text-primary mb-1">{mockHistory.length}</div>
|
||||
<div className="text-2xl font-bold text-primary mb-1">{statsData.totalProjects}</div>
|
||||
<div className="text-sm text-muted-foreground">總評審數</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6 text-center">
|
||||
<div className="text-2xl font-bold text-green-600 mb-1">
|
||||
{mockHistory.filter((item) => item.status === "completed").length}
|
||||
{statsData.completedProjects}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">已完成</div>
|
||||
</CardContent>
|
||||
@@ -162,10 +182,7 @@ export default function HistoryPage() {
|
||||
<Card>
|
||||
<CardContent className="pt-6 text-center">
|
||||
<div className="text-2xl font-bold text-blue-600 mb-1">
|
||||
{Math.round(
|
||||
mockHistory.filter((item) => item.score > 0).reduce((sum, item) => sum + item.score, 0) /
|
||||
mockHistory.filter((item) => item.score > 0).length,
|
||||
)}
|
||||
{statsData.averageScore}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">平均分數</div>
|
||||
</CardContent>
|
||||
@@ -173,14 +190,40 @@ export default function HistoryPage() {
|
||||
<Card>
|
||||
<CardContent className="pt-6 text-center">
|
||||
<div className="text-2xl font-bold text-yellow-600 mb-1">
|
||||
{mockHistory.filter((item) => item.status === "processing").length}
|
||||
{statsData.processingProjects}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">處理中</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Loading State */}
|
||||
{loading && (
|
||||
<Card>
|
||||
<CardContent className="pt-6 text-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary mx-auto mb-4" />
|
||||
<h3 className="font-medium mb-2">載入歷史記錄中...</h3>
|
||||
<p className="text-muted-foreground">請稍候片刻</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Error State */}
|
||||
{error && (
|
||||
<Card>
|
||||
<CardContent className="pt-6 text-center py-12">
|
||||
<FileText className="h-12 w-12 text-destructive mx-auto mb-4" />
|
||||
<h3 className="font-medium mb-2 text-destructive">載入失敗</h3>
|
||||
<p className="text-muted-foreground mb-4">{error}</p>
|
||||
<Button onClick={() => window.location.reload()}>
|
||||
重新載入
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* History List */}
|
||||
{!loading && !error && (
|
||||
<div className="space-y-4">
|
||||
{filteredHistory.map((item) => (
|
||||
<Card key={item.id} className="hover:shadow-md transition-shadow">
|
||||
@@ -239,7 +282,6 @@ export default function HistoryPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredHistory.length === 0 && (
|
||||
<Card>
|
||||
@@ -254,6 +296,8 @@ export default function HistoryPage() {
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
|
@@ -164,7 +164,7 @@ export class CriteriaItemService {
|
||||
items = [];
|
||||
}
|
||||
// 手動排序項目
|
||||
items.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0));
|
||||
items.sort((a: any, b: any) => (a.sort_order || 0) - (b.sort_order || 0));
|
||||
} catch (error) {
|
||||
console.error('解析 items JSON 失敗:', error);
|
||||
console.error('原始 items 數據:', row.items);
|
||||
@@ -190,7 +190,7 @@ export class CriteriaItemService {
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
weight: Number(row.weight) || 0,
|
||||
maxScore: Number(row.max_score) || 10, // 映射 max_score 到 maxScore 並轉換為數字
|
||||
max_score: Number(row.max_score) || 10,
|
||||
sort_order: row.sort_order,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at
|
||||
@@ -240,6 +240,14 @@ export class ProjectService {
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
}
|
||||
|
||||
static async getAll(): Promise<Project[]> {
|
||||
const sql = `
|
||||
SELECT * FROM projects
|
||||
ORDER BY created_at DESC
|
||||
`;
|
||||
return await query(sql, []) as Project[];
|
||||
}
|
||||
|
||||
static async findByUserId(userId: number, limit = 20, offset = 0): Promise<Project[]> {
|
||||
const sql = `
|
||||
SELECT * FROM projects
|
||||
@@ -266,7 +274,7 @@ export class ProjectService {
|
||||
template,
|
||||
files,
|
||||
websites,
|
||||
evaluation,
|
||||
evaluation: evaluation || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user