From d7e3aff825022ad48b7e2cccdadf891964ffc53b Mon Sep 17 00:00:00 2001 From: Lan Date: Wed, 3 Jun 2026 05:13:58 +0800 Subject: [PATCH] feat: add admin file detail modal --- src/composables/useAdminFiles.ts | 141 ++++++++++- src/i18n/locales/en-US.ts | 27 ++ src/i18n/locales/zh-CN.ts | 27 ++ src/services/file.ts | 7 + src/types/file.ts | 71 ++++++ src/views/manage/FileManageView.vue | 370 ++++++++++++++++++++++++++++ 6 files changed, 640 insertions(+), 3 deletions(-) diff --git a/src/composables/useAdminFiles.ts b/src/composables/useAdminFiles.ts index 0a57cdd..7b5a298 100644 --- a/src/composables/useAdminFiles.ts +++ b/src/composables/useAdminFiles.ts @@ -7,6 +7,8 @@ import type { AdminBatchDeleteFilesResponse, AdminBatchUpdateFilesRequest, AdminBatchUpdateFilesResponse, + AdminFileDetailResponse, + AdminFileDetailViewItem, AdminFilePatchPayload, AdminFileListParams, AdminFileStatusFilter, @@ -24,6 +26,7 @@ import { exportAdminTextFile, getSafeFilename } from '@/utils/download-action' +import { buildRetrieveUrl } from '@/utils/share-url' const emptySummary = (): AdminFileSummary => ({ totalFiles: 0, @@ -50,7 +53,7 @@ type ErrorWithResponse = { } } -const isLegacyBatchActionUnavailable = (error: unknown) => { +const isLegacyEndpointUnavailable = (error: unknown) => { const status = (error as ErrorWithResponse)?.response?.status return status === 404 || status === 405 } @@ -100,10 +103,14 @@ export function useAdminFiles() { const previewFile = ref(null) const previewMetaText = ref('') const isPreviewLoading = ref(false) + const showFileDetailModal = ref(false) + const selectedFileDetail = ref(null) + const isDetailLoading = ref(false) const downloadingFileId = ref(null) const selectedFileIds = ref>(new Set()) const isBatchDeleting = ref(false) const isBatchUpdating = ref(false) + let detailRequestSerial = 0 const isBatchActionRunning = computed(() => isBatchDeleting.value || isBatchUpdating.value) const totalPages = computed(() => Math.max(Math.ceil(params.value.total / params.value.size), 1)) const storageUsedText = computed(() => formatFileSize(summary.value.storageUsed)) @@ -208,6 +215,78 @@ export function useAdminFiles() { } } + const createFileDetailViewItem = ( + file: FileListItem | AdminFileDetailResponse + ): AdminFileDetailViewItem => { + const detail = file as AdminFileDetailResponse + const policy = detail.policy + const storage = detail.storage + const expiredAt = policy?.expiredAt ?? policy?.expired_at ?? file.expired_at ?? null + const expiredCount = policy?.expiredCount ?? policy?.expired_count ?? file.expired_count ?? null + const displayName = + detail.displayName ?? + detail.display_name ?? + detail.filename ?? + file.name ?? + `${file.prefix}${file.suffix}` ?? + file.code + const normalizedFile: FileListItem = { + ...file, + name: displayName, + expired_at: expiredAt, + expired_count: expiredCount + } + const viewItem = createFileViewItem(normalizedFile) + const remainingDownloadsValue = + policy?.remainingDownloads ?? policy?.remaining_downloads ?? viewItem.remainingDownloadsValue + const canPreviewText = + detail.canPreviewText ?? detail.can_preview_text ?? viewItem.canPreviewText + const canDownloadFile = detail.canDownload ?? detail.can_download ?? !viewItem.isExpiredFile + const textLengthValue = normalizeCount( + detail.textLength ?? detail.text_length ?? detail.text?.length + ) + const isPermanentFile = + detail.isPermanent ?? + detail.is_permanent ?? + policy?.isPermanent ?? + policy?.is_permanent ?? + (!expiredAt && (expiredCount === null || expiredCount === undefined || expiredCount < 0)) + const hasDownloadLimitFile = + detail.hasDownloadLimit ?? + detail.has_download_limit ?? + (expiredCount !== null && expiredCount !== undefined && expiredCount >= 0) + const hasExpirationTimeFile = + detail.hasExpirationTime ?? detail.has_expiration_time ?? Boolean(expiredAt) + const storageBackendValue = + storage?.backend ?? detail.storageBackend ?? detail.storage_backend ?? '-' + const isChunkedStorage = storage?.isChunked ?? storage?.is_chunked ?? viewItem.isChunkedFile + + return { + ...viewItem, + displayName, + displayExpiredAt: expiredAt ? formatTimestamp(expiredAt) : t('send.expiration.units.forever'), + displayCreatedAt: file.created_at ? formatTimestamp(file.created_at) : '-', + displayRetrieveUrl: buildRetrieveUrl(file.code), + remainingDownloadsValue, + canPreviewText, + textLengthValue, + canDownloadFile, + isPermanentFile, + hasDownloadLimitFile, + hasExpirationTimeFile, + isChunkedStorage: Boolean(isChunkedStorage), + storageBackendValue, + fileHashValue: storage?.fileHash ?? storage?.file_hash ?? detail.fileHash ?? detail.file_hash, + filePathValue: storage?.filePath ?? storage?.file_path ?? detail.filePath ?? detail.file_path, + uuidFileNameValue: + storage?.uuidFileName ?? + storage?.uuid_file_name ?? + detail.uuidFileName ?? + detail.uuid_file_name, + uploadIdValue: storage?.uploadId ?? storage?.upload_id ?? detail.uploadId ?? detail.upload_id + } + } + const syncSelectedFilesWithCurrentPage = () => { const visibleIds = new Set(tableData.value.map((file) => file.id)) selectedFileIds.value = new Set( @@ -433,7 +512,7 @@ export function useAdminFiles() { try { return await FileService.deleteAdminFiles(ids) } catch (error: unknown) { - if (!isLegacyBatchActionUnavailable(error)) { + if (!isLegacyEndpointUnavailable(error)) { throw error } @@ -458,7 +537,7 @@ export function useAdminFiles() { try { return await FileService.updateAdminFiles(payload) } catch (error: unknown) { - if (!isLegacyBatchActionUnavailable(error)) { + if (!isLegacyEndpointUnavailable(error)) { throw error } @@ -605,6 +684,35 @@ export function useAdminFiles() { previewMetaText.value = '' } + const openFileDetail = async (file: AdminFileViewItem) => { + const requestSerial = ++detailRequestSerial + selectedFileDetail.value = createFileDetailViewItem(file) + showFileDetailModal.value = true + isDetailLoading.value = true + + try { + const response = await FileService.getAdminFileDetail(file.id) + if (response.detail && requestSerial === detailRequestSerial) { + selectedFileDetail.value = createFileDetailViewItem(response.detail) + } + } catch (error: unknown) { + if (isLegacyEndpointUnavailable(error)) return + + alertStore.showAlert(getErrorMessage(error, t('fileManage.detailFailed')), 'error') + } finally { + if (requestSerial === detailRequestSerial) { + isDetailLoading.value = false + } + } + } + + const closeFileDetail = () => { + detailRequestSerial += 1 + showFileDetailModal.value = false + selectedFileDetail.value = null + isDetailLoading.value = false + } + const copyText = async () => { await copyToClipboard(previewText.value, { successMsg: t('fileManage.copySuccess'), @@ -613,6 +721,26 @@ export function useAdminFiles() { }) } + const copyDetailRetrieveCode = async () => { + if (!selectedFileDetail.value) return + + await copyToClipboard(selectedFileDetail.value.code, { + successMsg: t('fileManage.copyCodeSuccess'), + errorMsg: t('fileManage.copyFailed'), + notify: (message, type) => alertStore.showAlert(message, type) + }) + } + + const copyDetailRetrieveLink = async () => { + if (!selectedFileDetail.value) return + + await copyToClipboard(selectedFileDetail.value.displayRetrieveUrl, { + successMsg: t('fileManage.copyLinkSuccess'), + errorMsg: t('fileManage.copyFailed'), + notify: (message, type) => alertStore.showAlert(message, type) + }) + } + const exportPreviewText = () => { if (!previewFile.value || !previewText.value) return exportAdminTextFile(previewFile.value, previewText.value) @@ -648,6 +776,7 @@ export function useAdminFiles() { isBatchDeleting, isBatchUpdating, isCurrentPagePartiallySelected, + isDetailLoading, isPreviewLoading, isSaving, batchEditForm, @@ -656,19 +785,24 @@ export function useAdminFiles() { params, previewFile, previewMetaText, + selectedFileDetail, selectedCount, selectedFileIds, storageUsedText, summary, showBatchEditModal, showEditModal, + showFileDetailModal, editForm, showTextPreview, previewText, totalPages, closeEditModal, closeBatchEditModal, + closeFileDetail, closeTextPreview, + copyDetailRetrieveCode, + copyDetailRetrieveLink, copyText, clearSelection, deleteFile, @@ -682,6 +816,7 @@ export function useAdminFiles() { loadFiles, openBatchEditModal, openEditModal, + openFileDetail, openTextPreview, refreshFiles, resetFilters, diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 2af1260..afd7028 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -381,6 +381,10 @@ export default { subtitle: '{count} share records. Filter by status, type, and usage.', searchPlaceholder: 'Search code, name, or text...', allFiles: 'All Files', + detail: 'Details', + detailTitle: 'File Details', + detailLoading: 'Loading details...', + detailFailed: 'Failed to load details', editFileInfo: 'Edit File Information', saveChanges: 'Save Changes', refresh: 'Refresh List', @@ -405,7 +409,11 @@ export default { viewText: 'View', textPreview: 'Text Preview', copyText: 'Copy Text', + copyCode: 'Copy Code', + copyLink: 'Copy Link', copySuccess: 'Text copied to clipboard', + copyCodeSuccess: 'Retrieve code copied to clipboard', + copyLinkSuccess: 'Retrieve link copied to clipboard', copyFailed: 'Copy failed, please try again', charCount: '{count} characters', downloadFile: 'Download File', @@ -441,6 +449,25 @@ export default { batchUpdateFailed: 'Batch update failed', batchUpdateNoFields: 'Choose a policy to update', applyBatchEdit: 'Apply Changes', + overview: 'Overview', + policyInfo: 'Policy', + storageInfo: 'Storage', + permanent: 'Permanent', + createdAt: 'Created Time', + remainingDownloads: 'Remaining Downloads', + textLength: 'Text Length', + hasDownloadLimit: 'Has Retrieval Limit', + hasExpirationTime: 'Has Expiration', + canPreviewText: 'Can Preview Text', + canDownload: 'Can Download', + storageBackend: 'Storage Backend', + fileHash: 'File Hash', + isChunked: 'Chunked Upload', + filePath: 'Storage Path', + uuidFileName: 'Stored File Name', + uploadId: 'Upload Session', + yes: 'Yes', + no: 'No', headers: { select: 'Select', code: 'Retrieve Code', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index f17b33a..e0d5c1d 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -379,6 +379,10 @@ export default { subtitle: '共 {count} 条分享记录,可按状态、类型和使用情况快速筛选。', searchPlaceholder: '搜索取件码、名称或文本...', allFiles: '所有文件', + detail: '详情', + detailTitle: '文件详情', + detailLoading: '正在加载详情...', + detailFailed: '详情加载失败', editFileInfo: '编辑文件信息', saveChanges: '保存更改', refresh: '刷新列表', @@ -403,7 +407,11 @@ export default { viewText: '查看', textPreview: '文本预览', copyText: '复制文本', + copyCode: '复制取件码', + copyLink: '复制取件链接', copySuccess: '文本已复制到剪贴板', + copyCodeSuccess: '取件码已复制到剪贴板', + copyLinkSuccess: '取件链接已复制到剪贴板', copyFailed: '复制失败,请重试', charCount: '共 {count} 个字符', downloadFile: '下载文件', @@ -439,6 +447,25 @@ export default { batchUpdateFailed: '批量更新失败', batchUpdateNoFields: '请选择要更新的策略', applyBatchEdit: '应用更改', + overview: '概览', + policyInfo: '策略信息', + storageInfo: '存储信息', + permanent: '永久有效', + createdAt: '创建时间', + remainingDownloads: '剩余次数', + textLength: '文本长度', + hasDownloadLimit: '限制取件次数', + hasExpirationTime: '限制过期时间', + canPreviewText: '可预览文本', + canDownload: '可下载', + storageBackend: '存储后端', + fileHash: '文件哈希', + isChunked: '分片上传', + filePath: '存储路径', + uuidFileName: '存储文件名', + uploadId: '上传会话', + yes: '是', + no: '否', headers: { select: '选择', code: '取件码', diff --git a/src/services/file.ts b/src/services/file.ts index ab7e236..456b6f7 100644 --- a/src/services/file.ts +++ b/src/services/file.ts @@ -5,6 +5,7 @@ import type { AdminBatchUpdateFilesRequest, AdminBatchUpdateFilesResponse, AdminFilePatchPayload, + AdminFileDetailResponse, AdminFileListParams, AdminFilePreviewResponse, ApiResponse, @@ -128,6 +129,12 @@ export class FileService { return api.get('/admin/file/list', { params }) } + static async getAdminFileDetail(id: number): Promise> { + return api.get('/admin/file/detail', { + params: { id } + }) + } + static async updateFile(data: FileEditForm | AdminFilePatchPayload): Promise { return api.patch('/admin/file/update', data) } diff --git a/src/types/file.ts b/src/types/file.ts index 651ac0c..38ce03a 100644 --- a/src/types/file.ts +++ b/src/types/file.ts @@ -118,6 +118,77 @@ export interface AdminFilePreviewResponse { expired_at?: string | null } +export interface AdminFileDetailPolicy { + expiredAt?: string | null + expired_at?: string | null + expiredCount?: number | null + expired_count?: number | null + remainingDownloads?: number | null + remaining_downloads?: number | null + isExpired?: boolean + is_expired?: boolean + isPermanent?: boolean + is_permanent?: boolean +} + +export interface AdminFileDetailStorage { + backend?: string + filePath?: string | null + file_path?: string | null + uuidFileName?: string | null + uuid_file_name?: string | null + fileHash?: string | null + file_hash?: string | null + isChunked?: boolean + is_chunked?: boolean + uploadId?: string | null + upload_id?: string | null +} + +export interface AdminFileDetailResponse extends FileListItem { + filename?: string + displayName?: string + display_name?: string + isPermanent?: boolean + is_permanent?: boolean + hasDownloadLimit?: boolean + has_download_limit?: boolean + hasExpirationTime?: boolean + has_expiration_time?: boolean + textLength?: number + text_length?: number + canPreviewText?: boolean + can_preview_text?: boolean + canDownload?: boolean + can_download?: boolean + storageBackend?: string + storage_backend?: string + filePath?: string | null + file_path?: string | null + uuidFileName?: string | null + uuid_file_name?: string | null + uploadId?: string | null + upload_id?: string | null + policy?: AdminFileDetailPolicy + storage?: AdminFileDetailStorage +} + +export interface AdminFileDetailViewItem extends AdminFileViewItem { + displayCreatedAt: string + displayRetrieveUrl: string + textLengthValue: number + canDownloadFile: boolean + isPermanentFile: boolean + hasDownloadLimitFile: boolean + hasExpirationTimeFile: boolean + isChunkedStorage: boolean + storageBackendValue: string + fileHashValue?: string | null + filePathValue?: string | null + uuidFileNameValue?: string | null + uploadIdValue?: string | null +} + export interface AdminBatchDeleteFileFailure { id: number reason: string diff --git a/src/views/manage/FileManageView.vue b/src/views/manage/FileManageView.vue index 23cee8f..89b8fc7 100644 --- a/src/views/manage/FileManageView.vue +++ b/src/views/manage/FileManageView.vue @@ -341,6 +341,20 @@
+ + + +
+ +
+

+ {{ t('fileManage.overview') }} +

+
+
+

{{ item.label }}

+

+ {{ item.value }} +

+
+
+
+ +
+

+ {{ t('fileManage.policyInfo') }} +

+
+
+

{{ item.label }}

+

+ {{ item.value }} +

+
+
+
+ +
+

+ {{ t('fileManage.storageInfo') }} +

+
+
+

{{ item.label }}

+

+ {{ item.value }} +

+
+
+
+ +
+ + {{ t('fileManage.detailLoading') }} +
+ + + +