Lan
2025-09-04 16:14:42 +08:00
parent f83b3d7152
commit 2e52ed61bd
25 changed files with 1678 additions and 245 deletions
+1
View File
@@ -24,6 +24,7 @@
"qrcode.vue": "^3.6.0", "qrcode.vue": "^3.6.0",
"spark-md5": "^3.0.2", "spark-md5": "^3.0.2",
"vue": "^3.5.18", "vue": "^3.5.18",
"vue-i18n": "9",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
+40
View File
@@ -41,6 +41,9 @@ importers:
vue: vue:
specifier: ^3.5.18 specifier: ^3.5.18
version: 3.5.21(typescript@5.8.3) 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: vue-router:
specifier: ^4.5.1 specifier: ^4.5.1
version: 4.5.1(vue@3.5.21(typescript@5.8.3)) version: 4.5.1(vue@3.5.21(typescript@5.8.3))
@@ -481,6 +484,18 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'} 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': '@isaacs/balanced-match@4.0.1':
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
@@ -2085,6 +2100,12 @@ packages:
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 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: vue-router@4.5.1:
resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==} resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==}
peerDependencies: peerDependencies:
@@ -2483,6 +2504,18 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {} '@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/balanced-match@4.0.1': {}
'@isaacs/brace-expansion@5.0.0': '@isaacs/brace-expansion@5.0.0':
@@ -4086,6 +4119,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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)): vue-router@4.5.1(vue@3.5.21(typescript@5.8.3)):
dependencies: dependencies:
'@vue/devtools-api': 6.6.4 '@vue/devtools-api': 6.6.4
+23 -32
View File
@@ -1,44 +1,29 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watchEffect, provide, onMounted } from 'vue' import { ref, provide, onMounted, onUnmounted } from 'vue'
import { RouterView } from 'vue-router' import { RouterView } from 'vue-router'
import ThemeToggle from './components/common/ThemeToggle.vue' import ThemeToggle from './components/common/ThemeToggle.vue'
import { useRouter } from 'vue-router' import LanguageSwitcher from './components/common/LanguageSwitcher.vue'
const isDarkMode = ref(false) import { useRouter, useRoute } from 'vue-router'
const isLoading = ref(false)
const router = useRouter()
import AlertComponent from '@/components/common/AlertComponent.vue' import AlertComponent from '@/components/common/AlertComponent.vue'
import { useAlertStore } from '@/stores/alertStore' import { useAlertStore } from '@/stores/alertStore'
import { ConfigService } from '@/services' import { ConfigService } from '@/services'
import type { ApiResponse, ConfigState } from '@/types' 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 alertStore = useAlertStore()
// 检查系统颜色模式
const checkSystemColorScheme = () => {
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
}
// 从本地存储获取用户之前的选择 // 使用主题 composable
const getUserPreference = () => { const { isDarkMode, toggleTheme, initTheme } = useTheme()
const storedPreference = localStorage.getItem('colorMode')
if (storedPreference) {
return storedPreference === 'dark'
}
return null
}
// 设置颜色模式 // 清理函数
const setColorMode = (isDark: boolean) => { let cleanupThemeListener: (() => void) | null = null
isDarkMode.value = isDark
localStorage.setItem('colorMode', isDark ? 'dark' : 'light')
}
onMounted(() => { onMounted(() => {
const userPreference = getUserPreference() // 初始化主题并设置监听器
if (userPreference !== null) { cleanupThemeListener = initTheme()
setColorMode(userPreference)
} else {
setColorMode(checkSystemColorScheme())
}
ConfigService.getUserConfig().then((res: ApiResponse<ConfigState>) => { ConfigService.getUserConfig().then((res: ApiResponse<ConfigState>) => {
if (res.code === 200 && res.detail) { if (res.code === 200 && res.detail) {
localStorage.setItem('config', JSON.stringify(res.detail)) localStorage.setItem('config', JSON.stringify(res.detail))
@@ -54,8 +39,11 @@ onMounted(() => {
}) })
}) })
watchEffect(() => { onUnmounted(() => {
document.documentElement.classList.toggle('dark', isDarkMode.value) // 清理主题监听器
if (cleanupThemeListener) {
cleanupThemeListener()
}
}) })
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
@@ -70,19 +58,22 @@ router.afterEach(() => {
}) })
provide('isDarkMode', isDarkMode) provide('isDarkMode', isDarkMode)
provide('setColorMode', setColorMode) provide('toggleTheme', toggleTheme)
provide('isLoading', isLoading) provide('isLoading', isLoading)
</script> </script>
<template> <template>
<div :class="['app-container', isDarkMode ? 'dark' : 'light']"> <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" /> <ThemeToggle v-model="isDarkMode" />
</div>
<div v-if="isLoading" class="loading-overlay"> <div v-if="isLoading" class="loading-overlay">
<div class="loading-spinner"></div> <div class="loading-spinner"></div>
</div> </div>
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<component :is="Component" :key="$route.fullPath" /> <component :is="Component" :key="route.fullPath" />
</transition> </transition>
</RouterView> </RouterView>
+4 -1
View File
@@ -26,7 +26,7 @@
@click="removeAlert(alert.id)" @click="removeAlert(alert.id)"
class="inline-flex text-white hover:text-gray-200 focus:outline-none transition-colors duration-200" 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" /> <X class="h-5 w-5" />
</button> </button>
</div> </div>
@@ -47,6 +47,9 @@ import { storeToRefs } from 'pinia'
import { useAlertStore } from '@/stores/alertStore' import { useAlertStore } from '@/stores/alertStore'
import { CheckCircle, AlertTriangle, AlertCircle, Info, X } from 'lucide-vue-next' import { CheckCircle, AlertTriangle, AlertCircle, Info, X } from 'lucide-vue-next'
import { onMounted, onUnmounted } from 'vue' import { onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const alertStore = useAlertStore() const alertStore = useAlertStore()
const { alerts } = storeToRefs(alertStore) const { alerts } = storeToRefs(alertStore)
@@ -11,7 +11,7 @@
> >
<div class="flex justify-between items-center mb-4 flex-shrink-0"> <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']"> <h3 class="text-2xl font-bold" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
内容预览 {{ t('contentPreview.title') }}
</h3> </h3>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button <button
@@ -24,7 +24,7 @@
]" ]"
> >
<CopyIcon class="w-4 h-4" /> <CopyIcon class="w-4 h-4" />
复制 {{ t('common.copy') }}
</button> </button>
<button <button
@click="$emit('close')" @click="$emit('close')"
@@ -53,6 +53,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { inject } from 'vue' import { inject } from 'vue'
import { useI18n } from 'vue-i18n'
import { CopyIcon, XIcon } from 'lucide-vue-next' import { CopyIcon, XIcon } from 'lucide-vue-next'
interface Props { interface Props {
@@ -67,6 +68,7 @@ interface Emits {
defineProps<Props>() defineProps<Props>()
defineEmits<Emits>() defineEmits<Emits>()
const { t } = useI18n()
const isDarkMode = inject('isDarkMode') const isDarkMode = inject('isDarkMode')
</script> </script>
+14 -11
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="flex flex-col space-y-3"> <div class="flex flex-col space-y-3">
<label :class="['text-sm font-medium', isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label :class="['text-sm font-medium', isDarkMode ? 'text-gray-300' : 'text-gray-700']">
过期时间 {{ t('send.expiration.label') }}
</label> </label>
<div class="relative flex-grow group"> <div class="relative flex-grow group">
<div <div
@@ -85,11 +85,11 @@
: 'border-gray-200 text-gray-700 focus:ring-indigo-500/60' : 'border-gray-200 text-gray-700 focus:ring-indigo-500/60'
]" ]"
> >
<option value="count">次数</option> <option value="count">{{ t('send.expiration.units.times') }}</option>
<option value="minute">分钟</option> <option value="minute">{{ t('send.expiration.units.minutes') }}</option>
<option value="hour">小时</option> <option value="hour">{{ t('send.expiration.units.hours') }}</option>
<option value="day"></option> <option value="day">{{ t('send.expiration.units.days') }}</option>
<option value="forever">永久</option> <option value="forever">{{ t('send.expiration.units.forever') }}</option>
</select> </select>
<div <div
class="absolute right-4 top-1/2 transform -translate-y-1/2 pointer-events-none" class="absolute right-4 top-1/2 transform -translate-y-1/2 pointer-events-none"
@@ -111,6 +111,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { inject } from 'vue' import { inject } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
interface Props { interface Props {
expirationMethod: string expirationMethod: string
@@ -145,15 +148,15 @@ const incrementValue = (delta: number) => {
const getPlaceholder = () => { const getPlaceholder = () => {
switch (props.expirationMethod) { switch (props.expirationMethod) {
case 'count': case 'count':
return '输入次数' return t('send.expiration.placeholders.count')
case 'minute': case 'minute':
return '输入分钟数' return t('send.expiration.placeholders.minutes')
case 'hour': case 'hour':
return '输入小时数' return t('send.expiration.placeholders.hours')
case 'day': case 'day':
return '输入天数' return t('send.expiration.placeholders.days')
default: default:
return '输入数值' return t('send.expiration.placeholders.default')
} }
} }
</script> </script>
+12 -10
View File
@@ -13,7 +13,7 @@
class="text-2xl font-bold mb-6 truncate" class="text-2xl font-bold mb-6 truncate"
:class="[isDarkMode ? 'text-white' : 'text-gray-800']" :class="[isDarkMode ? 'text-white' : 'text-gray-800']"
> >
文件详情 {{ t('fileDetail.title') }}
</h3> </h3>
<div class="space-y-4" v-if="record"> <div class="space-y-4" v-if="record">
<div class="flex items-center"> <div class="flex items-center">
@@ -25,7 +25,7 @@
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
class="truncate flex-grow" class="truncate flex-grow"
> >
<span class="font-medium">文件名</span>{{ record.filename }} <span class="font-medium">{{ t('fileRecord.filename') }}</span>{{ record.filename }}
</p> </p>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
@@ -37,7 +37,7 @@
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
class="truncate flex-grow" class="truncate flex-grow"
> >
<span class="font-medium">取件日期</span>{{ record.date }} <span class="font-medium">{{ t('fileRecord.date') }}</span>{{ record.date }}
</p> </p>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
@@ -49,7 +49,7 @@
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
class="truncate flex-grow" class="truncate flex-grow"
> >
<span class="font-medium">文件大小</span>{{ record.size }} <span class="font-medium">{{ t('fileRecord.size') }}</span>{{ record.size }}
</p> </p>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
@@ -58,14 +58,14 @@
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']" :class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
/> />
<p :class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"> <p :class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']">
<span class="font-medium">文件内容</span> <span class="font-medium">{{ t('fileDetail.content') }}</span>
</p> </p>
<div v-if="record.filename === 'Text'" class="ml-2"> <div v-if="record.filename === 'Text'" class="ml-2">
<button <button
@click="$emit('preview-content')" @click="$emit('preview-content')"
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition duration-300" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition duration-300"
> >
预览内容 {{ t('fileDetail.previewContent') }}
</button> </button>
</div> </div>
<div v-else> <div v-else>
@@ -75,7 +75,7 @@
rel="noopener noreferrer" rel="noopener noreferrer"
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition duration-300" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition duration-300"
> >
点击下载 {{ t('fileDetail.download') }}
</a> </a>
</div> </div>
</div> </div>
@@ -86,13 +86,13 @@
class="text-lg font-semibold mb-3" class="text-lg font-semibold mb-3"
:class="[isDarkMode ? 'text-white' : 'text-gray-800']" :class="[isDarkMode ? 'text-white' : 'text-gray-800']"
> >
取件二维码 {{ t('fileDetail.qrCode') }}
</h4> </h4>
<div class="bg-white p-2 rounded-lg shadow-md"> <div class="bg-white p-2 rounded-lg shadow-md">
<QRCode :value="getQRCodeValue(record)" :size="128" level="M" /> <QRCode :value="getQRCodeValue(record)" :size="128" level="M" />
</div> </div>
<p class="mt-2 text-sm" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-600']"> <p class="mt-2 text-sm" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-600']">
扫描二维码快速取件 {{ t('fileDetail.scanQrCode') }}
</p> </p>
</div> </div>
@@ -100,7 +100,7 @@
@click="$emit('close')" @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" 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> </button>
</div> </div>
</div> </div>
@@ -109,6 +109,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { inject } from 'vue' import { inject } from 'vue'
import { useI18n } from 'vue-i18n'
import { FileIcon, CalendarIcon, HardDriveIcon, DownloadIcon } from 'lucide-vue-next' import { FileIcon, CalendarIcon, HardDriveIcon, DownloadIcon } from 'lucide-vue-next'
import QRCode from 'qrcode.vue' import QRCode from 'qrcode.vue'
@@ -134,6 +135,7 @@ interface Emits {
defineProps<Props>() defineProps<Props>()
defineEmits<Emits>() defineEmits<Emits>()
const { t } = useI18n()
const isDarkMode = inject('isDarkMode') const isDarkMode = inject('isDarkMode')
const baseUrl = window.location.origin const baseUrl = window.location.origin
+13 -6
View File
@@ -37,19 +37,22 @@
]" ]"
> >
<span class="block truncate"> <span class="block truncate">
{{ selectedFile ? selectedFile.name : placeholder }} {{ selectedFile ? selectedFile.name : placeholderText }}
</span> </span>
</p> </p>
<p :class="['mt-2 text-xs', isDarkMode ? 'text-gray-500' : 'text-gray-400']"> <p :class="['mt-2 text-xs', isDarkMode ? 'text-gray-500' : 'text-gray-400']">
{{ description }} {{ descriptionText }}
</p> </p>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, inject } from 'vue' import { ref, inject, computed } from 'vue'
import { UploadCloudIcon } from 'lucide-vue-next' import { UploadCloudIcon } from 'lucide-vue-next'
import BorderProgressBar from './BorderProgressBar.vue' import BorderProgressBar from './BorderProgressBar.vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
interface Props { interface Props {
selectedFile?: File | null selectedFile?: File | null
progress?: number progress?: number
@@ -63,17 +66,21 @@ interface Emits {
fileDrop: [event: DragEvent] fileDrop: [event: DragEvent]
} }
withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
selectedFile: null, selectedFile: null,
progress: 0, progress: 0,
placeholder: '点击或拖放文件到此处上传', placeholder: '',
description: '支持各种常见格式', description: '',
acceptedTypes: '*' acceptedTypes: '*'
}) })
const emit = defineEmits<Emits>() const emit = defineEmits<Emits>()
const isDarkMode = inject('isDarkMode') 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 fileInput = ref<HTMLInputElement | null>(null)
const triggerFileUpload = () => { const triggerFileUpload = () => {
+102
View File
@@ -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>
+4 -1
View File
@@ -18,7 +18,7 @@
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
> >
<ShieldCheckIcon class="w-4 h-4 mr-1 text-green-400" /> <ShieldCheckIcon class="w-4 h-4 mr-1 text-green-400" />
安全加密 {{ t('send.secureEncryption') }}
</span> </span>
<button <button
@click="$emit('toggle-drawer')" @click="$emit('toggle-drawer')"
@@ -35,6 +35,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { inject } from 'vue' import { inject } from 'vue'
import { ShieldCheckIcon, ClipboardListIcon } from 'lucide-vue-next' import { ShieldCheckIcon, ClipboardListIcon } from 'lucide-vue-next'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
interface Props { interface Props {
linkText?: string linkText?: string
+6 -3
View File
@@ -5,7 +5,7 @@
for="code" for="code"
class="block text-sm font-medium mb-2" class="block text-sm font-medium mb-2"
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
>取件码</label >{{ t('retrieve.codeInput.label') }}</label
> >
<div class="relative"> <div class="relative">
<input <input
@@ -19,7 +19,7 @@
{ 'ring-2 ring-red-500': error }, { 'ring-2 ring-red-500': error },
isDarkMode ? 'text-gray-300' : 'text-gray-800' isDarkMode ? 'text-gray-300' : 'text-gray-800'
]" ]"
placeholder="请输入5位取件码" :placeholder="t('retrieve.codeInput.placeholder')"
required required
:readonly="inputStatus.readonly" :readonly="inputStatus.readonly"
maxlength="5" maxlength="5"
@@ -46,7 +46,7 @@
:disabled="inputStatus.loading" :disabled="inputStatus.loading"
> >
<span class="flex items-center justify-center relative z-10"> <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 <ArrowRightIcon
class="w-5 h-5 ml-2 transition-transform duration-300 transform group-hover:translate-x-1" 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"> <script setup lang="ts">
import { ref, inject, watch } from 'vue' import { ref, inject, watch } from 'vue'
import { ArrowRightIcon } from 'lucide-vue-next' import { ArrowRightIcon } from 'lucide-vue-next'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
interface InputStatus { interface InputStatus {
readonly: boolean readonly: boolean
+5 -2
View File
@@ -12,7 +12,7 @@
: 'bg-gray-200 text-gray-700 hover:bg-gray-300' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
]" ]"
> >
发送文件 {{ t('nav.sendFile') }}
</button> </button>
<button <button
type="button" type="button"
@@ -26,7 +26,7 @@
: 'bg-gray-200 text-gray-700 hover:bg-gray-300' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
]" ]"
> >
发送文本 {{ t('send.sendText') }}
</button> </button>
</div> </div>
</template> </template>
@@ -34,6 +34,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { inject } from 'vue' import { inject } from 'vue'
import type { SendType } from '@/types' import type { SendType } from '@/types'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
interface Props { interface Props {
selectedType: SendType selectedType: SendType
+9 -3
View File
@@ -4,7 +4,7 @@
:value="modelValue" :value="modelValue"
@input="updateValue" @input="updateValue"
:rows="rows" :rows="rows"
:placeholder="placeholder" :placeholder="placeholderText"
:class="[ :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', '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 isDarkMode
@@ -16,7 +16,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { inject } from 'vue' import { inject, computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
interface Props { interface Props {
modelValue: string modelValue: string
@@ -28,7 +31,7 @@ interface Emits {
'update:modelValue': [value: string] 'update:modelValue': [value: string]
} }
withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
rows: 7, rows: 7,
placeholder: '在此输入要发送的文本...' placeholder: '在此输入要发送的文本...'
}) })
@@ -36,6 +39,9 @@ withDefaults(defineProps<Props>(), {
const emit = defineEmits<Emits>() const emit = defineEmits<Emits>()
const isDarkMode = inject('isDarkMode') const isDarkMode = inject('isDarkMode')
// 使用computed属性处理多语言文本
const placeholderText = computed(() => props.placeholder || t('send.uploadArea.textInput'))
const updateValue = (event: Event) => { const updateValue = (event: Event) => {
const target = event.target as HTMLTextAreaElement const target = event.target as HTMLTextAreaElement
emit('update:modelValue', target.value) emit('update:modelValue', target.value)
+3 -3
View File
@@ -3,17 +3,17 @@ import { inject } from 'vue'
import { SunIcon, MoonIcon } from 'lucide-vue-next' import { SunIcon, MoonIcon } from 'lucide-vue-next'
const isDarkMode = inject('isDarkMode') as { value: boolean } const isDarkMode = inject('isDarkMode') as { value: boolean }
const setColorMode = inject('setColorMode') as (isDark: boolean) => void const toggleTheme = inject('toggleTheme') as () => void
const toggleColorMode = () => { const toggleColorMode = () => {
setColorMode(!isDarkMode.value) toggleTheme()
} }
</script> </script>
<template> <template>
<button <button
@click="toggleColorMode" @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'" :class="isDarkMode ? 'bg-gray-800 text-yellow-300' : 'bg-white text-gray-800'"
> >
<SunIcon v-if="!isDarkMode" class="w-6 h-6" /> <SunIcon v-if="!isDarkMode" class="w-6 h-6" />
+50
View File
@@ -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' }
]
+597
View File
@@ -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'
}
}
+604
View File
@@ -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: '这里可以放置一些非立即需要的初始化代码'
}
}
+6 -4
View File
@@ -20,7 +20,7 @@
</div> </div>
</div> </div>
<h1 @click="router.push('/')" class="ml-2 text-xl font-semibold cursor-pointer" :class="[isDarkMode ? 'text-white' : 'text-gray-800']"> <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> </h1>
</div> </div>
<button @click="toggleSidebar" class="lg:hidden"> <button @click="toggleSidebar" class="lg:hidden">
@@ -75,6 +75,7 @@
import { ref, inject, onMounted, onUnmounted } from 'vue' import { ref, inject, onMounted, onUnmounted } from 'vue'
import { BoxIcon, MenuIcon, XIcon, FolderIcon, CogIcon, LayoutDashboardIcon } from 'lucide-vue-next' import { BoxIcon, MenuIcon, XIcon, FolderIcon, CogIcon, LayoutDashboardIcon } from 'lucide-vue-next'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
interface MenuItem { interface MenuItem {
id: string id: string
@@ -84,11 +85,12 @@ interface MenuItem {
} }
const router = useRouter() const router = useRouter()
const { t } = useI18n()
const isDarkMode = inject('isDarkMode') const isDarkMode = inject('isDarkMode')
const menuItems: MenuItem[] = [ const menuItems: MenuItem[] = [
{ id: 'Dashboard', name: '仪表盘', icon: LayoutDashboardIcon, redirect: '/admin/dashboard' }, { id: 'Dashboard', name: t('admin.dashboard.title'), icon: LayoutDashboardIcon, redirect: '/admin/dashboard' },
{ id: 'FileManage', name: '文件管理', icon: FolderIcon, redirect: '/admin/files' }, { id: 'FileManage', name: t('admin.fileManage.title'), icon: FolderIcon, redirect: '/admin/files' },
{ id: 'Settings', name: '系统设置', icon: CogIcon, redirect: '/admin/settings' } { id: 'Settings', name: t('admin.settings.title'), icon: CogIcon, redirect: '/admin/settings' }
] ]
const isSidebarOpen = ref(true) const isSidebarOpen = ref(true)
+2
View File
@@ -5,10 +5,12 @@ import { createPinia } from 'pinia'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import i18n from './i18n'
const app = createApp(App) const app = createApp(App)
app.use(createPinia()) app.use(createPinia())
app.use(router) app.use(router)
app.use(i18n)
app.mount('#app') app.mount('#app')
+12 -9
View File
@@ -49,26 +49,29 @@ export function formatFileSize(bytes: number, decimals: number = 2): string {
/** /**
* 格式化持续时间 * 格式化持续时间
* @param seconds 秒数 * @param seconds 秒数
* @param t 翻译函数
* @returns 格式化后的持续时间字符串 * @returns 格式化后的持续时间字符串
*/ */
export function formatDuration(seconds: number): string { export function formatDuration(seconds: number, t?: (key: string) => string): string {
if (seconds === 0) return '永久' if (seconds === 0) return t ? t('utils.time.forever') : 'Forever'
const units = [ const units = [
{ name: '', value: 86400 }, { key: 'utils.time.day', value: 86400 },
{ name: '小时', value: 3600 }, { key: 'utils.time.hour', value: 3600 },
{ name: '分钟', value: 60 }, { key: 'utils.time.minute', value: 60 },
{ name: '', value: 1 } { key: 'utils.time.second', value: 1 }
] ]
for (const unit of units) { for (const unit of units) {
if (seconds >= unit.value) { if (seconds >= unit.value) {
const value = Math.floor(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 return result
} }
} catch (error) { } catch (error) {
console.error('复制失败:', error) console.error('Copy failed:', error)
return false return false
} }
} }
+16 -14
View File
@@ -22,15 +22,15 @@
/> />
</div> </div>
<PageFooter <PageFooter
link-text="需要发送文件点击这里" :link-text="$t('retrieve.needSendFile')"
link-to="/send" link-to="/send"
drawer-text="取件记录" :drawer-text="$t('retrieve.recordsDrawer')"
@toggle-drawer="toggleDrawer" @toggle-drawer="toggleDrawer"
/> />
</div> </div>
</div> </div>
<SideDrawer :visible="showDrawer" title="取件记录" @close="toggleDrawer"> <SideDrawer :visible="showDrawer" :title="$t('retrieve.recordsDrawer')" @close="toggleDrawer">
<FileRecordList <FileRecordList
:records="records" :records="records"
@view-details="viewDetails" @view-details="viewDetails"
@@ -59,6 +59,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, inject, onMounted, watch } from 'vue' import { ref, inject, onMounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import PageHeader from '@/components/common/PageHeader.vue' import PageHeader from '@/components/common/PageHeader.vue'
import RetrieveForm from '@/components/common/RetrieveForm.vue' import RetrieveForm from '@/components/common/RetrieveForm.vue'
import PageFooter from '@/components/common/PageFooter.vue' import PageFooter from '@/components/common/PageFooter.vue'
@@ -101,6 +102,7 @@ import DOMPurify from 'dompurify'
import { useAlertStore } from '@/stores/alertStore' import { useAlertStore } from '@/stores/alertStore'
import { copyToClipboard } from '@/utils/clipboard' import { copyToClipboard } from '@/utils/clipboard'
const { t } = useI18n()
const alertStore = useAlertStore() const alertStore = useAlertStore()
const baseUrl = window.location.origin const baseUrl = window.location.origin
@@ -153,14 +155,14 @@ const getDownloadUrl = (record: FileRecord) => {
const copyContent = async () => { const copyContent = async () => {
if (selectedRecord.value && selectedRecord.value.content) { if (selectedRecord.value && selectedRecord.value.content) {
await copyToClipboard(selectedRecord.value.content, { await copyToClipboard(selectedRecord.value.content, {
successMsg: '内容已复制到剪贴板', successMsg: t('fileRecord.contentCopied'),
errorMsg: '复制失败,请重试' errorMsg: t('fileRecord.copyFailed')
}) })
} }
} }
const handleSubmit = async () => { const handleSubmit = async () => {
if (code.value.length !== 5) { if (code.value.length !== 5) {
alertStore.showAlert('请输入5位取件码', 'error') alertStore.showAlert(t('retrieve.messages.invalidCode'), 'error')
return return
} }
@@ -200,18 +202,18 @@ const handleSubmit = async () => {
selectedRecord.value = newFileData selectedRecord.value = newFileData
showPreview.value = true showPreview.value = true
} }
alertStore.showAlert('文件获取成功', 'success') alertStore.showAlert(t('retrieve.messages.retrieveSuccess'), 'success')
} else { } else {
alertStore.showAlert('无效的取件码', 'error') alertStore.showAlert(t('retrieve.messages.invalidCodeError'), 'error')
} }
} else { } else {
alertStore.showAlert('获取文件失败:' + res.detail, 'error') alertStore.showAlert(t('retrieve.messages.retrieveFailure') + res.detail, 'error')
} }
} catch (err: unknown) { } catch (err: unknown) {
console.error('取件失败:', err) console.error('Retrieve failed:', err)
const error = err as { response?: { data?: { detail?: string } }; message?: string } const error = err as { response?: { data?: { detail?: string } }; message?: string }
const errorMessage = error?.response?.data?.detail || error?.message || '未知错误' const errorMessage = error?.response?.data?.detail || error?.message || t('retrieve.messages.unknownError')
alertStore.showAlert('取件失败,请稍后重试:' + errorMessage, 'error') alertStore.showAlert(t('retrieve.messages.networkError') + errorMessage, 'error')
} finally { } finally {
inputStatus.value.readonly = false inputStatus.value.readonly = false
inputStatus.value.loading = false inputStatus.value.loading = false
@@ -220,9 +222,9 @@ const handleSubmit = async () => {
} }
const formatFileSize = (bytes: number) => { const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 Bytes' if (bytes === 0) return '0 ' + t('fileSize.bytes')
const k = 1024 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)) const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
} }
+42 -40
View File
@@ -32,14 +32,14 @@
<div v-else key="text" class="grid grid-cols-1 gap-8"> <div v-else key="text" class="grid grid-cols-1 gap-8">
<TextInputArea <TextInputArea
v-model="textContent" v-model="textContent"
placeholder="在此输入要发送的文本..." :placeholder="t('send.uploadArea.textInput')"
/> />
</div> </div>
</transition> </transition>
<!-- 过期方式选择 --> <!-- 过期方式选择 -->
<div class="flex flex-col space-y-3"> <div class="flex flex-col space-y-3">
<label :class="['text-sm font-medium', isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label :class="['text-sm font-medium', isDarkMode ? 'text-gray-300' : 'text-gray-700']">
过期时间 {{ t('send.expiration.label') }}
</label> </label>
<div class="relative flex-grow group"> <div class="relative flex-grow group">
<div <div
@@ -173,13 +173,13 @@
></span> ></span>
<span class="relative z-10 flex items-center justify-center text-lg"> <span class="relative z-10 flex items-center justify-center text-lg">
<SendIcon class="w-6 h-6 mr-2" /> <SendIcon class="w-6 h-6 mr-2" />
<span>安全寄送</span> <span>{{ t('send.submit') }}</span>
</span> </span>
</button> </button>
</form> </form>
<div class="mt-6 text-center"> <div class="mt-6 text-center">
<router-link to="/" class="text-indigo-400 hover:text-indigo-300 transition duration-300"> <router-link to="/" class="text-indigo-400 hover:text-indigo-300 transition duration-300">
需要取件点击这里 {{ t('send.needRetrieveFile') }}
</router-link> </router-link>
</div> </div>
</div> </div>
@@ -193,14 +193,14 @@
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
> >
<ShieldCheckIcon class="w-4 h-4 mr-1 text-green-400" /> <ShieldCheckIcon class="w-4 h-4 mr-1 text-green-400" />
安全加密 {{ t('send.secureEncryption') }}
</span> </span>
<button <button
@click="toggleDrawer" @click="toggleDrawer"
class="text-sm hover:text-indigo-300 transition duration-300 flex items-center" class="text-sm hover:text-indigo-300 transition duration-300 flex items-center"
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']" :class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
> >
发件记录 {{ t('send.sendRecords') }}
<ClipboardListIcon class="w-4 h-4 ml-1" /> <ClipboardListIcon class="w-4 h-4 ml-1" />
</button> </button>
</div> </div>
@@ -218,7 +218,7 @@
:class="[isDarkMode ? 'border-gray-700' : 'border-gray-200']" :class="[isDarkMode ? 'border-gray-700' : 'border-gray-200']"
> >
<h3 class="text-2xl font-bold" :class="[isDarkMode ? 'text-white' : 'text-gray-800']"> <h3 class="text-2xl font-bold" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
发件记录 {{ t('send.sendRecords') }}
</h3> </h3>
<button <button
@click="toggleDrawer" @click="toggleDrawer"
@@ -315,7 +315,7 @@
class="text-lg sm:text-xl font-semibold" class="text-lg sm:text-xl font-semibold"
:class="[isDarkMode ? 'text-white' : 'text-gray-900']" :class="[isDarkMode ? 'text-white' : 'text-gray-900']"
> >
文件详情 {{ t('send.fileDetails') }}
</h3> </h3>
<button <button
@click="selectedRecord = null" @click="selectedRecord = null"
@@ -486,6 +486,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, inject, onMounted, computed } from 'vue' import { ref, inject, onMounted, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { import {
SendIcon, SendIcon,
ClipboardListIcon, ClipboardListIcon,
@@ -547,6 +548,7 @@ const uploadProgress = ref(0)
const showDrawer = ref(false) const showDrawer = ref(false)
const selectedRecord = ref<ShareRecord | null>(null) const selectedRecord = ref<ShareRecord | null>(null)
const { t } = useI18n()
const alertStore = useAlertStore() const alertStore = useAlertStore()
const sendRecords = computed(() => fileDataStore.shareData) const sendRecords = computed(() => fileDataStore.shareData)
@@ -580,7 +582,7 @@ const handlePaste = async (event: ClipboardEvent) => {
if (file) { if (file) {
// 检查文件是否为空 // 检查文件是否为空
if (file.size === 0) { if (file.size === 0) {
alertStore.showAlert('无法读取空文件', 'error') alertStore.showAlert(t('send.messages.emptyFileError'), 'error')
return return
} }
@@ -589,9 +591,9 @@ const handlePaste = async (event: ClipboardEvent) => {
try { try {
fileHash.value = await calculateFileHash(file) fileHash.value = await calculateFileHash(file)
alertStore.showAlert('已从剪贴板添加文件:' + file.name, 'success') alertStore.showAlert(t('send.messages.fileAddedFromClipboard', { filename: file.name }), 'success')
} catch (err) { } catch (err) {
alertStore.showAlert('文件处理失败', 'error') alertStore.showAlert(t('send.messages.fileProcessingFailed'), 'error')
console.error('File hash calculation failed:', err) console.error('File hash calculation failed:', err)
} }
break break
@@ -708,32 +710,32 @@ const generateFallbackHash = (file: File): string => {
const getPlaceholder = (value: string = expirationMethod.value) => { const getPlaceholder = (value: string = expirationMethod.value) => {
switch (value) { switch (value) {
case 'day': case 'day':
return '输入天数' return t('send.expiration.placeholders.days')
case 'hour': case 'hour':
return '输入小时数' return t('send.expiration.placeholders.hours')
case 'minute': case 'minute':
return '输入分钟数' return t('send.expiration.placeholders.minutes')
case 'count': case 'count':
return '输入查看次数' return t('send.expiration.placeholders.count')
case 'forever': case 'forever':
return '永久' return t('send.expiration.placeholders.forever')
default: default:
return '输入值' return t('send.expiration.placeholders.default')
} }
} }
const getUnit = (value: string = expirationMethod.value) => { const getUnit = (value: string = expirationMethod.value) => {
switch (value) { switch (value) {
case 'day': case 'day':
return '天' return t('send.expiration.units.days')
case 'hour': case 'hour':
return '小时' return t('send.expiration.units.hours')
case 'minute': case 'minute':
return '分钟' return t('send.expiration.units.minutes')
case 'count': case 'count':
return '次' return t('send.expiration.units.times')
case 'forever': case 'forever':
return '永久' return t('send.expiration.units.forever')
default: default:
return '' return ''
} }
@@ -741,10 +743,10 @@ const getUnit = (value: string = expirationMethod.value) => {
const getExpirationTime = (method: string, value: string) => { const getExpirationTime = (method: string, value: string) => {
if (method === 'forever') { if (method === 'forever') {
return '永久' return t('send.expiration.units.forever')
} }
if (method === 'count') { if (method === 'count') {
return `${value}次后过期` return t('send.messages.expiresAfterCount', { count: value })
} }
const now = new Date() const now = new Date()
@@ -761,7 +763,7 @@ const getExpirationTime = (method: string, value: string) => {
now.setDate(now.getDate() + expireValue) now.setDate(now.getDate() + expireValue)
break break
default: default:
return `${value}${getUnit(method)}后过期` return t('send.messages.expiresAfter', { value, unit: getUnit(method) })
} }
const year = now.getFullYear() const year = now.getFullYear()
@@ -770,7 +772,7 @@ const getExpirationTime = (method: string, value: string) => {
const hours = now.getHours().toString().padStart(2, '0') const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().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) => { const handleChunkUpload = async (file: File) => {
@@ -792,7 +794,7 @@ const handleChunkUpload = async (file: File) => {
}) })
if (initResponse.code !== 200) { if (initResponse.code !== 200) {
throw new Error('初始化切片上传失败') throw new Error(t('send.messages.initChunkUploadFailed'))
} }
if (initResponse.detail?.existed) { if (initResponse.detail?.existed) {
return initResponse return initResponse
@@ -826,7 +828,7 @@ const handleChunkUpload = async (file: File) => {
) )
if (chunkResponse.code !== 200) { 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) { if (completeResponse.code !== 200) {
throw new Error('完成上传失败') throw new Error(t('send.messages.completeUploadFailed'))
} }
return completeResponse return completeResponse
@@ -849,7 +851,7 @@ const handleChunkUpload = async (file: File) => {
alertStore.showAlert(axiosError.response.data.detail, 'error') alertStore.showAlert(axiosError.response.data.detail, 'error')
} }
} else { } else {
alertStore.showAlert('上传失败,请稍后重试', 'error') alertStore.showAlert(t('send.messages.uploadFailed'), 'error')
} }
throw error throw error
} }
@@ -874,7 +876,7 @@ const handleDefaultFileUpload = async (file: File) => {
} }
const checkOpenUpload = () => { const checkOpenUpload = () => {
if (config.openUpload === 0 && localStorage.getItem('token') === null) { if (config.openUpload === 0 && localStorage.getItem('token') === null) {
alertStore.showAlert('游客上传功能已关闭', 'error') alertStore.showAlert(t('send.messages.guestUploadDisabled'), 'error')
return false return false
} }
return true return true
@@ -882,7 +884,7 @@ const checkOpenUpload = () => {
const checkFileSize = (file: File) => { const checkFileSize = (file: File) => {
if (file.size > config.uploadSize) { 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 selectedFile.value = null
return false return false
} }
@@ -921,22 +923,22 @@ const checkUpload = () => {
} }
const handleSubmit = async () => { const handleSubmit = async () => {
if (sendType.value === 'file' && !selectedFile.value) { if (sendType.value === 'file' && !selectedFile.value) {
alertStore.showAlert('请选择要上传的文件', 'error') alertStore.showAlert(t('send.messages.selectFile'), 'error')
return return
} }
if (sendType.value === 'text' && !textContent.value.trim()) { if (sendType.value === 'text' && !textContent.value.trim()) {
alertStore.showAlert('请输入要发送的文本', 'error') alertStore.showAlert(t('send.messages.enterText'), 'error')
return return
} }
if (expirationMethod.value !== 'forever' && !expirationValue.value) { if (expirationMethod.value !== 'forever' && !expirationValue.value) {
alertStore.showAlert('请输入过期值', 'error') alertStore.showAlert(t('send.messages.enterExpirationValue'), 'error')
return return
} }
// 添加过期时间验证 // 添加过期时间验证
if (!checkExpirationTime(expirationMethod.value, expirationValue.value)) { if (!checkExpirationTime(expirationMethod.value, expirationValue.value)) {
const maxDays = Math.floor(config.max_save_seconds / 86400) const maxDays = Math.floor(config.max_save_seconds / 86400)
alertStore.showAlert(`过期时间不能超过${maxDays}`, 'error') alertStore.showAlert(t('send.messages.expirationTooLong', { days: maxDays }), 'error')
return return
} }
@@ -977,14 +979,14 @@ const handleSubmit = async () => {
: `${(selectedFile.value!.size / (1024 * 1024)).toFixed(1)} MB`, : `${(selectedFile.value!.size / (1024 * 1024)).toFixed(1)} MB`,
expiration: expiration:
expirationMethod.value === 'forever' expirationMethod.value === 'forever'
? '永久' ? t('send.expiration.forever')
: getExpirationTime(expirationMethod.value, expirationValue.value), : getExpirationTime(expirationMethod.value, expirationValue.value),
retrieveCode: retrieveCode retrieveCode: retrieveCode
} }
fileDataStore.addShareDataRecord(newRecord) fileDataStore.addShareDataRecord(newRecord)
// 显示发送成功消息 // 显示发送成功消息
alertStore.showAlert(`文件发送成功!取件码:${retrieveCode}`, 'success') alertStore.showAlert(t('send.messages.sendSuccess', { code: retrieveCode }), 'success')
// 重置表单 - 只重置文件和文本内容,保留过期信息 // 重置表单 - 只重置文件和文本内容,保留过期信息
selectedFile.value = null selectedFile.value = null
textContent.value = '' textContent.value = ''
@@ -994,7 +996,7 @@ const handleSubmit = async () => {
// 自动复制取件码链接 // 自动复制取件码链接
await copyRetrieveLink(retrieveCode) await copyRetrieveLink(retrieveCode)
} else { } else {
throw new Error('服务器响应异常') throw new Error(t('send.messages.serverError'))
} }
} catch (error: unknown) { } catch (error: unknown) {
if (error && typeof error === 'object' && 'response' in error) { if (error && typeof error === 'object' && 'response' in error) {
@@ -1003,7 +1005,7 @@ const handleSubmit = async () => {
alertStore.showAlert(axiosError.response.data.detail, 'error') alertStore.showAlert(axiosError.response.data.detail, 'error')
} }
} else { } else {
alertStore.showAlert('发送失败,请稍后重试', 'error') alertStore.showAlert(t('send.messages.sendFailed'), 'error')
} }
} finally { } finally {
uploadProgress.value = 0 uploadProgress.value = 0
+14 -12
View File
@@ -1,53 +1,53 @@
<template> <template>
<div class="p-6 overflow-y-auto custom-scrollbar"> <div class="p-6 overflow-y-auto custom-scrollbar">
<h2 class="text-2xl font-bold mb-6" :class="[isDarkMode ? 'text-white' : 'text-gray-800']"> <h2 class="text-2xl font-bold mb-6" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
仪表盘 {{ t('admin.dashboard.title') }}
</h2> </h2>
<!-- 统计卡片区域 --> <!-- 统计卡片区域 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<StatCard <StatCard
title="总文件数" :title="t('admin.dashboard.totalFiles')"
:value="dashboardData.totalFiles" :value="dashboardData.totalFiles"
:icon="FileIcon" :icon="FileIcon"
icon-color="indigo" icon-color="indigo"
description-type="success"> description-type="success">
<template #description> <template #description>
<span>昨天{{ dashboardData.yesterdayCount }}</span> <span>{{ t('admin.dashboard.yesterday') }}{{ dashboardData.yesterdayCount }}</span>
<span class="ml-2">今天{{ dashboardData.todayCount }}</span> <span class="ml-2">{{ t('admin.dashboard.today') }}{{ dashboardData.todayCount }}</span>
</template> </template>
</StatCard> </StatCard>
<StatCard <StatCard
title="存储空间" :title="t('admin.dashboard.storageSpace')"
:value="dashboardData.storageUsed" :value="dashboardData.storageUsed"
:icon="HardDriveIcon" :icon="HardDriveIcon"
icon-color="purple" icon-color="purple"
description-type="success"> description-type="success">
<template #description> <template #description>
<span>昨天{{ dashboardData.yesterdaySize }}</span> <span>{{ t('admin.dashboard.yesterday') }}{{ dashboardData.yesterdaySize }}</span>
<span class="ml-2">今天{{ dashboardData.todaySize }}</span> <span class="ml-2">{{ t('admin.dashboard.today') }}{{ dashboardData.todaySize }}</span>
</template> </template>
</StatCard> </StatCard>
<StatCard <StatCard
title="活跃用户" :title="t('admin.dashboard.activeUsers')"
value="25" value="25"
:icon="UsersIcon" :icon="UsersIcon"
icon-color="green" icon-color="green"
description-type="error"> description-type="error">
<template #description> <template #description>
<span> 5% 较上周</span> <span>{{ t('admin.dashboard.weeklyChange') }}</span>
</template> </template>
</StatCard> </StatCard>
<StatCard <StatCard
title="系统状态" :title="t('admin.dashboard.systemStatus')"
value="正常" :value="t('admin.dashboard.normal')"
:icon="ActivityIcon" :icon="ActivityIcon"
icon-color="blue" icon-color="blue"
description-type="neutral"> description-type="neutral">
<template #description> <template #description>
服务器运行时间: {{ dashboardData.sysUptime }} {{ t('admin.dashboard.serverUptime') }}: {{ dashboardData.sysUptime }}
</template> </template>
</StatCard> </StatCard>
</div> </div>
@@ -75,7 +75,9 @@ import {
import { StatsService } from '@/services' import { StatsService } from '@/services'
import type { DashboardData } from '@/types' import type { DashboardData } from '@/types'
import StatCard from '@/components/common/StatCard.vue' import StatCard from '@/components/common/StatCard.vue'
import { useI18n } from 'vue-i18n'
const isDarkMode = inject('isDarkMode') const isDarkMode = inject('isDarkMode')
const { t } = useI18n()
const dashboardData = reactive<DashboardData>({ const dashboardData = reactive<DashboardData>({
totalFiles: 0, totalFiles: 0,
+32 -23
View File
@@ -3,7 +3,7 @@
<!-- 页面标题和统计信息 --> <!-- 页面标题和统计信息 -->
<div class="mb-8"> <div class="mb-8">
<h2 class="text-2xl font-bold mb-4" :class="[isDarkMode ? 'text-white' : 'text-gray-800']"> <h2 class="text-2xl font-bold mb-4" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
文件管理 {{ t('fileManage.title') }}
</h2> </h2>
</div> </div>
@@ -18,19 +18,19 @@
? 'bg-gray-700 border-gray-600 text-white placeholder-gray-400' ? 'bg-gray-700 border-gray-600 text-white placeholder-gray-400'
: 'bg-white border-gray-300 text-gray-900 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' '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']" /> <SearchIcon class="absolute left-3 top-3 w-5 h-5" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']" />
</div> </div>
<button @click="handleSearch" <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"> 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" /> <SearchIcon class="w-5 h-5 mr-2" />
搜索 {{ t('common.search') }}
</button> </button>
</div> </div>
</div> </div>
<!-- 文件列表 --> <!-- 文件列表 -->
<DataTable title="所有文件" :headers="fileTableHeaders"> <DataTable :title="t('fileManage.allFiles')" :headers="fileTableHeaders">
<template #body> <template #body>
<tr v-for="file in tableData" :key="file.id" class="hover:bg-opacity-50 transition-colors duration-200" <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']"> :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-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') : (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> </span>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <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' : 'bg-blue-50 text-blue-600 hover:bg-blue-100'
]"> ]">
<PencilIcon class="w-4 h-4 mr-1.5" /> <PencilIcon class="w-4 h-4 mr-1.5" />
编辑 {{ t('common.edit') }}
</button> </button>
<button @click="deleteFile(file.id)" <button @click="deleteFile(file.id)"
class="inline-flex items-center px-3 py-1.5 rounded-md transition-colors duration-200" :class="[ 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' : 'bg-red-50 text-red-600 hover:bg-red-100'
]"> ]">
<TrashIcon class="w-4 h-4 mr-1.5" /> <TrashIcon class="w-4 h-4 mr-1.5" />
删除 {{ t('common.delete') }}
</button> </button>
</div> </div>
</td> </td>
@@ -144,7 +144,7 @@
<PencilIcon class="w-5 h-5" :class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']" /> <PencilIcon class="w-5 h-5" :class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']" />
</div> </div>
<h3 class="text-xl font-semibold leading-6" :class="[isDarkMode ? 'text-white' : 'text-gray-900']"> <h3 class="text-xl font-semibold leading-6" :class="[isDarkMode ? 'text-white' : 'text-gray-900']">
编辑文件信息 {{ t('fileManage.editFileInfo') }}
</h3> </h3>
</div> </div>
<!-- 优化的关闭按钮 --> <!-- 优化的关闭按钮 -->
@@ -164,7 +164,7 @@
<div class="space-y-2 group"> <div class="space-y-2 group">
<label class="text-sm font-medium flex items-center space-x-2 transition-colors duration-200" <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']"> :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" <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']"> :class="[isDarkMode ? 'bg-gray-700 group-focus-within:bg-indigo-500/50' : 'bg-gray-200 group-focus-within:bg-indigo-500/30']">
</div> </div>
@@ -176,7 +176,7 @@
isDarkMode isDarkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:ring-indigo-500/50' ? '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' : 'bg-gray-50 text-gray-900 placeholder-gray-400 focus:ring-indigo-500'
]" placeholder="输入取件码"> ]" :placeholder="t('fileManage.form.codePlaceholder')">
<div <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"> 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']" /> <CheckIcon class="w-5 h-5" :class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']" />
@@ -188,7 +188,7 @@
<div class="space-y-2 group"> <div class="space-y-2 group">
<label class="text-sm font-medium flex items-center space-x-2 transition-colors duration-200" <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']"> :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" <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']"> :class="[isDarkMode ? 'bg-gray-700 group-focus-within:bg-indigo-500/50' : 'bg-gray-200 group-focus-within:bg-indigo-500/30']">
</div> </div>
@@ -200,7 +200,7 @@
isDarkMode isDarkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:ring-indigo-500/50' ? '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' : 'bg-gray-50 text-gray-900 placeholder-gray-400 focus:ring-indigo-500'
]" placeholder="输入文件名称"> ]" :placeholder="t('fileManage.form.filenamePlaceholder')">
<div <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"> 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']" /> <CheckIcon class="w-5 h-5" :class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']" />
@@ -212,7 +212,7 @@
<div class="space-y-2 group"> <div class="space-y-2 group">
<label class="text-sm font-medium flex items-center space-x-2 transition-colors duration-200" <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']"> :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" <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']"> :class="[isDarkMode ? 'bg-gray-700 group-focus-within:bg-indigo-500/50' : 'bg-gray-200 group-focus-within:bg-indigo-500/30']">
</div> </div>
@@ -224,7 +224,7 @@
isDarkMode isDarkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:ring-indigo-500/50' ? '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' : 'bg-gray-50 text-gray-900 placeholder-gray-400 focus:ring-indigo-500'
]" placeholder="输入文件后缀"> ]" :placeholder="t('fileManage.form.suffixPlaceholder')">
<div <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"> 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']" /> <CheckIcon class="w-5 h-5" :class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']" />
@@ -236,7 +236,7 @@
<div class="space-y-2 group"> <div class="space-y-2 group">
<label class="text-sm font-medium flex items-center space-x-2 transition-colors duration-200" <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']"> :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" <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']"> :class="[isDarkMode ? 'bg-gray-700 group-focus-within:bg-indigo-500/50' : 'bg-gray-200 group-focus-within:bg-indigo-500/30']">
</div> </div>
@@ -260,7 +260,7 @@
<div class="space-y-2 group"> <div class="space-y-2 group">
<label class="text-sm font-medium flex items-center space-x-2 transition-colors duration-200" <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']"> :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" <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']"> :class="[isDarkMode ? 'bg-gray-700 group-focus-within:bg-indigo-500/50' : 'bg-gray-200 group-focus-within:bg-indigo-500/30']">
</div> </div>
@@ -272,7 +272,7 @@
isDarkMode isDarkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:ring-indigo-500/50' ? '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' : 'bg-gray-50 text-gray-900 placeholder-gray-400 focus:ring-indigo-500'
]" placeholder="输入下载次数限制"> ]" :placeholder="t('fileManage.form.downloadLimitPlaceholder')">
<div <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"> 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']" /> <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-700/50 text-gray-300 hover:bg-gray-600/50 hover:text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
]"> ]">
取消 {{ t('common.cancel') }}
</button> </button>
<button @click="handleUpdate" <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"> 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" /> <CheckIcon class="w-4 h-4 mr-2" />
保存更改 {{ t('fileManage.saveChanges') }}
</button> </button>
</div> </div>
</div> </div>
@@ -309,6 +309,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { inject, ref, computed } from 'vue' import { inject, ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { FileService } from '@/services' import { FileService } from '@/services'
import type { FileListItem, FileEditForm } from '@/types' import type { FileListItem, FileEditForm } from '@/types'
import { import {
@@ -333,11 +334,19 @@ function formatTimestamp(timestamp: string): string {
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
} }
const { t } = useI18n()
const isDarkMode = inject('isDarkMode') const isDarkMode = inject('isDarkMode')
const tableData = ref<FileListItem[]>([]) const tableData = ref<FileListItem[]>([])
const alertStore = useAlertStore() 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({ const params = ref({
@@ -392,7 +401,7 @@ const handleUpdate = async () => {
closeEditModal() closeEditModal()
} catch (error: unknown) { } catch (error: unknown) {
const err = error as { response?: { data?: { detail?: string } } } 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 FileService.deleteAdminFile(id)
await loadFiles() await loadFiles()
} catch (error) { } 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 params.value.total = res.detail.total
} }
} catch (error) { } catch (error) {
console.error('加载文件列表失败:', error) console.error(t('manage.fileManage.loadFileListFailed'), error)
} }
} }
+62 -68
View File
@@ -3,8 +3,10 @@ import { inject, ref } from 'vue'
import { ConfigService } from '@/services' import { ConfigService } from '@/services'
import type { ConfigState } from '@/types' import type { ConfigState } from '@/types'
import { useAlertStore } from '@/stores/alertStore' import { useAlertStore } from '@/stores/alertStore'
import { useI18n } from 'vue-i18n'
const isDarkMode = inject('isDarkMode') const isDarkMode = inject('isDarkMode')
const { t } = useI18n()
const config = ref<ConfigState>({ const config = ref<ConfigState>({
name: '', name: '',
@@ -150,21 +152,21 @@ refreshData()
<template> <template>
<div class="p-6 h-screen overflow-y-auto custom-scrollbar"> <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']"> <h2 class="text-2xl font-bold mb-6" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
系统设置 {{ t('admin.settings.title') }}
</h2> </h2>
<div class="space-y-6 rounded-lg shadow-md p-6" :class="[isDarkMode ? 'bg-gray-800 bg-opacity-70' : 'bg-white']"> <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"> <section class="space-y-4">
<h3 class="text-lg font-medium mb-4" :class="[isDarkMode ? 'text-white' : 'text-gray-800']"> <h3 class="text-lg font-medium mb-4" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
基本设置 {{ t('admin.settings.basicSettings') }}
</h3> </h3>
<!-- 网基本信息 --> <!-- 网基本信息 -->
<div class="grid grid-cols-1 gap-6"> <div class="grid grid-cols-1 gap-6">
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
网站名称 {{ t('admin.settings.siteName') }}
</label> </label>
<input type="text" v-model="config.name" <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" 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"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
网站描述 {{ t('admin.settings.websiteDescription') }}
</label> </label>
<input type="text" v-model="config.description" <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" 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"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
管理员密码 {{ t('admin.settings.adminPassword') }}
</label> </label>
<div class="relative"> <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="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="[ :class="[
isDarkMode isDarkMode
@@ -202,14 +204,14 @@ refreshData()
]" /> ]" />
<div class="absolute inset-y-0 right-0 flex items-center pr-3 text-sm text-gray-400" <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']"> :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>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
关键词 {{ t('admin.settings.keywords') }}
</label> </label>
<input type="text" v-model="config.keywords" <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" 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"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
主题选择 {{ t('manage.settings.themeSelection') }}
</label> </label>
<select v-model="config.themesSelect" <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" 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>
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
Robots.txt {{ t('manage.settings.robotsFile') }}
</label> </label>
<textarea v-model="config.robotsText" rows="3" <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" 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="grid grid-cols-1 gap-6 mt-8">
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
通知标题 {{ t('manage.settings.notificationTitle') }}
</label> </label>
<input type="text" v-model="config.notify_title" <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" 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"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
通知内容 {{ t('manage.settings.notificationContent') }}
</label> </label>
<textarea v-model="config.notify_content" rows="3" <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" 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"> <div class="space-y-4">
<h3 class="text-lg font-medium mb-4" :class="[isDarkMode ? 'text-white' : 'text-gray-800']"> <h3 class="text-lg font-medium mb-4" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
存储设置 {{ t('manage.settings.storageSettings') }}
</h3> </h3>
<!-- 通知设置 --> <!-- 通知设置 -->
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
存储路径 {{ t('manage.settings.storagePath') }}
</label> </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="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="[ :class="[
isDarkMode isDarkMode
@@ -304,7 +306,7 @@ refreshData()
<div class="space-y-4"> <div class="space-y-4">
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
存储方式 {{ t('manage.settings.storageMethod') }}
</label> </label>
<select v-model="config.file_storage" <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" 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=" ]" 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'); 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="local">{{ t('manage.settings.localStorage') }}</option>
<option value="s3">S3 存储</option> <option value="s3">{{ t('manage.settings.s3Storage') }}</option>
<option value="webdav">Webdav 存储</option> <option value="webdav">{{ t('manage.settings.webdavStorage') }}</option>
</select> </select>
</div> </div>
<div class="space-y-2" v-if="config.file_storage === 'local'"> <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']"> <label class="block text-sm font-medium mb-2" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
开启切片上传实验性功能还在开发中目前仅本地存储可用可能会出现未知问题 {{ t('manage.settings.chunkUploadNote') }}
</label> </label>
<div class="flex items-center"> <div class="flex items-center">
<button type="button" @click="config.enableChunk = config.enableChunk === 1 ? 0 : 1" <button type="button" @click="config.enableChunk = config.enableChunk === 1 ? 0 : 1"
@@ -337,7 +339,7 @@ refreshData()
]" /> ]" />
</button> </button>
<span class="ml-3 text-sm" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <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> </span>
</div> </div>
</div> </div>
@@ -347,7 +349,7 @@ refreshData()
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
Webdav URL Webdav URL
</label> </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="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="[ :class="[
isDarkMode isDarkMode
@@ -360,7 +362,7 @@ refreshData()
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
Webdav Username Webdav Username
</label> </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="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="[ :class="[
isDarkMode isDarkMode
@@ -373,7 +375,7 @@ refreshData()
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
Webdav Password Webdav Password
</label> </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="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="[ :class="[
isDarkMode isDarkMode
@@ -388,7 +390,7 @@ refreshData()
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
S3 AccessKeyId {{ t('manage.settings.s3AccessKeyId') }}
</label> </label>
<input type="text" v-model="config.s3_access_key_id" <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" 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"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
S3 SecretAccessKey {{ t('manage.settings.s3SecretAccessKey') }}
</label> </label>
<input type="password" v-model="config.s3_secret_access_key" <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" 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"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
S3 BucketName {{ t('manage.settings.s3BucketName') }}
</label> </label>
<input type="text" v-model="config.s3_bucket_name" <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" 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"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
S3 EndpointUrl {{ t('manage.settings.s3EndpointUrl') }}
</label> </label>
<input type="text" v-model="config.s3_endpoint_url" <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" 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"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
S3 Region Name {{ t('manage.settings.s3RegionName') }}
</label> </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="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="[ :class="[
isDarkMode isDarkMode
@@ -453,7 +455,7 @@ refreshData()
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
S3 Signature Version {{ t('manage.settings.s3SignatureVersion') }}
</label> </label>
<select v-model="config.s3_signature_version" <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" 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' ? 'bg-gray-700 border-gray-600 text-white hover:border-gray-500'
: 'border-gray-300 hover:border-gray-400' : 'border-gray-300 hover:border-gray-400'
]"> ]">
<option value="s3v2">S3v2</option> <option value="s3v2">{{ t('manage.settings.s3v2') }}</option>
<option value="s3v4">S3v4</option> <option value="s3v4">{{ t('manage.settings.s3v4') }}</option>
</select> </select>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
S3 Hostname {{ t('manage.settings.s3Hostname') }}
</label> </label>
<input type="text" v-model="config.s3_hostname" <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" 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"> <div class="space-y-2">
<label class="block text-sm font-medium mb-2" <label class="block text-sm font-medium mb-2"
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
启用代理 {{ t('manage.settings.enableProxy') }}
</label> </label>
<div class="flex items-center"> <div class="flex items-center">
<button type="button" @click="config.s3_proxy = config.s3_proxy === 1 ? 0 : 1" <button type="button" @click="config.s3_proxy = config.s3_proxy === 1 ? 0 : 1"
@@ -498,7 +500,7 @@ refreshData()
]" /> ]" />
</button> </button>
<span class="ml-3 text-sm" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <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> </span>
</div> </div>
</div> </div>
@@ -510,13 +512,13 @@ refreshData()
<!-- 上传限制 --> <!-- 上传限制 -->
<div class="mt-8"> <div class="mt-8">
<h3 class="text-lg font-medium mb-4" :class="[isDarkMode ? 'text-white' : 'text-gray-800']"> <h3 class="text-lg font-medium mb-4" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
上传限制 {{ t('manage.settings.uploadLimits') }}
</h3> </h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
每分钟上传限制 {{ t('manage.settings.uploadPerMinute') }}
</label> </label>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<input type="number" v-model="config.uploadMinute" <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' ? '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' : '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> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
上传数量限制 {{ t('manage.settings.uploadCountLimit') }}
</label> </label>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<input type="number" v-model="config.uploadCount" <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' ? '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' : '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> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
文件大小限制 {{ t('manage.settings.fileSizeLimit') }}
</label> </label>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<input type="number" v-model="fileSize" <input type="number" v-model="fileSize"
@@ -565,16 +567,16 @@ refreshData()
? 'bg-gray-700 border-gray-600 text-white hover:border-gray-500' ? 'bg-gray-700 border-gray-600 text-white hover:border-gray-500'
: 'border-gray-300 hover:border-gray-400' : 'border-gray-300 hover:border-gray-400'
]"> ]">
<option value="KB">KB</option> <option value="KB">{{ t('manage.settings.fileSizeUnits.kb') }}</option>
<option value="MB">MB</option> <option value="MB">{{ t('manage.settings.fileSizeUnits.mb') }}</option>
<option value="GB">GB</option> <option value="GB">{{ t('manage.settings.fileSizeUnits.gb') }}</option>
</select> </select>
</div> </div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium mb-2" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium mb-2" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
过期方式 {{ t('manage.settings.expirationType') }}
</label> </label>
<div class="flex flex-wrap gap-3"> <div class="flex flex-wrap gap-3">
<label v-for="style in ['day', 'hour', 'minute', 'forever', 'count']" :key="style" <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-gray-700 border-gray-600 text-gray-300 hover:border-indigo-500'
: 'bg-white border-gray-300 text-gray-700 hover:border-indigo-500' : 'bg-white border-gray-300 text-gray-700 hover:border-indigo-500'
]"> ]">
{{ {{ t(`manage.settings.expiration.${style}`) }}
{
day: '按天',
hour: '按小时',
minute: '按分钟',
forever: '永久',
count: '按次数'
}[style]
}}
</div> </div>
</label> </label>
</div> </div>
@@ -605,7 +599,7 @@ refreshData()
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
最长保存时间 {{ t('manage.settings.maxSaveTime') }}
</label> </label>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<input type="number" v-model="saveTime" <input type="number" v-model="saveTime"
@@ -622,17 +616,17 @@ refreshData()
? 'bg-gray-700 border-gray-600 text-white hover:border-gray-500' ? 'bg-gray-700 border-gray-600 text-white hover:border-gray-500'
: 'border-gray-300 hover:border-gray-400' : 'border-gray-300 hover:border-gray-400'
]"> ]">
<option value="秒"></option> <option value="秒">{{ t('common.second') }}</option>
<option value="分"></option> <option value="分">{{ t('common.minute') }}</option>
<option value="时"></option> <option value="时">{{ t('common.hour') }}</option>
<option value="天"></option> <option value="天">{{ t('common.day') }}</option>
</select> </select>
</div> </div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium mb-2" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium mb-2" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
游客上传 {{ t('manage.settings.guestUpload') }}
</label> </label>
<div class="flex items-center"> <div class="flex items-center">
<button type="button" @click="config.openUpload = config.openUpload === 1 ? 0 : 1" <button type="button" @click="config.openUpload = config.openUpload === 1 ? 0 : 1"
@@ -647,7 +641,7 @@ refreshData()
]" /> ]" />
</button> </button>
<span class="ml-3 text-sm" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <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> </span>
</div> </div>
</div> </div>
@@ -657,13 +651,13 @@ refreshData()
<!-- 错误限制 --> <!-- 错误限制 -->
<div class="mt-8"> <div class="mt-8">
<h3 class="text-lg font-medium mb-4" :class="[isDarkMode ? 'text-white' : 'text-gray-800']"> <h3 class="text-lg font-medium mb-4" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
错误限制 {{ t('manage.settings.errorLimits') }}
</h3> </h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
每分钟错误限制 {{ t('manage.settings.errorPerMinute') }}
</label> </label>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<input type="number" v-model="config.errorMinute" <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' ? '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' : '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> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"> <label class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
错误次数限制 {{ t('manage.settings.errorCountLimit') }}
</label> </label>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<input type="number" v-model="config.errorCount" <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' ? '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' : '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> </div>
</div> </div>
@@ -699,7 +693,7 @@ refreshData()
<div class="flex justify-end mt-8"> <div class="flex justify-end mt-8">
<button @click="submitSave" <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"> 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> </button>
</div> </div>
</section> </section>