Lan
2026-01-07 18:54:41 +08:00
parent 1f76be5697
commit 394437294e
4 changed files with 143 additions and 109 deletions
+39 -38
View File
@@ -189,6 +189,7 @@ export default {
description: 'Supports various common formats' description: 'Supports various common formats'
}, },
submit: 'Secure Send', submit: 'Secure Send',
submitting: 'Sending...',
needRetrieveFile: 'Need to retrieve? Click here', needRetrieveFile: 'Need to retrieve? Click here',
sendRecords: 'Send Records', sendRecords: 'Send Records',
secureEncryption: 'Secure Encryption', secureEncryption: 'Secure Encryption',
@@ -217,7 +218,7 @@ export default {
forever: 'Forever' forever: 'Forever'
} }
}, },
time: { time: {
day: 'By Day', day: 'By Day',
hour: 'By Hour', hour: 'By Hour',
minute: 'By Minute', minute: 'By Minute',
@@ -311,7 +312,7 @@ export default {
} }
}, },
// Side drawer // Side drawer
drawer: { drawer: {
noRecords: 'No records' noRecords: 'No records'
}, },
@@ -466,42 +467,42 @@ export default {
version: 'Version v2.2.1 Updated: 2025-09-04' version: 'Version v2.2.1 Updated: 2025-09-04'
}, },
fileManage: { fileManage: {
title: 'File Management', title: 'File Management',
searchPlaceholder: 'Search file name, description...', searchPlaceholder: 'Search file name, description...',
allFiles: 'All Files', allFiles: 'All Files',
editFileInfo: 'Edit File Info', editFileInfo: 'Edit File Info',
saveChanges: 'Save Changes', saveChanges: 'Save Changes',
headers: { headers: {
code: 'Code', code: 'Code',
name: 'Name', name: 'Name',
size: 'Size', size: 'Size',
description: 'Description', description: 'Description',
expiration: 'Expiration', expiration: 'Expiration',
actions: 'Actions' actions: 'Actions'
}, },
form: { form: {
code: 'Code', code: 'Code',
codePlaceholder: 'Enter code', codePlaceholder: 'Enter code',
filename: 'File Name', filename: 'File Name',
filenamePlaceholder: 'Enter file name', filenamePlaceholder: 'Enter file name',
suffix: 'File Suffix', suffix: 'File Suffix',
suffixPlaceholder: 'Enter file suffix', suffixPlaceholder: 'Enter file suffix',
downloadLimit: 'Download Limit', downloadLimit: 'Download Limit',
downloadLimitPlaceholder: 'Enter download limit' downloadLimitPlaceholder: 'Enter download limit'
}, },
updateFailed: 'Update failed', updateFailed: 'Update failed',
deleteFailed: 'Delete failed', deleteFailed: 'Delete failed',
loadFileListFailed: 'Failed to load file list' loadFileListFailed: 'Failed to load file list'
}, },
login: { login: {
title: 'Login', title: 'Login',
password: 'Password', password: 'Password',
passwordPlaceholder: 'Password', passwordPlaceholder: 'Password',
loginButton: 'Login', loginButton: 'Login',
loggingIn: 'Logging in...', loggingIn: 'Logging in...',
invalidPassword: 'Invalid password', invalidPassword: 'Invalid password',
passwordTooShort: 'Password must be at least 6 characters', passwordTooShort: 'Password must be at least 6 characters',
loginFailed: 'Login failed', loginFailed: 'Login failed',
noValidToken: 'Login failed: No valid token received' noValidToken: 'Login failed: No valid token received'
} }
}, },
+26 -25
View File
@@ -190,6 +190,7 @@ export default {
description: '支持各种常见格式' description: '支持各种常见格式'
}, },
submit: '安全寄送', submit: '安全寄送',
submitting: '发送中...',
needRetrieveFile: '需要取件?点击这里', needRetrieveFile: '需要取件?点击这里',
sendRecords: '发件记录', sendRecords: '发件记录',
secureEncryption: '安全加密', secureEncryption: '安全加密',
@@ -311,35 +312,35 @@ export default {
}, },
// 文件记录 // 文件记录
fileRecord: { fileRecord: {
filename: '文件名', filename: '文件名',
size: '文件大小', size: '文件大小',
date: '取件日期', date: '取件日期',
code: '取件码', code: '取件码',
actions: '操作', actions: '操作',
download: '下载', download: '下载',
viewDetails: '查看详情', viewDetails: '查看详情',
deleteRecord: '删除记录', deleteRecord: '删除记录',
preview: '预览', preview: '预览',
copyContent: '复制内容', copyContent: '复制内容',
contentCopied: '内容已复制到剪贴板', contentCopied: '内容已复制到剪贴板',
copyFailed: '复制失败,请重试' copyFailed: '复制失败,请重试'
}, },
// 文件详情模态框 // 文件详情模态框
fileDetail: { fileDetail: {
title: '文件详情', title: '文件详情',
content: '文件内容', content: '文件内容',
previewContent: '预览内容', previewContent: '预览内容',
download: '点击下载', download: '点击下载',
qrCode: '取件二维码', qrCode: '取件二维码',
scanQrCode: '扫描二维码快速取件' scanQrCode: '扫描二维码快速取件'
}, },
// 内容预览 // 内容预览
contentPreview: { contentPreview: {
title: '内容预览' title: '内容预览'
}, },
// 侧边抽屉 // 侧边抽屉
drawer: { drawer: {
+75 -42
View File
@@ -30,10 +30,7 @@
/> />
</div> </div>
<div v-else key="text" class="grid grid-cols-1 gap-8"> <div v-else key="text" class="grid grid-cols-1 gap-8">
<TextInputArea <TextInputArea v-model="textContent" :placeholder="t('send.uploadArea.textInput')" />
v-model="textContent"
:placeholder="t('send.uploadArea.textInput')"
/>
</div> </div>
</transition> </transition>
<!-- 过期方式选择 --> <!-- 过期方式选择 -->
@@ -115,7 +112,9 @@
:class="[ :class="[
'absolute right-0 top-0 h-full appearance-none cursor-pointer transition-all duration-300', 'absolute right-0 top-0 h-full appearance-none cursor-pointer transition-all duration-300',
'focus:outline-none focus:ring-2 focus:ring-offset-0', 'focus:outline-none focus:ring-2 focus:ring-offset-0',
expirationMethod === 'forever' ? 'w-full px-5 rounded-2xl' : 'w-28 pl-4 pr-9 border-l rounded-r-2xl', expirationMethod === 'forever'
? 'w-full px-5 rounded-2xl'
: 'w-28 pl-4 pr-9 border-l rounded-r-2xl',
isDarkMode isDarkMode
? 'text-gray-100 border-gray-700/60 focus:ring-indigo-500/80 bg-gray-800/60' ? 'text-gray-100 border-gray-700/60 focus:ring-indigo-500/80 bg-gray-800/60'
: 'text-gray-900 border-gray-200 focus:ring-indigo-500/60 bg-white' : 'text-gray-900 border-gray-200 focus:ring-indigo-500/60 bg-white'
@@ -166,14 +165,36 @@
<!-- 提交按钮 --> <!-- 提交按钮 -->
<button <button
type="submit" type="submit"
class="w-full bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white font-bold py-4 px-6 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-opacity-50 transition-all duration-300 transform hover:scale-105 hover:shadow-lg relative overflow-hidden group" :disabled="isSubmitting"
class="w-full bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white font-bold py-4 px-6 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-opacity-50 transition-all duration-300 transform hover:scale-105 hover:shadow-lg relative overflow-hidden group disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none disabled:hover:scale-100"
> >
<span <span
class="absolute top-0 left-0 w-full h-full bg-white opacity-0 group-hover:opacity-20 transition-opacity duration-300" class="absolute top-0 left-0 w-full h-full bg-white opacity-0 group-hover:opacity-20 transition-opacity duration-300"
></span> ></span>
<span class="relative z-10 flex items-center justify-center text-lg"> <span class="relative z-10 flex items-center justify-center text-lg">
<SendIcon class="w-6 h-6 mr-2" /> <svg
<span>{{ t('send.submit') }}</span> v-if="isSubmitting"
class="w-6 h-6 mr-2 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="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>
<SendIcon v-else class="w-6 h-6 mr-2" />
<span>{{ isSubmitting ? t('send.submitting') : t('send.submit') }}</span>
</span> </span>
</button> </button>
</form> </form>
@@ -532,8 +553,6 @@ interface ShareRecord {
retrieveCode: string retrieveCode: string
} }
const config: Config = JSON.parse(localStorage.getItem('config') || '{}') as Config const config: Config = JSON.parse(localStorage.getItem('config') || '{}') as Config
const router = useRouter() const router = useRouter()
@@ -549,6 +568,7 @@ const expirationValue = ref('1')
const uploadProgress = ref(0) const uploadProgress = ref(0)
const showDrawer = ref(false) const showDrawer = ref(false)
const selectedRecord = ref<ShareRecord | null>(null) const selectedRecord = ref<ShareRecord | null>(null)
const isSubmitting = ref(false)
const { t } = useI18n() const { t } = useI18n()
const alertStore = useAlertStore() const alertStore = useAlertStore()
@@ -564,8 +584,6 @@ const {
const fileHash = ref('') const fileHash = ref('')
const handleFileSelected = async (file: File) => { const handleFileSelected = async (file: File) => {
selectedFile.value = file selectedFile.value = file
if (!checkOpenUpload()) return if (!checkOpenUpload()) return
@@ -573,8 +591,6 @@ const handleFileSelected = async (file: File) => {
fileHash.value = await calculateFileHash(file) fileHash.value = await calculateFileHash(file)
} }
const handleFileDrop = async (event: DragEvent) => { const handleFileDrop = async (event: DragEvent) => {
if (event.dataTransfer?.files && event.dataTransfer.files.length > 0) { if (event.dataTransfer?.files && event.dataTransfer.files.length > 0) {
const file = event.dataTransfer.files[0] const file = event.dataTransfer.files[0]
@@ -604,7 +620,10 @@ const handlePaste = async (event: ClipboardEvent) => {
try { try {
fileHash.value = await calculateFileHash(file) fileHash.value = await calculateFileHash(file)
alertStore.showAlert(t('send.messages.fileAddedFromClipboard', { filename: file.name }), 'success') alertStore.showAlert(
t('send.messages.fileAddedFromClipboard', { filename: file.name }),
'success'
)
} catch (err) { } catch (err) {
alertStore.showAlert(t('send.messages.fileProcessingFailed'), 'error') alertStore.showAlert(t('send.messages.fileProcessingFailed'), 'error')
console.error('File hash calculation failed:', err) console.error('File hash calculation failed:', err)
@@ -860,10 +879,13 @@ const handleChunkUpload = async (file: File) => {
} }
// 3. 完成上传 // 3. 完成上传
const completeResponse: ApiResponse<{ code?: string; name?: string }> = await api.post(`chunk/upload/complete/${uploadId}`, { const completeResponse: ApiResponse<{ code?: string; name?: string }> = await api.post(
expire_value: expirationValue.value ? parseInt(expirationValue.value) : 1, `chunk/upload/complete/${uploadId}`,
expire_style: expirationMethod.value {
}) expire_value: expirationValue.value ? parseInt(expirationValue.value) : 1,
expire_style: expirationMethod.value
}
)
if (completeResponse.code !== 200) { if (completeResponse.code !== 200) {
throw new Error(t('send.messages.completeUploadFailed')) throw new Error(t('send.messages.completeUploadFailed'))
@@ -898,7 +920,11 @@ const handleDefaultFileUpload = async (file: File) => {
formData.append('file', file) formData.append('file', file)
formData.append('expire_value', expirationValue.value) formData.append('expire_value', expirationValue.value)
formData.append('expire_style', expirationMethod.value) formData.append('expire_style', expirationMethod.value)
const response: ApiResponse<{ code?: string; name?: string }> = await api.post('share/file/', formData, config) const response: ApiResponse<{ code?: string; name?: string }> = await api.post(
'share/file/',
formData,
config
)
return response return response
} }
@@ -933,7 +959,10 @@ const checkOpenUpload = () => {
const checkFileSize = (file: File) => { const checkFileSize = (file: File) => {
if (file.size > config.uploadSize) { if (file.size > config.uploadSize) {
alertStore.showAlert(t('send.messages.fileSizeExceeded', { size: getStorageUnit(config.uploadSize) }), 'error') alertStore.showAlert(
t('send.messages.fileSizeExceeded', { size: getStorageUnit(config.uploadSize) }),
'error'
)
selectedFile.value = null selectedFile.value = null
return false return false
} }
@@ -971,27 +1000,30 @@ const checkUpload = () => {
return true return true
} }
const handleSubmit = async () => { const handleSubmit = async () => {
if (sendType.value === 'file' && !selectedFile.value) { if (isSubmitting.value) return
alertStore.showAlert(t('send.messages.selectFile'), 'error') isSubmitting.value = true
return
}
if (sendType.value === 'text' && !textContent.value.trim()) {
alertStore.showAlert(t('send.messages.enterText'), 'error')
return
}
if (expirationMethod.value !== 'forever' && !expirationValue.value) {
alertStore.showAlert(t('send.messages.enterExpirationValue'), 'error')
return
}
// 添加过期时间验证
if (!checkExpirationTime(expirationMethod.value, expirationValue.value)) {
const maxDays = Math.floor(config.max_save_seconds / 86400)
alertStore.showAlert(t('send.messages.expirationTooLong', { days: maxDays }), 'error')
return
}
try { try {
if (sendType.value === 'file' && !selectedFile.value) {
alertStore.showAlert(t('send.messages.selectFile'), 'error')
return
}
if (sendType.value === 'text' && !textContent.value.trim()) {
alertStore.showAlert(t('send.messages.enterText'), 'error')
return
}
if (expirationMethod.value !== 'forever' && !expirationValue.value) {
alertStore.showAlert(t('send.messages.enterExpirationValue'), 'error')
return
}
// 添加过期时间验证
if (!checkExpirationTime(expirationMethod.value, expirationValue.value)) {
const maxDays = Math.floor(config.max_save_seconds / 86400)
alertStore.showAlert(t('send.messages.expirationTooLong', { days: maxDays }), 'error')
return
}
let response: ApiResponse let response: ApiResponse
if (sendType.value === 'file') { if (sendType.value === 'file') {
@@ -1020,8 +1052,8 @@ const handleSubmit = async () => {
} }
if (response && response.code === 200) { if (response && response.code === 200) {
const retrieveCode = (response.detail as unknown as { code?: string })?.code || '' const retrieveCode = (response.detail as unknown as { code?: string })?.code || ''
const fileName = (response.detail as unknown as { name?: string })?.name || '' const fileName = (response.detail as unknown as { name?: string })?.name || ''
// 添加新的发送记录 // 添加新的发送记录
const newRecord: ShareRecord = { const newRecord: ShareRecord = {
id: Date.now(), id: Date.now(),
@@ -1064,6 +1096,7 @@ const handleSubmit = async () => {
} }
} finally { } finally {
uploadProgress.value = 0 uploadProgress.value = 0
isSubmitting.value = false
} }
} }
+1 -2
View File
@@ -3,11 +3,10 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx' import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
import tailwindcss from 'tailwindcss' import tailwindcss from 'tailwindcss'
import autoprefixer from 'autoprefixer' import autoprefixer from 'autoprefixer'
export default defineConfig({ export default defineConfig({
plugins: [vue(), vueJsx(), vueDevTools()], plugins: [vue(), vueJsx()],
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': fileURLToPath(new URL('./src', import.meta.url))