feat: 多语言支持 https://github.com/vastsa/FileCodeBox/issues/310 https://github.com/vastsa/FileCodeBox/issues/299
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"spark-md5": "^3.0.2",
|
||||
"vue": "^3.5.18",
|
||||
"vue-i18n": "9",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Generated
+40
@@ -41,6 +41,9 @@ importers:
|
||||
vue:
|
||||
specifier: ^3.5.18
|
||||
version: 3.5.21(typescript@5.8.3)
|
||||
vue-i18n:
|
||||
specifier: '9'
|
||||
version: 9.14.5(vue@3.5.21(typescript@5.8.3))
|
||||
vue-router:
|
||||
specifier: ^4.5.1
|
||||
version: 4.5.1(vue@3.5.21(typescript@5.8.3))
|
||||
@@ -481,6 +484,18 @@ packages:
|
||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||
engines: {node: '>=18.18'}
|
||||
|
||||
'@intlify/core-base@9.14.5':
|
||||
resolution: {integrity: sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/message-compiler@9.14.5':
|
||||
resolution: {integrity: sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/shared@9.14.5':
|
||||
resolution: {integrity: sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@isaacs/balanced-match@4.0.1':
|
||||
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -2085,6 +2100,12 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
|
||||
vue-i18n@9.14.5:
|
||||
resolution: {integrity: sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==}
|
||||
engines: {node: '>= 16'}
|
||||
peerDependencies:
|
||||
vue: ^3.0.0
|
||||
|
||||
vue-router@4.5.1:
|
||||
resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==}
|
||||
peerDependencies:
|
||||
@@ -2483,6 +2504,18 @@ snapshots:
|
||||
|
||||
'@humanwhocodes/retry@0.4.3': {}
|
||||
|
||||
'@intlify/core-base@9.14.5':
|
||||
dependencies:
|
||||
'@intlify/message-compiler': 9.14.5
|
||||
'@intlify/shared': 9.14.5
|
||||
|
||||
'@intlify/message-compiler@9.14.5':
|
||||
dependencies:
|
||||
'@intlify/shared': 9.14.5
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@intlify/shared@9.14.5': {}
|
||||
|
||||
'@isaacs/balanced-match@4.0.1': {}
|
||||
|
||||
'@isaacs/brace-expansion@5.0.0':
|
||||
@@ -4086,6 +4119,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vue-i18n@9.14.5(vue@3.5.21(typescript@5.8.3)):
|
||||
dependencies:
|
||||
'@intlify/core-base': 9.14.5
|
||||
'@intlify/shared': 9.14.5
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.21(typescript@5.8.3)
|
||||
|
||||
vue-router@4.5.1(vue@3.5.21(typescript@5.8.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 6.6.4
|
||||
|
||||
+23
-32
@@ -1,44 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watchEffect, provide, onMounted } from 'vue'
|
||||
import { ref, provide, onMounted, onUnmounted } from 'vue'
|
||||
import { RouterView } from 'vue-router'
|
||||
import ThemeToggle from './components/common/ThemeToggle.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
const isDarkMode = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const router = useRouter()
|
||||
import LanguageSwitcher from './components/common/LanguageSwitcher.vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import AlertComponent from '@/components/common/AlertComponent.vue'
|
||||
import { useAlertStore } from '@/stores/alertStore'
|
||||
import { ConfigService } from '@/services'
|
||||
import type { ApiResponse, ConfigState } from '@/types'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
|
||||
const isLoading = ref(false)
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const alertStore = useAlertStore()
|
||||
// 检查系统颜色模式
|
||||
const checkSystemColorScheme = () => {
|
||||
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
}
|
||||
|
||||
// 从本地存储获取用户之前的选择
|
||||
const getUserPreference = () => {
|
||||
const storedPreference = localStorage.getItem('colorMode')
|
||||
if (storedPreference) {
|
||||
return storedPreference === 'dark'
|
||||
}
|
||||
return null
|
||||
}
|
||||
// 使用主题 composable
|
||||
const { isDarkMode, toggleTheme, initTheme } = useTheme()
|
||||
|
||||
// 设置颜色模式
|
||||
const setColorMode = (isDark: boolean) => {
|
||||
isDarkMode.value = isDark
|
||||
localStorage.setItem('colorMode', isDark ? 'dark' : 'light')
|
||||
}
|
||||
// 清理函数
|
||||
let cleanupThemeListener: (() => void) | null = null
|
||||
|
||||
onMounted(() => {
|
||||
const userPreference = getUserPreference()
|
||||
if (userPreference !== null) {
|
||||
setColorMode(userPreference)
|
||||
} else {
|
||||
setColorMode(checkSystemColorScheme())
|
||||
}
|
||||
// 初始化主题并设置监听器
|
||||
cleanupThemeListener = initTheme()
|
||||
ConfigService.getUserConfig().then((res: ApiResponse<ConfigState>) => {
|
||||
if (res.code === 200 && res.detail) {
|
||||
localStorage.setItem('config', JSON.stringify(res.detail))
|
||||
@@ -54,8 +39,11 @@ onMounted(() => {
|
||||
})
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
document.documentElement.classList.toggle('dark', isDarkMode.value)
|
||||
onUnmounted(() => {
|
||||
// 清理主题监听器
|
||||
if (cleanupThemeListener) {
|
||||
cleanupThemeListener()
|
||||
}
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
@@ -70,19 +58,22 @@ router.afterEach(() => {
|
||||
})
|
||||
|
||||
provide('isDarkMode', isDarkMode)
|
||||
provide('setColorMode', setColorMode)
|
||||
provide('toggleTheme', toggleTheme)
|
||||
provide('isLoading', isLoading)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['app-container', isDarkMode ? 'dark' : 'light']">
|
||||
<div class="fixed top-4 right-4 z-50 flex items-center space-x-3">
|
||||
<LanguageSwitcher />
|
||||
<ThemeToggle v-model="isDarkMode" />
|
||||
</div>
|
||||
<div v-if="isLoading" class="loading-overlay">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" :key="$route.fullPath" />
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</transition>
|
||||
</RouterView>
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
@click="removeAlert(alert.id)"
|
||||
class="inline-flex text-white hover:text-gray-200 focus:outline-none transition-colors duration-200"
|
||||
>
|
||||
<span class="sr-only">关闭</span>
|
||||
<span class="sr-only">{{ t('common.close') }}</span>
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -47,6 +47,9 @@ import { storeToRefs } from 'pinia'
|
||||
import { useAlertStore } from '@/stores/alertStore'
|
||||
import { CheckCircle, AlertTriangle, AlertCircle, Info, X } from 'lucide-vue-next'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const alertStore = useAlertStore()
|
||||
const { alerts } = storeToRefs(alertStore)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
>
|
||||
<div class="flex justify-between items-center mb-4 flex-shrink-0">
|
||||
<h3 class="text-2xl font-bold" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
|
||||
内容预览
|
||||
{{ t('contentPreview.title') }}
|
||||
</h3>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
@@ -24,7 +24,7 @@
|
||||
]"
|
||||
>
|
||||
<CopyIcon class="w-4 h-4" />
|
||||
复制
|
||||
{{ t('common.copy') }}
|
||||
</button>
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
@@ -53,6 +53,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { CopyIcon, XIcon } from 'lucide-vue-next'
|
||||
|
||||
interface Props {
|
||||
@@ -67,6 +68,7 @@ interface Emits {
|
||||
|
||||
defineProps<Props>()
|
||||
defineEmits<Emits>()
|
||||
const { t } = useI18n()
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col space-y-3">
|
||||
<label :class="['text-sm font-medium', isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
过期时间
|
||||
{{ t('send.expiration.label') }}
|
||||
</label>
|
||||
<div class="relative flex-grow group">
|
||||
<div
|
||||
@@ -85,11 +85,11 @@
|
||||
: 'border-gray-200 text-gray-700 focus:ring-indigo-500/60'
|
||||
]"
|
||||
>
|
||||
<option value="count">次数</option>
|
||||
<option value="minute">分钟</option>
|
||||
<option value="hour">小时</option>
|
||||
<option value="day">天</option>
|
||||
<option value="forever">永久</option>
|
||||
<option value="count">{{ t('send.expiration.units.times') }}</option>
|
||||
<option value="minute">{{ t('send.expiration.units.minutes') }}</option>
|
||||
<option value="hour">{{ t('send.expiration.units.hours') }}</option>
|
||||
<option value="day">{{ t('send.expiration.units.days') }}</option>
|
||||
<option value="forever">{{ t('send.expiration.units.forever') }}</option>
|
||||
</select>
|
||||
<div
|
||||
class="absolute right-4 top-1/2 transform -translate-y-1/2 pointer-events-none"
|
||||
@@ -111,6 +111,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Props {
|
||||
expirationMethod: string
|
||||
@@ -145,15 +148,15 @@ const incrementValue = (delta: number) => {
|
||||
const getPlaceholder = () => {
|
||||
switch (props.expirationMethod) {
|
||||
case 'count':
|
||||
return '输入次数'
|
||||
return t('send.expiration.placeholders.count')
|
||||
case 'minute':
|
||||
return '输入分钟数'
|
||||
return t('send.expiration.placeholders.minutes')
|
||||
case 'hour':
|
||||
return '输入小时数'
|
||||
return t('send.expiration.placeholders.hours')
|
||||
case 'day':
|
||||
return '输入天数'
|
||||
return t('send.expiration.placeholders.days')
|
||||
default:
|
||||
return '输入数值'
|
||||
return t('send.expiration.placeholders.default')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -13,7 +13,7 @@
|
||||
class="text-2xl font-bold mb-6 truncate"
|
||||
:class="[isDarkMode ? 'text-white' : 'text-gray-800']"
|
||||
>
|
||||
文件详情
|
||||
{{ t('fileDetail.title') }}
|
||||
</h3>
|
||||
<div class="space-y-4" v-if="record">
|
||||
<div class="flex items-center">
|
||||
@@ -25,7 +25,7 @@
|
||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
|
||||
class="truncate flex-grow"
|
||||
>
|
||||
<span class="font-medium">文件名:</span>{{ record.filename }}
|
||||
<span class="font-medium">{{ t('fileRecord.filename') }}:</span>{{ record.filename }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
@@ -37,7 +37,7 @@
|
||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
|
||||
class="truncate flex-grow"
|
||||
>
|
||||
<span class="font-medium">取件日期:</span>{{ record.date }}
|
||||
<span class="font-medium">{{ t('fileRecord.date') }}:</span>{{ record.date }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
@@ -49,7 +49,7 @@
|
||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
|
||||
class="truncate flex-grow"
|
||||
>
|
||||
<span class="font-medium">文件大小:</span>{{ record.size }}
|
||||
<span class="font-medium">{{ t('fileRecord.size') }}:</span>{{ record.size }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
@@ -58,14 +58,14 @@
|
||||
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
|
||||
/>
|
||||
<p :class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']">
|
||||
<span class="font-medium">文件内容:</span>
|
||||
<span class="font-medium">{{ t('fileDetail.content') }}:</span>
|
||||
</p>
|
||||
<div v-if="record.filename === 'Text'" class="ml-2">
|
||||
<button
|
||||
@click="$emit('preview-content')"
|
||||
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition duration-300"
|
||||
>
|
||||
预览内容
|
||||
{{ t('fileDetail.previewContent') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
@@ -75,7 +75,7 @@
|
||||
rel="noopener noreferrer"
|
||||
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition duration-300"
|
||||
>
|
||||
点击下载
|
||||
{{ t('fileDetail.download') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,13 +86,13 @@
|
||||
class="text-lg font-semibold mb-3"
|
||||
:class="[isDarkMode ? 'text-white' : 'text-gray-800']"
|
||||
>
|
||||
取件二维码
|
||||
{{ t('fileDetail.qrCode') }}
|
||||
</h4>
|
||||
<div class="bg-white p-2 rounded-lg shadow-md">
|
||||
<QRCode :value="getQRCodeValue(record)" :size="128" level="M" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-600']">
|
||||
扫描二维码快速取件
|
||||
{{ t('fileDetail.scanQrCode') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
@click="$emit('close')"
|
||||
class="mt-8 w-full bg-gradient-to-r from-indigo-500 to-purple-600 text-white px-6 py-3 rounded-lg font-medium hover:from-indigo-600 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-opacity-50 transition duration-300 transform hover:scale-105"
|
||||
>
|
||||
关闭
|
||||
{{ t('common.close') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,6 +109,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { FileIcon, CalendarIcon, HardDriveIcon, DownloadIcon } from 'lucide-vue-next'
|
||||
import QRCode from 'qrcode.vue'
|
||||
|
||||
@@ -134,6 +135,7 @@ interface Emits {
|
||||
|
||||
defineProps<Props>()
|
||||
defineEmits<Emits>()
|
||||
const { t } = useI18n()
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
const baseUrl = window.location.origin
|
||||
|
||||
|
||||
@@ -37,19 +37,22 @@
|
||||
]"
|
||||
>
|
||||
<span class="block truncate">
|
||||
{{ selectedFile ? selectedFile.name : placeholder }}
|
||||
{{ selectedFile ? selectedFile.name : placeholderText }}
|
||||
</span>
|
||||
</p>
|
||||
<p :class="['mt-2 text-xs', isDarkMode ? 'text-gray-500' : 'text-gray-400']">
|
||||
{{ description }}
|
||||
{{ descriptionText }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject } from 'vue'
|
||||
import { ref, inject, computed } from 'vue'
|
||||
import { UploadCloudIcon } from 'lucide-vue-next'
|
||||
import BorderProgressBar from './BorderProgressBar.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
interface Props {
|
||||
selectedFile?: File | null
|
||||
progress?: number
|
||||
@@ -63,17 +66,21 @@ interface Emits {
|
||||
fileDrop: [event: DragEvent]
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
selectedFile: null,
|
||||
progress: 0,
|
||||
placeholder: '点击或拖放文件到此处上传',
|
||||
description: '支持各种常见格式',
|
||||
placeholder: '',
|
||||
description: '',
|
||||
acceptedTypes: '*'
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
|
||||
// 使用computed属性处理多语言文本
|
||||
const placeholderText = computed(() => props.placeholder || t('send.uploadArea.placeholder'))
|
||||
const descriptionText = computed(() => props.description || t('send.uploadArea.description'))
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const triggerFileUpload = () => {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<button
|
||||
@click="toggleDropdown"
|
||||
:class="[
|
||||
'flex items-center space-x-2 px-3 py-2 rounded-lg transition-all duration-200',
|
||||
isDarkMode
|
||||
? 'bg-gray-800/60 hover:bg-gray-700/80 text-gray-300 hover:text-white'
|
||||
: 'bg-gray-100 hover:bg-gray-200 text-gray-700 hover:text-gray-900'
|
||||
]"
|
||||
>
|
||||
<GlobeIcon class="w-4 h-4" />
|
||||
<span class="text-sm font-medium">{{ currentLanguage.name }}</span>
|
||||
<ChevronDownIcon
|
||||
:class="[
|
||||
'w-4 h-4 transition-transform duration-200',
|
||||
{ 'rotate-180': isDropdownOpen }
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<transition
|
||||
enter-active-class="transition ease-out duration-200"
|
||||
enter-from-class="opacity-0 scale-95"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
leave-active-class="transition ease-in duration-150"
|
||||
leave-from-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
v-if="isDropdownOpen"
|
||||
:class="[
|
||||
'absolute right-0 mt-2 w-32 rounded-lg shadow-lg ring-1 ring-black ring-opacity-5 z-50',
|
||||
isDarkMode
|
||||
? 'bg-gray-800 border border-gray-700'
|
||||
: 'bg-white border border-gray-200'
|
||||
]"
|
||||
>
|
||||
<div class="py-1">
|
||||
<button
|
||||
v-for="language in availableLocales"
|
||||
:key="language.code"
|
||||
@click="switchLanguage(language.code)"
|
||||
:class="[
|
||||
'w-full text-left px-4 py-2 text-sm transition-colors duration-150',
|
||||
currentLocale === language.code
|
||||
? isDarkMode
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'bg-indigo-50 text-indigo-600'
|
||||
: isDarkMode
|
||||
? 'text-gray-300 hover:bg-gray-700 hover:text-white'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
]"
|
||||
>
|
||||
{{ language.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, inject, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { GlobeIcon, ChevronDownIcon } from 'lucide-vue-next'
|
||||
import { availableLocales, setLocale } from '@/i18n/index'
|
||||
|
||||
const { locale } = useI18n()
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
const isDropdownOpen = ref(false)
|
||||
|
||||
const currentLocale = computed(() => locale.value)
|
||||
const currentLanguage = computed(() => {
|
||||
return availableLocales.find((lang: { code: string; name: string }) => lang.code === currentLocale.value) || availableLocales[0]
|
||||
})
|
||||
|
||||
const toggleDropdown = () => {
|
||||
isDropdownOpen.value = !isDropdownOpen.value
|
||||
}
|
||||
|
||||
const switchLanguage = (langCode: string) => {
|
||||
setLocale(langCode)
|
||||
isDropdownOpen.value = false
|
||||
}
|
||||
|
||||
// 点击外部关闭下拉菜单
|
||||
const handleClickOutside = (event: Event) => {
|
||||
const target = event.target as HTMLElement
|
||||
if (!target.closest('.relative')) {
|
||||
isDropdownOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
</script>
|
||||
@@ -18,7 +18,7 @@
|
||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
|
||||
>
|
||||
<ShieldCheckIcon class="w-4 h-4 mr-1 text-green-400" />
|
||||
安全加密
|
||||
{{ t('send.secureEncryption') }}
|
||||
</span>
|
||||
<button
|
||||
@click="$emit('toggle-drawer')"
|
||||
@@ -35,6 +35,9 @@
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { ShieldCheckIcon, ClipboardListIcon } from 'lucide-vue-next'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Props {
|
||||
linkText?: string
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
for="code"
|
||||
class="block text-sm font-medium mb-2"
|
||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
|
||||
>取件码</label
|
||||
>{{ t('retrieve.codeInput.label') }}</label
|
||||
>
|
||||
<div class="relative">
|
||||
<input
|
||||
@@ -19,7 +19,7 @@
|
||||
{ 'ring-2 ring-red-500': error },
|
||||
isDarkMode ? 'text-gray-300' : 'text-gray-800'
|
||||
]"
|
||||
placeholder="请输入5位取件码"
|
||||
:placeholder="t('retrieve.codeInput.placeholder')"
|
||||
required
|
||||
:readonly="inputStatus.readonly"
|
||||
maxlength="5"
|
||||
@@ -46,7 +46,7 @@
|
||||
:disabled="inputStatus.loading"
|
||||
>
|
||||
<span class="flex items-center justify-center relative z-10">
|
||||
<span>{{ inputStatus.loading ? '处理中...' : '提取文件' }}</span>
|
||||
<span>{{ inputStatus.loading ? t('common.loading') : t('retrieve.submit') }}</span>
|
||||
<ArrowRightIcon
|
||||
class="w-5 h-5 ml-2 transition-transform duration-300 transform group-hover:translate-x-1"
|
||||
/>
|
||||
@@ -61,6 +61,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, watch } from 'vue'
|
||||
import { ArrowRightIcon } from 'lucide-vue-next'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface InputStatus {
|
||||
readonly: boolean
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
]"
|
||||
>
|
||||
发送文件
|
||||
{{ t('nav.sendFile') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -26,7 +26,7 @@
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
]"
|
||||
>
|
||||
发送文本
|
||||
{{ t('send.sendText') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -34,6 +34,9 @@
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import type { SendType } from '@/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Props {
|
||||
selectedType: SendType
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
:value="modelValue"
|
||||
@input="updateValue"
|
||||
:rows="rows"
|
||||
:placeholder="placeholder"
|
||||
:placeholder="placeholderText"
|
||||
:class="[
|
||||
'flex-grow px-4 py-3 rounded-xl placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 transition duration-300 resize-none custom-scrollbar',
|
||||
isDarkMode
|
||||
@@ -16,7 +16,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { inject, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Props {
|
||||
modelValue: string
|
||||
@@ -28,7 +31,7 @@ interface Emits {
|
||||
'update:modelValue': [value: string]
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
rows: 7,
|
||||
placeholder: '在此输入要发送的文本...'
|
||||
})
|
||||
@@ -36,6 +39,9 @@ withDefaults(defineProps<Props>(), {
|
||||
const emit = defineEmits<Emits>()
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
|
||||
// 使用computed属性处理多语言文本
|
||||
const placeholderText = computed(() => props.placeholder || t('send.uploadArea.textInput'))
|
||||
|
||||
const updateValue = (event: Event) => {
|
||||
const target = event.target as HTMLTextAreaElement
|
||||
emit('update:modelValue', target.value)
|
||||
|
||||
@@ -3,17 +3,17 @@ import { inject } from 'vue'
|
||||
import { SunIcon, MoonIcon } from 'lucide-vue-next'
|
||||
|
||||
const isDarkMode = inject('isDarkMode') as { value: boolean }
|
||||
const setColorMode = inject('setColorMode') as (isDark: boolean) => void
|
||||
const toggleTheme = inject('toggleTheme') as () => void
|
||||
|
||||
const toggleColorMode = () => {
|
||||
setColorMode(!isDarkMode.value)
|
||||
toggleTheme()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
@click="toggleColorMode"
|
||||
class="fixed top-4 right-4 z-10 p-2 rounded-full transition-all duration-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transform hover:rotate-180"
|
||||
class="p-2 rounded-full transition-all duration-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transform hover:rotate-180"
|
||||
:class="isDarkMode ? 'bg-gray-800 text-yellow-300' : 'bg-white text-gray-800'"
|
||||
>
|
||||
<SunIcon v-if="!isDarkMode" class="w-6 h-6" />
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import zhCN from './locales/zh-CN'
|
||||
import enUS from './locales/en-US'
|
||||
|
||||
// 获取浏览器语言设置
|
||||
const getDefaultLocale = (): string => {
|
||||
const savedLocale = localStorage.getItem('locale')
|
||||
if (savedLocale) {
|
||||
return savedLocale
|
||||
}
|
||||
|
||||
const browserLocale = navigator.language.toLowerCase()
|
||||
if (browserLocale.startsWith('zh')) {
|
||||
return 'zh-CN'
|
||||
}
|
||||
return 'en-US'
|
||||
}
|
||||
|
||||
const messages = {
|
||||
'zh-CN': zhCN,
|
||||
'en-US': enUS
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false, // 使用 Composition API 模式
|
||||
locale: getDefaultLocale(),
|
||||
fallbackLocale: 'zh-CN',
|
||||
messages,
|
||||
globalInjection: true // 全局注入 $t 函数
|
||||
})
|
||||
|
||||
export default i18n
|
||||
|
||||
// 导出切换语言的函数
|
||||
export const setLocale = (locale: string) => {
|
||||
i18n.global.locale.value = locale as 'zh-CN' | 'en-US'
|
||||
localStorage.setItem('locale', locale)
|
||||
document.documentElement.lang = locale
|
||||
}
|
||||
|
||||
// 导出获取当前语言的函数
|
||||
export const getCurrentLocale = () => {
|
||||
return i18n.global.locale.value
|
||||
}
|
||||
|
||||
// 导出可用语言列表
|
||||
export const availableLocales = [
|
||||
{ code: 'zh-CN', name: '中文' },
|
||||
{ code: 'en-US', name: 'English' }
|
||||
]
|
||||
@@ -0,0 +1,597 @@
|
||||
export default {
|
||||
// Common
|
||||
common: {
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
close: 'Close',
|
||||
delete: 'Delete',
|
||||
edit: 'Edit',
|
||||
save: 'Save',
|
||||
add: 'Add',
|
||||
back: 'Back',
|
||||
next: 'Next',
|
||||
previous: 'Previous',
|
||||
loading: 'Loading...',
|
||||
noData: 'No Data',
|
||||
success: 'Success',
|
||||
error: 'Error',
|
||||
warning: 'Warning',
|
||||
info: 'Information',
|
||||
search: 'Search',
|
||||
copy: 'Copy',
|
||||
uploadSuccess: 'Upload successful',
|
||||
uploadFailed: 'Upload failed',
|
||||
copySuccess: 'Copy successful',
|
||||
copyFailed: 'Copy failed',
|
||||
deleteSuccess: 'Delete successful',
|
||||
deleteFailed: 'Delete failed',
|
||||
downloadSuccess: 'Download successful',
|
||||
downloadFailed: 'Download failed',
|
||||
shareSuccess: 'Share successful',
|
||||
shareFailed: 'Share failed',
|
||||
expiredFile: 'File has expired',
|
||||
fileNotFound: 'File not found',
|
||||
networkError: 'Network error',
|
||||
unknownError: 'Unknown error',
|
||||
invalidFileType: 'Invalid file type',
|
||||
fileTooLarge: 'File too large',
|
||||
uploadCanceled: 'Upload canceled',
|
||||
processing: 'Processing',
|
||||
pleaseWait: 'Please wait',
|
||||
fileDetails: 'File Details',
|
||||
enabled: 'Enabled',
|
||||
disabled: 'Disabled',
|
||||
minute: 'minute',
|
||||
files: 'files',
|
||||
second: 'second',
|
||||
hour: 'hour',
|
||||
day: 'day',
|
||||
times: 'times',
|
||||
appName: 'FileCodeBox - File Transfer',
|
||||
appDescription: 'Ready-to-use file transfer system'
|
||||
},
|
||||
|
||||
// Admin module
|
||||
admin: {
|
||||
dashboard: {
|
||||
title: 'Dashboard',
|
||||
totalFiles: 'Total Files',
|
||||
storageSpace: 'Storage Space',
|
||||
activeUsers: 'Active Users',
|
||||
systemStatus: 'System Status',
|
||||
yesterday: 'Yesterday:',
|
||||
today: 'Today:',
|
||||
weeklyChange: '↓ 5% from last week',
|
||||
normal: 'Normal',
|
||||
serverUptime: 'Server Uptime:',
|
||||
version: 'Version v2.2.1 Updated: 2025-09-04'
|
||||
},
|
||||
fileManage: {
|
||||
title: 'File Management'
|
||||
},
|
||||
settings: {
|
||||
title: 'System Settings',
|
||||
basicSettings: 'Basic Settings',
|
||||
websiteInfo: 'Website Basic Information',
|
||||
websiteName: 'Website Name',
|
||||
websiteDescription: 'Website Description',
|
||||
siteName: 'Site Name',
|
||||
adminPassword: 'Admin Password',
|
||||
passwordPlaceholder: 'Leave blank to keep current password',
|
||||
passwordNote: 'Leave blank to keep unchanged',
|
||||
keywords: 'Keywords',
|
||||
themeSelection: 'Theme Selection',
|
||||
robotsFile: 'Robots.txt',
|
||||
notificationSettings: 'Notification Settings',
|
||||
notificationTitle: 'Notification Title',
|
||||
notificationContent: 'Notification Content',
|
||||
storageSettings: 'Storage Settings',
|
||||
storagePath: 'Storage Path',
|
||||
storagePathPlaceholder: 'Leave blank to use default path, optional',
|
||||
storageMethod: 'Storage Method',
|
||||
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)',
|
||||
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',
|
||||
enabled: 'Enabled',
|
||||
disabled: 'Disabled',
|
||||
webdavUrl: 'Please enter Webdav URL',
|
||||
webdavUsername: 'Please enter Webdav Username',
|
||||
webdavPassword: 'Please enter Webdav Password',
|
||||
webdavUrlPlaceholder: 'Please enter Webdav URL',
|
||||
webdavUsernamePlaceholder: 'Please enter Webdav Username',
|
||||
webdavPasswordPlaceholder: 'Please enter Webdav Password',
|
||||
enableProxy: 'Enable Proxy',
|
||||
uploadLimits: 'Upload Limits',
|
||||
uploadRateLimit: 'Upload Rate Limit',
|
||||
uploadPerMinute: 'Upload Per Minute',
|
||||
minute: 'minute',
|
||||
uploadCountLimit: 'Upload Count Limit',
|
||||
files: 'files',
|
||||
fileSizeLimit: 'File Size Limit',
|
||||
expirationMethod: 'Expiration Method',
|
||||
expirationType: 'Expiration Type',
|
||||
expiration: {
|
||||
day: 'By Day',
|
||||
hour: 'By Hour',
|
||||
minute: 'By Minute',
|
||||
forever: 'Forever',
|
||||
count: 'By Count'
|
||||
},
|
||||
maxSaveTime: 'Maximum Save Time',
|
||||
timeUnits: {
|
||||
second: 'second',
|
||||
minute: 'minute',
|
||||
hour: 'hour',
|
||||
day: 'day'
|
||||
},
|
||||
guestUpload: 'Guest Upload',
|
||||
errorLimits: 'Error Limits',
|
||||
errorRateLimit: 'Error Rate Limit',
|
||||
errorPerMinute: 'Error Per Minute',
|
||||
errorCountLimit: 'Error Count Limit',
|
||||
times: 'times',
|
||||
saveChanges: 'Save Changes',
|
||||
fileSizeUnits: {
|
||||
kb: 'KB',
|
||||
mb: 'MB',
|
||||
gb: 'GB'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Navigation
|
||||
nav: {
|
||||
sendFile: 'Send File',
|
||||
retrieveFile: 'Retrieve',
|
||||
fileRecords: 'File Records'
|
||||
},
|
||||
|
||||
// Retrieve page
|
||||
retrieve: {
|
||||
title: 'File Retrieval',
|
||||
codeInput: {
|
||||
placeholder: 'Enter 5-digit retrieval code',
|
||||
label: 'Retrieval Code'
|
||||
},
|
||||
submit: 'Retrieve',
|
||||
needSendFile: 'Need to send a file? Click here',
|
||||
recordsDrawer: 'Retrieval Records',
|
||||
messages: {
|
||||
invalidCode: 'Please enter a 5-digit retrieval code',
|
||||
retrieveSuccess: 'File retrieved successfully',
|
||||
invalidCodeError: 'Invalid retrieval code',
|
||||
retrieveFailure: 'Failed to retrieve file: ',
|
||||
networkError: 'Retrieval failed, please try again later: ',
|
||||
unknownError: 'Unknown error'
|
||||
}
|
||||
},
|
||||
|
||||
// Send page
|
||||
send: {
|
||||
title: 'File Send',
|
||||
sendText: 'Send Text',
|
||||
uploadArea: {
|
||||
dragText: 'Drag files here or',
|
||||
clickText: 'click to select files',
|
||||
textInput: 'Enter text to send here...',
|
||||
placeholder: 'Click or drag files here to upload',
|
||||
description: 'Supports various common formats'
|
||||
},
|
||||
submit: 'Secure Send',
|
||||
needRetrieveFile: 'Need to retrieve? Click here',
|
||||
sendRecords: 'Send Records',
|
||||
secureEncryption: 'Secure Encryption',
|
||||
fileDetails: 'File Details',
|
||||
expirationMethod: 'Expiration Method',
|
||||
expiration: {
|
||||
day: 'By Day',
|
||||
hour: 'By Hour',
|
||||
minute: 'By Minute',
|
||||
forever: 'Forever',
|
||||
count: 'By Count',
|
||||
label: 'Expiration Time',
|
||||
placeholders: {
|
||||
days: 'Enter days',
|
||||
hours: 'Enter hours',
|
||||
minutes: 'Enter minutes',
|
||||
count: 'Enter view count',
|
||||
forever: 'Forever',
|
||||
default: 'Enter value'
|
||||
},
|
||||
units: {
|
||||
days: 'days',
|
||||
hours: 'hours',
|
||||
minutes: 'minutes',
|
||||
times: 'times',
|
||||
forever: 'Forever'
|
||||
}
|
||||
},
|
||||
time: {
|
||||
day: 'By Day',
|
||||
hour: 'By Hour',
|
||||
minute: 'By Minute',
|
||||
forever: 'Forever',
|
||||
count: 'By Count'
|
||||
},
|
||||
messages: {
|
||||
selectFile: 'Please select a file to upload',
|
||||
enterText: 'Please enter text to send',
|
||||
enterExpirationValue: 'Please enter expiration value',
|
||||
expirationTooLong: 'Expiration time cannot exceed {days} days',
|
||||
sendSuccess: 'File sent successfully! Retrieve code: {code}',
|
||||
initChunkUploadFailed: 'Failed to initialize chunk upload',
|
||||
chunkUploadFailed: 'Chunk {index} upload failed',
|
||||
completeUploadFailed: 'Failed to complete upload',
|
||||
uploadFailed: 'Upload failed, please try again later',
|
||||
guestUploadDisabled: 'Guest upload feature is disabled',
|
||||
fileSizeExceeded: 'File size exceeds limit ({size})',
|
||||
serverError: 'Server response error',
|
||||
sendFailed: 'Send failed, please try again later',
|
||||
expiresAfterCount: 'Expires after {count} times',
|
||||
expiresAt: 'Expires at {date}',
|
||||
emptyFileError: 'Cannot read empty file',
|
||||
fileAddedFromClipboard: 'File added from clipboard: {filename}',
|
||||
fileProcessingFailed: 'File processing failed',
|
||||
expiresAfter: 'Expires after {value} {unit}'
|
||||
}
|
||||
},
|
||||
|
||||
// File records
|
||||
fileRecord: {
|
||||
filename: 'Filename',
|
||||
size: 'Size',
|
||||
date: 'Date',
|
||||
code: 'Code',
|
||||
actions: 'Actions',
|
||||
download: 'Download',
|
||||
viewDetails: 'View Details',
|
||||
deleteRecord: 'Delete Record',
|
||||
preview: 'Preview',
|
||||
copyContent: 'Copy Content',
|
||||
contentCopied: 'Content copied to clipboard',
|
||||
copyFailed: 'Copy failed, please try again'
|
||||
},
|
||||
|
||||
// File Detail Modal
|
||||
fileDetail: {
|
||||
title: 'File Details',
|
||||
content: 'File Content',
|
||||
previewContent: 'Preview Content',
|
||||
download: 'Click to Download',
|
||||
qrCode: 'Retrieve QR Code',
|
||||
scanQrCode: 'Scan QR code for quick retrieval'
|
||||
},
|
||||
|
||||
// Content Preview Modal
|
||||
contentPreview: {
|
||||
title: 'Content Preview'
|
||||
},
|
||||
|
||||
// File Management
|
||||
fileManage: {
|
||||
title: 'File Management',
|
||||
searchPlaceholder: 'Search file name, description...',
|
||||
allFiles: 'All Files',
|
||||
editFileInfo: 'Edit File Information',
|
||||
saveChanges: 'Save Changes',
|
||||
headers: {
|
||||
code: 'Retrieve Code',
|
||||
name: 'Name',
|
||||
size: 'Size',
|
||||
description: 'Description',
|
||||
expiration: 'Expiration',
|
||||
actions: 'Actions'
|
||||
},
|
||||
form: {
|
||||
code: 'Retrieve Code',
|
||||
codePlaceholder: 'Enter retrieve code',
|
||||
filename: 'File Name',
|
||||
filenamePlaceholder: 'Enter file name',
|
||||
suffix: 'File Extension',
|
||||
suffixPlaceholder: 'Enter file extension',
|
||||
downloadLimit: 'Download Limit',
|
||||
downloadLimitPlaceholder: 'Enter download limit'
|
||||
}
|
||||
},
|
||||
|
||||
// Side drawer
|
||||
drawer: {
|
||||
noRecords: 'No records'
|
||||
},
|
||||
|
||||
// Admin pages
|
||||
manage: {
|
||||
settings: {
|
||||
title: 'System Settings',
|
||||
basicSettings: 'Basic Settings',
|
||||
websiteInfo: 'Website Basic Information',
|
||||
websiteName: 'Website Name',
|
||||
websiteDescription: 'Website Description',
|
||||
siteName: 'Site Name',
|
||||
adminPassword: 'Admin Password',
|
||||
passwordPlaceholder: 'Leave blank to keep current password',
|
||||
passwordNote: 'Leave blank to keep unchanged',
|
||||
keywords: 'Keywords',
|
||||
themeSelection: 'Theme Selection',
|
||||
notificationSettings: 'Notification Settings',
|
||||
notificationTitle: 'Notification Title',
|
||||
notificationContent: 'Notification Content',
|
||||
storageSettings: 'Storage Settings',
|
||||
storagePath: 'Storage Path',
|
||||
storagePathPlaceholder: 'Leave blank to use default path, optional',
|
||||
storageMethod: 'Storage Method',
|
||||
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)',
|
||||
enabled: 'Enabled',
|
||||
disabled: 'Disabled',
|
||||
webdavUrl: 'Please enter Webdav URL',
|
||||
webdavUsername: 'Please enter Webdav Username',
|
||||
webdavPassword: 'Please enter Webdav Password',
|
||||
webdavUrlPlaceholder: 'Please enter Webdav URL',
|
||||
webdavUsernamePlaceholder: 'Please enter Webdav Username',
|
||||
webdavPasswordPlaceholder: 'Please enter Webdav Password',
|
||||
enableProxy: 'Enable Proxy',
|
||||
uploadLimits: 'Upload Limits',
|
||||
uploadRateLimit: 'Upload Rate Limit',
|
||||
uploadPerMinute: 'Upload Per Minute',
|
||||
minute: 'minute',
|
||||
uploadCountLimit: 'Upload Count Limit',
|
||||
files: 'files',
|
||||
fileSizeLimit: 'File Size Limit',
|
||||
expirationMethod: 'Expiration Method',
|
||||
expirationType: 'Expiration Type',
|
||||
expiration: {
|
||||
day: 'By Day',
|
||||
hour: 'By Hour',
|
||||
minute: 'By Minute',
|
||||
forever: 'Forever',
|
||||
count: 'By Count'
|
||||
},
|
||||
maxSaveTime: 'Maximum Save Time',
|
||||
timeUnits: {
|
||||
second: 'second',
|
||||
minute: 'minute',
|
||||
hour: 'hour',
|
||||
day: 'day'
|
||||
},
|
||||
guestUpload: 'Guest Upload',
|
||||
errorLimits: 'Error Limits',
|
||||
errorRateLimit: 'Error Rate Limit',
|
||||
errorPerMinute: 'Error Per Minute',
|
||||
errorCountLimit: 'Error Count Limit',
|
||||
times: 'times',
|
||||
fileSizeUnits: {
|
||||
kb: 'KB',
|
||||
mb: 'MB',
|
||||
gb: 'GB'
|
||||
},
|
||||
saveChanges: 'Save Changes'
|
||||
},
|
||||
systemSettings: {
|
||||
title: 'System Settings',
|
||||
basicSettings: 'Basic Settings',
|
||||
websiteInfo: 'Website Basic Information',
|
||||
websiteName: 'Website Name',
|
||||
websiteDescription: 'Website Description',
|
||||
adminPassword: 'Admin Password',
|
||||
passwordPlaceholder: 'Leave blank to keep current password',
|
||||
passwordNote: 'Leave blank to keep unchanged',
|
||||
keywords: 'Keywords',
|
||||
themeSelection: 'Theme Selection',
|
||||
notificationSettings: 'Notification Settings',
|
||||
notificationTitle: 'Notification Title',
|
||||
notificationContent: 'Notification Content',
|
||||
storageSettings: 'Storage Settings',
|
||||
storagePath: 'Storage Path',
|
||||
storagePathPlaceholder: 'Leave blank to use default path, optional',
|
||||
storageMethod: 'Storage Method',
|
||||
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)',
|
||||
enabled: 'Enabled',
|
||||
disabled: 'Disabled',
|
||||
webdavUrl: 'Please enter Webdav URL',
|
||||
webdavUsername: 'Please enter Webdav Username',
|
||||
webdavPassword: 'Please enter Webdav Password',
|
||||
enableProxy: 'Enable Proxy',
|
||||
uploadLimits: 'Upload Limits',
|
||||
uploadRateLimit: 'Upload Rate Limit',
|
||||
minute: 'minute',
|
||||
uploadCountLimit: 'Upload Count Limit',
|
||||
files: 'files',
|
||||
fileSizeLimit: 'File Size Limit',
|
||||
expirationMethod: 'Expiration Method',
|
||||
expirationMethods: {
|
||||
day: 'By Day',
|
||||
hour: 'By Hour',
|
||||
minute: 'By Minute',
|
||||
forever: 'Forever',
|
||||
count: 'By Count'
|
||||
},
|
||||
expiration: {
|
||||
day: 'By Day',
|
||||
hour: 'By Hour',
|
||||
minute: 'By Minute',
|
||||
forever: 'Forever',
|
||||
count: 'By Count'
|
||||
},
|
||||
maxSaveTime: 'Maximum Save Time',
|
||||
timeUnits: {
|
||||
second: 'second',
|
||||
minute: 'minute',
|
||||
hour: 'hour',
|
||||
day: 'day'
|
||||
},
|
||||
guestUpload: 'Guest Upload',
|
||||
errorLimits: 'Error Limits',
|
||||
errorRateLimit: 'Error Rate Limit',
|
||||
errorCountLimit: 'Error Count Limit',
|
||||
times: 'times',
|
||||
saveSettings: 'Save Settings',
|
||||
saveSuccess: 'Save successful',
|
||||
saveFailed: 'Save failed',
|
||||
getConfigFailed: 'Failed to get configuration'
|
||||
},
|
||||
dashboard: {
|
||||
title: 'Dashboard',
|
||||
totalFiles: 'Total Files',
|
||||
storageSpace: 'Storage Space',
|
||||
activeUsers: 'Active Users',
|
||||
systemStatus: 'System Status',
|
||||
yesterday: 'Yesterday:',
|
||||
today: 'Today:',
|
||||
weeklyChange: '↓ 5% from last week',
|
||||
normal: 'Normal',
|
||||
serverUptime: 'Server Uptime:',
|
||||
version: 'Version v2.2.1 Updated: 2025-09-04'
|
||||
},
|
||||
fileManage: {
|
||||
title: 'File Management',
|
||||
searchPlaceholder: 'Search file name, description...',
|
||||
allFiles: 'All Files',
|
||||
editFileInfo: 'Edit File Info',
|
||||
saveChanges: 'Save Changes',
|
||||
headers: {
|
||||
code: 'Code',
|
||||
name: 'Name',
|
||||
size: 'Size',
|
||||
description: 'Description',
|
||||
expiration: 'Expiration',
|
||||
actions: 'Actions'
|
||||
},
|
||||
form: {
|
||||
code: 'Code',
|
||||
codePlaceholder: 'Enter code',
|
||||
filename: 'File Name',
|
||||
filenamePlaceholder: 'Enter file name',
|
||||
suffix: 'File Suffix',
|
||||
suffixPlaceholder: 'Enter file suffix',
|
||||
downloadLimit: 'Download Limit',
|
||||
downloadLimitPlaceholder: 'Enter download limit'
|
||||
},
|
||||
updateFailed: 'Update failed',
|
||||
deleteFailed: 'Delete failed',
|
||||
loadFileListFailed: 'Failed to load file list'
|
||||
},
|
||||
login: {
|
||||
title: 'Login',
|
||||
password: 'Password',
|
||||
passwordPlaceholder: 'Password',
|
||||
loginButton: 'Login',
|
||||
loggingIn: 'Logging in...',
|
||||
invalidPassword: 'Invalid password',
|
||||
passwordTooShort: 'Password must be at least 6 characters',
|
||||
loginFailed: 'Login failed',
|
||||
noValidToken: 'Login failed: No valid token received'
|
||||
}
|
||||
},
|
||||
|
||||
// File size units
|
||||
fileSize: {
|
||||
bytes: 'Bytes',
|
||||
kb: 'KB',
|
||||
mb: 'MB',
|
||||
gb: 'GB',
|
||||
tb: 'TB'
|
||||
},
|
||||
|
||||
// Utility functions
|
||||
utils: {
|
||||
// Time formatting
|
||||
formatTime: 'Format timestamp to readable format',
|
||||
formatFileSize: 'Format file size',
|
||||
formatDuration: 'Format duration',
|
||||
time: {
|
||||
forever: 'Forever',
|
||||
day: 'day',
|
||||
hour: 'hour',
|
||||
minute: 'minute',
|
||||
second: 'second'
|
||||
},
|
||||
copyToClipboard: 'Copy text to clipboard',
|
||||
copyFailed: 'Copy failed:',
|
||||
debounce: 'Debounce function',
|
||||
throttle: 'Throttle function',
|
||||
validateEmail: 'Validate email format',
|
||||
validateUrl: 'Validate URL format',
|
||||
generateRandomString: 'Generate random string',
|
||||
deepClone: 'Deep clone object',
|
||||
getFileExtension: 'Get file extension',
|
||||
isMobileDevice: 'Check if mobile device',
|
||||
formatNumber: 'Format number with thousand separators',
|
||||
clipboard: {
|
||||
title: 'Clipboard utility functions',
|
||||
copyText: 'Copy text to clipboard',
|
||||
copySuccess: 'Copy successful',
|
||||
copyFailed: 'Copy failed, please copy manually'
|
||||
}
|
||||
},
|
||||
|
||||
// Components
|
||||
components: {
|
||||
pagination: {
|
||||
showing: 'Showing',
|
||||
to: 'to',
|
||||
of: 'of',
|
||||
total: 'total',
|
||||
previous: 'Previous',
|
||||
next: 'Next'
|
||||
},
|
||||
languageSwitcher: {
|
||||
clickOutsideToClose: 'Click outside to close dropdown menu'
|
||||
},
|
||||
borderProgressBar: {
|
||||
borderWidth: 'Border width',
|
||||
cornerRadius: 'Corner radius',
|
||||
createGradient: 'Create gradient',
|
||||
drawBackground: 'Draw background',
|
||||
calculateProgress: 'Calculate progress',
|
||||
drawProgress: 'Draw progress'
|
||||
}
|
||||
},
|
||||
|
||||
// Miscellaneous
|
||||
misc: {
|
||||
emptyFileError: 'Cannot read empty file',
|
||||
fileAddedFromClipboard: 'File added from clipboard: ',
|
||||
fileProcessFailed: 'File processing failed',
|
||||
chunkSize: 'Keep 2MB chunk size for hash calculation',
|
||||
secureContext: 'If not in secure context (HTTP), return an alternative hash based on file info',
|
||||
cryptoFallback: 'If crypto.subtle.digest fails, use fallback method',
|
||||
generateAlternativeHash: 'Function to generate alternative hash',
|
||||
fileInfoHash: 'Generate simple hash using filename, size and last modified time',
|
||||
convertToHex: 'Convert to hex string and pad to 64 bits',
|
||||
defaultChunkSize: 'Default chunk size is 5MB',
|
||||
initChunkUpload: '1. Initialize chunk upload',
|
||||
uploadChunk: '2. Upload chunk',
|
||||
completeUpload: '3. Complete upload',
|
||||
chunkUploadFailed: 'Chunk upload failed: ',
|
||||
uploadProgressListener: 'Add upload progress listener',
|
||||
noLimitCheck: 'If no limit, return true directly',
|
||||
expirationValidation: 'Add expiration time validation',
|
||||
chunkUploadReplacement: 'Use chunk upload to replace direct upload',
|
||||
textUploadUnchanged: 'Text upload remains unchanged',
|
||||
addSendRecord: 'Add new send record',
|
||||
permanent: 'Permanent',
|
||||
sendSuccessMessage: 'Show send success message',
|
||||
resetForm: 'Reset form - only reset file and text content, keep expiration info',
|
||||
showDetails: 'Show details',
|
||||
autoCopyLink: 'Auto copy retrieve code link',
|
||||
delayedLoading: 'Use onMounted hook to delay load non-critical resources or initialization',
|
||||
nonCriticalInit: 'Place non-immediately needed initialization code here'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,604 @@
|
||||
export default {
|
||||
// 通用
|
||||
common: {
|
||||
confirm: '确认',
|
||||
cancel: '取消',
|
||||
close: '关闭',
|
||||
delete: '删除',
|
||||
edit: '编辑',
|
||||
save: '保存',
|
||||
add: '添加',
|
||||
back: '返回',
|
||||
next: '下一页',
|
||||
previous: '上一页',
|
||||
loading: '加载中...',
|
||||
success: '成功',
|
||||
error: '错误',
|
||||
warning: '警告',
|
||||
info: '信息',
|
||||
search: '搜索',
|
||||
copy: '复制',
|
||||
uploadSuccess: '上传成功',
|
||||
uploadFailed: '上传失败',
|
||||
copySuccess: '复制成功',
|
||||
copyFailed: '复制失败',
|
||||
deleteSuccess: '删除成功',
|
||||
deleteFailed: '删除失败',
|
||||
downloadSuccess: '下载成功',
|
||||
downloadFailed: '下载失败',
|
||||
shareSuccess: '分享成功',
|
||||
shareFailed: '分享失败',
|
||||
expiredFile: '文件已过期',
|
||||
fileNotFound: '文件不存在',
|
||||
networkError: '网络错误',
|
||||
unknownError: '未知错误',
|
||||
invalidFileType: '不支持的文件类型',
|
||||
fileTooLarge: '文件过大',
|
||||
uploadCanceled: '上传已取消',
|
||||
processing: '处理中...',
|
||||
pleaseWait: '请稍候...',
|
||||
fileDetails: '文件详情',
|
||||
enabled: '已开启',
|
||||
disabled: '已关闭',
|
||||
minute: '分钟',
|
||||
files: '个文件',
|
||||
second: '秒',
|
||||
hour: '小时',
|
||||
day: '天',
|
||||
times: '次',
|
||||
appName: '文件快递柜 - FileCodeBox',
|
||||
appDescription: '开箱即用的文件快传系统'
|
||||
},
|
||||
|
||||
// 管理员模块
|
||||
admin: {
|
||||
dashboard: {
|
||||
title: '仪表盘',
|
||||
totalFiles: '总文件数',
|
||||
storageSpace: '存储空间',
|
||||
activeUsers: '活跃用户',
|
||||
systemStatus: '系统状态',
|
||||
yesterday: '昨天:',
|
||||
today: '今天:',
|
||||
weeklyChange: '↓ 5% 较上周',
|
||||
normal: '正常',
|
||||
serverUptime: '服务器运行时间:',
|
||||
version: '版本 v2.2.1 更新时间:2025-09-04'
|
||||
},
|
||||
fileManage: {
|
||||
title: '文件管理'
|
||||
},
|
||||
settings: {
|
||||
title: '系统设置',
|
||||
basicSettings: '基本设置',
|
||||
websiteInfo: '网站基本信息',
|
||||
websiteName: '网站名称',
|
||||
websiteDescription: '网站描述',
|
||||
siteName: '网站名称',
|
||||
adminPassword: '管理员密码',
|
||||
passwordPlaceholder: '留空则不修改密码',
|
||||
passwordNote: '留空则不修改',
|
||||
keywords: '关键词',
|
||||
themeSelection: '主题选择',
|
||||
robotsFile: 'Robots.txt',
|
||||
notificationSettings: '通知设置',
|
||||
notificationTitle: '通知标题',
|
||||
notificationContent: '通知内容',
|
||||
storageSettings: '存储设置',
|
||||
storagePath: '存储路径',
|
||||
storagePathPlaceholder: '留空则使用默认路径,可不填写',
|
||||
storageMethod: '存储方式',
|
||||
localStorage: '本地存储',
|
||||
s3Storage: 'S3 存储',
|
||||
webdavStorage: 'Webdav 存储',
|
||||
chunkUploadNote: '开启切片上传(实验性功能,还在开发中,目前仅本地存储可用,可能会出现未知问题)',
|
||||
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',
|
||||
enabled: '已开启',
|
||||
disabled: '已关闭',
|
||||
webdavUrl: '请输入 Webdav URL',
|
||||
webdavUsername: '请输入 Webdav Username',
|
||||
webdavPassword: '请输入 Webdav Password',
|
||||
webdavUrlPlaceholder: '请输入 Webdav URL',
|
||||
webdavUsernamePlaceholder: '请输入 Webdav Username',
|
||||
webdavPasswordPlaceholder: '请输入 Webdav Password',
|
||||
enableProxy: '启用代理',
|
||||
uploadLimits: '上传限制',
|
||||
uploadRateLimit: '每分钟上传限制',
|
||||
uploadPerMinute: '每分钟上传限制',
|
||||
minute: '分钟',
|
||||
uploadCountLimit: '上传数量限制',
|
||||
files: '个文件',
|
||||
fileSizeLimit: '文件大小限制',
|
||||
expirationMethod: '过期方式',
|
||||
expirationType: '过期类型',
|
||||
expiration: {
|
||||
day: '按天',
|
||||
hour: '按小时',
|
||||
minute: '按分钟',
|
||||
forever: '永久',
|
||||
count: '按次数'
|
||||
},
|
||||
maxSaveTime: '最长保存时间',
|
||||
timeUnits: {
|
||||
second: '秒',
|
||||
minute: '分',
|
||||
hour: '时',
|
||||
day: '天'
|
||||
},
|
||||
guestUpload: '游客上传',
|
||||
errorLimits: '错误限制',
|
||||
errorRateLimit: '每分钟错误限制',
|
||||
errorPerMinute: '每分钟错误限制',
|
||||
errorCountLimit: '错误次数限制',
|
||||
times: '次',
|
||||
saveChanges: '保存更改',
|
||||
fileSizeUnits: {
|
||||
kb: 'KB',
|
||||
mb: 'MB',
|
||||
gb: 'GB'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 页面标题和导航
|
||||
nav: {
|
||||
sendFile: '发送文件',
|
||||
retrieveFile: '取件',
|
||||
fileRecords: '取件记录'
|
||||
},
|
||||
|
||||
// 取件页面
|
||||
retrieve: {
|
||||
title: '文件取件',
|
||||
codeInput: {
|
||||
placeholder: '请输入5位取件码',
|
||||
label: '取件码'
|
||||
},
|
||||
submit: '取件',
|
||||
needSendFile: '需要发送文件?点击这里',
|
||||
recordsDrawer: '取件记录',
|
||||
messages: {
|
||||
invalidCode: '请输入5位取件码',
|
||||
retrieveSuccess: '文件获取成功',
|
||||
invalidCodeError: '无效的取件码',
|
||||
retrieveFailure: '获取文件失败:',
|
||||
networkError: '取件失败,请稍后重试:',
|
||||
unknownError: '未知错误'
|
||||
}
|
||||
},
|
||||
|
||||
// 发送页面
|
||||
send: {
|
||||
title: '文件发送',
|
||||
sendText: '发送文本',
|
||||
fileDetails: '文件详情',
|
||||
expirationMethod: '过期方式',
|
||||
uploadArea: {
|
||||
dragText: '拖拽文件到此处或',
|
||||
clickText: '点击选择文件',
|
||||
textInput: '在此输入要发送的文本...',
|
||||
placeholder: '点击或拖放文件到此处上传',
|
||||
description: '支持各种常见格式'
|
||||
},
|
||||
submit: '安全寄送',
|
||||
needRetrieveFile: '需要取件?点击这里',
|
||||
sendRecords: '发件记录',
|
||||
secureEncryption: '安全加密',
|
||||
fileDetail: {
|
||||
title: '文件详情',
|
||||
content: '文件内容',
|
||||
previewContent: '预览内容',
|
||||
download: '点击下载',
|
||||
qrCode: '取件二维码',
|
||||
scanQrCode: '扫描二维码快速取件'
|
||||
},
|
||||
contentPreview: {
|
||||
title: '内容预览'
|
||||
},
|
||||
fileManage: {
|
||||
title: '文件管理',
|
||||
searchPlaceholder: '搜索文件名称、描述...',
|
||||
allFiles: '所有文件',
|
||||
editFileInfo: '编辑文件信息',
|
||||
saveChanges: '保存更改',
|
||||
headers: {
|
||||
code: '取件码',
|
||||
name: '名称',
|
||||
size: '大小',
|
||||
description: '描述',
|
||||
expiration: '过期时间',
|
||||
actions: '操作'
|
||||
},
|
||||
form: {
|
||||
code: '取件码',
|
||||
codePlaceholder: '输入取件码',
|
||||
filename: '文件名称',
|
||||
filenamePlaceholder: '输入文件名称',
|
||||
suffix: '文件后缀',
|
||||
suffixPlaceholder: '输入文件后缀',
|
||||
downloadLimit: '下载次数限制',
|
||||
downloadLimitPlaceholder: '输入下载次数限制'
|
||||
}
|
||||
},
|
||||
expiration: {
|
||||
label: '过期时间',
|
||||
placeholders: {
|
||||
days: '输入天数',
|
||||
hours: '输入小时数',
|
||||
minutes: '输入分钟数',
|
||||
count: '输入查看次数',
|
||||
forever: '永久',
|
||||
default: '输入值'
|
||||
},
|
||||
units: {
|
||||
days: '天',
|
||||
hours: '小时',
|
||||
minutes: '分钟',
|
||||
times: '次',
|
||||
forever: '永久'
|
||||
}
|
||||
},
|
||||
time: {
|
||||
day: '按天',
|
||||
hour: '按小时',
|
||||
minute: '按分钟',
|
||||
forever: '永久',
|
||||
count: '按次数'
|
||||
},
|
||||
messages: {
|
||||
selectFile: '请选择要上传的文件',
|
||||
enterText: '请输入要发送的文本',
|
||||
enterExpirationValue: '请输入过期值',
|
||||
expirationTooLong: '过期时间不能超过{days}天',
|
||||
sendSuccess: '文件发送成功!取件码:{code}',
|
||||
initChunkUploadFailed: '初始化切片上传失败',
|
||||
chunkUploadFailed: '切片 {index} 上传失败',
|
||||
completeUploadFailed: '完成上传失败',
|
||||
uploadFailed: '上传失败,请稍后重试',
|
||||
guestUploadDisabled: '游客上传功能已关闭',
|
||||
fileSizeExceeded: '文件大小超过限制 ({size})',
|
||||
serverError: '服务器响应异常',
|
||||
sendFailed: '发送失败,请稍后重试',
|
||||
expiresAfterCount: '{count}次后过期',
|
||||
expiresAt: '{date}过期',
|
||||
emptyFileError: '无法读取空文件',
|
||||
fileAddedFromClipboard: '已从剪贴板添加文件:{filename}',
|
||||
fileProcessingFailed: '文件处理失败',
|
||||
expiresAfter: '{value}{unit}后过期'
|
||||
}
|
||||
},
|
||||
|
||||
// 文件管理
|
||||
fileManage: {
|
||||
title: '文件管理',
|
||||
searchPlaceholder: '搜索文件名称、描述...',
|
||||
allFiles: '所有文件',
|
||||
editFileInfo: '编辑文件信息',
|
||||
saveChanges: '保存更改',
|
||||
headers: {
|
||||
code: '取件码',
|
||||
name: '名称',
|
||||
size: '大小',
|
||||
description: '描述',
|
||||
expiration: '过期时间',
|
||||
actions: '操作'
|
||||
},
|
||||
form: {
|
||||
code: '取件码',
|
||||
codePlaceholder: '输入取件码',
|
||||
filename: '文件名称',
|
||||
filenamePlaceholder: '输入文件名称',
|
||||
suffix: '文件后缀',
|
||||
suffixPlaceholder: '输入文件后缀',
|
||||
downloadLimit: '下载次数限制',
|
||||
downloadLimitPlaceholder: '输入下载次数限制'
|
||||
}
|
||||
},
|
||||
|
||||
// 文件记录
|
||||
fileRecord: {
|
||||
filename: '文件名',
|
||||
size: '文件大小',
|
||||
date: '取件日期',
|
||||
code: '取件码',
|
||||
actions: '操作',
|
||||
download: '下载',
|
||||
viewDetails: '查看详情',
|
||||
deleteRecord: '删除记录',
|
||||
preview: '预览',
|
||||
copyContent: '复制内容',
|
||||
contentCopied: '内容已复制到剪贴板',
|
||||
copyFailed: '复制失败,请重试'
|
||||
},
|
||||
|
||||
// 文件详情模态框
|
||||
fileDetail: {
|
||||
title: '文件详情',
|
||||
content: '文件内容',
|
||||
previewContent: '预览内容',
|
||||
download: '点击下载',
|
||||
qrCode: '取件二维码',
|
||||
scanQrCode: '扫描二维码快速取件'
|
||||
},
|
||||
|
||||
// 内容预览
|
||||
contentPreview: {
|
||||
title: '内容预览'
|
||||
},
|
||||
|
||||
// 侧边抽屉
|
||||
drawer: {
|
||||
noRecords: '暂无记录'
|
||||
},
|
||||
|
||||
// 文件大小单位
|
||||
fileSize: {
|
||||
bytes: 'Bytes',
|
||||
kb: 'KB',
|
||||
mb: 'MB',
|
||||
gb: 'GB',
|
||||
tb: 'TB'
|
||||
},
|
||||
|
||||
// 管理页面
|
||||
manage: {
|
||||
settings: {
|
||||
title: '系统设置',
|
||||
basicSettings: '基本设置',
|
||||
siteName: '网站名称',
|
||||
adminPassword: '管理员密码',
|
||||
passwordPlaceholder: '留空则不修改密码',
|
||||
passwordNote: '留空则不修改',
|
||||
keywords: '关键词',
|
||||
themeSelection: '主题选择',
|
||||
robotsFile: 'Robots.txt',
|
||||
notificationSettings: '通知设置',
|
||||
notificationTitle: '通知标题',
|
||||
notificationContent: '通知内容',
|
||||
storageSettings: '存储设置',
|
||||
storagePath: '存储路径',
|
||||
storagePathPlaceholder: '留空则使用默认路径,可不填写',
|
||||
storageMethod: '存储方式',
|
||||
localStorage: '本地存储',
|
||||
chunkUploadNote: '开启切片上传(实验性功能,还在开发中,目前仅本地存储可用,可能会出现未知问题)',
|
||||
uploadLimits: '上传限制',
|
||||
uploadPerMinute: '每分钟上传限制',
|
||||
uploadCountLimit: '上传数量限制',
|
||||
fileSizeLimit: '文件大小限制',
|
||||
expiration: {
|
||||
day: '按天',
|
||||
hour: '按小时',
|
||||
minute: '按分钟',
|
||||
forever: '永久',
|
||||
count: '按次数'
|
||||
},
|
||||
maxSaveTime: '最长保存时间',
|
||||
s3SecretAccessKey: 'S3 SecretAccessKey',
|
||||
fileSizeUnits: {
|
||||
kb: 'KB',
|
||||
mb: 'MB',
|
||||
gb: 'GB'
|
||||
},
|
||||
expirationType: '过期类型',
|
||||
guestUpload: '游客上传',
|
||||
errorLimits: '错误限制',
|
||||
errorPerMinute: '每分钟错误限制',
|
||||
errorCountLimit: '错误次数限制',
|
||||
saveChanges: '保存更改'
|
||||
},
|
||||
dashboard: {
|
||||
title: '仪表盘',
|
||||
totalFiles: '总文件数',
|
||||
storageSpace: '存储空间',
|
||||
activeUsers: '活跃用户',
|
||||
systemStatus: '系统状态',
|
||||
yesterday: '昨天:',
|
||||
today: '今天:',
|
||||
weeklyChange: '↓ 5% 较上周',
|
||||
normal: '正常',
|
||||
serverUptime: '服务器运行时间:',
|
||||
version: '版本 v2.2.1 更新时间:2025-09-04'
|
||||
},
|
||||
fileManage: {
|
||||
title: '文件管理',
|
||||
searchPlaceholder: '搜索文件名称、描述...',
|
||||
allFiles: '所有文件',
|
||||
editFileInfo: '编辑文件信息',
|
||||
saveChanges: '保存更改',
|
||||
headers: {
|
||||
code: '取件码',
|
||||
name: '名称',
|
||||
size: '大小',
|
||||
description: '描述',
|
||||
expiration: '过期时间',
|
||||
actions: '操作'
|
||||
},
|
||||
form: {
|
||||
code: '取件码',
|
||||
codePlaceholder: '输入取件码',
|
||||
filename: '文件名称',
|
||||
filenamePlaceholder: '输入文件名称',
|
||||
suffix: '文件后缀',
|
||||
suffixPlaceholder: '输入文件后缀',
|
||||
downloadLimit: '下载次数限制',
|
||||
downloadLimitPlaceholder: '输入下载次数限制'
|
||||
},
|
||||
updateFailed: '更新失败',
|
||||
deleteFailed: '删除失败',
|
||||
loadFileListFailed: '加载文件列表失败'
|
||||
},
|
||||
systemSettings: {
|
||||
title: '系统设置',
|
||||
basicSettings: '基本设置',
|
||||
websiteInfo: '网站基本信息',
|
||||
websiteName: '网站名称',
|
||||
websiteDescription: '网站描述',
|
||||
adminPassword: '管理员密码',
|
||||
passwordPlaceholder: '留空则不修改密码',
|
||||
passwordNote: '留空则不修改',
|
||||
keywords: '关键词',
|
||||
themeSelection: '主题选择',
|
||||
notificationSettings: '通知设置',
|
||||
notificationTitle: '通知标题',
|
||||
notificationContent: '通知内容',
|
||||
storageSettings: '存储设置',
|
||||
storagePath: '存储路径',
|
||||
storagePathPlaceholder: '留空则使用默认路径,可不填写',
|
||||
storageMethod: '存储方式',
|
||||
localStorage: '本地存储',
|
||||
s3Storage: 'S3 存储',
|
||||
webdavStorage: 'Webdav 存储',
|
||||
chunkUploadNote: '开启切片上传(实验性功能,还在开发中,目前仅本地存储可用,可能会出现未知问题)',
|
||||
enabled: '已开启',
|
||||
disabled: '已关闭',
|
||||
webdavUrl: '请输入 Webdav URL',
|
||||
webdavUsername: '请输入 Webdav Username',
|
||||
webdavPassword: '请输入 Webdav Password',
|
||||
enableProxy: '启用代理',
|
||||
uploadLimits: '上传限制',
|
||||
uploadRateLimit: '每分钟上传限制',
|
||||
minute: '分钟',
|
||||
uploadCountLimit: '上传数量限制',
|
||||
files: '个文件',
|
||||
fileSizeLimit: '文件大小限制',
|
||||
expirationMethod: '过期方式',
|
||||
expirationMethods: {
|
||||
day: '按天',
|
||||
hour: '按小时',
|
||||
minute: '按分钟',
|
||||
forever: '永久',
|
||||
count: '按次数'
|
||||
},
|
||||
expiration: {
|
||||
day: '按天',
|
||||
hour: '按小时',
|
||||
minute: '按分钟',
|
||||
forever: '永久',
|
||||
count: '按次数'
|
||||
},
|
||||
maxSaveTime: '最长保存时间',
|
||||
timeUnits: {
|
||||
second: '秒',
|
||||
minute: '分',
|
||||
hour: '时',
|
||||
day: '天'
|
||||
},
|
||||
guestUpload: '游客上传',
|
||||
errorLimits: '错误限制',
|
||||
errorRateLimit: '每分钟错误限制',
|
||||
errorCountLimit: '错误次数限制',
|
||||
times: '次',
|
||||
saveSettings: '保存设置',
|
||||
saveSuccess: '保存成功',
|
||||
saveFailed: '保存失败',
|
||||
getConfigFailed: '获取配置失败'
|
||||
},
|
||||
login: {
|
||||
title: '登录',
|
||||
password: '密码',
|
||||
passwordPlaceholder: '密码',
|
||||
loginButton: '登录',
|
||||
loggingIn: '登录中...',
|
||||
invalidPassword: '无效的密码',
|
||||
passwordTooShort: '密码长度至少为6位',
|
||||
loginFailed: '登录失败',
|
||||
noValidToken: '登录失败:未获取到有效令牌'
|
||||
}
|
||||
},
|
||||
|
||||
// 工具函数相关
|
||||
utils: {
|
||||
common: {
|
||||
formatTime: '格式化时间戳为可读格式',
|
||||
formatFileSize: '格式化文件大小',
|
||||
formatDuration: '格式化持续时间',
|
||||
time: {
|
||||
forever: '永久',
|
||||
day: '天',
|
||||
hour: '小时',
|
||||
minute: '分钟',
|
||||
second: '秒'
|
||||
},
|
||||
copyToClipboard: '复制文本到剪贴板',
|
||||
copyFailed: '复制失败:',
|
||||
debounce: '防抖函数',
|
||||
throttle: '节流函数',
|
||||
validateEmail: '验证邮箱格式',
|
||||
validateUrl: '验证URL格式',
|
||||
generateRandomString: '生成随机字符串',
|
||||
deepClone: '深度克隆对象',
|
||||
getFileExtension: '获取文件扩展名',
|
||||
isMobileDevice: '检查是否为移动设备',
|
||||
formatNumber: '格式化数字,添加千分位分隔符'
|
||||
},
|
||||
clipboard: {
|
||||
title: '剪贴板工具函数',
|
||||
copyText: '复制文本到剪贴板',
|
||||
copySuccess: '复制成功',
|
||||
copyFailed: '复制失败,请手动复制'
|
||||
}
|
||||
},
|
||||
|
||||
// 组件相关
|
||||
components: {
|
||||
pagination: {
|
||||
showing: '显示第',
|
||||
to: '到',
|
||||
of: '条,共',
|
||||
total: '条',
|
||||
previous: '上一页',
|
||||
next: '下一页'
|
||||
},
|
||||
languageSwitcher: {
|
||||
clickOutsideToClose: '点击外部关闭下拉菜单'
|
||||
},
|
||||
borderProgressBar: {
|
||||
borderWidth: '边框宽度',
|
||||
cornerRadius: '拐角半径',
|
||||
createGradient: '创建渐变',
|
||||
drawBackground: '绘制背景',
|
||||
calculateProgress: '计算进度',
|
||||
drawProgress: '绘制进度'
|
||||
}
|
||||
},
|
||||
|
||||
// 其他通用文本
|
||||
misc: {
|
||||
emptyFileError: '无法读取空文件',
|
||||
fileAddedFromClipboard: '已从剪贴板添加文件:',
|
||||
fileProcessFailed: '文件处理失败',
|
||||
chunkSize: '保持 2MB 的切片大小用于计算哈希',
|
||||
secureContext: '如果不是安全上下文(HTTP),则返回一个基于文件信息的替代哈希',
|
||||
cryptoFallback: '如果 crypto.subtle.digest 失败,使用替代方案',
|
||||
generateAlternativeHash: '生成替代哈希的函数',
|
||||
fileInfoHash: '使用文件名、大小和最后修改时间生成一个简单的哈希',
|
||||
convertToHex: '转换为16进制字符串并填充到64位',
|
||||
defaultChunkSize: '默认切片大小为5MB',
|
||||
initChunkUpload: '1. 初始化切片上传',
|
||||
uploadChunk: '2. 上传切片',
|
||||
completeUpload: '3. 完成上传',
|
||||
chunkUploadFailed: '切片上传失败:',
|
||||
uploadProgressListener: '添加上传进度监听',
|
||||
noLimitCheck: '如果没有限制,直接返回true',
|
||||
expirationValidation: '添加过期时间验证',
|
||||
chunkUploadReplacement: '使用切片上传替代原来的直接上传',
|
||||
textUploadUnchanged: '文本上传保持不变',
|
||||
addSendRecord: '添加新的发送记录',
|
||||
permanent: '永久',
|
||||
sendSuccessMessage: '显示发送成功消息',
|
||||
resetForm: '重置表单 - 只重置文件和文本内容,保留过期信息',
|
||||
showDetails: '显示详情',
|
||||
autoCopyLink: '自动复制取件码链接',
|
||||
delayedLoading: '使用 onMounted 钩子延迟加载一些非关键资源或初始化',
|
||||
nonCriticalInit: '这里可以放置一些非立即需要的初始化代码'
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<h1 @click="router.push('/')" class="ml-2 text-xl font-semibold cursor-pointer" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
|
||||
FileCodeBox
|
||||
{{ t('common.appName') }}
|
||||
</h1>
|
||||
</div>
|
||||
<button @click="toggleSidebar" class="lg:hidden">
|
||||
@@ -75,6 +75,7 @@
|
||||
import { ref, inject, onMounted, onUnmounted } from 'vue'
|
||||
import { BoxIcon, MenuIcon, XIcon, FolderIcon, CogIcon, LayoutDashboardIcon } from 'lucide-vue-next'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
interface MenuItem {
|
||||
id: string
|
||||
@@ -84,11 +85,12 @@ interface MenuItem {
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
const menuItems: MenuItem[] = [
|
||||
{ id: 'Dashboard', name: '仪表盘', icon: LayoutDashboardIcon, redirect: '/admin/dashboard' },
|
||||
{ id: 'FileManage', name: '文件管理', icon: FolderIcon, redirect: '/admin/files' },
|
||||
{ id: 'Settings', name: '系统设置', icon: CogIcon, redirect: '/admin/settings' }
|
||||
{ id: 'Dashboard', name: t('admin.dashboard.title'), icon: LayoutDashboardIcon, redirect: '/admin/dashboard' },
|
||||
{ id: 'FileManage', name: t('admin.fileManage.title'), icon: FolderIcon, redirect: '/admin/files' },
|
||||
{ id: 'Settings', name: t('admin.settings.title'), icon: CogIcon, redirect: '/admin/settings' }
|
||||
]
|
||||
|
||||
const isSidebarOpen = ref(true)
|
||||
|
||||
@@ -5,10 +5,12 @@ import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import i18n from './i18n'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(i18n)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
+12
-9
@@ -49,26 +49,29 @@ export function formatFileSize(bytes: number, decimals: number = 2): string {
|
||||
/**
|
||||
* 格式化持续时间
|
||||
* @param seconds 秒数
|
||||
* @param t 翻译函数
|
||||
* @returns 格式化后的持续时间字符串
|
||||
*/
|
||||
export function formatDuration(seconds: number): string {
|
||||
if (seconds === 0) return '永久'
|
||||
export function formatDuration(seconds: number, t?: (key: string) => string): string {
|
||||
if (seconds === 0) return t ? t('utils.time.forever') : 'Forever'
|
||||
|
||||
const units = [
|
||||
{ name: '天', value: 86400 },
|
||||
{ name: '小时', value: 3600 },
|
||||
{ name: '分钟', value: 60 },
|
||||
{ name: '秒', value: 1 }
|
||||
{ key: 'utils.time.day', value: 86400 },
|
||||
{ key: 'utils.time.hour', value: 3600 },
|
||||
{ key: 'utils.time.minute', value: 60 },
|
||||
{ key: 'utils.time.second', value: 1 }
|
||||
]
|
||||
|
||||
for (const unit of units) {
|
||||
if (seconds >= unit.value) {
|
||||
const value = Math.floor(seconds / unit.value)
|
||||
return `${value}${unit.name}`
|
||||
const unitName = t ? t(unit.key) : unit.key.split('.').pop()
|
||||
return `${value}${unitName}`
|
||||
}
|
||||
}
|
||||
|
||||
return `${seconds}秒`
|
||||
const secondName = t ? t('utils.time.second') : 'second'
|
||||
return `${seconds}${secondName}`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,7 +99,7 @@ export async function copyToClipboard(text: string): Promise<boolean> {
|
||||
return result
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error)
|
||||
console.error('Copy failed:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,15 +22,15 @@
|
||||
/>
|
||||
</div>
|
||||
<PageFooter
|
||||
link-text="需要发送文件?点击这里"
|
||||
:link-text="$t('retrieve.needSendFile')"
|
||||
link-to="/send"
|
||||
drawer-text="取件记录"
|
||||
:drawer-text="$t('retrieve.recordsDrawer')"
|
||||
@toggle-drawer="toggleDrawer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SideDrawer :visible="showDrawer" title="取件记录" @close="toggleDrawer">
|
||||
<SideDrawer :visible="showDrawer" :title="$t('retrieve.recordsDrawer')" @close="toggleDrawer">
|
||||
<FileRecordList
|
||||
:records="records"
|
||||
@view-details="viewDetails"
|
||||
@@ -59,6 +59,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import PageHeader from '@/components/common/PageHeader.vue'
|
||||
import RetrieveForm from '@/components/common/RetrieveForm.vue'
|
||||
import PageFooter from '@/components/common/PageFooter.vue'
|
||||
@@ -101,6 +102,7 @@ import DOMPurify from 'dompurify'
|
||||
import { useAlertStore } from '@/stores/alertStore'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
|
||||
const { t } = useI18n()
|
||||
const alertStore = useAlertStore()
|
||||
const baseUrl = window.location.origin
|
||||
|
||||
@@ -153,14 +155,14 @@ const getDownloadUrl = (record: FileRecord) => {
|
||||
const copyContent = async () => {
|
||||
if (selectedRecord.value && selectedRecord.value.content) {
|
||||
await copyToClipboard(selectedRecord.value.content, {
|
||||
successMsg: '内容已复制到剪贴板',
|
||||
errorMsg: '复制失败,请重试'
|
||||
successMsg: t('fileRecord.contentCopied'),
|
||||
errorMsg: t('fileRecord.copyFailed')
|
||||
})
|
||||
}
|
||||
}
|
||||
const handleSubmit = async () => {
|
||||
if (code.value.length !== 5) {
|
||||
alertStore.showAlert('请输入5位取件码', 'error')
|
||||
alertStore.showAlert(t('retrieve.messages.invalidCode'), 'error')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -200,18 +202,18 @@ const handleSubmit = async () => {
|
||||
selectedRecord.value = newFileData
|
||||
showPreview.value = true
|
||||
}
|
||||
alertStore.showAlert('文件获取成功', 'success')
|
||||
alertStore.showAlert(t('retrieve.messages.retrieveSuccess'), 'success')
|
||||
} else {
|
||||
alertStore.showAlert('无效的取件码', 'error')
|
||||
alertStore.showAlert(t('retrieve.messages.invalidCodeError'), 'error')
|
||||
}
|
||||
} else {
|
||||
alertStore.showAlert('获取文件失败:' + res.detail, 'error')
|
||||
alertStore.showAlert(t('retrieve.messages.retrieveFailure') + res.detail, 'error')
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
console.error('取件失败:', err)
|
||||
console.error('Retrieve failed:', err)
|
||||
const error = err as { response?: { data?: { detail?: string } }; message?: string }
|
||||
const errorMessage = error?.response?.data?.detail || error?.message || '未知错误'
|
||||
alertStore.showAlert('取件失败,请稍后重试:' + errorMessage, 'error')
|
||||
const errorMessage = error?.response?.data?.detail || error?.message || t('retrieve.messages.unknownError')
|
||||
alertStore.showAlert(t('retrieve.messages.networkError') + errorMessage, 'error')
|
||||
} finally {
|
||||
inputStatus.value.readonly = false
|
||||
inputStatus.value.loading = false
|
||||
@@ -220,9 +222,9 @@ const handleSubmit = async () => {
|
||||
}
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
if (bytes === 0) return '0 ' + t('fileSize.bytes')
|
||||
const k = 1024
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
||||
const sizes = [t('fileSize.bytes'), t('fileSize.kb'), t('fileSize.mb'), t('fileSize.gb'), t('fileSize.tb')]
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
+42
-40
@@ -32,14 +32,14 @@
|
||||
<div v-else key="text" class="grid grid-cols-1 gap-8">
|
||||
<TextInputArea
|
||||
v-model="textContent"
|
||||
placeholder="在此输入要发送的文本..."
|
||||
:placeholder="t('send.uploadArea.textInput')"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
<!-- 过期方式选择 -->
|
||||
<div class="flex flex-col space-y-3">
|
||||
<label :class="['text-sm font-medium', isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
过期时间
|
||||
{{ t('send.expiration.label') }}
|
||||
</label>
|
||||
<div class="relative flex-grow group">
|
||||
<div
|
||||
@@ -173,13 +173,13 @@
|
||||
></span>
|
||||
<span class="relative z-10 flex items-center justify-center text-lg">
|
||||
<SendIcon class="w-6 h-6 mr-2" />
|
||||
<span>安全寄送</span>
|
||||
<span>{{ t('send.submit') }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
<div class="mt-6 text-center">
|
||||
<router-link to="/" class="text-indigo-400 hover:text-indigo-300 transition duration-300">
|
||||
需要取件?点击这里
|
||||
{{ t('send.needRetrieveFile') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,14 +193,14 @@
|
||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
|
||||
>
|
||||
<ShieldCheckIcon class="w-4 h-4 mr-1 text-green-400" />
|
||||
安全加密
|
||||
{{ t('send.secureEncryption') }}
|
||||
</span>
|
||||
<button
|
||||
@click="toggleDrawer"
|
||||
class="text-sm hover:text-indigo-300 transition duration-300 flex items-center"
|
||||
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
|
||||
>
|
||||
发件记录
|
||||
{{ t('send.sendRecords') }}
|
||||
<ClipboardListIcon class="w-4 h-4 ml-1" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -218,7 +218,7 @@
|
||||
:class="[isDarkMode ? 'border-gray-700' : 'border-gray-200']"
|
||||
>
|
||||
<h3 class="text-2xl font-bold" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
|
||||
发件记录
|
||||
{{ t('send.sendRecords') }}
|
||||
</h3>
|
||||
<button
|
||||
@click="toggleDrawer"
|
||||
@@ -315,7 +315,7 @@
|
||||
class="text-lg sm:text-xl font-semibold"
|
||||
:class="[isDarkMode ? 'text-white' : 'text-gray-900']"
|
||||
>
|
||||
文件详情
|
||||
{{ t('send.fileDetails') }}
|
||||
</h3>
|
||||
<button
|
||||
@click="selectedRecord = null"
|
||||
@@ -486,6 +486,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, onMounted, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
SendIcon,
|
||||
ClipboardListIcon,
|
||||
@@ -547,6 +548,7 @@ const uploadProgress = ref(0)
|
||||
const showDrawer = ref(false)
|
||||
const selectedRecord = ref<ShareRecord | null>(null)
|
||||
|
||||
const { t } = useI18n()
|
||||
const alertStore = useAlertStore()
|
||||
const sendRecords = computed(() => fileDataStore.shareData)
|
||||
|
||||
@@ -580,7 +582,7 @@ const handlePaste = async (event: ClipboardEvent) => {
|
||||
if (file) {
|
||||
// 检查文件是否为空
|
||||
if (file.size === 0) {
|
||||
alertStore.showAlert('无法读取空文件', 'error')
|
||||
alertStore.showAlert(t('send.messages.emptyFileError'), 'error')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -589,9 +591,9 @@ const handlePaste = async (event: ClipboardEvent) => {
|
||||
|
||||
try {
|
||||
fileHash.value = await calculateFileHash(file)
|
||||
alertStore.showAlert('已从剪贴板添加文件:' + file.name, 'success')
|
||||
alertStore.showAlert(t('send.messages.fileAddedFromClipboard', { filename: file.name }), 'success')
|
||||
} catch (err) {
|
||||
alertStore.showAlert('文件处理失败', 'error')
|
||||
alertStore.showAlert(t('send.messages.fileProcessingFailed'), 'error')
|
||||
console.error('File hash calculation failed:', err)
|
||||
}
|
||||
break
|
||||
@@ -708,32 +710,32 @@ const generateFallbackHash = (file: File): string => {
|
||||
const getPlaceholder = (value: string = expirationMethod.value) => {
|
||||
switch (value) {
|
||||
case 'day':
|
||||
return '输入天数'
|
||||
return t('send.expiration.placeholders.days')
|
||||
case 'hour':
|
||||
return '输入小时数'
|
||||
return t('send.expiration.placeholders.hours')
|
||||
case 'minute':
|
||||
return '输入分钟数'
|
||||
return t('send.expiration.placeholders.minutes')
|
||||
case 'count':
|
||||
return '输入查看次数'
|
||||
return t('send.expiration.placeholders.count')
|
||||
case 'forever':
|
||||
return '永久'
|
||||
return t('send.expiration.placeholders.forever')
|
||||
default:
|
||||
return '输入值'
|
||||
return t('send.expiration.placeholders.default')
|
||||
}
|
||||
}
|
||||
|
||||
const getUnit = (value: string = expirationMethod.value) => {
|
||||
switch (value) {
|
||||
case 'day':
|
||||
return '天'
|
||||
return t('send.expiration.units.days')
|
||||
case 'hour':
|
||||
return '小时'
|
||||
return t('send.expiration.units.hours')
|
||||
case 'minute':
|
||||
return '分钟'
|
||||
return t('send.expiration.units.minutes')
|
||||
case 'count':
|
||||
return '次'
|
||||
return t('send.expiration.units.times')
|
||||
case 'forever':
|
||||
return '永久'
|
||||
return t('send.expiration.units.forever')
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
@@ -741,10 +743,10 @@ const getUnit = (value: string = expirationMethod.value) => {
|
||||
|
||||
const getExpirationTime = (method: string, value: string) => {
|
||||
if (method === 'forever') {
|
||||
return '永久'
|
||||
return t('send.expiration.units.forever')
|
||||
}
|
||||
if (method === 'count') {
|
||||
return `${value}次后过期`
|
||||
return t('send.messages.expiresAfterCount', { count: value })
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
@@ -761,7 +763,7 @@ const getExpirationTime = (method: string, value: string) => {
|
||||
now.setDate(now.getDate() + expireValue)
|
||||
break
|
||||
default:
|
||||
return `${value}${getUnit(method)}后过期`
|
||||
return t('send.messages.expiresAfter', { value, unit: getUnit(method) })
|
||||
}
|
||||
|
||||
const year = now.getFullYear()
|
||||
@@ -770,7 +772,7 @@ const getExpirationTime = (method: string, value: string) => {
|
||||
const hours = now.getHours().toString().padStart(2, '0')
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0')
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}过期`
|
||||
return t('send.messages.expiresAt', { date: `${year}-${month}-${day} ${hours}:${minutes}` })
|
||||
}
|
||||
|
||||
const handleChunkUpload = async (file: File) => {
|
||||
@@ -792,7 +794,7 @@ const handleChunkUpload = async (file: File) => {
|
||||
})
|
||||
|
||||
if (initResponse.code !== 200) {
|
||||
throw new Error('初始化切片上传失败')
|
||||
throw new Error(t('send.messages.initChunkUploadFailed'))
|
||||
}
|
||||
if (initResponse.detail?.existed) {
|
||||
return initResponse
|
||||
@@ -826,7 +828,7 @@ const handleChunkUpload = async (file: File) => {
|
||||
)
|
||||
|
||||
if (chunkResponse.code !== 200) {
|
||||
throw new Error(`切片 ${i} 上传失败`)
|
||||
throw new Error(t('send.messages.chunkUploadFailed', { index: i }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -837,7 +839,7 @@ const handleChunkUpload = async (file: File) => {
|
||||
})
|
||||
|
||||
if (completeResponse.code !== 200) {
|
||||
throw new Error('完成上传失败')
|
||||
throw new Error(t('send.messages.completeUploadFailed'))
|
||||
}
|
||||
|
||||
return completeResponse
|
||||
@@ -849,7 +851,7 @@ const handleChunkUpload = async (file: File) => {
|
||||
alertStore.showAlert(axiosError.response.data.detail, 'error')
|
||||
}
|
||||
} else {
|
||||
alertStore.showAlert('上传失败,请稍后重试', 'error')
|
||||
alertStore.showAlert(t('send.messages.uploadFailed'), 'error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
@@ -874,7 +876,7 @@ const handleDefaultFileUpload = async (file: File) => {
|
||||
}
|
||||
const checkOpenUpload = () => {
|
||||
if (config.openUpload === 0 && localStorage.getItem('token') === null) {
|
||||
alertStore.showAlert('游客上传功能已关闭', 'error')
|
||||
alertStore.showAlert(t('send.messages.guestUploadDisabled'), 'error')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -882,7 +884,7 @@ const checkOpenUpload = () => {
|
||||
|
||||
const checkFileSize = (file: File) => {
|
||||
if (file.size > config.uploadSize) {
|
||||
alertStore.showAlert(`文件大小超过限制 (${getStorageUnit(config.uploadSize)})`, 'error')
|
||||
alertStore.showAlert(t('send.messages.fileSizeExceeded', { size: getStorageUnit(config.uploadSize) }), 'error')
|
||||
selectedFile.value = null
|
||||
return false
|
||||
}
|
||||
@@ -921,22 +923,22 @@ const checkUpload = () => {
|
||||
}
|
||||
const handleSubmit = async () => {
|
||||
if (sendType.value === 'file' && !selectedFile.value) {
|
||||
alertStore.showAlert('请选择要上传的文件', 'error')
|
||||
alertStore.showAlert(t('send.messages.selectFile'), 'error')
|
||||
return
|
||||
}
|
||||
if (sendType.value === 'text' && !textContent.value.trim()) {
|
||||
alertStore.showAlert('请输入要发送的文本', 'error')
|
||||
alertStore.showAlert(t('send.messages.enterText'), 'error')
|
||||
return
|
||||
}
|
||||
if (expirationMethod.value !== 'forever' && !expirationValue.value) {
|
||||
alertStore.showAlert('请输入过期值', 'error')
|
||||
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(`过期时间不能超过${maxDays}天`, 'error')
|
||||
alertStore.showAlert(t('send.messages.expirationTooLong', { days: maxDays }), 'error')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -977,14 +979,14 @@ const handleSubmit = async () => {
|
||||
: `${(selectedFile.value!.size / (1024 * 1024)).toFixed(1)} MB`,
|
||||
expiration:
|
||||
expirationMethod.value === 'forever'
|
||||
? '永久'
|
||||
? t('send.expiration.forever')
|
||||
: getExpirationTime(expirationMethod.value, expirationValue.value),
|
||||
retrieveCode: retrieveCode
|
||||
}
|
||||
fileDataStore.addShareDataRecord(newRecord)
|
||||
|
||||
// 显示发送成功消息
|
||||
alertStore.showAlert(`文件发送成功!取件码:${retrieveCode}`, 'success')
|
||||
alertStore.showAlert(t('send.messages.sendSuccess', { code: retrieveCode }), 'success')
|
||||
// 重置表单 - 只重置文件和文本内容,保留过期信息
|
||||
selectedFile.value = null
|
||||
textContent.value = ''
|
||||
@@ -994,7 +996,7 @@ const handleSubmit = async () => {
|
||||
// 自动复制取件码链接
|
||||
await copyRetrieveLink(retrieveCode)
|
||||
} else {
|
||||
throw new Error('服务器响应异常')
|
||||
throw new Error(t('send.messages.serverError'))
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error && typeof error === 'object' && 'response' in error) {
|
||||
@@ -1003,7 +1005,7 @@ const handleSubmit = async () => {
|
||||
alertStore.showAlert(axiosError.response.data.detail, 'error')
|
||||
}
|
||||
} else {
|
||||
alertStore.showAlert('发送失败,请稍后重试', 'error')
|
||||
alertStore.showAlert(t('send.messages.sendFailed'), 'error')
|
||||
}
|
||||
} finally {
|
||||
uploadProgress.value = 0
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
<template>
|
||||
<div class="p-6 overflow-y-auto custom-scrollbar">
|
||||
<h2 class="text-2xl font-bold mb-6" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
|
||||
仪表盘
|
||||
{{ t('admin.dashboard.title') }}
|
||||
</h2>
|
||||
<!-- 统计卡片区域 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<StatCard
|
||||
title="总文件数"
|
||||
:title="t('admin.dashboard.totalFiles')"
|
||||
:value="dashboardData.totalFiles"
|
||||
:icon="FileIcon"
|
||||
icon-color="indigo"
|
||||
description-type="success">
|
||||
<template #description>
|
||||
<span>昨天:{{ dashboardData.yesterdayCount }}</span>
|
||||
<span class="ml-2">今天:{{ dashboardData.todayCount }}</span>
|
||||
<span>{{ t('admin.dashboard.yesterday') }}:{{ dashboardData.yesterdayCount }}</span>
|
||||
<span class="ml-2">{{ t('admin.dashboard.today') }}:{{ dashboardData.todayCount }}</span>
|
||||
</template>
|
||||
</StatCard>
|
||||
|
||||
<StatCard
|
||||
title="存储空间"
|
||||
:title="t('admin.dashboard.storageSpace')"
|
||||
:value="dashboardData.storageUsed"
|
||||
:icon="HardDriveIcon"
|
||||
icon-color="purple"
|
||||
description-type="success">
|
||||
<template #description>
|
||||
<span>昨天:{{ dashboardData.yesterdaySize }}</span>
|
||||
<span class="ml-2">今天:{{ dashboardData.todaySize }}</span>
|
||||
<span>{{ t('admin.dashboard.yesterday') }}:{{ dashboardData.yesterdaySize }}</span>
|
||||
<span class="ml-2">{{ t('admin.dashboard.today') }}:{{ dashboardData.todaySize }}</span>
|
||||
</template>
|
||||
</StatCard>
|
||||
|
||||
<StatCard
|
||||
title="活跃用户"
|
||||
:title="t('admin.dashboard.activeUsers')"
|
||||
value="25"
|
||||
:icon="UsersIcon"
|
||||
icon-color="green"
|
||||
description-type="error">
|
||||
<template #description>
|
||||
<span>↓ 5% 较上周</span>
|
||||
<span>{{ t('admin.dashboard.weeklyChange') }}</span>
|
||||
</template>
|
||||
</StatCard>
|
||||
|
||||
<StatCard
|
||||
title="系统状态"
|
||||
value="正常"
|
||||
:title="t('admin.dashboard.systemStatus')"
|
||||
:value="t('admin.dashboard.normal')"
|
||||
:icon="ActivityIcon"
|
||||
icon-color="blue"
|
||||
description-type="neutral">
|
||||
<template #description>
|
||||
服务器运行时间: {{ dashboardData.sysUptime }}
|
||||
{{ t('admin.dashboard.serverUptime') }}: {{ dashboardData.sysUptime }}
|
||||
</template>
|
||||
</StatCard>
|
||||
</div>
|
||||
@@ -75,7 +75,9 @@ import {
|
||||
import { StatsService } from '@/services'
|
||||
import type { DashboardData } from '@/types'
|
||||
import StatCard from '@/components/common/StatCard.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
const { t } = useI18n()
|
||||
|
||||
const dashboardData = reactive<DashboardData>({
|
||||
totalFiles: 0,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- 页面标题和统计信息 -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-bold mb-4" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
|
||||
文件管理
|
||||
{{ t('fileManage.title') }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -18,19 +18,19 @@
|
||||
? 'bg-gray-700 border-gray-600 text-white placeholder-gray-400'
|
||||
: 'bg-white border-gray-300 text-gray-900 placeholder-gray-400',
|
||||
'w-full pl-10 pr-4 py-2.5 rounded-lg border focus:ring-2 focus:ring-indigo-500 focus:border-transparent'
|
||||
]" placeholder="搜索文件名称、描述..." />
|
||||
]" :placeholder="t('fileManage.searchPlaceholder')" />
|
||||
<SearchIcon class="absolute left-3 top-3 w-5 h-5" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']" />
|
||||
</div>
|
||||
<button @click="handleSearch"
|
||||
class="px-4 py-2.5 rounded-lg inline-flex items-center transition-all duration-200 bg-indigo-600 hover:bg-indigo-700 text-white shadow-sm">
|
||||
<SearchIcon class="w-5 h-5 mr-2" />
|
||||
搜索
|
||||
{{ t('common.search') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<DataTable title="所有文件" :headers="fileTableHeaders">
|
||||
<DataTable :title="t('fileManage.allFiles')" :headers="fileTableHeaders">
|
||||
<template #body>
|
||||
<tr v-for="file in tableData" :key="file.id" class="hover:bg-opacity-50 transition-colors duration-200"
|
||||
:class="[isDarkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-50']">
|
||||
@@ -84,7 +84,7 @@
|
||||
? (isDarkMode ? 'bg-yellow-900/30 text-yellow-400' : 'bg-yellow-100 text-yellow-800')
|
||||
: (isDarkMode ? 'bg-green-900/30 text-green-400' : 'bg-green-100 text-green-800')
|
||||
]">
|
||||
{{ file.expired_at ? formatTimestamp(file.expired_at) : '永久' }}
|
||||
{{ file.expired_at ? formatTimestamp(file.expired_at) : t('send.expiration.units.forever') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
@@ -96,7 +96,7 @@
|
||||
: 'bg-blue-50 text-blue-600 hover:bg-blue-100'
|
||||
]">
|
||||
<PencilIcon class="w-4 h-4 mr-1.5" />
|
||||
编辑
|
||||
{{ t('common.edit') }}
|
||||
</button>
|
||||
<button @click="deleteFile(file.id)"
|
||||
class="inline-flex items-center px-3 py-1.5 rounded-md transition-colors duration-200" :class="[
|
||||
@@ -105,7 +105,7 @@
|
||||
: 'bg-red-50 text-red-600 hover:bg-red-100'
|
||||
]">
|
||||
<TrashIcon class="w-4 h-4 mr-1.5" />
|
||||
删除
|
||||
{{ t('common.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -144,7 +144,7 @@
|
||||
<PencilIcon class="w-5 h-5" :class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']" />
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold leading-6" :class="[isDarkMode ? 'text-white' : 'text-gray-900']">
|
||||
编辑文件信息
|
||||
{{ t('fileManage.editFileInfo') }}
|
||||
</h3>
|
||||
</div>
|
||||
<!-- 优化的关闭按钮 -->
|
||||
@@ -164,7 +164,7 @@
|
||||
<div class="space-y-2 group">
|
||||
<label class="text-sm font-medium flex items-center space-x-2 transition-colors duration-200"
|
||||
:class="[isDarkMode ? 'text-gray-300 group-focus-within:text-indigo-400' : 'text-gray-700 group-focus-within:text-indigo-600']">
|
||||
<span>取件码</span>
|
||||
<span>{{ t('fileManage.form.code') }}</span>
|
||||
<div class="h-px flex-1 transition-colors duration-200"
|
||||
:class="[isDarkMode ? 'bg-gray-700 group-focus-within:bg-indigo-500/50' : 'bg-gray-200 group-focus-within:bg-indigo-500/30']">
|
||||
</div>
|
||||
@@ -176,7 +176,7 @@
|
||||
isDarkMode
|
||||
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:ring-indigo-500/50'
|
||||
: 'bg-gray-50 text-gray-900 placeholder-gray-400 focus:ring-indigo-500'
|
||||
]" placeholder="输入取件码">
|
||||
]" :placeholder="t('fileManage.form.codePlaceholder')">
|
||||
<div
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none transition-opacity duration-200 opacity-0 group-focus-within:opacity-100">
|
||||
<CheckIcon class="w-5 h-5" :class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']" />
|
||||
@@ -188,7 +188,7 @@
|
||||
<div class="space-y-2 group">
|
||||
<label class="text-sm font-medium flex items-center space-x-2 transition-colors duration-200"
|
||||
:class="[isDarkMode ? 'text-gray-300 group-focus-within:text-indigo-400' : 'text-gray-700 group-focus-within:text-indigo-600']">
|
||||
<span>文件名称</span>
|
||||
<span>{{ t('fileManage.form.filename') }}</span>
|
||||
<div class="h-px flex-1 transition-colors duration-200"
|
||||
:class="[isDarkMode ? 'bg-gray-700 group-focus-within:bg-indigo-500/50' : 'bg-gray-200 group-focus-within:bg-indigo-500/30']">
|
||||
</div>
|
||||
@@ -200,7 +200,7 @@
|
||||
isDarkMode
|
||||
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:ring-indigo-500/50'
|
||||
: 'bg-gray-50 text-gray-900 placeholder-gray-400 focus:ring-indigo-500'
|
||||
]" placeholder="输入文件名称">
|
||||
]" :placeholder="t('fileManage.form.filenamePlaceholder')">
|
||||
<div
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none transition-opacity duration-200 opacity-0 group-focus-within:opacity-100">
|
||||
<CheckIcon class="w-5 h-5" :class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']" />
|
||||
@@ -212,7 +212,7 @@
|
||||
<div class="space-y-2 group">
|
||||
<label class="text-sm font-medium flex items-center space-x-2 transition-colors duration-200"
|
||||
:class="[isDarkMode ? 'text-gray-300 group-focus-within:text-indigo-400' : 'text-gray-700 group-focus-within:text-indigo-600']">
|
||||
<span>文件后缀</span>
|
||||
<span>{{ t('fileManage.form.suffix') }}</span>
|
||||
<div class="h-px flex-1 transition-colors duration-200"
|
||||
:class="[isDarkMode ? 'bg-gray-700 group-focus-within:bg-indigo-500/50' : 'bg-gray-200 group-focus-within:bg-indigo-500/30']">
|
||||
</div>
|
||||
@@ -224,7 +224,7 @@
|
||||
isDarkMode
|
||||
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:ring-indigo-500/50'
|
||||
: 'bg-gray-50 text-gray-900 placeholder-gray-400 focus:ring-indigo-500'
|
||||
]" placeholder="输入文件后缀">
|
||||
]" :placeholder="t('fileManage.form.suffixPlaceholder')">
|
||||
<div
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none transition-opacity duration-200 opacity-0 group-focus-within:opacity-100">
|
||||
<CheckIcon class="w-5 h-5" :class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']" />
|
||||
@@ -236,7 +236,7 @@
|
||||
<div class="space-y-2 group">
|
||||
<label class="text-sm font-medium flex items-center space-x-2 transition-colors duration-200"
|
||||
:class="[isDarkMode ? 'text-gray-300 group-focus-within:text-indigo-400' : 'text-gray-700 group-focus-within:text-indigo-600']">
|
||||
<span>过期时间</span>
|
||||
<span>{{ t('send.expiration.label') }}</span>
|
||||
<div class="h-px flex-1 transition-colors duration-200"
|
||||
:class="[isDarkMode ? 'bg-gray-700 group-focus-within:bg-indigo-500/50' : 'bg-gray-200 group-focus-within:bg-indigo-500/30']">
|
||||
</div>
|
||||
@@ -260,7 +260,7 @@
|
||||
<div class="space-y-2 group">
|
||||
<label class="text-sm font-medium flex items-center space-x-2 transition-colors duration-200"
|
||||
:class="[isDarkMode ? 'text-gray-300 group-focus-within:text-indigo-400' : 'text-gray-700 group-focus-within:text-indigo-600']">
|
||||
<span>下载次数限制</span>
|
||||
<span>{{ t('fileManage.form.downloadLimit') }}</span>
|
||||
<div class="h-px flex-1 transition-colors duration-200"
|
||||
:class="[isDarkMode ? 'bg-gray-700 group-focus-within:bg-indigo-500/50' : 'bg-gray-200 group-focus-within:bg-indigo-500/30']">
|
||||
</div>
|
||||
@@ -272,7 +272,7 @@
|
||||
isDarkMode
|
||||
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:ring-indigo-500/50'
|
||||
: 'bg-gray-50 text-gray-900 placeholder-gray-400 focus:ring-indigo-500'
|
||||
]" placeholder="输入下载次数限制">
|
||||
]" :placeholder="t('fileManage.form.downloadLimitPlaceholder')">
|
||||
<div
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none transition-opacity duration-200 opacity-0 group-focus-within:opacity-100">
|
||||
<CheckIcon class="w-5 h-5" :class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']" />
|
||||
@@ -292,12 +292,12 @@
|
||||
? 'bg-gray-700/50 text-gray-300 hover:bg-gray-600/50 hover:text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
]">
|
||||
取消
|
||||
{{ t('common.cancel') }}
|
||||
</button>
|
||||
<button @click="handleUpdate"
|
||||
class="inline-flex items-center justify-center rounded-lg px-4 py-2.5 text-sm font-medium transition-all duration-200 bg-gradient-to-r from-indigo-600 to-indigo-500 hover:from-indigo-500 hover:to-indigo-600 text-white shadow-lg shadow-indigo-500/25 hover:shadow-indigo-500/35">
|
||||
<CheckIcon class="w-4 h-4 mr-2" />
|
||||
保存更改
|
||||
{{ t('fileManage.saveChanges') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -309,6 +309,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { FileService } from '@/services'
|
||||
import type { FileListItem, FileEditForm } from '@/types'
|
||||
import {
|
||||
@@ -333,11 +334,19 @@ function formatTimestamp(timestamp: string): string {
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
const tableData = ref<FileListItem[]>([])
|
||||
const alertStore = useAlertStore()
|
||||
// 修改文件表头
|
||||
const fileTableHeaders = ['取件码', '名称', '大小', '描述', '过期时间', '操作']
|
||||
const fileTableHeaders = computed(() => [
|
||||
t('fileManage.headers.code'),
|
||||
t('fileManage.headers.name'),
|
||||
t('fileManage.headers.size'),
|
||||
t('fileManage.headers.description'),
|
||||
t('fileManage.headers.expiration'),
|
||||
t('fileManage.headers.actions')
|
||||
])
|
||||
|
||||
// 分页参数
|
||||
const params = ref({
|
||||
@@ -392,7 +401,7 @@ const handleUpdate = async () => {
|
||||
closeEditModal()
|
||||
} catch (error: unknown) {
|
||||
const err = error as { response?: { data?: { detail?: string } } }
|
||||
alertStore.showAlert(err.response?.data?.detail || '更新失败', 'error')
|
||||
alertStore.showAlert(err.response?.data?.detail || t('manage.fileManage.updateFailed'), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,7 +434,7 @@ const deleteFile = async (id: number) => {
|
||||
await FileService.deleteAdminFile(id)
|
||||
await loadFiles()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
console.error(t('manage.fileManage.deleteFailed'), error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,7 +469,7 @@ const loadFiles = async () => {
|
||||
params.value.total = res.detail.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载文件列表失败:', error)
|
||||
console.error(t('manage.fileManage.loadFileListFailed'), error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@ import { inject, ref } from 'vue'
|
||||
import { ConfigService } from '@/services'
|
||||
import type { ConfigState } from '@/types'
|
||||
import { useAlertStore } from '@/stores/alertStore'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
const { t } = useI18n()
|
||||
|
||||
const config = ref<ConfigState>({
|
||||
name: '',
|
||||
@@ -150,21 +152,21 @@ refreshData()
|
||||
<template>
|
||||
<div class="p-6 h-screen overflow-y-auto custom-scrollbar">
|
||||
<h2 class="text-2xl font-bold mb-6" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
|
||||
系统设置
|
||||
{{ t('admin.settings.title') }}
|
||||
</h2>
|
||||
|
||||
<div class="space-y-6 rounded-lg shadow-md p-6" :class="[isDarkMode ? 'bg-gray-800 bg-opacity-70' : 'bg-white']">
|
||||
<!-- 基本设置 -->
|
||||
<section class="space-y-4">
|
||||
<h3 class="text-lg font-medium mb-4" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
|
||||
基本设置
|
||||
{{ t('admin.settings.basicSettings') }}
|
||||
</h3>
|
||||
|
||||
<!-- 网基本信息 -->
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
网站名称
|
||||
{{ t('admin.settings.siteName') }}
|
||||
</label>
|
||||
<input type="text" v-model="config.name"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||
@@ -177,7 +179,7 @@ refreshData()
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
网站描述
|
||||
{{ t('admin.settings.websiteDescription') }}
|
||||
</label>
|
||||
<input type="text" v-model="config.description"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||
@@ -190,10 +192,10 @@ refreshData()
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
管理员密码
|
||||
{{ t('admin.settings.adminPassword') }}
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input type="password" minlength="6" v-model="config.admin_token" placeholder="留空则不修改密码"
|
||||
<input type="password" minlength="6" v-model="config.admin_token" :placeholder="t('admin.settings.passwordPlaceholder')"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||
:class="[
|
||||
isDarkMode
|
||||
@@ -202,14 +204,14 @@ refreshData()
|
||||
]" />
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3 text-sm text-gray-400"
|
||||
:class="[isDarkMode ? 'text-gray-500' : 'text-gray-400']">
|
||||
<span class="text-xs">留空则不修改</span>
|
||||
<span class="text-xs">{{ t('admin.settings.passwordNote') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
关键词
|
||||
{{ t('admin.settings.keywords') }}
|
||||
</label>
|
||||
<input type="text" v-model="config.keywords"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||
@@ -223,7 +225,7 @@ refreshData()
|
||||
<!-- 主题选择 -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
主题选择
|
||||
{{ t('manage.settings.themeSelection') }}
|
||||
</label>
|
||||
<select v-model="config.themesSelect"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border appearance-none bg-no-repeat bg-right focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none cursor-pointer"
|
||||
@@ -241,7 +243,7 @@ refreshData()
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
Robots.txt
|
||||
{{ t('manage.settings.robotsFile') }}
|
||||
</label>
|
||||
<textarea v-model="config.robotsText" rows="3"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border resize-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||
@@ -257,7 +259,7 @@ refreshData()
|
||||
<div class="grid grid-cols-1 gap-6 mt-8">
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
通知标题
|
||||
{{ t('manage.settings.notificationTitle') }}
|
||||
</label>
|
||||
<input type="text" v-model="config.notify_title"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||
@@ -270,7 +272,7 @@ refreshData()
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
通知内容
|
||||
{{ t('manage.settings.notificationContent') }}
|
||||
</label>
|
||||
<textarea v-model="config.notify_content" rows="3"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border resize-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||
@@ -285,14 +287,14 @@ refreshData()
|
||||
<!-- 存储设置 -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-medium mb-4" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
|
||||
存储设置
|
||||
{{ t('manage.settings.storageSettings') }}
|
||||
</h3>
|
||||
<!-- 通知设置 -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
存储路径
|
||||
{{ t('manage.settings.storagePath') }}
|
||||
</label>
|
||||
<input type="text" placeholder="留空则使用默认路径,可不填写" v-model="config.storage_path"
|
||||
<input type="text" :placeholder="t('manage.settings.storagePathPlaceholder')" v-model="config.storage_path"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||
:class="[
|
||||
isDarkMode
|
||||
@@ -304,7 +306,7 @@ refreshData()
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
存储方式
|
||||
{{ t('manage.settings.storageMethod') }}
|
||||
</label>
|
||||
<select v-model="config.file_storage"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border appearance-none bg-no-repeat bg-right focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none cursor-pointer"
|
||||
@@ -315,14 +317,14 @@ refreshData()
|
||||
]" style="
|
||||
background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2220%22%20height%3D%2220%22%20viewBox%3D%220%200%2020%2020%22%20fill%3D%22none%22%3E%3Cpath%20d%3D%22M7%208l3%203%203-3%22%20stroke%3D%22%236B7280%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E');
|
||||
">
|
||||
<option value="local">本地存储</option>
|
||||
<option value="s3">S3 存储</option>
|
||||
<option value="webdav">Webdav 存储</option>
|
||||
<option value="local">{{ t('manage.settings.localStorage') }}</option>
|
||||
<option value="s3">{{ t('manage.settings.s3Storage') }}</option>
|
||||
<option value="webdav">{{ t('manage.settings.webdavStorage') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2" v-if="config.file_storage === 'local'">
|
||||
<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"
|
||||
@@ -337,7 +339,7 @@ refreshData()
|
||||
]" />
|
||||
</button>
|
||||
<span class="ml-3 text-sm" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
{{ config.enableChunk === 1 ? '已开启' : '已关闭' }}
|
||||
{{ config.enableChunk === 1 ? t('common.enabled') : t('common.disabled') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -347,7 +349,7 @@ refreshData()
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
Webdav URL
|
||||
</label>
|
||||
<input type="text" placeholder="请输入 Webdav URL" v-model="config.webdav_url"
|
||||
<input type="text" :placeholder="t('manage.settings.webdavUrlPlaceholder')" v-model="config.webdav_url"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||
:class="[
|
||||
isDarkMode
|
||||
@@ -360,7 +362,7 @@ refreshData()
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
Webdav Username
|
||||
</label>
|
||||
<input type="text" placeholder="请输入 Webdav Username" v-model="config.webdav_username"
|
||||
<input type="text" :placeholder="t('manage.settings.webdavUsernamePlaceholder')" v-model="config.webdav_username"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||
:class="[
|
||||
isDarkMode
|
||||
@@ -373,7 +375,7 @@ refreshData()
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
Webdav Password
|
||||
</label>
|
||||
<input type="password" placeholder="请输入 Webdav Password" v-model="config.webdav_password"
|
||||
<input type="password" :placeholder="t('manage.settings.webdavPasswordPlaceholder')" v-model="config.webdav_password"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||
:class="[
|
||||
isDarkMode
|
||||
@@ -388,7 +390,7 @@ refreshData()
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
S3 AccessKeyId
|
||||
{{ t('manage.settings.s3AccessKeyId') }}
|
||||
</label>
|
||||
<input type="text" v-model="config.s3_access_key_id"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||
@@ -401,7 +403,7 @@ refreshData()
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
S3 SecretAccessKey
|
||||
{{ t('manage.settings.s3SecretAccessKey') }}
|
||||
</label>
|
||||
<input type="password" v-model="config.s3_secret_access_key"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||
@@ -414,7 +416,7 @@ refreshData()
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
S3 BucketName
|
||||
{{ t('manage.settings.s3BucketName') }}
|
||||
</label>
|
||||
<input type="text" v-model="config.s3_bucket_name"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||
@@ -427,7 +429,7 @@ refreshData()
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
S3 EndpointUrl
|
||||
{{ t('manage.settings.s3EndpointUrl') }}
|
||||
</label>
|
||||
<input type="text" v-model="config.s3_endpoint_url"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||
@@ -440,9 +442,9 @@ refreshData()
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
S3 Region Name
|
||||
{{ t('manage.settings.s3RegionName') }}
|
||||
</label>
|
||||
<input type="text" v-model="config.s3_region_name" placeholder="auto"
|
||||
<input type="text" v-model="config.s3_region_name" :placeholder="t('manage.settings.autoPlaceholder')"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||
:class="[
|
||||
isDarkMode
|
||||
@@ -453,7 +455,7 @@ refreshData()
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
S3 Signature Version
|
||||
{{ t('manage.settings.s3SignatureVersion') }}
|
||||
</label>
|
||||
<select v-model="config.s3_signature_version"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||
@@ -462,14 +464,14 @@ refreshData()
|
||||
? 'bg-gray-700 border-gray-600 text-white hover:border-gray-500'
|
||||
: 'border-gray-300 hover:border-gray-400'
|
||||
]">
|
||||
<option value="s3v2">S3v2</option>
|
||||
<option value="s3v4">S3v4</option>
|
||||
<option value="s3v2">{{ t('manage.settings.s3v2') }}</option>
|
||||
<option value="s3v4">{{ t('manage.settings.s3v4') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
S3 Hostname
|
||||
{{ t('manage.settings.s3Hostname') }}
|
||||
</label>
|
||||
<input type="text" v-model="config.s3_hostname"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||
@@ -483,7 +485,7 @@ refreshData()
|
||||
<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.enableProxy') }}
|
||||
</label>
|
||||
<div class="flex items-center">
|
||||
<button type="button" @click="config.s3_proxy = config.s3_proxy === 1 ? 0 : 1"
|
||||
@@ -498,7 +500,7 @@ refreshData()
|
||||
]" />
|
||||
</button>
|
||||
<span class="ml-3 text-sm" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
{{ config.s3_proxy === 1 ? '已开启' : '已关闭' }}
|
||||
{{ config.s3_proxy === 1 ? t('common.enabled') : t('common.disabled') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -510,13 +512,13 @@ refreshData()
|
||||
<!-- 上传限制 -->
|
||||
<div class="mt-8">
|
||||
<h3 class="text-lg font-medium mb-4" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
|
||||
上传限制
|
||||
{{ t('manage.settings.uploadLimits') }}
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
每分钟上传限制
|
||||
{{ t('manage.settings.uploadPerMinute') }}
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="number" v-model="config.uploadMinute"
|
||||
@@ -526,13 +528,13 @@ refreshData()
|
||||
? 'bg-gray-700 border-gray-600 text-white placeholder-gray-400 hover:border-gray-500'
|
||||
: 'border-gray-300 hover:border-gray-400 placeholder-gray-500'
|
||||
]" />
|
||||
<span :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">分钟</span>
|
||||
<span :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">{{ t('common.minute') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
上传数量限制
|
||||
{{ t('manage.settings.uploadCountLimit') }}
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="number" v-model="config.uploadCount"
|
||||
@@ -542,13 +544,13 @@ refreshData()
|
||||
? 'bg-gray-700 border-gray-600 text-white placeholder-gray-400 hover:border-gray-500'
|
||||
: 'border-gray-300 hover:border-gray-400 placeholder-gray-500'
|
||||
]" />
|
||||
<span :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">个文件</span>
|
||||
<span :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">{{ t('common.files') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
文件大小限制
|
||||
{{ t('manage.settings.fileSizeLimit') }}
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="number" v-model="fileSize"
|
||||
@@ -565,16 +567,16 @@ refreshData()
|
||||
? 'bg-gray-700 border-gray-600 text-white hover:border-gray-500'
|
||||
: 'border-gray-300 hover:border-gray-400'
|
||||
]">
|
||||
<option value="KB">KB</option>
|
||||
<option value="MB">MB</option>
|
||||
<option value="GB">GB</option>
|
||||
<option value="KB">{{ t('manage.settings.fileSizeUnits.kb') }}</option>
|
||||
<option value="MB">{{ t('manage.settings.fileSizeUnits.mb') }}</option>
|
||||
<option value="GB">{{ t('manage.settings.fileSizeUnits.gb') }}</option>
|
||||
</select>
|
||||
</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.expirationType') }}
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<label v-for="style in ['day', 'hour', 'minute', 'forever', 'count']" :key="style"
|
||||
@@ -589,15 +591,7 @@ refreshData()
|
||||
? 'bg-gray-700 border-gray-600 text-gray-300 hover:border-indigo-500'
|
||||
: 'bg-white border-gray-300 text-gray-700 hover:border-indigo-500'
|
||||
]">
|
||||
{{
|
||||
{
|
||||
day: '按天',
|
||||
hour: '按小时',
|
||||
minute: '按分钟',
|
||||
forever: '永久',
|
||||
count: '按次数'
|
||||
}[style]
|
||||
}}
|
||||
{{ t(`manage.settings.expiration.${style}`) }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@@ -605,7 +599,7 @@ refreshData()
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
最长保存时间
|
||||
{{ t('manage.settings.maxSaveTime') }}
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="number" v-model="saveTime"
|
||||
@@ -622,17 +616,17 @@ refreshData()
|
||||
? 'bg-gray-700 border-gray-600 text-white hover:border-gray-500'
|
||||
: 'border-gray-300 hover:border-gray-400'
|
||||
]">
|
||||
<option value="秒">秒</option>
|
||||
<option value="分">分</option>
|
||||
<option value="时">时</option>
|
||||
<option value="天">天</option>
|
||||
<option value="秒">{{ t('common.second') }}</option>
|
||||
<option value="分">{{ t('common.minute') }}</option>
|
||||
<option value="时">{{ t('common.hour') }}</option>
|
||||
<option value="天">{{ t('common.day') }}</option>
|
||||
</select>
|
||||
</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.guestUpload') }}
|
||||
</label>
|
||||
<div class="flex items-center">
|
||||
<button type="button" @click="config.openUpload = config.openUpload === 1 ? 0 : 1"
|
||||
@@ -647,7 +641,7 @@ refreshData()
|
||||
]" />
|
||||
</button>
|
||||
<span class="ml-3 text-sm" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
{{ config.openUpload === 1 ? '已开启' : '已关闭' }}
|
||||
{{ config.openUpload === 1 ? t('common.enabled') : t('common.disabled') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -657,13 +651,13 @@ refreshData()
|
||||
<!-- 错误限制 -->
|
||||
<div class="mt-8">
|
||||
<h3 class="text-lg font-medium mb-4" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
|
||||
错误限制
|
||||
{{ t('manage.settings.errorLimits') }}
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
每分钟错误限制
|
||||
{{ t('manage.settings.errorPerMinute') }}
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="number" v-model="config.errorMinute"
|
||||
@@ -673,13 +667,13 @@ refreshData()
|
||||
? 'bg-gray-700 border-gray-600 text-white placeholder-gray-400 hover:border-gray-500'
|
||||
: 'border-gray-300 hover:border-gray-400 placeholder-gray-500'
|
||||
]" />
|
||||
<span :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">分钟</span>
|
||||
<span :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">{{ t('common.minute') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
错误次数限制
|
||||
{{ t('manage.settings.errorCountLimit') }}
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="number" v-model="config.errorCount"
|
||||
@@ -689,7 +683,7 @@ refreshData()
|
||||
? 'bg-gray-700 border-gray-600 text-white placeholder-gray-400 hover:border-gray-500'
|
||||
: 'border-gray-300 hover:border-gray-400 placeholder-gray-500'
|
||||
]" />
|
||||
<span :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">次</span>
|
||||
<span :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">{{ t('common.times') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -699,7 +693,7 @@ refreshData()
|
||||
<div class="flex justify-end mt-8">
|
||||
<button @click="submitSave"
|
||||
class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200">
|
||||
保存设置
|
||||
{{ t('manage.settings.saveChanges') }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user