817 lines
32 KiB
TypeScript
817 lines
32 KiB
TypeScript
"use client"
|
||
|
||
import type React from "react"
|
||
|
||
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 { 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,
|
||
Loader2,
|
||
ChevronLeft,
|
||
ChevronRight,
|
||
} from "lucide-react"
|
||
import Link from "next/link"
|
||
import { parseExcelFile, type ImportResult, exportLogicQuestionsToExcel, exportCreativeQuestionsToExcel } from "@/lib/utils/excel-parser"
|
||
|
||
// 定義題目類型
|
||
interface LogicQuestion {
|
||
id: number
|
||
question: string
|
||
option_a: string
|
||
option_b: string
|
||
option_c: string
|
||
option_d: string
|
||
option_e?: string
|
||
correct_answer: string
|
||
explanation: string
|
||
}
|
||
|
||
interface CreativeQuestion {
|
||
id: number
|
||
statement: string
|
||
category: string
|
||
is_reverse: boolean
|
||
}
|
||
|
||
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 [logicQuestions, setLogicQuestions] = useState<LogicQuestion[]>([])
|
||
const [creativeQuestions, setCreativeQuestions] = useState<CreativeQuestion[]>([])
|
||
const [isLoading, setIsLoading] = useState(true)
|
||
const [error, setError] = useState<string | null>(null)
|
||
|
||
// 分頁狀態
|
||
const [currentPage, setCurrentPage] = useState(1)
|
||
const [currentLogicPage, setCurrentLogicPage] = useState(1)
|
||
const [itemsPerPage] = useState(10)
|
||
|
||
// 分頁計算
|
||
const totalPages = Math.ceil(creativeQuestions.length / itemsPerPage)
|
||
const startIndex = (currentPage - 1) * itemsPerPage
|
||
const endIndex = startIndex + itemsPerPage
|
||
const currentCreativeQuestions = creativeQuestions.slice(startIndex, endIndex)
|
||
|
||
// 邏輯題目分頁計算
|
||
const totalLogicPages = Math.ceil(logicQuestions.length / itemsPerPage)
|
||
const logicStartIndex = (currentLogicPage - 1) * itemsPerPage
|
||
const logicEndIndex = logicStartIndex + itemsPerPage
|
||
const currentLogicQuestions = logicQuestions.slice(logicStartIndex, logicEndIndex)
|
||
|
||
// 分頁處理函數
|
||
const handlePageChange = (page: number) => {
|
||
setCurrentPage(page)
|
||
}
|
||
|
||
const handlePreviousPage = () => {
|
||
if (currentPage > 1) {
|
||
setCurrentPage(currentPage - 1)
|
||
}
|
||
}
|
||
|
||
const handleNextPage = () => {
|
||
if (currentPage < totalPages) {
|
||
setCurrentPage(currentPage + 1)
|
||
}
|
||
}
|
||
|
||
// 邏輯題目分頁處理函數
|
||
const handleLogicPageChange = (page: number) => {
|
||
setCurrentLogicPage(page)
|
||
}
|
||
|
||
const handleLogicPreviousPage = () => {
|
||
if (currentLogicPage > 1) {
|
||
setCurrentLogicPage(currentLogicPage - 1)
|
||
}
|
||
}
|
||
|
||
const handleLogicNextPage = () => {
|
||
if (currentLogicPage < totalLogicPages) {
|
||
setCurrentLogicPage(currentLogicPage + 1)
|
||
}
|
||
}
|
||
|
||
// 從資料庫獲取題目
|
||
useEffect(() => {
|
||
const fetchQuestions = async () => {
|
||
try {
|
||
setIsLoading(true)
|
||
setError(null)
|
||
|
||
// 並行獲取兩種題目
|
||
const [logicResponse, creativeResponse] = await Promise.all([
|
||
fetch('/api/questions/logic'),
|
||
fetch('/api/questions/creative')
|
||
])
|
||
|
||
if (!logicResponse.ok || !creativeResponse.ok) {
|
||
throw new Error('獲取題目失敗')
|
||
}
|
||
|
||
const logicData = await logicResponse.json()
|
||
const creativeData = await creativeResponse.json()
|
||
|
||
if (logicData.success) {
|
||
setLogicQuestions(logicData.data)
|
||
}
|
||
|
||
if (creativeData.success) {
|
||
setCreativeQuestions(creativeData.data)
|
||
}
|
||
} catch (err) {
|
||
console.error('獲取題目失敗:', err)
|
||
setError(err instanceof Error ? err.message : '獲取題目失敗')
|
||
} finally {
|
||
setIsLoading(false)
|
||
}
|
||
}
|
||
|
||
fetchQuestions()
|
||
}, [])
|
||
|
||
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 formData = new FormData()
|
||
formData.append("file", selectedFile)
|
||
formData.append("type", importType)
|
||
|
||
const response = await fetch("/api/questions/import", {
|
||
method: "POST",
|
||
body: formData,
|
||
})
|
||
|
||
const result = await response.json()
|
||
setImportResult(result)
|
||
|
||
if (result.success) {
|
||
setSelectedFile(null)
|
||
// 重置檔案輸入
|
||
const fileInput = document.getElementById("file-input") as HTMLInputElement
|
||
if (fileInput) fileInput.value = ""
|
||
|
||
// 重新載入題目資料
|
||
const fetchQuestions = async () => {
|
||
try {
|
||
const [logicResponse, creativeResponse] = await Promise.all([
|
||
fetch('/api/questions/logic'),
|
||
fetch('/api/questions/creative')
|
||
])
|
||
|
||
if (logicResponse.ok) {
|
||
const logicData = await logicResponse.json()
|
||
if (logicData.success) {
|
||
setLogicQuestions(logicData.data)
|
||
}
|
||
}
|
||
|
||
if (creativeResponse.ok) {
|
||
const creativeData = await creativeResponse.json()
|
||
if (creativeData.success) {
|
||
setCreativeQuestions(creativeData.data)
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('重新載入題目失敗:', err)
|
||
}
|
||
}
|
||
|
||
fetchQuestions()
|
||
}
|
||
} catch (error) {
|
||
setImportResult({
|
||
success: false,
|
||
message: "匯入失敗,請檢查檔案格式是否正確",
|
||
errors: [error instanceof Error ? error.message : "未知錯誤"],
|
||
})
|
||
} finally {
|
||
setIsImporting(false)
|
||
}
|
||
}
|
||
|
||
const downloadTemplate = async (type: "logic" | "creative") => {
|
||
try {
|
||
const response = await fetch(`/api/questions/export?type=${type}`)
|
||
|
||
if (!response.ok) {
|
||
throw new Error('下載失敗')
|
||
}
|
||
|
||
const result = await response.json()
|
||
|
||
if (!result.success) {
|
||
throw new Error(result.message || '下載失敗')
|
||
}
|
||
|
||
// 解碼 Base64 資料,保留 UTF-8 BOM
|
||
const binaryString = atob(result.data)
|
||
const bytes = new Uint8Array(binaryString.length)
|
||
for (let i = 0; i < binaryString.length; i++) {
|
||
bytes[i] = binaryString.charCodeAt(i)
|
||
}
|
||
|
||
// 創建 Blob,保留原始字節資料
|
||
const blob = new Blob([bytes], {
|
||
type: 'text/csv;charset=utf-8'
|
||
})
|
||
|
||
const url = window.URL.createObjectURL(blob)
|
||
const link = document.createElement('a')
|
||
link.href = url
|
||
link.download = result.filename
|
||
document.body.appendChild(link)
|
||
link.click()
|
||
document.body.removeChild(link)
|
||
window.URL.revokeObjectURL(url)
|
||
} catch (error) {
|
||
console.error("下載範本失敗:", error)
|
||
setImportResult({
|
||
success: false,
|
||
message: "下載範本失敗,請稍後再試",
|
||
})
|
||
}
|
||
}
|
||
|
||
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" />
|
||
<span className="hidden sm:inline">返回儀表板</span>
|
||
</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>
|
||
|
||
{isLoading ? (
|
||
<div className="flex items-center justify-center py-8">
|
||
<Loader2 className="w-6 h-6 animate-spin mr-2" />
|
||
<span>載入題目中...</span>
|
||
</div>
|
||
) : error ? (
|
||
<Alert variant="destructive">
|
||
<AlertCircle className="h-4 w-4" />
|
||
<AlertDescription>{error}</AlertDescription>
|
||
</Alert>
|
||
) : (
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>題目ID</TableHead>
|
||
<TableHead>題目內容</TableHead>
|
||
<TableHead>選項數量</TableHead>
|
||
<TableHead>正確答案</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{currentLogicQuestions.map((question) => (
|
||
<TableRow key={question.id}>
|
||
<TableCell className="font-medium">{question.id}</TableCell>
|
||
<TableCell className="max-w-md truncate">{question.question}</TableCell>
|
||
<TableCell>
|
||
{question.option_e ? 5 : 4}
|
||
</TableCell>
|
||
<TableCell>
|
||
<Badge className="bg-green-500 text-white">{question.correct_answer}</Badge>
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
)}
|
||
|
||
{/* 邏輯題目分頁控制 */}
|
||
{!isLoading && !error && logicQuestions.length > itemsPerPage && (
|
||
<div className="flex flex-col sm:flex-row items-center justify-between mt-6 gap-4">
|
||
<div className="text-sm text-muted-foreground text-center sm:text-left">
|
||
顯示第 {logicStartIndex + 1} - {Math.min(logicEndIndex, logicQuestions.length)} 筆,共 {logicQuestions.length} 筆
|
||
</div>
|
||
|
||
{/* Desktop Pagination */}
|
||
<div className="hidden sm:flex items-center space-x-2">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={handleLogicPreviousPage}
|
||
disabled={currentLogicPage === 1}
|
||
>
|
||
<ChevronLeft className="h-4 w-4" />
|
||
上一頁
|
||
</Button>
|
||
|
||
<div className="flex items-center space-x-1">
|
||
{Array.from({ length: totalLogicPages }, (_, i) => i + 1).map((page) => (
|
||
<Button
|
||
key={page}
|
||
variant={currentLogicPage === page ? "default" : "outline"}
|
||
size="sm"
|
||
onClick={() => handleLogicPageChange(page)}
|
||
className="w-8 h-8 p-0"
|
||
>
|
||
{page}
|
||
</Button>
|
||
))}
|
||
</div>
|
||
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={handleLogicNextPage}
|
||
disabled={currentLogicPage === totalLogicPages}
|
||
>
|
||
下一頁
|
||
<ChevronRight className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Mobile Pagination */}
|
||
<div className="flex sm:hidden items-center space-x-2 w-full justify-center">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={handleLogicPreviousPage}
|
||
disabled={currentLogicPage === 1}
|
||
className="flex-1 max-w-[80px]"
|
||
>
|
||
<ChevronLeft className="h-4 w-4 mr-1" />
|
||
上一頁
|
||
</Button>
|
||
|
||
<div className="flex items-center space-x-1 px-2">
|
||
{(() => {
|
||
const maxVisiblePages = 3
|
||
const startPage = Math.max(1, currentLogicPage - 1)
|
||
const endPage = Math.min(totalLogicPages, startPage + maxVisiblePages - 1)
|
||
const pages = []
|
||
|
||
// 如果不在第一頁,顯示第一頁和省略號
|
||
if (startPage > 1) {
|
||
pages.push(
|
||
<Button
|
||
key={1}
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handleLogicPageChange(1)}
|
||
className="w-8 h-8 p-0"
|
||
>
|
||
1
|
||
</Button>
|
||
)
|
||
if (startPage > 2) {
|
||
pages.push(
|
||
<span key="ellipsis1" className="text-muted-foreground px-1">
|
||
...
|
||
</span>
|
||
)
|
||
}
|
||
}
|
||
|
||
// 顯示當前頁附近的頁碼
|
||
for (let i = startPage; i <= endPage; i++) {
|
||
pages.push(
|
||
<Button
|
||
key={i}
|
||
variant={currentLogicPage === i ? "default" : "outline"}
|
||
size="sm"
|
||
onClick={() => handleLogicPageChange(i)}
|
||
className="w-8 h-8 p-0"
|
||
>
|
||
{i}
|
||
</Button>
|
||
)
|
||
}
|
||
|
||
// 如果不在最後一頁,顯示省略號和最後一頁
|
||
if (endPage < totalLogicPages) {
|
||
if (endPage < totalLogicPages - 1) {
|
||
pages.push(
|
||
<span key="ellipsis2" className="text-muted-foreground px-1">
|
||
...
|
||
</span>
|
||
)
|
||
}
|
||
pages.push(
|
||
<Button
|
||
key={totalLogicPages}
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handleLogicPageChange(totalLogicPages)}
|
||
className="w-8 h-8 p-0"
|
||
>
|
||
{totalLogicPages}
|
||
</Button>
|
||
)
|
||
}
|
||
|
||
return pages
|
||
})()}
|
||
</div>
|
||
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={handleLogicNextPage}
|
||
disabled={currentLogicPage === totalLogicPages}
|
||
className="flex-1 max-w-[80px]"
|
||
>
|
||
下一頁
|
||
<ChevronRight className="h-4 w-4 ml-1" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</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>
|
||
|
||
{isLoading ? (
|
||
<div className="flex items-center justify-center py-8">
|
||
<Loader2 className="w-6 h-6 animate-spin mr-2" />
|
||
<span>載入題目中...</span>
|
||
</div>
|
||
) : error ? (
|
||
<Alert variant="destructive">
|
||
<AlertCircle className="h-4 w-4" />
|
||
<AlertDescription>{error}</AlertDescription>
|
||
</Alert>
|
||
) : (
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>題目ID</TableHead>
|
||
<TableHead>陳述內容</TableHead>
|
||
<TableHead>類別</TableHead>
|
||
<TableHead>反向計分</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{currentCreativeQuestions.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.is_reverse ? (
|
||
<Badge className="bg-orange-500 text-white">是</Badge>
|
||
) : (
|
||
<Badge variant="outline">否</Badge>
|
||
)}
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
)}
|
||
|
||
{/* 創意題目分頁控制 */}
|
||
{!isLoading && !error && creativeQuestions.length > itemsPerPage && (
|
||
<div className="flex flex-col sm:flex-row items-center justify-between mt-6 gap-4">
|
||
<div className="text-sm text-muted-foreground text-center sm:text-left">
|
||
顯示第 {startIndex + 1} - {Math.min(endIndex, creativeQuestions.length)} 筆,共 {creativeQuestions.length} 筆
|
||
</div>
|
||
|
||
{/* Desktop Pagination */}
|
||
<div className="hidden sm:flex items-center space-x-2">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={handlePreviousPage}
|
||
disabled={currentPage === 1}
|
||
>
|
||
<ChevronLeft className="h-4 w-4" />
|
||
上一頁
|
||
</Button>
|
||
|
||
<div className="flex items-center space-x-1">
|
||
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
|
||
<Button
|
||
key={page}
|
||
variant={currentPage === page ? "default" : "outline"}
|
||
size="sm"
|
||
onClick={() => handlePageChange(page)}
|
||
className="w-8 h-8 p-0"
|
||
>
|
||
{page}
|
||
</Button>
|
||
))}
|
||
</div>
|
||
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={handleNextPage}
|
||
disabled={currentPage === totalPages}
|
||
>
|
||
下一頁
|
||
<ChevronRight className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Mobile Pagination */}
|
||
<div className="flex sm:hidden items-center space-x-2 w-full justify-center">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={handlePreviousPage}
|
||
disabled={currentPage === 1}
|
||
className="flex-1 max-w-[80px]"
|
||
>
|
||
<ChevronLeft className="h-4 w-4 mr-1" />
|
||
上一頁
|
||
</Button>
|
||
|
||
<div className="flex items-center space-x-1 px-2">
|
||
{(() => {
|
||
const maxVisiblePages = 3
|
||
const startPage = Math.max(1, currentPage - 1)
|
||
const endPage = Math.min(totalPages, startPage + maxVisiblePages - 1)
|
||
const pages = []
|
||
|
||
// 如果不在第一頁,顯示第一頁和省略號
|
||
if (startPage > 1) {
|
||
pages.push(
|
||
<Button
|
||
key={1}
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handlePageChange(1)}
|
||
className="w-8 h-8 p-0"
|
||
>
|
||
1
|
||
</Button>
|
||
)
|
||
if (startPage > 2) {
|
||
pages.push(
|
||
<span key="ellipsis1" className="text-muted-foreground px-1">
|
||
...
|
||
</span>
|
||
)
|
||
}
|
||
}
|
||
|
||
// 顯示當前頁附近的頁碼
|
||
for (let i = startPage; i <= endPage; i++) {
|
||
pages.push(
|
||
<Button
|
||
key={i}
|
||
variant={currentPage === i ? "default" : "outline"}
|
||
size="sm"
|
||
onClick={() => handlePageChange(i)}
|
||
className="w-8 h-8 p-0"
|
||
>
|
||
{i}
|
||
</Button>
|
||
)
|
||
}
|
||
|
||
// 如果不在最後一頁,顯示省略號和最後一頁
|
||
if (endPage < totalPages) {
|
||
if (endPage < totalPages - 1) {
|
||
pages.push(
|
||
<span key="ellipsis2" className="text-muted-foreground px-1">
|
||
...
|
||
</span>
|
||
)
|
||
}
|
||
pages.push(
|
||
<Button
|
||
key={totalPages}
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handlePageChange(totalPages)}
|
||
className="w-8 h-8 p-0"
|
||
>
|
||
{totalPages}
|
||
</Button>
|
||
)
|
||
}
|
||
|
||
return pages
|
||
})()}
|
||
</div>
|
||
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={handleNextPage}
|
||
disabled={currentPage === totalPages}
|
||
className="flex-1 max-w-[80px]"
|
||
>
|
||
下一頁
|
||
<ChevronRight className="h-4 w-4 ml-1" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</TabsContent>
|
||
</Tabs>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|