feat: add batch file policy actions

This commit is contained in:
Lan
2026-06-03 06:22:56 +08:00
parent 47edefbdd7
commit 24b92fe1ac
6 changed files with 190 additions and 5 deletions
+135 -3
View File
@@ -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<Set<number>>(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<BatchPolicyActionOption[]>(
() => 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<FileListItem, 'expired_at'>) => {
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<FileListItem, 'id' | 'expired_at'>,
action: AdminFilePolicyAction
): AdminFilePatchPayload => {
if (action === 'make_permanent') {
@@ -889,6 +917,60 @@ export function useAdminFiles() {
}
}
const applyAdminFilesPolicyActionWithFallback = async (
ids: number[],
action: AdminFilePolicyAction
): Promise<ApiResponse<AdminBatchPolicyActionResponse>> => {
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,
+4
View File
@@ -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: {
+4
View File
@@ -480,6 +480,10 @@ export default {
batchUpdatePartialSuccess: '已更新 {count} 个文件,{failed} 个失败',
batchUpdateFailed: '批量更新失败',
batchUpdateNoFields: '请选择要更新的策略',
batchPolicyActionConfirm: '确认对选中的 {count} 个文件执行“{action}”?',
batchPolicyActionSuccess: '已处理 {count} 个文件',
batchPolicyActionPartialSuccess: '已处理 {count} 个文件,{failed} 个失败',
batchPolicyActionFailed: '批量策略动作执行失败',
policyActionSuccess: '文件策略已更新',
policyActionFailed: '策略动作执行失败',
policyActions: {
+16
View File
@@ -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<ApiResponse<AdminBatchPolicyActionResponse>> {
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<ApiResponse<AdminBatchUpdateFilesResponse>> {
+11
View File
@@ -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 {
+20 -2
View File
@@ -198,10 +198,25 @@
</template>
{{ t('fileManage.clearSelection') }}
</BaseButton>
<BaseButton
v-for="option in batchPolicyActionOptions"
:key="option.action"
variant="outline"
size="sm"
:title="option.description"
:disabled="!hasSelectedFiles || isBatchDeleting || isBatchUpdating"
:loading="isBatchPolicyActionRunning"
@click="applySelectedPolicyAction(option.action)"
>
<template #icon>
<component :is="getDetailPolicyActionIcon(option.action)" class="mr-2 h-4 w-4" />
</template>
{{ option.label }}
</BaseButton>
<BaseButton
variant="secondary"
size="sm"
:disabled="!hasSelectedFiles || isBatchDeleting"
:disabled="!hasSelectedFiles || isBatchDeleting || isBatchPolicyActionRunning"
:loading="isBatchUpdating"
@click="openBatchEditModal"
>
@@ -213,7 +228,7 @@
<BaseButton
variant="danger"
size="sm"
:disabled="!hasSelectedFiles || isBatchUpdating"
:disabled="!hasSelectedFiles || isBatchUpdating || isBatchPolicyActionRunning"
:loading="isBatchDeleting"
@click="deleteSelectedFiles"
>
@@ -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,