From 10193ff17a2eca2c0553b8e407a2bbd578fccb50 Mon Sep 17 00:00:00 2001 From: Lan Date: Sat, 15 Mar 2025 22:56:43 +0800 Subject: [PATCH 01/97] fix: https://github.com/vastsa/FileCodeBox/issues/292 --- src/views/RetrievewFileView.vue | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/views/RetrievewFileView.vue b/src/views/RetrievewFileView.vue index 14fd7bc..564c916 100644 --- a/src/views/RetrievewFileView.vue +++ b/src/views/RetrievewFileView.vue @@ -266,6 +266,7 @@ import api from '@/utils/api' import { saveAs } from 'file-saver' import { marked } from 'marked' import { useAlertStore } from '@/stores/alertStore' +import { copyToClipboard } from '@/utils/clipboard' const alertStore = useAlertStore() const baseUrl = window.location.origin @@ -304,12 +305,10 @@ watch(code, (newVal) => { // 在其他代码后添加复制功能 const copyContent = async () => { if (selectedRecord.value && selectedRecord.value.content) { - try { - await navigator.clipboard.writeText(selectedRecord.value.content) - alertStore.showAlert('内容已复制到剪贴板', 'success') - } catch (err) { - alertStore.showAlert('复制失败,请重试', 'error') - } + await copyToClipboard(selectedRecord.value.content, { + successMsg: '内容已复制到剪贴板', + errorMsg: '复制失败,请重试' + }) } } const handleSubmit = async () => { From 77101aa3d1869d60f5dc494a6ad2999bf0e01d7b Mon Sep 17 00:00:00 2001 From: Lan Date: Sat, 15 Mar 2025 23:43:24 +0800 Subject: [PATCH 02/97] fix: https://github.com/vastsa/FileCodeBox/issues/292 --- src/views/SendFileView.vue | 114 ++++++++++++++++++++++++++++--------- 1 file changed, 87 insertions(+), 27 deletions(-) diff --git a/src/views/SendFileView.vue b/src/views/SendFileView.vue index 9ce94db..6286c1b 100644 --- a/src/views/SendFileView.vue +++ b/src/views/SendFileView.vue @@ -1,5 +1,6 @@ @@ -177,7 +177,7 @@ const props = withDefaults(defineProps(), { totalBytes: 0, errorMessage: '', allowRetry: true, - retryText: '重试', + retryText: '', showProgressDetails: true }) @@ -188,6 +188,7 @@ const isDarkMode = useInjectedDarkMode() // 使用computed属性处理多语言文本 const placeholderText = computed(() => props.placeholder || t('send.uploadArea.placeholder')) const descriptionText = computed(() => props.description || t('send.uploadArea.description')) +const retryLabel = computed(() => props.retryText || t('send.uploadArea.retry')) const fileInput = ref(null) const isDragActive = ref(false) @@ -204,7 +205,7 @@ const displayText = computed(() => { return props.selectedFiles[0].name } if (props.selectedFiles && props.selectedFiles.length > 1) { - return `已选择 ${props.selectedFiles.length} 个文件` + return t('send.uploadArea.selectedFiles', { count: props.selectedFiles.length }) } if (props.selectedFile) { return props.selectedFile.name @@ -249,16 +250,16 @@ const statusDescription = computed(() => { return props.errorMessage } if (props.uploadStatus === 'initializing') { - return '正在初始化上传...' + return t('send.uploadArea.status.initializing') } if (props.uploadStatus === 'uploading') { - return '正在上传文件...' + return t('send.uploadArea.status.uploading') } if (props.uploadStatus === 'confirming') { - return '正在确认上传...' + return t('send.uploadArea.status.confirming') } if (isSuccess.value) { - return '上传成功!' + return t('send.uploadArea.status.success') } return descriptionText.value }) diff --git a/src/composables/useSendFlow.ts b/src/composables/useSendFlow.ts index a798816..3f6a6b8 100644 --- a/src/composables/useSendFlow.ts +++ b/src/composables/useSendFlow.ts @@ -32,8 +32,10 @@ export function useSendFlow() { const isSubmitting = ref(false) const fileHash = ref('') const sendRecords = computed(() => fileDataStore.shareData) - const uploadDescription = computed( - () => `支持各种常见格式,最大${getStorageUnit(config.value.uploadSize)}` + const uploadDescription = computed(() => + t('send.uploadArea.descriptionWithLimit', { + size: getStorageUnit(config.value.uploadSize) + }) ) const expirationOptions = computed(() => config.value.expireStyle.map((value) => ({ diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 8f91b08..39150eb 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -215,7 +215,16 @@ export default { clickText: 'click to select files', textInput: 'Enter text to send here...', placeholder: 'Click or drag files here to upload', - description: 'Supports various common formats' + description: 'Supports various common formats', + descriptionWithLimit: 'Supports common formats, up to {size}', + retry: 'Retry', + selectedFiles: '{count} files selected', + status: { + initializing: 'Preparing upload...', + uploading: 'Uploading files...', + confirming: 'Confirming upload...', + success: 'Upload complete!' + } }, submit: 'Secure Send', submitting: 'Sending...', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 8ff63c0..680b1ca 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -217,7 +217,16 @@ export default { clickText: '点击选择文件', textInput: '在此输入要发送的文本...', placeholder: '点击或拖放文件到此处上传', - description: '支持各种常见格式' + description: '支持各种常见格式', + descriptionWithLimit: '支持各种常见格式,最大{size}', + retry: '重试', + selectedFiles: '已选择 {count} 个文件', + status: { + initializing: '正在初始化上传...', + uploading: '正在上传文件...', + confirming: '正在确认上传...', + success: '上传成功!' + } }, submit: '安全寄送', submitting: '发送中...', From 32cbc10cd8d7eab845422c2ad4f3f59c21636445 Mon Sep 17 00:00:00 2001 From: Lan Date: Wed, 3 Jun 2026 02:39:30 +0800 Subject: [PATCH 51/97] chore: ignore local verification output --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8ee54e8..2d29e0a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ node_modules dist dist-ssr coverage +output *.local /cypress/videos/ From c6f33096be936cb092408dd95e1583d2736b693b Mon Sep 17 00:00:00 2001 From: Lan Date: Wed, 3 Jun 2026 02:52:38 +0800 Subject: [PATCH 52/97] feat: connect admin session and public config endpoints --- src/composables/index.ts | 1 + src/composables/useAdminLogin.ts | 8 +++++- src/composables/useAdminSession.ts | 35 ++++++++++++++++++++++++++ src/layout/AdminLayout/AdminLayout.vue | 15 +++++++---- src/services/config.ts | 28 ++++++++++++++++++++- src/types/api.ts | 1 + src/types/auth.ts | 2 ++ 7 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 src/composables/useAdminSession.ts diff --git a/src/composables/index.ts b/src/composables/index.ts index 5086761..c894854 100644 --- a/src/composables/index.ts +++ b/src/composables/index.ts @@ -1,5 +1,6 @@ export { useAdminFiles } from './useAdminFiles' export { useAdminLogin } from './useAdminLogin' +export { useAdminSession } from './useAdminSession' export { useAppShell } from './useAppShell' export { useDashboardStats } from './useDashboardStats' export { useInjectedDarkMode } from './useInjectedDarkMode' diff --git a/src/composables/useAdminLogin.ts b/src/composables/useAdminLogin.ts index 7c81627..ce6a219 100644 --- a/src/composables/useAdminLogin.ts +++ b/src/composables/useAdminLogin.ts @@ -35,7 +35,13 @@ export function useAdminLogin() { return false } - adminStore.setToken(response.detail.token) + adminStore.login({ + id: response.detail.id || 'admin', + username: response.detail.username || 'admin', + token: response.detail.token, + token_type: response.detail.token_type, + expires_at: response.detail.expires_at + }) return true } catch (error: unknown) { alertStore.showAlert(getErrorMessage(error, '登录失败'), 'error') diff --git a/src/composables/useAdminSession.ts b/src/composables/useAdminSession.ts new file mode 100644 index 0000000..9333146 --- /dev/null +++ b/src/composables/useAdminSession.ts @@ -0,0 +1,35 @@ +import { AuthService } from '@/services' +import { useAdminStore } from '@/stores/adminStore' + +export function useAdminSession() { + const adminStore = useAdminStore() + + const verifySession = async () => { + if (!adminStore.hasToken) return false + + try { + const response = await AuthService.verifyToken() + if (response.code === 200 && response.detail?.token) { + adminStore.login(response.detail) + return true + } + } catch { + adminStore.logout() + } + + return false + } + + const logout = async () => { + try { + await AuthService.logout() + } finally { + adminStore.logout() + } + } + + return { + verifySession, + logout + } +} diff --git a/src/layout/AdminLayout/AdminLayout.vue b/src/layout/AdminLayout/AdminLayout.vue index 6eb588f..24c3a7d 100644 --- a/src/layout/AdminLayout/AdminLayout.vue +++ b/src/layout/AdminLayout/AdminLayout.vue @@ -120,7 +120,7 @@ import { import { RouterLink, useRoute, useRouter } from 'vue-router' import { useI18n } from 'vue-i18n' import { ROUTE_NAMES, ROUTES } from '@/constants' -import { useAdminStore } from '@/stores/adminStore' +import { useAdminSession } from '@/composables' interface MenuItem { id: string @@ -133,7 +133,7 @@ const router = useRouter() const route = useRoute() const { t } = useI18n() const isDarkMode = inject('isDarkMode') -const adminStore = useAdminStore() +const { verifySession, logout } = useAdminSession() const menuItems: MenuItem[] = [ { id: ROUTE_NAMES.DASHBOARD, @@ -172,6 +172,11 @@ const handleResize = () => { onMounted(() => { handleResize() window.addEventListener('resize', handleResize) + void verifySession().then((isValid) => { + if (!isValid) { + void router.push(ROUTES.LOGIN) + } + }) }) onUnmounted(() => { @@ -179,9 +184,9 @@ onUnmounted(() => { }) // 登出处理 -const handleLogout = () => { - adminStore.logout() - router.push(ROUTES.LOGIN) +const handleLogout = async () => { + await logout() + await router.push(ROUTES.LOGIN) } diff --git a/src/services/config.ts b/src/services/config.ts index 9d60b75..0f183bf 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -1,13 +1,39 @@ import api from './client' import type { ApiResponse, ConfigState } from '@/types' +type PublicConfigEnvelope = { + config?: Partial + meta?: unknown +} + +const isPublicConfigEnvelope = ( + detail: ConfigState | PublicConfigEnvelope | null | undefined +): detail is PublicConfigEnvelope => { + return !!detail && typeof detail === 'object' && 'config' in detail +} + export class ConfigService { static async getConfig(): Promise> { return api.get('/admin/config/get') } static async getUserConfig(): Promise> { - return api.post('/') + try { + const response = (await api.get('/api/v1/config')) as ApiResponse< + ConfigState | PublicConfigEnvelope + > + + if (isPublicConfigEnvelope(response.detail) && response.detail.config) { + return { + ...response, + detail: response.detail.config as ConfigState + } + } + + return response as ApiResponse + } catch { + return api.post('/') + } } static async updateConfig(config: Partial): Promise { diff --git a/src/types/api.ts b/src/types/api.ts index 3ae1779..ebb6f43 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -1,6 +1,7 @@ export interface ApiResponse { code: number message?: string + msg?: string detail?: T } diff --git a/src/types/auth.ts b/src/types/auth.ts index 8558757..800740b 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -2,4 +2,6 @@ export interface AdminUser { id: string username: string token: string + token_type?: string + expires_at?: number } From 46496812451413e11cd286046d17115ebe1a5a33 Mon Sep 17 00:00:00 2001 From: Lan Date: Wed, 3 Jun 2026 03:01:40 +0800 Subject: [PATCH 53/97] fix: honor presign proxy upload URLs --- src/composables/usePresignedUpload.ts | 5 ++++- src/services/presign-upload.ts | 9 +++++++-- src/types/presign-upload.ts | 2 ++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/composables/usePresignedUpload.ts b/src/composables/usePresignedUpload.ts index 2ff2035..97630af 100644 --- a/src/composables/usePresignedUpload.ts +++ b/src/composables/usePresignedUpload.ts @@ -213,10 +213,13 @@ export function usePresignedUpload(options: UsePresignedUploadOptions = {}) { presignStatus.value = PRESIGN_UPLOAD_STATUS.UPLOADING const progressHandler = createProgressHandler(options?.onProgress) + const uploadUrl = + session.proxy_upload_url || session.upload_url || session.legacy_proxy_upload_url const response = await PresignUploadService.proxyUpload( session.upload_id, file, - progressHandler + progressHandler, + uploadUrl ) if (response.code === 200 && response.detail?.code) { diff --git a/src/services/presign-upload.ts b/src/services/presign-upload.ts index 3d1def8..47c6684 100644 --- a/src/services/presign-upload.ts +++ b/src/services/presign-upload.ts @@ -19,12 +19,17 @@ export class PresignUploadService { static async proxyUpload( uploadId: string, file: File, - onProgress?: (progress: UploadProgress) => void + onProgress?: (progress: UploadProgress) => void, + uploadUrl?: string ): Promise> { const formData = new FormData() formData.append('file', file) - return api.put(`/presign/upload/proxy/${uploadId}`, formData, multipartUploadConfig(onProgress)) + return api.put( + uploadUrl || `/presign/upload/proxy/${uploadId}`, + formData, + multipartUploadConfig(onProgress) + ) } static async confirmUpload( diff --git a/src/types/presign-upload.ts b/src/types/presign-upload.ts index 8f38144..abb09ea 100644 --- a/src/types/presign-upload.ts +++ b/src/types/presign-upload.ts @@ -22,6 +22,8 @@ export interface PresignInitRequest { export interface PresignInitResponse { upload_id: string upload_url: string + proxy_upload_url?: string + legacy_proxy_upload_url?: string mode: PresignUploadMode expires_in: number } From 7a9ad94a1a6b861dfa3341fd20a8308d01d48d77 Mon Sep 17 00:00:00 2001 From: Lan Date: Wed, 3 Jun 2026 03:13:04 +0800 Subject: [PATCH 54/97] feat: improve system settings save workflow --- src/composables/useSystemConfig.ts | 116 +++++-- src/i18n/locales/en-US.ts | 8 +- src/i18n/locales/zh-CN.ts | 8 +- src/views/manage/SystemSettingsView.vue | 422 ++++++++++++++++++------ 4 files changed, 413 insertions(+), 141 deletions(-) diff --git a/src/composables/useSystemConfig.ts b/src/composables/useSystemConfig.ts index 5b8ec87..1d27030 100644 --- a/src/composables/useSystemConfig.ts +++ b/src/composables/useSystemConfig.ts @@ -18,39 +18,78 @@ type ConfigFlagKey = 'enableChunk' | 's3_proxy' | 'openUpload' export function useSystemConfig() { const alertStore = useAlertStore() const configStore = useConfigStore() - + // 状态管理 const config = ref({ ...DEFAULT_CONFIG_STATE }) - const isLoading = ref(false) + const isRefreshing = ref(false) + const isSaving = ref(false) + const savedPayloadSnapshot = ref('') const fileSize = ref(1) const sizeUnit = ref('MB') const saveTime = ref(1) const saveTimeUnit = ref('天') - + + const isLoading = computed(() => isRefreshing.value || isSaving.value) + + const buildSubmitPayload = (): Partial => { + const payload: Partial = buildConfigSubmitPayload( + config.value, + { value: fileSize.value, unit: sizeUnit.value }, + { value: saveTime.value, unit: saveTimeUnit.value } + ) + + if (!payload.admin_token) { + delete payload.admin_token + } + + return payload + } + + const snapshotPayload = (payload: Partial) => JSON.stringify(payload) + + const normalizeEditableConfig = (nextConfig: Partial): ConfigState => ({ + ...DEFAULT_CONFIG_STATE, + ...nextConfig, + admin_token: '' + }) + + const markConfigSaved = () => { + savedPayloadSnapshot.value = snapshotPayload(buildSubmitPayload()) + } + + const isDirty = computed(() => { + if (!savedPayloadSnapshot.value) { + return false + } + + return snapshotPayload(buildSubmitPayload()) !== savedPayloadSnapshot.value + }) + // 从本地存储获取配置 const getStoredConfig = (): ConfigState | null => { - return readStoredConfig() + const storedConfig = readStoredConfig>() + return storedConfig ? normalizeEditableConfig(storedConfig) : null } - + // 保存配置到本地存储 const saveConfigToStorage = (configData: ConfigState) => { configStore.updateConfig(configData) } - + // 获取系统配置 const fetchConfig = async (): Promise => { try { - isLoading.value = true - + isRefreshing.value = true + const response = await ConfigService.getConfig() - + if (response.code === 200 && response.detail) { - config.value = { ...DEFAULT_CONFIG_STATE, ...response.detail } + config.value = normalizeEditableConfig(response.detail) const notifyMessage = configStore.applyRemoteConfig(config.value) if (notifyMessage) { alertStore.showAlert(notifyMessage, 'success') } - + return config.value } else { throw new Error(response.message || '获取配置失败') @@ -62,23 +101,25 @@ export function useSystemConfig() { config.value = storedConfig return config.value } - + alertStore.showAlert(getErrorMessage(error, '获取配置失败'), 'error') return null } finally { - isLoading.value = false + isRefreshing.value = false } } - + // 更新系统配置 const updateConfig = async (newConfig: Partial): Promise => { try { - isLoading.value = true - + isSaving.value = true + const response = await ConfigService.updateConfig(newConfig) - + if (response.code === 200) { - config.value = { ...config.value, ...newConfig } + config.value = normalizeEditableConfig({ ...config.value, ...newConfig }) + syncConfigForm(config.value) + markConfigSaved() saveConfigToStorage(config.value) alertStore.showAlert('配置更新成功!', 'success') return true @@ -89,7 +130,7 @@ export function useSystemConfig() { alertStore.showAlert(getErrorMessage(error, '更新配置失败'), 'error') return false } finally { - isLoading.value = false + isSaving.value = false } } @@ -111,52 +152,57 @@ export function useSystemConfig() { const latestConfig = await fetchConfig() if (latestConfig) { syncConfigForm(latestConfig) + markConfigSaved() } } - const submitConfig = () => - updateConfig( - buildConfigSubmitPayload( - config.value, - { value: fileSize.value, unit: sizeUnit.value }, - { value: saveTime.value, unit: saveTimeUnit.value } - ) - ) - + const submitConfig = () => { + if (!isDirty.value || isSaving.value) { + return Promise.resolve(false) + } + + return updateConfig(buildSubmitPayload()) + } + // 初始化配置 const initConfig = async () => { // 先尝试从本地存储加载 const storedConfig = getStoredConfig() if (storedConfig) { config.value = storedConfig + syncConfigForm(storedConfig) + markConfigSaved() } - + // 然后从服务器获取最新配置 - await fetchConfig() + await refreshConfig() } - + // 计算属性 const maxFileSizeMB = computed(() => { return Math.round(config.value.uploadSize / 1024 / 1024) }) - + const isConfigLoaded = computed(() => { return config.value.name !== DEFAULT_CONFIG_STATE.name || !isLoading.value }) - + return { // 状态 config, isLoading, + isRefreshing, + isSaving, + isDirty, fileSize, sizeUnit, saveTime, saveTimeUnit, - + // 计算属性 maxFileSizeMB, isConfigLoaded, - + // 方法 fetchConfig, updateConfig, diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 39150eb..7a1d55d 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -446,7 +446,13 @@ export default { mb: 'MB', gb: 'GB' }, - saveChanges: 'Save Settings' + saveChanges: 'Save Settings', + refreshConfig: 'Refresh Config', + refreshing: 'Refreshing', + saving: 'Saving', + unsavedChanges: 'Unsaved configuration changes', + allChangesSaved: 'All configuration changes are saved', + refreshBlocked: 'Save current changes before refreshing' }, systemSettings: { title: 'System Settings', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 680b1ca..0e5d958 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -477,7 +477,13 @@ export default { errorLimits: '访问保护', errorPerMinute: '检测时间窗口(在此时间内统计错误次数)', errorCountLimit: '允许错误次数(超过后临时封禁)', - saveChanges: '保存设置' + saveChanges: '保存设置', + refreshConfig: '刷新配置', + refreshing: '刷新中', + saving: '保存中', + unsavedChanges: '有未保存的配置变更', + allChangesSaved: '所有配置已保存', + refreshBlocked: '请先保存当前变更再刷新' }, dashboard: { title: '仪表盘', diff --git a/src/views/manage/SystemSettingsView.vue b/src/views/manage/SystemSettingsView.vue index e2b7153..317ed4c 100644 --- a/src/views/manage/SystemSettingsView.vue +++ b/src/views/manage/SystemSettingsView.vue @@ -1,6 +1,8 @@ From 060e77d343ca0daca6718c0be5c0f956a18cd857 Mon Sep 17 00:00:00 2001 From: Lan Date: Wed, 3 Jun 2026 03:39:13 +0800 Subject: [PATCH 56/97] feat: improve dashboard refresh state --- src/composables/useDashboardStats.ts | 130 +++++++++++++++++---------- src/i18n/locales/en-US.ts | 3 + src/i18n/locales/zh-CN.ts | 3 + src/types/dashboard.ts | 48 ++++++---- src/views/manage/DashboardView.vue | 107 +++++++++++++++++----- 5 files changed, 204 insertions(+), 87 deletions(-) diff --git a/src/composables/useDashboardStats.ts b/src/composables/useDashboardStats.ts index 452fc92..b28fa75 100644 --- a/src/composables/useDashboardStats.ts +++ b/src/composables/useDashboardStats.ts @@ -1,7 +1,11 @@ -import { reactive } from 'vue' +import { computed, ref, reactive } from 'vue' import { StatsService } from '@/services' import type { DashboardViewData } from '@/types' -import { formatFileSize } from '@/utils/common' +import { formatFileSize, getErrorMessage } from '@/utils/common' + +type UseDashboardStatsOptions = { + loadFailedMessage?: string +} const emptyDashboardData = (): DashboardViewData => ({ hasExtendedStats: false, @@ -50,59 +54,91 @@ const formatDuration = (startTimestamp: number | null) => { return `${days}天${hours}小时` } -export function useDashboardStats() { +const normalizeRecentFiles = (recentFiles: DashboardViewData['recentFiles']) => + recentFiles.map((file) => ({ + ...file, + size: toNumber(file.size), + expiredCount: toNumber(file.expiredCount), + usedCount: toNumber(file.usedCount) + })) + +export function useDashboardStats(options: UseDashboardStatsOptions = {}) { const dashboardData = reactive(emptyDashboardData()) + const isLoading = ref(false) + const errorMessage = ref('') + const lastUpdatedAt = ref(null) + const lastUpdatedText = computed(() => + lastUpdatedAt.value ? lastUpdatedAt.value.toLocaleString() : '-' + ) const fetchDashboardData = async () => { - const response = await StatsService.getDashboard() - if (!response.detail) return + isLoading.value = true + errorMessage.value = '' + + try { + const response = await StatsService.getDashboard() + if (!response.detail) { + throw new Error('No dashboard data') + } - 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 || [] + 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 = normalizeRecentFiles(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 + 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 + lastUpdatedAt.value = new Date() + } catch (error) { + errorMessage.value = getErrorMessage( + error, + options.loadFailedMessage || 'Failed to load dashboard data' + ) + } finally { + isLoading.value = false + } } return { dashboardData, - fetchDashboardData + errorMessage, + fetchDashboardData, + isLoading, + lastUpdatedText } } diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 91d328f..3edb985 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -65,6 +65,9 @@ export default { yesterdayShares: 'Yesterday: {count}', serverUptime: 'Uptime', refresh: 'Refresh', + refreshing: 'Refreshing', + lastUpdated: 'Last updated: {time}', + loadFailed: 'Failed to load dashboard data', fileHealth: 'File Health', fileHealthDesc: 'Real file records grouped by availability, expiry, and type.', activeFileRatio: 'Active Ratio', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 51d6610..2e59ff2 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -65,6 +65,9 @@ export default { yesterdayShares: '昨日分享:{count}', serverUptime: '运行时间', refresh: '刷新数据', + refreshing: '刷新中', + lastUpdated: '最近更新:{time}', + loadFailed: '仪表盘数据加载失败', fileHealth: '文件健康', fileHealthDesc: '基于真实文件记录统计有效、过期和类型分布。', activeFileRatio: '有效占比', diff --git a/src/types/dashboard.ts b/src/types/dashboard.ts index 5d402d3..a200857 100644 --- a/src/types/dashboard.ts +++ b/src/types/dashboard.ts @@ -1,24 +1,24 @@ export interface DashboardData { totalFiles: number - storageUsed: number + storageUsed: number | string yesterdayCount: number todayCount: number - yesterdaySize: number - todaySize: number + yesterdaySize: number | string + todaySize: number | string sysUptime: number | null - activeCount: number - expiredCount: number - textCount: number - fileCount: number - chunkedCount: number - usedCount: number - storageBackend: string - uploadSizeLimit: number - openUpload: number - enableChunk: number - maxSaveSeconds: number - topSuffixes: DashboardSuffixStat[] - recentFiles: DashboardRecentFile[] + activeCount?: number + expiredCount?: number + textCount?: number + fileCount?: number + chunkedCount?: number + usedCount?: number + storageBackend?: string + uploadSizeLimit?: number + openUpload?: number + enableChunk?: number + maxSaveSeconds?: number + topSuffixes?: DashboardSuffixStat[] + recentFiles?: DashboardRecentFile[] } export interface DashboardSuffixStat { @@ -42,6 +42,22 @@ export interface DashboardRecentFile { export interface DashboardViewData extends DashboardData { hasExtendedStats: boolean + activeCount: number + expiredCount: number + textCount: number + fileCount: number + chunkedCount: number + usedCount: number + storageBackend: string + uploadSizeLimit: number + openUpload: number + enableChunk: number + maxSaveSeconds: number + topSuffixes: DashboardSuffixStat[] + recentFiles: DashboardRecentFile[] + storageUsed: number + yesterdaySize: number + todaySize: number storageUsedText: string yesterdaySizeText: string todaySizeText: string diff --git a/src/views/manage/DashboardView.vue b/src/views/manage/DashboardView.vue index e5cf014..b00bfde 100644 --- a/src/views/manage/DashboardView.vue +++ b/src/views/manage/DashboardView.vue @@ -6,22 +6,38 @@

