feat: 适配切片,多语言

This commit is contained in:
Lan
2025-12-01 15:29:12 +08:00
parent be9ce3bb69
commit eb370ef8c6
4 changed files with 111 additions and 56 deletions
+3 -3
View File
@@ -92,7 +92,7 @@ export default {
localStorage: 'Local Storage',
s3Storage: 'S3 Storage',
webdavStorage: 'Webdav Storage',
chunkUploadNote: 'Enable chunk upload (experimental feature, still in development, currently only available for local storage, may have unknown issues)',
chunkUploadNote: 'Enable chunk upload (experimental feature, may have unknown issues)',
s3AccessKeyId: 'S3 AccessKeyId',
s3SecretAccessKey: 'S3 SecretAccessKey',
s3BucketName: 'S3 BucketName',
@@ -334,7 +334,7 @@ export default {
localStorage: 'Local Storage',
s3Storage: 'S3 Storage',
webdavStorage: 'Webdav Storage',
chunkUploadNote: 'Enable chunk upload (experimental feature, still in development, currently only available for local storage, may have unknown issues)',
chunkUploadNote: 'Enable chunk upload (experimental feature, may have unknown issues)',
enabled: 'Enabled',
disabled: 'Disabled',
webdavUrl: 'Please enter Webdav URL',
@@ -401,7 +401,7 @@ export default {
localStorage: 'Local Storage',
s3Storage: 'S3 Storage',
webdavStorage: 'Webdav Storage',
chunkUploadNote: 'Enable chunk upload (experimental feature, still in development, currently only available for local storage, may have unknown issues)',
chunkUploadNote: 'Enable chunk upload (experimental feature, may have unknown issues)',
enabled: 'Enabled',
disabled: 'Disabled',
webdavUrl: 'Please enter Webdav URL',
+18 -3
View File
@@ -91,7 +91,7 @@ export default {
localStorage: '本地存储',
s3Storage: 'S3 存储',
webdavStorage: 'Webdav 存储',
chunkUploadNote: '开启切片上传(实验性功能,还在开发中,目前仅本地存储可用,可能会出现未知问题)',
chunkUploadNote: '开启切片上传(实验性功能,可能会出现未知问题)',
s3AccessKeyId: 'S3 AccessKeyId',
s3SecretAccessKey: 'S3 SecretAccessKey',
s3BucketName: 'S3 BucketName',
@@ -369,7 +369,9 @@ export default {
storagePathPlaceholder: '留空则使用默认路径,可不填写',
storageMethod: '存储方式',
localStorage: '本地存储',
chunkUploadNote: '开启切片上传(实验性功能,还在开发中,目前仅本地存储可用,可能会出现未知问题)',
s3Storage: 'S3 存储',
webdavStorage: 'Webdav 存储',
chunkUploadNote: '开启切片上传(实验性功能,可能会出现未知问题)',
uploadLimits: '上传限制',
uploadPerMinute: '每分钟上传限制',
uploadCountLimit: '上传数量限制',
@@ -382,7 +384,20 @@ export default {
count: '按次数'
},
maxSaveTime: '最长保存时间',
s3AccessKeyId: 'S3 AccessKeyId',
s3SecretAccessKey: 'S3 SecretAccessKey',
s3BucketName: 'S3 BucketName',
s3EndpointUrl: 'S3 EndpointUrl',
s3RegionName: 'S3 Region Name',
s3SignatureVersion: 'S3 Signature Version',
s3Hostname: 'S3 Hostname',
s3v2: 'S3v2',
s3v4: 'S3v4',
autoPlaceholder: 'auto',
enableProxy: '启用代理',
webdavUrlPlaceholder: '请输入 Webdav URL',
webdavUsernamePlaceholder: '请输入 Webdav Username',
webdavPasswordPlaceholder: '请输入 Webdav Password',
fileSizeUnits: {
kb: 'KB',
mb: 'MB',
@@ -457,7 +472,7 @@ export default {
localStorage: '本地存储',
s3Storage: 'S3 存储',
webdavStorage: 'Webdav 存储',
chunkUploadNote: '开启切片上传(实验性功能,还在开发中,目前仅本地存储可用,可能会出现未知问题)',
chunkUploadNote: '开启切片上传(实验性功能,可能会出现未知问题)',
enabled: '已开启',
disabled: '已关闭',
webdavUrl: '请输入 Webdav URL',
+58 -41
View File
@@ -556,8 +556,11 @@ const fileHash = ref('')
const handleFileSelected = (file: File) => {
const handleFileSelected = async (file: File) => {
selectedFile.value = file
if (!checkOpenUpload()) return
if (!checkFileSize(file)) return
fileHash.value = await calculateFileHash(file)
}
@@ -648,49 +651,46 @@ const handlePaste = async (event: ClipboardEvent) => {
}
const calculateFileHash = async (file: File): Promise<string> => {
return new Promise((resolve) => {
const chunkSize = 2097152 // 保持 2MB 的切片大小用于计算哈希
const fileReader = new FileReader()
let currentChunk = 0
const chunks = Math.ceil(file.size / chunkSize)
fileReader.onload = async (e) => {
const chunk = new Uint8Array(e.target!.result as ArrayBuffer)
try {
// 尝试使用 crypto.subtle.digest
// 对于小文件,直接计算整个文件的哈希
if (file.size <= 10 * 1024 * 1024) {
const buffer = await file.arrayBuffer()
if (window.isSecureContext) {
const hashBuffer = await crypto.subtle.digest('SHA-256', chunk)
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
}
return generateFallbackHash(file)
}
currentChunk++
if (currentChunk < chunks) {
loadNext()
} else {
resolve(hashHex)
}
} else {
// 如果不是安全上下文(HTTP),则返回一个基于文件信息的替代哈希
const fallbackHash = generateFallbackHash(file)
resolve(fallbackHash)
// 对于大文件,取首尾各5MB计算哈希
const chunkSize = 5 * 1024 * 1024
const firstChunk = file.slice(0, chunkSize)
const lastChunk = file.slice(-chunkSize)
const [firstBuffer, lastBuffer] = await Promise.all([
firstChunk.arrayBuffer(),
lastChunk.arrayBuffer()
])
// 合并首尾数据
const combined = new Uint8Array(firstBuffer.byteLength + lastBuffer.byteLength + 16)
combined.set(new Uint8Array(firstBuffer), 0)
combined.set(new Uint8Array(lastBuffer), firstBuffer.byteLength)
// 添加文件大小信息
const sizeBytes = new TextEncoder().encode(file.size.toString())
combined.set(sizeBytes, firstBuffer.byteLength + lastBuffer.byteLength)
if (window.isSecureContext) {
const hashBuffer = await crypto.subtle.digest('SHA-256', combined)
const hashArray = Array.from(new Uint8Array(hashBuffer))
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
}
return generateFallbackHash(file)
} catch (err) {
// 如果 crypto.subtle.digest 失败,使用替代方案
const fallbackHash = generateFallbackHash(file)
console.error('File hash calculation failed:', err)
resolve(fallbackHash)
return generateFallbackHash(file)
}
}
const loadNext = () => {
const start = currentChunk * chunkSize
const end = start + chunkSize >= file.size ? file.size : start + chunkSize
fileReader.readAsArrayBuffer(file.slice(start, end))
}
loadNext()
})
}
// 生成替代哈希的函数
@@ -777,6 +777,11 @@ const getExpirationTime = (method: string, value: string) => {
const handleChunkUpload = async (file: File) => {
try {
// 确保文件哈希已计算
// 每次上传都重新计算哈希,确保哈希值正确
const hash = await calculateFileHash(file)
fileHash.value = hash
console.log('Calculated file hash:', hash)
// 默认切片大小为5MB
const chunkSize = 5 * 1024 * 1024
const chunks = Math.ceil(file.size / chunkSize)
@@ -786,6 +791,7 @@ const handleChunkUpload = async (file: File) => {
name?: string
upload_id?: string
existed?: boolean
uploaded_chunks?: number[]
}> = await api.post('chunk/upload/init/', {
file_name: file.name,
file_size: file.size,
@@ -800,17 +806,22 @@ const handleChunkUpload = async (file: File) => {
return initResponse
}
const uploadId = initResponse.detail?.upload_id
const uploadedChunks = new Set(initResponse.detail?.uploaded_chunks || [])
// 2. 上传切片
// 2. 上传切片(跳过已上传的)
for (let i = 0; i < chunks; i++) {
// 跳过已上传的块
if (uploadedChunks.has(i)) {
continue
}
const start = i * chunkSize
const end = Math.min(start + chunkSize, file.size)
const chunk = file.slice(start, end)
const chunkFormData = new FormData()
chunkFormData.append('chunk', new Blob([chunk], { type: file.type })) // 确保以Blob形式添加
chunkFormData.append('chunk', new Blob([chunk], { type: file.type }))
// 使用 application/x-www-form-urlencoded 格式
const chunkResponse: ApiResponse<unknown> = await api.post(
`chunk/upload/chunk/${uploadId}/${i}`,
chunkFormData,
@@ -819,10 +830,16 @@ const handleChunkUpload = async (file: File) => {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (progressEvent: { loaded: number; total?: number }) => {
// 计算已上传块的大小
const uploadedSize = Array.from(uploadedChunks).reduce((acc, idx) => {
const chunkEnd = Math.min((idx + 1) * chunkSize, file.size)
const chunkStart = idx * chunkSize
return acc + (chunkEnd - chunkStart)
}, 0)
const percentCompleted = Math.round(
((i * chunkSize + progressEvent.loaded) * 100) / file.size
((uploadedSize + i * chunkSize + progressEvent.loaded) * 100) / file.size
)
uploadProgress.value = percentCompleted
uploadProgress.value = Math.min(percentCompleted, 99)
}
}
)
+28 -5
View File
@@ -108,9 +108,9 @@ const refreshData = async () => {
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '获取配置失败'
const errorMessage = error instanceof Error ? error.message : t('manage.systemSettings.getConfigFailed')
alertStore.showAlert(errorMessage, 'error')
console.error('获取系统配置失败:', error)
console.error('Failed to get system config:', error)
}
}
// 转换文件大小为字节
@@ -136,12 +136,12 @@ const submitSave = () => {
ConfigService.updateConfig(formData).then((res) => {
if (res.code == 200) {
alertStore.showAlert('保存成功', 'success')
alertStore.showAlert(t('manage.systemSettings.saveSuccess'), 'success')
} else {
alertStore.showAlert(res.message || '保存失败', 'error')
alertStore.showAlert(res.message || t('manage.systemSettings.saveFailed'), 'error')
}
}).catch((error) => {
const errorMessage = error instanceof Error ? error.message : '保存失败'
const errorMessage = error instanceof Error ? error.message : t('manage.systemSettings.saveFailed')
alertStore.showAlert(errorMessage, 'error')
})
}
@@ -504,6 +504,29 @@ refreshData()
</span>
</div>
</div>
<div class="space-y-2">
<label class="block text-sm font-medium mb-2"
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
{{ t('manage.settings.chunkUploadNote') }}
</label>
<div class="flex items-center">
<button type="button" @click="config.enableChunk = config.enableChunk === 1 ? 0 : 1"
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
:class="[config.enableChunk === 1 ? 'bg-indigo-600' : 'bg-gray-200']" role="switch"
:aria-checked="config.enableChunk === 1">
<span
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
:class="[
config.enableChunk === 1 ? 'translate-x-5' : 'translate-x-0',
isDarkMode && config.enableChunk !== 1 ? 'bg-gray-100' : 'bg-white'
]" />
</button>
<span class="ml-3 text-sm" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
{{ config.enableChunk === 1 ? t('common.enabled') : t('common.disabled') }}
</span>
</div>
</div>
</div>
</div>
</div>