2025-06-29 09:21:29 +08:00

230 lines
8.8 KiB
TypeScript
Raw 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 React, { useState, useEffect, useCallback } from 'react'
interface VersionInfo {
version: string
download_url: string
checksum: string
}
export default function UploadPage() {
const [currentVersion, setCurrentVersion] = useState<VersionInfo | null>(null)
const [newVersion, setNewVersion] = useState('')
const [versionFile, setVersionFile] = useState<File | null>(null)
const [versionFileHash, setVersionFileHash] = useState('')
const [isUploading, setIsUploading] = useState(false)
const [uploadError, setUploadError] = useState('')
const [uploadSuccess, setUploadSuccess] = useState(false)
const canUploadVersion = newVersion && versionFile && !isUploading
// 计算文件 SHA-256
const calculateSha256 = async (file: File): Promise<string> => {
const buffer = await file.arrayBuffer()
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer)
const hashArray = Array.from(new Uint8Array(hashBuffer))
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
}
// 获取当前版本信息
const fetchCurrentVersion = useCallback(async () => {
try {
const response = await fetch('/api/version')
if (response.ok) {
const data: VersionInfo = await response.json()
setCurrentVersion(data)
}
} catch (err) {
console.error('获取版本信息失败:', err)
}
}, [])
// 处理版本文件选择
const handleVersionFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const input = event.target
if (input.files && input.files[0]) {
const file = input.files[0]
setVersionFile(file)
setVersionFileHash(await calculateSha256(file))
}
}
// 清除表单
const resetForm = () => {
setNewVersion('')
setVersionFile(null)
setVersionFileHash('')
setUploadError('')
}
// 上传新版本
const uploadVersion = async () => {
if (!versionFile || !newVersion) return
setIsUploading(true)
setUploadError('')
setUploadSuccess(false)
const formData = new FormData()
formData.append('file', versionFile)
formData.append('version', newVersion)
try {
const response = await fetch('/upload/version', {
method: 'POST',
body: formData
})
if (!response.ok) {
throw new Error('Upload failed')
}
await fetchCurrentVersion()
setUploadSuccess(true)
resetForm()
setTimeout(() => {
setUploadSuccess(false)
}, 3000)
} catch (err) {
console.error('上传失败:', err)
setUploadError('上传失败,请重试')
} finally {
setIsUploading(false)
}
}
useEffect(() => {
fetchCurrentVersion()
}, [fetchCurrentVersion])
return (
<div className="max-w-3xl mx-auto p-6">
<h1 className="text-3xl font-bold text-gray-800 dark:text-white mb-8"></h1>
{/* 当前版本信息卡片 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md mb-8 overflow-hidden">
<div className="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b dark:border-gray-600">
<h2 className="text-xl font-semibold text-gray-700 dark:text-gray-200"></h2>
</div>
<div className="p-6">
{currentVersion ? (
<div className="space-y-3">
<div className="grid grid-cols-12 gap-4 items-center">
<span className="text-gray-600 dark:text-gray-400 col-span-2">:</span>
<span className="font-medium text-gray-800 dark:text-gray-200 col-span-10">{currentVersion.version}</span>
</div>
<div className="grid grid-cols-12 gap-4 items-center">
<span className="text-gray-600 dark:text-gray-400 col-span-2">:</span>
<a
href={currentVersion.download_url}
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 break-all col-span-10"
>
{currentVersion.download_url}
</a>
</div>
<div className="grid grid-cols-12 gap-4 items-center">
<span className="text-gray-600 dark:text-gray-400 col-span-2">:</span>
<span className="font-mono text-sm text-gray-700 dark:text-gray-300 break-all col-span-10">
{currentVersion.checksum}
</span>
</div>
</div>
) : (
<div className="text-gray-500 dark:text-gray-400 italic"></div>
)}
</div>
</div>
{/* 上传新版本表单 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md">
<div className="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b dark:border-gray-600">
<h2 className="text-xl font-semibold text-gray-700 dark:text-gray-200"></h2>
</div>
<div className="p-6 space-y-6">
{/* 版本号输入 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<input
value={newVersion}
onChange={(e) => setNewVersion(e.target.value)}
type="text"
className="w-full px-4 py-2 border dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
placeholder="例如: 1.0.0"
/>
</div>
{/* 文件上传 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<div className="mt-1">
<div className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg px-6 py-8">
<div className="text-center">
<input
type="file"
onChange={handleVersionFileChange}
className="hidden"
id="file-upload"
/>
<label
htmlFor="file-upload"
className="cursor-pointer inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
</label>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
{versionFile?.name || '未选择文件'}
</p>
</div>
{versionFileHash && (
<div className="mt-4 text-center">
<p className="text-xs text-gray-500 dark:text-gray-400">SHA-256 :</p>
<p className="font-mono text-xs text-gray-600 dark:text-gray-400 break-all">
{versionFileHash}
</p>
</div>
)}
</div>
</div>
</div>
{/* 错误提示 */}
{uploadError && (
<div className="text-red-600 dark:text-red-400 text-sm">
{uploadError}
</div>
)}
{/* 成功提示 */}
{uploadSuccess && (
<div className="bg-green-50 dark:bg-green-900/50 text-green-800 dark:text-green-200 px-4 py-2 rounded-md text-sm">
</div>
)}
{/* 提交按钮 */}
<div className="flex justify-end">
<button
onClick={uploadVersion}
disabled={!canUploadVersion}
className="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:bg-gray-300 dark:disabled:bg-gray-600 disabled:cursor-not-allowed transition-colors"
>
{isUploading && (
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
)}
{isUploading ? '上传中...' : '上传新版本'}
</button>
</div>
</div>
</div>
</div>
)
}