{{ t('admin.dashboard.title') }}

+

+ {{ t('admin.dashboard.lastUpdated', { time: lastUpdatedText }) }} +

+
+ {{ errorMessage }} +
+
- +
@@ -157,10 +176,7 @@ :label="t('admin.dashboard.guestUpload')" :value="dashboardData.openUpload ? t('common.enabled') : t('common.disabled')" /> - +
@@ -168,7 +184,10 @@ {{ t('admin.dashboard.todayCapacityReference') }} {{ dashboardData.todaySizeRatio }}%
-
+
-
+
{{ t('common.noData') }}
- {{ item.suffix || t('admin.dashboard.textType') }} + {{ + item.suffix || t('admin.dashboard.textType') + }} {{ item.count }}
-
+
-
- +
+
- - - - @@ -241,7 +287,10 @@
+ {{ t('admin.dashboard.table.file') }} + {{ t('admin.dashboard.table.size') }} + {{ t('admin.dashboard.table.usage') }} + {{ t('admin.dashboard.table.status') }}
-
+
@@ -306,7 +355,10 @@ import { formatFileSize, formatTimestamp } from '@/utils/common' const isDarkMode = useInjectedDarkMode() const { t } = useI18n() -const { dashboardData, fetchDashboardData } = useDashboardStats() +const { dashboardData, errorMessage, fetchDashboardData, isLoading, lastUpdatedText } = + useDashboardStats({ + loadFailedMessage: t('admin.dashboard.loadFailed') + }) const primaryTextClass = computed(() => (isDarkMode.value ? 'text-white' : 'text-gray-900')) const mutedTextClass = computed(() => (isDarkMode.value ? 'text-gray-400' : 'text-gray-500')) @@ -381,10 +433,17 @@ const PolicyRow = defineComponent({ }, setup(props) { return () => - h('div', { class: 'flex items-center justify-between gap-4 border-b border-gray-200/60 pb-3 last:border-b-0 dark:border-gray-700' }, [ - h('span', { class: 'text-sm text-gray-500 dark:text-gray-400' }, props.label), - h('span', { class: 'text-sm font-medium text-gray-900 dark:text-white' }, props.value) - ]) + h( + 'div', + { + class: + 'flex items-center justify-between gap-4 border-b border-gray-200/60 pb-3 last:border-b-0 dark:border-gray-700' + }, + [ + h('span', { class: 'text-sm text-gray-500 dark:text-gray-400' }, props.label), + h('span', { class: 'text-sm font-medium text-gray-900 dark:text-white' }, props.value) + ] + ) } }) From e243f1597b9983b81565087e0a1ec25850cafe44 Mon Sep 17 00:00:00 2001 From: Lan Date: Wed, 3 Jun 2026 04:01:28 +0800 Subject: [PATCH 57/97] feat: upgrade admin file workspace --- src/composables/useAdminFiles.ts | 195 +++++++++- src/i18n/locales/en-US.ts | 35 +- src/i18n/locales/zh-CN.ts | 35 +- src/services/file.ts | 9 +- src/types/file.ts | 50 ++- src/views/manage/FileManageView.vue | 584 ++++++++++++++++++++-------- 6 files changed, 730 insertions(+), 178 deletions(-) diff --git a/src/composables/useAdminFiles.ts b/src/composables/useAdminFiles.ts index 936441a..d399c68 100644 --- a/src/composables/useAdminFiles.ts +++ b/src/composables/useAdminFiles.ts @@ -2,11 +2,36 @@ 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 type { + AdminFileListParams, + AdminFileStatusFilter, + AdminFileSummary, + AdminFileTypeFilter, + AdminFileViewItem, + FileEditForm, + FileListItem +} from '@/types' import { copyToClipboard } from '@/utils/clipboard' import { formatFileSize, formatTimestamp, getErrorMessage } from '@/utils/common' -const TEXT_PREVIEW_THRESHOLD = 30 +const emptySummary = (): AdminFileSummary => ({ + totalFiles: 0, + activeCount: 0, + expiredCount: 0, + textCount: 0, + fileCount: 0, + chunkedCount: 0, + storageUsed: 0, + usedCount: 0 +}) + +const normalizeCount = (value: number | string | null | undefined) => Number(value || 0) + +const isExpiredByDate = (value: string | null | undefined) => { + if (!value) return false + const timestamp = new Date(value).getTime() + return Number.isFinite(timestamp) && timestamp < Date.now() +} export function useAdminFiles() { const { t } = useI18n() @@ -14,11 +39,18 @@ export function useAdminFiles() { const tableData = ref([]) const hasLoadError = ref(false) - const params = ref({ + const isLoading = ref(false) + const isSaving = ref(false) + const summary = ref(emptySummary()) + const params = ref({ page: 1, size: 10, total: 0, - keyword: '' + keyword: '', + status: 'all', + type: 'all', + sortBy: 'created_at', + sortOrder: 'desc' }) const showEditModal = ref(false) @@ -33,17 +65,98 @@ export function useAdminFiles() { 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 totalPages = computed(() => Math.max(Math.ceil(params.value.total / params.value.size), 1)) + const storageUsedText = computed(() => formatFileSize(summary.value.storageUsed)) + + const requestParams = computed(() => ({ + page: params.value.page, + size: params.value.size, + keyword: params.value.keyword, + status: params.value.status === 'all' ? undefined : params.value.status, + type: params.value.type === 'all' ? undefined : params.value.type, + sortBy: params.value.sortBy, + sortOrder: params.value.sortOrder + })) + + const hasActiveFilters = computed( + () => + Boolean(params.value.keyword?.trim()) || + params.value.status !== 'all' || + params.value.type !== 'all' + ) + + const inferIsText = (file: FileListItem) => { + if (typeof file.isText === 'boolean') return file.isText + if (typeof file.is_text === 'boolean') return file.is_text + if (file.type) return file.type === 'text' + return Boolean(file.text) + } + + const inferIsExpired = (file: FileListItem) => { + if (typeof file.isExpired === 'boolean') return file.isExpired + if (typeof file.is_expired === 'boolean') return file.is_expired + if ( + file.expired_count !== null && + file.expired_count !== undefined && + file.expired_count <= 0 + ) { + return true + } + return isExpiredByDate(file.expired_at) + } + + const inferIsChunked = (file: FileListItem) => Boolean(file.isChunked ?? file.is_chunked) + + const getRemainingDownloads = (file: FileListItem) => { + if (file.remainingDownloads !== undefined) return file.remainingDownloads + if (file.remaining_downloads !== undefined) return file.remaining_downloads + if ( + file.expired_count !== null && + file.expired_count !== undefined && + file.expired_count >= 0 + ) { + return Math.max(file.expired_count, 0) + } + return null + } + + const buildFallbackSummary = (files: AdminFileViewItem[], total: number): AdminFileSummary => ({ + totalFiles: total, + activeCount: files.filter((file) => !file.isExpiredFile).length, + expiredCount: files.filter((file) => file.isExpiredFile).length, + textCount: files.filter((file) => file.isTextFile).length, + fileCount: files.filter((file) => !file.isTextFile).length, + chunkedCount: files.filter((file) => file.isChunkedFile).length, + storageUsed: files.reduce((totalSize, file) => totalSize + normalizeCount(file.size), 0), + usedCount: files.reduce( + (totalUsed, file) => totalUsed + normalizeCount(file.usedCount ?? file.used_count), + 0 + ) }) + const createFileViewItem = (file: FileListItem): AdminFileViewItem => { + const isTextFile = inferIsText(file) + const isExpiredFile = inferIsExpired(file) + const isChunkedFile = inferIsChunked(file) + const remainingDownloadsValue = getRemainingDownloads(file) + const usedCount = normalizeCount(file.usedCount ?? file.used_count) + + return { + ...file, + displayName: file.name || `${file.prefix}${file.suffix}` || file.code, + displaySize: formatFileSize(file.size), + displayExpiredAt: file.expired_at + ? formatTimestamp(file.expired_at) + : t('send.expiration.units.forever'), + displayUsage: `${usedCount} ${t('common.times')}`, + isTextFile, + isExpiredFile, + isChunkedFile, + remainingDownloadsValue, + canPreviewText: Boolean(file.text) + } + } + const resetEditForm = () => { editForm.value = { id: null, @@ -56,16 +169,23 @@ export function useAdminFiles() { } const loadFiles = async () => { + isLoading.value = true try { hasLoadError.value = false - const res = await FileService.getAdminFileList(params.value) + const res = await FileService.getAdminFileList(requestParams.value) if (!res.detail) return tableData.value = res.detail.data.map(createFileViewItem) params.value.total = res.detail.total + summary.value = res.detail.summary || buildFallbackSummary(tableData.value, res.detail.total) } catch (error) { hasLoadError.value = true - alertStore.showAlert(getErrorMessage(error, t('manage.fileManage.loadFileListFailed')), 'error') + alertStore.showAlert( + getErrorMessage(error, t('manage.fileManage.loadFileListFailed')), + 'error' + ) + } finally { + isLoading.value = false } } @@ -74,6 +194,32 @@ export function useAdminFiles() { await loadFiles() } + const refreshFiles = async () => { + await loadFiles() + } + + const resetFilters = async () => { + params.value.page = 1 + params.value.keyword = '' + params.value.status = 'all' + params.value.type = 'all' + params.value.sortBy = 'created_at' + params.value.sortOrder = 'desc' + await loadFiles() + } + + const setStatusFilter = async (status: AdminFileStatusFilter) => { + params.value.status = status + params.value.page = 1 + await loadFiles() + } + + const setTypeFilter = async (type: AdminFileTypeFilter) => { + params.value.type = type + params.value.page = 1 + await loadFiles() + } + const handlePageChange = async (page: number | string) => { if (typeof page === 'string') return if (page < 1 || page > totalPages.value) return @@ -100,12 +246,17 @@ export function useAdminFiles() { } const handleUpdate = async () => { + if (isSaving.value) return + + isSaving.value = true try { await FileService.updateFile(editForm.value) await loadFiles() closeEditModal() } catch (error: unknown) { alertStore.showAlert(getErrorMessage(error, t('manage.fileManage.updateFailed')), 'error') + } finally { + isSaving.value = false } } @@ -116,6 +267,9 @@ export function useAdminFiles() { try { await FileService.deleteAdminFile(id) + if (tableData.value.length === 1 && params.value.page > 1) { + params.value.page -= 1 + } await loadFiles() } catch (error: unknown) { alertStore.showAlert(getErrorMessage(error, t('manage.fileManage.deleteFailed')), 'error') @@ -143,7 +297,12 @@ export function useAdminFiles() { return { tableData, hasLoadError, + hasActiveFilters, + isLoading, + isSaving, params, + storageUsedText, + summary, showEditModal, editForm, showTextPreview, @@ -158,6 +317,10 @@ export function useAdminFiles() { handleUpdate, loadFiles, openEditModal, - openTextPreview + openTextPreview, + refreshFiles, + resetFilters, + setStatusFilter, + setTypeFilter } } diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 3edb985..9dfe151 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -378,10 +378,30 @@ export default { // File Management fileManage: { title: 'File Management', - searchPlaceholder: 'Search file name, description...', + subtitle: '{count} share records. Filter by status, type, and usage.', + searchPlaceholder: 'Search code, name, or text...', allFiles: 'All Files', editFileInfo: 'Edit File Information', saveChanges: 'Save Changes', + refresh: 'Refresh List', + resetFilters: 'Reset Filters', + totalFiles: 'All Records', + activeFiles: 'Available', + expiredFiles: 'Expired', + storageUsed: 'Storage Used', + statusLabel: 'Status', + typeLabel: 'Type', + all: 'All', + active: 'Available', + expired: 'Expired', + fileType: 'File', + textType: 'Text', + chunkedType: 'Chunked', + sortBy: 'Sort', + unlimited: 'Unlimited', + remaining: '{count} left', + loadError: 'Failed to load file list', + noMatches: 'No matching files', viewText: 'View', textPreview: 'Text Preview', copyText: 'Copy Text', @@ -391,11 +411,24 @@ export default { headers: { code: 'Retrieve Code', name: 'Name', + type: 'Type', size: 'Size', + usage: 'Retrievals', + status: 'Status', description: 'Description', expiration: 'Expiration', actions: 'Actions' }, + sort: { + createdAt: 'Created Time', + expiredAt: 'Expiration', + name: 'Name', + size: 'Size', + usedCount: 'Retrievals', + code: 'Retrieve Code', + desc: 'Descending', + asc: 'Ascending' + }, form: { code: 'Retrieve Code', codePlaceholder: 'Enter retrieve code', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 2e59ff2..cda7910 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -376,10 +376,30 @@ export default { // 文件管理 fileManage: { title: '文件管理', - searchPlaceholder: '搜索文件名称、描述...', + subtitle: '共 {count} 条分享记录,可按状态、类型和使用情况快速筛选。', + searchPlaceholder: '搜索取件码、名称或文本...', allFiles: '所有文件', editFileInfo: '编辑文件信息', saveChanges: '保存更改', + refresh: '刷新列表', + resetFilters: '重置筛选', + totalFiles: '全部记录', + activeFiles: '可取件', + expiredFiles: '已过期', + storageUsed: '占用空间', + statusLabel: '状态', + typeLabel: '类型', + all: '全部', + active: '可取件', + expired: '已过期', + fileType: '文件', + textType: '文本', + chunkedType: '分片', + sortBy: '排序', + unlimited: '不限次数', + remaining: '剩余 {count} 次', + loadError: '文件列表加载失败', + noMatches: '没有匹配的文件', viewText: '查看', textPreview: '文本预览', copyText: '复制文本', @@ -389,11 +409,24 @@ export default { headers: { code: '取件码', name: '名称', + type: '类型', size: '大小', + usage: '取件', + status: '状态', description: '描述', expiration: '过期时间', actions: '操作' }, + sort: { + createdAt: '创建时间', + expiredAt: '过期时间', + name: '名称', + size: '大小', + usedCount: '取件次数', + code: '取件码', + desc: '降序', + asc: '升序' + }, form: { code: '取件码', codePlaceholder: '输入取件码', diff --git a/src/services/file.ts b/src/services/file.ts index f88ae9e..9b469b4 100644 --- a/src/services/file.ts +++ b/src/services/file.ts @@ -7,6 +7,7 @@ import type { ChunkUploadInitResponse, ChunkUploadResponse, FileEditForm, + AdminFileListParams, FileInfo, FileListResponse, FileUploadResponse, @@ -116,11 +117,9 @@ export class FileService { return response.data } - static async getAdminFileList(params: { - page: number - size: number - keyword?: string - }): Promise> { + static async getAdminFileList( + params: AdminFileListParams + ): Promise> { return api.get('/admin/file/list', { params }) } diff --git a/src/types/file.ts b/src/types/file.ts index 662d465..b3cdfb0 100644 --- a/src/types/file.ts +++ b/src/types/file.ts @@ -16,17 +16,64 @@ export interface FileListItem { size: number text?: string description?: string - expired_at: string + expired_at: string | null expired_count: number | null created_at: string + name?: string + type?: 'text' | 'file' + status?: 'active' | 'expired' + isText?: boolean + is_text?: boolean + isExpired?: boolean + is_expired?: boolean + isChunked?: boolean + is_chunked?: boolean + remainingDownloads?: number | null + remaining_downloads?: number | null + usedCount?: number + used_count?: number + fileHash?: string | null + file_hash?: string | null } export interface AdminFileViewItem extends FileListItem { + displayName: string displaySize: string displayExpiredAt: string + displayUsage: string + isTextFile: boolean + isExpiredFile: boolean + isChunkedFile: boolean + remainingDownloadsValue: number | null canPreviewText: boolean } +export interface AdminFileSummary { + totalFiles: number + activeCount: number + expiredCount: number + textCount: number + fileCount: number + chunkedCount: number + storageUsed: number + usedCount: number +} + +export type AdminFileStatusFilter = 'all' | 'active' | 'expired' +export type AdminFileTypeFilter = 'all' | 'file' | 'text' | 'chunked' +export type AdminFileSortBy = 'created_at' | 'expired_at' | 'name' | 'size' | 'used_count' | 'code' +export type AdminFileSortOrder = 'asc' | 'desc' + +export interface AdminFileListParams { + page: number + size: number + keyword?: string + status?: AdminFileStatusFilter + type?: AdminFileTypeFilter + sortBy?: AdminFileSortBy + sortOrder?: AdminFileSortOrder +} + export interface FileEditForm { id: number | null code: string @@ -41,6 +88,7 @@ export interface FileListResponse { total: number page: number size: number + summary?: AdminFileSummary } export interface FileUploadResponse { diff --git a/src/views/manage/FileManageView.vue b/src/views/manage/FileManageView.vue index 2921a55..799b054 100644 --- a/src/views/manage/FileManageView.vue +++ b/src/views/manage/FileManageView.vue @@ -1,168 +1,315 @@