complete frontend - refact from vue
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
'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 mb-6">Base64 日志解码器</h1>
|
||||
|
||||
{/* 文件上传区域 */}
|
||||
<div className="mb-8">
|
||||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center">
|
||||
<label className="block mb-4 text-sm font-medium text-gray-700">
|
||||
选择日志文件:
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
accept=".txt,.log"
|
||||
onChange={handleFileChange}
|
||||
className="block w-full text-sm text-gray-500 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 file:text-blue-700 hover:file:bg-blue-100
|
||||
cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{errorMessage && (
|
||||
<div className="mb-6 p-4 bg-red-50 text-red-600 rounded-md">
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 解码结果 */}
|
||||
{decodedLines.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-lg font-semibold text-gray-700 mb-4">解码结果:</h2>
|
||||
<div
|
||||
ref={resultRef}
|
||||
className="bg-white shadow rounded-lg overflow-auto max-h-[600px]"
|
||||
>
|
||||
<div className="divide-y divide-gray-100">
|
||||
{decodedLines.map((line, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`py-1.5 px-4 hover:bg-gray-50 leading-tight ${
|
||||
line.isError ? 'bg-red-50' : ''
|
||||
}`}
|
||||
>
|
||||
<pre className="whitespace-pre-wrap font-mono text-sm text-gray-800">
|
||||
{line.content}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user