From e33a5deaf28c34ab5b6be701b7b9d00c70ba4033 Mon Sep 17 00:00:00 2001 From: Lan Date: Wed, 3 Jun 2026 07:01:38 +0800 Subject: [PATCH] feat: add admin file view presets --- src/composables/useAdminFiles.ts | 316 ++++++++++++++++++++++++++++ src/i18n/locales/en-US.ts | 18 ++ src/i18n/locales/zh-CN.ts | 18 ++ src/services/file.ts | 35 +++ src/types/file.ts | 37 ++++ src/views/manage/FileManageView.vue | 92 ++++++++ 6 files changed, 516 insertions(+) diff --git a/src/composables/useAdminFiles.ts b/src/composables/useAdminFiles.ts index 8b15aca..91a3b1e 100644 --- a/src/composables/useAdminFiles.ts +++ b/src/composables/useAdminFiles.ts @@ -22,6 +22,9 @@ import type { AdminFileStatusFilter, AdminFileSummary, AdminFileTypeFilter, + AdminFileViewPreset, + AdminFileViewPresetParams, + AdminFileViewPresetRequest, AdminFileViewItem, ApiResponse, FileEditForm, @@ -117,6 +120,89 @@ const formatLocalDateTime = (date: Date) => { return `${year}-${month}-${day}T${hours}:${minutes}` } +const builtInAllViewPresetId = 'built_in_all' +const adminFileStatusFilters: AdminFileStatusFilter[] = ['all', 'active', 'expired'] +const adminFileTypeFilters: AdminFileTypeFilter[] = ['all', 'file', 'text', 'chunked'] +const adminFileHealthFilters: AdminFileHealthFilter[] = [ + 'all', + 'attention', + 'danger', + 'warning', + 'healthy', + 'expired', + 'expiring_soon', + 'storage_issue', + 'never_retrieved', + 'permanent' +] +const adminFileSortFields = [ + 'created_at', + 'expired_at', + 'name', + 'size', + 'used_count', + 'code' +] as const +const adminFileSortOrders = ['asc', 'desc'] as const + +type RawViewPresetParams = Partial< + Record +> + +const normalizePresetChoice = ( + value: unknown, + allowedValues: readonly T[], + fallback: T +): T => { + const normalizedValue = String(value ?? fallback) + .replace('-', '_') + .trim() + .toLowerCase() + return allowedValues.includes(normalizedValue as T) ? (normalizedValue as T) : fallback +} + +const normalizePresetSize = (value: unknown) => { + const normalizedSize = Number(value) + if (!Number.isFinite(normalizedSize)) return 10 + return Math.min(Math.max(Math.trunc(normalizedSize), 1), 100) +} + +const buildDefaultViewPresetParams = (): AdminFileViewPresetParams => ({ + keyword: '', + status: 'all', + type: 'all', + health: 'all', + sortBy: 'created_at', + sortOrder: 'desc', + size: 10 +}) + +const normalizeViewPresetParams = (params: unknown): AdminFileViewPresetParams => { + const defaults = buildDefaultViewPresetParams() + const rawParams = + params && typeof params === 'object' + ? (params as RawViewPresetParams) + : ({} as RawViewPresetParams) + + return { + keyword: String(rawParams.keyword ?? defaults.keyword).trim(), + status: normalizePresetChoice(rawParams.status, adminFileStatusFilters, defaults.status), + type: normalizePresetChoice(rawParams.type, adminFileTypeFilters, defaults.type), + health: normalizePresetChoice(rawParams.health, adminFileHealthFilters, defaults.health), + sortBy: normalizePresetChoice( + rawParams.sortBy ?? rawParams.sort_by, + adminFileSortFields, + defaults.sortBy + ), + sortOrder: normalizePresetChoice( + rawParams.sortOrder ?? rawParams.sort_order, + adminFileSortOrders, + defaults.sortOrder + ), + size: normalizePresetSize(rawParams.size ?? defaults.size) + } +} + export function useAdminFiles() { const { t } = useI18n() const alertStore = useAlertStore() @@ -166,6 +252,11 @@ export function useAdminFiles() { const isBatchDeleting = ref(false) const isBatchUpdating = ref(false) const isBatchPolicyActionRunning = ref(false) + const savedViewPresets = ref([]) + const selectedViewPresetId = ref(builtInAllViewPresetId) + const isViewPresetLoading = ref(false) + const isViewPresetSaving = ref(false) + const isViewPresetDeleting = ref(false) let detailRequestSerial = 0 const isBatchActionRunning = computed( () => isBatchDeleting.value || isBatchUpdating.value || isBatchPolicyActionRunning.value @@ -203,6 +294,61 @@ export function useAdminFiles() { params.value.health !== 'all' ) + const buildCurrentViewPresetParams = (): AdminFileViewPresetParams => ({ + keyword: params.value.keyword || '', + status: params.value.status || 'all', + type: params.value.type || 'all', + health: params.value.health || 'all', + sortBy: params.value.sortBy || 'created_at', + sortOrder: params.value.sortOrder || 'desc', + size: params.value.size || 10 + }) + + const buildBuiltInViewPreset = ( + id: string, + nameKey: string, + filters: Partial = {} + ): AdminFileViewPreset => { + const presetFilters = normalizeViewPresetParams({ + ...buildDefaultViewPresetParams(), + ...filters + }) + + return { + id, + name: t(nameKey), + filters: presetFilters, + params: presetFilters, + isBuiltIn: true, + isDefault: id === builtInAllViewPresetId, + is_default: id === builtInAllViewPresetId + } + } + + const builtInViewPresets = computed(() => [ + buildBuiltInViewPreset(builtInAllViewPresetId, 'fileManage.viewPresetAll'), + buildBuiltInViewPreset('built_in_attention', 'fileManage.viewPresetAttention', { + health: 'attention' + }), + buildBuiltInViewPreset('built_in_expiring_soon', 'fileManage.viewPresetExpiringSoon', { + health: 'expiring_soon' + }), + buildBuiltInViewPreset('built_in_storage_issue', 'fileManage.viewPresetStorageIssue', { + health: 'storage_issue' + }), + buildBuiltInViewPreset('built_in_never_retrieved', 'fileManage.viewPresetNeverRetrieved', { + health: 'never_retrieved' + }), + buildBuiltInViewPreset('built_in_permanent', 'fileManage.viewPresetPermanent', { + health: 'permanent' + }) + ]) + + const viewPresets = computed(() => [ + ...builtInViewPresets.value, + ...savedViewPresets.value + ]) + const healthFilterOptions = computed(() => [ { value: 'all', label: t('fileManage.healthFilters.all'), count: summary.value.totalFiles }, { @@ -766,6 +912,161 @@ export function useAdminFiles() { } } + const getPresetParams = (preset: AdminFileViewPreset) => + normalizeViewPresetParams(preset.filters ?? preset.params) + + const normalizeSavedViewPreset = (preset: AdminFileViewPreset): AdminFileViewPreset | null => { + const id = String(preset.id || '').trim() + const name = String(preset.name || '').trim() + if (!id || !name) return null + + const presetParams = getPresetParams(preset) + return { + ...preset, + id, + name, + filters: presetParams, + params: presetParams, + isBuiltIn: false + } + } + + const upsertSavedViewPreset = (preset: AdminFileViewPreset) => { + const normalizedPreset = normalizeSavedViewPreset(preset) + if (!normalizedPreset) return null + + savedViewPresets.value = [ + ...savedViewPresets.value.filter((item) => item.id !== normalizedPreset.id), + normalizedPreset + ] + return normalizedPreset + } + + const buildViewPresetRequest = (name: string, id?: string): AdminFileViewPresetRequest | null => { + const normalizedName = name.trim() + if (!normalizedName) return null + + const filters = normalizeViewPresetParams(buildCurrentViewPresetParams()) + return { + id, + name: normalizedName, + filters, + params: filters + } + } + + const markViewPresetDirty = () => { + selectedViewPresetId.value = '' + } + + const loadViewPresets = async () => { + isViewPresetLoading.value = true + try { + const response = await FileService.getAdminFileViewPresets() + const rawPresets = response.detail?.presets ?? response.detail?.items ?? [] + savedViewPresets.value = rawPresets + .map((preset) => normalizeSavedViewPreset(preset)) + .filter((preset): preset is AdminFileViewPreset => Boolean(preset)) + + if ( + selectedViewPresetId.value && + !viewPresets.value.some((preset) => preset.id === selectedViewPresetId.value) + ) { + selectedViewPresetId.value = builtInAllViewPresetId + } + } catch (error: unknown) { + savedViewPresets.value = [] + alertStore.showAlert( + isLegacyEndpointUnavailable(error) + ? t('fileManage.viewPresetLoadFailed') + : getErrorMessage(error, t('fileManage.viewPresetLoadFailed')), + 'warning' + ) + } finally { + isViewPresetLoading.value = false + } + } + + const applyViewPreset = async (idOrPreset: string | AdminFileViewPreset) => { + const preset = + typeof idOrPreset === 'string' + ? viewPresets.value.find((item) => item.id === idOrPreset) + : idOrPreset + if (!preset) return + + const presetParams = getPresetParams(preset) + params.value.keyword = presetParams.keyword + params.value.status = presetParams.status + params.value.type = presetParams.type + params.value.health = presetParams.health + params.value.sortBy = presetParams.sortBy + params.value.sortOrder = presetParams.sortOrder + params.value.size = presetParams.size + params.value.page = 1 + selectedViewPresetId.value = preset.id + clearSelection() + await loadFiles() + } + + const saveCurrentViewPreset = async (name: string) => { + const request = buildViewPresetRequest(name) + if (!request || isViewPresetSaving.value) return null + + isViewPresetSaving.value = true + try { + const response = await FileService.saveAdminFileViewPreset(request) + const savedPreset = response.detail ? upsertSavedViewPreset(response.detail) : null + selectedViewPresetId.value = savedPreset?.id || selectedViewPresetId.value + alertStore.showAlert(t('fileManage.viewPresetSaveSuccess'), 'success') + return savedPreset + } catch (error: unknown) { + alertStore.showAlert(getErrorMessage(error, t('fileManage.viewPresetSaveFailed')), 'error') + return null + } finally { + isViewPresetSaving.value = false + } + } + + const updateSelectedViewPreset = async () => { + const preset = savedViewPresets.value.find((item) => item.id === selectedViewPresetId.value) + if (!preset || isViewPresetSaving.value) return null + + const request = buildViewPresetRequest(preset.name, preset.id) + if (!request) return null + + isViewPresetSaving.value = true + try { + const response = await FileService.saveAdminFileViewPreset(request) + const savedPreset = response.detail ? upsertSavedViewPreset(response.detail) : null + selectedViewPresetId.value = savedPreset?.id || preset.id + alertStore.showAlert(t('fileManage.viewPresetSaveSuccess'), 'success') + return savedPreset + } catch (error: unknown) { + alertStore.showAlert(getErrorMessage(error, t('fileManage.viewPresetSaveFailed')), 'error') + return null + } finally { + isViewPresetSaving.value = false + } + } + + const deleteSelectedViewPreset = async () => { + const preset = savedViewPresets.value.find((item) => item.id === selectedViewPresetId.value) + if (!preset || isViewPresetDeleting.value) return + if (!window.confirm(t('fileManage.viewPresetDeleteConfirm', { name: preset.name }))) return + + isViewPresetDeleting.value = true + try { + await FileService.deleteAdminFileViewPreset(preset.id) + savedViewPresets.value = savedViewPresets.value.filter((item) => item.id !== preset.id) + markViewPresetDirty() + alertStore.showAlert(t('fileManage.viewPresetDeleteSuccess'), 'success') + } catch (error: unknown) { + alertStore.showAlert(getErrorMessage(error, t('fileManage.viewPresetDeleteFailed')), 'error') + } finally { + isViewPresetDeleting.value = false + } + } + const refreshSelectedFileDetail = async (fileId: number, detail?: AdminFileDetailResponse) => { if (detail) { setSelectedFileDetail(detail) @@ -812,6 +1113,7 @@ export function useAdminFiles() { } const handleSearch = async () => { + markViewPresetDirty() params.value.page = 1 await loadFiles() } @@ -828,23 +1130,27 @@ export function useAdminFiles() { params.value.health = 'all' params.value.sortBy = 'created_at' params.value.sortOrder = 'desc' + selectedViewPresetId.value = builtInAllViewPresetId clearSelection() await loadFiles() } const setStatusFilter = async (status: AdminFileStatusFilter) => { + markViewPresetDirty() params.value.status = status params.value.page = 1 await loadFiles() } const setTypeFilter = async (type: AdminFileTypeFilter) => { + markViewPresetDirty() params.value.type = type params.value.page = 1 await loadFiles() } const setHealthFilter = async (health: AdminFileHealthFilter) => { + markViewPresetDirty() params.value.health = health params.value.page = 1 await loadFiles() @@ -1342,6 +1648,9 @@ export function useAdminFiles() { isDetailPolicyActionRunning, isPreviewLoading, isSaving, + isViewPresetDeleting, + isViewPresetLoading, + isViewPresetSaving, batchEditForm, batchPolicyActionOptions, detailPolicyActionOptions, @@ -1355,6 +1664,7 @@ export function useAdminFiles() { selectedFileDetail, selectedCount, selectedFileIds, + selectedViewPresetId, storageUsedText, summary, showBatchEditModal, @@ -1364,6 +1674,8 @@ export function useAdminFiles() { showTextPreview, previewText, totalPages, + viewPresets, + applyViewPreset, closeEditModal, closeBatchEditModal, closeFileDetail, @@ -1374,6 +1686,7 @@ export function useAdminFiles() { clearSelection, deleteFile, deleteSelectedFiles, + deleteSelectedViewPreset, downloadFile, exportPreviewText, handlePageChange, @@ -1382,6 +1695,7 @@ export function useAdminFiles() { applySelectedPolicyAction, handleBatchUpdate, handleUpdate, + loadViewPresets, loadFiles, openBatchEditModal, openEditModal, @@ -1389,9 +1703,11 @@ export function useAdminFiles() { openTextPreview, refreshFiles, resetFilters, + saveCurrentViewPreset, setHealthFilter, setStatusFilter, setTypeFilter, + updateSelectedViewPreset, updateDetailMetadata, toggleCurrentPageSelection, toggleFileSelection diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 5e26fd7..1bc0861 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -418,6 +418,24 @@ export default { statusLabel: 'Status', typeLabel: 'Type', healthLabel: 'Health', + viewPreset: 'View', + viewPresetCustom: 'Custom Filters', + viewPresetAll: 'All Files', + viewPresetAttention: 'Needs Attention', + viewPresetExpiringSoon: 'Expiring Soon', + viewPresetStorageIssue: 'Storage Issues', + viewPresetNeverRetrieved: 'Never Retrieved', + viewPresetPermanent: 'Permanent', + saveViewPreset: 'Save View', + updateViewPreset: 'Update View', + deleteViewPreset: 'Delete View', + viewPresetNamePrompt: 'Enter a view name', + viewPresetSaveSuccess: 'View saved', + viewPresetSaveFailed: 'Failed to save view', + viewPresetDeleteConfirm: 'Delete view "{name}"?', + viewPresetDeleteSuccess: 'View deleted', + viewPresetDeleteFailed: 'Failed to delete view', + viewPresetLoadFailed: 'Failed to load saved views; using built-in views', all: 'All', active: 'Available', expired: 'Expired', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 8e8d916..f755645 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -416,6 +416,24 @@ export default { statusLabel: '状态', typeLabel: '类型', healthLabel: '健康', + viewPreset: '视图', + viewPresetCustom: '自定义筛选', + viewPresetAll: '全部文件', + viewPresetAttention: '需关注', + viewPresetExpiringSoon: '即将过期', + viewPresetStorageIssue: '存储异常', + viewPresetNeverRetrieved: '未取件', + viewPresetPermanent: '永久有效', + saveViewPreset: '保存视图', + updateViewPreset: '覆盖视图', + deleteViewPreset: '删除视图', + viewPresetNamePrompt: '输入视图名称', + viewPresetSaveSuccess: '视图已保存', + viewPresetSaveFailed: '视图保存失败', + viewPresetDeleteConfirm: '确认删除视图“{name}”?', + viewPresetDeleteSuccess: '视图已删除', + viewPresetDeleteFailed: '视图删除失败', + viewPresetLoadFailed: '视图预设加载失败,已使用内置视图', all: '全部', active: '可取件', expired: '已过期', diff --git a/src/services/file.ts b/src/services/file.ts index 8b661d7..b1eef53 100644 --- a/src/services/file.ts +++ b/src/services/file.ts @@ -12,6 +12,9 @@ import type { AdminFileMetadataRequest, AdminFilePolicyActionRequest, AdminFilePreviewResponse, + AdminFileViewPreset, + AdminFileViewPresetRequest, + AdminFileViewPresetsResponse, ApiResponse, ChunkUploadCompleteRequest, ChunkUploadInitRequest, @@ -190,6 +193,24 @@ export class FileService { } } + static async getAdminFileViewPresets(): Promise> { + return api.get('/admin/file/view-presets') + } + + static async saveAdminFileViewPreset( + data: AdminFileViewPresetRequest + ): Promise> { + try { + return await api.patch('/admin/file/view-presets', data) + } catch (error: unknown) { + if (!isMethodFallbackError(error)) { + throw error + } + + return api.post('/admin/file/view-presets', data) + } + } + static async updateAdminFiles( data: AdminBatchUpdateFilesRequest ): Promise> { @@ -210,6 +231,20 @@ export class FileService { }) } + static async deleteAdminFileViewPreset(id: string): Promise { + try { + return await api.delete('/admin/file/view-presets', { + data: { id } + }) + } catch (error: unknown) { + if (!isMethodFallbackError(error)) { + throw error + } + + return api.post('/admin/file/view-presets/delete', { id }) + } + } + static async downloadAdminFile( id: number ): Promise<{ data: Blob; headers: Record }> { diff --git a/src/types/file.ts b/src/types/file.ts index 371d2c1..50fc5a4 100644 --- a/src/types/file.ts +++ b/src/types/file.ts @@ -102,6 +102,43 @@ export interface AdminFileListParams { sortOrder?: AdminFileSortOrder } +export interface AdminFileViewPresetParams { + keyword: string + status: AdminFileStatusFilter + type: AdminFileTypeFilter + health: AdminFileHealthFilter + sortBy: AdminFileSortBy + sortOrder: AdminFileSortOrder + size: number +} + +export interface AdminFileViewPreset { + id: string + name: string + filters?: AdminFileViewPresetParams + params?: AdminFileViewPresetParams + isBuiltIn?: boolean + isDefault?: boolean + is_default?: boolean + createdAt?: string | null + created_at?: string | null + updatedAt?: string | null + updated_at?: string | null +} + +export interface AdminFileViewPresetRequest { + id?: string + name: string + filters: AdminFileViewPresetParams + params?: AdminFileViewPresetParams +} + +export interface AdminFileViewPresetsResponse { + presets?: AdminFileViewPreset[] + items?: AdminFileViewPreset[] + total?: number +} + export interface FileEditForm { id: number | null code: string diff --git a/src/views/manage/FileManageView.vue b/src/views/manage/FileManageView.vue index 0b30edd..2fad4c3 100644 --- a/src/views/manage/FileManageView.vue +++ b/src/views/manage/FileManageView.vue @@ -159,6 +159,64 @@ {{ option.label }} +
+ + +
+ + + {{ t('fileManage.saveViewPreset') }} + + + + {{ t('fileManage.updateViewPreset') }} + + + + {{ t('fileManage.deleteViewPreset') }} + +
+
@@ -1178,6 +1236,9 @@ const { isDetailPolicyActionRunning, isPreviewLoading, isSaving, + isViewPresetDeleting, + isViewPresetLoading, + isViewPresetSaving, batchEditForm, batchPolicyActionOptions, detailPolicyActionOptions, @@ -1191,6 +1252,7 @@ const { selectedFileDetail, selectedCount, selectedFileIds, + selectedViewPresetId, storageUsedText, summary, showBatchEditModal, @@ -1199,6 +1261,8 @@ const { editForm, showTextPreview, previewText, + viewPresets, + applyViewPreset, closeBatchEditModal, closeEditModal, closeFileDetail, @@ -1209,6 +1273,7 @@ const { clearSelection, deleteFile, deleteSelectedFiles, + deleteSelectedViewPreset, downloadFile, exportPreviewText, handlePageChange, @@ -1217,6 +1282,7 @@ const { applySelectedPolicyAction, handleBatchUpdate, handleUpdate, + loadViewPresets, loadFiles, openBatchEditModal, openEditModal, @@ -1224,9 +1290,11 @@ const { openTextPreview, refreshFiles, resetFilters, + saveCurrentViewPreset, setHealthFilter, setStatusFilter, setTypeFilter, + updateSelectedViewPreset, updateDetailMetadata, toggleCurrentPageSelection, toggleFileSelection @@ -1364,6 +1432,11 @@ const detailInsightReasonLabels = computed(() => { return file.statusInsightReasons.map((reason) => t(`fileManage.insightReasons.${reason}`)) }) +const canModifySelectedViewPreset = computed(() => { + const preset = viewPresets.value.find((item) => item.id === selectedViewPresetId.value) + return Boolean(preset && !preset.isBuiltIn) +}) + const openDetailEditModal = () => { if (!selectedFileDetail.value) return @@ -1479,6 +1552,24 @@ const handleResetFilters = async () => { await syncHealthFilterQuery('all') } +const handleViewPresetChange = async (event: Event) => { + const target = event.target as HTMLSelectElement + if (!target.value) { + selectedViewPresetId.value = '' + return + } + + await applyViewPreset(target.value) + await syncHealthFilterQuery(params.value.health || 'all') +} + +const handleSaveViewPreset = async () => { + const name = window.prompt(t('fileManage.viewPresetNamePrompt')) + if (!name) return + + await saveCurrentViewPreset(name) +} + const batchEditModeOptions = computed< { value: AdminBatchEditMode; label: string; icon: Component }[] >(() => [ @@ -1626,6 +1717,7 @@ watch( onMounted(() => { params.value.health = getRouteHealthFilter() + void loadViewPresets() void loadFiles() })