From e2318f007d623c65182b8740152180196cecdb8c Mon Sep 17 00:00:00 2001 From: Lan Date: Wed, 3 Jun 2026 06:22:56 +0800 Subject: [PATCH] feat: add batch file policy actions --- src/composables/useAdminFiles.ts | 138 +++++++++++++++++++++++++++- src/i18n/locales/en-US.ts | 4 + src/i18n/locales/zh-CN.ts | 4 + src/services/file.ts | 16 ++++ src/types/file.ts | 11 +++ src/views/manage/FileManageView.vue | 22 ++++- 6 files changed, 190 insertions(+), 5 deletions(-) diff --git a/src/composables/useAdminFiles.ts b/src/composables/useAdminFiles.ts index fbcb658..b71cc44 100644 --- a/src/composables/useAdminFiles.ts +++ b/src/composables/useAdminFiles.ts @@ -5,6 +5,8 @@ import { useAlertStore } from '@/stores/alertStore' import type { AdminBatchEditForm, AdminBatchDeleteFilesResponse, + AdminBatchPolicyActionRequest, + AdminBatchPolicyActionResponse, AdminBatchUpdateFilesRequest, AdminBatchUpdateFilesResponse, AdminFileDetailResponse, @@ -81,6 +83,8 @@ type DetailPolicyActionOption = { description: string } +type BatchPolicyActionOption = DetailPolicyActionOption + type HealthFilterOption = { value: AdminFileHealthFilter label: string @@ -151,8 +155,11 @@ export function useAdminFiles() { const selectedFileIds = ref>(new Set()) const isBatchDeleting = ref(false) const isBatchUpdating = ref(false) + const isBatchPolicyActionRunning = ref(false) let detailRequestSerial = 0 - const isBatchActionRunning = computed(() => isBatchDeleting.value || isBatchUpdating.value) + const isBatchActionRunning = computed( + () => isBatchDeleting.value || isBatchUpdating.value || isBatchPolicyActionRunning.value + ) const totalPages = computed(() => Math.max(Math.ceil(params.value.total / params.value.size), 1)) const storageUsedText = computed(() => formatFileSize(summary.value.storageUsed)) const currentPageSelectedCount = computed( @@ -257,6 +264,10 @@ export function useAdminFiles() { } ]) + const batchPolicyActionOptions = computed( + () => detailPolicyActionOptions.value + ) + const inferIsText = (file: FileListItem) => { if (typeof file.isText === 'boolean') return file.isText if (typeof file.is_text === 'boolean') return file.is_text @@ -657,7 +668,24 @@ export function useAdminFiles() { return request } - const getPolicyActionBaseDate = (file: AdminFileDetailViewItem) => { + const buildBatchPolicyActionRequest = ( + ids: number[], + action: AdminFilePolicyAction + ): AdminBatchPolicyActionRequest => { + const request: AdminBatchPolicyActionRequest = { + ids, + action + } + + if (action === 'reset_download_limit') { + request.downloadLimit = detailPolicyDownloadLimit + request.download_limit = detailPolicyDownloadLimit + } + + return request + } + + const getPolicyActionBaseDate = (file: Pick) => { const now = new Date() const expiredAt = file.expired_at ? new Date(file.expired_at) : null if (!expiredAt || Number.isNaN(expiredAt.getTime()) || expiredAt.getTime() < now.getTime()) { @@ -667,7 +695,7 @@ export function useAdminFiles() { } const buildLegacyPolicyActionPayload = ( - file: AdminFileDetailViewItem, + file: Pick, action: AdminFilePolicyAction ): AdminFilePatchPayload => { if (action === 'make_permanent') { @@ -889,6 +917,60 @@ export function useAdminFiles() { } } + const applyAdminFilesPolicyActionWithFallback = async ( + ids: number[], + action: AdminFilePolicyAction + ): Promise> => { + const request = buildBatchPolicyActionRequest(ids, action) + try { + return await FileService.applyAdminFilesPolicyAction(request) + } catch (error: unknown) { + if (!isLegacyEndpointUnavailable(error)) { + throw error + } + + const filesById = new Map(tableData.value.map((file) => [file.id, file])) + const updated: number[] = [] + const missing: number[] = [] + const failed: { id: number; reason: string }[] = [] + + for (const id of ids) { + const file = filesById.get(id) + if (!file) { + missing.push(id) + continue + } + + try { + await FileService.updateFile(buildLegacyPolicyActionPayload(file, action)) + updated.push(id) + } catch (legacyError: unknown) { + failed.push({ id, reason: getErrorMessage(legacyError, t('common.unknownError')) }) + } + } + + return { + code: 200, + detail: { + requestedCount: ids.length, + requested_count: ids.length, + uniqueCount: ids.length, + unique_count: ids.length, + updatedCount: updated.length, + updated_count: updated.length, + missingCount: missing.length, + missing_count: missing.length, + failedCount: failed.length, + failed_count: failed.length, + action, + updated, + missing, + failed + } + } + } + } + const deleteSelectedFiles = async () => { if (!hasSelectedFiles.value || isBatchActionRunning.value) return @@ -972,6 +1054,53 @@ export function useAdminFiles() { } } + const applySelectedPolicyAction = async (action: AdminFilePolicyAction) => { + if (!hasSelectedFiles.value || isBatchActionRunning.value) return + + const ids = Array.from(selectedFileIds.value) + const actionLabel = + batchPolicyActionOptions.value.find((option) => option.action === action)?.label || action + if ( + !window.confirm( + t('fileManage.batchPolicyActionConfirm', { + count: ids.length, + action: actionLabel + }) + ) + ) { + return + } + + isBatchPolicyActionRunning.value = true + try { + const response = await applyAdminFilesPolicyActionWithFallback(ids, action) + const detail = response.detail + const updatedCount = detail?.updatedCount ?? detail?.updated_count ?? ids.length + const failedCount = + (detail?.failedCount ?? detail?.failed_count ?? 0) + + (detail?.missingCount ?? detail?.missing_count ?? 0) + + clearSelection() + await loadFiles() + alertStore.showAlert( + t( + failedCount > 0 + ? 'fileManage.batchPolicyActionPartialSuccess' + : 'fileManage.batchPolicyActionSuccess', + { + count: updatedCount, + failed: failedCount + } + ), + failedCount > 0 ? 'warning' : 'success' + ) + } catch (error: unknown) { + alertStore.showAlert(getErrorMessage(error, t('fileManage.batchPolicyActionFailed')), 'error') + } finally { + isBatchPolicyActionRunning.value = false + } + } + const openTextPreview = async (file: AdminFileViewItem) => { if (!file.isTextFile || isPreviewLoading.value) return @@ -1135,6 +1264,7 @@ export function useAdminFiles() { isAllCurrentPageSelected, isBatchActionRunning, isBatchDeleting, + isBatchPolicyActionRunning, isBatchUpdating, isCurrentPagePartiallySelected, isDetailLoading, @@ -1142,6 +1272,7 @@ export function useAdminFiles() { isPreviewLoading, isSaving, batchEditForm, + batchPolicyActionOptions, detailPolicyActionOptions, downloadingFileId, healthFilterOptions, @@ -1176,6 +1307,7 @@ export function useAdminFiles() { handlePageChange, handleSearch, applyDetailPolicyAction, + applySelectedPolicyAction, handleBatchUpdate, handleUpdate, loadFiles, diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 71cd77c..e178d84 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -482,6 +482,10 @@ export default { batchUpdatePartialSuccess: 'Updated {count} files, {failed} failed', batchUpdateFailed: 'Batch update failed', batchUpdateNoFields: 'Choose a policy to update', + batchPolicyActionConfirm: 'Apply "{action}" to the selected {count} files?', + batchPolicyActionSuccess: 'Processed {count} files', + batchPolicyActionPartialSuccess: 'Processed {count} files, {failed} failed', + batchPolicyActionFailed: 'Batch policy action failed', policyActionSuccess: 'File policy updated', policyActionFailed: 'Failed to apply policy action', policyActions: { diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 65da2cc..e25b470 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -480,6 +480,10 @@ export default { batchUpdatePartialSuccess: '已更新 {count} 个文件,{failed} 个失败', batchUpdateFailed: '批量更新失败', batchUpdateNoFields: '请选择要更新的策略', + batchPolicyActionConfirm: '确认对选中的 {count} 个文件执行“{action}”?', + batchPolicyActionSuccess: '已处理 {count} 个文件', + batchPolicyActionPartialSuccess: '已处理 {count} 个文件,{failed} 个失败', + batchPolicyActionFailed: '批量策略动作执行失败', policyActionSuccess: '文件策略已更新', policyActionFailed: '策略动作执行失败', policyActions: { diff --git a/src/services/file.ts b/src/services/file.ts index 382145d..d194f1e 100644 --- a/src/services/file.ts +++ b/src/services/file.ts @@ -2,6 +2,8 @@ import api, { rawApiClient } from './client' import { multipartUploadConfig } from './shared' import type { AdminBatchDeleteFilesResponse, + AdminBatchPolicyActionRequest, + AdminBatchPolicyActionResponse, AdminBatchUpdateFilesRequest, AdminBatchUpdateFilesResponse, AdminFilePatchPayload, @@ -159,6 +161,20 @@ export class FileService { } } + static async applyAdminFilesPolicyAction( + data: AdminBatchPolicyActionRequest + ): Promise> { + try { + return await api.patch('/admin/file/batch-policy-action', data) + } catch (error: unknown) { + if (!isMethodFallbackError(error)) { + throw error + } + + return api.post('/admin/file/batch-policy-action', data) + } + } + static async updateAdminFiles( data: AdminBatchUpdateFilesRequest ): Promise> { diff --git a/src/types/file.ts b/src/types/file.ts index 4b5d0f8..0685aa1 100644 --- a/src/types/file.ts +++ b/src/types/file.ts @@ -323,6 +323,17 @@ export interface AdminBatchUpdateFilesResponse { failed?: AdminBatchUpdateFileFailure[] } +export interface AdminBatchPolicyActionRequest { + ids: number[] + action: AdminFilePolicyAction + downloadLimit?: number + download_limit?: number +} + +export interface AdminBatchPolicyActionResponse extends AdminBatchUpdateFilesResponse { + action?: AdminFilePolicyAction | string +} + export type AdminBatchEditMode = 'expiresAt' | 'downloadLimit' | 'forever' export interface AdminBatchEditForm { diff --git a/src/views/manage/FileManageView.vue b/src/views/manage/FileManageView.vue index 42fe174..6bd359d 100644 --- a/src/views/manage/FileManageView.vue +++ b/src/views/manage/FileManageView.vue @@ -198,10 +198,25 @@ {{ t('fileManage.clearSelection') }} + + + {{ option.label }} + @@ -213,7 +228,7 @@ @@ -1093,6 +1108,7 @@ const { isAllCurrentPageSelected, isBatchActionRunning, isBatchDeleting, + isBatchPolicyActionRunning, isBatchUpdating, isCurrentPagePartiallySelected, isDetailLoading, @@ -1100,6 +1116,7 @@ const { isPreviewLoading, isSaving, batchEditForm, + batchPolicyActionOptions, detailPolicyActionOptions, downloadingFileId, healthFilterOptions, @@ -1133,6 +1150,7 @@ const { handlePageChange, handleSearch, applyDetailPolicyAction, + applySelectedPolicyAction, handleBatchUpdate, handleUpdate, loadFiles,