Initial commit
This commit is contained in:
3
app/admin/analytics/loading.tsx
Normal file
3
app/admin/analytics/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
430
app/admin/analytics/page.tsx
Normal file
430
app/admin/analytics/page.tsx
Normal file
@@ -0,0 +1,430 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { ProtectedRoute } from "@/components/protected-route"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Brain, Lightbulb, BarChart3, ArrowLeft, TrendingUp, Users, Award, Target } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useAuth, type User } from "@/lib/hooks/use-auth"
|
||||
|
||||
interface DepartmentStats {
|
||||
department: string
|
||||
totalUsers: number
|
||||
participatedUsers: number
|
||||
participationRate: number
|
||||
averageLogicScore: number
|
||||
averageCreativeScore: number
|
||||
averageCombinedScore: number
|
||||
overallAverage: number
|
||||
topPerformer: string | null
|
||||
testCounts: {
|
||||
logic: number
|
||||
creative: number
|
||||
combined: number
|
||||
}
|
||||
}
|
||||
|
||||
interface TestResult {
|
||||
userId: string
|
||||
userName: string
|
||||
userDepartment: string
|
||||
type: "logic" | "creative" | "combined"
|
||||
score: number
|
||||
completedAt: string
|
||||
}
|
||||
|
||||
export default function AnalyticsPage() {
|
||||
return (
|
||||
<ProtectedRoute adminOnly>
|
||||
<AnalyticsContent />
|
||||
</ProtectedRoute>
|
||||
)
|
||||
}
|
||||
|
||||
function AnalyticsContent() {
|
||||
const { user } = useAuth()
|
||||
const [departmentStats, setDepartmentStats] = useState<DepartmentStats[]>([])
|
||||
const [selectedDepartment, setSelectedDepartment] = useState("all")
|
||||
const [overallStats, setOverallStats] = useState({
|
||||
totalUsers: 0,
|
||||
totalParticipants: 0,
|
||||
overallParticipationRate: 0,
|
||||
averageScore: 0,
|
||||
totalTests: 0,
|
||||
})
|
||||
|
||||
const departments = ["人力資源部", "資訊技術部", "財務部", "行銷部", "業務部", "研發部", "客服部", "其他"]
|
||||
|
||||
useEffect(() => {
|
||||
loadAnalyticsData()
|
||||
}, [])
|
||||
|
||||
const loadAnalyticsData = () => {
|
||||
// Load users
|
||||
const users: User[] = JSON.parse(localStorage.getItem("hr_users") || "[]")
|
||||
|
||||
// Load all test results
|
||||
const allResults: TestResult[] = []
|
||||
|
||||
users.forEach((user: User) => {
|
||||
// Check for logic test results
|
||||
const logicKey = `logicTestResults_${user.id}`
|
||||
const logicResults = localStorage.getItem(logicKey)
|
||||
if (logicResults) {
|
||||
const data = JSON.parse(logicResults)
|
||||
allResults.push({
|
||||
userId: user.id,
|
||||
userName: user.name,
|
||||
userDepartment: user.department,
|
||||
type: "logic",
|
||||
score: data.score,
|
||||
completedAt: data.completedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// Check for creative test results
|
||||
const creativeKey = `creativeTestResults_${user.id}`
|
||||
const creativeResults = localStorage.getItem(creativeKey)
|
||||
if (creativeResults) {
|
||||
const data = JSON.parse(creativeResults)
|
||||
allResults.push({
|
||||
userId: user.id,
|
||||
userName: user.name,
|
||||
userDepartment: user.department,
|
||||
type: "creative",
|
||||
score: data.score,
|
||||
completedAt: data.completedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// Check for combined test results
|
||||
const combinedKey = `combinedTestResults_${user.id}`
|
||||
const combinedResults = localStorage.getItem(combinedKey)
|
||||
if (combinedResults) {
|
||||
const data = JSON.parse(combinedResults)
|
||||
allResults.push({
|
||||
userId: user.id,
|
||||
userName: user.name,
|
||||
userDepartment: user.department,
|
||||
type: "combined",
|
||||
score: data.overallScore,
|
||||
completedAt: data.completedAt,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Calculate department statistics
|
||||
const deptStats: DepartmentStats[] = departments
|
||||
.map((dept) => {
|
||||
const deptUsers = users.filter((u) => u.department === dept)
|
||||
const deptResults = allResults.filter((r) => r.userDepartment === dept)
|
||||
const participatedUsers = new Set(deptResults.map((r) => r.userId)).size
|
||||
|
||||
// Calculate average scores by test type
|
||||
const logicResults = deptResults.filter((r) => r.type === "logic")
|
||||
const creativeResults = deptResults.filter((r) => r.type === "creative")
|
||||
const combinedResults = deptResults.filter((r) => r.type === "combined")
|
||||
|
||||
const averageLogicScore =
|
||||
logicResults.length > 0
|
||||
? Math.round(logicResults.reduce((sum, r) => sum + r.score, 0) / logicResults.length)
|
||||
: 0
|
||||
|
||||
const averageCreativeScore =
|
||||
creativeResults.length > 0
|
||||
? Math.round(creativeResults.reduce((sum, r) => sum + r.score, 0) / creativeResults.length)
|
||||
: 0
|
||||
|
||||
const averageCombinedScore =
|
||||
combinedResults.length > 0
|
||||
? Math.round(combinedResults.reduce((sum, r) => sum + r.score, 0) / combinedResults.length)
|
||||
: 0
|
||||
|
||||
// Calculate overall average
|
||||
const allScores = [averageLogicScore, averageCreativeScore, averageCombinedScore].filter((s) => s > 0)
|
||||
const overallAverage =
|
||||
allScores.length > 0 ? Math.round(allScores.reduce((sum, s) => sum + s, 0) / allScores.length) : 0
|
||||
|
||||
// Find top performer
|
||||
const userScores = new Map<string, number[]>()
|
||||
deptResults.forEach((result) => {
|
||||
if (!userScores.has(result.userId)) {
|
||||
userScores.set(result.userId, [])
|
||||
}
|
||||
userScores.get(result.userId)!.push(result.score)
|
||||
})
|
||||
|
||||
let topPerformer: string | null = null
|
||||
let topScore = 0
|
||||
userScores.forEach((scores, userId) => {
|
||||
const avgScore = scores.reduce((sum, s) => sum + s, 0) / scores.length
|
||||
if (avgScore > topScore) {
|
||||
topScore = avgScore
|
||||
const user = users.find((u) => u.id === userId)
|
||||
topPerformer = user ? user.name : null
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
department: dept,
|
||||
totalUsers: deptUsers.length,
|
||||
participatedUsers,
|
||||
participationRate: deptUsers.length > 0 ? Math.round((participatedUsers / deptUsers.length) * 100) : 0,
|
||||
averageLogicScore,
|
||||
averageCreativeScore,
|
||||
averageCombinedScore,
|
||||
overallAverage,
|
||||
topPerformer,
|
||||
testCounts: {
|
||||
logic: logicResults.length,
|
||||
creative: creativeResults.length,
|
||||
combined: combinedResults.length,
|
||||
},
|
||||
}
|
||||
})
|
||||
.filter((stat) => stat.totalUsers > 0) // Only show departments with users
|
||||
|
||||
setDepartmentStats(deptStats)
|
||||
|
||||
// Calculate overall statistics
|
||||
const totalUsers = users.length
|
||||
const totalParticipants = new Set(allResults.map((r) => r.userId)).size
|
||||
const overallParticipationRate = totalUsers > 0 ? Math.round((totalParticipants / totalUsers) * 100) : 0
|
||||
const averageScore =
|
||||
allResults.length > 0 ? Math.round(allResults.reduce((sum, r) => sum + r.score, 0) / allResults.length) : 0
|
||||
|
||||
setOverallStats({
|
||||
totalUsers,
|
||||
totalParticipants,
|
||||
overallParticipationRate,
|
||||
averageScore,
|
||||
totalTests: allResults.length,
|
||||
})
|
||||
}
|
||||
|
||||
const getScoreColor = (score: number) => {
|
||||
if (score >= 90) return "text-green-600"
|
||||
if (score >= 80) return "text-blue-600"
|
||||
if (score >= 70) return "text-yellow-600"
|
||||
if (score >= 60) return "text-orange-600"
|
||||
return "text-red-600"
|
||||
}
|
||||
|
||||
const getParticipationColor = (rate: number) => {
|
||||
if (rate >= 80) return "text-green-600"
|
||||
if (rate >= 60) return "text-blue-600"
|
||||
if (rate >= 40) return "text-yellow-600"
|
||||
return "text-red-600"
|
||||
}
|
||||
|
||||
const filteredStats =
|
||||
selectedDepartment === "all"
|
||||
? departmentStats
|
||||
: departmentStats.filter((stat) => stat.department === selectedDepartment)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-card/50 backdrop-blur-sm">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href="/dashboard">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
返回儀表板
|
||||
</Link>
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">部門分析</h1>
|
||||
<p className="text-sm text-muted-foreground">查看各部門的測試結果統計和分析</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-7xl mx-auto space-y-8">
|
||||
{/* Overall Statistics */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-6">
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<Users className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-foreground mb-1">{overallStats.totalUsers}</div>
|
||||
<div className="text-sm text-muted-foreground">總用戶數</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="w-12 h-12 bg-blue-500/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<Target className="w-6 h-6 text-blue-500" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-foreground mb-1">{overallStats.totalParticipants}</div>
|
||||
<div className="text-sm text-muted-foreground">參與用戶</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="w-12 h-12 bg-green-500/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<TrendingUp className="w-6 h-6 text-green-500" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-foreground mb-1">{overallStats.overallParticipationRate}%</div>
|
||||
<div className="text-sm text-muted-foreground">參與率</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="w-12 h-12 bg-accent/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<Award className="w-6 h-6 text-accent" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-foreground mb-1">{overallStats.averageScore}</div>
|
||||
<div className="text-sm text-muted-foreground">平均分數</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="w-12 h-12 bg-purple-500/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<BarChart3 className="w-6 h-6 text-purple-500" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-foreground mb-1">{overallStats.totalTests}</div>
|
||||
<div className="text-sm text-muted-foreground">總測試次數</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Department Filter */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>部門篩選</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="max-w-xs">
|
||||
<Select value={selectedDepartment} onValueChange={setSelectedDepartment}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">所有部門</SelectItem>
|
||||
{departments.map((dept) => (
|
||||
<SelectItem key={dept} value={dept}>
|
||||
{dept}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Department Statistics */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{filteredStats.map((stat) => (
|
||||
<Card key={stat.department} className="border-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>{stat.department}</span>
|
||||
<Badge variant="outline">
|
||||
{stat.participatedUsers}/{stat.totalUsers} 人參與
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
參與率:
|
||||
<span className={getParticipationColor(stat.participationRate)}>{stat.participationRate}%</span>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Participation Rate */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">參與率</span>
|
||||
<span className="text-sm font-medium">{stat.participationRate}%</span>
|
||||
</div>
|
||||
<Progress value={stat.participationRate} className="h-2" />
|
||||
</div>
|
||||
|
||||
{/* Test Scores */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="text-center">
|
||||
<div className="w-10 h-10 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-2">
|
||||
<Brain className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div className={`text-lg font-bold ${getScoreColor(stat.averageLogicScore)}`}>
|
||||
{stat.averageLogicScore || "-"}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">邏輯思維</div>
|
||||
<div className="text-xs text-muted-foreground">({stat.testCounts.logic} 次)</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-10 h-10 bg-accent/10 rounded-full flex items-center justify-center mx-auto mb-2">
|
||||
<Lightbulb className="w-5 h-5 text-accent" />
|
||||
</div>
|
||||
<div className={`text-lg font-bold ${getScoreColor(stat.averageCreativeScore)}`}>
|
||||
{stat.averageCreativeScore || "-"}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">創意能力</div>
|
||||
<div className="text-xs text-muted-foreground">({stat.testCounts.creative} 次)</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-10 h-10 bg-gradient-to-r from-primary to-accent rounded-full flex items-center justify-center mx-auto mb-2">
|
||||
<BarChart3 className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div className={`text-lg font-bold ${getScoreColor(stat.averageCombinedScore)}`}>
|
||||
{stat.averageCombinedScore || "-"}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">綜合能力</div>
|
||||
<div className="text-xs text-muted-foreground">({stat.testCounts.combined} 次)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overall Average */}
|
||||
<div className="border-t pt-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-medium">部門平均分數</span>
|
||||
<span className={`text-xl font-bold ${getScoreColor(stat.overallAverage)}`}>
|
||||
{stat.overallAverage || "-"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top Performer */}
|
||||
{stat.topPerformer && (
|
||||
<div className="border-t pt-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-medium">表現最佳</span>
|
||||
<Badge className="bg-yellow-500 text-white">
|
||||
<Award className="w-3 h-3 mr-1" />
|
||||
{stat.topPerformer}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredStats.length === 0 && (
|
||||
<Card>
|
||||
<CardContent className="p-8 text-center">
|
||||
<div className="text-muted-foreground">
|
||||
{selectedDepartment === "all" ? "暫無部門數據" : "該部門暫無數據"}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
354
app/admin/questions/page.tsx
Normal file
354
app/admin/questions/page.tsx
Normal file
@@ -0,0 +1,354 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
import { ProtectedRoute } from "@/components/protected-route"
|
||||
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import {
|
||||
ArrowLeft,
|
||||
Upload,
|
||||
Download,
|
||||
Brain,
|
||||
Lightbulb,
|
||||
FileSpreadsheet,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
Info,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { logicQuestions } from "@/lib/questions/logic-questions"
|
||||
import { creativeQuestions } from "@/lib/questions/creative-questions"
|
||||
import { parseExcelFile, type ImportResult } from "@/lib/utils/excel-parser"
|
||||
|
||||
export default function QuestionsManagementPage() {
|
||||
return (
|
||||
<ProtectedRoute adminOnly>
|
||||
<QuestionsManagementContent />
|
||||
</ProtectedRoute>
|
||||
)
|
||||
}
|
||||
|
||||
function QuestionsManagementContent() {
|
||||
const [activeTab, setActiveTab] = useState("logic")
|
||||
const [isImporting, setIsImporting] = useState(false)
|
||||
const [importResult, setImportResult] = useState<ImportResult | null>(null)
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
const [importType, setImportType] = useState<"logic" | "creative">("logic")
|
||||
|
||||
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (file) {
|
||||
if (
|
||||
file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ||
|
||||
file.type === "application/vnd.ms-excel" ||
|
||||
file.name.endsWith(".xlsx") ||
|
||||
file.name.endsWith(".xls")
|
||||
) {
|
||||
setSelectedFile(file)
|
||||
setImportResult(null)
|
||||
} else {
|
||||
setImportResult({
|
||||
success: false,
|
||||
message: "請選擇 Excel 檔案 (.xlsx 或 .xls)",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!selectedFile) {
|
||||
setImportResult({
|
||||
success: false,
|
||||
message: "請先選擇要匯入的 Excel 檔案",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setIsImporting(true)
|
||||
|
||||
try {
|
||||
const result = await parseExcelFile(selectedFile, importType)
|
||||
setImportResult(result)
|
||||
|
||||
if (result.success) {
|
||||
setSelectedFile(null)
|
||||
// 重置檔案輸入
|
||||
const fileInput = document.getElementById("file-input") as HTMLInputElement
|
||||
if (fileInput) fileInput.value = ""
|
||||
}
|
||||
} catch (error) {
|
||||
setImportResult({
|
||||
success: false,
|
||||
message: "匯入失敗,請檢查檔案格式是否正確",
|
||||
errors: [error instanceof Error ? error.message : "未知錯誤"],
|
||||
})
|
||||
} finally {
|
||||
setIsImporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const downloadTemplate = (type: "logic" | "creative") => {
|
||||
let csvContent = ""
|
||||
|
||||
if (type === "logic") {
|
||||
csvContent = [
|
||||
["題目ID", "題目內容", "選項A", "選項B", "選項C", "選項D", "正確答案", "解釋"],
|
||||
[
|
||||
"1",
|
||||
"範例題目:如果所有A都是B,所有B都是C,那麼?",
|
||||
"所有A都是C",
|
||||
"所有C都是A",
|
||||
"有些A不是C",
|
||||
"無法確定",
|
||||
"A",
|
||||
"根據邏輯推理...",
|
||||
],
|
||||
["2", "在序列 2, 4, 8, 16, ? 中,下一個數字是?", "24", "32", "30", "28", "B", "每個數字都是前一個數字的2倍"],
|
||||
]
|
||||
.map((row) => row.join(","))
|
||||
.join("\n")
|
||||
} else {
|
||||
csvContent = [
|
||||
["題目ID", "陳述內容", "類別", "是否反向計分"],
|
||||
["1", "我經常能想出創新的解決方案", "innovation", "否"],
|
||||
["2", "我更喜歡按照既定規則工作", "flexibility", "是"],
|
||||
["3", "我喜歡嘗試新的做事方法", "innovation", "否"],
|
||||
]
|
||||
.map((row) => row.join(","))
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
const blob = new Blob(["\uFEFF" + csvContent], { type: "text/csv;charset=utf-8;" })
|
||||
const link = document.createElement("a")
|
||||
const url = URL.createObjectURL(blob)
|
||||
link.setAttribute("href", url)
|
||||
link.setAttribute("download", `${type === "logic" ? "邏輯思維" : "創意能力"}題目範本.csv`)
|
||||
link.style.visibility = "hidden"
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-card/50 backdrop-blur-sm">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href="/dashboard">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
返回儀表板
|
||||
</Link>
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">題目管理</h1>
|
||||
<p className="text-sm text-muted-foreground">管理測試題目和匯入新題目</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-6xl mx-auto space-y-8">
|
||||
{/* Import Section */}
|
||||
<Card className="border-2 border-primary/20">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Upload className="w-5 h-5 text-primary" />
|
||||
Excel 檔案匯入
|
||||
</CardTitle>
|
||||
<CardDescription>上傳 Excel 檔案來批量匯入測試題目。支援邏輯思維測試和創意能力測試題目。</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* File Upload */}
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="file-input">選擇 Excel 檔案</Label>
|
||||
<Input
|
||||
id="file-input"
|
||||
type="file"
|
||||
accept=".xlsx,.xls"
|
||||
onChange={handleFileSelect}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>題目類型</Label>
|
||||
<Select value={importType} onValueChange={(value: "logic" | "creative") => setImportType(value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="logic">邏輯思維測試</SelectItem>
|
||||
<SelectItem value="creative">創意能力測試</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedFile && (
|
||||
<div className="flex items-center gap-2 p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<FileSpreadsheet className="w-4 h-4 text-blue-600" />
|
||||
<span className="text-sm text-blue-800">已選擇:{selectedFile.name}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button onClick={handleImport} disabled={!selectedFile || isImporting} className="flex-1">
|
||||
{isImporting ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2" />
|
||||
匯入中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
開始匯入
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Import Result */}
|
||||
{importResult && (
|
||||
<Alert variant={importResult.success ? "default" : "destructive"}>
|
||||
{importResult.success ? <CheckCircle className="h-4 w-4" /> : <AlertCircle className="h-4 w-4" />}
|
||||
<AlertDescription>
|
||||
{importResult.message}
|
||||
{importResult.errors && (
|
||||
<ul className="mt-2 list-disc list-inside">
|
||||
{importResult.errors.map((error, index) => (
|
||||
<li key={index} className="text-sm">
|
||||
{error}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Template Download */}
|
||||
<div className="border-t pt-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Info className="w-4 h-4 text-blue-500" />
|
||||
<span className="text-sm font-medium">下載範本檔案</span>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" onClick={() => downloadTemplate("logic")} className="flex-1">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
邏輯思維範本
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => downloadTemplate("creative")} className="flex-1">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
創意能力範本
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Current Questions */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>現有題目管理</CardTitle>
|
||||
<CardDescription>查看和管理系統中的測試題目</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="logic" className="flex items-center gap-2">
|
||||
<Brain className="w-4 h-4" />
|
||||
邏輯思維 ({logicQuestions.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="creative" className="flex items-center gap-2">
|
||||
<Lightbulb className="w-4 h-4" />
|
||||
創意能力 ({creativeQuestions.length})
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="logic" className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-semibold">邏輯思維測試題目</h3>
|
||||
<Badge variant="outline">{logicQuestions.length} 道題目</Badge>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>題目ID</TableHead>
|
||||
<TableHead>題目內容</TableHead>
|
||||
<TableHead>選項數量</TableHead>
|
||||
<TableHead>正確答案</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{logicQuestions.slice(0, 10).map((question) => (
|
||||
<TableRow key={question.id}>
|
||||
<TableCell className="font-medium">{question.id}</TableCell>
|
||||
<TableCell className="max-w-md truncate">{question.question}</TableCell>
|
||||
<TableCell>{question.options.length}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className="bg-green-500 text-white">{question.correctAnswer}</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="creative" className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-semibold">創意能力測試題目</h3>
|
||||
<Badge variant="outline">{creativeQuestions.length} 道題目</Badge>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>題目ID</TableHead>
|
||||
<TableHead>陳述內容</TableHead>
|
||||
<TableHead>類別</TableHead>
|
||||
<TableHead>反向計分</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{creativeQuestions.slice(0, 10).map((question) => (
|
||||
<TableRow key={question.id}>
|
||||
<TableCell className="font-medium">{question.id}</TableCell>
|
||||
<TableCell className="max-w-md truncate">{question.statement}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">{question.category}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{question.isReverse ? (
|
||||
<Badge className="bg-orange-500 text-white">是</Badge>
|
||||
) : (
|
||||
<Badge variant="outline">否</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
3
app/admin/results/loading.tsx
Normal file
3
app/admin/results/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
421
app/admin/results/page.tsx
Normal file
421
app/admin/results/page.tsx
Normal file
@@ -0,0 +1,421 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { ProtectedRoute } from "@/components/protected-route"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Brain, Lightbulb, BarChart3, ArrowLeft, Search, Download, Filter } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useAuth, type User } from "@/lib/hooks/use-auth"
|
||||
|
||||
interface TestResult {
|
||||
userId: string
|
||||
userName: string
|
||||
userDepartment: string
|
||||
type: "logic" | "creative" | "combined"
|
||||
score: number
|
||||
completedAt: string
|
||||
details?: any
|
||||
}
|
||||
|
||||
export default function AdminResultsPage() {
|
||||
return (
|
||||
<ProtectedRoute adminOnly>
|
||||
<AdminResultsContent />
|
||||
</ProtectedRoute>
|
||||
)
|
||||
}
|
||||
|
||||
function AdminResultsContent() {
|
||||
const { user } = useAuth()
|
||||
const [results, setResults] = useState<TestResult[]>([])
|
||||
const [filteredResults, setFilteredResults] = useState<TestResult[]>([])
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [departmentFilter, setDepartmentFilter] = useState("all")
|
||||
const [testTypeFilter, setTestTypeFilter] = useState("all")
|
||||
const [stats, setStats] = useState({
|
||||
totalResults: 0,
|
||||
averageScore: 0,
|
||||
totalUsers: 0,
|
||||
completionRate: 0,
|
||||
})
|
||||
|
||||
const departments = ["人力資源部", "資訊技術部", "財務部", "行銷部", "業務部", "研發部", "客服部", "其他"]
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
filterResults()
|
||||
}, [results, searchTerm, departmentFilter, testTypeFilter])
|
||||
|
||||
const loadData = () => {
|
||||
// Load users
|
||||
const usersData = JSON.parse(localStorage.getItem("hr_users") || "[]")
|
||||
setUsers(usersData)
|
||||
|
||||
// Load all test results
|
||||
const allResults: TestResult[] = []
|
||||
|
||||
usersData.forEach((user: User) => {
|
||||
// Check for logic test results
|
||||
const logicKey = `logicTestResults_${user.id}`
|
||||
const logicResults = localStorage.getItem(logicKey)
|
||||
if (logicResults) {
|
||||
const data = JSON.parse(logicResults)
|
||||
allResults.push({
|
||||
userId: user.id,
|
||||
userName: user.name,
|
||||
userDepartment: user.department,
|
||||
type: "logic",
|
||||
score: data.score,
|
||||
completedAt: data.completedAt,
|
||||
details: data,
|
||||
})
|
||||
}
|
||||
|
||||
// Check for creative test results
|
||||
const creativeKey = `creativeTestResults_${user.id}`
|
||||
const creativeResults = localStorage.getItem(creativeKey)
|
||||
if (creativeResults) {
|
||||
const data = JSON.parse(creativeResults)
|
||||
allResults.push({
|
||||
userId: user.id,
|
||||
userName: user.name,
|
||||
userDepartment: user.department,
|
||||
type: "creative",
|
||||
score: data.score,
|
||||
completedAt: data.completedAt,
|
||||
details: data,
|
||||
})
|
||||
}
|
||||
|
||||
// Check for combined test results
|
||||
const combinedKey = `combinedTestResults_${user.id}`
|
||||
const combinedResults = localStorage.getItem(combinedKey)
|
||||
if (combinedResults) {
|
||||
const data = JSON.parse(combinedResults)
|
||||
allResults.push({
|
||||
userId: user.id,
|
||||
userName: user.name,
|
||||
userDepartment: user.department,
|
||||
type: "combined",
|
||||
score: data.overallScore,
|
||||
completedAt: data.completedAt,
|
||||
details: data,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Sort by completion date (newest first)
|
||||
allResults.sort((a, b) => new Date(b.completedAt).getTime() - new Date(a.completedAt).getTime())
|
||||
setResults(allResults)
|
||||
|
||||
// Calculate statistics
|
||||
const totalResults = allResults.length
|
||||
const averageScore =
|
||||
totalResults > 0 ? Math.round(allResults.reduce((sum, r) => sum + r.score, 0) / totalResults) : 0
|
||||
const totalUsers = usersData.length
|
||||
const usersWithResults = new Set(allResults.map((r) => r.userId)).size
|
||||
const completionRate = totalUsers > 0 ? Math.round((usersWithResults / totalUsers) * 100) : 0
|
||||
|
||||
setStats({
|
||||
totalResults,
|
||||
averageScore,
|
||||
totalUsers,
|
||||
completionRate,
|
||||
})
|
||||
}
|
||||
|
||||
const filterResults = () => {
|
||||
let filtered = results
|
||||
|
||||
// Filter by search term (user name)
|
||||
if (searchTerm) {
|
||||
filtered = filtered.filter((result) => result.userName.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
}
|
||||
|
||||
// Filter by department
|
||||
if (departmentFilter !== "all") {
|
||||
filtered = filtered.filter((result) => result.userDepartment === departmentFilter)
|
||||
}
|
||||
|
||||
// Filter by test type
|
||||
if (testTypeFilter !== "all") {
|
||||
filtered = filtered.filter((result) => result.type === testTypeFilter)
|
||||
}
|
||||
|
||||
setFilteredResults(filtered)
|
||||
}
|
||||
|
||||
const getTestTypeInfo = (type: string) => {
|
||||
switch (type) {
|
||||
case "logic":
|
||||
return {
|
||||
name: "邏輯思維",
|
||||
icon: Brain,
|
||||
color: "bg-primary",
|
||||
textColor: "text-primary",
|
||||
}
|
||||
case "creative":
|
||||
return {
|
||||
name: "創意能力",
|
||||
icon: Lightbulb,
|
||||
color: "bg-accent",
|
||||
textColor: "text-accent",
|
||||
}
|
||||
case "combined":
|
||||
return {
|
||||
name: "綜合能力",
|
||||
icon: BarChart3,
|
||||
color: "bg-gradient-to-r from-primary to-accent",
|
||||
textColor: "text-primary",
|
||||
}
|
||||
default:
|
||||
return {
|
||||
name: "未知",
|
||||
icon: BarChart3,
|
||||
color: "bg-muted",
|
||||
textColor: "text-muted-foreground",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getScoreLevel = (score: number) => {
|
||||
if (score >= 90) return { level: "優秀", color: "bg-green-500" }
|
||||
if (score >= 80) return { level: "良好", color: "bg-blue-500" }
|
||||
if (score >= 70) return { level: "中等", color: "bg-yellow-500" }
|
||||
if (score >= 60) return { level: "及格", color: "bg-orange-500" }
|
||||
return { level: "不及格", color: "bg-red-500" }
|
||||
}
|
||||
|
||||
const exportResults = () => {
|
||||
const csvContent = [
|
||||
["姓名", "部門", "測試類型", "分數", "等級", "完成時間"],
|
||||
...filteredResults.map((result) => [
|
||||
result.userName,
|
||||
result.userDepartment,
|
||||
getTestTypeInfo(result.type).name,
|
||||
result.score.toString(),
|
||||
getScoreLevel(result.score).level,
|
||||
new Date(result.completedAt).toLocaleString("zh-TW"),
|
||||
]),
|
||||
]
|
||||
.map((row) => row.join(","))
|
||||
.join("\n")
|
||||
|
||||
const blob = new Blob(["\uFEFF" + csvContent], { type: "text/csv;charset=utf-8;" })
|
||||
const link = document.createElement("a")
|
||||
const url = URL.createObjectURL(blob)
|
||||
link.setAttribute("href", url)
|
||||
link.setAttribute("download", `測試結果_${new Date().toISOString().split("T")[0]}.csv`)
|
||||
link.style.visibility = "hidden"
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-card/50 backdrop-blur-sm">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href="/dashboard">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
返回儀表板
|
||||
</Link>
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">所有測試結果</h1>
|
||||
<p className="text-sm text-muted-foreground">查看和分析所有員工的測試結果</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-7xl mx-auto space-y-8">
|
||||
{/* Statistics Overview */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<BarChart3 className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-foreground mb-1">{stats.totalResults}</div>
|
||||
<div className="text-sm text-muted-foreground">總測試次數</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="w-12 h-12 bg-blue-500/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<Brain className="w-6 h-6 text-blue-500" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-foreground mb-1">{stats.averageScore}</div>
|
||||
<div className="text-sm text-muted-foreground">平均分數</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="w-12 h-12 bg-green-500/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<Lightbulb className="w-6 h-6 text-green-500" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-foreground mb-1">{stats.totalUsers}</div>
|
||||
<div className="text-sm text-muted-foreground">總用戶數</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="w-12 h-12 bg-accent/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<BarChart3 className="w-6 h-6 text-accent" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-foreground mb-1">{stats.completionRate}%</div>
|
||||
<div className="text-sm text-muted-foreground">參與率</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Filter className="w-5 h-5" />
|
||||
篩選條件
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">搜尋用戶</label>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="輸入用戶姓名"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">部門</label>
|
||||
<Select value={departmentFilter} onValueChange={setDepartmentFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">所有部門</SelectItem>
|
||||
{departments.map((dept) => (
|
||||
<SelectItem key={dept} value={dept}>
|
||||
{dept}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">測試類型</label>
|
||||
<Select value={testTypeFilter} onValueChange={setTestTypeFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">所有類型</SelectItem>
|
||||
<SelectItem value="logic">邏輯思維</SelectItem>
|
||||
<SelectItem value="creative">創意能力</SelectItem>
|
||||
<SelectItem value="combined">綜合能力</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">操作</label>
|
||||
<Button onClick={exportResults} className="w-full">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
匯出結果
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Results Table */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>測試結果列表</CardTitle>
|
||||
<CardDescription>
|
||||
顯示 {filteredResults.length} 筆結果(共 {results.length} 筆)
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>用戶</TableHead>
|
||||
<TableHead>部門</TableHead>
|
||||
<TableHead>測試類型</TableHead>
|
||||
<TableHead>分數</TableHead>
|
||||
<TableHead>等級</TableHead>
|
||||
<TableHead>完成時間</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredResults.map((result, index) => {
|
||||
const testInfo = getTestTypeInfo(result.type)
|
||||
const scoreLevel = getScoreLevel(result.score)
|
||||
const Icon = testInfo.icon
|
||||
|
||||
return (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<div className="font-medium">{result.userName}</div>
|
||||
</TableCell>
|
||||
<TableCell>{result.userDepartment}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-8 h-8 ${testInfo.color} rounded-lg flex items-center justify-center`}>
|
||||
<Icon className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<span className={testInfo.textColor}>{testInfo.name}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-lg font-bold">{result.score}</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={`${scoreLevel.color} text-white`}>{scoreLevel.level}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm">{new Date(result.completedAt).toLocaleString("zh-TW")}</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{filteredResults.length === 0 && (
|
||||
<div className="text-center py-8">
|
||||
<div className="text-muted-foreground">沒有找到符合條件的測試結果</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
452
app/admin/users/page.tsx
Normal file
452
app/admin/users/page.tsx
Normal file
@@ -0,0 +1,452 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { ProtectedRoute } from "@/components/protected-route"
|
||||
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { Plus, Edit, Trash2, ArrowLeft } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useAuth, type User } from "@/lib/hooks/use-auth"
|
||||
|
||||
export default function UsersManagementPage() {
|
||||
return (
|
||||
<ProtectedRoute adminOnly>
|
||||
<UsersManagementContent />
|
||||
</ProtectedRoute>
|
||||
)
|
||||
}
|
||||
|
||||
function UsersManagementContent() {
|
||||
const { user: currentUser } = useAuth()
|
||||
const [users, setUsers] = useState<(User & { password?: string })[]>([])
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
||||
const [editingUser, setEditingUser] = useState<User | null>(null)
|
||||
const [newUser, setNewUser] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
department: "",
|
||||
role: "user" as "user" | "admin",
|
||||
})
|
||||
const [error, setError] = useState("")
|
||||
|
||||
const departments = ["人力資源部", "資訊技術部", "財務部", "行銷部", "業務部", "研發部", "客服部", "其他"]
|
||||
|
||||
useEffect(() => {
|
||||
loadUsers()
|
||||
}, [])
|
||||
|
||||
const loadUsers = () => {
|
||||
const usersData = JSON.parse(localStorage.getItem("hr_users") || "[]")
|
||||
setUsers(usersData)
|
||||
}
|
||||
|
||||
const handleAddUser = () => {
|
||||
setError("")
|
||||
|
||||
if (!newUser.name || !newUser.email || !newUser.password || !newUser.department) {
|
||||
setError("請填寫所有必填欄位")
|
||||
return
|
||||
}
|
||||
|
||||
if (users.some((u) => u.email === newUser.email)) {
|
||||
setError("該電子郵件已被使用")
|
||||
return
|
||||
}
|
||||
|
||||
const user = {
|
||||
...newUser,
|
||||
id: `user-${Date.now()}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
const updatedUsers = [...users, user]
|
||||
setUsers(updatedUsers)
|
||||
localStorage.setItem("hr_users", JSON.stringify(updatedUsers))
|
||||
|
||||
setNewUser({
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
department: "",
|
||||
role: "user",
|
||||
})
|
||||
setIsAddDialogOpen(false)
|
||||
}
|
||||
|
||||
const handleEditUser = (user: User) => {
|
||||
setEditingUser(user)
|
||||
setNewUser({
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
password: "",
|
||||
department: user.department,
|
||||
role: user.role,
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpdateUser = () => {
|
||||
if (!editingUser) return
|
||||
|
||||
setError("")
|
||||
|
||||
if (!newUser.name || !newUser.email || !newUser.department) {
|
||||
setError("請填寫所有必填欄位")
|
||||
return
|
||||
}
|
||||
|
||||
if (users.some((u) => u.email === newUser.email && u.id !== editingUser.id)) {
|
||||
setError("該電子郵件已被使用")
|
||||
return
|
||||
}
|
||||
|
||||
const updatedUsers = users.map((u) =>
|
||||
u.id === editingUser.id
|
||||
? {
|
||||
...u,
|
||||
name: newUser.name,
|
||||
email: newUser.email,
|
||||
department: newUser.department,
|
||||
role: newUser.role,
|
||||
...(newUser.password && { password: newUser.password }),
|
||||
}
|
||||
: u,
|
||||
)
|
||||
|
||||
setUsers(updatedUsers)
|
||||
localStorage.setItem("hr_users", JSON.stringify(updatedUsers))
|
||||
|
||||
setEditingUser(null)
|
||||
setNewUser({
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
department: "",
|
||||
role: "user",
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteUser = (userId: string) => {
|
||||
if (userId === currentUser?.id) {
|
||||
setError("無法刪除自己的帳戶")
|
||||
return
|
||||
}
|
||||
|
||||
const updatedUsers = users.filter((u) => u.id !== userId)
|
||||
setUsers(updatedUsers)
|
||||
localStorage.setItem("hr_users", JSON.stringify(updatedUsers))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-card/50 backdrop-blur-sm">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href="/dashboard">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
返回儀表板
|
||||
</Link>
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">用戶管理</h1>
|
||||
<p className="text-sm text-muted-foreground">管理系統用戶和權限</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">總用戶數</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{users.length}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">管理員</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{users.filter((u) => u.role === "admin").length}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">一般用戶</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{users.filter((u) => u.role === "user").length}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Users Table */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>用戶列表</CardTitle>
|
||||
<CardDescription>管理系統中的所有用戶</CardDescription>
|
||||
</div>
|
||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
新增用戶
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>新增用戶</DialogTitle>
|
||||
<DialogDescription>建立新的系統用戶帳戶</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">姓名</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={newUser.name}
|
||||
onChange={(e) => setNewUser({ ...newUser, name: e.target.value })}
|
||||
placeholder="請輸入姓名"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">電子郵件</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={newUser.email}
|
||||
onChange={(e) => setNewUser({ ...newUser, email: e.target.value })}
|
||||
placeholder="請輸入電子郵件"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">密碼</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={newUser.password}
|
||||
onChange={(e) => setNewUser({ ...newUser, password: e.target.value })}
|
||||
placeholder="請輸入密碼"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="department">部門</Label>
|
||||
<Select
|
||||
value={newUser.department}
|
||||
onValueChange={(value) => setNewUser({ ...newUser, department: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="請選擇部門" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{departments.map((dept) => (
|
||||
<SelectItem key={dept} value={dept}>
|
||||
{dept}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="role">角色</Label>
|
||||
<Select
|
||||
value={newUser.role}
|
||||
onValueChange={(value: "user" | "admin") => setNewUser({ ...newUser, role: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="user">一般用戶</SelectItem>
|
||||
<SelectItem value="admin">管理員</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleAddUser} className="flex-1">
|
||||
新增用戶
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setIsAddDialogOpen(false)} className="flex-1">
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>姓名</TableHead>
|
||||
<TableHead>電子郵件</TableHead>
|
||||
<TableHead>部門</TableHead>
|
||||
<TableHead>角色</TableHead>
|
||||
<TableHead>建立時間</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell className="font-medium">{user.name}</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell>{user.department}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={user.role === "admin" ? "default" : "secondary"}>
|
||||
{user.role === "admin" ? "管理員" : "一般用戶"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{new Date(user.createdAt).toLocaleDateString()}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => handleEditUser(user)}>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
{user.id !== currentUser?.id && (
|
||||
<Button variant="ghost" size="sm" onClick={() => handleDeleteUser(user.id)}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Edit User Dialog */}
|
||||
<Dialog open={!!editingUser} onOpenChange={() => setEditingUser(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>編輯用戶</DialogTitle>
|
||||
<DialogDescription>修改用戶資訊和權限</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-name">姓名</Label>
|
||||
<Input
|
||||
id="edit-name"
|
||||
value={newUser.name}
|
||||
onChange={(e) => setNewUser({ ...newUser, name: e.target.value })}
|
||||
placeholder="請輸入姓名"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-email">電子郵件</Label>
|
||||
<Input
|
||||
id="edit-email"
|
||||
type="email"
|
||||
value={newUser.email}
|
||||
onChange={(e) => setNewUser({ ...newUser, email: e.target.value })}
|
||||
placeholder="請輸入電子郵件"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-password">新密碼(留空表示不修改)</Label>
|
||||
<Input
|
||||
id="edit-password"
|
||||
type="password"
|
||||
value={newUser.password}
|
||||
onChange={(e) => setNewUser({ ...newUser, password: e.target.value })}
|
||||
placeholder="請輸入新密碼"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-department">部門</Label>
|
||||
<Select
|
||||
value={newUser.department}
|
||||
onValueChange={(value) => setNewUser({ ...newUser, department: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="請選擇部門" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{departments.map((dept) => (
|
||||
<SelectItem key={dept} value={dept}>
|
||||
{dept}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-role">角色</Label>
|
||||
<Select
|
||||
value={newUser.role}
|
||||
onValueChange={(value: "user" | "admin") => setNewUser({ ...newUser, role: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="user">一般用戶</SelectItem>
|
||||
<SelectItem value="admin">管理員</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleUpdateUser} className="flex-1">
|
||||
更新用戶
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setEditingUser(null)} className="flex-1">
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
33
app/dashboard/page.tsx
Normal file
33
app/dashboard/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useAuth } from "@/lib/hooks/use-auth"
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { user, isLoading } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
if (!user) {
|
||||
router.push("/")
|
||||
} else {
|
||||
router.push("/home")
|
||||
}
|
||||
}
|
||||
}, [user, isLoading, router])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
|
||||
<p className="text-muted-foreground">載入中...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
126
app/globals.css
Normal file
126
app/globals.css
Normal file
@@ -0,0 +1,126 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
/* 更新为专业HR系统配色方案 */
|
||||
--background: oklch(0.98 0.005 106);
|
||||
--foreground: oklch(0.15 0.02 258);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.15 0.02 258);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.15 0.02 258);
|
||||
--primary: oklch(0.45 0.15 258);
|
||||
--primary-foreground: oklch(0.98 0.005 106);
|
||||
--secondary: oklch(0.92 0.02 106);
|
||||
--secondary-foreground: oklch(0.15 0.02 258);
|
||||
--muted: oklch(0.95 0.01 106);
|
||||
--muted-foreground: oklch(0.55 0.02 258);
|
||||
--accent: oklch(0.65 0.12 180);
|
||||
--accent-foreground: oklch(0.98 0.005 106);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.98 0.005 106);
|
||||
--border: oklch(0.88 0.02 106);
|
||||
--input: oklch(0.95 0.01 106);
|
||||
--ring: oklch(0.45 0.15 258);
|
||||
--chart-1: oklch(0.45 0.15 258);
|
||||
--chart-2: oklch(0.65 0.12 180);
|
||||
--chart-3: oklch(0.55 0.18 140);
|
||||
--chart-4: oklch(0.75 0.08 60);
|
||||
--chart-5: oklch(0.35 0.12 300);
|
||||
--radius: 0.75rem;
|
||||
--sidebar: oklch(0.98 0.005 106);
|
||||
--sidebar-foreground: oklch(0.15 0.02 258);
|
||||
--sidebar-primary: oklch(0.45 0.15 258);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.005 106);
|
||||
--sidebar-accent: oklch(0.92 0.02 106);
|
||||
--sidebar-accent-foreground: oklch(0.15 0.02 258);
|
||||
--sidebar-border: oklch(0.88 0.02 106);
|
||||
--sidebar-ring: oklch(0.45 0.15 258);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.08 0.02 258);
|
||||
--foreground: oklch(0.95 0.01 106);
|
||||
--card: oklch(0.12 0.02 258);
|
||||
--card-foreground: oklch(0.95 0.01 106);
|
||||
--popover: oklch(0.12 0.02 258);
|
||||
--popover-foreground: oklch(0.95 0.01 106);
|
||||
--primary: oklch(0.65 0.15 258);
|
||||
--primary-foreground: oklch(0.08 0.02 258);
|
||||
--secondary: oklch(0.18 0.02 258);
|
||||
--secondary-foreground: oklch(0.95 0.01 106);
|
||||
--muted: oklch(0.15 0.02 258);
|
||||
--muted-foreground: oklch(0.65 0.02 258);
|
||||
--accent: oklch(0.55 0.12 180);
|
||||
--accent-foreground: oklch(0.08 0.02 258);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.95 0.01 106);
|
||||
--border: oklch(0.22 0.02 258);
|
||||
--input: oklch(0.18 0.02 258);
|
||||
--ring: oklch(0.65 0.15 258);
|
||||
--chart-1: oklch(0.65 0.15 258);
|
||||
--chart-2: oklch(0.55 0.12 180);
|
||||
--chart-3: oklch(0.45 0.18 140);
|
||||
--chart-4: oklch(0.65 0.08 60);
|
||||
--chart-5: oklch(0.55 0.12 300);
|
||||
--sidebar: oklch(0.12 0.02 258);
|
||||
--sidebar-foreground: oklch(0.95 0.01 106);
|
||||
--sidebar-primary: oklch(0.65 0.15 258);
|
||||
--sidebar-primary-foreground: oklch(0.08 0.02 258);
|
||||
--sidebar-accent: oklch(0.18 0.02 258);
|
||||
--sidebar-accent-foreground: oklch(0.95 0.01 106);
|
||||
--sidebar-border: oklch(0.22 0.02 258);
|
||||
--sidebar-ring: oklch(0.65 0.15 258);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: var(--font-inter);
|
||||
--font-mono: var(--font-jetbrains-mono);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
390
app/home/page.tsx
Normal file
390
app/home/page.tsx
Normal file
@@ -0,0 +1,390 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect, useRef } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Brain, Lightbulb, BarChart3, Users, Settings, Menu, ChevronDown } from "lucide-react"
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
|
||||
import Link from "next/link"
|
||||
import { useAuth } from "@/lib/hooks/use-auth"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { ProtectedRoute } from "@/components/protected-route"
|
||||
|
||||
export default function HomePage() {
|
||||
const { user, logout } = useAuth()
|
||||
const router = useRouter()
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleLogout = () => {
|
||||
logout()
|
||||
router.push("/")
|
||||
}
|
||||
|
||||
// 點擊外部關閉下拉選單
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsDropdownOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (isDropdownOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [isDropdownOpen])
|
||||
|
||||
// 調試信息
|
||||
console.log("Current user:", user)
|
||||
|
||||
return (
|
||||
<ProtectedRoute>
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-card/50 backdrop-blur-sm sticky top-0 z-50">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-primary rounded-lg flex items-center justify-center">
|
||||
<Users className="w-6 h-6 text-primary-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">HR 評估系統</h1>
|
||||
<p className="text-sm text-muted-foreground">員工能力測評平台</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:flex items-center gap-6">
|
||||
{/* Navigation Links */}
|
||||
<div className="flex items-center gap-12">
|
||||
<Link
|
||||
href={user?.role === "admin" ? "/admin/results" : "/results"}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{user?.role === "admin" ? "所有測試結果" : "我的測試結果"}
|
||||
</Link>
|
||||
{user?.role === "admin" && (
|
||||
<Link
|
||||
href="/admin/analytics"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
部門分析
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 自定義下拉選單 */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex items-center gap-3 hover:bg-accent px-6 py-6 text-foreground rounded-lg group"
|
||||
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||
>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium text-foreground group-hover:text-white">{user?.name || '系統管理員'}</p>
|
||||
<p className="text-xs text-muted-foreground group-hover:text-white/80">{user?.department || '人力資源部'}</p>
|
||||
</div>
|
||||
<ChevronDown className={`w-4 h-4 transition-transform group-hover:text-white ${isDropdownOpen ? 'rotate-180' : ''}`} />
|
||||
</Button>
|
||||
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute right-0 top-full mt-1 w-56 bg-background border border-border rounded-md shadow-lg z-50">
|
||||
<div className="py-1">
|
||||
<Link
|
||||
href="/settings"
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm text-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => setIsDropdownOpen(false)}
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
帳戶設定
|
||||
</Link>
|
||||
{user?.role === "admin" && (
|
||||
<>
|
||||
<div className="border-t border-border my-1"></div>
|
||||
<Link
|
||||
href="/admin/users"
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm text-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => setIsDropdownOpen(false)}
|
||||
>
|
||||
<Users className="w-4 h-4" />
|
||||
用戶管理
|
||||
</Link>
|
||||
<Link
|
||||
href="/admin/questions"
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm text-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => setIsDropdownOpen(false)}
|
||||
>
|
||||
<Brain className="w-4 h-4" />
|
||||
題目管理
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<div className="border-t border-border my-1"></div>
|
||||
<button
|
||||
onClick={() => {
|
||||
handleLogout()
|
||||
setIsDropdownOpen(false)
|
||||
}}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm text-red-600 hover:bg-red-50 hover:text-red-700 w-full text-left"
|
||||
>
|
||||
登出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="md:hidden">
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Menu className="w-5 h-5" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle className="text-left">選單</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="mt-6 space-y-4 px-4">
|
||||
<div className="pb-4 border-b">
|
||||
<p className="font-medium">{user?.name}</p>
|
||||
<p className="text-sm text-muted-foreground">{user?.department}</p>
|
||||
</div>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<Button asChild variant="ghost" className="w-full justify-start px-4">
|
||||
<Link href={user?.role === "admin" ? "/admin/results" : "/results"} className="flex items-center gap-2">
|
||||
<BarChart3 className="w-4 h-4" />
|
||||
{user?.role === "admin" ? "所有測試結果" : "我的測試結果"}
|
||||
</Link>
|
||||
</Button>
|
||||
{user?.role === "admin" && (
|
||||
<Button asChild variant="ghost" className="w-full justify-start px-4">
|
||||
<Link href="/admin/analytics" className="flex items-center gap-2">
|
||||
<BarChart3 className="w-4 h-4" />
|
||||
部門分析
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button asChild variant="ghost" className="w-full justify-start px-4">
|
||||
<Link href="/settings" className="flex items-center gap-2">
|
||||
<Settings className="w-4 h-4" />
|
||||
帳戶設定
|
||||
</Link>
|
||||
</Button>
|
||||
{user?.role === "admin" && (
|
||||
<>
|
||||
<Button asChild variant="ghost" className="w-full justify-start px-4">
|
||||
<Link href="/admin/users" className="flex items-center gap-2">
|
||||
<Users className="w-4 h-4" />
|
||||
用戶管理
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="ghost" className="w-full justify-start px-4">
|
||||
<Link href="/admin/questions" className="flex items-center gap-2">
|
||||
<Brain className="w-4 h-4" />
|
||||
題目管理
|
||||
</Link>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button onClick={handleLogout} variant="ghost" className="w-full justify-start text-red-600 px-4">
|
||||
登出
|
||||
</Button>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="py-8 md:py-16 px-4 flex items-center">
|
||||
<div className="container mx-auto text-center">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4 md:mb-6 text-balance">
|
||||
歡迎回來,{user?.name}
|
||||
</h2>
|
||||
<p className="text-lg md:text-xl text-muted-foreground text-pretty leading-relaxed">
|
||||
透過科學的邏輯思維測試和創意能力評估,全面了解您的綜合素質
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Test Cards / Admin Info Cards */}
|
||||
<section className="py-8 md:py-16 px-4">
|
||||
<div className="container mx-auto">
|
||||
{user?.role === "admin" ? (
|
||||
// 管理者看到的介紹卡片
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8 max-w-6xl mx-auto">
|
||||
{/* 邏輯思維測試介紹 */}
|
||||
<Card className="group hover:shadow-lg transition-all duration-300 border-2 hover:border-primary/20">
|
||||
<CardHeader className="text-center pb-4">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-3 group-hover:bg-primary/20 transition-colors">
|
||||
<Brain className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<CardTitle className="text-lg">邏輯思維測試</CardTitle>
|
||||
<CardDescription>評估邏輯推理、分析判斷和問題解決能力</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-center">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs text-muted-foreground">題目數量</span>
|
||||
<span className="text-sm font-bold text-primary">10題</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs text-muted-foreground">題目類型</span>
|
||||
<span className="text-sm font-bold text-primary">單選題</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs text-muted-foreground">預計時間</span>
|
||||
<span className="text-sm font-bold text-primary">15-20分鐘</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 創意能力測試介紹 */}
|
||||
<Card className="group hover:shadow-lg transition-all duration-300 border-2 hover:border-accent/20">
|
||||
<CardHeader className="text-center pb-4">
|
||||
<div className="w-12 h-12 bg-accent/10 rounded-full flex items-center justify-center mx-auto mb-3 group-hover:bg-accent/20 transition-colors">
|
||||
<Lightbulb className="w-6 h-6 text-accent" />
|
||||
</div>
|
||||
<CardTitle className="text-lg">創意能力測試</CardTitle>
|
||||
<CardDescription>評估創新思維、想像力和創造性解決問題的能力</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-center">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs text-muted-foreground">題目數量</span>
|
||||
<span className="text-sm font-bold text-accent">20題</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs text-muted-foreground">題目類型</span>
|
||||
<span className="text-sm font-bold text-accent">5級量表</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs text-muted-foreground">預計時間</span>
|
||||
<span className="text-sm font-bold text-accent">25-30分鐘</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 綜合測試介紹 */}
|
||||
<Card className="group hover:shadow-lg transition-all duration-300 border-2 border-primary/20 bg-gradient-to-br from-primary/5 to-accent/5 md:col-span-2 lg:col-span-1">
|
||||
<CardHeader className="text-center pb-4">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-primary to-accent rounded-full flex items-center justify-center mx-auto mb-3 group-hover:scale-110 transition-transform">
|
||||
<BarChart3 className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<CardTitle className="text-lg">綜合測試</CardTitle>
|
||||
<CardDescription>完整的邏輯思維 + 創意能力雙重評估,獲得全面的能力報告</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-center">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs text-muted-foreground">總題目數</span>
|
||||
<span className="text-sm font-bold text-primary">30題</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs text-muted-foreground">預計時間</span>
|
||||
<span className="text-sm font-bold text-accent">45分鐘</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
// 一般用戶看到的測試功能
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8 max-w-6xl mx-auto">
|
||||
{/* Logic Test */}
|
||||
<Card className="group hover:shadow-lg transition-all duration-300 border-2 hover:border-primary/20">
|
||||
<CardHeader className="text-center pb-4">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-3 group-hover:bg-primary/20 transition-colors">
|
||||
<Brain className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<CardTitle className="text-lg">邏輯思維測試</CardTitle>
|
||||
<CardDescription>評估邏輯推理能力</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-center">
|
||||
<Button asChild className="w-full">
|
||||
<Link href="/tests/logic">開始測試</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Creative Test */}
|
||||
<Card className="group hover:shadow-lg transition-all duration-300 border-2 hover:border-accent/20">
|
||||
<CardHeader className="text-center pb-4">
|
||||
<div className="w-12 h-12 bg-accent/10 rounded-full flex items-center justify-center mx-auto mb-3 group-hover:bg-accent/20 transition-colors">
|
||||
<Lightbulb className="w-6 h-6 text-accent" />
|
||||
</div>
|
||||
<CardTitle className="text-lg">創意能力測試</CardTitle>
|
||||
<CardDescription>評估創新思維能力</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-center">
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="w-full border-accent text-accent hover:bg-accent hover:text-accent-foreground bg-transparent"
|
||||
>
|
||||
<Link href="/tests/creative">開始測試</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Combined Test */}
|
||||
<Card className="group hover:shadow-lg transition-all duration-300 border-2 border-primary/20 bg-gradient-to-br from-primary/5 to-accent/5 md:col-span-2 lg:col-span-1">
|
||||
<CardHeader className="text-center pb-4">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-primary to-accent rounded-full flex items-center justify-center mx-auto mb-3 group-hover:scale-110 transition-transform">
|
||||
<BarChart3 className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<CardTitle className="text-lg">綜合測試</CardTitle>
|
||||
<CardDescription>完整能力評估</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-center">
|
||||
<Button asChild size="lg" className="w-full">
|
||||
<Link href="/tests/combined">開始測試</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="border-t bg-muted/30 py-12 px-4 mt-16">
|
||||
<div className="container mx-auto">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
{/* 左側內容 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
|
||||
<Users className="w-4 h-4 text-primary-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-lg font-semibold">HR 評估系統</span>
|
||||
<p className="text-muted-foreground text-sm">專業的員工能力測評解決方案,助力企業人才發展</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右側內容 */}
|
||||
<div className="text-sm text-muted-foreground">
|
||||
© 2025 HR 評估系統. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
)
|
||||
}
|
35
app/layout.tsx
Normal file
35
app/layout.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import type React from "react"
|
||||
import type { Metadata } from "next"
|
||||
import { Inter, JetBrains_Mono } from "next/font/google"
|
||||
import "./globals.css"
|
||||
import { AuthProvider } from "@/components/auth-provider"
|
||||
|
||||
const inter = Inter({
|
||||
variable: "--font-inter",
|
||||
subsets: ["latin"],
|
||||
})
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
variable: "--font-jetbrains-mono",
|
||||
subsets: ["latin"],
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "HR 評估系統",
|
||||
description: "專業的員工能力測評平台",
|
||||
generator: 'v0.app'
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-TW" className={`${inter.variable} ${jetbrainsMono.variable} antialiased`}>
|
||||
<body>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
129
app/login/page.tsx
Normal file
129
app/login/page.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
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 { Users, Eye, EyeOff } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useAuth } from "@/lib/hooks/use-auth"
|
||||
|
||||
export default function LoginPage() {
|
||||
const [email, setEmail] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
const { login } = useAuth()
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError("")
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const success = await login(email, password)
|
||||
if (success) {
|
||||
router.push("/dashboard")
|
||||
} else {
|
||||
setError("帳號或密碼錯誤")
|
||||
}
|
||||
} catch (err) {
|
||||
setError("登入失敗,請稍後再試")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-primary rounded-lg flex items-center justify-center mx-auto mb-4">
|
||||
<Users className="w-8 h-8 text-primary-foreground" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-foreground">HR 評估系統</h1>
|
||||
<p className="text-muted-foreground">請登入您的帳戶</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>登入</CardTitle>
|
||||
<CardDescription>輸入您的帳號和密碼以存取系統</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">電子郵件</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="請輸入電子郵件"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">密碼</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="請輸入密碼"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? "登入中..." : "登入"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
還沒有帳戶?{" "}
|
||||
<Link href="/register" className="text-primary hover:underline">
|
||||
立即註冊
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-4 bg-muted/50 rounded-lg">
|
||||
<p className="text-sm font-medium mb-2">測試帳戶:</p>
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<p>管理者:admin@company.com / admin123</p>
|
||||
<p>員工:user@company.com / user123</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
171
app/page.tsx
Normal file
171
app/page.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useAuth } from "@/lib/hooks/use-auth"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Users, Brain, Lightbulb, BarChart3, Shield, Clock } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
export default function HomePage() {
|
||||
const { user, isLoading } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && user) {
|
||||
router.push("/home")
|
||||
}
|
||||
}, [user, isLoading, router])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
|
||||
<p className="text-muted-foreground">載入中...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (user) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50">
|
||||
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-primary rounded-lg flex items-center justify-center">
|
||||
<Users className="w-6 h-6 text-primary-foreground" />
|
||||
</div>
|
||||
<span className="text-xl font-bold">HR 評估系統</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/login" className="text-muted-foreground hover:text-foreground transition-colors">
|
||||
登入
|
||||
</Link>
|
||||
<Button asChild size="sm">
|
||||
<Link href="/register">註冊</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="py-12 px-4">
|
||||
<div className="container mx-auto text-center">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-primary to-accent rounded-xl flex items-center justify-center mx-auto mb-6">
|
||||
<Users className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-foreground mb-4 text-balance">HR 評估系統</h1>
|
||||
<p className="text-lg md:text-xl text-muted-foreground mb-8 text-pretty leading-relaxed max-w-2xl mx-auto">
|
||||
專業的員工能力測評平台,透過科學的邏輯思維測試和創意能力評估,全面了解員工的綜合素質
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features Section */}
|
||||
<section className="py-20 px-4 bg-muted/30">
|
||||
<div className="container mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-4xl font-bold text-foreground mb-6">系統特色</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-3xl mx-auto text-pretty">
|
||||
提供專業的測試類型和完整的分析報告,幫助企業全面評估員工能力
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto">
|
||||
<div className="bg-background rounded-xl p-8 shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="w-16 h-16 bg-primary/10 rounded-xl flex items-center justify-center mb-6">
|
||||
<Brain className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold mb-4">邏輯思維測試</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
評估邏輯推理、分析判斷和問題解決能力,幫助識別具備優秀思維能力的人才
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-background rounded-xl p-8 shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="w-16 h-16 bg-accent/10 rounded-xl flex items-center justify-center mb-6">
|
||||
<Lightbulb className="w-8 h-8 text-accent" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold mb-4">創意能力測試</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
評估創新思維、想像力和創造性解決問題的能力,發掘具有創新潛力的員工
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-background rounded-xl p-8 shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-primary to-accent rounded-xl flex items-center justify-center mb-6">
|
||||
<BarChart3 className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold mb-4">詳細分析報告</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
提供完整的能力分析報告和發展建議,協助制定個人化的培訓計畫
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-background rounded-xl p-8 shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-xl flex items-center justify-center mb-6">
|
||||
<Shield className="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold mb-4">安全可靠</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
採用先進的資料加密技術,確保測試結果和個人資料的安全性
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-background rounded-xl p-8 shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="w-16 h-16 bg-blue-100 rounded-xl flex items-center justify-center mb-6">
|
||||
<Clock className="w-8 h-8 text-blue-600" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold mb-4">高效便捷</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
線上測試系統,隨時隨地進行評估,大幅提升HR工作效率
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-background rounded-xl p-8 shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="w-16 h-16 bg-purple-100 rounded-xl flex items-center justify-center mb-6">
|
||||
<Users className="w-8 h-8 text-purple-600" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold mb-4">多角色管理</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
支援管理者和員工不同角色,提供個人化的使用體驗和權限管理
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="border-t bg-muted/30 py-12 px-4">
|
||||
<div className="container mx-auto">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
{/* 左側內容 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
|
||||
<Users className="w-4 h-4 text-primary-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-lg font-semibold">HR 評估系統</span>
|
||||
<p className="text-muted-foreground text-sm">專業的員工能力測評解決方案,助力企業人才發展</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右側內容 */}
|
||||
<div className="text-sm text-muted-foreground">
|
||||
© 2025 HR 評估系統. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
203
app/register/page.tsx
Normal file
203
app/register/page.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { Users, Eye, EyeOff } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useAuth } from "@/lib/hooks/use-auth"
|
||||
|
||||
export default function RegisterPage() {
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
department: "",
|
||||
role: "user" as "user" | "admin",
|
||||
})
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
const { register } = useAuth()
|
||||
|
||||
const departments = ["人力資源部", "資訊技術部", "財務部", "行銷部", "業務部", "研發部", "客服部", "其他"]
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError("")
|
||||
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
setError("密碼確認不一致")
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.password.length < 6) {
|
||||
setError("密碼長度至少需要6個字元")
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const success = await register({
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
department: formData.department,
|
||||
role: formData.role,
|
||||
})
|
||||
|
||||
if (success) {
|
||||
router.push("/dashboard")
|
||||
} else {
|
||||
setError("註冊失敗,該電子郵件可能已被使用")
|
||||
}
|
||||
} catch (err) {
|
||||
setError("註冊失敗,請稍後再試")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-primary rounded-lg flex items-center justify-center mx-auto mb-4">
|
||||
<Users className="w-8 h-8 text-primary-foreground" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-foreground">HR 評估系統</h1>
|
||||
<p className="text-muted-foreground">建立您的帳戶</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>註冊</CardTitle>
|
||||
<CardDescription>填寫以下資訊以建立您的帳戶</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">姓名</Label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="請輸入您的姓名"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">電子郵件</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="請輸入電子郵件"
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="department">部門</Label>
|
||||
<Select
|
||||
value={formData.department}
|
||||
onValueChange={(value) => setFormData({ ...formData, department: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="請選擇部門" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{departments.map((dept) => (
|
||||
<SelectItem key={dept} value={dept}>
|
||||
{dept}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">密碼</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="請輸入密碼(至少6個字元)"
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">確認密碼</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type={showConfirmPassword ? "text" : "password"}
|
||||
placeholder="請再次輸入密碼"
|
||||
value={formData.confirmPassword}
|
||||
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
>
|
||||
{showConfirmPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? "註冊中..." : "註冊"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
已有帳戶?{" "}
|
||||
<Link href="/login" className="text-primary hover:underline">
|
||||
立即登入
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
295
app/results/combined/page.tsx
Normal file
295
app/results/combined/page.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { Brain, Lightbulb, BarChart3, Home, RotateCcw, TrendingUp, Target, Award } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { getRecommendations } from "@/lib/utils/score-calculator"
|
||||
|
||||
interface CombinedTestResults {
|
||||
type: string
|
||||
logicScore: number
|
||||
creativityScore: number
|
||||
overallScore: number
|
||||
level: string
|
||||
description: string
|
||||
breakdown: {
|
||||
logic: number
|
||||
creativity: number
|
||||
balance: number
|
||||
}
|
||||
logicAnswers: Record<number, string>
|
||||
creativeAnswers: Record<number, number>
|
||||
logicCorrect: number
|
||||
creativityTotal: number
|
||||
creativityMaxScore: number
|
||||
completedAt: string
|
||||
}
|
||||
|
||||
export default function CombinedResultsPage() {
|
||||
const [results, setResults] = useState<CombinedTestResults | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const savedResults = localStorage.getItem("combinedTestResults")
|
||||
if (savedResults) {
|
||||
setResults(JSON.parse(savedResults))
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!results) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardContent className="text-center py-8">
|
||||
<p className="text-muted-foreground mb-4">未找到测试结果</p>
|
||||
<Button asChild>
|
||||
<Link href="/tests/combined">重新测试</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const recommendations = getRecommendations(results.logicScore, results.creativityScore)
|
||||
|
||||
const getScoreColor = (score: number) => {
|
||||
if (score >= 90) return "text-green-600"
|
||||
if (score >= 80) return "text-blue-600"
|
||||
if (score >= 70) return "text-yellow-600"
|
||||
if (score >= 60) return "text-orange-600"
|
||||
return "text-red-600"
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-card/50 backdrop-blur-sm">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-r from-primary to-accent rounded-lg flex items-center justify-center">
|
||||
<BarChart3 className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">综合能力测试结果</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
完成时间:{new Date(results.completedAt).toLocaleString("zh-CN")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-6xl mx-auto space-y-8">
|
||||
{/* Overall Score */}
|
||||
<Card className="text-center bg-gradient-to-br from-primary/5 to-accent/5 border-2 border-primary/20">
|
||||
<CardHeader>
|
||||
<div className="w-32 h-32 bg-gradient-to-r from-primary to-accent rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<span className="text-4xl font-bold text-white">{results.overallScore}</span>
|
||||
</div>
|
||||
<CardTitle className="text-4xl mb-2">综合评估完成!</CardTitle>
|
||||
<div className="flex items-center justify-center gap-2 mb-4">
|
||||
<Badge className="bg-gradient-to-r from-primary to-accent text-white text-xl px-6 py-2">
|
||||
{results.level}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto text-pretty">{results.description}</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Progress value={results.overallScore} className="h-4 mb-6" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="text-center">
|
||||
<div className={`text-3xl font-bold mb-2 ${getScoreColor(results.logicScore)}`}>
|
||||
{results.logicScore}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">逻辑思维</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className={`text-3xl font-bold mb-2 ${getScoreColor(results.creativityScore)}`}>
|
||||
{results.creativityScore}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">创意能力</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className={`text-3xl font-bold mb-2 ${getScoreColor(results.breakdown.balance)}`}>
|
||||
{results.breakdown.balance}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">能力平衡</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Detailed Breakdown */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Logic Results */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Brain className="w-5 h-5 text-primary" />
|
||||
逻辑思维测试
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">得分</span>
|
||||
<span className={`text-2xl font-bold ${getScoreColor(results.logicScore)}`}>
|
||||
{results.logicScore}
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={results.logicScore} className="h-3" />
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div className="text-center p-3 bg-muted/50 rounded">
|
||||
<div className="font-bold text-green-600">{results.logicCorrect}</div>
|
||||
<div className="text-muted-foreground">答对题数</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-muted/50 rounded">
|
||||
<div className="font-bold text-primary">10</div>
|
||||
<div className="text-muted-foreground">总题数</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Creative Results */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Lightbulb className="w-5 h-5 text-accent" />
|
||||
创意能力测试
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">得分</span>
|
||||
<span className={`text-2xl font-bold ${getScoreColor(results.creativityScore)}`}>
|
||||
{results.creativityScore}
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={results.creativityScore} className="h-3" />
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div className="text-center p-3 bg-muted/50 rounded">
|
||||
<div className="font-bold text-accent">{results.creativityTotal}</div>
|
||||
<div className="text-muted-foreground">总得分</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-muted/50 rounded">
|
||||
<div className="font-bold text-primary">{results.creativityMaxScore}</div>
|
||||
<div className="text-muted-foreground">满分</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Ability Analysis */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
能力分析
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="text-center p-6 border rounded-lg">
|
||||
<Brain className="w-12 h-12 text-primary mx-auto mb-4" />
|
||||
<h3 className="font-semibold mb-2">逻辑思维</h3>
|
||||
<div className={`text-2xl font-bold mb-2 ${getScoreColor(results.logicScore)}`}>
|
||||
{results.logicScore}分
|
||||
</div>
|
||||
<Progress value={results.logicScore} className="h-2 mb-2" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{results.logicScore >= 80 ? "表现优秀" : results.logicScore >= 60 ? "表现良好" : "需要提升"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-6 border rounded-lg">
|
||||
<Lightbulb className="w-12 h-12 text-accent mx-auto mb-4" />
|
||||
<h3 className="font-semibold mb-2">创意能力</h3>
|
||||
<div className={`text-2xl font-bold mb-2 ${getScoreColor(results.creativityScore)}`}>
|
||||
{results.creativityScore}分
|
||||
</div>
|
||||
<Progress value={results.creativityScore} className="h-2 mb-2" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{results.creativityScore >= 80
|
||||
? "表现优秀"
|
||||
: results.creativityScore >= 60
|
||||
? "表现良好"
|
||||
: "需要提升"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-6 border rounded-lg">
|
||||
<Target className="w-12 h-12 text-green-600 mx-auto mb-4" />
|
||||
<h3 className="font-semibold mb-2">能力平衡</h3>
|
||||
<div className={`text-2xl font-bold mb-2 ${getScoreColor(results.breakdown.balance)}`}>
|
||||
{results.breakdown.balance}分
|
||||
</div>
|
||||
<Progress value={results.breakdown.balance} className="h-2 mb-2" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{results.breakdown.balance >= 80
|
||||
? "非常均衡"
|
||||
: results.breakdown.balance >= 60
|
||||
? "相对均衡"
|
||||
: "发展不均"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Recommendations */}
|
||||
{recommendations.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Award className="w-5 h-5" />
|
||||
发展建议
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{recommendations.map((recommendation, index) => (
|
||||
<div key={index} className="flex items-start gap-3 p-4 bg-muted/50 rounded-lg">
|
||||
<div className="w-6 h-6 bg-primary rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span className="text-xs font-bold text-primary-foreground">{index + 1}</span>
|
||||
</div>
|
||||
<p className="text-sm leading-relaxed">{recommendation}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button asChild size="lg">
|
||||
<Link href="/">
|
||||
<Home className="w-4 h-4 mr-2" />
|
||||
返回首页
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg">
|
||||
<Link href="/tests/combined">
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
重新测试
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg">
|
||||
<Link href="/results">查看所有成绩</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
225
app/results/creative/page.tsx
Normal file
225
app/results/creative/page.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { Lightbulb, Home, RotateCcw, TrendingUp } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { creativeQuestions } from "@/lib/questions/creative-questions"
|
||||
|
||||
interface CreativeTestResults {
|
||||
type: string
|
||||
score: number
|
||||
totalScore: number
|
||||
maxScore: number
|
||||
answers: Record<number, number>
|
||||
completedAt: string
|
||||
}
|
||||
|
||||
export default function CreativeResultsPage() {
|
||||
const [results, setResults] = useState<CreativeTestResults | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const savedResults = localStorage.getItem("creativeTestResults")
|
||||
if (savedResults) {
|
||||
setResults(JSON.parse(savedResults))
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!results) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardContent className="text-center py-8">
|
||||
<p className="text-muted-foreground mb-4">未找到测试结果</p>
|
||||
<Button asChild>
|
||||
<Link href="/tests/creative">重新测试</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const getCreativityLevel = (score: number) => {
|
||||
if (score >= 85) return { level: "极具创意", color: "bg-purple-500", description: "拥有卓越的创新思维和想象力" }
|
||||
if (score >= 75) return { level: "很有创意", color: "bg-blue-500", description: "具备较强的创造性思维能力" }
|
||||
if (score >= 65) return { level: "有一定创意", color: "bg-green-500", description: "具有一定的创新潜力" }
|
||||
if (score >= 50) return { level: "创意一般", color: "bg-yellow-500", description: "创造性思维有待提升" }
|
||||
return { level: "缺乏创意", color: "bg-red-500", description: "需要培养创新思维能力" }
|
||||
}
|
||||
|
||||
const creativityLevel = getCreativityLevel(results.score)
|
||||
|
||||
// Calculate category scores
|
||||
const categoryScores = {
|
||||
innovation: { total: 0, count: 0, name: "创新能力" },
|
||||
imagination: { total: 0, count: 0, name: "想象力" },
|
||||
flexibility: { total: 0, count: 0, name: "灵活性" },
|
||||
originality: { total: 0, count: 0, name: "原创性" },
|
||||
}
|
||||
|
||||
creativeQuestions.forEach((question, index) => {
|
||||
const answer = results.answers[index] || 1
|
||||
const score = question.isReverse ? 6 - answer : answer
|
||||
categoryScores[question.category].total += score
|
||||
categoryScores[question.category].count += 1
|
||||
})
|
||||
|
||||
const categoryResults = Object.entries(categoryScores).map(([key, data]) => ({
|
||||
category: key,
|
||||
name: data.name,
|
||||
score: data.count > 0 ? Math.round((data.total / (data.count * 5)) * 100) : 0,
|
||||
rawScore: data.total,
|
||||
maxRawScore: data.count * 5,
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-card/50 backdrop-blur-sm">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-accent rounded-lg flex items-center justify-center">
|
||||
<Lightbulb className="w-6 h-6 text-accent-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">创意能力测试结果</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
完成时间:{new Date(results.completedAt).toLocaleString("zh-CN")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-4xl mx-auto space-y-8">
|
||||
{/* Score Overview */}
|
||||
<Card className="text-center">
|
||||
<CardHeader>
|
||||
<div
|
||||
className={`w-24 h-24 ${creativityLevel.color} rounded-full flex items-center justify-center mx-auto mb-4`}
|
||||
>
|
||||
<span className="text-3xl font-bold text-white">{results.score}</span>
|
||||
</div>
|
||||
<CardTitle className="text-3xl mb-2">创意测试完成!</CardTitle>
|
||||
<div className="flex items-center justify-center gap-2 mb-4">
|
||||
<Badge variant="secondary" className="text-lg px-4 py-1">
|
||||
{creativityLevel.level}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-lg text-muted-foreground">{creativityLevel.description}</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-accent mb-1">{results.totalScore}</div>
|
||||
<div className="text-sm text-muted-foreground">总得分</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-primary mb-1">{results.maxScore}</div>
|
||||
<div className="text-sm text-muted-foreground">满分</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-green-600 mb-1">
|
||||
{Math.round((results.totalScore / results.maxScore) * 100)}%
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">得分率</div>
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={results.score} className="h-3 mb-4" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Category Analysis */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
能力维度分析
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{categoryResults.map((category) => (
|
||||
<div key={category.category} className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="font-medium">{category.name}</h3>
|
||||
<Badge variant="outline">{category.score}分</Badge>
|
||||
</div>
|
||||
<Progress value={category.score} className="h-2" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{category.rawScore} / {category.maxRawScore} 分
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Detailed Feedback */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>详细反馈</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-muted/50 rounded-lg">
|
||||
<h3 className="font-medium mb-2">创意能力评估</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
基于您的测试结果,您在创意思维方面表现为"{creativityLevel.level}"水平。
|
||||
{results.score >= 75 &&
|
||||
"您具备出色的创新思维能力,善于从不同角度思考问题,能够产生独特的想法和解决方案。"}
|
||||
{results.score >= 50 &&
|
||||
results.score < 75 &&
|
||||
"您具有一定的创造性思维潜力,建议多参与创新活动,培养发散性思维。"}
|
||||
{results.score < 50 && "建议您多接触创新思维训练,培养好奇心和探索精神,提升创造性解决问题的能力。"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{categoryResults.map((category) => (
|
||||
<div key={category.category} className="p-4 border rounded-lg">
|
||||
<h4 className="font-medium mb-2">{category.name}</h4>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Progress value={category.score} className="flex-1 h-2" />
|
||||
<span className="text-sm font-medium">{category.score}%</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{category.score >= 80 && "表现优秀,继续保持"}
|
||||
{category.score >= 60 && category.score < 80 && "表现良好,有提升空间"}
|
||||
{category.score < 60 && "需要重点提升"}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button asChild size="lg">
|
||||
<Link href="/">
|
||||
<Home className="w-4 h-4 mr-2" />
|
||||
返回首页
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg">
|
||||
<Link href="/tests/creative">
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
重新测试
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg">
|
||||
<Link href="/tests/logic">开始逻辑测试</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
194
app/results/logic/page.tsx
Normal file
194
app/results/logic/page.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { CheckCircle, XCircle, Brain, Home, RotateCcw } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { logicQuestions } from "@/lib/questions/logic-questions"
|
||||
|
||||
interface LogicTestResults {
|
||||
type: string
|
||||
score: number
|
||||
correctAnswers: number
|
||||
totalQuestions: number
|
||||
answers: Record<number, string>
|
||||
completedAt: string
|
||||
}
|
||||
|
||||
export default function LogicResultsPage() {
|
||||
const [results, setResults] = useState<LogicTestResults | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const savedResults = localStorage.getItem("logicTestResults")
|
||||
if (savedResults) {
|
||||
setResults(JSON.parse(savedResults))
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!results) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardContent className="text-center py-8">
|
||||
<p className="text-muted-foreground mb-4">未找到测试结果</p>
|
||||
<Button asChild>
|
||||
<Link href="/tests/logic">重新测试</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const getScoreLevel = (score: number) => {
|
||||
if (score >= 90) return { level: "优秀", color: "bg-green-500", description: "逻辑思维能力出色" }
|
||||
if (score >= 80) return { level: "良好", color: "bg-blue-500", description: "逻辑思维能力较强" }
|
||||
if (score >= 70) return { level: "中等", color: "bg-yellow-500", description: "逻辑思维能力一般" }
|
||||
if (score >= 60) return { level: "及格", color: "bg-orange-500", description: "逻辑思维能力需要提升" }
|
||||
return { level: "不及格", color: "bg-red-500", description: "逻辑思维能力有待加强" }
|
||||
}
|
||||
|
||||
const scoreLevel = getScoreLevel(results.score)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-card/50 backdrop-blur-sm">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-primary rounded-lg flex items-center justify-center">
|
||||
<Brain className="w-6 h-6 text-primary-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">逻辑思维测试结果</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
完成时间:{new Date(results.completedAt).toLocaleString("zh-CN")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-4xl mx-auto space-y-8">
|
||||
{/* Score Overview */}
|
||||
<Card className="text-center">
|
||||
<CardHeader>
|
||||
<div
|
||||
className={`w-24 h-24 ${scoreLevel.color} rounded-full flex items-center justify-center mx-auto mb-4`}
|
||||
>
|
||||
<span className="text-3xl font-bold text-white">{results.score}</span>
|
||||
</div>
|
||||
<CardTitle className="text-3xl mb-2">测试完成!</CardTitle>
|
||||
<div className="flex items-center justify-center gap-2 mb-4">
|
||||
<Badge variant="secondary" className="text-lg px-4 py-1">
|
||||
{scoreLevel.level}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-lg text-muted-foreground">{scoreLevel.description}</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-green-600 mb-1">{results.correctAnswers}</div>
|
||||
<div className="text-sm text-muted-foreground">答对题数</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-primary mb-1">{results.totalQuestions}</div>
|
||||
<div className="text-sm text-muted-foreground">总题数</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-accent mb-1">
|
||||
{Math.round((results.correctAnswers / results.totalQuestions) * 100)}%
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">正确率</div>
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={results.score} className="h-3 mb-4" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Detailed Results */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>详细结果</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
{logicQuestions.map((question, index) => {
|
||||
const userAnswer = results.answers[index]
|
||||
const isCorrect = userAnswer === question.correctAnswer
|
||||
const correctOption = question.options.find((opt) => opt.value === question.correctAnswer)
|
||||
const userOption = question.options.find((opt) => opt.value === userAnswer)
|
||||
|
||||
return (
|
||||
<div key={question.id} className="border rounded-lg p-4">
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<div className="flex-shrink-0 mt-1">
|
||||
{isCorrect ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||
) : (
|
||||
<XCircle className="w-5 h-5 text-red-500" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium mb-2 text-balance">
|
||||
第{index + 1}题:{question.question}
|
||||
</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground">你的答案:</span>
|
||||
<Badge variant={isCorrect ? "default" : "destructive"}>
|
||||
{userOption?.text || "未作答"}
|
||||
</Badge>
|
||||
</div>
|
||||
{!isCorrect && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground">正确答案:</span>
|
||||
<Badge variant="outline" className="border-green-500 text-green-700">
|
||||
{correctOption?.text}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{question.explanation && !isCorrect && (
|
||||
<div className="mt-2 p-3 bg-muted/50 rounded text-sm">
|
||||
<strong>解析:</strong>
|
||||
{question.explanation}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button asChild size="lg">
|
||||
<Link href="/">
|
||||
<Home className="w-4 h-4 mr-2" />
|
||||
返回首页
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg">
|
||||
<Link href="/tests/logic">
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
重新测试
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg">
|
||||
<Link href="/tests/creative">开始创意测试</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
376
app/results/page.tsx
Normal file
376
app/results/page.tsx
Normal file
@@ -0,0 +1,376 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { ProtectedRoute } from "@/components/protected-route"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { Brain, Lightbulb, BarChart3, Calendar, TrendingUp, Users, Eye, ArrowLeft } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useAuth } from "@/lib/hooks/use-auth"
|
||||
|
||||
interface TestResult {
|
||||
type: "logic" | "creative" | "combined"
|
||||
score: number
|
||||
completedAt: string
|
||||
details?: any
|
||||
}
|
||||
|
||||
export default function ResultsPage() {
|
||||
return (
|
||||
<ProtectedRoute>
|
||||
<ResultsContent />
|
||||
</ProtectedRoute>
|
||||
)
|
||||
}
|
||||
|
||||
function ResultsContent() {
|
||||
const { user } = useAuth()
|
||||
const [results, setResults] = useState<TestResult[]>([])
|
||||
const [stats, setStats] = useState({
|
||||
totalTests: 0,
|
||||
averageScore: 0,
|
||||
bestScore: 0,
|
||||
lastTestDate: null as string | null,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// Load all test results from localStorage
|
||||
const logicResults = localStorage.getItem("logicTestResults")
|
||||
const creativeResults = localStorage.getItem("creativeTestResults")
|
||||
const combinedResults = localStorage.getItem("combinedTestResults")
|
||||
|
||||
const allResults: TestResult[] = []
|
||||
|
||||
if (logicResults) {
|
||||
const data = JSON.parse(logicResults)
|
||||
allResults.push({
|
||||
type: "logic",
|
||||
score: data.score,
|
||||
completedAt: data.completedAt,
|
||||
details: data,
|
||||
})
|
||||
}
|
||||
|
||||
if (creativeResults) {
|
||||
const data = JSON.parse(creativeResults)
|
||||
allResults.push({
|
||||
type: "creative",
|
||||
score: data.score,
|
||||
completedAt: data.completedAt,
|
||||
details: data,
|
||||
})
|
||||
}
|
||||
|
||||
if (combinedResults) {
|
||||
const data = JSON.parse(combinedResults)
|
||||
allResults.push({
|
||||
type: "combined",
|
||||
score: data.overallScore,
|
||||
completedAt: data.completedAt,
|
||||
details: data,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort by completion date (newest first)
|
||||
allResults.sort((a, b) => new Date(b.completedAt).getTime() - new Date(a.completedAt).getTime())
|
||||
|
||||
setResults(allResults)
|
||||
|
||||
// Calculate statistics
|
||||
if (allResults.length > 0) {
|
||||
const totalScore = allResults.reduce((sum, result) => sum + result.score, 0)
|
||||
const averageScore = Math.round(totalScore / allResults.length)
|
||||
const bestScore = Math.max(...allResults.map((r) => r.score))
|
||||
const lastTestDate = allResults[0].completedAt
|
||||
|
||||
setStats({
|
||||
totalTests: allResults.length,
|
||||
averageScore,
|
||||
bestScore,
|
||||
lastTestDate,
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getTestTypeInfo = (type: string) => {
|
||||
switch (type) {
|
||||
case "logic":
|
||||
return {
|
||||
name: "邏輯思維測試",
|
||||
icon: Brain,
|
||||
color: "bg-primary",
|
||||
textColor: "text-primary",
|
||||
link: "/results/logic",
|
||||
}
|
||||
case "creative":
|
||||
return {
|
||||
name: "創意能力測試",
|
||||
icon: Lightbulb,
|
||||
color: "bg-accent",
|
||||
textColor: "text-accent",
|
||||
link: "/results/creative",
|
||||
}
|
||||
case "combined":
|
||||
return {
|
||||
name: "綜合能力測試",
|
||||
icon: BarChart3,
|
||||
color: "bg-gradient-to-r from-primary to-accent",
|
||||
textColor: "text-primary",
|
||||
link: "/results/combined",
|
||||
}
|
||||
default:
|
||||
return {
|
||||
name: "未知測試",
|
||||
icon: BarChart3,
|
||||
color: "bg-muted",
|
||||
textColor: "text-muted-foreground",
|
||||
link: "#",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getScoreLevel = (score: number) => {
|
||||
if (score >= 90) return { level: "優秀", color: "bg-green-500" }
|
||||
if (score >= 80) return { level: "良好", color: "bg-blue-500" }
|
||||
if (score >= 70) return { level: "中等", color: "bg-yellow-500" }
|
||||
if (score >= 60) return { level: "及格", color: "bg-orange-500" }
|
||||
return { level: "不及格", color: "bg-red-500" }
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-card/50 backdrop-blur-sm">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href="/dashboard">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
返回儀表板
|
||||
</Link>
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">我的測試結果</h1>
|
||||
<p className="text-sm text-muted-foreground">查看您的測試歷史和成績分析</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-4 py-16">
|
||||
<div className="max-w-2xl mx-auto text-center">
|
||||
<div className="w-24 h-24 bg-muted rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<BarChart3 className="w-12 h-12 text-muted-foreground" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-foreground mb-4">暫無測試記錄</h2>
|
||||
<p className="text-muted-foreground mb-8">您還沒有完成任何測試,開始您的第一次評估吧!</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button asChild size="lg">
|
||||
<Link href="/tests/logic">邏輯思維測試</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg">
|
||||
<Link href="/tests/creative">創意能力測試</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg">
|
||||
<Link href="/tests/combined">綜合能力測試</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-card/50 backdrop-blur-sm">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href="/dashboard">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
返回儀表板
|
||||
</Link>
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">我的測試結果</h1>
|
||||
<p className="text-sm text-muted-foreground">查看您的測試歷史和成績分析</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-6xl mx-auto space-y-8">
|
||||
{/* User Info */}
|
||||
{user && (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-primary rounded-full flex items-center justify-center">
|
||||
<Users className="w-6 h-6 text-primary-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">{user.name}</h2>
|
||||
<p className="text-muted-foreground">
|
||||
{user.department} • {user.role === "admin" ? "管理員" : "一般用戶"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Statistics Overview */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<Users className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-foreground mb-1">{stats.totalTests}</div>
|
||||
<div className="text-sm text-muted-foreground">完成測試</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="w-12 h-12 bg-blue-500/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<TrendingUp className="w-6 h-6 text-blue-500" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-foreground mb-1">{stats.averageScore}</div>
|
||||
<div className="text-sm text-muted-foreground">平均分數</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="w-12 h-12 bg-green-500/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<BarChart3 className="w-6 h-6 text-green-500" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-foreground mb-1">{stats.bestScore}</div>
|
||||
<div className="text-sm text-muted-foreground">最高分數</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="w-12 h-12 bg-accent/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<Calendar className="w-6 h-6 text-accent" />
|
||||
</div>
|
||||
<div className="text-sm font-bold text-foreground mb-1">
|
||||
{stats.lastTestDate ? new Date(stats.lastTestDate).toLocaleDateString("zh-TW") : "無"}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">最近測試</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Test History */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>測試歷史</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{results.map((result, index) => {
|
||||
const testInfo = getTestTypeInfo(result.type)
|
||||
const scoreLevel = getScoreLevel(result.score)
|
||||
const Icon = testInfo.icon
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-4 border rounded-lg hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-12 h-12 ${testInfo.color} rounded-lg flex items-center justify-center`}>
|
||||
<Icon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-foreground">{testInfo.name}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
完成時間:{new Date(result.completedAt).toLocaleString("zh-TW")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-foreground">{result.score}</div>
|
||||
<Badge className={`${scoreLevel.color} text-white`}>{scoreLevel.level}</Badge>
|
||||
</div>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href={testInfo.link}>
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
查看詳情
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Performance Chart */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>成績趨勢</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{results.map((result, index) => {
|
||||
const testInfo = getTestTypeInfo(result.type)
|
||||
return (
|
||||
<div key={index} className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className={`text-sm font-medium ${testInfo.textColor}`}>{testInfo.name}</span>
|
||||
<span className="text-sm font-medium">{result.score}分</span>
|
||||
</div>
|
||||
<Progress value={result.score} className="h-2" />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>繼續測試</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Button asChild className="h-auto p-4">
|
||||
<Link href="/tests/logic" className="flex flex-col items-center gap-2">
|
||||
<Brain className="w-8 h-8" />
|
||||
<span>邏輯思維測試</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" className="h-auto p-4 bg-transparent">
|
||||
<Link href="/tests/creative" className="flex flex-col items-center gap-2">
|
||||
<Lightbulb className="w-8 h-8" />
|
||||
<span>創意能力測試</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" className="h-auto p-4 bg-transparent">
|
||||
<Link href="/tests/combined" className="flex flex-col items-center gap-2">
|
||||
<BarChart3 className="w-8 h-8" />
|
||||
<span>綜合能力測試</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
346
app/settings/page.tsx
Normal file
346
app/settings/page.tsx
Normal file
@@ -0,0 +1,346 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { ProtectedRoute } from "@/components/protected-route"
|
||||
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { User, Lock, ArrowLeft, Save } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useAuth } from "@/lib/hooks/use-auth"
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
<ProtectedRoute>
|
||||
<SettingsContent />
|
||||
</ProtectedRoute>
|
||||
)
|
||||
}
|
||||
|
||||
function SettingsContent() {
|
||||
const { user, logout } = useAuth()
|
||||
const router = useRouter()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [message, setMessage] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
|
||||
// Profile form state
|
||||
const [profileData, setProfileData] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
department: "",
|
||||
})
|
||||
|
||||
// Password form state
|
||||
const [passwordData, setPasswordData] = useState({
|
||||
currentPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
})
|
||||
|
||||
const departments = ["人力資源部", "資訊技術部", "財務部", "行銷部", "業務部", "研發部", "客服部", "其他"]
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setProfileData({
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
department: user.department,
|
||||
})
|
||||
}
|
||||
}, [user])
|
||||
|
||||
const handleProfileUpdate = async () => {
|
||||
setError("")
|
||||
setMessage("")
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
// Get all users
|
||||
const users = JSON.parse(localStorage.getItem("hr_users") || "[]")
|
||||
|
||||
// Check if email is already taken by another user
|
||||
const emailExists = users.some((u: any) => u.email === profileData.email && u.id !== user?.id)
|
||||
if (emailExists) {
|
||||
setError("該電子郵件已被其他用戶使用")
|
||||
return
|
||||
}
|
||||
|
||||
// Update user data
|
||||
const updatedUsers = users.map((u: any) =>
|
||||
u.id === user?.id
|
||||
? { ...u, name: profileData.name, email: profileData.email, department: profileData.department }
|
||||
: u,
|
||||
)
|
||||
|
||||
localStorage.setItem("hr_users", JSON.stringify(updatedUsers))
|
||||
|
||||
// Update current user session
|
||||
const updatedUser = {
|
||||
...user!,
|
||||
name: profileData.name,
|
||||
email: profileData.email,
|
||||
department: profileData.department,
|
||||
}
|
||||
localStorage.setItem("hr_current_user", JSON.stringify(updatedUser))
|
||||
|
||||
setMessage("個人資料已成功更新")
|
||||
|
||||
// Refresh page to update user context
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 1500)
|
||||
} catch (err) {
|
||||
setError("更新失敗,請稍後再試")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePasswordChange = async () => {
|
||||
setError("")
|
||||
setMessage("")
|
||||
|
||||
if (passwordData.newPassword !== passwordData.confirmPassword) {
|
||||
setError("新密碼確認不一致")
|
||||
return
|
||||
}
|
||||
|
||||
if (passwordData.newPassword.length < 6) {
|
||||
setError("新密碼長度至少需要6個字元")
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
// Get all users
|
||||
const users = JSON.parse(localStorage.getItem("hr_users") || "[]")
|
||||
|
||||
// Find current user and verify current password
|
||||
const currentUser = users.find((u: any) => u.id === user?.id)
|
||||
if (!currentUser || currentUser.password !== passwordData.currentPassword) {
|
||||
setError("目前密碼不正確")
|
||||
return
|
||||
}
|
||||
|
||||
// Update password
|
||||
const updatedUsers = users.map((u: any) => (u.id === user?.id ? { ...u, password: passwordData.newPassword } : u))
|
||||
|
||||
localStorage.setItem("hr_users", JSON.stringify(updatedUsers))
|
||||
|
||||
setPasswordData({
|
||||
currentPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
})
|
||||
|
||||
setMessage("密碼已成功更新")
|
||||
} catch (err) {
|
||||
setError("密碼更新失敗,請稍後再試")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!user) return null
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-card/50 backdrop-blur-sm">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href="/dashboard">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
返回儀表板
|
||||
</Link>
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">帳戶設定</h1>
|
||||
<p className="text-sm text-muted-foreground">管理您的個人資料和帳戶設定</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-2xl mx-auto space-y-8">
|
||||
{/* Profile Settings */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<User className="w-5 h-5" />
|
||||
個人資料
|
||||
</CardTitle>
|
||||
<CardDescription>更新您的個人資訊</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">姓名</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={profileData.name}
|
||||
onChange={(e) => setProfileData({ ...profileData, name: e.target.value })}
|
||||
placeholder="請輸入您的姓名"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">電子郵件</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={profileData.email}
|
||||
onChange={(e) => setProfileData({ ...profileData, email: e.target.value })}
|
||||
placeholder="請輸入電子郵件"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="department">部門</Label>
|
||||
<Select
|
||||
value={profileData.department}
|
||||
onValueChange={(value) => setProfileData({ ...profileData, department: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="請選擇部門" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{departments.map((dept) => (
|
||||
<SelectItem key={dept} value={dept}>
|
||||
{dept}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>角色</Label>
|
||||
<div className="p-3 bg-muted rounded-md">
|
||||
<span className="text-sm font-medium">{user.role === "admin" ? "管理員" : "一般用戶"}</span>
|
||||
<p className="text-xs text-muted-foreground mt-1">角色由管理員設定,無法自行修改</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleProfileUpdate} disabled={isLoading} className="w-full">
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
{isLoading ? "更新中..." : "更新個人資料"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Password Settings */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Lock className="w-5 h-5" />
|
||||
密碼設定
|
||||
</CardTitle>
|
||||
<CardDescription>更改您的登入密碼</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="currentPassword">目前密碼</Label>
|
||||
<Input
|
||||
id="currentPassword"
|
||||
type="password"
|
||||
value={passwordData.currentPassword}
|
||||
onChange={(e) => setPasswordData({ ...passwordData, currentPassword: e.target.value })}
|
||||
placeholder="請輸入目前密碼"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="newPassword">新密碼</Label>
|
||||
<Input
|
||||
id="newPassword"
|
||||
type="password"
|
||||
value={passwordData.newPassword}
|
||||
onChange={(e) => setPasswordData({ ...passwordData, newPassword: e.target.value })}
|
||||
placeholder="請輸入新密碼(至少6個字元)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">確認新密碼</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
value={passwordData.confirmPassword}
|
||||
onChange={(e) => setPasswordData({ ...passwordData, confirmPassword: e.target.value })}
|
||||
placeholder="請再次輸入新密碼"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button onClick={handlePasswordChange} disabled={isLoading} className="w-full">
|
||||
<Lock className="w-4 h-4 mr-2" />
|
||||
{isLoading ? "更新中..." : "更新密碼"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Account Info */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>帳戶資訊</CardTitle>
|
||||
<CardDescription>您的帳戶詳細資訊</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">用戶ID:</span>
|
||||
<span className="font-mono">{user.id}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">建立時間:</span>
|
||||
<span>{new Date(user.createdAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h4 className="font-medium">登出帳戶</h4>
|
||||
<p className="text-sm text-muted-foreground">結束目前的登入狀態</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
logout()
|
||||
router.push("/login")
|
||||
}}
|
||||
>
|
||||
登出
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Messages */}
|
||||
{message && (
|
||||
<Alert>
|
||||
<AlertDescription className="text-green-600">{message}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
316
app/tests/combined/page.tsx
Normal file
316
app/tests/combined/page.tsx
Normal file
@@ -0,0 +1,316 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { TestLayout } from "@/components/test-layout"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { logicQuestions } from "@/lib/questions/logic-questions"
|
||||
import { creativeQuestions } from "@/lib/questions/creative-questions"
|
||||
import { calculateCombinedScore } from "@/lib/utils/score-calculator"
|
||||
|
||||
type TestPhase = "logic" | "creative" | "completed"
|
||||
|
||||
export default function CombinedTestPage() {
|
||||
const router = useRouter()
|
||||
const [phase, setPhase] = useState<TestPhase>("logic")
|
||||
const [currentQuestion, setCurrentQuestion] = useState(0)
|
||||
const [logicAnswers, setLogicAnswers] = useState<Record<number, string>>({})
|
||||
const [creativeAnswers, setCreativeAnswers] = useState<Record<number, number>>({})
|
||||
const [timeRemaining, setTimeRemaining] = useState(45 * 60) // 45 minutes total
|
||||
|
||||
// Timer effect
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setTimeRemaining((prev) => {
|
||||
if (prev <= 1) {
|
||||
handleSubmit()
|
||||
return 0
|
||||
}
|
||||
return prev - 1
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(timer)
|
||||
}, [])
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60)
|
||||
const secs = seconds % 60
|
||||
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`
|
||||
}
|
||||
|
||||
const getCurrentQuestions = () => {
|
||||
return phase === "logic" ? logicQuestions : creativeQuestions
|
||||
}
|
||||
|
||||
const getTotalQuestions = () => {
|
||||
return logicQuestions.length + creativeQuestions.length
|
||||
}
|
||||
|
||||
const getOverallProgress = () => {
|
||||
const logicCompleted = phase === "logic" ? currentQuestion : logicQuestions.length
|
||||
const creativeCompleted = phase === "creative" ? currentQuestion : 0
|
||||
return logicCompleted + creativeCompleted
|
||||
}
|
||||
|
||||
const handleAnswerChange = (value: string) => {
|
||||
if (phase === "logic") {
|
||||
setLogicAnswers((prev) => ({
|
||||
...prev,
|
||||
[currentQuestion]: value,
|
||||
}))
|
||||
} else {
|
||||
setCreativeAnswers((prev) => ({
|
||||
...prev,
|
||||
[currentQuestion]: Number.parseInt(value),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
const currentQuestions = getCurrentQuestions()
|
||||
|
||||
if (currentQuestion < currentQuestions.length - 1) {
|
||||
setCurrentQuestion((prev) => prev + 1)
|
||||
} else if (phase === "logic") {
|
||||
// Switch to creative phase
|
||||
setPhase("creative")
|
||||
setCurrentQuestion(0)
|
||||
} else {
|
||||
// Complete the test
|
||||
handleSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (currentQuestion > 0) {
|
||||
setCurrentQuestion((prev) => prev - 1)
|
||||
} else if (phase === "creative") {
|
||||
// Go back to logic phase
|
||||
setPhase("logic")
|
||||
setCurrentQuestion(logicQuestions.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
// Calculate logic score
|
||||
let logicCorrect = 0
|
||||
logicQuestions.forEach((question, index) => {
|
||||
if (logicAnswers[index] === question.correctAnswer) {
|
||||
logicCorrect++
|
||||
}
|
||||
})
|
||||
const logicScore = Math.round((logicCorrect / logicQuestions.length) * 100)
|
||||
|
||||
// Calculate creativity score
|
||||
let creativityTotal = 0
|
||||
creativeQuestions.forEach((question, index) => {
|
||||
const answer = creativeAnswers[index] || 1
|
||||
creativityTotal += question.isReverse ? 6 - answer : answer
|
||||
})
|
||||
const creativityMaxScore = creativeQuestions.length * 5
|
||||
const creativityScore = Math.round((creativityTotal / creativityMaxScore) * 100)
|
||||
|
||||
// Calculate combined score
|
||||
const combinedResult = calculateCombinedScore(logicScore, creativityScore)
|
||||
|
||||
// Store results
|
||||
const results = {
|
||||
type: "combined",
|
||||
logicScore,
|
||||
creativityScore,
|
||||
overallScore: combinedResult.overallScore,
|
||||
level: combinedResult.level,
|
||||
description: combinedResult.description,
|
||||
breakdown: combinedResult.breakdown,
|
||||
logicAnswers,
|
||||
creativeAnswers,
|
||||
logicCorrect,
|
||||
creativityTotal,
|
||||
creativityMaxScore,
|
||||
completedAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
localStorage.setItem("combinedTestResults", JSON.stringify(results))
|
||||
router.push("/results/combined")
|
||||
}
|
||||
|
||||
const currentQuestions = getCurrentQuestions()
|
||||
const currentQ = currentQuestions[currentQuestion]
|
||||
const isLastQuestion = phase === "creative" && currentQuestion === creativeQuestions.length - 1
|
||||
const hasAnswer =
|
||||
phase === "logic" ? logicAnswers[currentQuestion] !== undefined : creativeAnswers[currentQuestion] !== undefined
|
||||
|
||||
const getPhaseTitle = () => {
|
||||
if (phase === "logic") return "第一部分:邏輯思維測試"
|
||||
return "第二部分:創意能力測試"
|
||||
}
|
||||
|
||||
const getQuestionNumber = () => {
|
||||
if (phase === "logic") return currentQuestion + 1
|
||||
return logicQuestions.length + currentQuestion + 1
|
||||
}
|
||||
|
||||
return (
|
||||
<TestLayout
|
||||
title="綜合能力測試"
|
||||
currentQuestion={getQuestionNumber()}
|
||||
totalQuestions={getTotalQuestions()}
|
||||
timeRemaining={formatTime(timeRemaining)}
|
||||
onBack={() => router.push("/")}
|
||||
>
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Phase Indicator */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-foreground">{getPhaseTitle()}</h2>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{phase === "logic"
|
||||
? `${currentQuestion + 1}/${logicQuestions.length}`
|
||||
: `${currentQuestion + 1}/${creativeQuestions.length}`}
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={(getOverallProgress() / getTotalQuestions()) * 100} className="h-2" />
|
||||
</div>
|
||||
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl text-balance">
|
||||
{phase === "logic" ? currentQ.question : currentQ.statement}
|
||||
</CardTitle>
|
||||
{phase === "creative" && (
|
||||
<p className="text-sm text-muted-foreground">請根據這個描述與你的實際情況的符合程度進行選擇</p>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RadioGroup
|
||||
value={
|
||||
phase === "logic"
|
||||
? logicAnswers[currentQuestion] || ""
|
||||
: creativeAnswers[currentQuestion]?.toString() || ""
|
||||
}
|
||||
onValueChange={handleAnswerChange}
|
||||
className="space-y-4"
|
||||
>
|
||||
{phase === "logic"
|
||||
? // Logic question options
|
||||
currentQ.options?.map((option: any, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center space-x-3 p-4 rounded-lg border hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<RadioGroupItem value={option.value} id={`option-${index}`} />
|
||||
<Label htmlFor={`option-${index}`} className="flex-1 cursor-pointer text-base leading-relaxed">
|
||||
{option.text}
|
||||
</Label>
|
||||
</div>
|
||||
))
|
||||
: // Creative question options
|
||||
[
|
||||
{ value: "5", label: "我最符合", color: "text-green-600" },
|
||||
{ value: "4", label: "比較符合", color: "text-green-500" },
|
||||
{ value: "3", label: "一般", color: "text-yellow-500" },
|
||||
{ value: "2", label: "不太符合", color: "text-orange-500" },
|
||||
{ value: "1", label: "與我不符", color: "text-red-500" },
|
||||
].map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className="flex items-center space-x-4 p-4 rounded-lg border hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<RadioGroupItem value={option.value} id={`option-${option.value}`} />
|
||||
<Label
|
||||
htmlFor={`option-${option.value}`}
|
||||
className={`flex-1 cursor-pointer text-base font-medium ${option.color}`}
|
||||
>
|
||||
{option.label}
|
||||
</Label>
|
||||
<div className="text-sm text-muted-foreground font-mono">{option.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex justify-between items-center">
|
||||
<Button variant="outline" onClick={handlePrevious} disabled={phase === "logic" && currentQuestion === 0}>
|
||||
上一題
|
||||
</Button>
|
||||
|
||||
<div className="flex gap-2 max-w-md overflow-x-auto">
|
||||
{/* Logic questions indicators */}
|
||||
{logicQuestions.map((_, index) => (
|
||||
<button
|
||||
key={`logic-${index}`}
|
||||
onClick={() => {
|
||||
if (phase === "logic" || phase === "creative") {
|
||||
setPhase("logic")
|
||||
setCurrentQuestion(index)
|
||||
}
|
||||
}}
|
||||
className={`w-8 h-8 rounded-full text-sm font-medium transition-colors flex-shrink-0 ${
|
||||
phase === "logic" && index === currentQuestion
|
||||
? "bg-primary text-primary-foreground"
|
||||
: logicAnswers[index] !== undefined
|
||||
? "bg-primary/70 text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground hover:bg-muted/80"
|
||||
}`}
|
||||
>
|
||||
{index + 1}
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Separator */}
|
||||
<div className="w-px h-8 bg-border mx-2"></div>
|
||||
|
||||
{/* Creative questions indicators */}
|
||||
{creativeQuestions.map((_, index) => (
|
||||
<button
|
||||
key={`creative-${index}`}
|
||||
onClick={() => {
|
||||
if (phase === "creative") {
|
||||
setCurrentQuestion(index)
|
||||
}
|
||||
}}
|
||||
className={`w-8 h-8 rounded-full text-sm font-medium transition-colors flex-shrink-0 ${
|
||||
phase === "creative" && index === currentQuestion
|
||||
? "bg-accent text-accent-foreground"
|
||||
: creativeAnswers[index] !== undefined
|
||||
? "bg-accent/70 text-accent-foreground"
|
||||
: phase === "creative"
|
||||
? "bg-muted text-muted-foreground hover:bg-muted/80"
|
||||
: "bg-muted/50 text-muted-foreground/50"
|
||||
}`}
|
||||
disabled={phase === "logic"}
|
||||
>
|
||||
{logicQuestions.length + index + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isLastQuestion ? (
|
||||
<Button onClick={handleSubmit} disabled={!hasAnswer} className="bg-green-600 hover:bg-green-700">
|
||||
提交測試
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleNext} disabled={!hasAnswer}>
|
||||
{phase === "logic" && currentQuestion === logicQuestions.length - 1 ? "進入第二部分" : "下一題"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress Summary */}
|
||||
<div className="mt-8 text-center text-sm text-muted-foreground">
|
||||
總進度:已完成 {getOverallProgress()} / {getTotalQuestions()} 題
|
||||
<br />
|
||||
當前階段:{phase === "logic" ? "邏輯思維測試" : "創意能力測試"} (
|
||||
{Object.keys(phase === "logic" ? logicAnswers : creativeAnswers).length} / {currentQuestions.length} 題)
|
||||
</div>
|
||||
</div>
|
||||
</TestLayout>
|
||||
)
|
||||
}
|
177
app/tests/creative/page.tsx
Normal file
177
app/tests/creative/page.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { TestLayout } from "@/components/test-layout"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { creativeQuestions } from "@/lib/questions/creative-questions"
|
||||
|
||||
export default function CreativeTestPage() {
|
||||
const router = useRouter()
|
||||
const [currentQuestion, setCurrentQuestion] = useState(0)
|
||||
const [answers, setAnswers] = useState<Record<number, number>>({})
|
||||
const [timeRemaining, setTimeRemaining] = useState(30 * 60) // 30 minutes in seconds
|
||||
|
||||
// Timer effect
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setTimeRemaining((prev) => {
|
||||
if (prev <= 1) {
|
||||
handleSubmit()
|
||||
return 0
|
||||
}
|
||||
return prev - 1
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(timer)
|
||||
}, [])
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60)
|
||||
const secs = seconds % 60
|
||||
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`
|
||||
}
|
||||
|
||||
const handleAnswerChange = (value: string) => {
|
||||
setAnswers((prev) => ({
|
||||
...prev,
|
||||
[currentQuestion]: Number.parseInt(value),
|
||||
}))
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentQuestion < creativeQuestions.length - 1) {
|
||||
setCurrentQuestion((prev) => prev + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (currentQuestion > 0) {
|
||||
setCurrentQuestion((prev) => prev - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
// Calculate score based on creativity scoring
|
||||
let totalScore = 0
|
||||
creativeQuestions.forEach((question, index) => {
|
||||
const answer = answers[index] || 1
|
||||
// For creativity, higher scores indicate more creative thinking
|
||||
totalScore += question.isReverse ? 6 - answer : answer
|
||||
})
|
||||
|
||||
const maxScore = creativeQuestions.length * 5
|
||||
const score = Math.round((totalScore / maxScore) * 100)
|
||||
|
||||
// Store results in localStorage
|
||||
const results = {
|
||||
type: "creative",
|
||||
score,
|
||||
totalScore,
|
||||
maxScore,
|
||||
answers,
|
||||
completedAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
localStorage.setItem("creativeTestResults", JSON.stringify(results))
|
||||
router.push("/results/creative")
|
||||
}
|
||||
|
||||
const currentQ = creativeQuestions[currentQuestion]
|
||||
const isLastQuestion = currentQuestion === creativeQuestions.length - 1
|
||||
const hasAnswer = answers[currentQuestion] !== undefined
|
||||
|
||||
const scaleOptions = [
|
||||
{ value: "5", label: "我最符合", color: "text-green-600" },
|
||||
{ value: "4", label: "比较符合", color: "text-green-500" },
|
||||
{ value: "3", label: "一般", color: "text-yellow-500" },
|
||||
{ value: "2", label: "不太符合", color: "text-orange-500" },
|
||||
{ value: "1", label: "与我不符", color: "text-red-500" },
|
||||
]
|
||||
|
||||
return (
|
||||
<TestLayout
|
||||
title="創意能力測試"
|
||||
currentQuestion={currentQuestion + 1}
|
||||
totalQuestions={creativeQuestions.length}
|
||||
timeRemaining={formatTime(timeRemaining)}
|
||||
onBack={() => router.push("/")}
|
||||
>
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl text-balance">{currentQ.statement}</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">請根據這個描述與你的實際情況的符合程度進行選擇</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RadioGroup
|
||||
value={answers[currentQuestion]?.toString() || ""}
|
||||
onValueChange={handleAnswerChange}
|
||||
className="space-y-4"
|
||||
>
|
||||
{scaleOptions.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className="flex items-center space-x-4 p-4 rounded-lg border hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<RadioGroupItem value={option.value} id={`option-${option.value}`} />
|
||||
<Label
|
||||
htmlFor={`option-${option.value}`}
|
||||
className={`flex-1 cursor-pointer text-base font-medium ${option.color}`}
|
||||
>
|
||||
{option.label}
|
||||
</Label>
|
||||
<div className="text-sm text-muted-foreground font-mono">{option.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex justify-between items-center">
|
||||
<Button variant="outline" onClick={handlePrevious} disabled={currentQuestion === 0}>
|
||||
上一題
|
||||
</Button>
|
||||
|
||||
<div className="flex gap-2 max-w-md overflow-x-auto">
|
||||
{creativeQuestions.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setCurrentQuestion(index)}
|
||||
className={`w-8 h-8 rounded-full text-sm font-medium transition-colors flex-shrink-0 ${
|
||||
index === currentQuestion
|
||||
? "bg-accent text-accent-foreground"
|
||||
: answers[index] !== undefined
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground hover:bg-muted/80"
|
||||
}`}
|
||||
>
|
||||
{index + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isLastQuestion ? (
|
||||
<Button onClick={handleSubmit} disabled={!hasAnswer} className="bg-green-600 hover:bg-green-700">
|
||||
提交測試
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleNext} disabled={!hasAnswer}>
|
||||
下一題
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress Summary */}
|
||||
<div className="mt-8 text-center text-sm text-muted-foreground">
|
||||
已完成 {Object.keys(answers).length} / {creativeQuestions.length} 題
|
||||
</div>
|
||||
</div>
|
||||
</TestLayout>
|
||||
)
|
||||
}
|
166
app/tests/logic/page.tsx
Normal file
166
app/tests/logic/page.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { TestLayout } from "@/components/test-layout"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { logicQuestions } from "@/lib/questions/logic-questions"
|
||||
|
||||
export default function LogicTestPage() {
|
||||
const router = useRouter()
|
||||
const [currentQuestion, setCurrentQuestion] = useState(0)
|
||||
const [answers, setAnswers] = useState<Record<number, string>>({})
|
||||
const [timeRemaining, setTimeRemaining] = useState(20 * 60) // 20 minutes in seconds
|
||||
|
||||
// Timer effect
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setTimeRemaining((prev) => {
|
||||
if (prev <= 1) {
|
||||
handleSubmit()
|
||||
return 0
|
||||
}
|
||||
return prev - 1
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(timer)
|
||||
}, [])
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60)
|
||||
const secs = seconds % 60
|
||||
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`
|
||||
}
|
||||
|
||||
const handleAnswerChange = (value: string) => {
|
||||
setAnswers((prev) => ({
|
||||
...prev,
|
||||
[currentQuestion]: value,
|
||||
}))
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentQuestion < logicQuestions.length - 1) {
|
||||
setCurrentQuestion((prev) => prev + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (currentQuestion > 0) {
|
||||
setCurrentQuestion((prev) => prev - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
// Calculate score
|
||||
let correctAnswers = 0
|
||||
logicQuestions.forEach((question, index) => {
|
||||
if (answers[index] === question.correctAnswer) {
|
||||
correctAnswers++
|
||||
}
|
||||
})
|
||||
|
||||
const score = Math.round((correctAnswers / logicQuestions.length) * 100)
|
||||
|
||||
// Store results in localStorage
|
||||
const results = {
|
||||
type: "logic",
|
||||
score,
|
||||
correctAnswers,
|
||||
totalQuestions: logicQuestions.length,
|
||||
answers,
|
||||
completedAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
localStorage.setItem("logicTestResults", JSON.stringify(results))
|
||||
router.push("/results/logic")
|
||||
}
|
||||
|
||||
const currentQ = logicQuestions[currentQuestion]
|
||||
const isLastQuestion = currentQuestion === logicQuestions.length - 1
|
||||
const hasAnswer = answers[currentQuestion] !== undefined
|
||||
|
||||
return (
|
||||
<TestLayout
|
||||
title="邏輯思維測試"
|
||||
currentQuestion={currentQuestion + 1}
|
||||
totalQuestions={logicQuestions.length}
|
||||
timeRemaining={formatTime(timeRemaining)}
|
||||
onBack={() => router.push("/")}
|
||||
>
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl text-balance">{currentQ.question}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RadioGroup value={answers[currentQuestion] || ""} onValueChange={handleAnswerChange} className="space-y-4">
|
||||
{currentQ.options.map((option, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center space-x-3 p-4 rounded-lg border hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<RadioGroupItem value={option.value} id={`option-${index}`} />
|
||||
<Label htmlFor={`option-${index}`} className="flex-1 cursor-pointer text-base leading-relaxed">
|
||||
{option.text}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<Button variant="outline" onClick={handlePrevious} disabled={currentQuestion === 0} size="sm">
|
||||
上一題
|
||||
</Button>
|
||||
|
||||
{isLastQuestion ? (
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!hasAnswer}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
size="sm"
|
||||
>
|
||||
提交測試
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleNext} disabled={!hasAnswer} size="sm">
|
||||
下一題
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-2 px-2">
|
||||
{logicQuestions.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setCurrentQuestion(index)}
|
||||
className={`w-8 h-8 rounded-full text-sm font-medium transition-colors flex-shrink-0 ${
|
||||
index === currentQuestion
|
||||
? "bg-primary text-primary-foreground"
|
||||
: answers[index] !== undefined
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "bg-muted text-muted-foreground hover:bg-muted/80"
|
||||
}`}
|
||||
>
|
||||
{index + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Summary */}
|
||||
<div className="mt-8 text-center text-sm text-muted-foreground">
|
||||
已完成 {Object.keys(answers).length} / {logicQuestions.length} 題
|
||||
</div>
|
||||
</div>
|
||||
</TestLayout>
|
||||
)
|
||||
}
|
151
app/tests/page.tsx
Normal file
151
app/tests/page.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Brain, Lightbulb, BarChart3, ArrowLeft } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
export default function TestsPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-card/50 backdrop-blur-sm">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href="/">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
返回首頁
|
||||
</Link>
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">測試中心</h1>
|
||||
<p className="text-sm text-muted-foreground">選擇您要進行的測試類型</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-bold text-foreground mb-4">選擇測試類型</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
我們提供三種不同的測試模式,您可以根據需要選擇單項測試或綜合評估
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Logic Test */}
|
||||
<Card className="group hover:shadow-lg transition-all duration-300 border-2 hover:border-primary/20">
|
||||
<CardHeader className="text-center pb-4">
|
||||
<div className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4 group-hover:bg-primary/20 transition-colors">
|
||||
<Brain className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl">邏輯思維測試</CardTitle>
|
||||
<CardDescription className="text-base">評估邏輯推理、分析判斷和問題解決能力</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-center">
|
||||
<div className="space-y-3 mb-6">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">題目數量</span>
|
||||
<span className="font-medium">10 題</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">題目類型</span>
|
||||
<span className="font-medium">單選題</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">預計時間</span>
|
||||
<span className="font-medium">15-20 分鐘</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button asChild className="w-full">
|
||||
<Link href="/tests/logic">開始測試</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Creative Test */}
|
||||
<Card className="group hover:shadow-lg transition-all duration-300 border-2 hover:border-accent/20">
|
||||
<CardHeader className="text-center pb-4">
|
||||
<div className="w-16 h-16 bg-accent/10 rounded-full flex items-center justify-center mx-auto mb-4 group-hover:bg-accent/20 transition-colors">
|
||||
<Lightbulb className="w-8 h-8 text-accent" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl">創意能力測試</CardTitle>
|
||||
<CardDescription className="text-base">評估創新思維、想像力和創造性解決問題的能力</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-center">
|
||||
<div className="space-y-3 mb-6">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">題目數量</span>
|
||||
<span className="font-medium">20 題</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">題目類型</span>
|
||||
<span className="font-medium">5級量表</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">預計時間</span>
|
||||
<span className="font-medium">25-30 分鐘</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="w-full border-accent text-accent hover:bg-accent hover:text-accent-foreground bg-transparent"
|
||||
>
|
||||
<Link href="/tests/creative">開始測試</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Combined Test */}
|
||||
<Card className="group hover:shadow-lg transition-all duration-300 border-2 border-primary/20 bg-gradient-to-br from-primary/5 to-accent/5 lg:col-span-1">
|
||||
<CardHeader className="text-center pb-4">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-primary to-accent rounded-full flex items-center justify-center mx-auto mb-4 group-hover:scale-110 transition-transform">
|
||||
<BarChart3 className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl">綜合能力測試</CardTitle>
|
||||
<CardDescription className="text-base">完整的邏輯思維 + 創意能力雙重評估</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-center">
|
||||
<div className="space-y-3 mb-6">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">題目數量</span>
|
||||
<span className="font-medium">30 題</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">測試內容</span>
|
||||
<span className="font-medium">邏輯 + 創意</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">預計時間</span>
|
||||
<span className="font-medium">40-45 分鐘</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button asChild size="lg" className="w-full text-lg">
|
||||
<Link href="/tests/combined">開始綜合測試</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Additional Info */}
|
||||
<div className="mt-12 text-center">
|
||||
<Card className="max-w-2xl mx-auto">
|
||||
<CardContent className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-3">測試說明</h3>
|
||||
<div className="text-sm text-muted-foreground space-y-2 text-left">
|
||||
<p>• 所有測試都有時間限制,請合理安排答題時間</p>
|
||||
<p>• 邏輯題和創意題不會混合出現,會分別進行</p>
|
||||
<p>• 綜合測試將先進行邏輯題,再進行創意題</p>
|
||||
<p>• 測試結果會自動保存,您可以隨時查看歷史成績</p>
|
||||
<p>• 建議在安靜的環境中完成測試,以獲得最佳結果</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user