實現完整的後台得獎資訊

This commit is contained in:
2025-09-27 02:42:22 +08:00
parent 2597a07514
commit b06fd94c99
8 changed files with 302 additions and 111 deletions

View File

@@ -102,6 +102,11 @@ export function CompetitionManagement() {
// 可用用戶狀態
const [availableUsers, setAvailableUsers] = useState<any[]>([])
const [isLoadingUsers, setIsLoadingUsers] = useState(false)
// 獎項資料庫整合狀態
const [dbAwards, setDbAwards] = useState<any[]>([])
const [isLoadingAwards, setIsLoadingAwards] = useState(false)
const [awardStats, setAwardStats] = useState<any>(null)
const [showCreateCompetition, setShowCreateCompetition] = useState(false)
const [showAddJudge, setShowAddJudge] = useState(false)
@@ -704,6 +709,7 @@ export function CompetitionManagement() {
fetchTeams()
fetchTeamStats()
fetchAvailableApps()
loadAwardsData()
}, [])
// 當競賽列表載入完成後,載入每個競賽的評分進度
@@ -1980,6 +1986,15 @@ export function CompetitionManagement() {
}
const handleViewAward = async (award: any) => {
console.log('🎯 handleViewAward 被調用:', {
award: award ? {
id: award.id,
competitionId: award.competitionId,
awardName: award.award_name,
hasCompetitionId: !!award.competitionId
} : null
});
setSelectedAward(award)
setShowAwardDetail(true)
@@ -1992,6 +2007,7 @@ export function CompetitionManagement() {
if (data.success) {
console.log('✅ 獲取到評審團:', data.data.length, '位');
console.log('👥 評審團詳細資料:', data.data);
setCompetitionJudges(data.data);
} else {
console.error('❌ 獲取評審團失敗:', data.message);
@@ -2001,6 +2017,8 @@ export function CompetitionManagement() {
console.error('❌ 載入評審團失敗:', error);
setCompetitionJudges([]);
}
} else {
console.log('❌ 獎項沒有 competitionId無法載入評審團');
}
}
@@ -2080,6 +2098,41 @@ export function CompetitionManagement() {
setShowManualScoring(true)
}
// 載入獎項資料
const loadAwardsData = async () => {
setIsLoadingAwards(true)
try {
console.log('🔍 開始載入獎項資料...')
const response = await fetch('/api/admin/awards')
const data = await response.json()
if (data.success) {
console.log('✅ 獎項資料載入成功:', data.data.length, '個獎項')
setDbAwards(data.data)
// 計算獎項統計
const stats = {
total: data.data.length,
top3: data.data.filter((award: any) => award.rank && award.rank <= 3).length,
popular: data.data.filter((award: any) => award.award_type === 'popular').length,
competitions: [...new Set(data.data.map((award: any) => award.competition_id))].length
}
setAwardStats(stats)
console.log('📊 獎項統計:', stats)
} else {
console.error('❌ 獎項資料載入失敗:', data.message)
setDbAwards([])
setAwardStats({ total: 0, top3: 0, popular: 0, competitions: 0 })
}
} catch (error) {
console.error('❌ 載入獎項資料失敗:', error)
setDbAwards([])
setAwardStats({ total: 0, top3: 0, popular: 0, competitions: 0 })
} finally {
setIsLoadingAwards(false)
}
}
// 載入競賽相關數據用於評分
const loadCompetitionDataForScoring = async (competition: any) => {
try {
@@ -2735,7 +2788,8 @@ export function CompetitionManagement() {
// 获取筛选后的奖项
const getFilteredAwards = () => {
let filteredAwards = [...awards]
// 使用資料庫中的獎項資料,如果沒有則使用 context 中的資料作為備用
let filteredAwards = dbAwards.length > 0 ? [...dbAwards] : [...awards]
// 搜索功能 - 按应用名称、创作者或奖项名称搜索
if (awardSearchQuery.trim()) {
@@ -3888,24 +3942,26 @@ export function CompetitionManagement() {
{/* 统计信息 */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
<div className="text-center p-3 bg-blue-50 rounded-lg">
<div className="text-lg font-bold text-blue-600">{getFilteredAwards().length}</div>
<div className="text-lg font-bold text-blue-600">
{isLoadingAwards ? '...' : getFilteredAwards().length}
</div>
<div className="text-xs text-blue-600"></div>
</div>
<div className="text-center p-3 bg-yellow-50 rounded-lg">
<div className="text-lg font-bold text-yellow-600">
{getFilteredAwards().filter((a) => parseInt(a.rank) > 0 && parseInt(a.rank) <= 3).length}
{isLoadingAwards ? '...' : (awardStats?.top3 ?? getFilteredAwards().filter((a) => parseInt(a.rank) > 0 && parseInt(a.rank) <= 3).length)}
</div>
<div className="text-xs text-yellow-600"></div>
</div>
<div className="text-center p-3 bg-red-50 rounded-lg">
<div className="text-lg font-bold text-red-600">
{getFilteredAwards().filter((a) => a.awardType === "popular").length}
{isLoadingAwards ? '...' : (awardStats?.popular ?? getFilteredAwards().filter((a) => a.award_type === "popular").length)}
</div>
<div className="text-xs text-red-600"></div>
</div>
<div className="text-center p-3 bg-green-50 rounded-lg">
<div className="text-lg font-bold text-green-600">
{new Set(getFilteredAwards().map((a) => `${a.year}-${a.month}`)).size}
{isLoadingAwards ? '...' : (awardStats?.competitions ?? new Set(getFilteredAwards().map((a) => `${a.year}-${a.month}`)).size)}
</div>
<div className="text-xs text-green-600"></div>
</div>
@@ -7815,52 +7871,68 @@ export function CompetitionManagement() {
<Label htmlFor="award-name">
<span className="text-red-500">*</span>
</Label>
<Input
id="award-name"
value={newAward.awardName}
onChange={(e) => setNewAward({ ...newAward, awardName: e.target.value })}
placeholder="例如:最佳創新獎、金獎、銀獎"
/>
<Select
value={newAward.customAwardTypeId || "custom-input"}
onValueChange={(value: any) => {
if (value === "custom-input") {
setNewAward({ ...newAward, customAwardTypeId: "", awardName: "" })
} else {
// 檢查是否為自定義獎項類型
const customAwardType = competitionAwardTypes.find(type => type.id === value)
if (customAwardType) {
setNewAward({
...newAward,
customAwardTypeId: value,
awardName: customAwardType.name
})
}
}
}}
disabled={loadingAwardTypes}
>
<SelectTrigger>
<SelectValue placeholder={loadingAwardTypes ? "載入中..." : "選擇獎項名稱或自定義輸入"} />
</SelectTrigger>
<SelectContent>
<SelectItem value="custom-input"></SelectItem>
{competitionAwardTypes.map((awardType) => (
<SelectItem key={awardType.id} value={awardType.id}>
<div className="flex items-center space-x-2">
<span>{awardType.icon}</span>
<span>{awardType.name}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
{!newAward.customAwardTypeId && (
<Input
id="award-name"
value={newAward.awardName}
onChange={(e) => setNewAward({ ...newAward, awardName: e.target.value })}
placeholder="例如:最佳創新獎、金獎、銀獎、人氣獎"
/>
)}
{competitionAwardTypes.length > 0 && (
<p className="text-xs text-gray-500">
{competitionAwardTypes.length}
</p>
)}
</div>
{/* 獎項類型 */}
<div className="space-y-2">
<Label></Label>
<Select
value={newAward.customAwardTypeId ?? newAward.awardType}
value={newAward.awardType}
onValueChange={(value: any) => {
// 檢查是否為自定義獎項類型
const customAwardType = competitionAwardTypes.find(type => type.id === value)
if (customAwardType) {
setNewAward({
...newAward,
awardType: 'custom',
customAwardTypeId: value,
awardName: customAwardType.name
})
} else {
setNewAward({ ...newAward, awardType: value, customAwardTypeId: "" })
}
setNewAward({ ...newAward, awardType: value })
}}
disabled={loadingAwardTypes}
>
<SelectTrigger>
<SelectValue placeholder={loadingAwardTypes ? "載入中..." : "選擇獎項類型"} />
<SelectValue placeholder="選擇獎項類型" />
</SelectTrigger>
<SelectContent>
{competitionAwardTypes.length > 0 ? (
// 顯示競賽自定義的獎項類型
competitionAwardTypes.map((awardType) => (
<SelectItem key={awardType.id} value={awardType.id}>
<div className="flex items-center space-x-2">
<span>{awardType.icon}</span>
<span>{awardType.name}</span>
</div>
</SelectItem>
))
) : (
// 如果沒有自定義獎項類型,顯示預設選項
<>
<SelectItem value="gold">🥇 </SelectItem>
<SelectItem value="silver">🥈 </SelectItem>
<SelectItem value="bronze">🥉 </SelectItem>
@@ -7868,15 +7940,8 @@ export function CompetitionManagement() {
<SelectItem value="innovation">💡 </SelectItem>
<SelectItem value="technical"> </SelectItem>
<SelectItem value="custom">🏆 </SelectItem>
</>
)}
</SelectContent>
</Select>
{competitionAwardTypes.length > 0 && (
<p className="text-xs text-gray-500">
{competitionAwardTypes.length}
</p>
)}
</div>
{/* 獎項類別 */}

View File

@@ -60,33 +60,95 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0)
const [competitionJudges, setCompetitionJudges] = useState<any[]>([])
// 添加調試資訊
console.log('🏆 AwardDetailDialog 渲染:', {
open,
award: award ? {
id: award.id,
competitionId: award.competitionId,
awardName: award.awardName,
hasCompetitionId: !!award.competitionId
} : null
});
const competition = competitions.find((c) => c.id === award.competitionId)
const judgeScores = getJudgeScores(award.id)
const appData = getAppData(award.id)
// 載入競賽評審團資訊
useEffect(() => {
console.log('🔍 useEffect 觸發:', { open, competitionId: award.competitionId, awardId: award.id });
if (open && award.competitionId) {
const loadCompetitionJudges = async () => {
const loadCompetitionJudges = async (retryCount = 0) => {
try {
console.log('🔍 載入競賽評審團:', award.competitionId);
const response = await fetch(`/api/competitions/${award.competitionId}/judges`);
console.log('🔍 載入競賽評審團:', award.competitionId, '重試次數:', retryCount);
console.log('🏆 獎項資料:', award);
// 添加時間戳防止快取,並設置快取控制標頭
const timestamp = Date.now();
const apiUrl = `/api/competitions/${award.competitionId}/judges?t=${timestamp}`;
console.log('🌐 API URL:', apiUrl);
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
});
console.log('📡 API 回應狀態:', response.status);
console.log('📡 API 回應標頭:', Object.fromEntries(response.headers.entries()));
if (!response.ok) {
const errorText = await response.text();
console.error('❌ API 錯誤回應:', errorText);
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
}
const data = await response.json();
console.log('📄 API 回應資料:', JSON.stringify(data, null, 2));
if (data.success && data.data && data.data.judges) {
console.log('✅ 獲取到評審團:', data.data.judges.length, '位');
console.log('👥 評審團詳細資料:', data.data.judges);
setCompetitionJudges(data.data.judges);
} else {
console.error('❌ 獲取評審團失敗:', data.message);
setCompetitionJudges([]);
console.error('❌ 獲取評審團失敗:', data.message || '未知錯誤');
console.error('❌ 完整錯誤資料:', data);
// 如果沒有評審資料且是第一次嘗試,重試一次
if (retryCount === 0) {
console.log('🔄 重試載入評審團...');
setTimeout(() => loadCompetitionJudges(1), 1000);
} else {
setCompetitionJudges([]);
}
}
} catch (error) {
console.error('❌ 載入評審團失敗:', error);
setCompetitionJudges([]);
// 如果是網路錯誤且是第一次嘗試,重試一次
if (retryCount === 0) {
console.log('🔄 網路錯誤,重試載入評審團...');
setTimeout(() => loadCompetitionJudges(1), 2000);
} else {
setCompetitionJudges([]);
}
}
};
// 清空之前的評審資料,確保重新載入
setCompetitionJudges([]);
loadCompetitionJudges();
} else {
console.log('❌ useEffect 條件不滿足:', {
open,
competitionId: award.competitionId,
hasCompetitionId: !!award.competitionId
});
}
}, [open, award.competitionId]);

View File

@@ -287,7 +287,7 @@ export function PopularityRankings() {
return (
<Card
key={app.id}
key={`app-${app.id}-${index}`}
className="hover:shadow-lg transition-all duration-300 bg-gradient-to-br from-yellow-50 to-orange-50 border border-yellow-200 flex flex-col"
>
<CardContent className="p-4 flex flex-col flex-1">
@@ -454,7 +454,7 @@ export function PopularityRankings() {
return (
<Card
key={team.id}
key={`team-${team.id}-${index}`}
className="hover:shadow-lg transition-all duration-300 bg-gradient-to-br from-green-50 to-blue-50 border border-green-200 flex flex-col"
>
<CardContent className="p-4 flex flex-col flex-1">
@@ -487,8 +487,8 @@ export function PopularityRankings() {
<div className="mb-4 flex-1">
<h5 className="text-sm font-medium text-gray-700 mb-2"> ({team.members.length})</h5>
<div className="space-y-1">
{team.members.slice(0, 3).map((member: any) => (
<div key={member.id} className="flex items-center space-x-2 text-xs">
{team.members.slice(0, 3).map((member: any, memberIndex: number) => (
<div key={`member-${member.id}-${memberIndex}`} className="flex items-center space-x-2 text-xs">
<div className="w-6 h-6 bg-green-100 rounded-full flex items-center justify-center text-green-700 font-medium">
{member.name[0]}
</div>
@@ -687,8 +687,8 @@ export function PopularityRankings() {
</div>
) : competitionJudges.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{competitionJudges.map((judge) => (
<div key={judge.id} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
{competitionJudges.map((judge, judgeIndex) => (
<div key={`judge-${judge.id}-${judgeIndex}`} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<Avatar>
<AvatarImage src={judge.avatar} />
<AvatarFallback className="bg-purple-100 text-purple-700">{judge.name[0]}</AvatarFallback>
@@ -697,8 +697,8 @@ export function PopularityRankings() {
<h4 className="font-medium">{judge.name}</h4>
<p className="text-sm text-gray-600">{judge.title}</p>
<div className="flex flex-wrap gap-1 mt-1">
{judge.expertise.slice(0, 2).map((skill) => (
<Badge key={skill} variant="secondary" className="text-xs">
{judge.expertise.slice(0, 2).map((skill, skillIndex) => (
<Badge key={`skill-${skill}-${skillIndex}`} variant="secondary" className="text-xs">
{skill}
</Badge>
))}