新增評分項目設定、資料庫整合

This commit is contained in:
2025-09-22 00:33:12 +08:00
parent 8de09129be
commit 9d4c586ad3
20 changed files with 2321 additions and 79 deletions

View File

@@ -1,6 +1,6 @@
"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, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
@@ -9,60 +9,93 @@ import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Slider } from "@/components/ui/slider"
import { Badge } from "@/components/ui/badge"
import { Plus, Trash2, Save, RotateCcw, FileText } from "lucide-react"
import { Plus, Trash2, Save, RotateCcw, FileText, Loader2 } from "lucide-react"
import { useToast } from "@/hooks/use-toast"
interface CriteriaItem {
id: string
id: number | string
name: string
description: string
weight: number
maxScore: number
}
const defaultCriteria: CriteriaItem[] = [
{
id: "1",
name: "內容品質",
description: "內容的準確性、完整性和專業度",
weight: 25,
maxScore: 10,
},
{
id: "2",
name: "視覺設計",
description: "版面設計、色彩搭配和視覺效果",
weight: 20,
maxScore: 10,
},
{
id: "3",
name: "邏輯結構",
description: "內容組織的邏輯性和條理性",
weight: 20,
maxScore: 10,
},
{
id: "4",
name: "創新性",
description: "創意思維和獨特觀點的展現",
weight: 15,
maxScore: 10,
},
{
id: "5",
name: "實用性",
description: "內容的實際應用價值和可操作性",
weight: 20,
maxScore: 10,
},
]
interface CriteriaTemplate {
id: number
name: string
description: string
total_weight: number
items: CriteriaItem[]
}
export default function CriteriaPage() {
const [criteria, setCriteria] = useState<CriteriaItem[]>(defaultCriteria)
const [templateName, setTemplateName] = useState("預設評分標準")
const [criteria, setCriteria] = useState<CriteriaItem[]>([])
const [templateName, setTemplateName] = useState("")
const [templateDescription, setTemplateDescription] = useState("")
const [isLoading, setIsLoading] = useState(true)
const [isSaving, setIsSaving] = useState(false)
const [currentTemplate, setCurrentTemplate] = useState<CriteriaTemplate | null>(null)
const { toast } = useToast()
// 載入評分標準
useEffect(() => {
loadTemplate()
}, [])
const loadTemplate = async () => {
try {
setIsLoading(true)
const response = await fetch('/api/criteria-templates')
const result = await response.json()
if (result.success && result.data && result.data.length > 0) {
// 載入第一個模板(單一模板模式)
const template = result.data[0]
setCurrentTemplate(template)
setTemplateName(template.name)
setTemplateDescription(template.description || '')
setCriteria(template.items || [])
} else {
// 如果沒有模板,載入預設模板
await loadDefaultTemplate()
}
} catch (error) {
console.error('載入評分標準失敗:', error)
// 如果載入失敗,嘗試載入預設模板
await loadDefaultTemplate()
} finally {
setIsLoading(false)
}
}
const loadDefaultTemplate = async () => {
try {
const response = await fetch('/api/criteria-templates/default')
const result = await response.json()
if (result.success && result.data) {
const template = result.data
setCurrentTemplate(template)
setTemplateName(template.name)
setTemplateDescription(template.description || '')
setCriteria(template.items || [])
} else {
// 如果連預設模板都沒有,使用空模板
setCurrentTemplate(null)
setTemplateName('')
setTemplateDescription('')
setCriteria([])
}
} catch (error) {
console.error('載入預設評分標準失敗:', error)
// 使用空模板
setCurrentTemplate(null)
setTemplateName('')
setTemplateDescription('')
setCriteria([])
}
}
const addCriteria = () => {
const newCriteria: CriteriaItem = {
id: Date.now().toString(),
@@ -83,12 +116,12 @@ export default function CriteriaPage() {
}
const updateWeight = (id: string, weight: number[]) => {
updateCriteria(id, "weight", weight[0])
updateCriteria(id, "weight", Number(weight[0]) || 0)
}
const totalWeight = criteria.reduce((sum, item) => sum + item.weight, 0)
const totalWeight = criteria.reduce((sum, item) => sum + (Number(item.weight) || 0), 0)
const saveCriteria = () => {
const saveCriteria = async () => {
if (totalWeight !== 100) {
toast({
title: "權重設定錯誤",
@@ -107,20 +140,98 @@ export default function CriteriaPage() {
return
}
// 這裡會連接到後端 API 儲存評分標準
toast({
title: "儲存成功",
description: "評分標準已成功儲存",
})
if (!templateName.trim()) {
toast({
title: "設定不完整",
description: "請填寫模板名稱",
variant: "destructive",
})
return
}
try {
setIsSaving(true)
const templateData = {
name: templateName.trim(),
description: templateDescription.trim(),
items: criteria.map(item => ({
name: item.name.trim(),
description: item.description.trim(),
weight: item.weight,
maxScore: item.maxScore
}))
}
// 單一模板模式,總是使用 POST 進行覆蓋
const url = '/api/criteria-templates'
const method = 'POST'
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(templateData),
})
const result = await response.json()
if (result.success) {
toast({
title: "儲存成功",
description: "評分標準已成功儲存",
})
// 重新載入資料
await loadTemplate()
} else {
throw new Error(result.error || '儲存失敗')
}
} catch (error) {
console.error('儲存評分標準失敗:', error)
toast({
title: "儲存失敗",
description: error instanceof Error ? error.message : "無法儲存評分標準",
variant: "destructive",
})
} finally {
setIsSaving(false)
}
}
const resetToDefault = () => {
setCriteria(defaultCriteria)
setTemplateName("預設評分標準")
toast({
title: "已重置",
description: "評分標準已重置為預設值",
})
const resetToDefault = async () => {
try {
await loadDefaultTemplate()
toast({
title: "已重置",
description: "評分標準已重置為預設值",
})
} catch (error) {
toast({
title: "重置失敗",
description: "無法重置評分標準",
variant: "destructive",
})
}
}
if (isLoading) {
return (
<div className="min-h-screen bg-background">
<Sidebar />
<main className="md:ml-64 p-6">
<div className="max-w-4xl mx-auto">
<div className="flex items-center justify-center py-12">
<div className="text-center">
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4" />
<p className="text-muted-foreground">...</p>
</div>
</div>
</div>
</main>
</div>
)
}
return (
@@ -140,16 +251,29 @@ export default function CriteriaPage() {
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
</CardTitle>
</CardHeader>
<CardContent>
<Input
value={templateName}
onChange={(e) => setTemplateName(e.target.value)}
placeholder="輸入評分標準名稱"
className="max-w-md"
/>
<CardContent className="space-y-4">
<div>
<Label htmlFor="template-name" className="mb-2 block"> *</Label>
<Input
id="template-name"
value={templateName}
onChange={(e) => setTemplateName(e.target.value)}
placeholder="輸入評分標準名稱"
/>
</div>
<div>
<Label htmlFor="template-description" className="mb-2 block"></Label>
<Textarea
id="template-description"
value={templateDescription}
onChange={(e) => setTemplateDescription(e.target.value)}
placeholder="輸入模板描述(選填)"
rows={2}
/>
</div>
</CardContent>
</Card>
@@ -158,7 +282,9 @@ export default function CriteriaPage() {
<CardContent className="pt-6">
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium"></span>
<Badge variant={totalWeight === 100 ? "default" : "destructive"}>{totalWeight}%</Badge>
<Badge variant={totalWeight === 100 ? "default" : "destructive"}>
{Number(totalWeight).toFixed(1)}%
</Badge>
</div>
<div className="w-full bg-muted rounded-full h-2">
<div
@@ -190,7 +316,7 @@ export default function CriteriaPage() {
<CardContent className="space-y-4">
<div className="grid md:grid-cols-2 gap-4">
<div>
<Label htmlFor={`name-${item.id}`}></Label>
<Label htmlFor={`name-${item.id}`} className="mb-2 block"></Label>
<Input
id={`name-${item.id}`}
value={item.name}
@@ -199,7 +325,7 @@ export default function CriteriaPage() {
/>
</div>
<div>
<Label htmlFor={`maxScore-${item.id}`}>滿</Label>
<Label htmlFor={`maxScore-${item.id}`} className="mb-2 block">滿</Label>
<Input
id={`maxScore-${item.id}`}
type="number"
@@ -212,7 +338,7 @@ export default function CriteriaPage() {
</div>
<div>
<Label htmlFor={`description-${item.id}`}></Label>
<Label htmlFor={`description-${item.id}`} className="mb-2 block"></Label>
<Textarea
id={`description-${item.id}`}
value={item.description}
@@ -225,10 +351,10 @@ export default function CriteriaPage() {
<div>
<div className="flex items-center justify-between mb-2">
<Label></Label>
<Badge variant="outline">{item.weight}%</Badge>
<Badge variant="outline">{Number(item.weight).toFixed(1)}%</Badge>
</div>
<Slider
value={[item.weight]}
value={[Number(item.weight) || 0]}
onValueChange={(value) => updateWeight(item.id, value)}
max={100}
min={0}
@@ -253,11 +379,20 @@ export default function CriteriaPage() {
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-4">
<Button onClick={saveCriteria} className="flex-1">
<Save className="h-4 w-4 mr-2" />
<Button onClick={saveCriteria} disabled={isSaving || isLoading} className="flex-1">
{isSaving ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
...
</>
) : (
<>
<Save className="h-4 w-4 mr-2" />
</>
)}
</Button>
<Button onClick={resetToDefault} variant="outline">
<Button onClick={resetToDefault} variant="outline" disabled={isLoading || isSaving}>
<RotateCcw className="h-4 w-4 mr-2" />
</Button>
@@ -278,7 +413,7 @@ export default function CriteriaPage() {
{item.description && <div className="text-sm text-muted-foreground mt-1">{item.description}</div>}
</div>
<div className="text-right">
<div className="font-medium">: {item.weight}%</div>
<div className="font-medium">: {Number(item.weight).toFixed(1)}%</div>
<div className="text-sm text-muted-foreground">滿: {item.maxScore}</div>
</div>
</div>