完成評審評分機制

This commit is contained in:
2025-09-18 18:34:31 +08:00
parent 2101767690
commit ffa1e45f63
54 changed files with 5730 additions and 709 deletions

View File

@@ -42,67 +42,164 @@ export default function JudgeScoringPage() {
const [error, setError] = useState("")
const [success, setSuccess] = useState("")
const [showAccessCode, setShowAccessCode] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [competitionRules, setCompetitionRules] = useState<any[]>([])
// Judge data - empty for production
const mockJudges: Judge[] = []
// Scoring items - empty for production
const mockScoringItems: ScoringItem[] = []
const handleLogin = () => {
const handleLogin = async () => {
setError("")
setIsLoading(true)
if (!judgeId.trim() || !accessCode.trim()) {
setError("請填寫評審ID和存取碼")
setIsLoading(false)
return
}
if (accessCode !== "judge2024") {
setError("存取碼錯誤")
setIsLoading(false)
return
}
const judge = mockJudges.find(j => j.id === judgeId)
if (!judge) {
setError("評審ID不存在")
return
try {
// 獲取評審的評分任務
const response = await fetch(`/api/judge/scoring-tasks?judgeId=${judgeId}`)
const data = await response.json()
if (data.success) {
setCurrentJudge(data.data.judge)
setScoringItems(data.data.tasks)
setIsLoggedIn(true)
setSuccess("登入成功!")
setTimeout(() => setSuccess(""), 3000)
// 載入競賽規則
await loadCompetitionRules()
} else {
setError(data.message || "登入失敗")
}
} catch (err) {
console.error('登入失敗:', err)
setError("登入失敗,請重試")
} finally {
setIsLoading(false)
}
setCurrentJudge(judge)
setScoringItems(mockScoringItems)
setIsLoggedIn(true)
setSuccess("登入成功!")
setTimeout(() => setSuccess(""), 3000)
}
const handleStartScoring = (item: ScoringItem) => {
const loadCompetitionRules = async () => {
try {
// 使用正確的競賽ID
const response = await fetch('/api/competitions/be47d842-91f1-11f0-8595-bd825523ae01/rules')
const data = await response.json()
if (data.success) {
setCompetitionRules(data.data)
}
} catch (err) {
console.error('載入競賽規則失敗:', err)
}
}
const handleStartScoring = async (item: ScoringItem) => {
setSelectedItem(item)
setScores({})
setComments("")
// 如果是重新評分,嘗試載入現有的評分數據
if (item.status === "completed") {
try {
// 這裡可以添加載入現有評分數據的邏輯
// 暫時使用默認值
const initialScores: Record<string, number> = {}
if (competitionRules && competitionRules.length > 0) {
competitionRules.forEach((rule: any) => {
initialScores[rule.name] = 0
})
} else {
initialScores.innovation = 0
initialScores.technical = 0
initialScores.usability = 0
initialScores.presentation = 0
initialScores.impact = 0
}
setScores(initialScores)
setComments("")
} catch (err) {
console.error('載入現有評分數據失敗:', err)
}
} else {
// 新評分初始化為0
const initialScores: Record<string, number> = {}
if (competitionRules && competitionRules.length > 0) {
competitionRules.forEach((rule: any) => {
initialScores[rule.name] = 0
})
} else {
initialScores.innovation = 0
initialScores.technical = 0
initialScores.usability = 0
initialScores.presentation = 0
initialScores.impact = 0
}
setScores(initialScores)
setComments("")
}
setShowScoringDialog(true)
}
const handleSubmitScore = async () => {
if (!selectedItem) return
if (!selectedItem || !currentJudge) return
setIsSubmitting(true)
// 模擬提交評分
setTimeout(() => {
setScoringItems(prev => prev.map(item =>
item.id === selectedItem.id
? { ...item, status: "completed", score: Object.values(scores).reduce((a, b) => a + b, 0) / Object.values(scores).length, submittedAt: new Date().toISOString() }
: item
))
try {
// 計算總分 (1-10分制轉換為100分制)
const totalScore = (Object.values(scores).reduce((a, b) => a + b, 0) / Object.values(scores).length) * 10
setShowScoringDialog(false)
setSelectedItem(null)
setScores({})
setComments("")
// 提交評分到 API
const response = await fetch('/api/admin/scoring', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
judgeId: currentJudge.id,
participantId: selectedItem.id,
participantType: 'app',
scores: scores,
comments: comments.trim(),
competitionId: 'be47d842-91f1-11f0-8595-bd825523ae01', // 正確的競賽ID
isEdit: selectedItem.status === "completed", // 如果是重新評分,標記為編輯模式
recordId: selectedItem.status === "completed" ? selectedItem.id : null
})
})
const data = await response.json()
if (data.success) {
// 更新本地狀態
setScoringItems(prev => prev.map(item =>
item.id === selectedItem.id
? { ...item, status: "completed", score: totalScore, submittedAt: new Date().toISOString() }
: item
))
setShowScoringDialog(false)
setSelectedItem(null)
setScores({})
setComments("")
setSuccess("評分提交成功!")
setTimeout(() => setSuccess(""), 3000)
} else {
setError(data.message || "評分提交失敗")
}
} catch (err) {
console.error('評分提交失敗:', err)
setError("評分提交失敗,請重試")
} finally {
setIsSubmitting(false)
setSuccess("評分提交成功!")
setTimeout(() => setSuccess(""), 3000)
}, 1000)
}
}
const getProgress = () => {
@@ -111,6 +208,23 @@ export default function JudgeScoringPage() {
return { total, completed, percentage: total > 0 ? Math.round((completed / total) * 100) : 0 }
}
const isFormValid = () => {
// 檢查所有評分項目是否都已評分
const rules = competitionRules && competitionRules.length > 0 ? competitionRules : [
{ name: "創新性" }, { name: "技術性" }, { name: "實用性" },
{ name: "展示效果" }, { name: "影響力" }
]
const allScoresFilled = rules.every((rule: any) =>
scores[rule.name] && scores[rule.name] > 0
)
// 檢查評審意見是否填寫
const commentsFilled = comments.trim().length > 0
return allScoresFilled && commentsFilled
}
const progress = getProgress()
if (!isLoggedIn) {
@@ -170,9 +284,19 @@ export default function JudgeScoringPage() {
onClick={handleLogin}
className="w-full"
size="lg"
disabled={isLoading}
>
<LogIn className="w-4 h-4 mr-2" />
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
<>
<LogIn className="w-4 h-4 mr-2" />
</>
)}
</Button>
<div className="text-center text-sm text-gray-500">
@@ -268,7 +392,7 @@ export default function JudgeScoringPage() {
<User className="w-4 h-4 text-green-600" />
</div>
)}
<span className="font-medium">{item.name}</span>
<span className="font-medium">{item.display_name || item.name}</span>
<Badge variant="outline">
{item.type === "individual" ? "個人" : "團隊"}
</Badge>
@@ -277,10 +401,21 @@ export default function JudgeScoringPage() {
<div className="flex items-center space-x-4">
{item.status === "completed" ? (
<div className="text-center">
<div className="text-lg font-bold text-green-600">{item.score}</div>
<div className="text-xs text-gray-500">/ 10</div>
<div className="text-xs text-gray-500">{item.submittedAt}</div>
<div className="flex items-center space-x-3">
<div className="text-center">
<div className="text-lg font-bold text-green-600">{item.score}</div>
<div className="text-xs text-gray-500">/ 100</div>
<div className="text-xs text-gray-500">
{item.submittedAt ? new Date(item.submittedAt).toLocaleDateString('zh-TW') : ''}
</div>
</div>
<Button
onClick={() => handleStartScoring(item)}
variant="outline"
size="sm"
>
</Button>
</div>
) : (
<Button
@@ -313,15 +448,18 @@ export default function JudgeScoringPage() {
{/* 評分項目 */}
<div className="space-y-4">
<h3 className="text-lg font-semibold"></h3>
{[
{(competitionRules && competitionRules.length > 0 ? competitionRules : [
{ name: "創新性", description: "創新程度和獨特性" },
{ name: "技術性", description: "技術實現的複雜度和品質" },
{ name: "實用性", description: "實際應用價值和用戶體驗" },
{ name: "展示效果", description: "展示的清晰度和吸引力" },
{ name: "影響力", description: "對行業或社會的潛在影響" }
].map((criterion, index) => (
]).map((criterion, index) => (
<div key={index} className="space-y-2">
<Label>{criterion.name}</Label>
<Label className="flex items-center space-x-1">
<span>{criterion.name}</span>
<span className="text-red-500">*</span>
</Label>
<p className="text-sm text-gray-600">{criterion.description}</p>
<div className="flex space-x-2">
{Array.from({ length: 10 }, (_, i) => i + 1).map((score) => (
@@ -339,19 +477,29 @@ export default function JudgeScoringPage() {
</button>
))}
</div>
{!scores[criterion.name] && (
<p className="text-xs text-red-500"></p>
)}
</div>
))}
</div>
{/* 評審意見 */}
<div className="space-y-2">
<Label></Label>
<Label className="flex items-center space-x-1">
<span></span>
<span className="text-red-500">*</span>
</Label>
<Textarea
placeholder="請詳細填寫評審意見、優點分析、改進建議等..."
value={comments}
onChange={(e) => setComments(e.target.value)}
rows={4}
className={!comments.trim() ? "border-red-300" : ""}
/>
{!comments.trim() && (
<p className="text-xs text-red-500"></p>
)}
</div>
{/* 總分顯示 */}
@@ -360,9 +508,9 @@ export default function JudgeScoringPage() {
<span className="font-semibold"></span>
<span className="text-2xl font-bold text-blue-600">
{Object.values(scores).length > 0
? Math.round(Object.values(scores).reduce((a, b) => a + b, 0) / Object.values(scores).length)
? Math.round(Object.values(scores).reduce((a, b) => a + b, 0) / Object.values(scores).length * 10)
: 0
} / 10
} / 100
</span>
</div>
</div>
@@ -377,7 +525,7 @@ export default function JudgeScoringPage() {
</Button>
<Button
onClick={handleSubmitScore}
disabled={isSubmitting || Object.keys(scores).length < 5 || !comments.trim()}
disabled={isSubmitting || !isFormValid()}
>
{isSubmitting ? (
<>