refactor: modularize frontend flows
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
export { useAdminFiles } from './useAdminFiles'
|
||||
export { useAdminLogin } from './useAdminLogin'
|
||||
export { useAppShell } from './useAppShell'
|
||||
export { useDashboardStats } from './useDashboardStats'
|
||||
export { useInjectedDarkMode } from './useInjectedDarkMode'
|
||||
export { usePresignedUpload } from './usePresignedUpload'
|
||||
export { usePublicConfigBootstrap } from './usePublicConfigBootstrap'
|
||||
export { useRetrieveFlow } from './useRetrieveFlow'
|
||||
export { useRouteLoading } from './useRouteLoading'
|
||||
export { useSendFlow } from './useSendFlow'
|
||||
export { useSystemConfig } from './useSystemConfig'
|
||||
export { useTheme } from './useTheme'
|
||||
@@ -0,0 +1,163 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { FileService } from '@/services'
|
||||
import { useAlertStore } from '@/stores/alertStore'
|
||||
import type { AdminFileViewItem, FileEditForm, FileListItem } from '@/types'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
import { formatFileSize, formatTimestamp, getErrorMessage } from '@/utils/common'
|
||||
|
||||
const TEXT_PREVIEW_THRESHOLD = 30
|
||||
|
||||
export function useAdminFiles() {
|
||||
const { t } = useI18n()
|
||||
const alertStore = useAlertStore()
|
||||
|
||||
const tableData = ref<AdminFileViewItem[]>([])
|
||||
const hasLoadError = ref(false)
|
||||
const params = ref({
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0,
|
||||
keyword: ''
|
||||
})
|
||||
|
||||
const showEditModal = ref(false)
|
||||
const editForm = ref<FileEditForm>({
|
||||
id: null,
|
||||
code: '',
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
expired_at: '',
|
||||
expired_count: null
|
||||
})
|
||||
|
||||
const showTextPreview = ref(false)
|
||||
const previewText = ref('')
|
||||
const totalPages = computed(() => Math.ceil(params.value.total / params.value.size))
|
||||
|
||||
const createFileViewItem = (file: FileListItem): AdminFileViewItem => ({
|
||||
...file,
|
||||
displaySize: formatFileSize(file.size),
|
||||
displayExpiredAt: file.expired_at
|
||||
? formatTimestamp(file.expired_at)
|
||||
: t('send.expiration.units.forever'),
|
||||
canPreviewText: Boolean(file.text && file.text.length > TEXT_PREVIEW_THRESHOLD)
|
||||
})
|
||||
|
||||
const resetEditForm = () => {
|
||||
editForm.value = {
|
||||
id: null,
|
||||
code: '',
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
expired_at: '',
|
||||
expired_count: null
|
||||
}
|
||||
}
|
||||
|
||||
const loadFiles = async () => {
|
||||
try {
|
||||
hasLoadError.value = false
|
||||
const res = await FileService.getAdminFileList(params.value)
|
||||
if (!res.detail) return
|
||||
|
||||
tableData.value = res.detail.data.map(createFileViewItem)
|
||||
params.value.total = res.detail.total
|
||||
} catch (error) {
|
||||
hasLoadError.value = true
|
||||
alertStore.showAlert(getErrorMessage(error, t('manage.fileManage.loadFileListFailed')), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = async () => {
|
||||
params.value.page = 1
|
||||
await loadFiles()
|
||||
}
|
||||
|
||||
const handlePageChange = async (page: number | string) => {
|
||||
if (typeof page === 'string') return
|
||||
if (page < 1 || page > totalPages.value) return
|
||||
|
||||
params.value.page = page
|
||||
await loadFiles()
|
||||
}
|
||||
|
||||
const openEditModal = (file: FileListItem) => {
|
||||
editForm.value = {
|
||||
id: file.id,
|
||||
code: file.code,
|
||||
prefix: file.prefix,
|
||||
suffix: file.suffix,
|
||||
expired_at: file.expired_at ? file.expired_at.slice(0, 16) : '',
|
||||
expired_count: file.expired_count
|
||||
}
|
||||
showEditModal.value = true
|
||||
}
|
||||
|
||||
const closeEditModal = () => {
|
||||
showEditModal.value = false
|
||||
resetEditForm()
|
||||
}
|
||||
|
||||
const handleUpdate = async () => {
|
||||
try {
|
||||
await FileService.updateFile(editForm.value)
|
||||
await loadFiles()
|
||||
closeEditModal()
|
||||
} catch (error: unknown) {
|
||||
alertStore.showAlert(getErrorMessage(error, t('manage.fileManage.updateFailed')), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const deleteFile = async (id: number) => {
|
||||
if (!window.confirm(t('manage.fileManage.deleteConfirm'))) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await FileService.deleteAdminFile(id)
|
||||
await loadFiles()
|
||||
} catch (error: unknown) {
|
||||
alertStore.showAlert(getErrorMessage(error, t('manage.fileManage.deleteFailed')), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const openTextPreview = (text: string) => {
|
||||
previewText.value = text
|
||||
showTextPreview.value = true
|
||||
}
|
||||
|
||||
const closeTextPreview = () => {
|
||||
showTextPreview.value = false
|
||||
previewText.value = ''
|
||||
}
|
||||
|
||||
const copyText = async () => {
|
||||
await copyToClipboard(previewText.value, {
|
||||
successMsg: t('fileManage.copySuccess'),
|
||||
errorMsg: t('fileManage.copyFailed'),
|
||||
notify: (message, type) => alertStore.showAlert(message, type)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
tableData,
|
||||
hasLoadError,
|
||||
params,
|
||||
showEditModal,
|
||||
editForm,
|
||||
showTextPreview,
|
||||
previewText,
|
||||
totalPages,
|
||||
closeEditModal,
|
||||
closeTextPreview,
|
||||
copyText,
|
||||
deleteFile,
|
||||
handlePageChange,
|
||||
handleSearch,
|
||||
handleUpdate,
|
||||
loadFiles,
|
||||
openEditModal,
|
||||
openTextPreview
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ref } from 'vue'
|
||||
import { AuthService } from '@/services'
|
||||
import { useAdminStore } from '@/stores/adminStore'
|
||||
import { useAlertStore } from '@/stores/alertStore'
|
||||
import { getErrorMessage } from '@/utils/common'
|
||||
|
||||
export function useAdminLogin() {
|
||||
const alertStore = useAlertStore()
|
||||
const adminStore = useAdminStore()
|
||||
const password = ref('')
|
||||
const isLoading = ref(false)
|
||||
|
||||
const validateForm = () => {
|
||||
if (!password.value) {
|
||||
alertStore.showAlert('无效的密码', 'error')
|
||||
return false
|
||||
}
|
||||
|
||||
if (password.value.length < 6) {
|
||||
alertStore.showAlert('密码长度至少为6位', 'error')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!validateForm()) return false
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
const response = await AuthService.login(password.value)
|
||||
if (!response.detail?.token) {
|
||||
alertStore.showAlert('登录失败:未获取到有效令牌', 'error')
|
||||
return false
|
||||
}
|
||||
|
||||
adminStore.setToken(response.detail.token)
|
||||
return true
|
||||
} catch (error: unknown) {
|
||||
alertStore.showAlert(getErrorMessage(error, '登录失败'), 'error')
|
||||
return false
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
password,
|
||||
isLoading,
|
||||
handleSubmit
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { computed, onMounted, onUnmounted, provide } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { AUTH_EVENTS } from '@/services'
|
||||
import { ROUTES } from '@/constants'
|
||||
import { useTheme } from './useTheme'
|
||||
import { usePublicConfigBootstrap } from './usePublicConfigBootstrap'
|
||||
import { useRouteLoading } from './useRouteLoading'
|
||||
|
||||
export function useAppShell() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { isDarkMode, toggleTheme, initTheme } = useTheme()
|
||||
const { isLoading, setupRouteLoading } = useRouteLoading(router)
|
||||
const { syncPublicConfig } = usePublicConfigBootstrap()
|
||||
const showGlobalControls = computed(() => route.meta.showGlobalControls !== false)
|
||||
|
||||
let cleanupThemeListener: (() => void) | null = null
|
||||
|
||||
const handleUnauthorized = () => {
|
||||
if (router.currentRoute.value.path !== ROUTES.LOGIN) {
|
||||
void router.push({
|
||||
path: ROUTES.LOGIN,
|
||||
query: {
|
||||
redirect: router.currentRoute.value.fullPath
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
cleanupThemeListener = initTheme()
|
||||
setupRouteLoading()
|
||||
window.addEventListener(AUTH_EVENTS.UNAUTHORIZED, handleUnauthorized)
|
||||
void syncPublicConfig()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanupThemeListener?.()
|
||||
window.removeEventListener(AUTH_EVENTS.UNAUTHORIZED, handleUnauthorized)
|
||||
})
|
||||
|
||||
provide('isDarkMode', isDarkMode)
|
||||
provide('toggleTheme', toggleTheme)
|
||||
provide('isLoading', isLoading)
|
||||
|
||||
return {
|
||||
isDarkMode,
|
||||
isLoading,
|
||||
route,
|
||||
showGlobalControls
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { reactive } from 'vue'
|
||||
import { StatsService } from '@/services'
|
||||
import type { DashboardViewData } from '@/types'
|
||||
import { formatFileSize } from '@/utils/common'
|
||||
|
||||
const emptyDashboardData = (): DashboardViewData => ({
|
||||
hasExtendedStats: false,
|
||||
totalFiles: 0,
|
||||
storageUsed: 0,
|
||||
yesterdayCount: 0,
|
||||
todayCount: 0,
|
||||
yesterdaySize: 0,
|
||||
todaySize: 0,
|
||||
sysUptime: null,
|
||||
activeCount: 0,
|
||||
expiredCount: 0,
|
||||
textCount: 0,
|
||||
fileCount: 0,
|
||||
chunkedCount: 0,
|
||||
usedCount: 0,
|
||||
storageBackend: '-',
|
||||
uploadSizeLimit: 0,
|
||||
openUpload: 0,
|
||||
enableChunk: 0,
|
||||
maxSaveSeconds: 0,
|
||||
topSuffixes: [],
|
||||
recentFiles: [],
|
||||
storageUsedText: '0 Bytes',
|
||||
yesterdaySizeText: '0 Bytes',
|
||||
todaySizeText: '0 Bytes',
|
||||
uploadSizeLimitText: '0 Bytes',
|
||||
sysUptimeText: '-',
|
||||
activeRatio: 0,
|
||||
textRatio: 0,
|
||||
fileRatio: 0,
|
||||
todaySizeRatio: 0
|
||||
})
|
||||
|
||||
const toNumber = (value: number | string | null | undefined) => Number(value || 0)
|
||||
|
||||
const clampRatio = (value: number) => Math.max(0, Math.min(100, Math.round(value)))
|
||||
|
||||
const hasOwn = (target: object, key: string) => Object.prototype.hasOwnProperty.call(target, key)
|
||||
|
||||
const formatDuration = (startTimestamp: number | null) => {
|
||||
if (!startTimestamp) return '-'
|
||||
const uptime = Date.now() - startTimestamp
|
||||
const days = Math.floor(uptime / (24 * 60 * 60 * 1000))
|
||||
const hours = Math.floor((uptime % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000))
|
||||
return `${days}天${hours}小时`
|
||||
}
|
||||
|
||||
export function useDashboardStats() {
|
||||
const dashboardData = reactive<DashboardViewData>(emptyDashboardData())
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
const response = await StatsService.getDashboard()
|
||||
if (!response.detail) return
|
||||
|
||||
const detail = response.detail
|
||||
dashboardData.totalFiles = toNumber(detail.totalFiles)
|
||||
dashboardData.storageUsed = toNumber(detail.storageUsed)
|
||||
dashboardData.yesterdayCount = toNumber(detail.yesterdayCount)
|
||||
dashboardData.todayCount = toNumber(detail.todayCount)
|
||||
dashboardData.yesterdaySize = toNumber(detail.yesterdaySize)
|
||||
dashboardData.todaySize = toNumber(detail.todaySize)
|
||||
dashboardData.sysUptime = detail.sysUptime
|
||||
dashboardData.hasExtendedStats = hasOwn(detail, 'activeCount')
|
||||
dashboardData.activeCount = dashboardData.hasExtendedStats
|
||||
? toNumber(detail.activeCount)
|
||||
: dashboardData.totalFiles
|
||||
dashboardData.expiredCount = toNumber(detail.expiredCount)
|
||||
dashboardData.textCount = toNumber(detail.textCount)
|
||||
dashboardData.fileCount = toNumber(detail.fileCount)
|
||||
dashboardData.chunkedCount = toNumber(detail.chunkedCount)
|
||||
dashboardData.usedCount = toNumber(detail.usedCount)
|
||||
dashboardData.storageBackend = detail.storageBackend || '-'
|
||||
dashboardData.uploadSizeLimit = toNumber(detail.uploadSizeLimit)
|
||||
dashboardData.openUpload = toNumber(detail.openUpload)
|
||||
dashboardData.enableChunk = toNumber(detail.enableChunk)
|
||||
dashboardData.maxSaveSeconds = toNumber(detail.maxSaveSeconds)
|
||||
dashboardData.topSuffixes = detail.topSuffixes || []
|
||||
dashboardData.recentFiles = detail.recentFiles || []
|
||||
|
||||
dashboardData.storageUsedText = formatFileSize(dashboardData.storageUsed)
|
||||
dashboardData.yesterdaySizeText = formatFileSize(dashboardData.yesterdaySize)
|
||||
dashboardData.todaySizeText = formatFileSize(dashboardData.todaySize)
|
||||
dashboardData.uploadSizeLimitText = formatFileSize(dashboardData.uploadSizeLimit)
|
||||
dashboardData.sysUptimeText = formatDuration(dashboardData.sysUptime)
|
||||
dashboardData.activeRatio = dashboardData.totalFiles
|
||||
? clampRatio((dashboardData.activeCount / dashboardData.totalFiles) * 100)
|
||||
: 0
|
||||
dashboardData.textRatio = dashboardData.totalFiles
|
||||
? clampRatio((dashboardData.textCount / dashboardData.totalFiles) * 100)
|
||||
: 0
|
||||
dashboardData.fileRatio = dashboardData.totalFiles
|
||||
? clampRatio((dashboardData.fileCount / dashboardData.totalFiles) * 100)
|
||||
: 0
|
||||
dashboardData.todaySizeRatio = dashboardData.uploadSizeLimit
|
||||
? clampRatio((dashboardData.todaySize / dashboardData.uploadSizeLimit) * 100)
|
||||
: 0
|
||||
}
|
||||
|
||||
return {
|
||||
dashboardData,
|
||||
fetchDashboardData
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
import { ref, computed, readonly } from 'vue'
|
||||
import { FileService } from '@/services'
|
||||
import { useAlertStore } from '@/stores/alertStore'
|
||||
import { usePresignedUpload } from '@/composables/usePresignedUpload'
|
||||
import type { UploadProgress, UploadStatus, PresignUploadOptions, ExpireStyle, ConfigState } from '@/types'
|
||||
import { UPLOAD_STATUS, FILE_SIZE_LIMITS, STORAGE_KEYS } from '@/constants'
|
||||
|
||||
/**
|
||||
* 获取最大文件大小限制(字节)
|
||||
* 优先从后端配置获取,否则使用默认值
|
||||
*/
|
||||
function getMaxFileSize(): number {
|
||||
try {
|
||||
const configStr = localStorage.getItem(STORAGE_KEYS.CONFIG)
|
||||
if (configStr) {
|
||||
const config = JSON.parse(configStr) as Partial<ConfigState>
|
||||
if (config.uploadSize && config.uploadSize > 0) {
|
||||
// uploadSize 单位是字节
|
||||
return config.uploadSize
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 解析失败时使用默认值
|
||||
}
|
||||
return FILE_SIZE_LIMITS.MAX_FILE_SIZE
|
||||
}
|
||||
|
||||
export interface FileUploadOptions {
|
||||
/** 是否使用预签名上传,默认 false 保持向后兼容 */
|
||||
usePresigned?: boolean
|
||||
/** 过期时间值 */
|
||||
expireValue?: number
|
||||
/** 过期时间类型 */
|
||||
expireStyle?: ExpireStyle
|
||||
/** 进度回调 */
|
||||
onProgress?: (progress: UploadProgress) => void
|
||||
}
|
||||
|
||||
export function useFileUpload(options?: { defaultUsePresigned?: boolean }) {
|
||||
const alertStore = useAlertStore()
|
||||
|
||||
// 预签名上传 composable
|
||||
const presignedUpload = usePresignedUpload()
|
||||
|
||||
// 是否默认使用预签名上传
|
||||
const defaultUsePresigned = options?.defaultUsePresigned ?? false
|
||||
|
||||
// 状态管理
|
||||
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 isUsingPresigned = ref<boolean>(false)
|
||||
|
||||
// 计算属性
|
||||
const isUploading = computed(() => {
|
||||
if (isUsingPresigned.value) {
|
||||
return presignedUpload.isUploading.value || presignedUpload.isInitializing.value || presignedUpload.isConfirming.value
|
||||
}
|
||||
return uploadStatus.value === UPLOAD_STATUS.UPLOADING
|
||||
})
|
||||
const isSuccess = computed(() => {
|
||||
if (isUsingPresigned.value) {
|
||||
return presignedUpload.isSuccess.value
|
||||
}
|
||||
return uploadStatus.value === UPLOAD_STATUS.SUCCESS
|
||||
})
|
||||
const isError = computed(() => {
|
||||
if (isUsingPresigned.value) {
|
||||
return presignedUpload.isError.value
|
||||
}
|
||||
return uploadStatus.value === UPLOAD_STATUS.ERROR
|
||||
})
|
||||
const isIdle = computed(() => {
|
||||
if (isUsingPresigned.value) {
|
||||
return presignedUpload.presignStatus.value === 'idle'
|
||||
}
|
||||
return uploadStatus.value === UPLOAD_STATUS.IDLE
|
||||
})
|
||||
|
||||
// 文件验证
|
||||
const validateFile = (file: File): boolean => {
|
||||
const maxFileSize = getMaxFileSize()
|
||||
if (file.size > maxFileSize) {
|
||||
alertStore.showAlert(
|
||||
`文件大小不能超过 ${Math.round(maxFileSize / 1024 / 1024)}MB`,
|
||||
'error'
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件(支持预签名上传和传统上传)
|
||||
*/
|
||||
const uploadFile = async (file: File, uploadOptions?: FileUploadOptions): Promise<string | null> => {
|
||||
const shouldUsePresigned = uploadOptions?.usePresigned ?? defaultUsePresigned
|
||||
|
||||
if (!validateFile(file)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 记录当前使用的上传方式
|
||||
isUsingPresigned.value = shouldUsePresigned
|
||||
currentFile.value = file
|
||||
|
||||
if (shouldUsePresigned) {
|
||||
// 使用预签名上传
|
||||
return await uploadFileWithPresigned(file, uploadOptions)
|
||||
} else {
|
||||
// 使用传统上传方式
|
||||
return await uploadFileTraditional(file, uploadOptions?.onProgress)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 传统上传方式
|
||||
*/
|
||||
const uploadFileTraditional = async (
|
||||
file: File,
|
||||
onProgress?: (progress: UploadProgress) => void
|
||||
): Promise<string | null> => {
|
||||
try {
|
||||
uploadStatus.value = UPLOAD_STATUS.UPLOADING
|
||||
uploadedCode.value = ''
|
||||
|
||||
const response = await FileService.uploadFile(file, (progress) => {
|
||||
uploadProgress.value = progress
|
||||
onProgress?.(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 uploadFileWithPresigned = async (
|
||||
file: File,
|
||||
uploadOptions?: FileUploadOptions
|
||||
): Promise<string | null> => {
|
||||
// 构建预签名上传选项
|
||||
const presignOptions: PresignUploadOptions = {
|
||||
expireValue: uploadOptions?.expireValue,
|
||||
expireStyle: uploadOptions?.expireStyle,
|
||||
onProgress: (progress) => {
|
||||
// 同步进度到本 composable 的状态
|
||||
uploadProgress.value = progress
|
||||
uploadOptions?.onProgress?.(progress)
|
||||
}
|
||||
}
|
||||
|
||||
const result = await presignedUpload.uploadFile(file, presignOptions)
|
||||
|
||||
if (result) {
|
||||
// 同步预签名上传的结果到本 composable 的状态
|
||||
uploadedCode.value = result
|
||||
uploadStatus.value = UPLOAD_STATUS.SUCCESS
|
||||
} else {
|
||||
uploadStatus.value = UPLOAD_STATUS.ERROR
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 上传文本
|
||||
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
|
||||
|
||||
// 如果使用预签名上传,也重置预签名状态
|
||||
if (isUsingPresigned.value) {
|
||||
presignedUpload.reset()
|
||||
}
|
||||
isUsingPresigned.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消上传
|
||||
*/
|
||||
const cancelUpload = async (): Promise<void> => {
|
||||
if (isUsingPresigned.value) {
|
||||
await presignedUpload.cancelUpload()
|
||||
}
|
||||
resetUpload()
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
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),
|
||||
isUsingPresigned: readonly(isUsingPresigned),
|
||||
|
||||
// 预签名上传相关状态(透传)
|
||||
presignStatus: presignedUpload.presignStatus,
|
||||
uploadSession: presignedUpload.uploadSession,
|
||||
currentMode: presignedUpload.currentMode,
|
||||
presignErrorMessage: presignedUpload.errorMessage,
|
||||
|
||||
// 计算属性
|
||||
isUploading,
|
||||
isSuccess,
|
||||
isError,
|
||||
isIdle,
|
||||
|
||||
// 预签名上传计算属性(透传)
|
||||
isInitializing: presignedUpload.isInitializing,
|
||||
isConfirming: presignedUpload.isConfirming,
|
||||
|
||||
// 方法
|
||||
uploadFile,
|
||||
uploadText,
|
||||
resetUpload,
|
||||
cancelUpload,
|
||||
validateFile,
|
||||
formatFileSize,
|
||||
|
||||
// 预签名上传方法(透传)
|
||||
getPresignStatus: presignedUpload.getStatus
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { computed, inject, unref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
export function useInjectedDarkMode() {
|
||||
const injectedDarkMode = inject<Ref<boolean> | boolean>('isDarkMode', false)
|
||||
return computed(() => Boolean(unref(injectedDarkMode)))
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { ref, computed, readonly } from 'vue'
|
||||
import { PresignUploadService } from '@/services'
|
||||
import { useAlertStore } from '@/stores/alertStore'
|
||||
import { FILE_SIZE_LIMITS, STORAGE_KEYS } from '@/constants'
|
||||
import type {
|
||||
PresignUploadStatus,
|
||||
PresignUploadMode,
|
||||
@@ -10,29 +8,9 @@ import type {
|
||||
PresignStatusResponse,
|
||||
UploadProgress,
|
||||
ExpireStyle,
|
||||
ConfigState
|
||||
AlertType
|
||||
} from '@/types'
|
||||
import axios from 'axios'
|
||||
|
||||
/**
|
||||
* 获取最大文件大小限制(字节)
|
||||
* 优先从后端配置获取,否则使用默认值
|
||||
*/
|
||||
function getMaxFileSize(): number {
|
||||
try {
|
||||
const configStr = localStorage.getItem(STORAGE_KEYS.CONFIG)
|
||||
if (configStr) {
|
||||
const config = JSON.parse(configStr) as Partial<ConfigState>
|
||||
if (config.uploadSize && config.uploadSize > 0) {
|
||||
// uploadSize 单位是字节
|
||||
return config.uploadSize
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 解析失败时使用默认值
|
||||
}
|
||||
return FILE_SIZE_LIMITS.MAX_FILE_SIZE
|
||||
}
|
||||
import { getErrorMessage } from '@/utils/common'
|
||||
|
||||
// 预签名上传状态常量
|
||||
export const PRESIGN_UPLOAD_STATUS = {
|
||||
@@ -48,13 +26,24 @@ export const PRESIGN_UPLOAD_STATUS = {
|
||||
const DEFAULT_EXPIRE_VALUE = 1
|
||||
const DEFAULT_EXPIRE_STYLE: ExpireStyle = 'day'
|
||||
|
||||
type ErrorWithResponse = {
|
||||
response?: {
|
||||
status?: number
|
||||
}
|
||||
}
|
||||
|
||||
type PresignedUploadNotifier = (message: string, type: AlertType) => void
|
||||
|
||||
type UsePresignedUploadOptions = {
|
||||
getMaxFileSize?: () => number
|
||||
notify?: PresignedUploadNotifier
|
||||
}
|
||||
|
||||
/**
|
||||
* 预签名上传 Composable
|
||||
* 支持 S3 直传模式和服务器代理模式
|
||||
*/
|
||||
export function usePresignedUpload() {
|
||||
const alertStore = useAlertStore()
|
||||
|
||||
export function usePresignedUpload(options: UsePresignedUploadOptions = {}) {
|
||||
// 状态管理
|
||||
const presignStatus = ref<PresignUploadStatus>(PRESIGN_UPLOAD_STATUS.IDLE)
|
||||
const uploadSession = ref<PresignInitResponse | null>(null)
|
||||
@@ -74,15 +63,23 @@ export function usePresignedUpload() {
|
||||
const isError = computed(() => presignStatus.value === PRESIGN_UPLOAD_STATUS.ERROR)
|
||||
const currentMode = computed<PresignUploadMode | null>(() => uploadSession.value?.mode ?? null)
|
||||
|
||||
const notify: PresignedUploadNotifier = (message, type) => {
|
||||
options.notify?.(message, type)
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件大小验证
|
||||
*/
|
||||
const validateFileSize = (file: File): boolean => {
|
||||
const maxFileSize = getMaxFileSize()
|
||||
const maxFileSize = options.getMaxFileSize?.()
|
||||
if (!maxFileSize) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (file.size > maxFileSize) {
|
||||
const maxSizeMB = Math.round(maxFileSize / 1024 / 1024)
|
||||
errorMessage.value = `文件大小不能超过 ${maxSizeMB}MB`
|
||||
alertStore.showAlert(errorMessage.value, 'error')
|
||||
notify(errorMessage.value, 'error')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -113,34 +110,16 @@ export function usePresignedUpload() {
|
||||
*/
|
||||
const handleUploadError = (error: unknown): void => {
|
||||
presignStatus.value = PRESIGN_UPLOAD_STATUS.ERROR
|
||||
const status = (error as ErrorWithResponse)?.response?.status
|
||||
const fallback =
|
||||
status === 404
|
||||
? '上传会话不存在或已过期'
|
||||
: status === 500
|
||||
? '服务器错误,请稍后重试'
|
||||
: '上传失败,请重试'
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
const status = error.response?.status
|
||||
const detail = error.response?.data?.detail
|
||||
|
||||
switch (status) {
|
||||
case 400:
|
||||
errorMessage.value = detail || '请求参数错误'
|
||||
break
|
||||
case 403:
|
||||
errorMessage.value = detail || '操作被禁止'
|
||||
break
|
||||
case 404:
|
||||
errorMessage.value = '上传会话不存在或已过期'
|
||||
break
|
||||
case 500:
|
||||
errorMessage.value = '服务器错误,请稍后重试'
|
||||
break
|
||||
default:
|
||||
errorMessage.value = detail || '上传失败,请重试'
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage.value = error.message
|
||||
} else {
|
||||
errorMessage.value = '未知错误'
|
||||
}
|
||||
|
||||
alertStore.showAlert(errorMessage.value, 'error')
|
||||
errorMessage.value = getErrorMessage(error, fallback)
|
||||
notify(errorMessage.value, 'error')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,7 +190,7 @@ export function usePresignedUpload() {
|
||||
total: file.size,
|
||||
percentage: 100
|
||||
}
|
||||
alertStore.showAlert('文件上传成功!', 'success')
|
||||
notify('文件上传成功!', 'success')
|
||||
return uploadedCode.value
|
||||
} else {
|
||||
throw new Error(confirmResponse.message || '确认上传失败')
|
||||
@@ -249,7 +228,7 @@ export function usePresignedUpload() {
|
||||
total: file.size,
|
||||
percentage: 100
|
||||
}
|
||||
alertStore.showAlert('文件上传成功!', 'success')
|
||||
notify('文件上传成功!', 'success')
|
||||
return uploadedCode.value
|
||||
} else {
|
||||
throw new Error(response.message || '代理上传失败')
|
||||
@@ -290,7 +269,7 @@ export function usePresignedUpload() {
|
||||
|
||||
try {
|
||||
await PresignUploadService.cancelUpload(uploadSession.value.upload_id)
|
||||
alertStore.showAlert('上传已取消', 'info')
|
||||
notify('上传已取消', 'info')
|
||||
} catch (error) {
|
||||
// 取消失败时静默处理,因为会话可能已过期
|
||||
console.warn('取消上传失败:', error)
|
||||
@@ -313,7 +292,7 @@ export function usePresignedUpload() {
|
||||
// 检查会话是否过期
|
||||
if (response.detail.is_expired) {
|
||||
errorMessage.value = '上传会话已过期'
|
||||
alertStore.showAlert(errorMessage.value, 'warning')
|
||||
notify(errorMessage.value, 'warning')
|
||||
}
|
||||
return response.detail
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ConfigService } from '@/services'
|
||||
import { useAlertStore } from '@/stores/alertStore'
|
||||
import { useConfigStore } from '@/stores/configStore'
|
||||
|
||||
export function usePublicConfigBootstrap() {
|
||||
const alertStore = useAlertStore()
|
||||
const configStore = useConfigStore()
|
||||
|
||||
const syncPublicConfig = async () => {
|
||||
const res = await ConfigService.getUserConfig()
|
||||
|
||||
if (res.code !== 200 || !res.detail) {
|
||||
return
|
||||
}
|
||||
|
||||
const notifyMessage = configStore.applyRemoteConfig(res.detail)
|
||||
if (notifyMessage) {
|
||||
alertStore.showAlert(notifyMessage, 'success')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
syncPublicConfig
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { FileService } from '@/services'
|
||||
import { useAlertStore } from '@/stores/alertStore'
|
||||
import { useFileDataStore } from '@/stores/fileData'
|
||||
import type { ReceivedFileRecord } from '@/types'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
import { getErrorMessage } from '@/utils/common'
|
||||
import { renderMarkdownPreview } from '@/utils/content-preview'
|
||||
import { downloadReceivedRecord } from '@/utils/download-action'
|
||||
|
||||
type InputStatus = {
|
||||
readonly: boolean
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
export function useRetrieveFlow() {
|
||||
const { t } = useI18n()
|
||||
const alertStore = useAlertStore()
|
||||
const fileStore = useFileDataStore()
|
||||
const { receiveData: records } = storeToRefs(fileStore)
|
||||
|
||||
const code = ref('')
|
||||
const inputStatus = ref<InputStatus>({
|
||||
readonly: false,
|
||||
loading: false
|
||||
})
|
||||
const error = ref('')
|
||||
const selectedRecord = ref<ReceivedFileRecord | null>(null)
|
||||
const showDrawer = ref(false)
|
||||
const showPreview = ref(false)
|
||||
const renderedContent = ref('')
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 ' + t('fileSize.bytes')
|
||||
const k = 1024
|
||||
const sizes = [
|
||||
t('fileSize.bytes'),
|
||||
t('fileSize.kb'),
|
||||
t('fileSize.mb'),
|
||||
t('fileSize.gb'),
|
||||
t('fileSize.tb')
|
||||
]
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
const createRecord = (detail: {
|
||||
code: string
|
||||
name: string
|
||||
text: string
|
||||
size: number
|
||||
}): ReceivedFileRecord => {
|
||||
const isFile = detail.text.startsWith('/share/download') || detail.name !== 'Text'
|
||||
return {
|
||||
id: Date.now(),
|
||||
code: detail.code,
|
||||
filename: detail.name,
|
||||
size: formatFileSize(detail.size),
|
||||
downloadUrl: isFile ? detail.text : null,
|
||||
content: isFile ? null : detail.text,
|
||||
date: new Date().toLocaleString()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (code.value.length !== 5) {
|
||||
alertStore.showAlert(t('retrieve.messages.invalidCode'), 'error')
|
||||
return
|
||||
}
|
||||
|
||||
inputStatus.value.readonly = true
|
||||
inputStatus.value.loading = true
|
||||
|
||||
try {
|
||||
const res = await FileService.selectFile(code.value)
|
||||
if (res.code === 200 && res.detail) {
|
||||
const newFileData = createRecord(res.detail)
|
||||
if (!fileStore.receiveData.some((file) => file.code === newFileData.code)) {
|
||||
fileStore.addReceiveData(newFileData)
|
||||
}
|
||||
selectedRecord.value = newFileData
|
||||
if (newFileData.content) {
|
||||
showPreview.value = true
|
||||
}
|
||||
alertStore.showAlert(t('retrieve.messages.retrieveSuccess'), 'success')
|
||||
} else {
|
||||
alertStore.showAlert(t('retrieve.messages.retrieveFailure') + res.detail, 'error')
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = getErrorMessage(err, t('retrieve.messages.unknownError'))
|
||||
alertStore.showAlert(t('retrieve.messages.networkError') + errorMessage, 'error')
|
||||
} finally {
|
||||
inputStatus.value.readonly = false
|
||||
inputStatus.value.loading = false
|
||||
code.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const copyContent = async () => {
|
||||
if (selectedRecord.value?.content) {
|
||||
await copyToClipboard(selectedRecord.value.content, {
|
||||
successMsg: t('fileRecord.contentCopied'),
|
||||
errorMsg: t('fileRecord.copyFailed'),
|
||||
notify: (message, type) => alertStore.showAlert(message, type)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const viewDetails = (record: ReceivedFileRecord) => {
|
||||
selectedRecord.value = record
|
||||
}
|
||||
|
||||
const closeDetails = () => {
|
||||
selectedRecord.value = null
|
||||
}
|
||||
|
||||
const deleteRecord = (id: number) => {
|
||||
const index = records.value.findIndex((record) => record.id === id)
|
||||
if (index !== -1) {
|
||||
fileStore.deleteReceiveData(index)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleDrawer = () => {
|
||||
showDrawer.value = !showDrawer.value
|
||||
}
|
||||
|
||||
const downloadRecord = (record: ReceivedFileRecord) => {
|
||||
downloadReceivedRecord(record)
|
||||
}
|
||||
|
||||
const showContentPreview = () => {
|
||||
showPreview.value = true
|
||||
}
|
||||
|
||||
const closeContentPreview = () => {
|
||||
showPreview.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => selectedRecord.value?.content,
|
||||
async (content) => {
|
||||
if (content) {
|
||||
renderedContent.value = await renderMarkdownPreview(content)
|
||||
} else {
|
||||
renderedContent.value = ''
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
return {
|
||||
code,
|
||||
inputStatus,
|
||||
error,
|
||||
records,
|
||||
selectedRecord,
|
||||
showDrawer,
|
||||
showPreview,
|
||||
renderedContent,
|
||||
closeContentPreview,
|
||||
closeDetails,
|
||||
copyContent,
|
||||
deleteRecord,
|
||||
downloadRecord,
|
||||
handleSubmit,
|
||||
showContentPreview,
|
||||
toggleDrawer,
|
||||
viewDetails
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import type { Router } from 'vue-router'
|
||||
|
||||
const ROUTE_LOADING_DELAY = 200
|
||||
|
||||
export function useRouteLoading(router: Router) {
|
||||
const isLoading = ref(false)
|
||||
let loadingTimer: number | null = null
|
||||
let cleanupBeforeGuard: (() => void) | null = null
|
||||
let cleanupAfterHook: (() => void) | null = null
|
||||
|
||||
const clearLoadingTimer = () => {
|
||||
if (loadingTimer !== null) {
|
||||
window.clearTimeout(loadingTimer)
|
||||
loadingTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
const setupRouteLoading = () => {
|
||||
cleanupBeforeGuard = router.beforeEach((to) => {
|
||||
clearLoadingTimer()
|
||||
isLoading.value = to.meta.showRouteLoading !== false
|
||||
})
|
||||
|
||||
cleanupAfterHook = router.afterEach(() => {
|
||||
clearLoadingTimer()
|
||||
loadingTimer = window.setTimeout(() => {
|
||||
isLoading.value = false
|
||||
loadingTimer = null
|
||||
}, ROUTE_LOADING_DELAY)
|
||||
})
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
clearLoadingTimer()
|
||||
cleanupBeforeGuard?.()
|
||||
cleanupAfterHook?.()
|
||||
})
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
setupRouteLoading
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAlertStore } from '@/stores/alertStore'
|
||||
import { useAdminStore } from '@/stores/adminStore'
|
||||
import { useConfigStore } from '@/stores/configStore'
|
||||
import { useFileDataStore } from '@/stores/fileData'
|
||||
import type { SendType, SentFileRecord } from '@/types'
|
||||
import { getClipboardFile, insertTextAtSelection } from '@/utils/clipboard-paste'
|
||||
import { getErrorMessage } from '@/utils/common'
|
||||
import { getStorageUnit } from '@/utils/convert'
|
||||
import { calculateFileHash } from '@/utils/file-processing'
|
||||
import { buildSentRecord, isExpirationWithinLimit } from '@/utils/send-record'
|
||||
import { createSentRecordActions } from '@/utils/sent-record-actions'
|
||||
import { useSendSubmit } from './useSendSubmit'
|
||||
|
||||
export function useSendFlow() {
|
||||
const { t } = useI18n()
|
||||
const alertStore = useAlertStore()
|
||||
const adminStore = useAdminStore()
|
||||
const configStore = useConfigStore()
|
||||
const fileDataStore = useFileDataStore()
|
||||
const config = computed(() => configStore.config)
|
||||
const sendType = ref<SendType>('file')
|
||||
const selectedFile = ref<File | null>(null)
|
||||
const selectedFiles = ref<File[]>([])
|
||||
const textContent = ref('')
|
||||
const expirationMethod = ref(config.value.expireStyle[0] || 'day')
|
||||
const expirationValue = ref('1')
|
||||
const uploadProgress = ref(0)
|
||||
const showDrawer = ref(false)
|
||||
const selectedRecord = ref<SentFileRecord | null>(null)
|
||||
const isSubmitting = ref(false)
|
||||
const fileHash = ref('')
|
||||
const sendRecords = computed(() => fileDataStore.shareData)
|
||||
const uploadDescription = computed(
|
||||
() => `支持各种常见格式,最大${getStorageUnit(config.value.uploadSize)}`
|
||||
)
|
||||
const expirationOptions = computed(() =>
|
||||
config.value.expireStyle.map((value) => ({
|
||||
value,
|
||||
label: getUnit(value)
|
||||
}))
|
||||
)
|
||||
watch(
|
||||
() => config.value.expireStyle,
|
||||
(expireStyle) => {
|
||||
if (expireStyle.length > 0 && !expireStyle.includes(expirationMethod.value)) {
|
||||
expirationMethod.value = expireStyle[0]
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
const notifyCopyResult = (message: string, type: 'success' | 'error') => {
|
||||
alertStore.showAlert(message, type)
|
||||
}
|
||||
const sentRecordActions = createSentRecordActions(notifyCopyResult)
|
||||
const { resetPresignUpload, submitFile, submitText } = useSendSubmit({
|
||||
getMaxFileSize: () => configStore.uploadSizeLimit,
|
||||
notify: (message, type) => alertStore.showAlert(message, type),
|
||||
translate: t,
|
||||
onProgress: (progress) => {
|
||||
uploadProgress.value = progress
|
||||
},
|
||||
onHashCalculated: (hash) => {
|
||||
fileHash.value = hash
|
||||
}
|
||||
})
|
||||
|
||||
const checkOpenUpload = () => {
|
||||
if (config.value.openUpload === 0 && !adminStore.hasToken) {
|
||||
alertStore.showAlert(t('send.messages.guestUploadDisabled'), 'error')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const checkFileSize = (file: File) => {
|
||||
if (file.size > config.value.uploadSize) {
|
||||
alertStore.showAlert(
|
||||
t('send.messages.fileSizeExceeded', { size: getStorageUnit(config.value.uploadSize) }),
|
||||
'error'
|
||||
)
|
||||
selectedFile.value = null
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const checkExpirationTime = (method: string, value: string): boolean =>
|
||||
isExpirationWithinLimit(method, value, config.value.max_save_seconds || 0)
|
||||
|
||||
const checkUpload = () => {
|
||||
if (!selectedFile.value) return false
|
||||
if (!checkOpenUpload()) return false
|
||||
if (!checkFileSize(selectedFile.value)) return false
|
||||
if (!checkExpirationTime(expirationMethod.value, expirationValue.value)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
const handleFileSelected = async (file: File) => {
|
||||
selectedFile.value = file
|
||||
selectedFiles.value = []
|
||||
if (!checkOpenUpload()) return
|
||||
if (!checkFileSize(file)) return
|
||||
fileHash.value = await calculateFileHash(file)
|
||||
}
|
||||
|
||||
const handleFilesSelected = async (files: File[]) => {
|
||||
if (!checkOpenUpload()) return
|
||||
selectedFiles.value = files
|
||||
selectedFile.value = null
|
||||
fileHash.value = ''
|
||||
}
|
||||
|
||||
const handleFileDrop = async (event: DragEvent) => {
|
||||
if (!event.dataTransfer?.files || event.dataTransfer.files.length === 0) return
|
||||
const files = Array.from(event.dataTransfer.files)
|
||||
if (files.length === 1) {
|
||||
const file = files[0]
|
||||
selectedFile.value = file
|
||||
selectedFiles.value = []
|
||||
if (!checkUpload()) return
|
||||
fileHash.value = await calculateFileHash(file)
|
||||
} else {
|
||||
if (!checkOpenUpload()) return
|
||||
selectedFiles.value = files
|
||||
selectedFile.value = null
|
||||
fileHash.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const handlePaste = async (event: ClipboardEvent) => {
|
||||
const items = event.clipboardData?.items
|
||||
if (!items) return
|
||||
|
||||
const file = getClipboardFile(items)
|
||||
if (file) {
|
||||
if (file.size === 0) {
|
||||
alertStore.showAlert(t('send.messages.emptyFileError'), 'error')
|
||||
return
|
||||
}
|
||||
|
||||
selectedFile.value = file
|
||||
if (!checkUpload()) return
|
||||
|
||||
try {
|
||||
fileHash.value = await calculateFileHash(file)
|
||||
alertStore.showAlert(
|
||||
t('send.messages.fileAddedFromClipboard', { filename: file.name }),
|
||||
'success'
|
||||
)
|
||||
} catch (err) {
|
||||
alertStore.showAlert(t('send.messages.fileProcessingFailed'), 'error')
|
||||
console.error('File hash calculation failed:', err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const textItem = items[0]
|
||||
if (!textItem) return
|
||||
|
||||
sendType.value = 'text'
|
||||
textItem.getAsString((str: string) => {
|
||||
const trimmedStr = str.trim()
|
||||
if (!trimmedStr) return
|
||||
|
||||
const textareaElement = document.getElementById('text-content') as HTMLTextAreaElement
|
||||
if (!textareaElement) {
|
||||
textContent.value += trimmedStr
|
||||
return
|
||||
}
|
||||
|
||||
const insertion = insertTextAtSelection({
|
||||
text: textContent.value,
|
||||
insertText: trimmedStr,
|
||||
selectionStart: textareaElement.selectionStart,
|
||||
selectionEnd: textareaElement.selectionEnd
|
||||
})
|
||||
textContent.value = insertion.value
|
||||
|
||||
setTimeout(() => {
|
||||
textareaElement.setSelectionRange(insertion.cursor, insertion.cursor)
|
||||
textareaElement.focus()
|
||||
}, 0)
|
||||
})
|
||||
}
|
||||
|
||||
const getUnit = (value: string = expirationMethod.value) => {
|
||||
switch (value) {
|
||||
case 'day':
|
||||
return t('send.expiration.units.days')
|
||||
case 'hour':
|
||||
return t('send.expiration.units.hours')
|
||||
case 'minute':
|
||||
return t('send.expiration.units.minutes')
|
||||
case 'count':
|
||||
return t('send.expiration.units.times')
|
||||
case 'forever':
|
||||
return t('send.expiration.units.forever')
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (isSubmitting.value) return
|
||||
isSubmitting.value = true
|
||||
|
||||
try {
|
||||
if (sendType.value === 'file' && !selectedFile.value && selectedFiles.value.length === 0) {
|
||||
alertStore.showAlert(t('send.messages.selectFile'), 'error')
|
||||
return
|
||||
}
|
||||
if (sendType.value === 'text' && !textContent.value.trim()) {
|
||||
alertStore.showAlert(t('send.messages.enterText'), 'error')
|
||||
return
|
||||
}
|
||||
if (!checkOpenUpload()) {
|
||||
return
|
||||
}
|
||||
if (expirationMethod.value !== 'forever' && !expirationValue.value) {
|
||||
alertStore.showAlert(t('send.messages.enterExpirationValue'), 'error')
|
||||
return
|
||||
}
|
||||
|
||||
if (!checkExpirationTime(expirationMethod.value, expirationValue.value)) {
|
||||
const maxDays = Math.floor(config.value.max_save_seconds / 86400)
|
||||
alertStore.showAlert(t('send.messages.expirationTooLong', { days: maxDays }), 'error')
|
||||
return
|
||||
}
|
||||
|
||||
const expireValue = expirationValue.value ? parseInt(expirationValue.value) : 1
|
||||
let response
|
||||
if (sendType.value === 'file') {
|
||||
response = await submitFile({
|
||||
selectedFile: selectedFile.value,
|
||||
selectedFiles: selectedFiles.value,
|
||||
expireValue,
|
||||
expireStyle: expirationMethod.value,
|
||||
enableChunk: Boolean(config.value.enableChunk),
|
||||
validateFileSize: checkFileSize
|
||||
})
|
||||
} else {
|
||||
response = await submitText({
|
||||
text: textContent.value,
|
||||
expireValue,
|
||||
expireStyle: expirationMethod.value
|
||||
})
|
||||
}
|
||||
|
||||
if (!response) return
|
||||
|
||||
if (response?.code === 200) {
|
||||
const newRecord = buildSentRecord({
|
||||
response,
|
||||
sendType: sendType.value,
|
||||
textContent: textContent.value,
|
||||
selectedFile: selectedFile.value,
|
||||
selectedFiles: selectedFiles.value,
|
||||
expirationMethod: expirationMethod.value,
|
||||
expirationValue: expirationValue.value,
|
||||
translate: t,
|
||||
getUnit
|
||||
})
|
||||
fileDataStore.addShareDataRecord(newRecord)
|
||||
alertStore.showAlert(
|
||||
t('send.messages.sendSuccess', { code: newRecord.retrieveCode }),
|
||||
'success'
|
||||
)
|
||||
selectedFile.value = null
|
||||
selectedFiles.value = []
|
||||
textContent.value = ''
|
||||
uploadProgress.value = 0
|
||||
resetPresignUpload()
|
||||
selectedRecord.value = newRecord
|
||||
await sentRecordActions.copyLink(newRecord)
|
||||
} else {
|
||||
throw new Error(t('send.messages.serverError'))
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
alertStore.showAlert(getErrorMessage(error, t('send.messages.sendFailed')), 'error')
|
||||
} finally {
|
||||
uploadProgress.value = 0
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const toggleDrawer = () => {
|
||||
showDrawer.value = !showDrawer.value
|
||||
}
|
||||
|
||||
const viewDetails = (record: SentFileRecord) => {
|
||||
selectedRecord.value = record
|
||||
}
|
||||
|
||||
const closeDetails = () => {
|
||||
selectedRecord.value = null
|
||||
}
|
||||
|
||||
const deleteRecord = (id: number) => {
|
||||
const index = fileDataStore.shareData.findIndex((record) => record.id === id)
|
||||
if (index !== -1) {
|
||||
fileDataStore.deleteShareData(index)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
sendType,
|
||||
selectedFile,
|
||||
selectedFiles,
|
||||
textContent,
|
||||
expirationMethod,
|
||||
expirationValue,
|
||||
uploadProgress,
|
||||
showDrawer,
|
||||
selectedRecord,
|
||||
isSubmitting,
|
||||
sendRecords,
|
||||
uploadDescription,
|
||||
expirationOptions,
|
||||
closeDetails,
|
||||
deleteRecord,
|
||||
copySentRecordCode: sentRecordActions.copyCode,
|
||||
copySentRecordLink: sentRecordActions.copyLink,
|
||||
copySentRecordWgetCommand: sentRecordActions.copyWgetCommand,
|
||||
getQRCodeValue: sentRecordActions.getQRCodeValue,
|
||||
getUnit,
|
||||
handleFileDrop,
|
||||
handleFileSelected,
|
||||
handleFilesSelected,
|
||||
handlePaste,
|
||||
handleSubmit,
|
||||
toggleDrawer,
|
||||
viewDetails
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { FileService, uploadChunkedFile } from '@/services'
|
||||
import type { AlertType, ApiResponse, ExpireStyle, UploadProgress } from '@/types'
|
||||
import { calculateFileHash, packFilesAsZip } from '@/utils/file-processing'
|
||||
import { usePresignedUpload } from './usePresignedUpload'
|
||||
|
||||
type Translate = (
|
||||
key: string,
|
||||
params?: Record<string, string | number | undefined>
|
||||
) => string
|
||||
|
||||
type UseSendSubmitOptions = {
|
||||
getMaxFileSize: () => number
|
||||
notify: (message: string, type: AlertType) => void
|
||||
translate: Translate
|
||||
onProgress: (progress: number) => void
|
||||
onHashCalculated: (hash: string) => void
|
||||
}
|
||||
|
||||
type SubmitFileOptions = {
|
||||
selectedFile: File | null
|
||||
selectedFiles: File[]
|
||||
expireValue: number
|
||||
expireStyle: string
|
||||
enableChunk: boolean
|
||||
validateFileSize: (file: File) => boolean
|
||||
}
|
||||
|
||||
type SubmitTextOptions = {
|
||||
text: string
|
||||
expireValue: number
|
||||
expireStyle: string
|
||||
}
|
||||
|
||||
export function useSendSubmit(options: UseSendSubmitOptions) {
|
||||
const { uploadFile: presignUploadFile, reset: resetPresignUpload } = usePresignedUpload({
|
||||
getMaxFileSize: options.getMaxFileSize,
|
||||
notify: options.notify
|
||||
})
|
||||
|
||||
const handleChunkUpload = async (
|
||||
file: File,
|
||||
expireValue: number,
|
||||
expireStyle: string
|
||||
): Promise<ApiResponse> => {
|
||||
return uploadChunkedFile(file, {
|
||||
expireValue,
|
||||
expireStyle,
|
||||
onHashCalculated: options.onHashCalculated,
|
||||
onProgress: (progress: UploadProgress) => {
|
||||
options.onProgress(progress.percentage)
|
||||
},
|
||||
messages: {
|
||||
initFailed: options.translate('send.messages.initChunkUploadFailed'),
|
||||
chunkFailed: (index) => options.translate('send.messages.chunkUploadFailed', { index }),
|
||||
completeFailed: options.translate('send.messages.completeUploadFailed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handlePresignedUpload = async (
|
||||
file: File,
|
||||
expireValue: number,
|
||||
expireStyle: string
|
||||
): Promise<ApiResponse<{ code?: string; name?: string }>> => {
|
||||
const code = await presignUploadFile(file, {
|
||||
expireValue,
|
||||
expireStyle: expireStyle as ExpireStyle,
|
||||
onProgress: (progress) => {
|
||||
options.onProgress(progress.percentage)
|
||||
}
|
||||
})
|
||||
|
||||
if (!code) {
|
||||
throw new Error(options.translate('send.messages.uploadFailed'))
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
detail: {
|
||||
code,
|
||||
name: file.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const submitFile = async ({
|
||||
selectedFile,
|
||||
selectedFiles,
|
||||
expireValue,
|
||||
expireStyle,
|
||||
enableChunk,
|
||||
validateFileSize
|
||||
}: SubmitFileOptions): Promise<ApiResponse | null> => {
|
||||
let fileToUpload = selectedFile
|
||||
|
||||
if (selectedFiles.length > 0) {
|
||||
options.notify('正在打包文件...', 'success')
|
||||
fileToUpload = await packFilesAsZip(selectedFiles)
|
||||
if (!validateFileSize(fileToUpload)) {
|
||||
return null
|
||||
}
|
||||
options.onHashCalculated(await calculateFileHash(fileToUpload))
|
||||
}
|
||||
|
||||
if (!fileToUpload) {
|
||||
throw new Error(options.translate('send.messages.selectFile'))
|
||||
}
|
||||
|
||||
return enableChunk
|
||||
? handleChunkUpload(fileToUpload, expireValue, expireStyle)
|
||||
: handlePresignedUpload(fileToUpload, expireValue, expireStyle)
|
||||
}
|
||||
|
||||
const submitText = ({ text, expireValue, expireStyle }: SubmitTextOptions) =>
|
||||
FileService.uploadText(text, expireValue, expireStyle)
|
||||
|
||||
return {
|
||||
resetPresignUpload,
|
||||
submitFile,
|
||||
submitText
|
||||
}
|
||||
}
|
||||
@@ -1,61 +1,54 @@
|
||||
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'
|
||||
import { useConfigStore } from '@/stores/configStore'
|
||||
import type { ConfigState } from '@/types'
|
||||
import { DEFAULT_CONFIG_STATE, readStoredConfig } from '@/utils/config-storage'
|
||||
import { getErrorMessage } from '@/utils/common'
|
||||
import {
|
||||
buildConfigSubmitPayload,
|
||||
bytesToFileSizeForm,
|
||||
secondsToSaveTimeForm,
|
||||
type FileSizeUnit,
|
||||
type SaveTimeUnit
|
||||
} from '@/utils/config-form'
|
||||
|
||||
type ConfigFlagKey = 'enableChunk' | 's3_proxy' | 'openUpload'
|
||||
|
||||
export function useSystemConfig() {
|
||||
const alertStore = useAlertStore()
|
||||
const configStore = useConfigStore()
|
||||
|
||||
// 状态管理
|
||||
const config = ref<SystemConfig>({ ...DEFAULT_CONFIG })
|
||||
const config = ref<ConfigState>({ ...DEFAULT_CONFIG_STATE })
|
||||
const isLoading = ref(false)
|
||||
const fileSize = ref(1)
|
||||
const sizeUnit = ref<FileSizeUnit>('MB')
|
||||
const saveTime = ref(1)
|
||||
const saveTimeUnit = ref<SaveTimeUnit>('天')
|
||||
|
||||
// 从本地存储获取配置
|
||||
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 getStoredConfig = (): ConfigState | null => {
|
||||
return readStoredConfig<ConfigState>()
|
||||
}
|
||||
|
||||
// 保存配置到本地存储
|
||||
const saveConfigToStorage = (configData: SystemConfig) => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEYS.CONFIG, JSON.stringify(configData))
|
||||
} catch (error) {
|
||||
console.error('保存配置到本地存储失败:', error)
|
||||
}
|
||||
const saveConfigToStorage = (configData: ConfigState) => {
|
||||
configStore.updateConfig(configData)
|
||||
}
|
||||
|
||||
// 获取系统配置
|
||||
const fetchConfig = async (): Promise<SystemConfig | null> => {
|
||||
const fetchConfig = async (): Promise<ConfigState | 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'
|
||||
)
|
||||
}
|
||||
config.value = { ...DEFAULT_CONFIG_STATE, ...response.detail }
|
||||
const notifyMessage = configStore.applyRemoteConfig(config.value)
|
||||
if (notifyMessage) {
|
||||
alertStore.showAlert(notifyMessage, 'success')
|
||||
}
|
||||
|
||||
return config.value
|
||||
@@ -70,8 +63,7 @@ export function useSystemConfig() {
|
||||
return config.value
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : '获取配置失败'
|
||||
alertStore.showAlert(errorMessage, 'error')
|
||||
alertStore.showAlert(getErrorMessage(error, '获取配置失败'), 'error')
|
||||
return null
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
@@ -79,7 +71,7 @@ export function useSystemConfig() {
|
||||
}
|
||||
|
||||
// 更新系统配置
|
||||
const updateConfig = async (newConfig: Partial<SystemConfig>): Promise<boolean> => {
|
||||
const updateConfig = async (newConfig: Partial<ConfigState>): Promise<boolean> => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
@@ -94,13 +86,42 @@ export function useSystemConfig() {
|
||||
throw new Error(response.message || '更新配置失败')
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '更新配置失败'
|
||||
alertStore.showAlert(errorMessage, 'error')
|
||||
alertStore.showAlert(getErrorMessage(error, '更新配置失败'), 'error')
|
||||
return false
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const toggleConfigFlag = (key: ConfigFlagKey) => {
|
||||
config.value[key] = config.value[key] === 1 ? 0 : 1
|
||||
}
|
||||
|
||||
const syncConfigForm = (nextConfig: ConfigState) => {
|
||||
const sizeForm = bytesToFileSizeForm(nextConfig.uploadSize)
|
||||
fileSize.value = sizeForm.value
|
||||
sizeUnit.value = sizeForm.unit
|
||||
|
||||
const saveTimeForm = secondsToSaveTimeForm(nextConfig.max_save_seconds)
|
||||
saveTime.value = saveTimeForm.value
|
||||
saveTimeUnit.value = saveTimeForm.unit
|
||||
}
|
||||
|
||||
const refreshConfig = async () => {
|
||||
const latestConfig = await fetchConfig()
|
||||
if (latestConfig) {
|
||||
syncConfigForm(latestConfig)
|
||||
}
|
||||
}
|
||||
|
||||
const submitConfig = () =>
|
||||
updateConfig(
|
||||
buildConfigSubmitPayload(
|
||||
config.value,
|
||||
{ value: fileSize.value, unit: sizeUnit.value },
|
||||
{ value: saveTime.value, unit: saveTimeUnit.value }
|
||||
)
|
||||
)
|
||||
|
||||
// 初始化配置
|
||||
const initConfig = async () => {
|
||||
@@ -116,17 +137,21 @@ export function useSystemConfig() {
|
||||
|
||||
// 计算属性
|
||||
const maxFileSizeMB = computed(() => {
|
||||
return Math.round(config.value.maxFileSize / 1024 / 1024)
|
||||
return Math.round(config.value.uploadSize / 1024 / 1024)
|
||||
})
|
||||
|
||||
const isConfigLoaded = computed(() => {
|
||||
return config.value.name !== DEFAULT_CONFIG.name || !isLoading.value
|
||||
return config.value.name !== DEFAULT_CONFIG_STATE.name || !isLoading.value
|
||||
})
|
||||
|
||||
return {
|
||||
// 状态
|
||||
config,
|
||||
isLoading,
|
||||
fileSize,
|
||||
sizeUnit,
|
||||
saveTime,
|
||||
saveTimeUnit,
|
||||
|
||||
// 计算属性
|
||||
maxFileSizeMB,
|
||||
@@ -135,8 +160,11 @@ export function useSystemConfig() {
|
||||
// 方法
|
||||
fetchConfig,
|
||||
updateConfig,
|
||||
refreshConfig,
|
||||
submitConfig,
|
||||
toggleConfigFlag,
|
||||
initConfig,
|
||||
getStoredConfig,
|
||||
saveConfigToStorage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { STORAGE_KEYS, THEME_MODES } from '@/constants'
|
||||
import { THEME_MODES } from '@/constants'
|
||||
import type { ThemeMode } from '@/types'
|
||||
import { readStoredThemeMode, writeStoredThemeMode } from '@/utils/preference-storage'
|
||||
|
||||
export function useTheme() {
|
||||
// 状态管理
|
||||
@@ -14,7 +15,7 @@ export function useTheme() {
|
||||
|
||||
// 从本地存储获取用户之前的选择
|
||||
const getUserPreference = (): ThemeMode | null => {
|
||||
const storedPreference = localStorage.getItem(STORAGE_KEYS.COLOR_MODE)
|
||||
const storedPreference = readStoredThemeMode()
|
||||
if (storedPreference && Object.values(THEME_MODES).includes(storedPreference as ThemeMode)) {
|
||||
return storedPreference as ThemeMode
|
||||
}
|
||||
@@ -24,7 +25,7 @@ export function useTheme() {
|
||||
// 设置颜色模式
|
||||
const setThemeMode = (mode: ThemeMode) => {
|
||||
themeMode.value = mode
|
||||
localStorage.setItem(STORAGE_KEYS.COLOR_MODE, mode)
|
||||
writeStoredThemeMode(mode)
|
||||
|
||||
// 根据模式设置实际的暗色模式状态
|
||||
if (mode === THEME_MODES.SYSTEM) {
|
||||
@@ -135,4 +136,4 @@ export function useTheme() {
|
||||
initTheme,
|
||||
checkSystemColorScheme
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user