159 lines
4.9 KiB
TypeScript
159 lines
4.9 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useRef, useEffect } from 'react'
|
||
|
||
interface DecodedLine {
|
||
content: string
|
||
isError: boolean
|
||
}
|
||
|
||
export default function DecodePage() {
|
||
const [decodedLines, setDecodedLines] = useState<DecodedLine[]>([])
|
||
const [errorMessage, setErrorMessage] = useState('')
|
||
const resultRef = useRef<HTMLDivElement>(null)
|
||
|
||
// 转换 Unix 时间戳为本地时间字符串
|
||
const formatTimestamp = (line: string): string => {
|
||
const match = line.match(/^\[(\d+)\](.*)/)
|
||
if (match) {
|
||
const timestamp = parseInt(match[1])
|
||
const date = new Date(timestamp * 1000)
|
||
const formattedTime = date.toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit',
|
||
hour12: false
|
||
})
|
||
return `[${formattedTime}]${match[2]}`
|
||
}
|
||
return line
|
||
}
|
||
|
||
const decodeBase64 = (base64String: string): string => {
|
||
try {
|
||
const binaryString = atob(base64String.trim())
|
||
const bytes = new Uint8Array(binaryString.length)
|
||
for (let i = 0; i < binaryString.length; i++) {
|
||
bytes[i] = binaryString.charCodeAt(i)
|
||
}
|
||
|
||
const decoder = new TextDecoder('utf-8')
|
||
return decoder.decode(bytes)
|
||
} catch (e) {
|
||
throw new Error(`解码失败: ${(e as Error).message}`)
|
||
}
|
||
}
|
||
|
||
const scrollToBottom = () => {
|
||
if (resultRef.current) {
|
||
resultRef.current.scrollTop = resultRef.current.scrollHeight
|
||
}
|
||
}
|
||
|
||
// 当解码行数更新时滚动到底部
|
||
useEffect(() => {
|
||
if (decodedLines.length > 0) {
|
||
setTimeout(scrollToBottom, 100)
|
||
}
|
||
}, [decodedLines])
|
||
|
||
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||
const file = event.target.files?.[0]
|
||
|
||
if (!file) {
|
||
setErrorMessage('请选择文件')
|
||
return
|
||
}
|
||
|
||
try {
|
||
const text = await file.text()
|
||
const lines = text.split('\n')
|
||
|
||
const processedLines: DecodedLine[] = lines
|
||
.filter(line => line.trim())
|
||
.map((line, index) => {
|
||
try {
|
||
const decodedLine = decodeBase64(line)
|
||
const formattedLine = formatTimestamp(decodedLine)
|
||
return {
|
||
content: formattedLine,
|
||
isError: false
|
||
}
|
||
} catch (e) {
|
||
console.error(`第 ${index + 1} 行解码失败:`, e)
|
||
return {
|
||
content: `第 ${index + 1} 行解码失败: ${line}`,
|
||
isError: true
|
||
}
|
||
}
|
||
})
|
||
|
||
setDecodedLines(processedLines)
|
||
setErrorMessage('')
|
||
} catch (e) {
|
||
setErrorMessage('文件读取失败,请重试')
|
||
console.error('文件读取错误:', e)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="max-w-4xl mx-auto p-6">
|
||
<h1 className="text-2xl font-bold text-gray-800 dark:text-white mb-6">Base64 日志解码器</h1>
|
||
|
||
{/* 文件上传区域 */}
|
||
<div className="mb-8">
|
||
<div className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-6 text-center">
|
||
<label className="block mb-4 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||
选择日志文件:
|
||
</label>
|
||
<input
|
||
type="file"
|
||
accept=".txt,.log"
|
||
onChange={handleFileChange}
|
||
className="block w-full text-sm text-gray-500 dark:text-gray-400 file:mr-4 file:py-2 file:px-4
|
||
file:rounded-md file:border-0 file:text-sm file:font-semibold
|
||
file:bg-blue-50 dark:file:bg-blue-900/50 file:text-blue-700 dark:file:text-blue-300 hover:file:bg-blue-100 dark:hover:file:bg-blue-800/50
|
||
cursor-pointer"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 错误提示 */}
|
||
{errorMessage && (
|
||
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/50 text-red-600 dark:text-red-300 rounded-md">
|
||
{errorMessage}
|
||
</div>
|
||
)}
|
||
|
||
{/* 解码结果 */}
|
||
{decodedLines.length > 0 && (
|
||
<div className="space-y-2">
|
||
<h2 className="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-4">解码结果:</h2>
|
||
<div
|
||
ref={resultRef}
|
||
className="bg-white dark:bg-gray-800 shadow rounded-lg overflow-auto max-h-[600px]"
|
||
>
|
||
<div className="divide-y divide-gray-100 dark:divide-gray-700">
|
||
{decodedLines.map((line, index) => (
|
||
<div
|
||
key={index}
|
||
className={`py-1.5 px-4 hover:bg-gray-50 dark:hover:bg-gray-700 leading-tight ${
|
||
line.isError ? 'bg-red-50 dark:bg-red-900/30' : ''
|
||
}`}
|
||
>
|
||
<pre className="whitespace-pre-wrap font-mono text-sm text-gray-800 dark:text-gray-200">
|
||
{line.content}
|
||
</pre>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|