feat: show admin file detail lifecycle
This commit is contained in:
@@ -8,7 +8,9 @@ import type {
|
|||||||
AdminBatchUpdateFilesRequest,
|
AdminBatchUpdateFilesRequest,
|
||||||
AdminBatchUpdateFilesResponse,
|
AdminBatchUpdateFilesResponse,
|
||||||
AdminFileDetailResponse,
|
AdminFileDetailResponse,
|
||||||
|
AdminFileDetailTimelineItem,
|
||||||
AdminFileDetailViewItem,
|
AdminFileDetailViewItem,
|
||||||
|
AdminFileInsightSeverity,
|
||||||
AdminFilePatchPayload,
|
AdminFilePatchPayload,
|
||||||
AdminFileListParams,
|
AdminFileListParams,
|
||||||
AdminFileStatusFilter,
|
AdminFileStatusFilter,
|
||||||
@@ -20,7 +22,7 @@ import type {
|
|||||||
FileListItem
|
FileListItem
|
||||||
} from '@/types'
|
} from '@/types'
|
||||||
import { copyToClipboard } from '@/utils/clipboard'
|
import { copyToClipboard } from '@/utils/clipboard'
|
||||||
import { formatFileSize, formatTimestamp, getErrorMessage } from '@/utils/common'
|
import { formatDuration, formatFileSize, formatTimestamp, getErrorMessage } from '@/utils/common'
|
||||||
import {
|
import {
|
||||||
downloadAdminManagedFile,
|
downloadAdminManagedFile,
|
||||||
exportAdminTextFile,
|
exportAdminTextFile,
|
||||||
@@ -215,6 +217,61 @@ export function useAdminFiles() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeInsightSeverity = (
|
||||||
|
severity: AdminFileInsightSeverity | undefined
|
||||||
|
): AdminFileInsightSeverity => {
|
||||||
|
if (severity === 'success' || severity === 'warning' || severity === 'danger') {
|
||||||
|
return severity
|
||||||
|
}
|
||||||
|
if (severity === 'info' || severity === 'neutral') {
|
||||||
|
return severity
|
||||||
|
}
|
||||||
|
return 'neutral'
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTimelineValue = (item: AdminFileDetailTimelineItem) => {
|
||||||
|
if (item.key === 'download_limit') {
|
||||||
|
if (item.status === 'unlimited') return t('fileManage.unlimited')
|
||||||
|
if (typeof item.value === 'number') return t('fileManage.remaining', { count: item.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.key === 'retrieved' && typeof item.value === 'number') {
|
||||||
|
return `${item.value} ${t('common.times')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.key === 'expiration_policy') {
|
||||||
|
if (item.status === 'unlimited') return t('send.expiration.units.forever')
|
||||||
|
if (typeof item.value === 'number') {
|
||||||
|
const duration = formatDuration(Math.abs(item.value), (key) => t(key))
|
||||||
|
return item.value <= 0
|
||||||
|
? t('fileManage.timeline.expiration_policy.overdue', { time: duration })
|
||||||
|
: t('fileManage.timeline.expiration_policy.remaining', { time: duration })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.detail) return item.detail
|
||||||
|
if (item.value !== null && item.value !== undefined) return String(item.value)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const createTimelineViewItems = (items: AdminFileDetailTimelineItem[] = []) =>
|
||||||
|
items.map((item) => {
|
||||||
|
const timestampText = item.timestamp ? formatTimestamp(item.timestamp) : ''
|
||||||
|
const valueText = formatTimelineValue(item)
|
||||||
|
const metaParts = [timestampText, valueText].filter(Boolean)
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
severity: normalizeInsightSeverity(item.severity),
|
||||||
|
displayTitle: t(`fileManage.timeline.${item.key}.title`),
|
||||||
|
displayDescription: t(`fileManage.timeline.${item.key}.description`, {
|
||||||
|
status: t(`fileManage.timeline.status.${item.status || 'pending'}`),
|
||||||
|
detail: item.detail || '',
|
||||||
|
value: valueText || ''
|
||||||
|
}),
|
||||||
|
displayMeta: metaParts.length > 0 ? metaParts.join(' · ') : '-'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const createFileDetailViewItem = (
|
const createFileDetailViewItem = (
|
||||||
file: FileListItem | AdminFileDetailResponse
|
file: FileListItem | AdminFileDetailResponse
|
||||||
): AdminFileDetailViewItem => {
|
): AdminFileDetailViewItem => {
|
||||||
@@ -260,6 +317,12 @@ export function useAdminFiles() {
|
|||||||
const storageBackendValue =
|
const storageBackendValue =
|
||||||
storage?.backend ?? detail.storageBackend ?? detail.storage_backend ?? '-'
|
storage?.backend ?? detail.storageBackend ?? detail.storage_backend ?? '-'
|
||||||
const isChunkedStorage = storage?.isChunked ?? storage?.is_chunked ?? viewItem.isChunkedFile
|
const isChunkedStorage = storage?.isChunked ?? storage?.is_chunked ?? viewItem.isChunkedFile
|
||||||
|
const statusInsights = detail.statusInsights ?? detail.status_insights
|
||||||
|
const statusInsightState =
|
||||||
|
statusInsights?.state ??
|
||||||
|
(viewItem.isExpiredFile ? 'expired' : isPermanentFile ? 'permanent' : 'available')
|
||||||
|
const statusInsightNextAction =
|
||||||
|
statusInsights?.nextAction ?? statusInsights?.next_action ?? 'monitor'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...viewItem,
|
...viewItem,
|
||||||
@@ -283,7 +346,13 @@ export function useAdminFiles() {
|
|||||||
storage?.uuid_file_name ??
|
storage?.uuid_file_name ??
|
||||||
detail.uuidFileName ??
|
detail.uuidFileName ??
|
||||||
detail.uuid_file_name,
|
detail.uuid_file_name,
|
||||||
uploadIdValue: storage?.uploadId ?? storage?.upload_id ?? detail.uploadId ?? detail.upload_id
|
uploadIdValue: storage?.uploadId ?? storage?.upload_id ?? detail.uploadId ?? detail.upload_id,
|
||||||
|
statusInsightSeverity: normalizeInsightSeverity(statusInsights?.severity),
|
||||||
|
statusInsightState,
|
||||||
|
statusInsightNextAction,
|
||||||
|
statusInsightReasons: statusInsights?.reasons || [],
|
||||||
|
statusInsightMetrics: statusInsights?.metrics,
|
||||||
|
detailTimeline: createTimelineViewItems(detail.timeline || [])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -452,6 +452,9 @@ export default {
|
|||||||
overview: 'Overview',
|
overview: 'Overview',
|
||||||
policyInfo: 'Policy',
|
policyInfo: 'Policy',
|
||||||
storageInfo: 'Storage',
|
storageInfo: 'Storage',
|
||||||
|
statusInsight: 'Status Insight',
|
||||||
|
nextAction: 'Next Action',
|
||||||
|
lifecycle: 'Lifecycle',
|
||||||
permanent: 'Permanent',
|
permanent: 'Permanent',
|
||||||
createdAt: 'Created Time',
|
createdAt: 'Created Time',
|
||||||
remainingDownloads: 'Remaining Downloads',
|
remainingDownloads: 'Remaining Downloads',
|
||||||
@@ -468,6 +471,70 @@ export default {
|
|||||||
uploadId: 'Upload Session',
|
uploadId: 'Upload Session',
|
||||||
yes: 'Yes',
|
yes: 'Yes',
|
||||||
no: 'No',
|
no: 'No',
|
||||||
|
insightSeverity: {
|
||||||
|
success: 'Healthy',
|
||||||
|
warning: 'Needs Attention',
|
||||||
|
danger: 'Issue',
|
||||||
|
info: 'Info',
|
||||||
|
neutral: 'Watch'
|
||||||
|
},
|
||||||
|
insightStates: {
|
||||||
|
available: 'Available for retrieval',
|
||||||
|
expired: 'Unavailable for retrieval',
|
||||||
|
storage_incomplete: 'Storage metadata incomplete',
|
||||||
|
expiring_soon: 'Expiring soon',
|
||||||
|
permanent: 'Permanent'
|
||||||
|
},
|
||||||
|
insightActions: {
|
||||||
|
monitor: 'Keep monitoring',
|
||||||
|
extend_or_delete: 'Extend retention or clean up',
|
||||||
|
inspect_storage: 'Inspect storage path and file name',
|
||||||
|
extend_expiration: 'Extend expiration if needed'
|
||||||
|
},
|
||||||
|
insightReasons: {
|
||||||
|
expired: 'Past expiration time',
|
||||||
|
download_limit_exhausted: 'Retrieval limit exhausted',
|
||||||
|
expires_soon: 'Expires within 24 hours',
|
||||||
|
never_retrieved: 'Never retrieved',
|
||||||
|
storage_metadata_incomplete: 'Missing downloadable storage metadata',
|
||||||
|
chunked_upload: 'Created by chunked upload'
|
||||||
|
},
|
||||||
|
timeline: {
|
||||||
|
status: {
|
||||||
|
done: 'Done',
|
||||||
|
pending: 'Pending',
|
||||||
|
active: 'Active',
|
||||||
|
expired: 'Expired',
|
||||||
|
exhausted: 'Exhausted',
|
||||||
|
unlimited: 'Unlimited'
|
||||||
|
},
|
||||||
|
created: {
|
||||||
|
title: 'Record Created',
|
||||||
|
description: 'The share record has been created and is currently {status}.'
|
||||||
|
},
|
||||||
|
content_ready: {
|
||||||
|
title: 'Content Ready',
|
||||||
|
description: 'Content type is {detail}; current status is {status}.'
|
||||||
|
},
|
||||||
|
upload_session: {
|
||||||
|
title: 'Upload Session',
|
||||||
|
description: 'Upload session recorded: {detail}.'
|
||||||
|
},
|
||||||
|
expiration_policy: {
|
||||||
|
title: 'Expiration Policy',
|
||||||
|
description: 'Expiration policy is currently {status}.',
|
||||||
|
remaining: '{time} left',
|
||||||
|
overdue: '{time} overdue'
|
||||||
|
},
|
||||||
|
download_limit: {
|
||||||
|
title: 'Retrieval Limit',
|
||||||
|
description: 'Retrieval limit is currently {status}; {value}.'
|
||||||
|
},
|
||||||
|
retrieved: {
|
||||||
|
title: 'Retrieval History',
|
||||||
|
description: 'Retrieved {value}; current status is {status}.'
|
||||||
|
}
|
||||||
|
},
|
||||||
headers: {
|
headers: {
|
||||||
select: 'Select',
|
select: 'Select',
|
||||||
code: 'Retrieve Code',
|
code: 'Retrieve Code',
|
||||||
|
|||||||
@@ -450,6 +450,9 @@ export default {
|
|||||||
overview: '概览',
|
overview: '概览',
|
||||||
policyInfo: '策略信息',
|
policyInfo: '策略信息',
|
||||||
storageInfo: '存储信息',
|
storageInfo: '存储信息',
|
||||||
|
statusInsight: '状态洞察',
|
||||||
|
nextAction: '建议动作',
|
||||||
|
lifecycle: '生命周期',
|
||||||
permanent: '永久有效',
|
permanent: '永久有效',
|
||||||
createdAt: '创建时间',
|
createdAt: '创建时间',
|
||||||
remainingDownloads: '剩余次数',
|
remainingDownloads: '剩余次数',
|
||||||
@@ -466,6 +469,70 @@ export default {
|
|||||||
uploadId: '上传会话',
|
uploadId: '上传会话',
|
||||||
yes: '是',
|
yes: '是',
|
||||||
no: '否',
|
no: '否',
|
||||||
|
insightSeverity: {
|
||||||
|
success: '正常',
|
||||||
|
warning: '需关注',
|
||||||
|
danger: '异常',
|
||||||
|
info: '提示',
|
||||||
|
neutral: '待观察'
|
||||||
|
},
|
||||||
|
insightStates: {
|
||||||
|
available: '当前可正常取件',
|
||||||
|
expired: '当前已不可取件',
|
||||||
|
storage_incomplete: '存储信息不完整',
|
||||||
|
expiring_soon: '即将过期',
|
||||||
|
permanent: '永久有效'
|
||||||
|
},
|
||||||
|
insightActions: {
|
||||||
|
monitor: '保持观察',
|
||||||
|
extend_or_delete: '延长有效期或清理记录',
|
||||||
|
inspect_storage: '检查存储路径与文件名',
|
||||||
|
extend_expiration: '按需延长过期时间'
|
||||||
|
},
|
||||||
|
insightReasons: {
|
||||||
|
expired: '已超过过期时间',
|
||||||
|
download_limit_exhausted: '取件次数已用尽',
|
||||||
|
expires_soon: '24 小时内过期',
|
||||||
|
never_retrieved: '尚未被取件',
|
||||||
|
storage_metadata_incomplete: '缺少可下载存储信息',
|
||||||
|
chunked_upload: '由分片上传生成'
|
||||||
|
},
|
||||||
|
timeline: {
|
||||||
|
status: {
|
||||||
|
done: '已完成',
|
||||||
|
pending: '等待中',
|
||||||
|
active: '生效中',
|
||||||
|
expired: '已过期',
|
||||||
|
exhausted: '已用尽',
|
||||||
|
unlimited: '不限'
|
||||||
|
},
|
||||||
|
created: {
|
||||||
|
title: '记录创建',
|
||||||
|
description: '分享记录已创建,当前状态为 {status}。'
|
||||||
|
},
|
||||||
|
content_ready: {
|
||||||
|
title: '内容就绪',
|
||||||
|
description: '内容类型为 {detail},当前状态为 {status}。'
|
||||||
|
},
|
||||||
|
upload_session: {
|
||||||
|
title: '上传会话',
|
||||||
|
description: '上传会话已记录:{detail}。'
|
||||||
|
},
|
||||||
|
expiration_policy: {
|
||||||
|
title: '过期策略',
|
||||||
|
description: '过期策略当前为 {status}。',
|
||||||
|
remaining: '还剩 {time}',
|
||||||
|
overdue: '已超时 {time}'
|
||||||
|
},
|
||||||
|
download_limit: {
|
||||||
|
title: '取件次数',
|
||||||
|
description: '取件次数策略当前为 {status},{value}。'
|
||||||
|
},
|
||||||
|
retrieved: {
|
||||||
|
title: '取件记录',
|
||||||
|
description: '累计取件 {value},当前状态为 {status}。'
|
||||||
|
}
|
||||||
|
},
|
||||||
headers: {
|
headers: {
|
||||||
select: '选择',
|
select: '选择',
|
||||||
code: '取件码',
|
code: '取件码',
|
||||||
|
|||||||
@@ -145,6 +145,44 @@ export interface AdminFileDetailStorage {
|
|||||||
upload_id?: string | null
|
upload_id?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AdminFileInsightSeverity = 'success' | 'warning' | 'danger' | 'info' | 'neutral'
|
||||||
|
|
||||||
|
export interface AdminFileDetailInsightMetrics {
|
||||||
|
ageSeconds?: number
|
||||||
|
age_seconds?: number
|
||||||
|
secondsUntilExpiration?: number | null
|
||||||
|
seconds_until_expiration?: number | null
|
||||||
|
remainingDownloads?: number | null
|
||||||
|
remaining_downloads?: number | null
|
||||||
|
usedCount?: number
|
||||||
|
used_count?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminFileDetailStatusInsights {
|
||||||
|
severity?: AdminFileInsightSeverity
|
||||||
|
state?: string
|
||||||
|
nextAction?: string
|
||||||
|
next_action?: string
|
||||||
|
reasons?: string[]
|
||||||
|
metrics?: AdminFileDetailInsightMetrics
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminFileDetailTimelineItem {
|
||||||
|
key: string
|
||||||
|
status?: string
|
||||||
|
severity?: AdminFileInsightSeverity
|
||||||
|
timestamp?: string | null
|
||||||
|
value?: number | string | null
|
||||||
|
detail?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminFileDetailTimelineViewItem extends AdminFileDetailTimelineItem {
|
||||||
|
severity: AdminFileInsightSeverity
|
||||||
|
displayTitle: string
|
||||||
|
displayDescription: string
|
||||||
|
displayMeta: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminFileDetailResponse extends FileListItem {
|
export interface AdminFileDetailResponse extends FileListItem {
|
||||||
filename?: string
|
filename?: string
|
||||||
displayName?: string
|
displayName?: string
|
||||||
@@ -171,6 +209,9 @@ export interface AdminFileDetailResponse extends FileListItem {
|
|||||||
upload_id?: string | null
|
upload_id?: string | null
|
||||||
policy?: AdminFileDetailPolicy
|
policy?: AdminFileDetailPolicy
|
||||||
storage?: AdminFileDetailStorage
|
storage?: AdminFileDetailStorage
|
||||||
|
statusInsights?: AdminFileDetailStatusInsights
|
||||||
|
status_insights?: AdminFileDetailStatusInsights
|
||||||
|
timeline?: AdminFileDetailTimelineItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminFileDetailViewItem extends AdminFileViewItem {
|
export interface AdminFileDetailViewItem extends AdminFileViewItem {
|
||||||
@@ -187,6 +228,12 @@ export interface AdminFileDetailViewItem extends AdminFileViewItem {
|
|||||||
filePathValue?: string | null
|
filePathValue?: string | null
|
||||||
uuidFileNameValue?: string | null
|
uuidFileNameValue?: string | null
|
||||||
uploadIdValue?: string | null
|
uploadIdValue?: string | null
|
||||||
|
statusInsightSeverity: AdminFileInsightSeverity
|
||||||
|
statusInsightState: string
|
||||||
|
statusInsightNextAction: string
|
||||||
|
statusInsightReasons: string[]
|
||||||
|
statusInsightMetrics?: AdminFileDetailInsightMetrics
|
||||||
|
detailTimeline: AdminFileDetailTimelineViewItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminBatchDeleteFileFailure {
|
export interface AdminBatchDeleteFileFailure {
|
||||||
|
|||||||
@@ -585,6 +585,43 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="rounded-lg border px-4 py-4"
|
||||||
|
:class="getInsightPanelClass(selectedFileDetail.statusInsightSeverity)"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="text-xs font-medium uppercase" :class="[mutedTextClass]">
|
||||||
|
{{ t('fileManage.statusInsight') }}
|
||||||
|
</p>
|
||||||
|
<h4 class="mt-1 text-base font-semibold" :class="[primaryTextClass]">
|
||||||
|
{{ getInsightStateLabel(selectedFileDetail.statusInsightState) }}
|
||||||
|
</h4>
|
||||||
|
<p class="mt-1 text-sm" :class="[mutedTextClass]">
|
||||||
|
{{ t('fileManage.nextAction') }}:
|
||||||
|
{{ getInsightActionLabel(selectedFileDetail.statusInsightNextAction) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="inline-flex w-fit items-center rounded-full px-2.5 py-1 text-xs font-medium"
|
||||||
|
:class="getInsightBadgeClass(selectedFileDetail.statusInsightSeverity)"
|
||||||
|
>
|
||||||
|
{{ getInsightSeverityLabel(selectedFileDetail.statusInsightSeverity) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="detailInsightReasonLabels.length > 0" class="mt-3 flex flex-wrap gap-2">
|
||||||
|
<span
|
||||||
|
v-for="reason in detailInsightReasonLabels"
|
||||||
|
:key="reason"
|
||||||
|
class="rounded-full px-2.5 py-1 text-xs font-medium"
|
||||||
|
:class="isDarkMode ? 'bg-gray-700 text-gray-300' : 'bg-white text-gray-600 shadow-sm'"
|
||||||
|
>
|
||||||
|
{{ reason }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="space-y-3">
|
<section class="space-y-3">
|
||||||
<h4 class="text-sm font-semibold" :class="[primaryTextClass]">
|
<h4 class="text-sm font-semibold" :class="[primaryTextClass]">
|
||||||
{{ t('fileManage.overview') }}
|
{{ t('fileManage.overview') }}
|
||||||
@@ -623,6 +660,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section v-if="selectedFileDetail.detailTimeline.length > 0" class="space-y-3">
|
||||||
|
<h4 class="text-sm font-semibold" :class="[primaryTextClass]">
|
||||||
|
{{ t('fileManage.lifecycle') }}
|
||||||
|
</h4>
|
||||||
|
<div
|
||||||
|
class="rounded-lg border px-4 py-4"
|
||||||
|
:class="[isDarkMode ? 'border-gray-700 bg-gray-700/30' : 'border-gray-200 bg-white']"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in selectedFileDetail.detailTimeline"
|
||||||
|
:key="`${item.key}-${index}`"
|
||||||
|
class="grid grid-cols-[auto_minmax(0,1fr)] gap-3"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<span
|
||||||
|
class="mt-1 h-3 w-3 rounded-full"
|
||||||
|
:class="getTimelineDotClass(item.severity)"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="index < selectedFileDetail.detailTimeline.length - 1"
|
||||||
|
class="my-1 w-px flex-1"
|
||||||
|
:class="[isDarkMode ? 'bg-gray-600' : 'bg-gray-200']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mb-3 rounded-lg border px-3 py-2 last:mb-0"
|
||||||
|
:class="[
|
||||||
|
isDarkMode ? 'border-gray-700 bg-gray-800/60' : 'border-gray-100 bg-gray-50'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between">
|
||||||
|
<p class="text-sm font-medium" :class="[primaryTextClass]">
|
||||||
|
{{ item.displayTitle }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs" :class="[mutedTextClass]">
|
||||||
|
{{ item.displayMeta }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p class="mt-1 text-sm" :class="[mutedTextClass]">
|
||||||
|
{{ item.displayDescription }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="space-y-3">
|
<section class="space-y-3">
|
||||||
<h4 class="text-sm font-semibold" :class="[primaryTextClass]">
|
<h4 class="text-sm font-semibold" :class="[primaryTextClass]">
|
||||||
{{ t('fileManage.storageInfo') }}
|
{{ t('fileManage.storageInfo') }}
|
||||||
@@ -885,6 +968,7 @@ import { computed, onMounted, type Component } from 'vue'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import type {
|
import type {
|
||||||
AdminBatchEditMode,
|
AdminBatchEditMode,
|
||||||
|
AdminFileInsightSeverity,
|
||||||
AdminFileSortBy,
|
AdminFileSortBy,
|
||||||
AdminFileSortOrder,
|
AdminFileSortOrder,
|
||||||
AdminFileStatusFilter,
|
AdminFileStatusFilter,
|
||||||
@@ -1106,6 +1190,13 @@ const detailStorageItems = computed<DetailInfoItem[]>(() => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const detailInsightReasonLabels = computed(() => {
|
||||||
|
const file = selectedFileDetail.value
|
||||||
|
if (!file) return []
|
||||||
|
|
||||||
|
return file.statusInsightReasons.map((reason) => t(`fileManage.insightReasons.${reason}`))
|
||||||
|
})
|
||||||
|
|
||||||
const openDetailEditModal = () => {
|
const openDetailEditModal = () => {
|
||||||
if (!selectedFileDetail.value) return
|
if (!selectedFileDetail.value) return
|
||||||
|
|
||||||
@@ -1246,6 +1337,63 @@ const getStatusBadgeClass = (file: AdminFileViewItem) => {
|
|||||||
return isDarkMode.value ? 'bg-green-900/30 text-green-300' : 'bg-green-100 text-green-700'
|
return isDarkMode.value ? 'bg-green-900/30 text-green-300' : 'bg-green-100 text-green-700'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getInsightSeverityLabel = (severity: AdminFileInsightSeverity) =>
|
||||||
|
t(`fileManage.insightSeverity.${severity}`)
|
||||||
|
|
||||||
|
const getInsightStateLabel = (state: string) => t(`fileManage.insightStates.${state}`)
|
||||||
|
|
||||||
|
const getInsightActionLabel = (action: string) => t(`fileManage.insightActions.${action}`)
|
||||||
|
|
||||||
|
const getInsightPanelClass = (severity: AdminFileInsightSeverity) => {
|
||||||
|
const darkClasses: Record<AdminFileInsightSeverity, string> = {
|
||||||
|
success: 'border-emerald-500/20 bg-emerald-500/10',
|
||||||
|
warning: 'border-amber-500/20 bg-amber-500/10',
|
||||||
|
danger: 'border-red-500/20 bg-red-500/10',
|
||||||
|
info: 'border-blue-500/20 bg-blue-500/10',
|
||||||
|
neutral: 'border-gray-700 bg-gray-700/30'
|
||||||
|
}
|
||||||
|
const lightClasses: Record<AdminFileInsightSeverity, string> = {
|
||||||
|
success: 'border-emerald-100 bg-emerald-50',
|
||||||
|
warning: 'border-amber-100 bg-amber-50',
|
||||||
|
danger: 'border-red-100 bg-red-50',
|
||||||
|
info: 'border-blue-100 bg-blue-50',
|
||||||
|
neutral: 'border-gray-200 bg-gray-50'
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDarkMode.value ? darkClasses[severity] : lightClasses[severity]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInsightBadgeClass = (severity: AdminFileInsightSeverity) => {
|
||||||
|
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-700 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[severity] : lightClasses[severity]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTimelineDotClass = (severity: AdminFileInsightSeverity) => {
|
||||||
|
const classes: Record<AdminFileInsightSeverity, string> = {
|
||||||
|
success: 'bg-emerald-500',
|
||||||
|
warning: 'bg-amber-500',
|
||||||
|
danger: 'bg-red-500',
|
||||||
|
info: 'bg-blue-500',
|
||||||
|
neutral: isDarkMode.value ? 'bg-gray-500' : 'bg-gray-300'
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes[severity]
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void loadFiles()
|
void loadFiles()
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user