Lan
2025-09-04 16:14:42 +08:00
parent 0537088b9d
commit 813ef5ba51
25 changed files with 1678 additions and 245 deletions
+4 -1
View File
@@ -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>
+14 -11
View File
@@ -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>
+12 -10
View File
@@ -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
+13 -6
View File
@@ -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 = () => {
+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']"
>
<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
+6 -3
View File
@@ -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
+5 -2
View File
@@ -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
+9 -3
View File
@@ -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 -3
View File
@@ -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" />