feat: 优化代码结构(在Claude的帮助下)
This commit is contained in:
+5
-3
@@ -3,12 +3,13 @@ import { ref, watchEffect, provide, onMounted } from 'vue'
|
|||||||
import { RouterView } from 'vue-router'
|
import { RouterView } from 'vue-router'
|
||||||
import ThemeToggle from './components/common/ThemeToggle.vue'
|
import ThemeToggle from './components/common/ThemeToggle.vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import api from './utils/api'
|
|
||||||
const isDarkMode = ref(false)
|
const isDarkMode = ref(false)
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
import AlertComponent from '@/components/common/AlertComponent.vue'
|
import AlertComponent from '@/components/common/AlertComponent.vue'
|
||||||
import { useAlertStore } from '@/stores/alertStore'
|
import { useAlertStore } from '@/stores/alertStore'
|
||||||
|
import { ConfigService } from '@/services'
|
||||||
|
import type { ApiResponse, ConfigState } from '@/types'
|
||||||
|
|
||||||
const alertStore = useAlertStore()
|
const alertStore = useAlertStore()
|
||||||
// 检查系统颜色模式
|
// 检查系统颜色模式
|
||||||
@@ -38,8 +39,9 @@ onMounted(() => {
|
|||||||
} else {
|
} else {
|
||||||
setColorMode(checkSystemColorScheme())
|
setColorMode(checkSystemColorScheme())
|
||||||
}
|
}
|
||||||
api.post('/', {}).then((res: any) => {
|
ConfigService.getUserConfig().then((res: ApiResponse<ConfigState>) => {
|
||||||
if (res.code === 200) {
|
if (res.code === 200 && res.detail) {
|
||||||
|
console.log(res);
|
||||||
localStorage.setItem('config', JSON.stringify(res.detail))
|
localStorage.setItem('config', JSON.stringify(res.detail))
|
||||||
if (
|
if (
|
||||||
res.detail.notify_title &&
|
res.detail.notify_title &&
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
@@ -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 { 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', () => {
|
export const useAdminStore = defineStore('admin', () => {
|
||||||
const adminPassword = ref(localStorage.getItem('adminPassword') || '')
|
// 状态
|
||||||
function updateAdminPwd(pwd: string) {
|
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
|
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'
|
import { defineStore } from 'pinia'
|
||||||
|
import type { Alert, AlertType } from '@/types'
|
||||||
interface Alert {
|
import { TIME_CONSTANTS } from '@/constants'
|
||||||
id: number
|
|
||||||
message: string
|
|
||||||
type: 'success' | 'error' | 'warning' | 'info'
|
|
||||||
progress: number
|
|
||||||
duration: number
|
|
||||||
startTime: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useAlertStore = defineStore('alert', {
|
export const useAlertStore = defineStore('alert', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@@ -16,8 +9,8 @@ export const useAlertStore = defineStore('alert', {
|
|||||||
actions: {
|
actions: {
|
||||||
showAlert(
|
showAlert(
|
||||||
message: string,
|
message: string,
|
||||||
type: 'success' | 'error' | 'warning' | 'info' = 'info',
|
type: AlertType = 'info',
|
||||||
duration = 5000
|
duration = TIME_CONSTANTS.ALERT_DURATION
|
||||||
) {
|
) {
|
||||||
const id = Date.now()
|
const id = Date.now()
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
|
|||||||
+274
-23
@@ -1,39 +1,290 @@
|
|||||||
import { defineStore } from 'pinia'
|
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', () => {
|
export const useFileDataStore = defineStore('fileData', () => {
|
||||||
const receiveData = reactive(JSON.parse(localStorage.getItem('receiveData') || '[]') || []) // 接收的数据
|
// 上传相关状态
|
||||||
const shareData = reactive(JSON.parse(localStorage.getItem('shareData') || '[]') || []) // 接收的数据
|
const uploadStatus = ref<UploadStatus>(UPLOAD_STATUS.IDLE)
|
||||||
function save() {
|
const uploadProgress = ref<UploadProgress>({
|
||||||
localStorage.setItem('receiveData', JSON.stringify(receiveData))
|
loaded: 0,
|
||||||
localStorage.setItem('shareData', JSON.stringify(shareData))
|
total: 0,
|
||||||
}
|
percentage: 0
|
||||||
function addReceiveData(data: any) {
|
})
|
||||||
receiveData.unshift(data)
|
const uploadedCode = ref('')
|
||||||
save()
|
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 addShareData(data: any) {
|
const setUploadProgress = (progress: UploadProgress) => {
|
||||||
shareData.unshift(data)
|
uploadProgress.value = progress
|
||||||
save()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteReceiveData(index: number) {
|
const setUploadedCode = (code: string) => {
|
||||||
receiveData.splice(index, 1)
|
uploadedCode.value = code
|
||||||
save()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteShareData(index: number) {
|
const setCurrentFile = (file: File | null) => {
|
||||||
shareData.splice(index, 1)
|
currentFile.value = file
|
||||||
save()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加分享数据方法
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
receiveData,
|
// 上传状态
|
||||||
shareData,
|
uploadStatus,
|
||||||
save,
|
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,
|
addShareData,
|
||||||
|
|
||||||
|
// 接收数据状态和方法
|
||||||
|
receiveData,
|
||||||
addReceiveData,
|
addReceiveData,
|
||||||
|
removeReceiveData,
|
||||||
deleteReceiveData,
|
deleteReceiveData,
|
||||||
deleteShareData
|
clearReceiveData,
|
||||||
|
|
||||||
|
// 分享数据状态和方法
|
||||||
|
shareData,
|
||||||
|
addShareDataRecord,
|
||||||
|
deleteShareData,
|
||||||
|
clearShareData
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-15
@@ -1,18 +1,18 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { TIME_CONSTANTS } from '@/constants'
|
||||||
|
|
||||||
// 从环境变量中获取 API 基础 URL
|
// 从环境变量中获取 API 基础 URL
|
||||||
const baseURL =
|
const baseURL =
|
||||||
import.meta.env.MODE === 'production'
|
import.meta.env.MODE === 'production'
|
||||||
? import.meta.env.VITE_API_BASE_URL_PROD
|
? import.meta.env.VITE_API_BASE_URL_PROD
|
||||||
: import.meta.env.VITE_API_BASE_URL_DEV
|
: import.meta.env.VITE_API_BASE_URL_DEV
|
||||||
|
|
||||||
// 确保 baseURL 是一个有效的字符串
|
// 确保 baseURL 是一个有效的字符串
|
||||||
const sanitizedBaseURL = typeof baseURL === 'string' ? baseURL : ''
|
const sanitizedBaseURL = typeof baseURL === 'string' ? baseURL : ''
|
||||||
|
|
||||||
// 创建 axios 实例
|
// 创建 axios 实例
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: sanitizedBaseURL,
|
baseURL: sanitizedBaseURL,
|
||||||
timeout: 1000000000000000, // 请求超时时间
|
timeout: TIME_CONSTANTS.REQUEST_TIMEOUT, // 30秒超时
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
@@ -46,27 +46,26 @@ api.interceptors.response.use(
|
|||||||
(error) => {
|
(error) => {
|
||||||
// 处理错误响应
|
// 处理错误响应
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
switch (error.response.status) {
|
const { status } = error.response
|
||||||
|
switch (status) {
|
||||||
case 401:
|
case 401:
|
||||||
console.error('未授权,请重新登录')
|
localStorage.removeItem('token')
|
||||||
localStorage.clear()
|
// 使用 router 进行导航而不是直接修改 location
|
||||||
window.location.href = '/#/login'
|
if (window.location.hash !== '#/login') {
|
||||||
|
window.location.href = '/#/login'
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case 403:
|
case 403:
|
||||||
// 禁止访问
|
|
||||||
console.error('禁止访问')
|
|
||||||
break
|
|
||||||
case 404:
|
case 404:
|
||||||
// 未找到
|
case 500:
|
||||||
console.error('请求的资源不存在')
|
|
||||||
break
|
|
||||||
default:
|
default:
|
||||||
console.error('发生错误:', error.response.data)
|
// 错误信息通过Promise.reject传递给调用方处理
|
||||||
|
break
|
||||||
}
|
}
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
console.error('未收到响应:', error.request)
|
// 网络错误,通过Promise.reject传递给调用方处理
|
||||||
} else {
|
} else {
|
||||||
console.error('请求配置错误:', error.message)
|
// 请求配置错误,通过Promise.reject传递给调用方处理
|
||||||
}
|
}
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -386,16 +386,7 @@ interface InputStatus {
|
|||||||
loading: boolean
|
loading: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ApiResponse {
|
|
||||||
code: number
|
|
||||||
message?: string
|
|
||||||
detail?: {
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
text: string
|
|
||||||
size: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
import {
|
import {
|
||||||
BoxIcon,
|
BoxIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
@@ -415,6 +406,14 @@ import QRCode from 'qrcode.vue'
|
|||||||
import { useFileDataStore } from '@/stores/fileData'
|
import { useFileDataStore } from '@/stores/fileData'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import api from '@/utils/api'
|
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 { saveAs } from 'file-saver'
|
||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
import DOMPurify from 'dompurify'
|
import DOMPurify from 'dompurify'
|
||||||
@@ -491,7 +490,7 @@ const handleSubmit = async () => {
|
|||||||
const response = await api.post('/share/select/', {
|
const response = await api.post('/share/select/', {
|
||||||
code: code.value
|
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 && res.code === 200) {
|
||||||
if (res.detail) {
|
if (res.detail) {
|
||||||
@@ -574,8 +573,6 @@ const getQRCodeValue = (record: FileRecord) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const downloadRecord = (record: FileRecord) => {
|
const downloadRecord = (record: FileRecord) => {
|
||||||
console.log(record)
|
|
||||||
|
|
||||||
if (record.downloadUrl) {
|
if (record.downloadUrl) {
|
||||||
// 如果是文件,直接下载
|
// 如果是文件,直接下载
|
||||||
window.open(
|
window.open(
|
||||||
|
|||||||
+17
-20
@@ -594,6 +594,7 @@ import QRCode from 'qrcode.vue'
|
|||||||
import { useFileDataStore } from '@/stores/fileData'
|
import { useFileDataStore } from '@/stores/fileData'
|
||||||
import { useAlertStore } from '@/stores/alertStore'
|
import { useAlertStore } from '@/stores/alertStore'
|
||||||
import api from '@/utils/api'
|
import api from '@/utils/api'
|
||||||
|
import type { ApiResponse } from '@/types'
|
||||||
import { copyRetrieveLink, copyRetrieveCode, copyWgetCommand } from '@/utils/clipboard'
|
import { copyRetrieveLink, copyRetrieveCode, copyWgetCommand } from '@/utils/clipboard'
|
||||||
import { getStorageUnit } from '@/utils/convert'
|
import { getStorageUnit } from '@/utils/convert'
|
||||||
|
|
||||||
@@ -615,15 +616,7 @@ interface ShareRecord {
|
|||||||
retrieveCode: string
|
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
|
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 selectedFile = ref<File | null>(null)
|
||||||
const textContent = ref('')
|
const textContent = ref('')
|
||||||
const fileInput = ref<HTMLInputElement | null>(null)
|
const fileInput = ref<HTMLInputElement | null>(null)
|
||||||
const expirationMethod = ref('day')
|
const expirationMethod = ref(config.expireStyle?.[0] || 'day')
|
||||||
const expirationValue = ref('1')
|
const expirationValue = ref('1')
|
||||||
const uploadProgress = ref(0)
|
const uploadProgress = ref(0)
|
||||||
const showDrawer = ref(false)
|
const showDrawer = ref(false)
|
||||||
@@ -880,7 +873,12 @@ const handleChunkUpload = async (file: File) => {
|
|||||||
const chunkSize = 5 * 1024 * 1024
|
const chunkSize = 5 * 1024 * 1024
|
||||||
const chunks = Math.ceil(file.size / chunkSize)
|
const chunks = Math.ceil(file.size / chunkSize)
|
||||||
// 1. 初始化切片上传
|
// 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_name: file.name,
|
||||||
file_size: file.size,
|
file_size: file.size,
|
||||||
chunk_size: chunkSize,
|
chunk_size: chunkSize,
|
||||||
@@ -890,10 +888,10 @@ const handleChunkUpload = async (file: File) => {
|
|||||||
if (initResponse.code !== 200) {
|
if (initResponse.code !== 200) {
|
||||||
throw new Error('初始化切片上传失败')
|
throw new Error('初始化切片上传失败')
|
||||||
}
|
}
|
||||||
if (initResponse.detail.existed) {
|
if (initResponse.detail?.existed) {
|
||||||
return initResponse
|
return initResponse
|
||||||
}
|
}
|
||||||
const uploadId = initResponse.detail.upload_id
|
const uploadId = initResponse.detail?.upload_id
|
||||||
|
|
||||||
// 2. 上传切片
|
// 2. 上传切片
|
||||||
for (let i = 0; i < chunks; i++) {
|
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形式添加
|
chunkFormData.append('chunk', new Blob([chunk], { type: file.type })) // 确保以Blob形式添加
|
||||||
|
|
||||||
// 使用 application/x-www-form-urlencoded 格式
|
// 使用 application/x-www-form-urlencoded 格式
|
||||||
const chunkResponse: ApiResponse = await api.post(
|
const chunkResponse: ApiResponse<unknown> = await api.post(
|
||||||
`chunk/upload/chunk/${uploadId}/${i}`,
|
`chunk/upload/chunk/${uploadId}/${i}`,
|
||||||
chunkFormData,
|
chunkFormData,
|
||||||
{
|
{
|
||||||
@@ -927,7 +925,7 @@ const handleChunkUpload = async (file: File) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. 完成上传
|
// 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_value: expirationValue.value ? parseInt(expirationValue.value) : 1,
|
||||||
expire_style: expirationMethod.value
|
expire_style: expirationMethod.value
|
||||||
})
|
})
|
||||||
@@ -965,7 +963,7 @@ const handleDefaultFileUpload = async (file: File) => {
|
|||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
formData.append('expire_value', expirationValue.value)
|
formData.append('expire_value', expirationValue.value)
|
||||||
formData.append('expire_style', expirationMethod.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
|
return response
|
||||||
}
|
}
|
||||||
const checkOpenUpload = () => {
|
const checkOpenUpload = () => {
|
||||||
@@ -1060,8 +1058,8 @@ const handleSubmit = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (response && response.code === 200) {
|
if (response && response.code === 200) {
|
||||||
const retrieveCode = response.detail.code || ''
|
const retrieveCode = (response.detail as unknown as { code?: string })?.code || ''
|
||||||
const fileName = response.detail.name || ''
|
const fileName = (response.detail as unknown as { name?: string })?.name || ''
|
||||||
// 添加新的发送记录
|
// 添加新的发送记录
|
||||||
const newRecord: ShareRecord = {
|
const newRecord: ShareRecord = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
@@ -1077,7 +1075,7 @@ const handleSubmit = async () => {
|
|||||||
: getExpirationTime(expirationMethod.value, expirationValue.value),
|
: getExpirationTime(expirationMethod.value, expirationValue.value),
|
||||||
retrieveCode: retrieveCode
|
retrieveCode: retrieveCode
|
||||||
}
|
}
|
||||||
fileDataStore.addShareData(newRecord)
|
fileDataStore.addShareDataRecord(newRecord)
|
||||||
|
|
||||||
// 显示发送成功消息
|
// 显示发送成功消息
|
||||||
alertStore.showAlert(`文件发送成功!取件码:${retrieveCode}`, 'success')
|
alertStore.showAlert(`文件发送成功!取件码:${retrieveCode}`, 'success')
|
||||||
@@ -1093,7 +1091,6 @@ const handleSubmit = async () => {
|
|||||||
throw new Error('服务器响应异常')
|
throw new Error('服务器响应异常')
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error('发送失败:', error)
|
|
||||||
if (error && typeof error === 'object' && 'response' in error) {
|
if (error && typeof error === 'object' && 'response' in error) {
|
||||||
const axiosError = error as { response?: { data?: { detail?: string } } }
|
const axiosError = error as { response?: { data?: { detail?: string } } }
|
||||||
if (axiosError.response?.data?.detail) {
|
if (axiosError.response?.data?.detail) {
|
||||||
|
|||||||
@@ -113,14 +113,12 @@ import {
|
|||||||
HardDriveIcon,
|
HardDriveIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
ActivityIcon,
|
ActivityIcon,
|
||||||
UploadIcon,
|
|
||||||
TrashIcon,
|
|
||||||
UserIcon
|
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
|
import { StatsService } from '@/services'
|
||||||
|
import type { DashboardData } from '@/types'
|
||||||
const isDarkMode = inject('isDarkMode')
|
const isDarkMode = inject('isDarkMode')
|
||||||
import api from '@/utils/api'
|
|
||||||
|
|
||||||
const dashboardData: any = reactive({
|
const dashboardData = reactive<DashboardData>({
|
||||||
totalFiles: 0,
|
totalFiles: 0,
|
||||||
storageUsed: 0,
|
storageUsed: 0,
|
||||||
yesterdayCount: 0,
|
yesterdayCount: 0,
|
||||||
@@ -130,30 +128,6 @@ const dashboardData: any = reactive({
|
|||||||
sysUptime: 0
|
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 getSysUptime = (startTimestamp: number) => {
|
||||||
const now = new Date().getTime()
|
const now = new Date().getTime()
|
||||||
const uptime = now - startTimestamp
|
const uptime = now - startTimestamp
|
||||||
@@ -181,14 +155,16 @@ const getLocalstorageUsed = (nowUsedBit: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const getDashboardData = async () => {
|
const getDashboardData = async () => {
|
||||||
const response: any = await api.get('admin/dashboard')
|
const response = await StatsService.getDashboard()
|
||||||
dashboardData.totalFiles = response.detail.totalFiles
|
if (response.detail) {
|
||||||
dashboardData.storageUsed = getLocalstorageUsed(response.detail.storageUsed)
|
dashboardData.totalFiles = response.detail.totalFiles
|
||||||
dashboardData.yesterdaySize = getLocalstorageUsed(response.detail.yesterdaySize)
|
dashboardData.storageUsed = getLocalstorageUsed(response.detail.storageUsed.toString())
|
||||||
dashboardData.todaySize = getLocalstorageUsed(response.detail.todaySize)
|
dashboardData.yesterdaySize = getLocalstorageUsed(response.detail.yesterdaySize.toString())
|
||||||
dashboardData.yesterdayCount = response.detail.yesterdayCount
|
dashboardData.todaySize = getLocalstorageUsed(response.detail.todaySize.toString())
|
||||||
dashboardData.todayCount = response.detail.todayCount
|
dashboardData.yesterdayCount = response.detail.yesterdayCount
|
||||||
dashboardData.sysUptime = getSysUptime(response.detail.sysUptime)
|
dashboardData.todayCount = response.detail.todayCount
|
||||||
|
dashboardData.sysUptime = getSysUptime(Number(response.detail.sysUptime))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -381,7 +381,8 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { inject, ref, computed } from 'vue'
|
import { inject, ref, computed } from 'vue'
|
||||||
import api from '@/utils/api'
|
import { FileService } from '@/services'
|
||||||
|
import type { FileListItem, FileEditForm } from '@/types'
|
||||||
import {
|
import {
|
||||||
FileIcon,
|
FileIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
@@ -405,7 +406,7 @@ function formatTimestamp(timestamp: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isDarkMode = inject('isDarkMode')
|
const isDarkMode = inject('isDarkMode')
|
||||||
const tableData: any = ref([])
|
const tableData = ref<FileListItem[]>([])
|
||||||
const alertStore = useAlertStore()
|
const alertStore = useAlertStore()
|
||||||
// 修改文件表头
|
// 修改文件表头
|
||||||
const fileTableHeaders = ['取件码', '名称', '大小', '描述', '过期时间', '操作']
|
const fileTableHeaders = ['取件码', '名称', '大小', '描述', '过期时间', '操作']
|
||||||
@@ -420,7 +421,7 @@ const params = ref({
|
|||||||
|
|
||||||
// 添加编辑相关的状态
|
// 添加编辑相关的状态
|
||||||
const showEditModal = ref(false)
|
const showEditModal = ref(false)
|
||||||
const editForm = ref({
|
const editForm = ref<FileEditForm>({
|
||||||
id: null,
|
id: null,
|
||||||
code: '',
|
code: '',
|
||||||
prefix: '',
|
prefix: '',
|
||||||
@@ -430,7 +431,7 @@ const editForm = ref({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 打开编辑模态框
|
// 打开编辑模态框
|
||||||
const openEditModal = (file: any) => {
|
const openEditModal = (file: FileListItem) => {
|
||||||
editForm.value = {
|
editForm.value = {
|
||||||
id: file.id,
|
id: file.id,
|
||||||
code: file.code,
|
code: file.code,
|
||||||
@@ -458,99 +459,86 @@ const closeEditModal = () => {
|
|||||||
// 处理更新
|
// 处理更新
|
||||||
const handleUpdate = async () => {
|
const handleUpdate = async () => {
|
||||||
try {
|
try {
|
||||||
await api({
|
await FileService.updateFile(editForm.value)
|
||||||
url: 'admin/file/update',
|
|
||||||
method: 'patch',
|
|
||||||
data: editForm.value
|
|
||||||
})
|
|
||||||
await loadFiles()
|
await loadFiles()
|
||||||
closeEditModal()
|
closeEditModal()
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
alertStore.showAlert(error.response.data.detail, 'error')
|
const err = error as { response?: { data?: { detail?: string } } }
|
||||||
|
alertStore.showAlert(err.response?.data?.detail || '更新失败', 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载文件处理
|
// 下载文件处理 - 暂时移除未使用的函数
|
||||||
const downloadFile = async (id: number) => {
|
// const downloadFile = async (id: number) => {
|
||||||
try {
|
// try {
|
||||||
const response = await api({
|
// const response = await FileService.downloadAdminFile(id)
|
||||||
url: 'admin/file/download',
|
|
||||||
method: 'get',
|
|
||||||
params: { id },
|
|
||||||
responseType: 'blob'
|
|
||||||
})
|
|
||||||
|
|
||||||
const contentDisposition = response.headers['content-disposition']
|
// const contentDisposition = response.headers['content-disposition']
|
||||||
let filename = 'file'
|
// let filename = 'file'
|
||||||
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
|
// const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
|
||||||
if (filenameMatch != null && filenameMatch[1]) {
|
// if (filenameMatch != null && filenameMatch[1]) {
|
||||||
filename = filenameMatch[1].replace(/['"]/g, '')
|
// filename = filenameMatch[1].replace(/['"]/g, '')
|
||||||
}
|
// }
|
||||||
|
|
||||||
// @ts-ignore
|
// // @ts-expect-error - showSaveFilePicker is not in standard Window interface
|
||||||
if (window.showSaveFilePicker) {
|
// if (window.showSaveFilePicker) {
|
||||||
await saveFileByWebApi(response.data, filename)
|
// await saveFileByWebApi(response.data, filename)
|
||||||
} else {
|
// } else {
|
||||||
await saveFileByElementA(response.data, filename)
|
// await saveFileByElementA(response.data, filename)
|
||||||
}
|
// }
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.error('下载失败:', error)
|
// console.error('下载失败:', error)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 删除文件处理
|
// 删除文件处理
|
||||||
const deleteFile = async (id: number) => {
|
const deleteFile = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
await api({
|
await FileService.deleteAdminFile(id)
|
||||||
url: 'admin/file/delete',
|
|
||||||
method: 'delete',
|
|
||||||
data: { id }
|
|
||||||
})
|
|
||||||
await loadFiles()
|
await loadFiles()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除失败:', error)
|
console.error('删除失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文件保存辅助函数
|
// 文件保存辅助函数 - 暂时移除未使用的函数
|
||||||
async function saveFileByElementA(fileBlob: Blob, filename: string) {
|
// async function saveFileByElementA(fileBlob: Blob, filename: string) {
|
||||||
const downloadUrl = window.URL.createObjectURL(fileBlob)
|
// const downloadUrl = window.URL.createObjectURL(fileBlob)
|
||||||
const link = document.createElement('a')
|
// const link = document.createElement('a')
|
||||||
link.href = downloadUrl
|
// link.href = downloadUrl
|
||||||
link.download = filename
|
// link.download = filename
|
||||||
document.body.appendChild(link)
|
// document.body.appendChild(link)
|
||||||
link.click()
|
// link.click()
|
||||||
window.URL.revokeObjectURL(downloadUrl)
|
// window.URL.revokeObjectURL(downloadUrl)
|
||||||
document.body.removeChild(link)
|
// document.body.removeChild(link)
|
||||||
}
|
// }
|
||||||
|
|
||||||
async function saveFileByWebApi(fileBlob: Blob, filename: string) {
|
// async function saveFileByWebApi(fileBlob: Blob, filename: string) {
|
||||||
// @ts-ignore
|
// // @ts-expect-error - showSaveFilePicker is not in standard Window interface
|
||||||
const newHandle = await window.showSaveFilePicker({
|
// const newHandle = await window.showSaveFilePicker({
|
||||||
suggestedName: filename
|
// suggestedName: filename
|
||||||
})
|
// })
|
||||||
const writableStream = await newHandle.createWritable()
|
// const writableStream = await newHandle.createWritable()
|
||||||
await writableStream.write(fileBlob)
|
// await writableStream.write(fileBlob)
|
||||||
await writableStream.close()
|
// await writableStream.close()
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 加载文件列表
|
// 加载文件列表
|
||||||
const loadFiles = async () => {
|
const loadFiles = async () => {
|
||||||
try {
|
try {
|
||||||
const res: any = await api({
|
const res = await FileService.getAdminFileList(params.value)
|
||||||
url: '/admin/file/list',
|
if (res.detail) {
|
||||||
method: 'get',
|
tableData.value = res.detail.data
|
||||||
params: params.value
|
params.value.total = res.detail.total
|
||||||
})
|
}
|
||||||
tableData.value = res.detail.data
|
|
||||||
params.value.total = res.detail.total
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载文件列表失败:', 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
|
if (page < 1 || page > totalPages.value) return
|
||||||
params.value.page = page
|
params.value.page = page
|
||||||
await loadFiles()
|
await loadFiles()
|
||||||
|
|||||||
@@ -1,33 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="[
|
<div
|
||||||
'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',
|
:class="[
|
||||||
isDarkMode ? 'bg-gray-900' : 'bg-gray-50'
|
'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="absolute inset-0 z-0">
|
||||||
<div class="cyber-grid"></div>
|
<div class="cyber-grid"></div>
|
||||||
<div class="floating-particles"></div>
|
<div class="floating-particles"></div>
|
||||||
</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"
|
<div
|
||||||
:class="[isDarkMode ? 'bg-gray-800 border-gray-600' : 'bg-white/70 border-gray-200']">
|
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>
|
||||||
<div class="mx-auto h-16 w-16 relative">
|
<div class="mx-auto h-16 w-16 relative">
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-gradient-to-r from-cyan-500 via-purple-500 to-pink-500 rounded-full animate-spin-slow">
|
class="absolute inset-0 bg-gradient-to-r from-cyan-500 via-purple-500 to-pink-500 rounded-full animate-spin-slow"
|
||||||
</div>
|
></div>
|
||||||
<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">
|
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>
|
||||||
<div :class="[
|
<div
|
||||||
'absolute inset-1 rounded-full flex items-center justify-center',
|
:class="[
|
||||||
isDarkMode ? 'bg-gray-800' : 'bg-white'
|
'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']" />
|
<BoxIcon :class="['h-8 w-8', isDarkMode ? 'text-cyan-400' : 'text-cyan-600']" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 :class="[
|
<h2
|
||||||
'mt-6 text-center text-3xl font-extrabold',
|
:class="[
|
||||||
isDarkMode ? 'text-white' : 'text-gray-900'
|
'mt-6 text-center text-3xl font-extrabold',
|
||||||
]">
|
isDarkMode ? 'text-white' : 'text-gray-900'
|
||||||
|
]"
|
||||||
|
>
|
||||||
登录
|
登录
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,23 +44,35 @@
|
|||||||
<div class="rounded-md shadow-sm -space-y-px">
|
<div class="rounded-md shadow-sm -space-y-px">
|
||||||
<div>
|
<div>
|
||||||
<label for="password" class="sr-only">密码</label>
|
<label for="password" class="sr-only">密码</label>
|
||||||
<input id="password" name="password" type="password" autocomplete="current-password" required
|
<input
|
||||||
v-model="password" :class="[
|
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',
|
'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
|
isDarkMode
|
||||||
? 'bg-gray-800/50 border-gray-600 text-white placeholder-gray-400 hover:border-gray-500'
|
? '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'
|
: 'bg-white/50 border-gray-300 text-gray-900 hover:border-gray-400'
|
||||||
]" placeholder="密码" />
|
]"
|
||||||
|
placeholder="密码"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button type="submit" :class="[
|
<button
|
||||||
'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',
|
type="submit"
|
||||||
isDarkMode
|
:class="[
|
||||||
? 'bg-gradient-to-r from-cyan-500 to-purple-500 hover:from-cyan-600 hover:to-purple-600'
|
'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',
|
||||||
: 'bg-gradient-to-r from-cyan-600 to-purple-600 hover:from-cyan-700 hover:to-purple-700',
|
isDarkMode
|
||||||
isLoading ? 'opacity-75 cursor-not-allowed' : ''
|
? 'bg-gradient-to-r from-cyan-500 to-purple-500 hover:from-cyan-600 hover:to-purple-600'
|
||||||
]" :disabled="isLoading">
|
: '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>
|
<span class="absolute left-0 inset-y-0 flex items-center pl-3"> </span>
|
||||||
{{ isLoading ? '登录中...' : '登录' }}
|
{{ isLoading ? '登录中...' : '登录' }}
|
||||||
</button>
|
</button>
|
||||||
@@ -66,9 +86,15 @@
|
|||||||
import { ref, inject } from 'vue'
|
import { ref, inject } from 'vue'
|
||||||
import { BoxIcon } from 'lucide-vue-next'
|
import { BoxIcon } from 'lucide-vue-next'
|
||||||
import api from '@/utils/api'
|
import api from '@/utils/api'
|
||||||
|
import type { ApiResponse } from '@/types'
|
||||||
import { useAlertStore } from '@/stores/alertStore'
|
import { useAlertStore } from '@/stores/alertStore'
|
||||||
import { useAdminData } from '@/stores/adminStore'
|
import { useAdminData } from '@/stores/adminStore'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
// 登录响应类型定义
|
||||||
|
interface LoginResponse {
|
||||||
|
token: string
|
||||||
|
}
|
||||||
const alertStore = useAlertStore()
|
const alertStore = useAlertStore()
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
@@ -91,19 +117,20 @@ const router = useRouter()
|
|||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!validateForm()) return
|
if (!validateForm()) return
|
||||||
api
|
api
|
||||||
.post('/admin/login', { password: password.value })
|
.post<ApiResponse<LoginResponse>>('/admin/login', { password: password.value })
|
||||||
.then((res: any) => {
|
.then((res) => {
|
||||||
adminStore.updateAdminPwd(res.detail.token)
|
adminStore.setToken(res.data.detail?.token || '')
|
||||||
router.push('/admin')
|
router.push('/admin')
|
||||||
})
|
})
|
||||||
.catch((error: any) => {
|
.catch((error) => {
|
||||||
alertStore.showAlert(error.response.data.detail, 'error')
|
alertStore.showAlert(error.response?.data?.detail || '登录失败', 'error')
|
||||||
})
|
})
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||||
// 处理登录成功
|
// 处理登录成功
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
// 处理错误
|
// 处理错误
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
@@ -145,7 +172,8 @@ button:active:not(:disabled) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cyber-grid {
|
.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%);
|
linear-gradient(90deg, transparent 95%, rgba(99, 102, 241, 0.1) 50%);
|
||||||
background-size: 30px 30px;
|
background-size: 30px 30px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,47 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { inject, ref } from 'vue'
|
import { inject, ref } from 'vue'
|
||||||
import api from '@/utils/api'
|
import { ConfigService } from '@/services'
|
||||||
|
import type { ConfigState } from '@/types'
|
||||||
import { useAlertStore } from '@/stores/alertStore'
|
import { useAlertStore } from '@/stores/alertStore'
|
||||||
|
|
||||||
const isDarkMode = inject('isDarkMode')
|
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>({
|
const config = ref<ConfigState>({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -100,48 +63,54 @@ const convertToSeconds = (time: number, unit: string): number => {
|
|||||||
return time * units[unit as keyof typeof units]
|
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 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 convertToBytes = (size: number, unit: string): number => {
|
||||||
const units = {
|
const units = {
|
||||||
@@ -163,16 +132,15 @@ const submitSave = () => {
|
|||||||
formData.max_save_seconds = convertToSeconds(saveTime.value, saveTimeUnit.value)
|
formData.max_save_seconds = convertToSeconds(saveTime.value, saveTimeUnit.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
api({
|
ConfigService.updateConfig(formData).then((res) => {
|
||||||
url: 'admin/config/update',
|
|
||||||
method: 'patch',
|
|
||||||
data: formData
|
|
||||||
}).then((res: any) => {
|
|
||||||
if (res.code == 200) {
|
if (res.code == 200) {
|
||||||
alertStore.showAlert('保存成功', 'success')
|
alertStore.showAlert('保存成功', 'success')
|
||||||
} else {
|
} else {
|
||||||
alertStore.showAlert(res.message, 'error')
|
alertStore.showAlert(res.message || '保存失败', 'error')
|
||||||
}
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '保存失败'
|
||||||
|
alertStore.showAlert(errorMessage, 'error')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -13,7 +13,12 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"]
|
||||||
},
|
},
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": true,
|
||||||
"strict":false
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user