mirror of
https://github.com/vastsa/FileCodeBoxFronted.git
synced 2025-12-20 08:50:00 +08:00
feat: 优化代码结构(在Claude的帮助下)
This commit is contained in:
@@ -3,12 +3,13 @@ import { ref, watchEffect, provide, onMounted } from 'vue'
|
||||
import { RouterView } from 'vue-router'
|
||||
import ThemeToggle from './components/common/ThemeToggle.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import api from './utils/api'
|
||||
const isDarkMode = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const router = useRouter()
|
||||
import AlertComponent from '@/components/common/AlertComponent.vue'
|
||||
import { useAlertStore } from '@/stores/alertStore'
|
||||
import { ConfigService } from '@/services'
|
||||
import type { ApiResponse, ConfigState } from '@/types'
|
||||
|
||||
const alertStore = useAlertStore()
|
||||
// 检查系统颜色模式
|
||||
@@ -38,8 +39,9 @@ onMounted(() => {
|
||||
} else {
|
||||
setColorMode(checkSystemColorScheme())
|
||||
}
|
||||
api.post('/', {}).then((res: any) => {
|
||||
if (res.code === 200) {
|
||||
ConfigService.getUserConfig().then((res: ApiResponse<ConfigState>) => {
|
||||
if (res.code === 200 && res.detail) {
|
||||
console.log(res);
|
||||
localStorage.setItem('config', JSON.stringify(res.detail))
|
||||
if (
|
||||
res.detail.notify_title &&
|
||||
|
||||
193
src/components/common/ContentPreviewModal.vue
Normal file
193
src/components/common/ContentPreviewModal.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="visible"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
@click.self="$emit('close')"
|
||||
>
|
||||
<div
|
||||
class="p-6 rounded-2xl max-w-3xl w-full mx-4 shadow-2xl transform transition-all duration-300 ease-out backdrop-filter backdrop-blur-lg bg-opacity-70 max-h-[85vh] overflow-hidden flex flex-col"
|
||||
:class="[isDarkMode ? 'bg-gray-800' : 'bg-white']"
|
||||
>
|
||||
<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>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
@click="$emit('copy-content')"
|
||||
class="px-4 py-1.5 rounded-lg transition duration-300 flex items-center gap-2 text-sm font-medium"
|
||||
:class="[
|
||||
isDarkMode
|
||||
? 'bg-gray-700 hover:bg-gray-600 text-gray-300 hover:text-white'
|
||||
: 'bg-gray-100 hover:bg-gray-200 text-gray-700 hover:text-gray-900'
|
||||
]"
|
||||
>
|
||||
<CopyIcon class="w-4 h-4" />
|
||||
复制
|
||||
</button>
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
class="p-1.5 rounded-lg transition duration-300 hover:bg-opacity-10"
|
||||
:class="[
|
||||
isDarkMode
|
||||
? 'text-gray-400 hover:text-white hover:bg-white'
|
||||
: 'text-gray-500 hover:text-gray-900 hover:bg-black'
|
||||
]"
|
||||
>
|
||||
<XIcon class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar">
|
||||
<div
|
||||
class="prose max-w-none p-6 rounded-xl break-words overflow-wrap-anywhere"
|
||||
:class="[isDarkMode ? 'prose-invert bg-gray-900 bg-opacity-50' : 'bg-gray-50']"
|
||||
v-html="renderedContent"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { CopyIcon, XIcon } from 'lucide-vue-next'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
renderedContent: string
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
close: []
|
||||
'copy-content': []
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
defineEmits<Emits>()
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.3) rgba(243, 244, 246, 0.5);
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(243, 244, 246, 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(156, 163, 175, 0.5);
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(156, 163, 175, 0.7);
|
||||
}
|
||||
|
||||
/* 深色模式下的滚动条样式 */
|
||||
:deep([class*='dark']) .custom-scrollbar {
|
||||
scrollbar-color: rgba(75, 85, 99, 0.5) rgba(31, 41, 55, 0.5);
|
||||
}
|
||||
|
||||
:deep([class*='dark']) .custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(31, 41, 55, 0.5);
|
||||
}
|
||||
|
||||
:deep([class*='dark']) .custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(75, 85, 99, 0.5);
|
||||
}
|
||||
|
||||
:deep([class*='dark']) .custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(75, 85, 99, 0.7);
|
||||
}
|
||||
|
||||
/* 确保滚动容器有背景色 */
|
||||
.custom-scrollbar {
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
/* 文本换行相关样式 */
|
||||
.break-words {
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.overflow-wrap-anywhere {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* 添加 Markdown 样式 */
|
||||
:deep(.prose) {
|
||||
text-align: left;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
:deep(.prose h1),
|
||||
:deep(.prose h2),
|
||||
:deep(.prose h3),
|
||||
:deep(.prose h4),
|
||||
:deep(.prose h5),
|
||||
:deep(.prose h6) {
|
||||
color: rgb(79, 70, 229);
|
||||
/* text-indigo-600 */
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
:deep(.prose p),
|
||||
:deep(.prose div),
|
||||
:deep(.prose span),
|
||||
:deep(.prose code),
|
||||
:deep(.prose pre) {
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
:deep(.prose pre) {
|
||||
white-space: pre-wrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
:deep(.prose code) {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:deep(.prose h1),
|
||||
:deep(.prose h2),
|
||||
:deep(.prose h3),
|
||||
:deep(.prose h4),
|
||||
:deep(.prose h5),
|
||||
:deep(.prose h6) {
|
||||
color: rgb(129, 140, 248);
|
||||
/* text-indigo-400 */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
159
src/components/common/ExpirationSelector.vue
Normal file
159
src/components/common/ExpirationSelector.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div class="flex flex-col space-y-3">
|
||||
<label :class="['text-sm font-medium', isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
过期时间
|
||||
</label>
|
||||
<div class="relative flex-grow group">
|
||||
<div
|
||||
:class="[
|
||||
'relative h-12 rounded-2xl border transition-all duration-300 shadow-sm',
|
||||
isDarkMode
|
||||
? 'bg-gray-800/60 border-gray-700/60 group-hover:border-gray-600/80 group-hover:shadow-lg group-hover:shadow-gray-900/20'
|
||||
: 'bg-white border-gray-200 group-hover:border-gray-300 group-hover:shadow-md group-hover:shadow-gray-200/50'
|
||||
]"
|
||||
>
|
||||
<template v-if="expirationMethod !== 'forever'">
|
||||
<input
|
||||
:value="expirationValue"
|
||||
@input="updateValue"
|
||||
type="number"
|
||||
:placeholder="getPlaceholder()"
|
||||
min="1"
|
||||
:class="[
|
||||
'w-full h-full px-5 pr-32 rounded-2xl placeholder-gray-400 transition-all duration-300',
|
||||
'focus:outline-none focus:ring-2 focus:ring-offset-0',
|
||||
'[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none',
|
||||
'bg-transparent',
|
||||
isDarkMode
|
||||
? 'text-gray-100 focus:ring-indigo-500/80 placeholder-gray-500'
|
||||
: 'text-gray-900 focus:ring-indigo-500/60 placeholder-gray-400'
|
||||
]"
|
||||
/>
|
||||
<div
|
||||
class="absolute right-28 top-0 h-full flex flex-col border-l"
|
||||
:class="[isDarkMode ? 'border-gray-700/60' : 'border-gray-200']"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@click="incrementValue(1)"
|
||||
class="flex-1 px-2 flex items-center justify-center transition-all duration-200"
|
||||
:class="[
|
||||
isDarkMode
|
||||
? 'hover:bg-gray-700/60 text-gray-400 hover:text-gray-200'
|
||||
: 'hover:bg-gray-50 text-gray-500 hover:text-gray-700'
|
||||
]"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 15l7-7 7 7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="incrementValue(-1)"
|
||||
class="flex-1 px-2 flex items-center justify-center transition-all duration-200"
|
||||
:class="[
|
||||
isDarkMode
|
||||
? 'hover:bg-gray-700/60 text-gray-400 hover:text-gray-200'
|
||||
: 'hover:bg-gray-50 text-gray-500 hover:text-gray-700'
|
||||
]"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<select
|
||||
:value="expirationMethod"
|
||||
@change="updateMethod"
|
||||
:class="[
|
||||
'absolute right-0 top-0 h-full px-4 rounded-r-2xl border-l transition-all duration-300',
|
||||
'focus:outline-none focus:ring-2 focus:ring-offset-0',
|
||||
'bg-transparent appearance-none cursor-pointer',
|
||||
isDarkMode
|
||||
? 'border-gray-700/60 text-gray-300 focus:ring-indigo-500/80'
|
||||
: '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>
|
||||
</select>
|
||||
<div
|
||||
class="absolute right-4 top-1/2 transform -translate-y-1/2 pointer-events-none"
|
||||
:class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
|
||||
interface Props {
|
||||
expirationMethod: string
|
||||
expirationValue: number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
'update:expirationMethod': [value: string]
|
||||
'update:expirationValue': [value: number]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
|
||||
const updateMethod = (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement
|
||||
emit('update:expirationMethod', target.value)
|
||||
}
|
||||
|
||||
const updateValue = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('update:expirationValue', parseInt(target.value) || 1)
|
||||
}
|
||||
|
||||
const incrementValue = (delta: number) => {
|
||||
const currentValue = props.expirationValue || 1
|
||||
const newValue = Math.max(1, currentValue + delta)
|
||||
emit('update:expirationValue', newValue)
|
||||
}
|
||||
|
||||
const getPlaceholder = () => {
|
||||
switch (props.expirationMethod) {
|
||||
case 'count':
|
||||
return '输入次数'
|
||||
case 'minute':
|
||||
return '输入分钟数'
|
||||
case 'hour':
|
||||
return '输入小时数'
|
||||
case 'day':
|
||||
return '输入天数'
|
||||
default:
|
||||
return '输入数值'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
170
src/components/common/FileDetailModal.vue
Normal file
170
src/components/common/FileDetailModal.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="visible"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
@click.self="$emit('close')"
|
||||
>
|
||||
<div
|
||||
class="p-8 rounded-2xl max-w-md w-full mx-4 shadow-2xl transform transition-all duration-300 ease-out backdrop-filter backdrop-blur-lg overflow-hidden"
|
||||
:class="[isDarkMode ? 'bg-gray-800 bg-opacity-70' : 'bg-white bg-opacity-95']"
|
||||
>
|
||||
<h3
|
||||
class="text-2xl font-bold mb-6 truncate"
|
||||
:class="[isDarkMode ? 'text-white' : 'text-gray-800']"
|
||||
>
|
||||
文件详情
|
||||
</h3>
|
||||
<div class="space-y-4" v-if="record">
|
||||
<div class="flex items-center">
|
||||
<FileIcon
|
||||
class="w-6 h-6 mr-3 flex-shrink-0"
|
||||
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
|
||||
/>
|
||||
<p
|
||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
|
||||
class="truncate flex-grow"
|
||||
>
|
||||
<span class="font-medium">文件名:</span>{{ record.filename }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<CalendarIcon
|
||||
class="w-6 h-6 mr-3 flex-shrink-0"
|
||||
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
|
||||
/>
|
||||
<p
|
||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
|
||||
class="truncate flex-grow"
|
||||
>
|
||||
<span class="font-medium">取件日期:</span>{{ record.date }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<HardDriveIcon
|
||||
class="w-6 h-6 mr-3 flex-shrink-0"
|
||||
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
|
||||
/>
|
||||
<p
|
||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
|
||||
class="truncate flex-grow"
|
||||
>
|
||||
<span class="font-medium">文件大小:</span>{{ record.size }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<DownloadIcon
|
||||
class="w-6 h-6 mr-3"
|
||||
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
|
||||
/>
|
||||
<p :class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']">
|
||||
<span class="font-medium">文件内容:</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"
|
||||
>
|
||||
预览内容
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<a
|
||||
:href="getDownloadUrl(record)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition duration-300"
|
||||
>
|
||||
点击下载
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex flex-col items-center" v-if="record">
|
||||
<h4
|
||||
class="text-lg font-semibold mb-3"
|
||||
:class="[isDarkMode ? 'text-white' : 'text-gray-800']"
|
||||
>
|
||||
取件二维码
|
||||
</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']">
|
||||
扫描二维码快速取件
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@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"
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { FileIcon, CalendarIcon, HardDriveIcon, DownloadIcon } from 'lucide-vue-next'
|
||||
import QRCode from 'qrcode.vue'
|
||||
|
||||
interface FileRecord {
|
||||
id: number
|
||||
code: string
|
||||
filename: string
|
||||
size: string
|
||||
downloadUrl: string | null
|
||||
content: string | null
|
||||
date: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
record: FileRecord | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
close: []
|
||||
'preview-content': []
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
defineEmits<Emits>()
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
const baseUrl = window.location.origin
|
||||
|
||||
const getDownloadUrl = (record: FileRecord) => {
|
||||
if (record.downloadUrl) {
|
||||
if (record.downloadUrl.startsWith('http')) {
|
||||
return record.downloadUrl
|
||||
} else {
|
||||
return `${baseUrl}${record.downloadUrl}`
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const getQRCodeValue = (record: FileRecord) => {
|
||||
if (record.downloadUrl) {
|
||||
return `${baseUrl}${record.downloadUrl}`
|
||||
} else {
|
||||
return `${baseUrl}?code=${record.code}`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
112
src/components/common/FileRecordList.vue
Normal file
112
src/components/common/FileRecordList.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="flex-grow overflow-y-auto p-6">
|
||||
<transition-group name="list" tag="div" class="space-y-4">
|
||||
<div
|
||||
v-for="record in records"
|
||||
:key="record.id"
|
||||
class="bg-opacity-50 rounded-lg p-4 flex items-center shadow-md hover:shadow-lg transition duration-300 transform hover:scale-102"
|
||||
:class="[isDarkMode ? 'bg-gray-800 hover:bg-gray-700' : 'bg-gray-100 hover:bg-white']"
|
||||
>
|
||||
<div class="flex-shrink-0 mr-4">
|
||||
<FileIcon
|
||||
class="w-10 h-10"
|
||||
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-grow min-w-0 mr-4">
|
||||
<p
|
||||
class="font-medium text-lg truncate"
|
||||
:class="[isDarkMode ? 'text-white' : 'text-gray-800']"
|
||||
>
|
||||
{{ record.filename }}
|
||||
</p>
|
||||
<p
|
||||
class="text-sm truncate"
|
||||
:class="[isDarkMode ? 'text-gray-400' : 'text-gray-600']"
|
||||
>
|
||||
{{ record.date }} · {{ record.size }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0 flex space-x-2">
|
||||
<button
|
||||
@click="$emit('view-details', record)"
|
||||
class="p-2 rounded-full hover:bg-opacity-20 transition duration-300"
|
||||
:class="[
|
||||
isDarkMode
|
||||
? 'hover:bg-indigo-400 text-indigo-400'
|
||||
: 'hover:bg-indigo-100 text-indigo-600'
|
||||
]"
|
||||
>
|
||||
<EyeIcon class="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
@click="$emit('download-record', record)"
|
||||
class="p-2 rounded-full hover:bg-opacity-20 transition duration-300"
|
||||
:class="[
|
||||
isDarkMode
|
||||
? 'hover:bg-green-400 text-green-400'
|
||||
: 'hover:bg-green-100 text-green-600'
|
||||
]"
|
||||
>
|
||||
<DownloadIcon class="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
@click="$emit('delete-record', record.id)"
|
||||
class="p-2 rounded-full hover:bg-opacity-20 transition duration-300"
|
||||
:class="[
|
||||
isDarkMode ? 'hover:bg-red-400 text-red-400' : 'hover:bg-red-100 text-red-600'
|
||||
]"
|
||||
>
|
||||
<TrashIcon class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { FileIcon, EyeIcon, DownloadIcon, TrashIcon } from 'lucide-vue-next'
|
||||
|
||||
interface FileRecord {
|
||||
id: number
|
||||
code: string
|
||||
filename: string
|
||||
size: string
|
||||
downloadUrl: string | null
|
||||
content: string | null
|
||||
date: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
records: FileRecord[]
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
'view-details': [record: FileRecord]
|
||||
'download-record': [record: FileRecord]
|
||||
'delete-record': [id: number]
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
defineEmits<Emits>()
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
.list-move {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
94
src/components/common/FileUploadArea.vue
Normal file
94
src/components/common/FileUploadArea.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div
|
||||
class="rounded-xl p-8 flex flex-col items-center justify-center border-2 border-dashed transition-all duration-300 group cursor-pointer relative"
|
||||
:class="[
|
||||
isDarkMode
|
||||
? 'bg-gray-800 bg-opacity-50 border-gray-600 hover:border-indigo-500'
|
||||
: 'bg-gray-100 border-gray-300 hover:border-indigo-500'
|
||||
]"
|
||||
@click="triggerFileUpload"
|
||||
@dragover.prevent
|
||||
@drop.prevent="handleFileDrop"
|
||||
>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
class="hidden"
|
||||
@change="handleFileUpload"
|
||||
:accept="acceptedTypes"
|
||||
/>
|
||||
<div class="absolute inset-0 w-full h-full" v-if="progress > 0">
|
||||
<BorderProgressBar :progress="progress" />
|
||||
</div>
|
||||
<UploadCloudIcon
|
||||
:class="[
|
||||
'w-16 h-16 transition-colors duration-300',
|
||||
isDarkMode
|
||||
? 'text-gray-400 group-hover:text-indigo-400'
|
||||
: 'text-gray-600 group-hover:text-indigo-600'
|
||||
]"
|
||||
/>
|
||||
<p
|
||||
:class="[
|
||||
'mt-4 text-sm transition-colors duration-300 w-full text-center',
|
||||
isDarkMode
|
||||
? 'text-gray-400 group-hover:text-indigo-400'
|
||||
: 'text-gray-600 group-hover:text-indigo-600'
|
||||
]"
|
||||
>
|
||||
<span class="block truncate">
|
||||
{{ selectedFile ? selectedFile.name : placeholder }}
|
||||
</span>
|
||||
</p>
|
||||
<p :class="['mt-2 text-xs', isDarkMode ? 'text-gray-500' : 'text-gray-400']">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject } from 'vue'
|
||||
import { UploadCloudIcon } from 'lucide-vue-next'
|
||||
import BorderProgressBar from './BorderProgressBar.vue'
|
||||
interface Props {
|
||||
selectedFile?: File | null
|
||||
progress?: number
|
||||
placeholder?: string
|
||||
description?: string
|
||||
acceptedTypes?: string
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
fileSelected: [file: File]
|
||||
fileDrop: [event: DragEvent]
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
selectedFile: null,
|
||||
progress: 0,
|
||||
placeholder: '点击或拖放文件到此处上传',
|
||||
description: '支持各种常见格式',
|
||||
acceptedTypes: '*'
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const triggerFileUpload = () => {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
const handleFileUpload = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const file = target.files?.[0]
|
||||
if (file) {
|
||||
emit('fileSelected', file)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileDrop = (event: DragEvent) => {
|
||||
emit('fileDrop', event)
|
||||
}
|
||||
</script>
|
||||
53
src/components/common/SendTypeSelector.vue
Normal file
53
src/components/common/SendTypeSelector.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="flex justify-center space-x-4 mb-6">
|
||||
<button
|
||||
type="button"
|
||||
@click="selectType('file')"
|
||||
:class="[
|
||||
'px-4 py-2 rounded-lg transition-colors duration-300',
|
||||
selectedType === 'file'
|
||||
? 'bg-indigo-600 text-white'
|
||||
: isDarkMode
|
||||
? 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
]"
|
||||
>
|
||||
发送文件
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="selectType('text')"
|
||||
:class="[
|
||||
'px-4 py-2 rounded-lg transition-colors duration-300',
|
||||
selectedType === 'text'
|
||||
? 'bg-indigo-600 text-white'
|
||||
: isDarkMode
|
||||
? 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
]"
|
||||
>
|
||||
发送文本
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import type { SendType } from '@/types'
|
||||
|
||||
interface Props {
|
||||
selectedType: SendType
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
'update:selectedType': [type: SendType]
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
|
||||
const selectType = (type: SendType) => {
|
||||
emit('update:selectedType', type)
|
||||
}
|
||||
</script>
|
||||
63
src/components/common/SideDrawer.vue
Normal file
63
src/components/common/SideDrawer.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<transition name="drawer">
|
||||
<div
|
||||
v-if="visible"
|
||||
class="fixed inset-y-0 right-0 w-full sm:w-120 bg-opacity-70 backdrop-filter backdrop-blur-xl shadow-2xl z-50 overflow-hidden flex flex-col"
|
||||
:class="[isDarkMode ? 'bg-gray-900' : 'bg-white']"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between items-center p-6 border-b"
|
||||
:class="[isDarkMode ? 'border-gray-700' : 'border-gray-200']"
|
||||
>
|
||||
<h3 class="text-2xl font-bold" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
class="hover:text-white transition duration-300"
|
||||
:class="[isDarkMode ? 'text-gray-400' : 'text-gray-800']"
|
||||
>
|
||||
<XIcon class="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { XIcon } from 'lucide-vue-next'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
title: string
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
close: []
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
defineEmits<Emits>()
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.drawer-enter-active,
|
||||
.drawer-leave-active {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.drawer-enter-from,
|
||||
.drawer-leave-to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:w-120 {
|
||||
width: 30rem;
|
||||
/* 480px */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
88
src/components/common/TextInputArea.vue
Normal file
88
src/components/common/TextInputArea.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<textarea
|
||||
:value="modelValue"
|
||||
@input="updateValue"
|
||||
:rows="rows"
|
||||
:placeholder="placeholder"
|
||||
: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
|
||||
? 'bg-gray-800 bg-opacity-50 text-white'
|
||||
: 'bg-white text-gray-900 border border-gray-300'
|
||||
]"
|
||||
></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: string
|
||||
rows?: number
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
'update:modelValue': [value: string]
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
rows: 7,
|
||||
placeholder: '在此输入要发送的文本...'
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
|
||||
const updateValue = (event: Event) => {
|
||||
const target = event.target as HTMLTextAreaElement
|
||||
emit('update:modelValue', target.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义滚动条样式 */
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.3) rgba(243, 244, 246, 0.5);
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(243, 244, 246, 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(156, 163, 175, 0.5);
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(156, 163, 175, 0.7);
|
||||
}
|
||||
|
||||
/* 深色模式下的滚动条样式 */
|
||||
:deep([class*='dark']) .custom-scrollbar {
|
||||
scrollbar-color: rgba(75, 85, 99, 0.5) rgba(31, 41, 55, 0.5);
|
||||
}
|
||||
|
||||
:deep([class*='dark']) .custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(31, 41, 55, 0.5);
|
||||
}
|
||||
|
||||
:deep([class*='dark']) .custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(75, 85, 99, 0.5);
|
||||
}
|
||||
|
||||
:deep([class*='dark']) .custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(75, 85, 99, 0.7);
|
||||
}
|
||||
</style>
|
||||
144
src/composables/useFileDownload.ts
Normal file
144
src/composables/useFileDownload.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { FileService } from '@/services'
|
||||
import { useAlertStore } from '@/stores/alertStore'
|
||||
import type { FileInfo } from '@/types'
|
||||
import { saveAs } from 'file-saver'
|
||||
|
||||
export function useFileDownload() {
|
||||
const alertStore = useAlertStore()
|
||||
|
||||
// 状态管理
|
||||
const isLoading = ref(false)
|
||||
const fileInfo = ref<FileInfo | null>(null)
|
||||
const downloadCode = ref('')
|
||||
|
||||
// 计算属性
|
||||
const hasFileInfo = computed(() => fileInfo.value !== null)
|
||||
const canDownload = computed(() => hasFileInfo.value && !isLoading.value)
|
||||
|
||||
// 获取文件信息
|
||||
const getFileInfo = async (code: string): Promise<FileInfo | null> => {
|
||||
if (!code.trim()) {
|
||||
alertStore.showAlert('请输入取件码', 'warning')
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
downloadCode.value = code
|
||||
|
||||
const response = await FileService.getFile(code)
|
||||
|
||||
if (response.code === 200 && response.detail) {
|
||||
fileInfo.value = response.detail
|
||||
return response.detail
|
||||
} else {
|
||||
throw new Error(response.message || '文件不存在或已过期')
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '获取文件信息失败'
|
||||
alertStore.showAlert(errorMessage, 'error')
|
||||
fileInfo.value = null
|
||||
return null
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const downloadFile = async (code?: string): Promise<boolean> => {
|
||||
const targetCode = code || downloadCode.value
|
||||
|
||||
if (!targetCode.trim()) {
|
||||
alertStore.showAlert('请输入取件码', 'warning')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
// 如果没有文件信息,先获取
|
||||
if (!fileInfo.value || downloadCode.value !== targetCode) {
|
||||
const info = await getFileInfo(targetCode)
|
||||
if (!info) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const blob = await FileService.downloadFile(targetCode)
|
||||
|
||||
// 使用 file-saver 保存文件
|
||||
if (fileInfo.value?.name) {
|
||||
saveAs(blob, fileInfo.value.name)
|
||||
alertStore.showAlert('文件下载成功!', 'success')
|
||||
return true
|
||||
} else {
|
||||
throw new Error('文件名获取失败')
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '下载失败'
|
||||
alertStore.showAlert(errorMessage, 'error')
|
||||
return false
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
const resetDownload = () => {
|
||||
isLoading.value = false
|
||||
fileInfo.value = null
|
||||
downloadCode.value = ''
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeString: string): string => {
|
||||
try {
|
||||
const date = new Date(timeString)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
} catch {
|
||||
return timeString
|
||||
}
|
||||
}
|
||||
|
||||
// 检查文件是否过期
|
||||
const isFileExpired = computed(() => {
|
||||
if (!fileInfo.value?.expireTime) return false
|
||||
return new Date(fileInfo.value.expireTime) < new Date()
|
||||
})
|
||||
|
||||
return {
|
||||
// 状态
|
||||
isLoading,
|
||||
fileInfo,
|
||||
downloadCode,
|
||||
|
||||
// 计算属性
|
||||
hasFileInfo,
|
||||
canDownload,
|
||||
isFileExpired,
|
||||
|
||||
// 方法
|
||||
getFileInfo,
|
||||
downloadFile,
|
||||
resetDownload,
|
||||
formatFileSize,
|
||||
formatTime
|
||||
}
|
||||
}
|
||||
139
src/composables/useFileUpload.ts
Normal file
139
src/composables/useFileUpload.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { ref, computed, readonly } from 'vue'
|
||||
import { FileService } from '@/services'
|
||||
import { useAlertStore } from '@/stores/alertStore'
|
||||
import type { UploadProgress, UploadStatus } from '@/types'
|
||||
import { UPLOAD_STATUS, FILE_SIZE_LIMITS } from '@/constants'
|
||||
|
||||
export function useFileUpload() {
|
||||
const alertStore = useAlertStore()
|
||||
|
||||
// 状态管理
|
||||
const uploadStatus = ref<UploadStatus>(UPLOAD_STATUS.IDLE)
|
||||
const uploadProgress = ref<UploadProgress>({
|
||||
loaded: 0,
|
||||
total: 0,
|
||||
percentage: 0
|
||||
})
|
||||
const uploadedCode = ref<string>('')
|
||||
const currentFile = ref<File | null>(null)
|
||||
|
||||
// 计算属性
|
||||
const isUploading = computed(() => uploadStatus.value === UPLOAD_STATUS.UPLOADING)
|
||||
const isSuccess = computed(() => uploadStatus.value === UPLOAD_STATUS.SUCCESS)
|
||||
const isError = computed(() => uploadStatus.value === UPLOAD_STATUS.ERROR)
|
||||
const isIdle = computed(() => uploadStatus.value === UPLOAD_STATUS.IDLE)
|
||||
|
||||
// 文件验证
|
||||
const validateFile = (file: File): boolean => {
|
||||
if (file.size > FILE_SIZE_LIMITS.MAX_FILE_SIZE) {
|
||||
alertStore.showAlert(
|
||||
`文件大小不能超过 ${Math.round(FILE_SIZE_LIMITS.MAX_FILE_SIZE / 1024 / 1024)}MB`,
|
||||
'error'
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
const uploadFile = async (file: File): Promise<string | null> => {
|
||||
if (!validateFile(file)) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
uploadStatus.value = UPLOAD_STATUS.UPLOADING
|
||||
currentFile.value = file
|
||||
uploadedCode.value = ''
|
||||
|
||||
const response = await FileService.uploadFile(file, (progress) => {
|
||||
uploadProgress.value = progress
|
||||
})
|
||||
|
||||
if (response.code === 200 && response.detail?.code) {
|
||||
uploadStatus.value = UPLOAD_STATUS.SUCCESS
|
||||
uploadedCode.value = String(response.detail.code)
|
||||
alertStore.showAlert('文件上传成功!', 'success')
|
||||
return String(response.detail.code)
|
||||
} else {
|
||||
throw new Error(response.message || '上传失败')
|
||||
}
|
||||
} catch (error) {
|
||||
uploadStatus.value = UPLOAD_STATUS.ERROR
|
||||
const errorMessage = error instanceof Error ? error.message : '上传失败'
|
||||
alertStore.showAlert(errorMessage, 'error')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 上传文本
|
||||
const uploadText = async (text: string): Promise<string | null> => {
|
||||
if (!text.trim()) {
|
||||
alertStore.showAlert('请输入要发送的文本内容', 'warning')
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
uploadStatus.value = UPLOAD_STATUS.UPLOADING
|
||||
uploadedCode.value = ''
|
||||
|
||||
const response = await FileService.uploadText(text)
|
||||
|
||||
if (response.code === 200 && response.detail?.code) {
|
||||
uploadStatus.value = UPLOAD_STATUS.SUCCESS
|
||||
uploadedCode.value = String(response.detail.code)
|
||||
alertStore.showAlert('文本发送成功!', 'success')
|
||||
return String(response.detail.code)
|
||||
} else {
|
||||
throw new Error(response.message || '发送失败')
|
||||
}
|
||||
} catch (error) {
|
||||
uploadStatus.value = UPLOAD_STATUS.ERROR
|
||||
const errorMessage = error instanceof Error ? error.message : '发送失败'
|
||||
alertStore.showAlert(errorMessage, 'error')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
const resetUpload = () => {
|
||||
uploadStatus.value = UPLOAD_STATUS.IDLE
|
||||
uploadProgress.value = {
|
||||
loaded: 0,
|
||||
total: 0,
|
||||
percentage: 0
|
||||
}
|
||||
uploadedCode.value = ''
|
||||
currentFile.value = null
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
uploadStatus: readonly(uploadStatus),
|
||||
uploadProgress: readonly(uploadProgress),
|
||||
uploadedCode: readonly(uploadedCode),
|
||||
currentFile: readonly(currentFile),
|
||||
|
||||
// 计算属性
|
||||
isUploading,
|
||||
isSuccess,
|
||||
isError,
|
||||
isIdle,
|
||||
|
||||
// 方法
|
||||
uploadFile,
|
||||
uploadText,
|
||||
resetUpload,
|
||||
validateFile,
|
||||
formatFileSize
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { ConfigService } from '@/services'
|
||||
import { useAlertStore } from '@/stores/alertStore'
|
||||
import type { SystemConfig } from '@/types'
|
||||
import { STORAGE_KEYS, DEFAULT_CONFIG } from '@/constants'
|
||||
|
||||
export function useSystemConfig() {
|
||||
const alertStore = useAlertStore()
|
||||
|
||||
// 状态管理
|
||||
const config = ref<SystemConfig>({ ...DEFAULT_CONFIG })
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 从本地存储获取配置
|
||||
const getStoredConfig = (): SystemConfig | null => {
|
||||
try {
|
||||
const storedConfig = localStorage.getItem(STORAGE_KEYS.CONFIG)
|
||||
if (storedConfig) {
|
||||
return JSON.parse(storedConfig)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析本地配置失败:', error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 保存配置到本地存储
|
||||
const saveConfigToStorage = (configData: SystemConfig) => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEYS.CONFIG, JSON.stringify(configData))
|
||||
} catch (error) {
|
||||
console.error('保存配置到本地存储失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取系统配置
|
||||
const fetchConfig = async (): Promise<SystemConfig | null> => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
const response = await ConfigService.getConfig()
|
||||
|
||||
if (response.code === 200 && response.detail) {
|
||||
config.value = { ...DEFAULT_CONFIG, ...response.detail }
|
||||
saveConfigToStorage(config.value)
|
||||
|
||||
// 处理通知
|
||||
if (response.detail.notify_title && response.detail.notify_content) {
|
||||
const notifyKey = response.detail.notify_title + response.detail.notify_content
|
||||
const lastNotify = localStorage.getItem(STORAGE_KEYS.NOTIFY)
|
||||
|
||||
if (lastNotify !== notifyKey) {
|
||||
localStorage.setItem(STORAGE_KEYS.NOTIFY, notifyKey)
|
||||
alertStore.showAlert(
|
||||
`${response.detail.notify_title}: ${response.detail.notify_content}`,
|
||||
'success'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return config.value
|
||||
} else {
|
||||
throw new Error(response.message || '获取配置失败')
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果网络请求失败,尝试使用本地存储的配置
|
||||
const storedConfig = getStoredConfig()
|
||||
if (storedConfig) {
|
||||
config.value = storedConfig
|
||||
return config.value
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : '获取配置失败'
|
||||
alertStore.showAlert(errorMessage, 'error')
|
||||
return null
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新系统配置
|
||||
const updateConfig = async (newConfig: Partial<SystemConfig>): Promise<boolean> => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
const response = await ConfigService.updateConfig(newConfig)
|
||||
|
||||
if (response.code === 200) {
|
||||
config.value = { ...config.value, ...newConfig }
|
||||
saveConfigToStorage(config.value)
|
||||
alertStore.showAlert('配置更新成功!', 'success')
|
||||
return true
|
||||
} else {
|
||||
throw new Error(response.message || '更新配置失败')
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '更新配置失败'
|
||||
alertStore.showAlert(errorMessage, 'error')
|
||||
return false
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化配置
|
||||
const initConfig = async () => {
|
||||
// 先尝试从本地存储加载
|
||||
const storedConfig = getStoredConfig()
|
||||
if (storedConfig) {
|
||||
config.value = storedConfig
|
||||
}
|
||||
|
||||
// 然后从服务器获取最新配置
|
||||
await fetchConfig()
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const maxFileSizeMB = computed(() => {
|
||||
return Math.round(config.value.maxFileSize / 1024 / 1024)
|
||||
})
|
||||
|
||||
const isConfigLoaded = computed(() => {
|
||||
return config.value.name !== DEFAULT_CONFIG.name || !isLoading.value
|
||||
})
|
||||
|
||||
return {
|
||||
// 状态
|
||||
config,
|
||||
isLoading,
|
||||
|
||||
// 计算属性
|
||||
maxFileSizeMB,
|
||||
isConfigLoaded,
|
||||
|
||||
// 方法
|
||||
fetchConfig,
|
||||
updateConfig,
|
||||
initConfig,
|
||||
getStoredConfig,
|
||||
saveConfigToStorage
|
||||
}
|
||||
}
|
||||
138
src/composables/useTheme.ts
Normal file
138
src/composables/useTheme.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { STORAGE_KEYS, THEME_MODES } from '@/constants'
|
||||
import type { ThemeMode } from '@/types'
|
||||
|
||||
export function useTheme() {
|
||||
// 状态管理
|
||||
const isDarkMode = ref(false)
|
||||
const themeMode = ref<ThemeMode>(THEME_MODES.SYSTEM)
|
||||
|
||||
// 检查系统颜色模式
|
||||
const checkSystemColorScheme = (): boolean => {
|
||||
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
}
|
||||
|
||||
// 从本地存储获取用户之前的选择
|
||||
const getUserPreference = (): ThemeMode | null => {
|
||||
const storedPreference = localStorage.getItem(STORAGE_KEYS.COLOR_MODE)
|
||||
if (storedPreference && Object.values(THEME_MODES).includes(storedPreference as ThemeMode)) {
|
||||
return storedPreference as ThemeMode
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 设置颜色模式
|
||||
const setThemeMode = (mode: ThemeMode) => {
|
||||
themeMode.value = mode
|
||||
localStorage.setItem(STORAGE_KEYS.COLOR_MODE, mode)
|
||||
|
||||
// 根据模式设置实际的暗色模式状态
|
||||
if (mode === THEME_MODES.SYSTEM) {
|
||||
isDarkMode.value = checkSystemColorScheme()
|
||||
} else {
|
||||
isDarkMode.value = mode === THEME_MODES.DARK
|
||||
}
|
||||
|
||||
// 更新 HTML 类名
|
||||
updateHtmlClass()
|
||||
}
|
||||
|
||||
// 更新 HTML 元素的类名
|
||||
const updateHtmlClass = () => {
|
||||
const html = document.documentElement
|
||||
if (isDarkMode.value) {
|
||||
html.classList.add('dark')
|
||||
} else {
|
||||
html.classList.remove('dark')
|
||||
}
|
||||
}
|
||||
|
||||
// 切换主题
|
||||
const toggleTheme = () => {
|
||||
if (themeMode.value === THEME_MODES.LIGHT) {
|
||||
setThemeMode(THEME_MODES.DARK)
|
||||
} else if (themeMode.value === THEME_MODES.DARK) {
|
||||
setThemeMode(THEME_MODES.SYSTEM)
|
||||
} else {
|
||||
setThemeMode(THEME_MODES.LIGHT)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听系统主题变化
|
||||
const setupSystemThemeListener = () => {
|
||||
if (window.matchMedia) {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
if (themeMode.value === THEME_MODES.SYSTEM) {
|
||||
isDarkMode.value = e.matches
|
||||
updateHtmlClass()
|
||||
}
|
||||
}
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange)
|
||||
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
mediaQuery.removeEventListener('change', handleChange)
|
||||
}
|
||||
}
|
||||
return () => {}
|
||||
}
|
||||
|
||||
// 初始化主题
|
||||
const initTheme = () => {
|
||||
const userPreference = getUserPreference()
|
||||
if (userPreference) {
|
||||
setThemeMode(userPreference)
|
||||
} else {
|
||||
setThemeMode(THEME_MODES.SYSTEM)
|
||||
}
|
||||
|
||||
// 设置系统主题监听
|
||||
return setupSystemThemeListener()
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const themeIcon = computed(() => {
|
||||
switch (themeMode.value) {
|
||||
case THEME_MODES.LIGHT:
|
||||
return 'sun'
|
||||
case THEME_MODES.DARK:
|
||||
return 'moon'
|
||||
case THEME_MODES.SYSTEM:
|
||||
return 'monitor'
|
||||
default:
|
||||
return 'monitor'
|
||||
}
|
||||
})
|
||||
|
||||
const themeLabel = computed(() => {
|
||||
switch (themeMode.value) {
|
||||
case THEME_MODES.LIGHT:
|
||||
return '浅色模式'
|
||||
case THEME_MODES.DARK:
|
||||
return '深色模式'
|
||||
case THEME_MODES.SYSTEM:
|
||||
return '跟随系统'
|
||||
default:
|
||||
return '跟随系统'
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
// 状态
|
||||
isDarkMode,
|
||||
themeMode,
|
||||
|
||||
// 计算属性
|
||||
themeIcon,
|
||||
themeLabel,
|
||||
|
||||
// 方法
|
||||
setThemeMode,
|
||||
toggleTheme,
|
||||
initTheme,
|
||||
checkSystemColorScheme
|
||||
}
|
||||
}
|
||||
88
src/constants/index.ts
Normal file
88
src/constants/index.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
// 应用常量配置
|
||||
|
||||
// 存储键名
|
||||
export const STORAGE_KEYS = {
|
||||
COLOR_MODE: 'colorMode',
|
||||
ADMIN_PASSWORD: 'adminPassword',
|
||||
TOKEN: 'token',
|
||||
CONFIG: 'config',
|
||||
NOTIFY: 'notify'
|
||||
} as const
|
||||
|
||||
// 主题模式
|
||||
export const THEME_MODES = {
|
||||
LIGHT: 'light',
|
||||
DARK: 'dark',
|
||||
SYSTEM: 'system'
|
||||
} as const
|
||||
|
||||
// 发送类型
|
||||
export const SEND_TYPES = {
|
||||
FILE: 'file',
|
||||
TEXT: 'text'
|
||||
} as const
|
||||
|
||||
// 警告类型
|
||||
export const ALERT_TYPES = {
|
||||
SUCCESS: 'success',
|
||||
ERROR: 'error',
|
||||
WARNING: 'warning',
|
||||
INFO: 'info'
|
||||
} as const
|
||||
|
||||
// 上传状态
|
||||
export const UPLOAD_STATUS = {
|
||||
IDLE: 'idle',
|
||||
UPLOADING: 'uploading',
|
||||
SUCCESS: 'success',
|
||||
ERROR: 'error'
|
||||
} as const
|
||||
|
||||
// API 响应状态码
|
||||
export const API_STATUS_CODES = {
|
||||
SUCCESS: 200,
|
||||
UNAUTHORIZED: 401,
|
||||
FORBIDDEN: 403,
|
||||
NOT_FOUND: 404,
|
||||
SERVER_ERROR: 500
|
||||
} as const
|
||||
|
||||
// 文件大小限制 (字节)
|
||||
export const FILE_SIZE_LIMITS = {
|
||||
MAX_FILE_SIZE: 100 * 1024 * 1024, // 100MB
|
||||
CHUNK_SIZE: 1024 * 1024 // 1MB
|
||||
} as const
|
||||
|
||||
// 时间相关常量
|
||||
export const TIME_CONSTANTS = {
|
||||
ALERT_DURATION: 5000, // 5秒
|
||||
REQUEST_TIMEOUT: 30000, // 30秒
|
||||
PROGRESS_UPDATE_INTERVAL: 100 // 100毫秒
|
||||
} as const
|
||||
|
||||
// 路由路径
|
||||
export const ROUTES = {
|
||||
HOME: '/',
|
||||
SEND: '/send',
|
||||
ADMIN: '/admin',
|
||||
LOGIN: '/login',
|
||||
DASHBOARD: '/admin/dashboard',
|
||||
FILE_MANAGE: '/admin/files',
|
||||
SETTINGS: '/admin/settings'
|
||||
} as const
|
||||
|
||||
// 正则表达式
|
||||
export const REGEX_PATTERNS = {
|
||||
EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
PHONE: /^1[3-9]\d{9}$/,
|
||||
CODE: /^[A-Za-z0-9]{4,8}$/ // 取件码格式
|
||||
} as const
|
||||
|
||||
// 默认配置
|
||||
export const DEFAULT_CONFIG = {
|
||||
name: 'FileCodeBox',
|
||||
description: '文件传输工具',
|
||||
maxFileSize: FILE_SIZE_LIMITS.MAX_FILE_SIZE,
|
||||
allowedFileTypes: ['*'] as string[],
|
||||
expireDays: 7
|
||||
}
|
||||
14
src/hooks/index.ts
Normal file
14
src/hooks/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// 自定义 Vue hooks 导出文件
|
||||
// 这里可以导出所有自定义的 Vue hooks
|
||||
|
||||
// 示例:导出常用的自定义 hooks
|
||||
// export { useLocalStorage } from './useLocalStorage'
|
||||
// export { useDebounce } from './useDebounce'
|
||||
// export { useThrottle } from './useThrottle'
|
||||
// export { useEventListener } from './useEventListener'
|
||||
// export { useClickOutside } from './useClickOutside'
|
||||
|
||||
// 目前项目中的业务逻辑已经通过 composables 进行了封装
|
||||
// hooks 文件夹主要用于存放更通用的、可复用的 Vue hooks
|
||||
|
||||
export {}
|
||||
137
src/services/index.ts
Normal file
137
src/services/index.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
// API 服务层
|
||||
import api from '@/utils/api'
|
||||
import type { ApiResponse, FileInfo, ConfigState, AdminUser, FileUploadResponse, TextSendResponse, DashboardData, FileListResponse, FileEditForm } from '@/types'
|
||||
import type { UploadProgress } from '@/types'
|
||||
|
||||
// 系统配置服务
|
||||
export class ConfigService {
|
||||
static async getConfig(): Promise<ApiResponse<ConfigState>> {
|
||||
return api.get('/admin/config/get')
|
||||
}
|
||||
static async getUserConfig(): Promise<ApiResponse<ConfigState>> {
|
||||
return api.post('/')
|
||||
}
|
||||
static async updateConfig(config: Partial<ConfigState>): Promise<ApiResponse> {
|
||||
return api.patch('/admin/config/update', config)
|
||||
}
|
||||
}
|
||||
|
||||
// 文件服务
|
||||
export class FileService {
|
||||
static async uploadFile(
|
||||
file: File,
|
||||
onProgress?: (progress: UploadProgress) => void
|
||||
): Promise<ApiResponse<FileUploadResponse>> {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
return api.post('/share/file/', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (onProgress && progressEvent.total) {
|
||||
const progress: UploadProgress = {
|
||||
loaded: progressEvent.loaded,
|
||||
total: progressEvent.total,
|
||||
percentage: Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
||||
}
|
||||
onProgress(progress)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static async uploadText(text: string): Promise<ApiResponse<TextSendResponse>> {
|
||||
return api.post('/share/text/', { content: text })
|
||||
}
|
||||
|
||||
static async getFile(code: string): Promise<ApiResponse<FileInfo>> {
|
||||
return api.get(`/file/${code}`)
|
||||
}
|
||||
|
||||
static async downloadFile(code: string): Promise<Blob> {
|
||||
const response = await api.get(`/download/${code}`, {
|
||||
responseType: 'blob'
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
static async deleteFile(fileId: string): Promise<ApiResponse> {
|
||||
return api.post('/admin/file/delete', { id: fileId })
|
||||
}
|
||||
|
||||
static async getFileList(page = 1, limit = 10): Promise<ApiResponse<{
|
||||
files: FileInfo[]
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
}>> {
|
||||
return api.get('/admin/file/list', {
|
||||
params: { page, size: limit }
|
||||
})
|
||||
}
|
||||
|
||||
// 文件管理相关方法
|
||||
static async getAdminFileList(params: { page: number; size: number; keyword?: string }): Promise<ApiResponse<FileListResponse>> {
|
||||
return api.get('/admin/file/list', { params })
|
||||
}
|
||||
|
||||
static async updateFile(data: FileEditForm): Promise<ApiResponse> {
|
||||
return api.post('/admin/file/update', data)
|
||||
}
|
||||
|
||||
static async deleteAdminFile(id: number): Promise<ApiResponse> {
|
||||
return api.post('/admin/file/delete', { id })
|
||||
}
|
||||
|
||||
static async downloadAdminFile(id: number): Promise<{ data: Blob; headers: Record<string, string> }> {
|
||||
return api.get('/admin/file/download', {
|
||||
params: { id },
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 认证服务
|
||||
export class AuthService {
|
||||
static async login(password: string): Promise<ApiResponse<AdminUser>> {
|
||||
return api.post('/admin/login', { password })
|
||||
}
|
||||
|
||||
static async logout(): Promise<ApiResponse> {
|
||||
return api.post('/admin/logout')
|
||||
}
|
||||
|
||||
static async verifyToken(): Promise<ApiResponse<AdminUser>> {
|
||||
return api.get('/admin/verify')
|
||||
}
|
||||
}
|
||||
|
||||
// 统计服务
|
||||
export class StatsService {
|
||||
static async getDashboardStats(): Promise<ApiResponse<{
|
||||
totalFiles: number
|
||||
totalDownloads: number
|
||||
todayUploads: number
|
||||
todayDownloads: number
|
||||
storageUsed: number
|
||||
recentFiles: FileInfo[]
|
||||
}>> {
|
||||
return api.get('/admin/dashboard')
|
||||
}
|
||||
|
||||
static async getDashboard(): Promise<ApiResponse<DashboardData>> {
|
||||
return api.get('/admin/dashboard')
|
||||
}
|
||||
}
|
||||
|
||||
// 导出所有服务
|
||||
export const services = {
|
||||
config: ConfigService,
|
||||
file: FileService,
|
||||
auth: AuthService,
|
||||
stats: StatsService
|
||||
}
|
||||
|
||||
export default services
|
||||
@@ -1,11 +1,79 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { STORAGE_KEYS } from '@/constants'
|
||||
import type { AdminUser } from '@/types'
|
||||
|
||||
export const useAdminData = defineStore('adminData', () => {
|
||||
const adminPassword = ref(localStorage.getItem('adminPassword') || '')
|
||||
function updateAdminPwd(pwd: string) {
|
||||
export const useAdminStore = defineStore('admin', () => {
|
||||
// 状态
|
||||
const adminPassword = ref(localStorage.getItem(STORAGE_KEYS.ADMIN_PASSWORD) || '')
|
||||
const token = ref(localStorage.getItem(STORAGE_KEYS.TOKEN) || '')
|
||||
const isLoggedIn = ref(false)
|
||||
const userInfo = ref<AdminUser | null>(null)
|
||||
|
||||
// 计算属性
|
||||
const isAuthenticated = computed(() => {
|
||||
return isLoggedIn.value && !!token.value
|
||||
})
|
||||
|
||||
// 方法
|
||||
const updateAdminPassword = (pwd: string) => {
|
||||
adminPassword.value = pwd
|
||||
localStorage.setItem('token', pwd)
|
||||
localStorage.setItem(STORAGE_KEYS.ADMIN_PASSWORD, pwd)
|
||||
}
|
||||
|
||||
const setToken = (newToken: string) => {
|
||||
token.value = newToken
|
||||
localStorage.setItem(STORAGE_KEYS.TOKEN, newToken)
|
||||
}
|
||||
|
||||
const setUserInfo = (user: AdminUser) => {
|
||||
userInfo.value = user
|
||||
isLoggedIn.value = true
|
||||
setToken(user.token)
|
||||
}
|
||||
|
||||
const login = (user: AdminUser) => {
|
||||
setUserInfo(user)
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
adminPassword.value = ''
|
||||
token.value = ''
|
||||
isLoggedIn.value = false
|
||||
userInfo.value = null
|
||||
|
||||
// 清除本地存储
|
||||
localStorage.removeItem(STORAGE_KEYS.ADMIN_PASSWORD)
|
||||
localStorage.removeItem(STORAGE_KEYS.TOKEN)
|
||||
}
|
||||
|
||||
const initAuth = () => {
|
||||
const storedToken = localStorage.getItem(STORAGE_KEYS.TOKEN)
|
||||
if (storedToken) {
|
||||
token.value = storedToken
|
||||
isLoggedIn.value = true
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
adminPassword,
|
||||
token,
|
||||
isLoggedIn,
|
||||
userInfo,
|
||||
|
||||
// 计算属性
|
||||
isAuthenticated,
|
||||
|
||||
// 方法
|
||||
updateAdminPassword,
|
||||
setToken,
|
||||
setUserInfo,
|
||||
login,
|
||||
logout,
|
||||
initAuth
|
||||
}
|
||||
return { adminPassword, updateAdminPwd }
|
||||
})
|
||||
|
||||
// 保持向后兼容
|
||||
export const useAdminData = useAdminStore
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
interface Alert {
|
||||
id: number
|
||||
message: string
|
||||
type: 'success' | 'error' | 'warning' | 'info'
|
||||
progress: number
|
||||
duration: number
|
||||
startTime: number
|
||||
}
|
||||
import type { Alert, AlertType } from '@/types'
|
||||
import { TIME_CONSTANTS } from '@/constants'
|
||||
|
||||
export const useAlertStore = defineStore('alert', {
|
||||
state: () => ({
|
||||
@@ -16,8 +9,8 @@ export const useAlertStore = defineStore('alert', {
|
||||
actions: {
|
||||
showAlert(
|
||||
message: string,
|
||||
type: 'success' | 'error' | 'warning' | 'info' = 'info',
|
||||
duration = 5000
|
||||
type: AlertType = 'info',
|
||||
duration = TIME_CONSTANTS.ALERT_DURATION
|
||||
) {
|
||||
const id = Date.now()
|
||||
const startTime = Date.now()
|
||||
|
||||
@@ -1,39 +1,290 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { reactive } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { FileInfo, UploadProgress, UploadStatus } from '@/types'
|
||||
import { UPLOAD_STATUS } from '@/constants'
|
||||
|
||||
export const useFileDataStore = defineStore('fileData', () => {
|
||||
const receiveData = reactive(JSON.parse(localStorage.getItem('receiveData') || '[]') || []) // 接收的数据
|
||||
const shareData = reactive(JSON.parse(localStorage.getItem('shareData') || '[]') || []) // 接收的数据
|
||||
function save() {
|
||||
localStorage.setItem('receiveData', JSON.stringify(receiveData))
|
||||
localStorage.setItem('shareData', JSON.stringify(shareData))
|
||||
// 上传相关状态
|
||||
const uploadStatus = ref<UploadStatus>(UPLOAD_STATUS.IDLE)
|
||||
const uploadProgress = ref<UploadProgress>({
|
||||
loaded: 0,
|
||||
total: 0,
|
||||
percentage: 0
|
||||
})
|
||||
const uploadedCode = ref('')
|
||||
const currentFile = ref<File | null>(null)
|
||||
|
||||
// 下载相关状态
|
||||
const downloadCode = ref('')
|
||||
const fileInfo = ref<FileInfo | null>(null)
|
||||
const isDownloading = ref(false)
|
||||
|
||||
// 文件列表状态(管理页面使用)
|
||||
const fileList = ref<FileInfo[]>([])
|
||||
const totalFiles = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const isLoadingList = ref(false)
|
||||
|
||||
// 接收数据状态(取件记录)
|
||||
const receiveData = ref<Array<{
|
||||
id: number
|
||||
code: string
|
||||
filename: string
|
||||
size: string
|
||||
downloadUrl: string | null
|
||||
content: string | null
|
||||
date: string
|
||||
}>>([])
|
||||
|
||||
// 分享数据状态(发送记录)
|
||||
const shareData = ref<Array<{
|
||||
id: number
|
||||
filename: string
|
||||
date: string
|
||||
size: string
|
||||
expiration: string
|
||||
retrieveCode: string
|
||||
}>>([])
|
||||
|
||||
// 计算属性
|
||||
const isUploading = computed(() => uploadStatus.value === UPLOAD_STATUS.UPLOADING)
|
||||
const isUploadSuccess = computed(() => uploadStatus.value === UPLOAD_STATUS.SUCCESS)
|
||||
const isUploadError = computed(() => uploadStatus.value === UPLOAD_STATUS.ERROR)
|
||||
const hasFileInfo = computed(() => fileInfo.value !== null)
|
||||
const canDownload = computed(() => hasFileInfo.value && !isDownloading.value)
|
||||
|
||||
const totalPages = computed(() => {
|
||||
return Math.ceil(totalFiles.value / pageSize.value)
|
||||
})
|
||||
|
||||
// 上传相关方法
|
||||
const setUploadStatus = (status: UploadStatus) => {
|
||||
uploadStatus.value = status
|
||||
}
|
||||
function addReceiveData(data: any) {
|
||||
receiveData.unshift(data)
|
||||
save()
|
||||
|
||||
const setUploadProgress = (progress: UploadProgress) => {
|
||||
uploadProgress.value = progress
|
||||
}
|
||||
|
||||
const setUploadedCode = (code: string) => {
|
||||
uploadedCode.value = code
|
||||
}
|
||||
|
||||
const setCurrentFile = (file: File | null) => {
|
||||
currentFile.value = file
|
||||
}
|
||||
|
||||
const resetUpload = () => {
|
||||
uploadStatus.value = UPLOAD_STATUS.IDLE
|
||||
uploadProgress.value = {
|
||||
loaded: 0,
|
||||
total: 0,
|
||||
percentage: 0
|
||||
}
|
||||
uploadedCode.value = ''
|
||||
currentFile.value = null
|
||||
}
|
||||
|
||||
// 下载相关方法
|
||||
const setDownloadCode = (code: string) => {
|
||||
downloadCode.value = code
|
||||
}
|
||||
|
||||
const setFileInfo = (info: FileInfo | null) => {
|
||||
fileInfo.value = info
|
||||
}
|
||||
|
||||
const setDownloading = (loading: boolean) => {
|
||||
isDownloading.value = loading
|
||||
}
|
||||
|
||||
const resetDownload = () => {
|
||||
downloadCode.value = ''
|
||||
fileInfo.value = null
|
||||
isDownloading.value = false
|
||||
}
|
||||
|
||||
// 文件列表相关方法
|
||||
const setFileList = (files: FileInfo[]) => {
|
||||
fileList.value = files
|
||||
}
|
||||
|
||||
const addFile = (file: FileInfo) => {
|
||||
fileList.value.unshift(file)
|
||||
totalFiles.value += 1
|
||||
}
|
||||
|
||||
const removeFile = (fileId: string) => {
|
||||
const index = fileList.value.findIndex(file => file.id === fileId)
|
||||
if (index > -1) {
|
||||
fileList.value.splice(index, 1)
|
||||
totalFiles.value -= 1
|
||||
}
|
||||
}
|
||||
|
||||
const updateFile = (fileId: string, updates: Partial<FileInfo>) => {
|
||||
const index = fileList.value.findIndex(file => file.id === fileId)
|
||||
if (index > -1) {
|
||||
fileList.value[index] = { ...fileList.value[index], ...updates }
|
||||
}
|
||||
}
|
||||
|
||||
const setTotalFiles = (total: number) => {
|
||||
totalFiles.value = total
|
||||
}
|
||||
|
||||
const setCurrentPage = (page: number) => {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
const setPageSize = (size: number) => {
|
||||
pageSize.value = size
|
||||
}
|
||||
|
||||
const setLoadingList = (loading: boolean) => {
|
||||
isLoadingList.value = loading
|
||||
}
|
||||
|
||||
const resetFileList = () => {
|
||||
fileList.value = []
|
||||
totalFiles.value = 0
|
||||
currentPage.value = 1
|
||||
isLoadingList.value = false
|
||||
}
|
||||
|
||||
function addShareData(data: any) {
|
||||
shareData.unshift(data)
|
||||
save()
|
||||
// 添加分享数据方法
|
||||
const addShareData = (data: { code: string; name?: string }) => {
|
||||
setUploadedCode(data.code)
|
||||
if (data.name) {
|
||||
// 如果有文件名,可以创建一个临时的 FileInfo 对象
|
||||
const fileInfo: FileInfo = {
|
||||
id: data.code,
|
||||
name: data.name,
|
||||
size: 0,
|
||||
type: '',
|
||||
uploadTime: new Date().toISOString(),
|
||||
downloadCount: 0
|
||||
}
|
||||
addFile(fileInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// 接收数据相关方法
|
||||
const addReceiveData = (data: {
|
||||
id: number
|
||||
code: string
|
||||
filename: string
|
||||
size: string
|
||||
downloadUrl: string | null
|
||||
content: string | null
|
||||
date: string
|
||||
}) => {
|
||||
receiveData.value.push(data)
|
||||
}
|
||||
|
||||
function deleteReceiveData(index: number) {
|
||||
receiveData.splice(index, 1)
|
||||
save()
|
||||
const removeReceiveData = (id: number) => {
|
||||
const index = receiveData.value.findIndex(item => item.id === id)
|
||||
if (index > -1) {
|
||||
receiveData.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteReceiveData = (index: number) => {
|
||||
if (index > -1 && index < receiveData.value.length) {
|
||||
receiveData.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function deleteShareData(index: number) {
|
||||
shareData.splice(index, 1)
|
||||
save()
|
||||
const clearReceiveData = () => {
|
||||
receiveData.value = []
|
||||
}
|
||||
|
||||
// 分享数据相关方法
|
||||
const addShareDataRecord = (data: {
|
||||
id: number
|
||||
filename: string
|
||||
date: string
|
||||
size: string
|
||||
expiration: string
|
||||
retrieveCode: string
|
||||
}) => {
|
||||
shareData.value.push(data)
|
||||
}
|
||||
|
||||
const deleteShareData = (index: number) => {
|
||||
if (index > -1 && index < shareData.value.length) {
|
||||
shareData.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const clearShareData = () => {
|
||||
shareData.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
receiveData,
|
||||
shareData,
|
||||
save,
|
||||
// 上传状态
|
||||
uploadStatus,
|
||||
uploadProgress,
|
||||
uploadedCode,
|
||||
currentFile,
|
||||
|
||||
// 下载状态
|
||||
downloadCode,
|
||||
fileInfo,
|
||||
isDownloading,
|
||||
|
||||
// 文件列表状态
|
||||
fileList,
|
||||
totalFiles,
|
||||
currentPage,
|
||||
pageSize,
|
||||
isLoadingList,
|
||||
|
||||
// 计算属性
|
||||
isUploading,
|
||||
isUploadSuccess,
|
||||
isUploadError,
|
||||
hasFileInfo,
|
||||
canDownload,
|
||||
totalPages,
|
||||
|
||||
// 上传方法
|
||||
setUploadStatus,
|
||||
setUploadProgress,
|
||||
setUploadedCode,
|
||||
setCurrentFile,
|
||||
resetUpload,
|
||||
|
||||
// 下载方法
|
||||
setDownloadCode,
|
||||
setFileInfo,
|
||||
setDownloading,
|
||||
resetDownload,
|
||||
|
||||
// 文件列表方法
|
||||
setFileList,
|
||||
addFile,
|
||||
removeFile,
|
||||
updateFile,
|
||||
setTotalFiles,
|
||||
setCurrentPage,
|
||||
setPageSize,
|
||||
setLoadingList,
|
||||
resetFileList,
|
||||
addShareData,
|
||||
|
||||
// 接收数据状态和方法
|
||||
receiveData,
|
||||
addReceiveData,
|
||||
removeReceiveData,
|
||||
deleteReceiveData,
|
||||
deleteShareData
|
||||
clearReceiveData,
|
||||
|
||||
// 分享数据状态和方法
|
||||
shareData,
|
||||
addShareDataRecord,
|
||||
deleteShareData,
|
||||
clearShareData
|
||||
}
|
||||
})
|
||||
|
||||
180
src/types/index.ts
Normal file
180
src/types/index.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
// 通用类型定义
|
||||
export interface ApiResponse<T = unknown> {
|
||||
code: number
|
||||
message?: string
|
||||
detail?: T
|
||||
}
|
||||
|
||||
// 文件相关类型
|
||||
export interface FileInfo {
|
||||
id: string
|
||||
name: string
|
||||
size: number
|
||||
type: string
|
||||
uploadTime: string
|
||||
downloadCount: number
|
||||
expireTime?: string
|
||||
}
|
||||
|
||||
// 文件管理相关类型
|
||||
export interface FileListItem {
|
||||
id: number
|
||||
code: string
|
||||
prefix: string
|
||||
suffix: string
|
||||
size: number
|
||||
text?: string
|
||||
description?: string
|
||||
expired_at: string
|
||||
expired_count: number | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface FileEditForm {
|
||||
id: number | null
|
||||
code: string
|
||||
prefix: string
|
||||
suffix: string
|
||||
expired_at: string
|
||||
expired_count: number | null
|
||||
}
|
||||
|
||||
export interface FileListResponse {
|
||||
data: FileListItem[]
|
||||
total: number
|
||||
page: number
|
||||
size: number
|
||||
}
|
||||
|
||||
// 文件上传响应类型
|
||||
export interface FileUploadResponse {
|
||||
code: number
|
||||
name: string
|
||||
}
|
||||
|
||||
// 文本发送响应类型
|
||||
export interface TextSendResponse {
|
||||
code: number
|
||||
}
|
||||
|
||||
export interface UploadProgress {
|
||||
loaded: number
|
||||
total: number
|
||||
percentage: number
|
||||
}
|
||||
|
||||
// 系统配置类型
|
||||
export interface SystemConfig {
|
||||
name: string
|
||||
description?: string
|
||||
maxFileSize: number
|
||||
allowedFileTypes: string[]
|
||||
expireDays: number
|
||||
notify_title?: string
|
||||
notify_content?: string
|
||||
}
|
||||
|
||||
// 主题选择项类型
|
||||
export interface ThemeChoice {
|
||||
key: string
|
||||
name: string
|
||||
author: string
|
||||
version: string
|
||||
}
|
||||
|
||||
// 完整的系统配置状态类型
|
||||
export interface ConfigState {
|
||||
name: string
|
||||
description: string
|
||||
file_storage: string
|
||||
themesChoices: ThemeChoice[]
|
||||
expireStyle: string[]
|
||||
admin_token: string
|
||||
robotsText: string
|
||||
keywords: string
|
||||
notify_title: string
|
||||
notify_content: string
|
||||
openUpload: number
|
||||
uploadSize: number
|
||||
storage_path: string
|
||||
uploadMinute: number
|
||||
max_save_seconds: number
|
||||
opacity: number
|
||||
enableChunk: number
|
||||
s3_access_key_id: string
|
||||
background: string
|
||||
showAdminAddr: number
|
||||
page_explain: string
|
||||
s3_secret_access_key: string
|
||||
aws_session_token: string
|
||||
s3_signature_version: string
|
||||
s3_region_name: string
|
||||
s3_bucket_name: string
|
||||
s3_endpoint_url: string
|
||||
s3_hostname: string
|
||||
uploadCount: number
|
||||
errorMinute: number
|
||||
errorCount: number
|
||||
s3_proxy: number
|
||||
themesSelect: string
|
||||
webdav_url: string
|
||||
webdav_username: string
|
||||
webdav_password: string
|
||||
}
|
||||
|
||||
// 系统配置API响应类型
|
||||
export interface ConfigResponse {
|
||||
code: number
|
||||
message?: string
|
||||
detail?: ConfigState
|
||||
}
|
||||
|
||||
// 用户相关类型
|
||||
export interface AdminUser {
|
||||
id: string
|
||||
username: string
|
||||
token: string
|
||||
}
|
||||
|
||||
// Dashboard 数据类型
|
||||
export interface DashboardData {
|
||||
totalFiles: number
|
||||
storageUsed: number | string
|
||||
yesterdayCount: number
|
||||
todayCount: number
|
||||
yesterdaySize: number | string
|
||||
todaySize: number | string
|
||||
sysUptime: number | string
|
||||
}
|
||||
|
||||
// 主题相关类型
|
||||
export type ThemeMode = 'light' | 'dark' | 'system'
|
||||
|
||||
// 发送类型
|
||||
export type SendType = 'file' | 'text'
|
||||
|
||||
// 警告类型
|
||||
export type AlertType = 'success' | 'error' | 'warning' | 'info'
|
||||
|
||||
export interface Alert {
|
||||
id: number
|
||||
message: string
|
||||
type: AlertType
|
||||
progress: number
|
||||
duration: number
|
||||
startTime: number
|
||||
}
|
||||
|
||||
// 文件上传状态
|
||||
export type UploadStatus = 'idle' | 'uploading' | 'success' | 'error'
|
||||
|
||||
// 路由相关类型
|
||||
export interface RouteConfig {
|
||||
path: string
|
||||
name: string
|
||||
component: () => Promise<{ default: object }>
|
||||
meta?: {
|
||||
requiresAuth?: boolean
|
||||
title?: string
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
import axios from 'axios'
|
||||
import { TIME_CONSTANTS } from '@/constants'
|
||||
|
||||
// 从环境变量中获取 API 基础 URL
|
||||
const baseURL =
|
||||
import.meta.env.MODE === 'production'
|
||||
? import.meta.env.VITE_API_BASE_URL_PROD
|
||||
: import.meta.env.VITE_API_BASE_URL_DEV
|
||||
|
||||
// 确保 baseURL 是一个有效的字符串
|
||||
const sanitizedBaseURL = typeof baseURL === 'string' ? baseURL : ''
|
||||
|
||||
// 创建 axios 实例
|
||||
const api = axios.create({
|
||||
baseURL: sanitizedBaseURL,
|
||||
timeout: 1000000000000000, // 请求超时时间
|
||||
timeout: TIME_CONSTANTS.REQUEST_TIMEOUT, // 30秒超时
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
@@ -46,27 +46,26 @@ api.interceptors.response.use(
|
||||
(error) => {
|
||||
// 处理错误响应
|
||||
if (error.response) {
|
||||
switch (error.response.status) {
|
||||
const { status } = error.response
|
||||
switch (status) {
|
||||
case 401:
|
||||
console.error('未授权,请重新登录')
|
||||
localStorage.clear()
|
||||
window.location.href = '/#/login'
|
||||
localStorage.removeItem('token')
|
||||
// 使用 router 进行导航而不是直接修改 location
|
||||
if (window.location.hash !== '#/login') {
|
||||
window.location.href = '/#/login'
|
||||
}
|
||||
break
|
||||
case 403:
|
||||
// 禁止访问
|
||||
console.error('禁止访问')
|
||||
break
|
||||
case 404:
|
||||
// 未找到
|
||||
console.error('请求的资源不存在')
|
||||
break
|
||||
case 500:
|
||||
default:
|
||||
console.error('发生错误:', error.response.data)
|
||||
// 错误信息通过Promise.reject传递给调用方处理
|
||||
break
|
||||
}
|
||||
} else if (error.request) {
|
||||
console.error('未收到响应:', error.request)
|
||||
// 网络错误,通过Promise.reject传递给调用方处理
|
||||
} else {
|
||||
console.error('请求配置错误:', error.message)
|
||||
// 请求配置错误,通过Promise.reject传递给调用方处理
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
@@ -386,16 +386,7 @@ interface InputStatus {
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
code: number
|
||||
message?: string
|
||||
detail?: {
|
||||
code: string
|
||||
name: string
|
||||
text: string
|
||||
size: number
|
||||
}
|
||||
}
|
||||
|
||||
import {
|
||||
BoxIcon,
|
||||
EyeIcon,
|
||||
@@ -415,6 +406,14 @@ import QRCode from 'qrcode.vue'
|
||||
import { useFileDataStore } from '@/stores/fileData'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import api from '@/utils/api'
|
||||
import type { ApiResponse } from '@/types'
|
||||
|
||||
interface RetrieveResponse {
|
||||
code: string
|
||||
name: string
|
||||
text: string
|
||||
size: number
|
||||
}
|
||||
import { saveAs } from 'file-saver'
|
||||
import { marked } from 'marked'
|
||||
import DOMPurify from 'dompurify'
|
||||
@@ -491,7 +490,7 @@ const handleSubmit = async () => {
|
||||
const response = await api.post('/share/select/', {
|
||||
code: code.value
|
||||
})
|
||||
const res = (response.data || response) as ApiResponse
|
||||
const res = (response.data || response) as ApiResponse<RetrieveResponse>
|
||||
|
||||
if (res && res.code === 200) {
|
||||
if (res.detail) {
|
||||
@@ -574,8 +573,6 @@ const getQRCodeValue = (record: FileRecord) => {
|
||||
}
|
||||
|
||||
const downloadRecord = (record: FileRecord) => {
|
||||
console.log(record)
|
||||
|
||||
if (record.downloadUrl) {
|
||||
// 如果是文件,直接下载
|
||||
window.open(
|
||||
|
||||
@@ -594,6 +594,7 @@ import QRCode from 'qrcode.vue'
|
||||
import { useFileDataStore } from '@/stores/fileData'
|
||||
import { useAlertStore } from '@/stores/alertStore'
|
||||
import api from '@/utils/api'
|
||||
import type { ApiResponse } from '@/types'
|
||||
import { copyRetrieveLink, copyRetrieveCode, copyWgetCommand } from '@/utils/clipboard'
|
||||
import { getStorageUnit } from '@/utils/convert'
|
||||
|
||||
@@ -615,15 +616,7 @@ interface ShareRecord {
|
||||
retrieveCode: string
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
code: number
|
||||
detail: {
|
||||
code?: string
|
||||
name?: string
|
||||
upload_id?: string
|
||||
existed?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const config: Config = JSON.parse(localStorage.getItem('config') || '{}') as Config
|
||||
|
||||
@@ -635,7 +628,7 @@ const sendType = ref('file')
|
||||
const selectedFile = ref<File | null>(null)
|
||||
const textContent = ref('')
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const expirationMethod = ref('day')
|
||||
const expirationMethod = ref(config.expireStyle?.[0] || 'day')
|
||||
const expirationValue = ref('1')
|
||||
const uploadProgress = ref(0)
|
||||
const showDrawer = ref(false)
|
||||
@@ -880,7 +873,12 @@ const handleChunkUpload = async (file: File) => {
|
||||
const chunkSize = 5 * 1024 * 1024
|
||||
const chunks = Math.ceil(file.size / chunkSize)
|
||||
// 1. 初始化切片上传
|
||||
const initResponse: ApiResponse = await api.post('chunk/upload/init/', {
|
||||
const initResponse: ApiResponse<{
|
||||
code?: string
|
||||
name?: string
|
||||
upload_id?: string
|
||||
existed?: boolean
|
||||
}> = await api.post('chunk/upload/init/', {
|
||||
file_name: file.name,
|
||||
file_size: file.size,
|
||||
chunk_size: chunkSize,
|
||||
@@ -890,10 +888,10 @@ const handleChunkUpload = async (file: File) => {
|
||||
if (initResponse.code !== 200) {
|
||||
throw new Error('初始化切片上传失败')
|
||||
}
|
||||
if (initResponse.detail.existed) {
|
||||
if (initResponse.detail?.existed) {
|
||||
return initResponse
|
||||
}
|
||||
const uploadId = initResponse.detail.upload_id
|
||||
const uploadId = initResponse.detail?.upload_id
|
||||
|
||||
// 2. 上传切片
|
||||
for (let i = 0; i < chunks; i++) {
|
||||
@@ -905,7 +903,7 @@ const handleChunkUpload = async (file: File) => {
|
||||
chunkFormData.append('chunk', new Blob([chunk], { type: file.type })) // 确保以Blob形式添加
|
||||
|
||||
// 使用 application/x-www-form-urlencoded 格式
|
||||
const chunkResponse: ApiResponse = await api.post(
|
||||
const chunkResponse: ApiResponse<unknown> = await api.post(
|
||||
`chunk/upload/chunk/${uploadId}/${i}`,
|
||||
chunkFormData,
|
||||
{
|
||||
@@ -927,7 +925,7 @@ const handleChunkUpload = async (file: File) => {
|
||||
}
|
||||
|
||||
// 3. 完成上传
|
||||
const completeResponse: ApiResponse = await api.post(`chunk/upload/complete/${uploadId}`, {
|
||||
const completeResponse: ApiResponse<{ code?: string; name?: string }> = await api.post(`chunk/upload/complete/${uploadId}`, {
|
||||
expire_value: expirationValue.value ? parseInt(expirationValue.value) : 1,
|
||||
expire_style: expirationMethod.value
|
||||
})
|
||||
@@ -965,7 +963,7 @@ const handleDefaultFileUpload = async (file: File) => {
|
||||
formData.append('file', file)
|
||||
formData.append('expire_value', expirationValue.value)
|
||||
formData.append('expire_style', expirationMethod.value)
|
||||
const response: ApiResponse = await api.post('share/file/', formData, config)
|
||||
const response: ApiResponse<{ code?: string; name?: string }> = await api.post('share/file/', formData, config)
|
||||
return response
|
||||
}
|
||||
const checkOpenUpload = () => {
|
||||
@@ -1060,8 +1058,8 @@ const handleSubmit = async () => {
|
||||
}
|
||||
|
||||
if (response && response.code === 200) {
|
||||
const retrieveCode = response.detail.code || ''
|
||||
const fileName = response.detail.name || ''
|
||||
const retrieveCode = (response.detail as unknown as { code?: string })?.code || ''
|
||||
const fileName = (response.detail as unknown as { name?: string })?.name || ''
|
||||
// 添加新的发送记录
|
||||
const newRecord: ShareRecord = {
|
||||
id: Date.now(),
|
||||
@@ -1077,7 +1075,7 @@ const handleSubmit = async () => {
|
||||
: getExpirationTime(expirationMethod.value, expirationValue.value),
|
||||
retrieveCode: retrieveCode
|
||||
}
|
||||
fileDataStore.addShareData(newRecord)
|
||||
fileDataStore.addShareDataRecord(newRecord)
|
||||
|
||||
// 显示发送成功消息
|
||||
alertStore.showAlert(`文件发送成功!取件码:${retrieveCode}`, 'success')
|
||||
@@ -1093,7 +1091,6 @@ const handleSubmit = async () => {
|
||||
throw new Error('服务器响应异常')
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error('发送失败:', error)
|
||||
if (error && typeof error === 'object' && 'response' in error) {
|
||||
const axiosError = error as { response?: { data?: { detail?: string } } }
|
||||
if (axiosError.response?.data?.detail) {
|
||||
|
||||
@@ -113,14 +113,12 @@ import {
|
||||
HardDriveIcon,
|
||||
UsersIcon,
|
||||
ActivityIcon,
|
||||
UploadIcon,
|
||||
TrashIcon,
|
||||
UserIcon
|
||||
} from 'lucide-vue-next'
|
||||
import { StatsService } from '@/services'
|
||||
import type { DashboardData } from '@/types'
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
import api from '@/utils/api'
|
||||
|
||||
const dashboardData: any = reactive({
|
||||
const dashboardData = reactive<DashboardData>({
|
||||
totalFiles: 0,
|
||||
storageUsed: 0,
|
||||
yesterdayCount: 0,
|
||||
@@ -130,30 +128,6 @@ const dashboardData: any = reactive({
|
||||
sysUptime: 0
|
||||
})
|
||||
|
||||
// 添加最近活动数据
|
||||
const recentActivities = [
|
||||
{
|
||||
icon: UploadIcon,
|
||||
description: '张三上传了文件 "项目计划.pdf"',
|
||||
time: '10分钟前'
|
||||
},
|
||||
{
|
||||
icon: UserIcon,
|
||||
description: '新用户李四加入了系统',
|
||||
time: '30分钟前'
|
||||
},
|
||||
{
|
||||
icon: TrashIcon,
|
||||
description: '王五删除了文件 "旧文档.doc"',
|
||||
time: '1小时前'
|
||||
},
|
||||
{
|
||||
icon: FileIcon,
|
||||
description: '系统自动备份完成',
|
||||
time: '2小时前'
|
||||
}
|
||||
]
|
||||
|
||||
const getSysUptime = (startTimestamp: number) => {
|
||||
const now = new Date().getTime()
|
||||
const uptime = now - startTimestamp
|
||||
@@ -181,14 +155,16 @@ const getLocalstorageUsed = (nowUsedBit: string) => {
|
||||
}
|
||||
}
|
||||
const getDashboardData = async () => {
|
||||
const response: any = await api.get('admin/dashboard')
|
||||
dashboardData.totalFiles = response.detail.totalFiles
|
||||
dashboardData.storageUsed = getLocalstorageUsed(response.detail.storageUsed)
|
||||
dashboardData.yesterdaySize = getLocalstorageUsed(response.detail.yesterdaySize)
|
||||
dashboardData.todaySize = getLocalstorageUsed(response.detail.todaySize)
|
||||
dashboardData.yesterdayCount = response.detail.yesterdayCount
|
||||
dashboardData.todayCount = response.detail.todayCount
|
||||
dashboardData.sysUptime = getSysUptime(response.detail.sysUptime)
|
||||
const response = await StatsService.getDashboard()
|
||||
if (response.detail) {
|
||||
dashboardData.totalFiles = response.detail.totalFiles
|
||||
dashboardData.storageUsed = getLocalstorageUsed(response.detail.storageUsed.toString())
|
||||
dashboardData.yesterdaySize = getLocalstorageUsed(response.detail.yesterdaySize.toString())
|
||||
dashboardData.todaySize = getLocalstorageUsed(response.detail.todaySize.toString())
|
||||
dashboardData.yesterdayCount = response.detail.yesterdayCount
|
||||
dashboardData.todayCount = response.detail.todayCount
|
||||
dashboardData.sysUptime = getSysUptime(Number(response.detail.sysUptime))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -381,7 +381,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, ref, computed } from 'vue'
|
||||
import api from '@/utils/api'
|
||||
import { FileService } from '@/services'
|
||||
import type { FileListItem, FileEditForm } from '@/types'
|
||||
import {
|
||||
FileIcon,
|
||||
SearchIcon,
|
||||
@@ -405,7 +406,7 @@ function formatTimestamp(timestamp: string): string {
|
||||
}
|
||||
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
const tableData: any = ref([])
|
||||
const tableData = ref<FileListItem[]>([])
|
||||
const alertStore = useAlertStore()
|
||||
// 修改文件表头
|
||||
const fileTableHeaders = ['取件码', '名称', '大小', '描述', '过期时间', '操作']
|
||||
@@ -420,7 +421,7 @@ const params = ref({
|
||||
|
||||
// 添加编辑相关的状态
|
||||
const showEditModal = ref(false)
|
||||
const editForm = ref({
|
||||
const editForm = ref<FileEditForm>({
|
||||
id: null,
|
||||
code: '',
|
||||
prefix: '',
|
||||
@@ -430,7 +431,7 @@ const editForm = ref({
|
||||
})
|
||||
|
||||
// 打开编辑模态框
|
||||
const openEditModal = (file: any) => {
|
||||
const openEditModal = (file: FileListItem) => {
|
||||
editForm.value = {
|
||||
id: file.id,
|
||||
code: file.code,
|
||||
@@ -458,99 +459,86 @@ const closeEditModal = () => {
|
||||
// 处理更新
|
||||
const handleUpdate = async () => {
|
||||
try {
|
||||
await api({
|
||||
url: 'admin/file/update',
|
||||
method: 'patch',
|
||||
data: editForm.value
|
||||
})
|
||||
await FileService.updateFile(editForm.value)
|
||||
await loadFiles()
|
||||
closeEditModal()
|
||||
} catch (error: any) {
|
||||
alertStore.showAlert(error.response.data.detail, 'error')
|
||||
} catch (error: unknown) {
|
||||
const err = error as { response?: { data?: { detail?: string } } }
|
||||
alertStore.showAlert(err.response?.data?.detail || '更新失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件处理
|
||||
const downloadFile = async (id: number) => {
|
||||
try {
|
||||
const response = await api({
|
||||
url: 'admin/file/download',
|
||||
method: 'get',
|
||||
params: { id },
|
||||
responseType: 'blob'
|
||||
})
|
||||
// 下载文件处理 - 暂时移除未使用的函数
|
||||
// const downloadFile = async (id: number) => {
|
||||
// try {
|
||||
// const response = await FileService.downloadAdminFile(id)
|
||||
|
||||
const contentDisposition = response.headers['content-disposition']
|
||||
let filename = 'file'
|
||||
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
|
||||
if (filenameMatch != null && filenameMatch[1]) {
|
||||
filename = filenameMatch[1].replace(/['"]/g, '')
|
||||
}
|
||||
// const contentDisposition = response.headers['content-disposition']
|
||||
// let filename = 'file'
|
||||
// const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
|
||||
// if (filenameMatch != null && filenameMatch[1]) {
|
||||
// filename = filenameMatch[1].replace(/['"]/g, '')
|
||||
// }
|
||||
|
||||
// @ts-ignore
|
||||
if (window.showSaveFilePicker) {
|
||||
await saveFileByWebApi(response.data, filename)
|
||||
} else {
|
||||
await saveFileByElementA(response.data, filename)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
}
|
||||
}
|
||||
// // @ts-expect-error - showSaveFilePicker is not in standard Window interface
|
||||
// if (window.showSaveFilePicker) {
|
||||
// await saveFileByWebApi(response.data, filename)
|
||||
// } else {
|
||||
// await saveFileByElementA(response.data, filename)
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('下载失败:', error)
|
||||
// }
|
||||
// }
|
||||
|
||||
// 删除文件处理
|
||||
const deleteFile = async (id: number) => {
|
||||
try {
|
||||
await api({
|
||||
url: 'admin/file/delete',
|
||||
method: 'delete',
|
||||
data: { id }
|
||||
})
|
||||
await FileService.deleteAdminFile(id)
|
||||
await loadFiles()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 文件保存辅助函数
|
||||
async function saveFileByElementA(fileBlob: Blob, filename: string) {
|
||||
const downloadUrl = window.URL.createObjectURL(fileBlob)
|
||||
const link = document.createElement('a')
|
||||
link.href = downloadUrl
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
window.URL.revokeObjectURL(downloadUrl)
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
// 文件保存辅助函数 - 暂时移除未使用的函数
|
||||
// async function saveFileByElementA(fileBlob: Blob, filename: string) {
|
||||
// const downloadUrl = window.URL.createObjectURL(fileBlob)
|
||||
// const link = document.createElement('a')
|
||||
// link.href = downloadUrl
|
||||
// link.download = filename
|
||||
// document.body.appendChild(link)
|
||||
// link.click()
|
||||
// window.URL.revokeObjectURL(downloadUrl)
|
||||
// document.body.removeChild(link)
|
||||
// }
|
||||
|
||||
async function saveFileByWebApi(fileBlob: Blob, filename: string) {
|
||||
// @ts-ignore
|
||||
const newHandle = await window.showSaveFilePicker({
|
||||
suggestedName: filename
|
||||
})
|
||||
const writableStream = await newHandle.createWritable()
|
||||
await writableStream.write(fileBlob)
|
||||
await writableStream.close()
|
||||
}
|
||||
// async function saveFileByWebApi(fileBlob: Blob, filename: string) {
|
||||
// // @ts-expect-error - showSaveFilePicker is not in standard Window interface
|
||||
// const newHandle = await window.showSaveFilePicker({
|
||||
// suggestedName: filename
|
||||
// })
|
||||
// const writableStream = await newHandle.createWritable()
|
||||
// await writableStream.write(fileBlob)
|
||||
// await writableStream.close()
|
||||
// }
|
||||
|
||||
// 加载文件列表
|
||||
const loadFiles = async () => {
|
||||
try {
|
||||
const res: any = await api({
|
||||
url: '/admin/file/list',
|
||||
method: 'get',
|
||||
params: params.value
|
||||
})
|
||||
tableData.value = res.detail.data
|
||||
params.value.total = res.detail.total
|
||||
const res = await FileService.getAdminFileList(params.value)
|
||||
if (res.detail) {
|
||||
tableData.value = res.detail.data
|
||||
params.value.total = res.detail.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载文件列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 页码改变处理函数
|
||||
const handlePageChange = async (page: any) => {
|
||||
const handlePageChange = async (page: number | string) => {
|
||||
if (typeof page === 'string') return // 忽略省略号
|
||||
if (page < 1 || page > totalPages.value) return
|
||||
params.value.page = page
|
||||
await loadFiles()
|
||||
|
||||
@@ -1,33 +1,41 @@
|
||||
<template>
|
||||
<div :class="[
|
||||
'min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 transition-colors duration-200 relative overflow-hidden',
|
||||
isDarkMode ? 'bg-gray-900' : 'bg-gray-50'
|
||||
]">
|
||||
<div
|
||||
:class="[
|
||||
'min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 transition-colors duration-200 relative overflow-hidden',
|
||||
isDarkMode ? 'bg-gray-900' : 'bg-gray-50'
|
||||
]"
|
||||
>
|
||||
<div class="absolute inset-0 z-0">
|
||||
<div class="cyber-grid"></div>
|
||||
<div class="floating-particles"></div>
|
||||
</div>
|
||||
<div class="max-w-md w-full space-y-8 backdrop-blur-lg bg-opacity-20 p-8 rounded-xl border border-opacity-20"
|
||||
:class="[isDarkMode ? 'bg-gray-800 border-gray-600' : 'bg-white/70 border-gray-200']">
|
||||
<div
|
||||
class="max-w-md w-full space-y-8 backdrop-blur-lg bg-opacity-20 p-8 rounded-xl border border-opacity-20"
|
||||
:class="[isDarkMode ? 'bg-gray-800 border-gray-600' : 'bg-white/70 border-gray-200']"
|
||||
>
|
||||
<div>
|
||||
<div class="mx-auto h-16 w-16 relative">
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-r from-cyan-500 via-purple-500 to-pink-500 rounded-full animate-spin-slow">
|
||||
</div>
|
||||
class="absolute inset-0 bg-gradient-to-r from-cyan-500 via-purple-500 to-pink-500 rounded-full animate-spin-slow"
|
||||
></div>
|
||||
<div
|
||||
class="absolute -inset-2 bg-gradient-to-r from-cyan-500 via-purple-500 to-pink-500 rounded-full opacity-50 blur-md animate-pulse">
|
||||
</div>
|
||||
<div :class="[
|
||||
'absolute inset-1 rounded-full flex items-center justify-center',
|
||||
isDarkMode ? 'bg-gray-800' : 'bg-white'
|
||||
]">
|
||||
class="absolute -inset-2 bg-gradient-to-r from-cyan-500 via-purple-500 to-pink-500 rounded-full opacity-50 blur-md animate-pulse"
|
||||
></div>
|
||||
<div
|
||||
:class="[
|
||||
'absolute inset-1 rounded-full flex items-center justify-center',
|
||||
isDarkMode ? 'bg-gray-800' : 'bg-white'
|
||||
]"
|
||||
>
|
||||
<BoxIcon :class="['h-8 w-8', isDarkMode ? 'text-cyan-400' : 'text-cyan-600']" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 :class="[
|
||||
'mt-6 text-center text-3xl font-extrabold',
|
||||
isDarkMode ? 'text-white' : 'text-gray-900'
|
||||
]">
|
||||
<h2
|
||||
:class="[
|
||||
'mt-6 text-center text-3xl font-extrabold',
|
||||
isDarkMode ? 'text-white' : 'text-gray-900'
|
||||
]"
|
||||
>
|
||||
登录
|
||||
</h2>
|
||||
</div>
|
||||
@@ -36,23 +44,35 @@
|
||||
<div class="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<label for="password" class="sr-only">密码</label>
|
||||
<input id="password" name="password" type="password" autocomplete="current-password" required
|
||||
v-model="password" :class="[
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
v-model="password"
|
||||
:class="[
|
||||
'appearance-none rounded-t-md relative block w-full px-4 py-3 border transition-all duration-200 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-cyan-500 focus:z-10 sm:text-sm backdrop-blur-sm',
|
||||
isDarkMode
|
||||
? 'bg-gray-800/50 border-gray-600 text-white placeholder-gray-400 hover:border-gray-500'
|
||||
: 'bg-white/50 border-gray-300 text-gray-900 hover:border-gray-400'
|
||||
]" placeholder="密码" />
|
||||
]"
|
||||
placeholder="密码"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" :class="[
|
||||
'group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-medium rounded-md text-white transition-all duration-300 transform hover:scale-[1.02] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 shadow-lg hover:shadow-cyan-500/50',
|
||||
isDarkMode
|
||||
? 'bg-gradient-to-r from-cyan-500 to-purple-500 hover:from-cyan-600 hover:to-purple-600'
|
||||
: 'bg-gradient-to-r from-cyan-600 to-purple-600 hover:from-cyan-700 hover:to-purple-700',
|
||||
isLoading ? 'opacity-75 cursor-not-allowed' : ''
|
||||
]" :disabled="isLoading">
|
||||
<button
|
||||
type="submit"
|
||||
:class="[
|
||||
'group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-medium rounded-md text-white transition-all duration-300 transform hover:scale-[1.02] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 shadow-lg hover:shadow-cyan-500/50',
|
||||
isDarkMode
|
||||
? 'bg-gradient-to-r from-cyan-500 to-purple-500 hover:from-cyan-600 hover:to-purple-600'
|
||||
: 'bg-gradient-to-r from-cyan-600 to-purple-600 hover:from-cyan-700 hover:to-purple-700',
|
||||
isLoading ? 'opacity-75 cursor-not-allowed' : ''
|
||||
]"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
<span class="absolute left-0 inset-y-0 flex items-center pl-3"> </span>
|
||||
{{ isLoading ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
@@ -66,9 +86,15 @@
|
||||
import { ref, inject } from 'vue'
|
||||
import { BoxIcon } from 'lucide-vue-next'
|
||||
import api from '@/utils/api'
|
||||
import type { ApiResponse } from '@/types'
|
||||
import { useAlertStore } from '@/stores/alertStore'
|
||||
import { useAdminData } from '@/stores/adminStore'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
// 登录响应类型定义
|
||||
interface LoginResponse {
|
||||
token: string
|
||||
}
|
||||
const alertStore = useAlertStore()
|
||||
const password = ref('')
|
||||
const isLoading = ref(false)
|
||||
@@ -91,19 +117,20 @@ const router = useRouter()
|
||||
const handleSubmit = async () => {
|
||||
if (!validateForm()) return
|
||||
api
|
||||
.post('/admin/login', { password: password.value })
|
||||
.then((res: any) => {
|
||||
adminStore.updateAdminPwd(res.detail.token)
|
||||
.post<ApiResponse<LoginResponse>>('/admin/login', { password: password.value })
|
||||
.then((res) => {
|
||||
adminStore.setToken(res.data.detail?.token || '')
|
||||
router.push('/admin')
|
||||
})
|
||||
.catch((error: any) => {
|
||||
alertStore.showAlert(error.response.data.detail, 'error')
|
||||
.catch((error) => {
|
||||
alertStore.showAlert(error.response?.data?.detail || '登录失败', 'error')
|
||||
})
|
||||
isLoading.value = true
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
// 处理登录成功
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
// 处理错误
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
@@ -145,7 +172,8 @@ button:active:not(:disabled) {
|
||||
}
|
||||
|
||||
.cyber-grid {
|
||||
background-image: linear-gradient(transparent 95%, rgba(99, 102, 241, 0.1) 50%),
|
||||
background-image:
|
||||
linear-gradient(transparent 95%, rgba(99, 102, 241, 0.1) 50%),
|
||||
linear-gradient(90deg, transparent 95%, rgba(99, 102, 241, 0.1) 50%);
|
||||
background-size: 30px 30px;
|
||||
width: 100%;
|
||||
|
||||
@@ -1,47 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { inject, ref } from 'vue'
|
||||
import api from '@/utils/api'
|
||||
import { ConfigService } from '@/services'
|
||||
import type { ConfigState } from '@/types'
|
||||
import { useAlertStore } from '@/stores/alertStore'
|
||||
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
interface ConfigState {
|
||||
name: string
|
||||
description: string
|
||||
file_storage: string
|
||||
themesChoices: any[]
|
||||
expireStyle: string[]
|
||||
admin_token: string
|
||||
robotsText: string
|
||||
keywords: string
|
||||
notify_title: string
|
||||
notify_content: string
|
||||
openUpload: number
|
||||
uploadSize: number
|
||||
storage_path: string
|
||||
uploadMinute: number
|
||||
max_save_seconds: number
|
||||
opacity: number
|
||||
enableChunk: number
|
||||
s3_access_key_id: string
|
||||
background: string
|
||||
showAdminAddr: number
|
||||
page_explain: string
|
||||
s3_secret_access_key: string
|
||||
aws_session_token: string
|
||||
s3_signature_version: string
|
||||
s3_region_name: string
|
||||
s3_bucket_name: string
|
||||
s3_endpoint_url: string
|
||||
s3_hostname: string
|
||||
uploadCount: number
|
||||
errorMinute: number
|
||||
errorCount: number
|
||||
s3_proxy: number
|
||||
themesSelect: string
|
||||
webdav_url: string
|
||||
webdav_username: string
|
||||
webdav_password: string
|
||||
}
|
||||
|
||||
const config = ref<ConfigState>({
|
||||
name: '',
|
||||
@@ -100,48 +63,54 @@ const convertToSeconds = (time: number, unit: string): number => {
|
||||
return time * units[unit as keyof typeof units]
|
||||
}
|
||||
|
||||
const refreshData = () => {
|
||||
api({
|
||||
url: 'admin/config/get',
|
||||
method: 'get'
|
||||
}).then((res: any) => {
|
||||
config.value = res.detail
|
||||
|
||||
// 将字节转换为合适的单位
|
||||
const size = config.value.uploadSize
|
||||
if (size >= 1024 * 1024 * 1024) {
|
||||
fileSize.value = Math.round(size / (1024 * 1024 * 1024))
|
||||
sizeUnit.value = 'GB'
|
||||
} else if (size >= 1024 * 1024) {
|
||||
fileSize.value = Math.round(size / (1024 * 1024))
|
||||
sizeUnit.value = 'MB'
|
||||
} else {
|
||||
fileSize.value = Math.round(size / 1024)
|
||||
sizeUnit.value = 'KB'
|
||||
}
|
||||
|
||||
// 时间单位转换逻辑
|
||||
const seconds = config.value.max_save_seconds
|
||||
if (seconds === 0) {
|
||||
// 如果是0,显示为7天
|
||||
saveTime.value = 7
|
||||
saveTimeUnit.value = '天'
|
||||
} else if (seconds % 86400 === 0 && seconds >= 86400) {
|
||||
saveTime.value = seconds / 86400
|
||||
saveTimeUnit.value = '天'
|
||||
} else if (seconds % 3600 === 0 && seconds >= 3600) {
|
||||
saveTime.value = seconds / 3600
|
||||
saveTimeUnit.value = '时'
|
||||
} else if (seconds % 60 === 0 && seconds >= 60) {
|
||||
saveTime.value = seconds / 60
|
||||
saveTimeUnit.value = '分'
|
||||
} else {
|
||||
saveTime.value = seconds
|
||||
saveTimeUnit.value = '秒'
|
||||
}
|
||||
})
|
||||
}
|
||||
const alertStore = useAlertStore()
|
||||
|
||||
const refreshData = async () => {
|
||||
try {
|
||||
const res = await ConfigService.getConfig()
|
||||
if (res.code === 200 && res.detail) {
|
||||
// 直接使用ConfigState类型的响应数据
|
||||
config.value = res.detail
|
||||
|
||||
// 将字节转换为合适的单位
|
||||
const size = config.value.uploadSize
|
||||
if (size >= 1024 * 1024 * 1024) {
|
||||
fileSize.value = Math.round(size / (1024 * 1024 * 1024))
|
||||
sizeUnit.value = 'GB'
|
||||
} else if (size >= 1024 * 1024) {
|
||||
fileSize.value = Math.round(size / (1024 * 1024))
|
||||
sizeUnit.value = 'MB'
|
||||
} else {
|
||||
fileSize.value = Math.round(size / 1024)
|
||||
sizeUnit.value = 'KB'
|
||||
}
|
||||
|
||||
// 时间单位转换逻辑
|
||||
const seconds = config.value.max_save_seconds
|
||||
if (seconds === 0) {
|
||||
// 如果是0,显示为7天
|
||||
saveTime.value = 7
|
||||
saveTimeUnit.value = '天'
|
||||
} else if (seconds % 86400 === 0 && seconds >= 86400) {
|
||||
saveTime.value = seconds / 86400
|
||||
saveTimeUnit.value = '天'
|
||||
} else if (seconds % 3600 === 0 && seconds >= 3600) {
|
||||
saveTime.value = seconds / 3600
|
||||
saveTimeUnit.value = '时'
|
||||
} else if (seconds % 60 === 0 && seconds >= 60) {
|
||||
saveTime.value = seconds / 60
|
||||
saveTimeUnit.value = '分'
|
||||
} else {
|
||||
saveTime.value = seconds
|
||||
saveTimeUnit.value = '秒'
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '获取配置失败'
|
||||
alertStore.showAlert(errorMessage, 'error')
|
||||
console.error('获取系统配置失败:', error)
|
||||
}
|
||||
}
|
||||
// 转换文件大小为字节
|
||||
const convertToBytes = (size: number, unit: string): number => {
|
||||
const units = {
|
||||
@@ -163,16 +132,15 @@ const submitSave = () => {
|
||||
formData.max_save_seconds = convertToSeconds(saveTime.value, saveTimeUnit.value)
|
||||
}
|
||||
|
||||
api({
|
||||
url: 'admin/config/update',
|
||||
method: 'patch',
|
||||
data: formData
|
||||
}).then((res: any) => {
|
||||
ConfigService.updateConfig(formData).then((res) => {
|
||||
if (res.code == 200) {
|
||||
alertStore.showAlert('保存成功', 'success')
|
||||
} else {
|
||||
alertStore.showAlert(res.message, 'error')
|
||||
alertStore.showAlert(res.message || '保存失败', 'error')
|
||||
}
|
||||
}).catch((error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : '保存失败'
|
||||
alertStore.showAlert(errorMessage, 'error')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,12 @@
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"noImplicitAny": false,
|
||||
"strict":false
|
||||
"noImplicitAny": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user