建立檔案
This commit is contained in:
7
app/admin/page.tsx
Normal file
7
app/admin/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { AdminPanel } from "@/components/admin/admin-panel"
|
||||
|
||||
export default function AdminPage() {
|
||||
return <AdminPanel />
|
||||
}
|
195
app/admin/scoring-form-test/page.tsx
Normal file
195
app/admin/scoring-form-test/page.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { CheckCircle, Edit, Loader2 } from "lucide-react"
|
||||
|
||||
export default function ScoringFormTestPage() {
|
||||
const [showScoringForm, setShowScoringForm] = useState(false)
|
||||
const [manualScoring, setManualScoring] = useState({
|
||||
judgeId: "judge1",
|
||||
participantId: "app1",
|
||||
scores: {
|
||||
"創新性": 0,
|
||||
"技術性": 0,
|
||||
"實用性": 0,
|
||||
"展示效果": 0,
|
||||
"影響力": 0
|
||||
},
|
||||
comments: ""
|
||||
})
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const scoringRules = [
|
||||
{ name: "創新性", description: "技術創新程度和獨特性", weight: 25 },
|
||||
{ name: "技術性", description: "技術實現的複雜度和穩定性", weight: 20 },
|
||||
{ name: "實用性", description: "實際應用價值和用戶體驗", weight: 25 },
|
||||
{ name: "展示效果", description: "演示效果和表達能力", weight: 15 },
|
||||
{ name: "影響力", description: "對行業和社會的潛在影響", weight: 15 }
|
||||
]
|
||||
|
||||
const calculateTotalScore = (scores: Record<string, number>): number => {
|
||||
let totalScore = 0
|
||||
let totalWeight = 0
|
||||
|
||||
scoringRules.forEach(rule => {
|
||||
const score = scores[rule.name] || 0
|
||||
const weight = rule.weight || 1
|
||||
totalScore += score * weight
|
||||
totalWeight += weight
|
||||
})
|
||||
|
||||
return totalWeight > 0 ? Math.round(totalScore / totalWeight) : 0
|
||||
}
|
||||
|
||||
const handleSubmitScore = async () => {
|
||||
setIsLoading(true)
|
||||
// 模擬提交
|
||||
setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
setShowScoringForm(false)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold">評分表單測試</h1>
|
||||
<p className="text-gray-600">測試完整的評分表單功能</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>評分表單演示</CardTitle>
|
||||
<CardDescription>點擊按鈕查看完整的評分表單</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button onClick={() => setShowScoringForm(true)} size="lg">
|
||||
<Edit className="w-5 h-5 mr-2" />
|
||||
開啟評分表單
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={showScoringForm} onOpenChange={setShowScoringForm}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center space-x-2">
|
||||
<Edit className="w-5 h-5" />
|
||||
<span>評分表單</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
為參賽者進行評分,請根據各項指標進行評分
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 評分項目 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">評分項目</h3>
|
||||
{scoringRules.map((rule, index) => (
|
||||
<div key={index} className="space-y-4 p-6 border rounded-lg bg-white shadow-sm">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<Label className="text-lg font-semibold text-gray-900">{rule.name}</Label>
|
||||
<p className="text-sm text-gray-600 mt-2 leading-relaxed">{rule.description}</p>
|
||||
<p className="text-xs text-purple-600 mt-2 font-medium">權重:{rule.weight}%</p>
|
||||
</div>
|
||||
<div className="text-right ml-4">
|
||||
<span className="text-2xl font-bold text-blue-600">
|
||||
{manualScoring.scores[rule.name] || 0} / 10
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 評分按鈕 */}
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{Array.from({ length: 10 }, (_, i) => i + 1).map((score) => (
|
||||
<button
|
||||
key={score}
|
||||
type="button"
|
||||
onClick={() => setManualScoring({
|
||||
...manualScoring,
|
||||
scores: { ...manualScoring.scores, [rule.name]: score }
|
||||
})}
|
||||
className={`w-12 h-12 rounded-lg border-2 font-semibold text-lg transition-all duration-200 ${
|
||||
(manualScoring.scores[rule.name] || 0) === score
|
||||
? 'bg-blue-600 text-white border-blue-600 shadow-lg scale-105'
|
||||
: 'bg-white text-gray-700 border-gray-300 hover:border-blue-400 hover:bg-blue-50 hover:scale-105'
|
||||
}`}
|
||||
>
|
||||
{score}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 總分顯示 */}
|
||||
<div className="p-6 bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg border-2 border-blue-200">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<span className="text-xl font-bold text-gray-900">總分</span>
|
||||
<p className="text-sm text-gray-600 mt-1">根據權重計算的綜合評分</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-4xl font-bold text-blue-600">
|
||||
{calculateTotalScore(manualScoring.scores)}
|
||||
</span>
|
||||
<span className="text-xl text-gray-500 font-medium">/ 10</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 評審意見 */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-lg font-semibold">評審意見 *</Label>
|
||||
<Textarea
|
||||
placeholder="請詳細填寫評審意見、優點分析、改進建議等..."
|
||||
value={manualScoring.comments}
|
||||
onChange={(e) => setManualScoring({ ...manualScoring, comments: e.target.value })}
|
||||
rows={6}
|
||||
className="min-h-[120px] resize-none"
|
||||
/>
|
||||
<p className="text-xs text-gray-500">請提供具體的評審意見,包括項目的優點、不足之處和改進建議</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-4 pt-6 border-t border-gray-200">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={() => setShowScoringForm(false)}
|
||||
className="px-8"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmitScore}
|
||||
disabled={isLoading}
|
||||
size="lg"
|
||||
className="px-8 bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
|
||||
提交中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle className="w-5 h-5 mr-2" />
|
||||
提交評分
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
13
app/admin/scoring-test/page.tsx
Normal file
13
app/admin/scoring-test/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ScoringManagement } from "@/components/admin/scoring-management"
|
||||
|
||||
export default function ScoringTestPage() {
|
||||
return (
|
||||
<div className="container mx-auto py-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold">評分管理測試</h1>
|
||||
<p className="text-gray-600">測試動態評分項目功能</p>
|
||||
</div>
|
||||
<ScoringManagement />
|
||||
</div>
|
||||
)
|
||||
}
|
13
app/admin/scoring/page.tsx
Normal file
13
app/admin/scoring/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ScoringManagement } from "@/components/admin/scoring-management"
|
||||
|
||||
export default function ScoringPage() {
|
||||
return (
|
||||
<div className="container mx-auto py-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold">評分管理</h1>
|
||||
<p className="text-gray-600">管理競賽評分,查看已完成和未完成的評分內容</p>
|
||||
</div>
|
||||
<ScoringManagement />
|
||||
</div>
|
||||
)
|
||||
}
|
572
app/competition/page.tsx
Normal file
572
app/competition/page.tsx
Normal file
@@ -0,0 +1,572 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useAuth } from "@/contexts/auth-context"
|
||||
import { useCompetition } from "@/contexts/competition-context"
|
||||
import { Trophy, Award, Medal, Target, Users, Lightbulb, ArrowLeft, Plus, Search, X } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { PopularityRankings } from "@/components/competition/popularity-rankings"
|
||||
import { CompetitionDetailDialog } from "@/components/competition/competition-detail-dialog"
|
||||
import { AwardDetailDialog } from "@/components/competition/award-detail-dialog"
|
||||
|
||||
export default function CompetitionPage() {
|
||||
const { user, canAccessAdmin } = useAuth()
|
||||
const { competitions, awards, getAwardsByYear, getCompetitionRankings } = useCompetition()
|
||||
|
||||
const [selectedCompetitionTypeFilter, setSelectedCompetitionTypeFilter] = useState("all")
|
||||
const [selectedMonthFilter, setSelectedMonthFilter] = useState("all")
|
||||
const [selectedAwardCategory, setSelectedAwardCategory] = useState("all")
|
||||
const [selectedYear, setSelectedYear] = useState(2024)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [showCompetitionDetail, setShowCompetitionDetail] = useState(false)
|
||||
const [selectedRanking, setSelectedRanking] = useState<any>(null)
|
||||
const [selectedCompetitionType, setSelectedCompetitionType] = useState<"individual" | "team" | "proposal">(
|
||||
"individual",
|
||||
)
|
||||
const [showAwardDetail, setShowAwardDetail] = useState(false)
|
||||
const [selectedAward, setSelectedAward] = useState<any>(null)
|
||||
|
||||
const getCompetitionTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "individual":
|
||||
return <Target className="w-4 h-4" />
|
||||
case "team":
|
||||
return <Users className="w-4 h-4" />
|
||||
case "proposal":
|
||||
return <Lightbulb className="w-4 h-4" />
|
||||
case "mixed":
|
||||
return <Trophy className="w-4 h-4" />
|
||||
default:
|
||||
return <Trophy className="w-4 h-4" />
|
||||
}
|
||||
}
|
||||
|
||||
const getCompetitionTypeText = (type: string) => {
|
||||
switch (type) {
|
||||
case "individual":
|
||||
return "個人賽"
|
||||
case "team":
|
||||
return "團隊賽"
|
||||
case "proposal":
|
||||
return "提案賽"
|
||||
case "mixed":
|
||||
return "混合賽"
|
||||
default:
|
||||
return "競賽"
|
||||
}
|
||||
}
|
||||
|
||||
const getCompetitionTypeColor = (type: string) => {
|
||||
switch (type) {
|
||||
case "individual":
|
||||
return "bg-blue-100 text-blue-800 border-blue-200"
|
||||
case "team":
|
||||
return "bg-green-100 text-green-800 border-green-200"
|
||||
case "proposal":
|
||||
return "bg-purple-100 text-purple-800 border-purple-200"
|
||||
case "mixed":
|
||||
return "bg-gradient-to-r from-blue-100 via-green-100 to-purple-100 text-gray-800 border-gray-200"
|
||||
default:
|
||||
return "bg-gray-100 text-gray-800 border-gray-200"
|
||||
}
|
||||
}
|
||||
|
||||
const handleShowCompetitionDetail = (ranking: any, type: "individual" | "team" | "proposal") => {
|
||||
setSelectedRanking(ranking)
|
||||
setSelectedCompetitionType(type)
|
||||
setShowCompetitionDetail(true)
|
||||
}
|
||||
|
||||
const handleShowAwardDetail = (award: any) => {
|
||||
setSelectedAward(award)
|
||||
setShowAwardDetail(true)
|
||||
}
|
||||
|
||||
const getFilteredAwards = () => {
|
||||
let filteredAwards = getAwardsByYear(selectedYear)
|
||||
|
||||
// 搜索功能 - 按应用名称、创作者或奖项名称搜索
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase().trim()
|
||||
filteredAwards = filteredAwards.filter((award) => {
|
||||
return (
|
||||
award.appName?.toLowerCase().includes(query) ||
|
||||
award.creator?.toLowerCase().includes(query) ||
|
||||
award.awardName?.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (selectedCompetitionTypeFilter !== "all") {
|
||||
filteredAwards = filteredAwards.filter((award) => award.competitionType === selectedCompetitionTypeFilter)
|
||||
}
|
||||
|
||||
if (selectedMonthFilter !== "all") {
|
||||
filteredAwards = filteredAwards.filter((award) => award.month === Number.parseInt(selectedMonthFilter))
|
||||
}
|
||||
|
||||
if (selectedAwardCategory !== "all") {
|
||||
if (selectedAwardCategory === "ranking") {
|
||||
filteredAwards = filteredAwards.filter((award) => award.rank > 0 && award.rank <= 3)
|
||||
} else if (selectedAwardCategory === "popular") {
|
||||
filteredAwards = filteredAwards.filter((award) => award.awardType === "popular")
|
||||
} else {
|
||||
filteredAwards = filteredAwards.filter((award) => award.category === selectedAwardCategory)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredAwards.sort((a, b) => {
|
||||
// Sort by month first, then by rank
|
||||
if (a.month !== b.month) return b.month - a.month
|
||||
if (a.rank !== b.rank) {
|
||||
if (a.rank === 0) return 1
|
||||
if (b.rank === 0) return -1
|
||||
return a.rank - b.rank
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
const filteredAwards = getFilteredAwards()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50">
|
||||
{/* Header */}
|
||||
<header className="bg-white/80 backdrop-blur-sm border-b border-gray-200 sticky top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => window.history.back()}
|
||||
className="text-gray-700 hover:text-blue-600 hover:bg-blue-50"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
返回主頁
|
||||
</Button>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-8 h-8 bg-gradient-to-r from-blue-600 to-purple-600 rounded-lg flex items-center justify-center">
|
||||
<Trophy className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-900">競賽專區</h1>
|
||||
<p className="text-xs text-gray-500">COMPETITION CENTER</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Hero Section */}
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl font-bold text-gray-900 mb-4">AI 創新競賽</h2>
|
||||
<p className="text-xl text-gray-600 max-w-3xl mx-auto mb-8">
|
||||
展示最優秀的 AI 應用作品,見證創新技術的競技與榮耀
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="rankings" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2 mb-8">
|
||||
<TabsTrigger value="rankings" className="flex items-center space-x-2">
|
||||
<Trophy className="w-4 h-4" />
|
||||
<span>競賽排行</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="awards" className="flex items-center space-x-2">
|
||||
<Award className="w-4 h-4" />
|
||||
<span>得獎作品</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="rankings">
|
||||
<PopularityRankings />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="awards">
|
||||
<div className="space-y-8">
|
||||
{/* Enhanced Filter Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Medal className="w-5 h-5 text-purple-500" />
|
||||
<span>得獎作品展示</span>
|
||||
</CardTitle>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Select
|
||||
value={selectedYear.toString()}
|
||||
onValueChange={(value) => setSelectedYear(Number.parseInt(value))}
|
||||
>
|
||||
<SelectTrigger className="w-24">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="2024">2024年</SelectItem>
|
||||
<SelectItem value="2023">2023年</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-gray-600">展示 {selectedYear} 年度各項競賽的得獎作品</p>
|
||||
{searchQuery && (
|
||||
<div className="text-sm text-blue-600 bg-blue-50 px-3 py-1 rounded-full">
|
||||
搜尋關鍵字:「{searchQuery}」
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search and Filter Controls */}
|
||||
<div className="space-y-4">
|
||||
{/* Search Bar */}
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Search className="h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="搜尋應用名稱、創作者或獎項名稱..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 pr-10 w-full md:w-96"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery("")}
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Filter Controls */}
|
||||
<div className="flex flex-wrap gap-4 items-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm font-medium text-gray-700">競賽類型:</span>
|
||||
<Select value={selectedCompetitionTypeFilter} onValueChange={setSelectedCompetitionTypeFilter}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部類型</SelectItem>
|
||||
<SelectItem value="individual">個人賽</SelectItem>
|
||||
<SelectItem value="team">團隊賽</SelectItem>
|
||||
<SelectItem value="proposal">提案賽</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm font-medium text-gray-700">月份:</span>
|
||||
<Select value={selectedMonthFilter} onValueChange={setSelectedMonthFilter}>
|
||||
<SelectTrigger className="w-24">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部</SelectItem>
|
||||
<SelectItem value="1">1月</SelectItem>
|
||||
<SelectItem value="2">2月</SelectItem>
|
||||
<SelectItem value="3">3月</SelectItem>
|
||||
<SelectItem value="4">4月</SelectItem>
|
||||
<SelectItem value="5">5月</SelectItem>
|
||||
<SelectItem value="6">6月</SelectItem>
|
||||
<SelectItem value="7">7月</SelectItem>
|
||||
<SelectItem value="8">8月</SelectItem>
|
||||
<SelectItem value="9">9月</SelectItem>
|
||||
<SelectItem value="10">10月</SelectItem>
|
||||
<SelectItem value="11">11月</SelectItem>
|
||||
<SelectItem value="12">12月</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm font-medium text-gray-700">獎項類型:</span>
|
||||
<Select value={selectedAwardCategory} onValueChange={setSelectedAwardCategory}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部獎項</SelectItem>
|
||||
<SelectItem value="ranking">前三名</SelectItem>
|
||||
<SelectItem value="popular">人氣獎</SelectItem>
|
||||
<SelectItem value="innovation">創新類</SelectItem>
|
||||
<SelectItem value="technical">技術類</SelectItem>
|
||||
<SelectItem value="practical">實用類</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Clear Filters Button */}
|
||||
{(searchQuery || selectedCompetitionTypeFilter !== "all" || selectedMonthFilter !== "all" || selectedAwardCategory !== "all") && (
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSearchQuery("")
|
||||
setSelectedCompetitionTypeFilter("all")
|
||||
setSelectedMonthFilter("all")
|
||||
setSelectedAwardCategory("all")
|
||||
}}
|
||||
className="text-gray-600 hover:text-gray-800"
|
||||
>
|
||||
<X className="w-4 h-4 mr-1" />
|
||||
清除所有篩選
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistics */}
|
||||
<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">{filteredAwards.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">
|
||||
{filteredAwards.filter((a) => a.rank > 0 && 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">
|
||||
{filteredAwards.filter((a) => a.awardType === "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(filteredAwards.map((a) => `${a.year}-${a.month}`)).size}
|
||||
</div>
|
||||
<div className="text-xs text-green-600">競賽場次</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Awards Grid with Enhanced Display */}
|
||||
{filteredAwards.length > 0 ? (
|
||||
<div className="space-y-8">
|
||||
{/* Group awards by month */}
|
||||
{Array.from(new Set(filteredAwards.map((award) => award.month)))
|
||||
.sort((a, b) => b - a)
|
||||
.map((month) => {
|
||||
const monthAwards = filteredAwards.filter((award) => award.month === month)
|
||||
const competition = competitions.find((c) => c.month === month && c.year === selectedYear)
|
||||
|
||||
return (
|
||||
<div key={month} className="space-y-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<h3 className="text-xl font-bold text-gray-900">
|
||||
{selectedYear}年{month}月競賽得獎作品
|
||||
</h3>
|
||||
{competition && (
|
||||
<Badge variant="outline" className={getCompetitionTypeColor(competition.type)}>
|
||||
{getCompetitionTypeIcon(competition.type)}
|
||||
<span className="ml-1">{getCompetitionTypeText(competition.type)}</span>
|
||||
</Badge>
|
||||
)}
|
||||
<Badge variant="secondary" className="bg-gray-100 text-gray-700">
|
||||
{monthAwards.length} 個獎項
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{monthAwards.map((award) => (
|
||||
<Card
|
||||
key={award.id}
|
||||
className="relative overflow-hidden border-0 shadow-lg bg-gradient-to-br from-white to-gray-50 hover:shadow-xl transition-shadow cursor-pointer"
|
||||
onClick={() => handleShowAwardDetail(award)}
|
||||
>
|
||||
{/* Rank Badge */}
|
||||
{award.rank > 0 && award.rank <= 3 && (
|
||||
<div className="absolute top-2 left-2 z-10">
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold text-white ${
|
||||
award.rank === 1
|
||||
? "bg-yellow-500"
|
||||
: award.rank === 2
|
||||
? "bg-gray-400"
|
||||
: award.rank === 3
|
||||
? "bg-orange-600"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{award.rank}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="absolute top-4 right-4 text-3xl">{award.icon}</div>
|
||||
|
||||
<CardHeader className="pb-3 pt-12">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={`w-fit ${
|
||||
award.awardType === "popular"
|
||||
? "bg-red-100 text-red-800 border-red-200"
|
||||
: award.rank === 1
|
||||
? "bg-yellow-100 text-yellow-800 border-yellow-200"
|
||||
: award.rank === 2
|
||||
? "bg-gray-100 text-gray-800 border-gray-200"
|
||||
: award.rank === 3
|
||||
? "bg-orange-100 text-orange-800 border-orange-200"
|
||||
: "bg-blue-100 text-blue-800 border-blue-200"
|
||||
}`}
|
||||
>
|
||||
{award.awardName}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={getCompetitionTypeColor(award.competitionType)}
|
||||
>
|
||||
{getCompetitionTypeIcon(award.competitionType)}
|
||||
<span className="ml-1">{getCompetitionTypeText(award.competitionType)}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<CardTitle className="text-lg line-clamp-2">
|
||||
{award.appName || award.proposalTitle || award.teamName}
|
||||
</CardTitle>
|
||||
<p className="text-sm text-gray-500">by {award.creator}</p>
|
||||
<div className="text-xs text-gray-400">
|
||||
{award.year}年{award.month}月
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="pt-0">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">
|
||||
{award.competitionType === "proposal"
|
||||
? "評審評分"
|
||||
: award.awardType === "popular"
|
||||
? award.competitionType === "team"
|
||||
? "人氣指數"
|
||||
: "收藏數"
|
||||
: "評審評分"}
|
||||
</span>
|
||||
<span className="font-bold text-lg text-gray-900">
|
||||
{award.awardType === "popular" && award.competitionType === "team"
|
||||
? `${award.score}`
|
||||
: award.awardType === "popular"
|
||||
? `${award.score}`
|
||||
: award.score}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleShowAwardDetail(award)
|
||||
}}
|
||||
>
|
||||
查看得獎詳情
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="text-center py-12">
|
||||
<div className="space-y-4">
|
||||
{searchQuery ? (
|
||||
<Search className="w-16 h-16 text-gray-400 mx-auto" />
|
||||
) : (
|
||||
<Medal className="w-16 h-16 text-gray-400 mx-auto" />
|
||||
)}
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-600 mb-2">
|
||||
{searchQuery ? (
|
||||
<>找不到包含「{searchQuery}」的得獎作品</>
|
||||
) : (
|
||||
<>
|
||||
{selectedYear}年{selectedMonthFilter !== "all" ? `${selectedMonthFilter}月` : ""}
|
||||
暫無符合條件的得獎作品
|
||||
</>
|
||||
)}
|
||||
</h3>
|
||||
<p className="text-gray-500">
|
||||
{searchQuery
|
||||
? "嘗試使用其他關鍵字或調整篩選條件"
|
||||
: "請調整篩選條件查看其他得獎作品"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-transparent"
|
||||
onClick={() => {
|
||||
setSearchQuery("")
|
||||
setSelectedCompetitionTypeFilter("all")
|
||||
setSelectedMonthFilter("all")
|
||||
setSelectedAwardCategory("all")
|
||||
}}
|
||||
>
|
||||
<X className="w-4 h-4 mr-1" />
|
||||
清除所有篩選
|
||||
</Button>
|
||||
{searchQuery && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-transparent"
|
||||
onClick={() => setSearchQuery("")}
|
||||
>
|
||||
清除搜尋
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* Competition Detail Dialog */}
|
||||
{selectedRanking && (
|
||||
<CompetitionDetailDialog
|
||||
open={showCompetitionDetail}
|
||||
onOpenChange={setShowCompetitionDetail}
|
||||
ranking={selectedRanking}
|
||||
competitionType={selectedCompetitionType}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Award Detail Dialog */}
|
||||
{selectedAward && (
|
||||
<AwardDetailDialog open={showAwardDetail} onOpenChange={setShowAwardDetail} award={selectedAward} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
100
app/globals.css
Normal file
100
app/globals.css
Normal file
@@ -0,0 +1,100 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* 隱藏滾動條 */
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none; /* Safari and Chrome */
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem;
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
384
app/judge-scoring/page.tsx
Normal file
384
app/judge-scoring/page.tsx
Normal file
@@ -0,0 +1,384 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { AlertTriangle, CheckCircle, User, Trophy, LogIn, Loader2 } from "lucide-react"
|
||||
|
||||
interface Judge {
|
||||
id: string
|
||||
name: string
|
||||
specialty: string
|
||||
}
|
||||
|
||||
interface ScoringItem {
|
||||
id: string
|
||||
name: string
|
||||
type: "individual" | "team"
|
||||
status: "pending" | "completed"
|
||||
score?: number
|
||||
submittedAt?: string
|
||||
}
|
||||
|
||||
export default function JudgeScoringPage() {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false)
|
||||
const [judgeId, setJudgeId] = useState("")
|
||||
const [accessCode, setAccessCode] = useState("")
|
||||
const [currentJudge, setCurrentJudge] = useState<Judge | null>(null)
|
||||
const [scoringItems, setScoringItems] = useState<ScoringItem[]>([])
|
||||
const [selectedItem, setSelectedItem] = useState<ScoringItem | null>(null)
|
||||
const [showScoringDialog, setShowScoringDialog] = useState(false)
|
||||
const [scores, setScores] = useState<Record<string, number>>({})
|
||||
const [comments, setComments] = useState("")
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
const [success, setSuccess] = useState("")
|
||||
|
||||
// Judge data - empty for production
|
||||
const mockJudges: Judge[] = []
|
||||
|
||||
// Scoring items - empty for production
|
||||
const mockScoringItems: ScoringItem[] = []
|
||||
|
||||
const handleLogin = () => {
|
||||
setError("")
|
||||
|
||||
if (!judgeId.trim() || !accessCode.trim()) {
|
||||
setError("請填寫評審ID和存取碼")
|
||||
return
|
||||
}
|
||||
|
||||
if (accessCode !== "judge2024") {
|
||||
setError("存取碼錯誤")
|
||||
return
|
||||
}
|
||||
|
||||
const judge = mockJudges.find(j => j.id === judgeId)
|
||||
if (!judge) {
|
||||
setError("評審ID不存在")
|
||||
return
|
||||
}
|
||||
|
||||
setCurrentJudge(judge)
|
||||
setScoringItems(mockScoringItems)
|
||||
setIsLoggedIn(true)
|
||||
setSuccess("登入成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
}
|
||||
|
||||
const handleStartScoring = (item: ScoringItem) => {
|
||||
setSelectedItem(item)
|
||||
setScores({})
|
||||
setComments("")
|
||||
setShowScoringDialog(true)
|
||||
}
|
||||
|
||||
const handleSubmitScore = async () => {
|
||||
if (!selectedItem) 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
|
||||
))
|
||||
|
||||
setShowScoringDialog(false)
|
||||
setSelectedItem(null)
|
||||
setScores({})
|
||||
setComments("")
|
||||
setIsSubmitting(false)
|
||||
setSuccess("評分提交成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const getProgress = () => {
|
||||
const total = scoringItems.length
|
||||
const completed = scoringItems.filter(item => item.status === "completed").length
|
||||
return { total, completed, percentage: total > 0 ? Math.round((completed / total) * 100) : 0 }
|
||||
}
|
||||
|
||||
const progress = getProgress()
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-4">
|
||||
<Trophy className="w-8 h-8 text-blue-600" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl">評審評分系統</CardTitle>
|
||||
<CardDescription>
|
||||
請輸入您的評審ID和存取碼進行登入
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="judgeId">評審ID</Label>
|
||||
<Input
|
||||
id="judgeId"
|
||||
placeholder="例如:j1"
|
||||
value={judgeId}
|
||||
onChange={(e) => setJudgeId(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="accessCode">存取碼</Label>
|
||||
<Input
|
||||
id="accessCode"
|
||||
type="password"
|
||||
placeholder="請輸入存取碼"
|
||||
value={accessCode}
|
||||
onChange={(e) => setAccessCode(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleLogin}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
<LogIn className="w-4 h-4 mr-2" />
|
||||
登入評分系統
|
||||
</Button>
|
||||
|
||||
<div className="text-center text-sm text-gray-500">
|
||||
<p>評審ID範例:j1, j2, j3, j4, j5</p>
|
||||
<p>存取碼:judge2024</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-4">
|
||||
<div className="max-w-6xl mx-auto space-y-6">
|
||||
{/* 成功訊息 */}
|
||||
{success && (
|
||||
<Alert className="border-green-200 bg-green-50">
|
||||
<CheckCircle className="h-4 w-4 text-green-600" />
|
||||
<AlertDescription className="text-green-800">{success}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* 評審資訊 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Avatar className="w-12 h-12">
|
||||
<AvatarFallback className="text-lg font-semibold">
|
||||
{currentJudge?.name.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{currentJudge?.name}</h1>
|
||||
<p className="text-gray-600">{currentJudge?.specialty}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsLoggedIn(false)}
|
||||
>
|
||||
登出
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
{/* 評分進度 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>評分進度</CardTitle>
|
||||
<CardDescription>
|
||||
您的評分任務進度概覽
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>完成進度</span>
|
||||
<span>{progress.completed} / {progress.total}</span>
|
||||
</div>
|
||||
<Progress value={progress.percentage} className="h-2" />
|
||||
<div className="text-center">
|
||||
<span className="text-2xl font-bold text-blue-600">{progress.percentage}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 評分項目列表 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>評分項目</CardTitle>
|
||||
<CardDescription>
|
||||
請為以下項目進行評分
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{scoringItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
{item.type === "individual" ? (
|
||||
<User className="w-4 h-4 text-blue-600" />
|
||||
) : (
|
||||
<div className="flex space-x-1">
|
||||
<User className="w-4 h-4 text-green-600" />
|
||||
<User className="w-4 h-4 text-green-600" />
|
||||
</div>
|
||||
)}
|
||||
<span className="font-medium">{item.name}</span>
|
||||
<Badge variant="outline">
|
||||
{item.type === "individual" ? "個人" : "團隊"}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => handleStartScoring(item)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
開始評分
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 評分對話框 */}
|
||||
<Dialog open={showScoringDialog} onOpenChange={setShowScoringDialog}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>評分項目:{selectedItem?.name}</DialogTitle>
|
||||
<DialogDescription>
|
||||
請為此項目進行評分,滿分為10分
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 評分項目 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">評分項目</h3>
|
||||
{[
|
||||
{ name: "創新性", description: "創新程度和獨特性" },
|
||||
{ name: "技術性", description: "技術實現的複雜度和品質" },
|
||||
{ name: "實用性", description: "實際應用價值和用戶體驗" },
|
||||
{ name: "展示效果", description: "展示的清晰度和吸引力" },
|
||||
{ name: "影響力", description: "對行業或社會的潛在影響" }
|
||||
].map((criterion, index) => (
|
||||
<div key={index} className="space-y-2">
|
||||
<Label>{criterion.name}</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) => (
|
||||
<button
|
||||
key={score}
|
||||
type="button"
|
||||
onClick={() => setScores(prev => ({ ...prev, [criterion.name]: score }))}
|
||||
className={`w-10 h-10 rounded border-2 font-semibold transition-all ${
|
||||
scores[criterion.name] === score
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white text-gray-700 border-gray-300 hover:border-blue-400'
|
||||
}`}
|
||||
>
|
||||
{score}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 評審意見 */}
|
||||
<div className="space-y-2">
|
||||
<Label>評審意見</Label>
|
||||
<Textarea
|
||||
placeholder="請詳細填寫評審意見、優點分析、改進建議等..."
|
||||
value={comments}
|
||||
onChange={(e) => setComments(e.target.value)}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 總分顯示 */}
|
||||
<div className="p-4 bg-blue-50 rounded-lg">
|
||||
<div className="flex justify-between items-center">
|
||||
<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)
|
||||
: 0
|
||||
} / 10
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-4 pt-6 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowScoringDialog(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmitScore}
|
||||
disabled={isSubmitting || Object.keys(scores).length < 5 || !comments.trim()}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
提交中...
|
||||
</>
|
||||
) : (
|
||||
"提交評分"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
33
app/layout.tsx
Normal file
33
app/layout.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import type React from "react"
|
||||
import { Inter } from "next/font/google"
|
||||
import "./globals.css"
|
||||
import { AuthProvider } from "@/contexts/auth-context"
|
||||
import { CompetitionProvider } from "@/contexts/competition-context"
|
||||
import { Toaster } from "@/components/ui/toaster"
|
||||
import { ChatBot } from "@/components/chat-bot"
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] })
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="zh-TW">
|
||||
<body className={inter.className}>
|
||||
<AuthProvider>
|
||||
<CompetitionProvider>
|
||||
{children}
|
||||
<Toaster />
|
||||
<ChatBot />
|
||||
</CompetitionProvider>
|
||||
</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
generator: 'v0.dev'
|
||||
};
|
7
app/loading.tsx
Normal file
7
app/loading.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-purple-600"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
1017
app/page.tsx
Normal file
1017
app/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
3
app/register/loading.tsx
Normal file
3
app/register/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
408
app/register/page.tsx
Normal file
408
app/register/page.tsx
Normal file
@@ -0,0 +1,408 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { useAuth } from "@/contexts/auth-context"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Brain, User, Mail, Building, Lock, Loader2, CheckCircle, AlertTriangle, Shield, Code } from "lucide-react"
|
||||
|
||||
export default function RegisterPage() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const { register, isLoading } = useAuth()
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
department: "",
|
||||
})
|
||||
const [error, setError] = useState("")
|
||||
const [success, setSuccess] = useState("")
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
// 從 URL 參數獲取邀請資訊
|
||||
const invitationToken = searchParams.get("token")
|
||||
const invitedEmail = searchParams.get("email")
|
||||
const invitedRole = searchParams.get("role") || "user"
|
||||
const isInvitedUser = !!(invitationToken && invitedEmail)
|
||||
|
||||
useEffect(() => {
|
||||
if (isInvitedUser) {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
email: decodeURIComponent(invitedEmail),
|
||||
}))
|
||||
}
|
||||
}, [isInvitedUser, invitedEmail])
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }))
|
||||
setError("")
|
||||
}
|
||||
|
||||
const getRoleText = (role: string) => {
|
||||
switch (role) {
|
||||
case "admin":
|
||||
return "管理員"
|
||||
case "developer":
|
||||
return "開發者"
|
||||
case "user":
|
||||
return "一般用戶"
|
||||
default:
|
||||
return role
|
||||
}
|
||||
}
|
||||
|
||||
const getRoleIcon = (role: string) => {
|
||||
switch (role) {
|
||||
case "admin":
|
||||
return <Shield className="w-4 h-4 text-purple-600" />
|
||||
case "developer":
|
||||
return <Code className="w-4 h-4 text-green-600" />
|
||||
case "user":
|
||||
return <User className="w-4 h-4 text-blue-600" />
|
||||
default:
|
||||
return <User className="w-4 h-4 text-blue-600" />
|
||||
}
|
||||
}
|
||||
|
||||
const getRoleColor = (role: string) => {
|
||||
switch (role) {
|
||||
case "admin":
|
||||
return "bg-purple-100 text-purple-800 border-purple-200"
|
||||
case "developer":
|
||||
return "bg-green-100 text-green-800 border-green-200"
|
||||
case "user":
|
||||
return "bg-blue-100 text-blue-800 border-blue-200"
|
||||
default:
|
||||
return "bg-gray-100 text-gray-800 border-gray-200"
|
||||
}
|
||||
}
|
||||
|
||||
const getRoleDescription = (role: string) => {
|
||||
switch (role) {
|
||||
case "admin":
|
||||
return "可以訪問管理後台,管理用戶和審核應用"
|
||||
case "developer":
|
||||
return "可以提交 AI 應用申請,參與平台建設"
|
||||
case "user":
|
||||
return "可以瀏覽和收藏應用,參與評價互動"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError("")
|
||||
setIsSubmitting(true)
|
||||
|
||||
// 表單驗證
|
||||
if (!formData.name || !formData.email || !formData.password || !formData.department) {
|
||||
setError("請填寫所有必填欄位")
|
||||
setIsSubmitting(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
setError("密碼確認不一致")
|
||||
setIsSubmitting(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.password.length < 6) {
|
||||
setError("密碼長度至少需要 6 個字符")
|
||||
setIsSubmitting(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const success = await register({
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
department: formData.department,
|
||||
})
|
||||
|
||||
if (success) {
|
||||
setSuccess("註冊成功!正在跳轉...")
|
||||
setTimeout(() => {
|
||||
router.push("/")
|
||||
}, 2000)
|
||||
} else {
|
||||
setError("註冊失敗,請檢查資料或聯繫管理員")
|
||||
}
|
||||
} catch (err) {
|
||||
setError("註冊過程中發生錯誤,請稍後再試")
|
||||
}
|
||||
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardContent className="text-center py-8">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<CheckCircle className="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">註冊成功!</h3>
|
||||
<p className="text-gray-600 mb-4">歡迎加入強茂集團 AI 展示平台</p>
|
||||
<p className="text-sm text-gray-500">正在跳轉到首頁...</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex items-center justify-center space-x-2 mb-4">
|
||||
<div className="w-10 h-10 bg-gradient-to-r from-blue-600 to-purple-600 rounded-lg flex items-center justify-center">
|
||||
<Brain className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">強茂集團 AI 展示平台</h1>
|
||||
</div>
|
||||
</div>
|
||||
{isInvitedUser ? (
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">完成註冊</h2>
|
||||
<p className="text-gray-600">您已受邀加入平台,請完成以下資訊</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">建立帳戶</h2>
|
||||
<p className="text-gray-600">加入強茂集團 AI 展示平台</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>註冊資訊</CardTitle>
|
||||
<CardDescription>
|
||||
{isInvitedUser ? "請填寫您的個人資訊完成註冊" : "請填寫以下資訊建立您的帳戶"}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* Invitation Info */}
|
||||
{isInvitedUser && (
|
||||
<div className="mb-6">
|
||||
<div className="bg-blue-50 rounded-lg p-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<CheckCircle className="w-5 h-5 text-blue-600 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-blue-900 mb-2">邀請資訊</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-blue-700">電子郵件:</span>
|
||||
<span className="text-sm font-medium text-blue-900">{invitedEmail}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-blue-700">預設角色:</span>
|
||||
<Badge variant="outline" className={getRoleColor(invitedRole)}>
|
||||
<div className="flex items-center space-x-1">
|
||||
{getRoleIcon(invitedRole)}
|
||||
<span>{getRoleText(invitedRole)}</span>
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="mt-3 pt-3 border-t border-blue-200">
|
||||
<p className="text-xs text-blue-600">{getRoleDescription(invitedRole)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error/Success Messages */}
|
||||
{error && (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">姓名 *</Label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
||||
placeholder="請輸入您的姓名"
|
||||
className="pl-10"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">電子郵件 *</Label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange("email", e.target.value)}
|
||||
placeholder="請輸入電子郵件"
|
||||
className="pl-10"
|
||||
disabled={isSubmitting || isInvitedUser}
|
||||
readOnly={isInvitedUser}
|
||||
/>
|
||||
</div>
|
||||
{isInvitedUser && <p className="text-xs text-gray-500">此電子郵件由邀請連結自動填入</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="department">部門 *</Label>
|
||||
<div className="relative">
|
||||
<Building className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4 z-10" />
|
||||
<Select
|
||||
value={formData.department}
|
||||
onValueChange={(value) => handleInputChange("department", value)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<SelectTrigger className="pl-10">
|
||||
<SelectValue placeholder="請選擇您的部門" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="HQBU">HQBU</SelectItem>
|
||||
<SelectItem value="ITBU">ITBU</SelectItem>
|
||||
<SelectItem value="MBU1">MBU1</SelectItem>
|
||||
<SelectItem value="SBU">SBU</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">密碼 *</Label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={(e) => handleInputChange("password", e.target.value)}
|
||||
placeholder="請輸入密碼(至少 6 個字符)"
|
||||
className="pl-10"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">確認密碼 *</Label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
value={formData.confirmPassword}
|
||||
onChange={(e) => handleInputChange("confirmPassword", e.target.value)}
|
||||
placeholder="請再次輸入密碼"
|
||||
className="pl-10"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
|
||||
disabled={isSubmitting || isLoading}
|
||||
>
|
||||
{isSubmitting || isLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
註冊中...
|
||||
</>
|
||||
) : (
|
||||
"完成註冊"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-sm text-gray-600">
|
||||
已有帳戶?{" "}
|
||||
<Button
|
||||
variant="link"
|
||||
className="p-0 h-auto font-normal text-blue-600 hover:text-blue-700"
|
||||
onClick={() => router.push("/")}
|
||||
>
|
||||
返回首頁登入
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Role Information */}
|
||||
{!isInvitedUser && (
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">角色說明</CardTitle>
|
||||
<CardDescription>了解不同角色的權限和功能</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-3">
|
||||
<div className="flex items-start space-x-3 p-3 bg-blue-50 rounded-lg">
|
||||
<User className="w-5 h-5 text-blue-600 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-blue-900">一般用戶</h4>
|
||||
<p className="text-sm text-blue-700">可以瀏覽和收藏應用,參與評價互動</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-3 p-3 bg-green-50 rounded-lg">
|
||||
<Code className="w-5 h-5 text-green-600 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-green-900">開發者</h4>
|
||||
<p className="text-sm text-green-700">可以提交 AI 應用申請,參與平台建設</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-3 p-3 bg-purple-50 rounded-lg">
|
||||
<Shield className="w-5 h-5 text-purple-600 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-purple-900">管理員</h4>
|
||||
<p className="text-sm text-purple-700">可以訪問管理後台,管理用戶和審核應用</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 p-3 rounded-lg">
|
||||
<p className="text-xs text-gray-600">
|
||||
<strong>注意:</strong>新註冊用戶預設為一般用戶角色。如需其他角色權限,請聯繫管理員進行調整。
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user