230 lines
8.2 KiB
TypeScript
230 lines
8.2 KiB
TypeScript
'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/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('/api/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 mb-8">软件版本管理</h1>
|
||
|
||
{/* 当前版本信息卡片 */}
|
||
<div className="bg-white rounded-lg shadow-md mb-8 overflow-hidden">
|
||
<div className="bg-gray-50 px-6 py-4 border-b">
|
||
<h2 className="text-xl font-semibold text-gray-700">当前版本信息</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 col-span-2">版本号:</span>
|
||
<span className="font-medium text-gray-800 col-span-10">{currentVersion.version}</span>
|
||
</div>
|
||
<div className="grid grid-cols-12 gap-4 items-center">
|
||
<span className="text-gray-600 col-span-2">下载地址:</span>
|
||
<a
|
||
href={currentVersion.download_url}
|
||
className="text-blue-600 hover:text-blue-800 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 col-span-2">校验和:</span>
|
||
<span className="font-mono text-sm text-gray-700 break-all col-span-10">
|
||
{currentVersion.checksum}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="text-gray-500 italic">暂无版本信息</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 上传新版本表单 */}
|
||
<div className="bg-white rounded-lg shadow-md">
|
||
<div className="bg-gray-50 px-6 py-4 border-b">
|
||
<h2 className="text-xl font-semibold text-gray-700">上传新版本</h2>
|
||
</div>
|
||
<div className="p-6 space-y-6">
|
||
{/* 版本号输入 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
版本号
|
||
</label>
|
||
<input
|
||
value={newVersion}
|
||
onChange={(e) => setNewVersion(e.target.value)}
|
||
type="text"
|
||
className="w-full px-4 py-2 border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition"
|
||
placeholder="例如: 1.0.0"
|
||
/>
|
||
</div>
|
||
|
||
{/* 文件上传 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
选择文件
|
||
</label>
|
||
<div className="mt-1">
|
||
<div className="border-2 border-dashed border-gray-300 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">
|
||
{versionFile?.name || '未选择文件'}
|
||
</p>
|
||
</div>
|
||
{versionFileHash && (
|
||
<div className="mt-4 text-center">
|
||
<p className="text-xs text-gray-500">SHA-256 校验和:</p>
|
||
<p className="font-mono text-xs text-gray-600 break-all">
|
||
{versionFileHash}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 错误提示 */}
|
||
{uploadError && (
|
||
<div className="text-red-600 text-sm">
|
||
{uploadError}
|
||
</div>
|
||
)}
|
||
|
||
{/* 成功提示 */}
|
||
{uploadSuccess && (
|
||
<div className="bg-green-50 text-green-800 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 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>
|
||
)
|
||
}
|