feat: add admin file view summary
This commit is contained in:
@@ -22,6 +22,10 @@ import type {
|
|||||||
AdminFileStatusFilter,
|
AdminFileStatusFilter,
|
||||||
AdminFileSummary,
|
AdminFileSummary,
|
||||||
AdminFileTypeFilter,
|
AdminFileTypeFilter,
|
||||||
|
AdminFileViewSummary,
|
||||||
|
AdminFileViewSummaryItem,
|
||||||
|
AdminFileViewSummaryViewData,
|
||||||
|
AdminFileViewSummaryViewItem,
|
||||||
AdminFileViewPreset,
|
AdminFileViewPreset,
|
||||||
AdminFileViewPresetParams,
|
AdminFileViewPresetParams,
|
||||||
AdminFileViewPresetRequest,
|
AdminFileViewPresetRequest,
|
||||||
@@ -58,6 +62,18 @@ const emptySummary = (): AdminFileSummary => ({
|
|||||||
usedCount: 0
|
usedCount: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const emptyViewSummary = (): AdminFileViewSummaryViewData => ({
|
||||||
|
total: 0,
|
||||||
|
filteredTotal: 0,
|
||||||
|
allTotal: 0,
|
||||||
|
activeFilterCount: 0,
|
||||||
|
hasFilters: false,
|
||||||
|
strongestSeverity: 'success',
|
||||||
|
summary: emptySummary(),
|
||||||
|
cards: [],
|
||||||
|
actions: []
|
||||||
|
})
|
||||||
|
|
||||||
const normalizeCount = (value: number | string | null | undefined) => Number(value || 0)
|
const normalizeCount = (value: number | string | null | undefined) => Number(value || 0)
|
||||||
|
|
||||||
const isExpiredByDate = (value: string | null | undefined) => {
|
const isExpiredByDate = (value: string | null | undefined) => {
|
||||||
@@ -212,6 +228,7 @@ export function useAdminFiles() {
|
|||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const isSaving = ref(false)
|
const isSaving = ref(false)
|
||||||
const summary = ref<AdminFileSummary>(emptySummary())
|
const summary = ref<AdminFileSummary>(emptySummary())
|
||||||
|
const viewSummary = ref<AdminFileViewSummaryViewData>(emptyViewSummary())
|
||||||
const params = ref<AdminFileListParams & { total: number }>({
|
const params = ref<AdminFileListParams & { total: number }>({
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 10,
|
size: 10,
|
||||||
@@ -523,7 +540,7 @@ export function useAdminFiles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const normalizeInsightSeverity = (
|
const normalizeInsightSeverity = (
|
||||||
severity: AdminFileInsightSeverity | undefined
|
severity: AdminFileInsightSeverity | string | undefined
|
||||||
): AdminFileInsightSeverity => {
|
): AdminFileInsightSeverity => {
|
||||||
if (severity === 'success' || severity === 'warning' || severity === 'danger') {
|
if (severity === 'success' || severity === 'warning' || severity === 'danger') {
|
||||||
return severity
|
return severity
|
||||||
@@ -534,6 +551,131 @@ export function useAdminFiles() {
|
|||||||
return 'neutral'
|
return 'neutral'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeViewSummaryItems = (
|
||||||
|
items: AdminFileViewSummaryItem[] = []
|
||||||
|
): AdminFileViewSummaryViewItem[] =>
|
||||||
|
items
|
||||||
|
.filter((item) => item && item.key)
|
||||||
|
.map((item) => {
|
||||||
|
const actionTypeValue = item.actionType ?? item.action_type ?? 'filter'
|
||||||
|
const sourceKeyValue = item.sourceKey ?? item.source_key ?? item.key
|
||||||
|
const suggestedActionValue = item.suggestedAction ?? item.suggested_action ?? sourceKeyValue
|
||||||
|
const targetHealthValue = item.targetHealth ?? item.target_health ?? item.health ?? 'all'
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
count: normalizeCount(item.count),
|
||||||
|
priority: normalizeCount(item.priority),
|
||||||
|
severity: normalizeInsightSeverity(item.severity),
|
||||||
|
actionTypeValue,
|
||||||
|
sourceKeyValue,
|
||||||
|
suggestedActionValue,
|
||||||
|
targetHealthValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const buildFallbackViewSummary = (
|
||||||
|
files: AdminFileViewItem[],
|
||||||
|
total: number,
|
||||||
|
rawSummary?: Partial<AdminFileSummary>
|
||||||
|
): AdminFileViewSummaryViewData => {
|
||||||
|
const normalizedSummary = normalizeSummary(rawSummary, files, total)
|
||||||
|
const cards: AdminFileViewSummaryItem[] = [
|
||||||
|
{
|
||||||
|
key: 'storage_issue',
|
||||||
|
severity: 'danger',
|
||||||
|
priority: 100,
|
||||||
|
count: normalizedSummary.storageIssueCount,
|
||||||
|
targetHealth: 'storage_issue'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'expired',
|
||||||
|
severity: normalizedSummary.expiredCount >= 10 ? 'danger' : 'warning',
|
||||||
|
priority: 90,
|
||||||
|
count: normalizedSummary.expiredCount,
|
||||||
|
targetHealth: 'expired'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'expiring_soon',
|
||||||
|
severity: 'warning',
|
||||||
|
priority: 80,
|
||||||
|
count: normalizedSummary.expiringSoonCount,
|
||||||
|
targetHealth: 'expiring_soon'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'never_retrieved',
|
||||||
|
severity: 'neutral',
|
||||||
|
priority: 60,
|
||||||
|
count: normalizedSummary.neverRetrievedCount,
|
||||||
|
targetHealth: 'never_retrieved'
|
||||||
|
}
|
||||||
|
].filter((item) => item.count > 0)
|
||||||
|
|
||||||
|
if (cards.length === 0) {
|
||||||
|
cards.push({
|
||||||
|
key: normalizedSummary.totalFiles > 0 ? 'healthy' : 'empty',
|
||||||
|
severity: normalizedSummary.totalFiles > 0 ? 'success' : 'neutral',
|
||||||
|
priority: 10,
|
||||||
|
count: normalizedSummary.totalFiles,
|
||||||
|
targetHealth: normalizedSummary.totalFiles > 0 ? 'healthy' : 'all'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedCards = normalizeViewSummaryItems(cards)
|
||||||
|
return {
|
||||||
|
total,
|
||||||
|
filteredTotal: total,
|
||||||
|
allTotal: summary.value.totalFiles || total,
|
||||||
|
activeFilterCount: hasActiveFilters.value ? 1 : 0,
|
||||||
|
hasFilters: hasActiveFilters.value,
|
||||||
|
strongestSeverity: normalizedCards[0]?.severity ?? 'success',
|
||||||
|
summary: normalizedSummary,
|
||||||
|
cards: normalizedCards,
|
||||||
|
actions: normalizedCards.filter((item) => item.severity !== 'success')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeViewSummary = (
|
||||||
|
rawViewSummary: AdminFileViewSummary | undefined,
|
||||||
|
files: AdminFileViewItem[],
|
||||||
|
total: number,
|
||||||
|
rawSummary?: Partial<AdminFileSummary>
|
||||||
|
): AdminFileViewSummaryViewData => {
|
||||||
|
if (!rawViewSummary) {
|
||||||
|
return buildFallbackViewSummary(files, total, rawSummary)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cards = normalizeViewSummaryItems(rawViewSummary.cards ?? rawViewSummary.items ?? [])
|
||||||
|
const actions = normalizeViewSummaryItems(rawViewSummary.actions ?? [])
|
||||||
|
const rawSummarySource =
|
||||||
|
rawViewSummary.summary ??
|
||||||
|
rawViewSummary.healthSummary ??
|
||||||
|
rawViewSummary.health_summary ??
|
||||||
|
rawSummary
|
||||||
|
const filteredTotal = rawViewSummary.filteredTotal ?? rawViewSummary.filtered_total ?? total
|
||||||
|
const allTotal = rawViewSummary.allTotal ?? rawViewSummary.all_total ?? summary.value.totalFiles
|
||||||
|
const strongestSeverity =
|
||||||
|
rawViewSummary.strongestSeverity ??
|
||||||
|
rawViewSummary.strongest_severity ??
|
||||||
|
cards[0]?.severity ??
|
||||||
|
'success'
|
||||||
|
const normalizedSummary = normalizeSummary(rawSummarySource, files, total)
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: normalizeCount(rawViewSummary.total ?? total),
|
||||||
|
filteredTotal: normalizeCount(filteredTotal),
|
||||||
|
allTotal: normalizeCount(allTotal),
|
||||||
|
activeFilterCount: normalizeCount(
|
||||||
|
rawViewSummary.activeFilterCount ?? rawViewSummary.active_filter_count
|
||||||
|
),
|
||||||
|
hasFilters: Boolean(rawViewSummary.hasFilters ?? rawViewSummary.has_filters),
|
||||||
|
strongestSeverity: normalizeInsightSeverity(strongestSeverity),
|
||||||
|
summary: normalizedSummary,
|
||||||
|
cards: cards.length > 0 ? cards : buildFallbackViewSummary(files, total, rawSummary).cards,
|
||||||
|
actions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const createFileViewItem = (file: FileListItem): AdminFileViewItem => {
|
const createFileViewItem = (file: FileListItem): AdminFileViewItem => {
|
||||||
const isTextFile = inferIsText(file)
|
const isTextFile = inferIsText(file)
|
||||||
const isExpiredFile = inferIsExpired(file)
|
const isExpiredFile = inferIsExpired(file)
|
||||||
@@ -1100,6 +1242,17 @@ export function useAdminFiles() {
|
|||||||
tableData.value = res.detail.data.map(createFileViewItem)
|
tableData.value = res.detail.data.map(createFileViewItem)
|
||||||
params.value.total = res.detail.total
|
params.value.total = res.detail.total
|
||||||
summary.value = normalizeSummary(res.detail.summary, tableData.value, res.detail.total)
|
summary.value = normalizeSummary(res.detail.summary, tableData.value, res.detail.total)
|
||||||
|
viewSummary.value = normalizeViewSummary(
|
||||||
|
res.detail.viewSummary ??
|
||||||
|
res.detail.view_summary ??
|
||||||
|
res.detail.currentViewSummary ??
|
||||||
|
res.detail.current_view_summary ??
|
||||||
|
res.detail.actionSummary ??
|
||||||
|
res.detail.action_summary,
|
||||||
|
tableData.value,
|
||||||
|
res.detail.total,
|
||||||
|
res.detail.summary
|
||||||
|
)
|
||||||
syncSelectedFilesWithCurrentPage()
|
syncSelectedFilesWithCurrentPage()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
hasLoadError.value = true
|
hasLoadError.value = true
|
||||||
@@ -1667,6 +1820,7 @@ export function useAdminFiles() {
|
|||||||
selectedViewPresetId,
|
selectedViewPresetId,
|
||||||
storageUsedText,
|
storageUsedText,
|
||||||
summary,
|
summary,
|
||||||
|
viewSummary,
|
||||||
showBatchEditModal,
|
showBatchEditModal,
|
||||||
showEditModal,
|
showEditModal,
|
||||||
showFileDetailModal,
|
showFileDetailModal,
|
||||||
|
|||||||
@@ -576,6 +576,57 @@ export default {
|
|||||||
viewPresetDeleteSuccess: 'View deleted',
|
viewPresetDeleteSuccess: 'View deleted',
|
||||||
viewPresetDeleteFailed: 'Failed to delete view',
|
viewPresetDeleteFailed: 'Failed to delete view',
|
||||||
viewPresetLoadFailed: 'Failed to load saved views; using built-in views',
|
viewPresetLoadFailed: 'Failed to load saved views; using built-in views',
|
||||||
|
viewSummary: {
|
||||||
|
title: 'Current View Action Summary',
|
||||||
|
description:
|
||||||
|
'Groups risks and next actions from the current filters. Click an item to open its queue.',
|
||||||
|
filteredTotal: 'Current results',
|
||||||
|
allTotal: 'All records',
|
||||||
|
activeFilters: 'Filters',
|
||||||
|
action: 'View queue',
|
||||||
|
reset: 'Clear filters',
|
||||||
|
severity: {
|
||||||
|
success: 'Stable',
|
||||||
|
warning: 'Action needed',
|
||||||
|
danger: 'High risk',
|
||||||
|
info: 'Info',
|
||||||
|
neutral: 'Watch'
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
storage_issue: {
|
||||||
|
title: '{count} storage issues',
|
||||||
|
description:
|
||||||
|
'Review records missing download metadata and verify storage paths, file names, and settings.'
|
||||||
|
},
|
||||||
|
expired: {
|
||||||
|
title: '{count} expired items',
|
||||||
|
description:
|
||||||
|
'Handle unavailable shares by deleting stale records or extending files that should remain available.'
|
||||||
|
},
|
||||||
|
expiring_soon: {
|
||||||
|
title: '{count} expiring soon',
|
||||||
|
description:
|
||||||
|
'These shares expire within 24 hours. Extend them early or notify recipients.'
|
||||||
|
},
|
||||||
|
never_retrieved: {
|
||||||
|
title: '{count} never retrieved',
|
||||||
|
description:
|
||||||
|
'Review shares that have not been retrieved to identify ineffective uploads or follow-up gaps.'
|
||||||
|
},
|
||||||
|
permanent: {
|
||||||
|
title: '{count} permanent shares',
|
||||||
|
description: 'Review long-retention shares to prevent unbounded historical growth.'
|
||||||
|
},
|
||||||
|
healthy: {
|
||||||
|
title: 'Current view is stable',
|
||||||
|
description: 'No file in the current filters needs priority handling.'
|
||||||
|
},
|
||||||
|
empty: {
|
||||||
|
title: 'No results in this view',
|
||||||
|
description: 'Adjust or clear filters to inspect other file queues.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
all: 'All',
|
all: 'All',
|
||||||
active: 'Available',
|
active: 'Available',
|
||||||
expired: 'Expired',
|
expired: 'Expired',
|
||||||
|
|||||||
@@ -559,6 +559,52 @@ export default {
|
|||||||
viewPresetDeleteSuccess: '视图已删除',
|
viewPresetDeleteSuccess: '视图已删除',
|
||||||
viewPresetDeleteFailed: '视图删除失败',
|
viewPresetDeleteFailed: '视图删除失败',
|
||||||
viewPresetLoadFailed: '视图预设加载失败,已使用内置视图',
|
viewPresetLoadFailed: '视图预设加载失败,已使用内置视图',
|
||||||
|
viewSummary: {
|
||||||
|
title: '当前视图处置摘要',
|
||||||
|
description: '基于当前筛选结果聚合风险和下一步动作,点击可切换到对应队列。',
|
||||||
|
filteredTotal: '当前结果',
|
||||||
|
allTotal: '全部记录',
|
||||||
|
activeFilters: '筛选条件',
|
||||||
|
action: '查看队列',
|
||||||
|
reset: '清空筛选',
|
||||||
|
severity: {
|
||||||
|
success: '稳定',
|
||||||
|
warning: '需处理',
|
||||||
|
danger: '高风险',
|
||||||
|
info: '提示',
|
||||||
|
neutral: '观察'
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
storage_issue: {
|
||||||
|
title: '存储异常 {count} 项',
|
||||||
|
description: '优先检查缺少下载元数据的记录,确认存储路径、文件名和配置。'
|
||||||
|
},
|
||||||
|
expired: {
|
||||||
|
title: '已过期 {count} 项',
|
||||||
|
description: '集中处理已不可取件的分享,删除无效记录或续期仍需保留的文件。'
|
||||||
|
},
|
||||||
|
expiring_soon: {
|
||||||
|
title: '即将过期 {count} 项',
|
||||||
|
description: '这些分享将在 24 小时内失效,适合提前续期或提醒使用方。'
|
||||||
|
},
|
||||||
|
never_retrieved: {
|
||||||
|
title: '未取件 {count} 项',
|
||||||
|
description: '查看创建后尚未被取件的分享,用于识别无效上传或触达缺口。'
|
||||||
|
},
|
||||||
|
permanent: {
|
||||||
|
title: '永久有效 {count} 项',
|
||||||
|
description: '复核长期保留的分享,避免历史文件无限增长。'
|
||||||
|
},
|
||||||
|
healthy: {
|
||||||
|
title: '当前视图稳定',
|
||||||
|
description: '当前筛选结果未发现需要优先处理的文件。'
|
||||||
|
},
|
||||||
|
empty: {
|
||||||
|
title: '当前视图无结果',
|
||||||
|
description: '调整筛选条件或清空筛选,查看其他文件队列。'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
all: '全部',
|
all: '全部',
|
||||||
active: '可取件',
|
active: '可取件',
|
||||||
expired: '已过期',
|
expired: '已过期',
|
||||||
|
|||||||
@@ -102,6 +102,64 @@ export interface AdminFileListParams {
|
|||||||
sortOrder?: AdminFileSortOrder
|
sortOrder?: AdminFileSortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminFileViewSummaryItem {
|
||||||
|
key: string
|
||||||
|
severity: AdminFileInsightSeverity | string
|
||||||
|
priority?: number
|
||||||
|
count: number
|
||||||
|
actionType?: string
|
||||||
|
action_type?: string
|
||||||
|
health?: AdminFileHealthFilter | string
|
||||||
|
targetHealth?: AdminFileHealthFilter | string
|
||||||
|
target_health?: AdminFileHealthFilter | string
|
||||||
|
suggestedAction?: string
|
||||||
|
suggested_action?: string
|
||||||
|
sourceKey?: string
|
||||||
|
source_key?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminFileViewSummary {
|
||||||
|
total?: number
|
||||||
|
filteredTotal?: number
|
||||||
|
filtered_total?: number
|
||||||
|
allTotal?: number
|
||||||
|
all_total?: number
|
||||||
|
activeFilterCount?: number
|
||||||
|
active_filter_count?: number
|
||||||
|
hasFilters?: boolean
|
||||||
|
has_filters?: boolean
|
||||||
|
strongestSeverity?: AdminFileInsightSeverity | string
|
||||||
|
strongest_severity?: AdminFileInsightSeverity | string
|
||||||
|
filters?: Record<string, unknown>
|
||||||
|
summary?: Partial<AdminFileSummary>
|
||||||
|
healthSummary?: Partial<AdminFileSummary>
|
||||||
|
health_summary?: Partial<AdminFileSummary>
|
||||||
|
cards?: AdminFileViewSummaryItem[]
|
||||||
|
items?: AdminFileViewSummaryItem[]
|
||||||
|
actions?: AdminFileViewSummaryItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminFileViewSummaryViewItem extends AdminFileViewSummaryItem {
|
||||||
|
severity: AdminFileInsightSeverity
|
||||||
|
priority: number
|
||||||
|
actionTypeValue: string
|
||||||
|
sourceKeyValue: string
|
||||||
|
suggestedActionValue: string
|
||||||
|
targetHealthValue: AdminFileHealthFilter | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminFileViewSummaryViewData {
|
||||||
|
total: number
|
||||||
|
filteredTotal: number
|
||||||
|
allTotal: number
|
||||||
|
activeFilterCount: number
|
||||||
|
hasFilters: boolean
|
||||||
|
strongestSeverity: AdminFileInsightSeverity
|
||||||
|
summary: AdminFileSummary
|
||||||
|
cards: AdminFileViewSummaryViewItem[]
|
||||||
|
actions: AdminFileViewSummaryViewItem[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminFileViewPresetParams {
|
export interface AdminFileViewPresetParams {
|
||||||
keyword: string
|
keyword: string
|
||||||
status: AdminFileStatusFilter
|
status: AdminFileStatusFilter
|
||||||
@@ -189,6 +247,12 @@ export interface FileListResponse {
|
|||||||
page: number
|
page: number
|
||||||
size: number
|
size: number
|
||||||
summary?: AdminFileSummary
|
summary?: AdminFileSummary
|
||||||
|
viewSummary?: AdminFileViewSummary
|
||||||
|
view_summary?: AdminFileViewSummary
|
||||||
|
currentViewSummary?: AdminFileViewSummary
|
||||||
|
current_view_summary?: AdminFileViewSummary
|
||||||
|
actionSummary?: AdminFileViewSummary
|
||||||
|
action_summary?: AdminFileViewSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminFilePreviewResponse {
|
export interface AdminFilePreviewResponse {
|
||||||
|
|||||||
@@ -51,6 +51,83 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section class="mb-6 rounded-lg border p-4" :class="[panelClass]">
|
||||||
|
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<div
|
||||||
|
class="rounded-lg p-2"
|
||||||
|
:class="isDarkMode ? 'bg-blue-500/10 text-blue-300' : 'bg-blue-50 text-blue-600'"
|
||||||
|
>
|
||||||
|
<ShieldCheckIcon class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<h3 class="text-base font-semibold" :class="[primaryTextClass]">
|
||||||
|
{{ t('fileManage.viewSummary.title') }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 text-sm" :class="[mutedTextClass]">
|
||||||
|
{{ t('fileManage.viewSummary.description') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-3 gap-2 sm:min-w-[360px]">
|
||||||
|
<div
|
||||||
|
v-for="metric in viewSummaryMetrics"
|
||||||
|
:key="metric.label"
|
||||||
|
class="rounded-lg border px-3 py-2"
|
||||||
|
:class="[subtleSectionClass]"
|
||||||
|
>
|
||||||
|
<p class="text-xs" :class="[mutedTextClass]">{{ metric.label }}</p>
|
||||||
|
<p class="mt-1 text-lg font-semibold" :class="[primaryTextClass]">
|
||||||
|
{{ metric.value }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 grid grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-4">
|
||||||
|
<button
|
||||||
|
v-for="item in viewSummary.cards"
|
||||||
|
:key="item.key"
|
||||||
|
type="button"
|
||||||
|
class="group flex min-h-28 items-start gap-3 rounded-lg border px-4 py-3 text-left transition-colors"
|
||||||
|
:class="getViewSummaryItemClass(item)"
|
||||||
|
@click="openViewSummaryItem(item)"
|
||||||
|
>
|
||||||
|
<span class="rounded-lg p-2" :class="getViewSummaryIconClass(item)">
|
||||||
|
<component :is="getViewSummaryIcon(item)" class="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
<span class="min-w-0 flex-1">
|
||||||
|
<span class="flex items-start justify-between gap-2">
|
||||||
|
<span class="text-sm font-semibold" :class="[primaryTextClass]">
|
||||||
|
{{ getViewSummaryTitle(item) }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="shrink-0 rounded-full px-2 py-0.5 text-[11px] font-medium"
|
||||||
|
:class="getInsightBadgeClass(item.severity)"
|
||||||
|
>
|
||||||
|
{{ getViewSummarySeverityLabel(item) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="mt-1 block text-xs leading-5" :class="[mutedTextClass]">
|
||||||
|
{{ getViewSummaryDescription(item) }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="mt-3 inline-flex items-center text-xs font-medium"
|
||||||
|
:class="[isDarkMode ? 'text-blue-300' : 'text-blue-600']"
|
||||||
|
>
|
||||||
|
{{ getViewSummaryActionLabel(item) }}
|
||||||
|
<ChevronRightIcon
|
||||||
|
class="ml-1 h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="mb-6 rounded-lg border p-4" :class="[panelClass]">
|
<section class="mb-6 rounded-lg border p-4" :class="[panelClass]">
|
||||||
<div class="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
|
<div class="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
|
||||||
<div class="flex min-w-0 flex-1 flex-col gap-3 md:flex-row">
|
<div class="flex min-w-0 flex-1 flex-col gap-3 md:flex-row">
|
||||||
@@ -1173,12 +1250,16 @@ import type {
|
|||||||
AdminFileSortOrder,
|
AdminFileSortOrder,
|
||||||
AdminFileStatusFilter,
|
AdminFileStatusFilter,
|
||||||
AdminFileTypeFilter,
|
AdminFileTypeFilter,
|
||||||
|
AdminFileViewSummaryViewItem,
|
||||||
AdminFileViewItem
|
AdminFileViewItem
|
||||||
} from '@/types'
|
} from '@/types'
|
||||||
import {
|
import {
|
||||||
ActivityIcon,
|
ActivityIcon,
|
||||||
|
AlertTriangleIcon,
|
||||||
ArchiveIcon,
|
ArchiveIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
@@ -1192,6 +1273,7 @@ import {
|
|||||||
RefreshCwIcon,
|
RefreshCwIcon,
|
||||||
RotateCcwIcon,
|
RotateCcwIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
|
ShieldCheckIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
XIcon
|
XIcon
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
@@ -1255,6 +1337,7 @@ const {
|
|||||||
selectedViewPresetId,
|
selectedViewPresetId,
|
||||||
storageUsedText,
|
storageUsedText,
|
||||||
summary,
|
summary,
|
||||||
|
viewSummary,
|
||||||
showBatchEditModal,
|
showBatchEditModal,
|
||||||
showEditModal,
|
showEditModal,
|
||||||
showFileDetailModal,
|
showFileDetailModal,
|
||||||
@@ -1484,6 +1567,21 @@ const summaryCards = computed(() => [
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const viewSummaryMetrics = computed(() => [
|
||||||
|
{
|
||||||
|
label: t('fileManage.viewSummary.filteredTotal'),
|
||||||
|
value: viewSummary.value.filteredTotal
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('fileManage.viewSummary.allTotal'),
|
||||||
|
value: viewSummary.value.allTotal
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('fileManage.viewSummary.activeFilters'),
|
||||||
|
value: viewSummary.value.activeFilterCount
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
const statusFilterOptions = computed<{ value: AdminFileStatusFilter; label: string }[]>(() => [
|
const statusFilterOptions = computed<{ value: AdminFileStatusFilter; label: string }[]>(() => [
|
||||||
{ value: 'all', label: t('fileManage.all') },
|
{ value: 'all', label: t('fileManage.all') },
|
||||||
{ value: 'active', label: t('fileManage.active') },
|
{ value: 'active', label: t('fileManage.active') },
|
||||||
@@ -1524,10 +1622,78 @@ const healthFilterValues: AdminFileHealthFilter[] = [
|
|||||||
'permanent'
|
'permanent'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const isHealthFilterValue = (value: string): value is AdminFileHealthFilter =>
|
||||||
|
healthFilterValues.includes(value as AdminFileHealthFilter)
|
||||||
|
|
||||||
|
const getViewSummaryItemKey = (item: AdminFileViewSummaryViewItem) =>
|
||||||
|
item.sourceKeyValue || item.key
|
||||||
|
|
||||||
|
const getViewSummaryTitle = (item: AdminFileViewSummaryViewItem) =>
|
||||||
|
t(`fileManage.viewSummary.items.${getViewSummaryItemKey(item)}.title`, {
|
||||||
|
count: item.count
|
||||||
|
})
|
||||||
|
|
||||||
|
const getViewSummaryDescription = (item: AdminFileViewSummaryViewItem) =>
|
||||||
|
t(`fileManage.viewSummary.items.${getViewSummaryItemKey(item)}.description`, {
|
||||||
|
count: item.count
|
||||||
|
})
|
||||||
|
|
||||||
|
const getViewSummaryActionLabel = (item: AdminFileViewSummaryViewItem) =>
|
||||||
|
item.targetHealthValue === 'all'
|
||||||
|
? t('fileManage.viewSummary.reset')
|
||||||
|
: t('fileManage.viewSummary.action')
|
||||||
|
|
||||||
|
const getViewSummarySeverityLabel = (item: AdminFileViewSummaryViewItem) =>
|
||||||
|
t(`fileManage.viewSummary.severity.${item.severity}`)
|
||||||
|
|
||||||
|
const getViewSummaryIcon = (item: AdminFileViewSummaryViewItem) => {
|
||||||
|
if (item.severity === 'success') return CheckCircleIcon
|
||||||
|
if (item.severity === 'danger' || item.severity === 'warning') return AlertTriangleIcon
|
||||||
|
return ShieldCheckIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
const getViewSummaryItemClass = (item: AdminFileViewSummaryViewItem) => {
|
||||||
|
const darkClasses: Record<AdminFileInsightSeverity, string> = {
|
||||||
|
success: 'border-emerald-500/20 bg-emerald-500/10 hover:border-emerald-500/40',
|
||||||
|
warning: 'border-amber-500/20 bg-amber-500/10 hover:border-amber-500/40',
|
||||||
|
danger: 'border-red-500/20 bg-red-500/10 hover:border-red-500/40',
|
||||||
|
info: 'border-blue-500/20 bg-blue-500/10 hover:border-blue-500/40',
|
||||||
|
neutral: 'border-gray-700 bg-gray-700/30 hover:border-gray-600'
|
||||||
|
}
|
||||||
|
const lightClasses: Record<AdminFileInsightSeverity, string> = {
|
||||||
|
success: 'border-emerald-100 bg-emerald-50 hover:border-emerald-200',
|
||||||
|
warning: 'border-amber-100 bg-amber-50 hover:border-amber-200',
|
||||||
|
danger: 'border-red-100 bg-red-50 hover:border-red-200',
|
||||||
|
info: 'border-blue-100 bg-blue-50 hover:border-blue-200',
|
||||||
|
neutral: 'border-gray-200 bg-gray-50 hover:border-gray-300'
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDarkMode.value ? darkClasses[item.severity] : lightClasses[item.severity]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getViewSummaryIconClass = (item: AdminFileViewSummaryViewItem) => {
|
||||||
|
const darkClasses: Record<AdminFileInsightSeverity, string> = {
|
||||||
|
success: 'bg-emerald-900/40 text-emerald-200',
|
||||||
|
warning: 'bg-amber-900/40 text-amber-200',
|
||||||
|
danger: 'bg-red-900/40 text-red-200',
|
||||||
|
info: 'bg-blue-900/40 text-blue-200',
|
||||||
|
neutral: 'bg-gray-800 text-gray-300'
|
||||||
|
}
|
||||||
|
const lightClasses: Record<AdminFileInsightSeverity, string> = {
|
||||||
|
success: 'bg-emerald-100 text-emerald-700',
|
||||||
|
warning: 'bg-amber-100 text-amber-700',
|
||||||
|
danger: 'bg-red-100 text-red-700',
|
||||||
|
info: 'bg-blue-100 text-blue-700',
|
||||||
|
neutral: 'bg-gray-100 text-gray-700'
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDarkMode.value ? darkClasses[item.severity] : lightClasses[item.severity]
|
||||||
|
}
|
||||||
|
|
||||||
const getRouteHealthFilter = (): AdminFileHealthFilter => {
|
const getRouteHealthFilter = (): AdminFileHealthFilter => {
|
||||||
const health = route.query.health
|
const health = route.query.health
|
||||||
if (typeof health === 'string' && healthFilterValues.includes(health as AdminFileHealthFilter)) {
|
if (typeof health === 'string' && isHealthFilterValue(health)) {
|
||||||
return health as AdminFileHealthFilter
|
return health
|
||||||
}
|
}
|
||||||
return 'all'
|
return 'all'
|
||||||
}
|
}
|
||||||
@@ -1552,6 +1718,16 @@ const handleResetFilters = async () => {
|
|||||||
await syncHealthFilterQuery('all')
|
await syncHealthFilterQuery('all')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openViewSummaryItem = async (item: AdminFileViewSummaryViewItem) => {
|
||||||
|
const health = String(item.targetHealthValue || 'all')
|
||||||
|
if (health !== 'all' && isHealthFilterValue(health)) {
|
||||||
|
await handleHealthFilterChange(health)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleResetFilters()
|
||||||
|
}
|
||||||
|
|
||||||
const handleViewPresetChange = async (event: Event) => {
|
const handleViewPresetChange = async (event: Event) => {
|
||||||
const target = event.target as HTMLSelectElement
|
const target = event.target as HTMLSelectElement
|
||||||
if (!target.value) {
|
if (!target.value) {
|
||||||
|
|||||||
Reference in New Issue
Block a user