Files
2025-10-12 04:19:52 +08:00

1495 lines
71 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useEffect, useState } from "react"
import { useParams, useRouter } from "next/navigation"
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 { CheckCircle, XCircle, Brain, Lightbulb, BarChart3, ArrowLeft, Loader2, Printer, Share2 } from "lucide-react"
import Link from "next/link"
import { CreativeAnalysis } from "@/components/creative-analysis"
import { CombinedAnalysis } from "@/components/combined-analysis"
interface User {
id: string
name: string
email: string
department: string
role: string
}
interface TestResult {
id: string
userId: string
type: "logic" | "creative" | "combined"
score: number
completedAt: string
isTimeout?: boolean
details?: {
logicScore?: number
creativeScore?: number
abilityBalance?: number
breakdown?: any
}
dimensionScores?: {
innovation: { percentage: number, rawScore: number, maxScore: number }
imagination: { percentage: number, rawScore: number, maxScore: number }
flexibility: { percentage: number, rawScore: number, maxScore: number }
originality: { percentage: number, rawScore: number, maxScore: number }
}
}
interface Question {
id: number
question?: string
statement?: string
option_a?: string
option_b?: string
option_c?: string
option_d?: string
option_e?: string
correct_answer?: 'A' | 'B' | 'C' | 'D' | 'E'
explanation?: string
type: 'logic' | 'creative'
userAnswer?: string | number
isCorrect?: boolean
score?: number
category?: 'innovation' | 'imagination' | 'flexibility' | 'originality'
created_at: string
}
interface DetailData {
result: TestResult
user: User
questions: Question[]
}
export default function AdminResultDetailPage() {
return (
<ProtectedRoute adminOnly>
<AdminResultDetailContent />
</ProtectedRoute>
)
}
function AdminResultDetailContent() {
const params = useParams()
const router = useRouter()
const testResultId = params.testResultId as string
const [detailData, setDetailData] = useState<DetailData | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [showDetailedResults, setShowDetailedResults] = useState(true)
useEffect(() => {
const loadDetailData = async () => {
if (!testResultId) return
setIsLoading(true)
setError(null)
try {
// 從 URL 參數獲取測試類型,如果沒有則嘗試從結果中獲取
const urlParams = new URLSearchParams(window.location.search)
const testType = urlParams.get('testType') as "logic" | "creative" | "combined"
if (!testType) {
setError("缺少測試類型參數")
return
}
const response = await fetch(`/api/admin/test-results/detail?testResultId=${testResultId}&testType=${testType}`)
const data = await response.json()
if (data.success) {
setDetailData(data.data)
} else {
setError(data.message || "載入詳細結果失敗")
}
} catch (error) {
console.error("載入詳細結果錯誤:", error)
setError("載入詳細結果時發生錯誤")
} finally {
setIsLoading(false)
}
}
loadDetailData()
}, [testResultId])
if (isLoading) {
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">
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4" />
<p className="text-muted-foreground">...</p>
</CardContent>
</Card>
</div>
)
}
if (error || !detailData) {
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">{error || "未找到測試結果"}</p>
<Button asChild>
<Link href="/admin/results"></Link>
</Button>
</CardContent>
</Card>
</div>
)
}
const { result, user, questions } = detailData
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 getDimensionInfo = (category: string) => {
switch (category) {
case "innovation":
return {
name: "創新能力",
color: "bg-blue-500",
textColor: "text-blue-600",
borderColor: "border-blue-200"
}
case "imagination":
return {
name: "想像力",
color: "bg-purple-500",
textColor: "text-purple-600",
borderColor: "border-purple-200"
}
case "flexibility":
return {
name: "靈活性",
color: "bg-green-500",
textColor: "text-green-600",
borderColor: "border-green-200"
}
case "originality":
return {
name: "原創性",
color: "bg-orange-500",
textColor: "text-orange-600",
borderColor: "border-orange-200"
}
default:
return {
name: "未知維度",
color: "bg-gray-500",
textColor: "text-gray-600",
borderColor: "border-gray-200"
}
}
}
const getScoreLevel = (score: number, type: string) => {
if (type === "logic") {
if (score === 100) return {
level: "邏輯巔峰者",
color: "bg-purple-600",
description: "近乎完美的邏輯典範!你像一台「推理引擎」,嚴謹又高效,幾乎不受陷阱干擾。",
suggestion: "多和他人分享你的思考路徑,能幫助團隊整體邏輯力提升。"
}
if (score >= 80) return {
level: "邏輯大師",
color: "bg-green-500",
description: "你的思維如同精密儀器,能快速抓住題目關鍵,並做出有效推理。常常是團隊中「冷靜的分析者」。",
suggestion: "挑戰更高層次的難題,讓你的邏輯力更加精進。"
}
if (score >= 60) return {
level: "邏輯高手",
color: "bg-blue-500",
description: "邏輯清晰穩定,大部分情境都能正確判斷。偶爾會因粗心錯過陷阱。",
suggestion: "在思維縝密之餘,更加留心細節,就能把錯誤率降到最低。"
}
if (score >= 30) return {
level: "邏輯學徒",
color: "bg-yellow-500",
description: "已經抓到一些邏輯規律,能解決中等難度的問題。遇到複雜情境時,仍可能卡關。",
suggestion: "嘗試將問題拆解成小步驟,就像組裝樂高,每一塊拼好,答案就自然浮現。"
}
return {
level: "邏輯探險新手",
color: "bg-red-500",
description: "還在邏輯森林的入口徘徊。思考時可能忽略細節,或被陷阱誤導。",
suggestion: "多練習經典邏輯題,像是在拼拼圖般,慢慢建立清晰的分析步驟。"
}
} else if (type === "creative") {
if (score >= 90) return {
level: "創意大師",
color: "bg-purple-600",
description: "你的創意如泉水般源源不絕,總能提出令人驚豔的解決方案!",
suggestion: "繼續保持這種創意精神,並嘗試將創意轉化為實際行動。"
}
if (score >= 75) return {
level: "創意引領者",
color: "bg-blue-500",
description: "你是靈感的推動者!總是能在團體中主動拋出新想法,激發別人跟進。",
suggestion: "持續累積學習,讓你的靈感不僅是點子,而能帶動真正的行動。"
}
if (score >= 55) return {
level: "創意實踐者",
color: "bg-green-500",
description: "靈感已經隨手可得,在團體中也常被認為是「有創意的人」。",
suggestion: "再給自己一點勇氣,不要害怕挑戰慣例,你的創意將更有力量。"
}
if (score >= 35) return {
level: "創意開拓者",
color: "bg-yellow-500",
description: "你其實有自己的想法,但有時習慣跟隨大多數人的步伐。",
suggestion: "試著勇敢說出腦中天馬行空的念頭,你會發現,這些點子或許就是團隊需要的突破口。"
}
return {
level: "創意萌芽者",
color: "bg-red-500",
description: "還在創意旅程的起點。雖然暫時表現平淡,但這正是無限潛力的開端!",
suggestion: "觀察生活小事,或閱讀不同領域的內容,讓靈感一點一滴積累。"
}
} else {
// combined
if (score >= 90) return {
level: "全能高手",
color: "bg-gradient-to-r from-purple-500 to-blue-500",
description: "邏輯與創意完美結合,是團隊中的全能型人才!",
suggestion: "繼續保持這種平衡,並嘗試帶領團隊解決複雜問題。"
}
if (score >= 80) return {
level: "綜合專家",
color: "bg-gradient-to-r from-blue-500 to-green-500",
description: "邏輯思維和創意能力都很出色,能夠勝任各種挑戰。",
suggestion: "繼續精進兩種能力,成為更全面的專業人才。"
}
if (score >= 70) return {
level: "平衡發展者",
color: "bg-gradient-to-r from-green-500 to-yellow-500",
description: "邏輯和創意能力都有一定水準,正在朝全面發展邁進。",
suggestion: "針對較弱的能力進行重點提升,達到更好的平衡。"
}
if (score >= 50) return {
level: "潛力新星",
color: "bg-gradient-to-r from-yellow-500 to-orange-500",
description: "有發展潛力,需要更多練習來提升綜合能力。",
suggestion: "制定學習計劃,系統性地提升邏輯和創意能力。"
}
return {
level: "待提升",
color: "bg-gradient-to-r from-orange-500 to-red-500",
description: "綜合能力有待提升,建議系統性訓練邏輯思維和創意能力。",
suggestion: "從基礎開始,逐步建立邏輯思維和創意思維。"
}
}
}
const testTypeInfo = getTestTypeInfo(result.type)
const scoreLevel = getScoreLevel(result.score, result.type)
const IconComponent = testTypeInfo.icon
// 計算統計數據
const logicQuestions = questions.filter(q => q.type === 'logic')
const creativeQuestions = questions.filter(q => q.type === 'creative')
const correctAnswers = logicQuestions.filter(q => q.isCorrect).length
const totalQuestions = questions.length
// 計算創意測試的統計數據
const creativeTotalScore = creativeQuestions.reduce((sum, q) => sum + (q.score || 0), 0)
const creativeMaxScore = creativeQuestions.length * 5
const creativeScorePercentage = creativeQuestions.length > 0 ? Math.round((creativeTotalScore / creativeMaxScore) * 100) : 0
// 如果沒有從答案中獲得分數,使用結果中的分數
const displayTotalScore = creativeTotalScore > 0 ? creativeTotalScore : result.score
const displayMaxScore = creativeMaxScore > 0 ? creativeMaxScore : 100
const displayScorePercentage = creativeScorePercentage > 0 ? creativeScorePercentage : result.score
return (
<div className="min-h-screen bg-background admin-result-print">
{/* Header */}
<header className="border-b bg-card/50 backdrop-blur-sm">
<div className="container mx-auto px-3 sm:px-4 py-3 sm:py-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
{/* 返回按鈕和標題區域 */}
<div className="flex items-start gap-3">
<Button variant="ghost" size="sm" asChild className="flex-shrink-0">
<Link href="/admin/results">
<ArrowLeft className="w-4 h-4 sm:mr-2" />
<span className="hidden sm:inline"></span>
</Link>
</Button>
<div className={`w-8 h-8 sm:w-10 sm:h-10 ${testTypeInfo.color} rounded-lg flex items-center justify-center flex-shrink-0`}>
<IconComponent className="w-5 h-5 sm:w-6 sm:h-6 text-white" />
</div>
<div className="min-w-0 flex-1">
<h1 className="text-base sm:text-lg lg:text-xl font-bold text-foreground leading-tight">
{user.name} - {testTypeInfo.name}
</h1>
<p className="text-xs sm:text-sm text-muted-foreground mt-1">
{new Date(result.completedAt).toLocaleString("zh-TW")}
</p>
</div>
</div>
{/* 功能按鈕區域 */}
<div className="flex justify-end gap-2 sm:gap-3">
<Button
onClick={() => {
if (navigator.share) {
navigator.share({
title: `${user.name} - ${testTypeInfo.name}測試結果`,
text: `查看${user.name}${testTypeInfo.name}測試結果`,
url: window.location.href
})
} else {
navigator.clipboard.writeText(window.location.href)
alert('連結已複製到剪貼簿')
}
}}
variant="outline"
size="sm"
className="print:hidden flex-shrink-0"
>
<Share2 className="w-4 h-4 sm:mr-2" />
<span className="hidden sm:inline"></span>
</Button>
<Button
onClick={() => {
// 創建一個新的列印窗口
const printWindow = window.open('', '_blank', 'width=800,height=600')
if (printWindow) {
// 獲取當前頁面的數據
const userInfo = `
<div class="card equal-height">
<h3>
<svg class="icon icon-user" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
用戶資訊
</h3>
<div class="info-grid">
<div class="info-label">姓名:</div>
<div class="info-value">${user.name}</div>
<div class="info-label">電子郵件:</div>
<div class="info-value">${user.email}</div>
<div class="info-label">部門:</div>
<div class="info-value">${user.department}</div>
<div class="info-label">角色:</div>
<div class="info-value">${user.role}</div>
</div>
</div>
`
const testResult = `
<div class="card equal-height centered-content">
<div class="score-circle">
<span>${result.score}</span>
</div>
<h3>綜合測試${result.isTimeout ? '(時間到)' : ''}完成!</h3>
<div class="level-badge">${getScoreLevel(result.score, result.type).level}</div>
${result.isTimeout ? '<div class="timeout-badge">時間到強制提交</div>' : ''}
<div class="description">${getScoreLevel(result.score, result.type).description}</div>
</div>
`
let logicTest = ''
let creativeTest = ''
let recommendations = ['繼續保持現有的學習節奏,持續提升各方面能力'] // 默認建議
// 定義默認變數,避免作用域問題
let logicScore = 0
let creativityScore = 0
let balanceScore = 0
let logicStatus = '需要加強'
let creativityStatus = '需要提升'
let balanceStatus = '失衡嚴重'
if (result.type === 'combined' && result.details) {
// 調試:輸出數據結構
console.log('Print Data Debug:', {
details: result.details,
result: result
})
// 從 breakdown 中獲取正確的數據
const logicBreakdown = result.details.breakdown || {}
const creativeBreakdown = result.details.creativeBreakdown || {}
console.log('Logic breakdown:', logicBreakdown)
console.log('Creative breakdown:', creativeBreakdown)
// 使用與網頁相同的計算方式
const logicQuestions = questions.filter(q => q.type === 'logic')
const creativeQuestions = questions.filter(q => q.type === 'creative')
// 邏輯測驗:計算正確答案數
const logicCorrectAnswers = logicQuestions.filter(q => q.isCorrect).length
const logicTotalQuestions = logicQuestions.length
// 創意測驗:計算實際分數
const creativeScore = creativeQuestions.reduce((sum, q) => sum + (q.score || 0), 0)
const creativeMaxScore = creativeQuestions.length * 5 // 每題5分
console.log('Extracted values:', {
logicCorrectAnswers,
logicTotalQuestions,
creativeScore,
creativeMaxScore
})
const logicLevelData = getScoreLevel(result.details.logicScore || 0, 'logic')
const creativeLevelData = getScoreLevel(result.details.creativeScore || 0, 'creative')
// 使用與 CombinedAnalysis 組件相同的邏輯
logicScore = result.details.logicScore || 0
creativityScore = result.details.creativeScore || 0
balanceScore = ((logicScore + creativityScore) / 2)
// 獲取能力狀態(與 CombinedAnalysis 組件相同)
const getAbilityStatus = (score: number) => {
if (score >= 80) return "表現優秀"
if (score >= 60) return "表現良好"
if (score >= 40) return "需要提升"
return "需要加強"
}
// 獲取平衡狀態(與 CombinedAnalysis 組件相同)
const getBalanceStatus = (score: number) => {
if (score >= 80) return "非常均衡"
if (score >= 60) return "相對均衡"
if (score >= 40) return "需要平衡"
return "失衡嚴重"
}
// 生成發展建議(與 CombinedAnalysis 組件相同的邏輯)
const generateRecommendations = () => {
const recommendations = []
if (logicScore < 70) {
recommendations.push("建議加強邏輯思維訓練,多做推理題和數學題")
recommendations.push("學習系統性思維方法,如思維導圖、流程圖等")
}
if (creativityScore < 70) {
recommendations.push("建議參與更多創意活動,如頭腦風暴、設計思維工作坊")
recommendations.push("培養好奇心,多接觸不同領域的知識和經驗")
}
const scoreDiff = Math.abs(logicScore - creativityScore)
if (scoreDiff > 20) {
if (logicScore > creativityScore) {
recommendations.push("您的邏輯思維較強,建議平衡發展創意能力")
} else {
recommendations.push("您的創意能力較強,建議平衡發展邏輯思維")
}
}
if (logicScore >= 80 && creativityScore >= 80) {
recommendations.push("您具備優秀的綜合能力,建議承擔更多挑戰性工作")
recommendations.push("可以考慮擔任需要創新和分析並重的領導角色")
}
return recommendations.length > 0 ? recommendations : ["繼續保持現有的學習節奏,持續提升各方面能力"]
}
recommendations = generateRecommendations()
logicStatus = getAbilityStatus(logicScore)
creativityStatus = getAbilityStatus(creativityScore)
balanceStatus = getBalanceStatus(balanceScore)
console.log('Generated recommendations:', recommendations)
logicTest = `
<div class="card">
<h4>
<svg class="icon icon-brain" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>
</svg>
邏輯思維測試
</h4>
<div class="score-section">
<div class="score-text">得分</div>
<div class="progress-container">
<div class="progress-bar logic-progress">
<div class="progress-fill" style="width: ${Math.min((logicCorrectAnswers / (logicTotalQuestions || 1)) * 100, 100)}%"></div>
</div>
<span class="score-number logic-score">${logicCorrectAnswers}</span>
</div>
</div>
<div class="stats-grid">
<div class="stat-box correct">
<div class="stat-number">${logicCorrectAnswers}</div>
<div class="stat-label">答對題數</div>
</div>
<div class="stat-box total">
<div class="stat-number">${logicTotalQuestions}</div>
<div class="stat-label">總題數</div>
</div>
</div>
<div class="level-section">
<div class="level-badge logic-badge">${logicLevelData.level}</div>
<div class="description">${logicLevelData.description}</div>
<div class="suggestion">
<svg class="icon suggestion-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"></path>
</svg>
${logicLevelData.suggestion}
</div>
</div>
</div>
`
creativeTest = `
<div class="card">
<h4>
<svg class="icon icon-lightbulb" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>
</svg>
創意能力測試
</h4>
<div class="score-section">
<div class="score-text">得分</div>
<div class="progress-container">
<div class="progress-bar creative-progress">
<div class="progress-fill" style="width: ${Math.min((creativeScore / (creativeMaxScore || 1)) * 100, 100)}%"></div>
</div>
<span class="score-number creative-score">${creativeScore}</span>
</div>
</div>
<div class="stats-grid">
<div class="stat-box raw">
<div class="stat-number">${creativeScore}</div>
<div class="stat-label">原始得分</div>
</div>
<div class="stat-box max">
<div class="stat-number">${creativeMaxScore}</div>
<div class="stat-label">滿分</div>
</div>
</div>
<div class="level-section">
<div class="level-badge creative-badge">${creativeLevelData.level}</div>
<div class="description">${creativeLevelData.description}</div>
<div class="suggestion">
<svg class="icon suggestion-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"></path>
</svg>
${creativeLevelData.suggestion}
</div>
</div>
</div>
`
}
// 創建一頁 A4 的緊湊列印內容
const printContent = `
<!DOCTYPE html>
<html>
<head>
<title>測試結果列印</title>
<style>
* { box-sizing: border-box; }
body {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
margin: 0;
padding: 0.6rem;
background: #f8fafc;
color: #0f172a;
line-height: 1.4;
font-size: 0.75rem;
}
.header {
text-align: center;
margin-bottom: 0.8rem;
padding-bottom: 0.6rem;
border-bottom: 1px solid #e2e8f0;
}
.header h1 {
margin: 0 0 0.25rem 0;
font-size: 1.125rem;
font-weight: 700;
color: #0f172a;
}
.header .subtitle {
color: #64748b;
font-size: 0.7rem;
}
.row {
display: flex;
gap: 0.6rem;
margin-bottom: 0.8rem;
align-items: stretch;
}
.card {
flex: 1;
background: white;
border: 1px solid #e2e8f0;
border-radius: 0.375rem;
padding: 0.8rem;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
display: flex;
flex-direction: column;
}
.equal-height {
min-height: 13rem;
display: flex;
flex-direction: column;
}
.centered-content {
text-align: center;
justify-content: center;
align-items: center;
}
.card h3 {
margin: 0 0 0.5rem 0;
font-size: 0.875rem;
font-weight: 600;
color: #0f172a;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 0.25rem;
display: flex;
align-items: center;
gap: 0.25rem;
}
.card h4 {
margin: 0 0 0.5rem 0;
font-size: 0.8rem;
font-weight: 600;
color: #374151;
display: flex;
align-items: center;
gap: 0.25rem;
}
.icon {
width: 0.875rem;
height: 0.875rem;
display: inline-block;
}
.icon-user { color: #3b82f6; }
.icon-brain { color: #3b82f6; }
.icon-lightbulb { color: #f59e0b; }
.icon-target { color: #10b981; }
.icon-trending { color: #8b5cf6; }
.icon-search { color: #ef4444; }
.info-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.6rem 1rem;
margin-bottom: 0.6rem;
align-items: center;
}
.info-label {
font-weight: 500;
color: #6b7280;
font-size: 0.7rem;
white-space: nowrap;
}
.info-value {
color: #0f172a;
font-size: 0.7rem;
word-break: break-all;
}
.score-circle {
width: 2.8rem;
height: 2.8rem;
border-radius: 50%;
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 0.6rem;
box-shadow: 0 2px 4px -1px rgb(0 0 0 / 0.1);
}
.score-circle span {
color: white;
font-size: 1.1rem;
font-weight: bold;
}
.level-badge {
display: inline-block;
background: #f1f5f9;
color: #475569;
padding: 0.2rem 0.4rem;
border-radius: 9999px;
font-size: 0.65rem;
font-weight: 500;
margin-bottom: 0.4rem;
}
.timeout-badge {
background: #fef2f2;
color: #dc2626;
border: 1px solid #fecaca;
padding: 0.3rem;
border-radius: 0.25rem;
font-size: 0.65rem;
font-weight: 500;
text-align: center;
margin-bottom: 0.6rem;
}
.description {
color: #4b5563;
font-size: 0.65rem;
margin-bottom: 0.6rem;
line-height: 1.4;
}
.stats {
display: flex;
gap: 0.6rem;
margin-bottom: 0.6rem;
}
.stat-item {
flex: 1;
text-align: center;
padding: 0.5rem;
background: #f8fafc;
border-radius: 0.25rem;
border: 1px solid #e2e8f0;
}
.stat-number {
font-size: 0.9rem;
font-weight: bold;
color: #0f172a;
}
.stat-label {
font-size: 0.65rem;
color: #64748b;
margin-top: 0.2rem;
}
.score-section {
margin-bottom: 0.8rem;
}
.score-text {
font-size: 0.65rem;
color: #6b7280;
margin-bottom: 0.3rem;
}
.progress-container {
display: flex;
align-items: center;
gap: 0.6rem;
}
.progress-bar {
flex: 1;
height: 0.4rem;
background: #e5e7eb;
border-radius: 9999px;
overflow: hidden;
position: relative;
}
.logic-progress .progress-fill {
background: #dc2626;
}
.creative-progress .progress-fill {
background: #f59e0b;
}
.progress-fill {
height: 100%;
border-radius: 9999px;
transition: width 0.3s ease;
}
.score-number {
font-size: 0.8rem;
font-weight: bold;
min-width: 1.8rem;
text-align: right;
}
.logic-score {
color: #dc2626;
}
.creative-score {
color: #f59e0b;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.6rem;
margin-bottom: 0.8rem;
}
.stat-box {
background: #f8fafc;
border-radius: 0.375rem;
padding: 0.6rem;
text-align: center;
border: 1px solid #e2e8f0;
}
.stat-box.correct .stat-number {
color: #059669;
}
.stat-box.total .stat-number {
color: #3b82f6;
}
.stat-box.raw .stat-number {
color: #059669;
}
.stat-box.max .stat-number {
color: #3b82f6;
}
.level-section {
margin-bottom: 0.6rem;
}
.level-badge {
display: inline-block;
background: #f1f5f9;
color: #475569;
padding: 0.2rem 0.5rem;
border-radius: 9999px;
font-size: 0.65rem;
font-weight: 500;
margin-bottom: 0.6rem;
}
.logic-badge {
background: #fef2f2;
color: #dc2626;
}
.creative-badge {
background: #fef3c7;
color: #f59e0b;
}
.description {
color: #4b5563;
font-size: 0.65rem;
margin-bottom: 0.6rem;
line-height: 1.4;
}
.suggestion {
display: flex;
align-items: flex-start;
gap: 0.3rem;
color: #374151;
font-size: 0.65rem;
line-height: 1.4;
}
.suggestion-icon {
width: 0.8rem;
height: 0.8rem;
color: #6b7280;
flex-shrink: 0;
margin-top: 0.15rem;
}
.ability-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.6rem;
margin-bottom: 0.8rem;
}
.ability-card {
background: white;
border: 1px solid #e2e8f0;
border-radius: 0.375rem;
padding: 0.6rem;
text-align: center;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.ability-icon {
width: 1.3rem;
height: 1.3rem;
margin: 0 auto 0.3rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.ability-title {
font-size: 0.65rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.3rem;
}
.ability-score {
font-size: 1.1rem;
font-weight: bold;
margin-bottom: 0.25rem;
}
.ability-score.logic { color: #dc2626; }
.ability-score.creative { color: #f59e0b; }
.ability-score.balance { color: #f59e0b; }
.ability-status {
font-size: 0.65rem;
color: #6b7280;
margin-bottom: 0.3rem;
}
.progress-bar {
width: 100%;
height: 0.3rem;
background: #e5e7eb;
border-radius: 9999px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #3b82f6;
border-radius: 9999px;
}
.suggestions {
background: white;
border: 1px solid #e2e8f0;
border-radius: 0.375rem;
padding: 0.8rem;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
margin-top: 0.8rem;
margin-bottom: 0;
}
.suggestions h3 {
margin: 0 0 0.6rem 0;
font-size: 0.85rem;
font-weight: 600;
color: #0f172a;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 0.3rem;
display: flex;
align-items: center;
gap: 0.3rem;
}
.suggestion-item {
display: flex;
align-items: flex-start;
gap: 0.5rem;
margin-bottom: 0.4rem;
padding: 0.4rem;
background: #f8fafc;
border-radius: 0.25rem;
border: 1px solid #e2e8f0;
}
.suggestion-item:last-child {
margin-bottom: 0;
}
.suggestion-number {
width: 1.1rem;
height: 1.1rem;
background: #3b82f6;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.65rem;
font-weight: bold;
flex-shrink: 0;
}
.suggestion-text {
color: #374151;
font-size: 0.65rem;
line-height: 1.4;
}
.no-print {
position: fixed;
top: 0.5rem;
right: 0.5rem;
z-index: 1000;
}
.no-print button {
background: #3b82f6;
color: white;
border: none;
padding: 0.375rem 0.75rem;
border-radius: 0.25rem;
margin-left: 0.25rem;
cursor: pointer;
font-weight: 500;
font-size: 0.7rem;
}
.no-print button:hover {
background: #2563eb;
}
.no-print button.secondary {
background: #6b7280;
}
.no-print button.secondary:hover {
background: #4b5563;
}
@media print {
body {
background: white;
margin: 0.4cm;
padding: 0;
font-size: 0.7rem;
}
.no-print { display: none; }
.card, .ability-card, .suggestions {
box-shadow: none;
border: 1px solid #d1d5db;
}
@page {
size: A4;
margin: 0.4cm;
}
}
</style>
</head>
<body>
<div class="header">
<h1>${user.name} - 綜合能力測試結果</h1>
<div class="subtitle">完成時間: ${new Date(result.completedAt).toLocaleString("zh-TW")}</div>
</div>
<!-- 第一列:用戶資料 + 測驗結果 -->
<div class="row">
${userInfo}
${testResult}
</div>
<!-- 第二列:邏輯測驗 + 創意測驗 -->
${logicTest && creativeTest ? `
<div class="row">
${logicTest}
${creativeTest}
</div>
` : ''}
<!-- 第三列:能力分析 -->
${result.type === 'combined' && result.details ? `
<div class="card">
<h3>
<svg class="icon icon-trending" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path>
</svg>
能力分析
</h3>
<div class="ability-grid">
<div class="ability-card">
<div class="ability-icon" style="background: #dbeafe;">
<svg class="icon icon-brain" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>
</svg>
</div>
<div class="ability-title">邏輯思維</div>
<div class="ability-score logic">${logicScore}</div>
<div class="ability-status">${logicStatus}</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${Math.min(logicScore, 100)}%"></div>
</div>
</div>
<div class="ability-card">
<div class="ability-icon" style="background: #fef3c7;">
<svg class="icon icon-lightbulb" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>
</svg>
</div>
<div class="ability-title">創意能力</div>
<div class="ability-score creative">${creativityScore}</div>
<div class="ability-status">${creativityStatus}</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${Math.min(creativityScore, 100)}%"></div>
</div>
</div>
<div class="ability-card">
<div class="ability-icon" style="background: #d1fae5;">
<svg class="icon icon-target" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div class="ability-title">能力平衡</div>
<div class="ability-score balance">${Math.round(balanceScore)}</div>
<div class="ability-status">${balanceStatus}</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${Math.min(balanceScore, 100)}%"></div>
</div>
</div>
</div>
</div>
<!-- 第四列:發展建議 -->
<div class="suggestions">
<h3>
<svg class="icon icon-search" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
發展建議
</h3>
${recommendations.map((recommendation, index) => `
<div class="suggestion-item">
<div class="suggestion-number">${index + 1}</div>
<div class="suggestion-text">${recommendation}</div>
</div>
`).join('')}
</div>
` : ''}
<div class="no-print">
<button onclick="window.print()">列印</button>
<button onclick="window.close()" class="secondary">關閉</button>
</div>
</body>
</html>
`
printWindow.document.write(printContent)
printWindow.document.close()
// 等待內容載入後自動列印
printWindow.onload = () => {
setTimeout(() => {
printWindow.print()
}, 500)
}
} else {
// 如果無法打開新窗口,使用原始方法
window.print()
}
}}
variant="outline"
size="sm"
className="print:hidden flex-shrink-0"
>
<Printer className="w-4 h-4 sm:mr-2" />
<span className="hidden sm:inline"></span>
</Button>
</div>
</div>
</div>
</header>
<div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto space-y-8">
{/* User Info */}
<Card className="print-user-info">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-muted-foreground"></label>
<p className="text-sm font-medium">{user.name}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground"></label>
<p className="text-sm">{user.email}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground"></label>
<p className="text-sm">{user.department}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground"></label>
<Badge variant="outline">{user.role}</Badge>
</div>
</div>
</CardContent>
</Card>
{/* Logic Test Overview */}
{result.type === 'logic' && (
<Card className="text-center mb-6 print-logic-test">
<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">{result.score}</span>
</div>
<CardTitle className="text-3xl mb-2">
{result.isTimeout ? '(時間到)' : ''}
</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>
{result.isTimeout && (
<Badge variant="destructive" className="text-lg px-4 py-1">
</Badge>
)}
</div>
<p className="text-lg text-muted-foreground mb-3">{scoreLevel.description}</p>
<div className="bg-muted/50 rounded-lg p-4 text-sm">
<p className="text-muted-foreground">
<span className="font-medium">👉 </span>
{scoreLevel.suggestion}
</p>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="text-center">
<div className="text-2xl font-bold text-green-600 mb-1">{correctAnswers}</div>
<div className="text-xs text-muted-foreground"></div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-primary mb-1">{totalQuestions}</div>
<div className="text-xs text-muted-foreground"></div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-accent mb-1">
{totalQuestions > 0 ? Math.round((correctAnswers / totalQuestions) * 100) : 0}%
</div>
<div className="text-xs text-muted-foreground"></div>
</div>
</div>
<Progress value={result.score} className="h-3 mb-4" />
</CardContent>
</Card>
)}
{/* Creative Test Overview */}
{result.type === 'creative' && (
<Card className="text-center mb-6 print-creative-test">
<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">{result.score}</span>
</div>
<CardTitle className="text-3xl mb-2">
{result.isTimeout ? '(時間到)' : ''}
</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>
{result.isTimeout && (
<Badge variant="destructive" className="text-lg px-4 py-1">
</Badge>
)}
</div>
<p className="text-lg text-muted-foreground mb-3">{scoreLevel.description}</p>
<div className="bg-muted/50 rounded-lg p-4 text-sm">
<p className="text-muted-foreground">
<span className="font-medium">👉 </span>
{scoreLevel.suggestion}
</p>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="text-center">
<div className="text-2xl font-bold text-green-600 mb-1">{displayTotalScore}</div>
<div className="text-xs text-muted-foreground"></div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-primary mb-1">{displayMaxScore}</div>
<div className="text-xs text-muted-foreground">滿</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-accent mb-1">
{displayScorePercentage}%
</div>
<div className="text-xs text-muted-foreground"></div>
</div>
</div>
<Progress value={result.score} className="h-3 mb-4" />
</CardContent>
</Card>
)}
{/* Creative Analysis for Creative Tests */}
{result.type === 'creative' && result.dimensionScores && (
<CreativeAnalysis
score={result.score}
dimensionScores={result.dimensionScores}
creativityLevel={scoreLevel}
totalScore={displayTotalScore}
maxScore={displayMaxScore}
/>
)}
{/* Combined Test Analysis */}
{result.type === 'combined' && result.details && (
<CombinedAnalysis
overallScore={result.score}
logicScore={result.details.logicScore || 0}
creativityScore={result.details.creativeScore || 0}
balanceScore={result.details.abilityBalance || 0}
level={getScoreLevel(result.score, result.type).level}
description={getScoreLevel(result.score, result.type).description}
isTimeout={result.isTimeout}
logicBreakdown={result.details.breakdown}
creativityBreakdown={result.details.breakdown}
// 個別測試結果的詳細資料
logicCorrectAnswers={logicQuestions.filter(q => q.isCorrect).length}
logicTotalQuestions={logicQuestions.length}
logicLevel={getScoreLevel(result.details.logicScore || 0, 'logic').level}
logicDescription={getScoreLevel(result.details.logicScore || 0, 'logic').description}
logicSuggestion={getScoreLevel(result.details.logicScore || 0, 'logic').suggestion}
creativityTotalScore={creativeQuestions.reduce((sum, q) => sum + (q.score || 0), 0)}
creativityMaxScore={creativeQuestions.length * 5}
creativityLevel={getScoreLevel(result.details.creativeScore || 0, 'creative').level}
creativityDescription={getScoreLevel(result.details.creativeScore || 0, 'creative').description}
creativitySuggestion={getScoreLevel(result.details.creativeScore || 0, 'creative').suggestion}
/>
)}
{/* Detailed Results */}
{questions.length > 0 && showDetailedResults && (
<Card id="detailed-results-card" className="hide-in-print">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Logic Questions */}
{logicQuestions.length > 0 && (
<div>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Brain className="w-5 h-5 text-blue-600" />
</h3>
<div className="space-y-4">
{logicQuestions.map((question, index) => {
const userAnswer = question.userAnswer as string
const isCorrect = question.isCorrect
const getOptionText = (option: string) => {
switch (option) {
case 'A': return question.option_a
case 'B': return question.option_b
case 'C': return question.option_c
case 'D': return question.option_d
case 'E': return question.option_e
default: return "未作答"
}
}
const correctOptionText = getOptionText(question.correct_answer || '')
const userOptionText = userAnswer ? getOptionText(userAnswer) : "未作答"
return (
<div key={question.id} className="border rounded-lg p-3 sm:p-4 bg-blue-50/30">
<div className="flex items-start gap-3 mb-3">
<div className="flex-shrink-0 mt-1">
{isCorrect ? (
<CheckCircle className="w-4 h-4 sm:w-5 sm:h-5 text-green-500" />
) : (
<XCircle className="w-4 h-4 sm:w-5 sm:h-5 text-red-500" />
)}
</div>
<div className="flex-1 min-w-0">
<h4 className="font-medium mb-2 text-sm sm:text-base text-balance">
{index + 1}{question.question}
</h4>
<div className="space-y-2 text-xs sm:text-sm">
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
<span className="text-muted-foreground text-xs"></span>
<Badge variant={isCorrect ? "default" : "destructive"} className="text-xs w-fit">
{userAnswer ? `${userAnswer}. ${userOptionText}` : "未作答"}
</Badge>
</div>
{!isCorrect && (
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
<span className="text-muted-foreground text-xs"></span>
<Badge variant="outline" className="border-green-500 text-green-700 text-xs w-fit">
{question.correct_answer}. {correctOptionText}
</Badge>
</div>
)}
{question.explanation && (
<div className="mt-2 p-2 sm:p-3 bg-muted/50 rounded text-xs sm:text-sm">
<strong></strong>
{question.explanation}
</div>
)}
</div>
</div>
</div>
</div>
)
})}
</div>
</div>
)}
{/* Creative Questions */}
{creativeQuestions.length > 0 && (
<div>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Lightbulb className="w-5 h-5 text-green-600" />
</h3>
<div className="space-y-4">
{creativeQuestions.map((question, index) => {
const dimensionInfo = getDimensionInfo(question.category || 'innovation')
return (
<div key={question.id} className={`border rounded-lg p-3 sm:p-4 ${dimensionInfo.borderColor} bg-opacity-30`} style={{ backgroundColor: `${dimensionInfo.color.replace('bg-', '')}10` }}>
<div className="flex items-start justify-between mb-2 sm:mb-3">
<div className="flex items-center gap-2">
<h4 className="font-medium text-sm sm:text-base"> {index + 1} </h4>
<Badge variant="outline" className={`${dimensionInfo.textColor} ${dimensionInfo.borderColor} text-xs`}>
{dimensionInfo.name}
</Badge>
</div>
<Badge variant="outline" className="text-green-600 border-green-600 text-xs">
{question.score}
</Badge>
</div>
<div className="space-y-2 sm:space-y-3">
<div>
<label className="text-sm font-medium text-muted-foreground"></label>
<p className="text-xs sm:text-sm mt-1 break-words">{question.statement}</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
<div>
<label className="text-sm font-medium text-muted-foreground"></label>
<p className="text-xs sm:text-sm mt-1">{question.userAnswer}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground"></label>
<p className="text-xs sm:text-sm mt-1 font-bold">{question.score} </p>
</div>
</div>
</div>
</div>
)
})}
</div>
</div>
)}
</div>
</CardContent>
</Card>
)}
{/* Actions */}
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4 justify-center">
<Button asChild size="lg" className="w-full sm:w-auto">
<Link href="/admin/results">
<ArrowLeft className="w-4 h-4 mr-2" />
<span></span>
</Link>
</Button>
</div>
</div>
</div>
</div>
)
}