feat: 多语言支持 https://github.com/vastsa/FileCodeBox/issues/310 https://github.com/vastsa/FileCodeBox/issues/299
This commit is contained in:
@@ -26,7 +26,7 @@
|
||||
@click="removeAlert(alert.id)"
|
||||
class="inline-flex text-white hover:text-gray-200 focus:outline-none transition-colors duration-200"
|
||||
>
|
||||
<span class="sr-only">关闭</span>
|
||||
<span class="sr-only">{{ t('common.close') }}</span>
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -47,6 +47,9 @@ import { storeToRefs } from 'pinia'
|
||||
import { useAlertStore } from '@/stores/alertStore'
|
||||
import { CheckCircle, AlertTriangle, AlertCircle, Info, X } from 'lucide-vue-next'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const alertStore = useAlertStore()
|
||||
const { alerts } = storeToRefs(alertStore)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
>
|
||||
<div class="flex justify-between items-center mb-4 flex-shrink-0">
|
||||
<h3 class="text-2xl font-bold" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
|
||||
内容预览
|
||||
{{ t('contentPreview.title') }}
|
||||
</h3>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
@@ -24,7 +24,7 @@
|
||||
]"
|
||||
>
|
||||
<CopyIcon class="w-4 h-4" />
|
||||
复制
|
||||
{{ t('common.copy') }}
|
||||
</button>
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
@@ -53,6 +53,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { CopyIcon, XIcon } from 'lucide-vue-next'
|
||||
|
||||
interface Props {
|
||||
@@ -67,6 +68,7 @@ interface Emits {
|
||||
|
||||
defineProps<Props>()
|
||||
defineEmits<Emits>()
|
||||
const { t } = useI18n()
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col space-y-3">
|
||||
<label :class="['text-sm font-medium', isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
过期时间
|
||||
{{ t('send.expiration.label') }}
|
||||
</label>
|
||||
<div class="relative flex-grow group">
|
||||
<div
|
||||
@@ -85,11 +85,11 @@
|
||||
: 'border-gray-200 text-gray-700 focus:ring-indigo-500/60'
|
||||
]"
|
||||
>
|
||||
<option value="count">次数</option>
|
||||
<option value="minute">分钟</option>
|
||||
<option value="hour">小时</option>
|
||||
<option value="day">天</option>
|
||||
<option value="forever">永久</option>
|
||||
<option value="count">{{ t('send.expiration.units.times') }}</option>
|
||||
<option value="minute">{{ t('send.expiration.units.minutes') }}</option>
|
||||
<option value="hour">{{ t('send.expiration.units.hours') }}</option>
|
||||
<option value="day">{{ t('send.expiration.units.days') }}</option>
|
||||
<option value="forever">{{ t('send.expiration.units.forever') }}</option>
|
||||
</select>
|
||||
<div
|
||||
class="absolute right-4 top-1/2 transform -translate-y-1/2 pointer-events-none"
|
||||
@@ -111,6 +111,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Props {
|
||||
expirationMethod: string
|
||||
@@ -145,15 +148,15 @@ const incrementValue = (delta: number) => {
|
||||
const getPlaceholder = () => {
|
||||
switch (props.expirationMethod) {
|
||||
case 'count':
|
||||
return '输入次数'
|
||||
return t('send.expiration.placeholders.count')
|
||||
case 'minute':
|
||||
return '输入分钟数'
|
||||
return t('send.expiration.placeholders.minutes')
|
||||
case 'hour':
|
||||
return '输入小时数'
|
||||
return t('send.expiration.placeholders.hours')
|
||||
case 'day':
|
||||
return '输入天数'
|
||||
return t('send.expiration.placeholders.days')
|
||||
default:
|
||||
return '输入数值'
|
||||
return t('send.expiration.placeholders.default')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -13,7 +13,7 @@
|
||||
class="text-2xl font-bold mb-6 truncate"
|
||||
:class="[isDarkMode ? 'text-white' : 'text-gray-800']"
|
||||
>
|
||||
文件详情
|
||||
{{ t('fileDetail.title') }}
|
||||
</h3>
|
||||
<div class="space-y-4" v-if="record">
|
||||
<div class="flex items-center">
|
||||
@@ -25,7 +25,7 @@
|
||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
|
||||
class="truncate flex-grow"
|
||||
>
|
||||
<span class="font-medium">文件名:</span>{{ record.filename }}
|
||||
<span class="font-medium">{{ t('fileRecord.filename') }}:</span>{{ record.filename }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
@@ -37,7 +37,7 @@
|
||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
|
||||
class="truncate flex-grow"
|
||||
>
|
||||
<span class="font-medium">取件日期:</span>{{ record.date }}
|
||||
<span class="font-medium">{{ t('fileRecord.date') }}:</span>{{ record.date }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
@@ -49,7 +49,7 @@
|
||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
|
||||
class="truncate flex-grow"
|
||||
>
|
||||
<span class="font-medium">文件大小:</span>{{ record.size }}
|
||||
<span class="font-medium">{{ t('fileRecord.size') }}:</span>{{ record.size }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
@@ -58,14 +58,14 @@
|
||||
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
|
||||
/>
|
||||
<p :class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']">
|
||||
<span class="font-medium">文件内容:</span>
|
||||
<span class="font-medium">{{ t('fileDetail.content') }}:</span>
|
||||
</p>
|
||||
<div v-if="record.filename === 'Text'" class="ml-2">
|
||||
<button
|
||||
@click="$emit('preview-content')"
|
||||
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition duration-300"
|
||||
>
|
||||
预览内容
|
||||
{{ t('fileDetail.previewContent') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
@@ -75,7 +75,7 @@
|
||||
rel="noopener noreferrer"
|
||||
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition duration-300"
|
||||
>
|
||||
点击下载
|
||||
{{ t('fileDetail.download') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,13 +86,13 @@
|
||||
class="text-lg font-semibold mb-3"
|
||||
:class="[isDarkMode ? 'text-white' : 'text-gray-800']"
|
||||
>
|
||||
取件二维码
|
||||
{{ t('fileDetail.qrCode') }}
|
||||
</h4>
|
||||
<div class="bg-white p-2 rounded-lg shadow-md">
|
||||
<QRCode :value="getQRCodeValue(record)" :size="128" level="M" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-600']">
|
||||
扫描二维码快速取件
|
||||
{{ t('fileDetail.scanQrCode') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
@click="$emit('close')"
|
||||
class="mt-8 w-full bg-gradient-to-r from-indigo-500 to-purple-600 text-white px-6 py-3 rounded-lg font-medium hover:from-indigo-600 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-opacity-50 transition duration-300 transform hover:scale-105"
|
||||
>
|
||||
关闭
|
||||
{{ t('common.close') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,6 +109,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { FileIcon, CalendarIcon, HardDriveIcon, DownloadIcon } from 'lucide-vue-next'
|
||||
import QRCode from 'qrcode.vue'
|
||||
|
||||
@@ -134,6 +135,7 @@ interface Emits {
|
||||
|
||||
defineProps<Props>()
|
||||
defineEmits<Emits>()
|
||||
const { t } = useI18n()
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
const baseUrl = window.location.origin
|
||||
|
||||
|
||||
@@ -37,19 +37,22 @@
|
||||
]"
|
||||
>
|
||||
<span class="block truncate">
|
||||
{{ selectedFile ? selectedFile.name : placeholder }}
|
||||
{{ selectedFile ? selectedFile.name : placeholderText }}
|
||||
</span>
|
||||
</p>
|
||||
<p :class="['mt-2 text-xs', isDarkMode ? 'text-gray-500' : 'text-gray-400']">
|
||||
{{ description }}
|
||||
{{ descriptionText }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject } from 'vue'
|
||||
import { ref, inject, computed } from 'vue'
|
||||
import { UploadCloudIcon } from 'lucide-vue-next'
|
||||
import BorderProgressBar from './BorderProgressBar.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
interface Props {
|
||||
selectedFile?: File | null
|
||||
progress?: number
|
||||
@@ -63,17 +66,21 @@ interface Emits {
|
||||
fileDrop: [event: DragEvent]
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
selectedFile: null,
|
||||
progress: 0,
|
||||
placeholder: '点击或拖放文件到此处上传',
|
||||
description: '支持各种常见格式',
|
||||
placeholder: '',
|
||||
description: '',
|
||||
acceptedTypes: '*'
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
|
||||
// 使用computed属性处理多语言文本
|
||||
const placeholderText = computed(() => props.placeholder || t('send.uploadArea.placeholder'))
|
||||
const descriptionText = computed(() => props.description || t('send.uploadArea.description'))
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const triggerFileUpload = () => {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<button
|
||||
@click="toggleDropdown"
|
||||
:class="[
|
||||
'flex items-center space-x-2 px-3 py-2 rounded-lg transition-all duration-200',
|
||||
isDarkMode
|
||||
? 'bg-gray-800/60 hover:bg-gray-700/80 text-gray-300 hover:text-white'
|
||||
: 'bg-gray-100 hover:bg-gray-200 text-gray-700 hover:text-gray-900'
|
||||
]"
|
||||
>
|
||||
<GlobeIcon class="w-4 h-4" />
|
||||
<span class="text-sm font-medium">{{ currentLanguage.name }}</span>
|
||||
<ChevronDownIcon
|
||||
:class="[
|
||||
'w-4 h-4 transition-transform duration-200',
|
||||
{ 'rotate-180': isDropdownOpen }
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<transition
|
||||
enter-active-class="transition ease-out duration-200"
|
||||
enter-from-class="opacity-0 scale-95"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
leave-active-class="transition ease-in duration-150"
|
||||
leave-from-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
v-if="isDropdownOpen"
|
||||
:class="[
|
||||
'absolute right-0 mt-2 w-32 rounded-lg shadow-lg ring-1 ring-black ring-opacity-5 z-50',
|
||||
isDarkMode
|
||||
? 'bg-gray-800 border border-gray-700'
|
||||
: 'bg-white border border-gray-200'
|
||||
]"
|
||||
>
|
||||
<div class="py-1">
|
||||
<button
|
||||
v-for="language in availableLocales"
|
||||
:key="language.code"
|
||||
@click="switchLanguage(language.code)"
|
||||
:class="[
|
||||
'w-full text-left px-4 py-2 text-sm transition-colors duration-150',
|
||||
currentLocale === language.code
|
||||
? isDarkMode
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'bg-indigo-50 text-indigo-600'
|
||||
: isDarkMode
|
||||
? 'text-gray-300 hover:bg-gray-700 hover:text-white'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
]"
|
||||
>
|
||||
{{ language.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, inject, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { GlobeIcon, ChevronDownIcon } from 'lucide-vue-next'
|
||||
import { availableLocales, setLocale } from '@/i18n/index'
|
||||
|
||||
const { locale } = useI18n()
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
const isDropdownOpen = ref(false)
|
||||
|
||||
const currentLocale = computed(() => locale.value)
|
||||
const currentLanguage = computed(() => {
|
||||
return availableLocales.find((lang: { code: string; name: string }) => lang.code === currentLocale.value) || availableLocales[0]
|
||||
})
|
||||
|
||||
const toggleDropdown = () => {
|
||||
isDropdownOpen.value = !isDropdownOpen.value
|
||||
}
|
||||
|
||||
const switchLanguage = (langCode: string) => {
|
||||
setLocale(langCode)
|
||||
isDropdownOpen.value = false
|
||||
}
|
||||
|
||||
// 点击外部关闭下拉菜单
|
||||
const handleClickOutside = (event: Event) => {
|
||||
const target = event.target as HTMLElement
|
||||
if (!target.closest('.relative')) {
|
||||
isDropdownOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
</script>
|
||||
@@ -18,7 +18,7 @@
|
||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
|
||||
>
|
||||
<ShieldCheckIcon class="w-4 h-4 mr-1 text-green-400" />
|
||||
安全加密
|
||||
{{ t('send.secureEncryption') }}
|
||||
</span>
|
||||
<button
|
||||
@click="$emit('toggle-drawer')"
|
||||
@@ -35,6 +35,9 @@
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { ShieldCheckIcon, ClipboardListIcon } from 'lucide-vue-next'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Props {
|
||||
linkText?: string
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
for="code"
|
||||
class="block text-sm font-medium mb-2"
|
||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
|
||||
>取件码</label
|
||||
>{{ t('retrieve.codeInput.label') }}</label
|
||||
>
|
||||
<div class="relative">
|
||||
<input
|
||||
@@ -19,7 +19,7 @@
|
||||
{ 'ring-2 ring-red-500': error },
|
||||
isDarkMode ? 'text-gray-300' : 'text-gray-800'
|
||||
]"
|
||||
placeholder="请输入5位取件码"
|
||||
:placeholder="t('retrieve.codeInput.placeholder')"
|
||||
required
|
||||
:readonly="inputStatus.readonly"
|
||||
maxlength="5"
|
||||
@@ -46,7 +46,7 @@
|
||||
:disabled="inputStatus.loading"
|
||||
>
|
||||
<span class="flex items-center justify-center relative z-10">
|
||||
<span>{{ inputStatus.loading ? '处理中...' : '提取文件' }}</span>
|
||||
<span>{{ inputStatus.loading ? t('common.loading') : t('retrieve.submit') }}</span>
|
||||
<ArrowRightIcon
|
||||
class="w-5 h-5 ml-2 transition-transform duration-300 transform group-hover:translate-x-1"
|
||||
/>
|
||||
@@ -61,6 +61,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, watch } from 'vue'
|
||||
import { ArrowRightIcon } from 'lucide-vue-next'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface InputStatus {
|
||||
readonly: boolean
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
]"
|
||||
>
|
||||
发送文件
|
||||
{{ t('nav.sendFile') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -26,7 +26,7 @@
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
]"
|
||||
>
|
||||
发送文本
|
||||
{{ t('send.sendText') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -34,6 +34,9 @@
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import type { SendType } from '@/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Props {
|
||||
selectedType: SendType
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
:value="modelValue"
|
||||
@input="updateValue"
|
||||
:rows="rows"
|
||||
:placeholder="placeholder"
|
||||
:placeholder="placeholderText"
|
||||
:class="[
|
||||
'flex-grow px-4 py-3 rounded-xl placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 transition duration-300 resize-none custom-scrollbar',
|
||||
isDarkMode
|
||||
@@ -16,7 +16,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { inject, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Props {
|
||||
modelValue: string
|
||||
@@ -28,7 +31,7 @@ interface Emits {
|
||||
'update:modelValue': [value: string]
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
rows: 7,
|
||||
placeholder: '在此输入要发送的文本...'
|
||||
})
|
||||
@@ -36,6 +39,9 @@ withDefaults(defineProps<Props>(), {
|
||||
const emit = defineEmits<Emits>()
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
|
||||
// 使用computed属性处理多语言文本
|
||||
const placeholderText = computed(() => props.placeholder || t('send.uploadArea.textInput'))
|
||||
|
||||
const updateValue = (event: Event) => {
|
||||
const target = event.target as HTMLTextAreaElement
|
||||
emit('update:modelValue', target.value)
|
||||
|
||||
@@ -3,17 +3,17 @@ import { inject } from 'vue'
|
||||
import { SunIcon, MoonIcon } from 'lucide-vue-next'
|
||||
|
||||
const isDarkMode = inject('isDarkMode') as { value: boolean }
|
||||
const setColorMode = inject('setColorMode') as (isDark: boolean) => void
|
||||
const toggleTheme = inject('toggleTheme') as () => void
|
||||
|
||||
const toggleColorMode = () => {
|
||||
setColorMode(!isDarkMode.value)
|
||||
toggleTheme()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
@click="toggleColorMode"
|
||||
class="fixed top-4 right-4 z-10 p-2 rounded-full transition-all duration-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transform hover:rotate-180"
|
||||
class="p-2 rounded-full transition-all duration-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transform hover:rotate-180"
|
||||
:class="isDarkMode ? 'bg-gray-800 text-yellow-300' : 'bg-white text-gray-800'"
|
||||
>
|
||||
<SunIcon v-if="!isDarkMode" class="w-6 h-6" />
|
||||
|
||||
Reference in New Issue
Block a user