From 4d8b394eed61cc1a3500ab615de3b3fb7c3fb24e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B3=E4=BD=A9=E5=BA=AD?= Date: Tue, 7 Oct 2025 13:04:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=8C=AF=E5=87=BA=20excel=20?= =?UTF-8?q?=E7=95=B0=E5=B8=B8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/page.tsx | 53 ++++++++++++++++++++++++++++++- app/api/admin/export-csv/route.ts | 48 ++++++++++++++++++++++------ 2 files changed, 90 insertions(+), 11 deletions(-) diff --git a/app/admin/page.tsx b/app/admin/page.tsx index ebdda2a..e708a64 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -103,6 +103,7 @@ export default function AdminPage() { const [statusFilter, setStatusFilter] = useState("all") const [visibilityFilter, setVisibilityFilter] = useState("all") const [isExporting, setIsExporting] = useState(false) + const [isExportingExcel, setIsExportingExcel] = useState(false) const [showCategoryGuide, setShowCategoryGuide] = useState(false) const [showPrivacyDetails, setShowPrivacyDetails] = useState(false) @@ -427,6 +428,43 @@ export default function AdminPage() { } } + // 匯出 Excel + const exportToExcel = async () => { + try { + setIsExportingExcel(true) + + const response = await fetch('/api/admin/export-excel', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: filteredWishes, + filename: `困擾案例數據_${new Date().toISOString().split('T')[0]}.xlsx` + }) + }) + + if (response.ok) { + const blob = await response.blob() + const url = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `困擾案例數據_${new Date().toISOString().split('T')[0]}.xlsx` + document.body.appendChild(a) + a.click() + window.URL.revokeObjectURL(url) + document.body.removeChild(a) + } else { + throw new Error('匯出失敗') + } + } catch (error) { + console.error('匯出 Excel 失敗:', error) + alert('匯出失敗,請稍後再試') + } finally { + setIsExportingExcel(false) + } + } + useEffect(() => { fetchData() }, []) @@ -575,7 +613,7 @@ export default function AdminPage() { -
+
+ +
diff --git a/app/api/admin/export-csv/route.ts b/app/api/admin/export-csv/route.ts index c80cedd..4a32fac 100644 --- a/app/api/admin/export-csv/route.ts +++ b/app/api/admin/export-csv/route.ts @@ -29,27 +29,55 @@ export async function POST(request: NextRequest) { const csvRows = [headers.join(',')] data.forEach((wish: any) => { + // 清理和轉義 CSV 數據 + const cleanText = (text: string) => { + if (!text) return '' + return text + .replace(/"/g, '""') // 轉義雙引號 + .replace(/\r?\n/g, ' ') // 將換行符替換為空格 + .replace(/\t/g, ' ') // 將製表符替換為空格 + .trim() + } + const row = [ wish.id, - `"${wish.title.replace(/"/g, '""')}"`, - `"${wish.current_pain.replace(/"/g, '""')}"`, - `"${wish.expected_solution.replace(/"/g, '""')}"`, - `"${(wish.expected_effect || '').replace(/"/g, '""')}"`, + `"${cleanText(wish.title)}"`, + `"${cleanText(wish.current_pain)}"`, + `"${cleanText(wish.expected_solution)}"`, + `"${cleanText(wish.expected_effect || '')}"`, wish.is_public ? '是' : '否', - `"${(wish.email || '').replace(/"/g, '""')}"`, + `"${cleanText(wish.email || '')}"`, wish.status === 'active' ? '活躍' : '非活躍', - `"${(wish.category || '未分類').replace(/"/g, '""')}"`, + `"${cleanText(wish.category || '未分類')}"`, wish.priority, wish.like_count, - `"${new Date(wish.created_at).toLocaleString('zh-TW')}"`, - `"${new Date(wish.updated_at).toLocaleString('zh-TW')}"`, - `"${wish.user_session.replace(/"/g, '""')}"` + `"${new Date(wish.created_at).toLocaleString('zh-TW', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + })}"`, + `"${new Date(wish.updated_at).toLocaleString('zh-TW', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + })}"`, + `"${cleanText(wish.user_session)}"` ] csvRows.push(row.join(',')) }) const csvContent = csvRows.join('\n') - const csvBuffer = Buffer.from(csvContent, 'utf-8') + // 添加 UTF-8 BOM 以確保 Excel 正確識別中文編碼 + const csvBuffer = Buffer.concat([ + Buffer.from('\uFEFF', 'utf8'), // UTF-8 BOM + Buffer.from(csvContent, 'utf-8') + ]) console.log(`✅ 成功生成 CSV 文件: ${data.length} 筆數據`)