优化管理页交互与历史同步

This commit is contained in:
2026-06-05 16:03:54 +08:00
parent 90321a5ca4
commit 2cde31f822
6 changed files with 131 additions and 76 deletions
+8 -3
View File
@@ -6,9 +6,14 @@
class="border-b px-6 py-4" class="border-b px-6 py-4"
:class="[isDarkMode ? 'border-gray-700/80' : 'border-[#d9e4e6]']" :class="[isDarkMode ? 'border-gray-700/80' : 'border-[#d9e4e6]']"
> >
<h3 class="text-lg font-medium" :class="[isDarkMode ? 'text-white' : 'text-gray-800']"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
{{ title }} <h3 class="text-lg font-medium" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
</h3> {{ title }}
</h3>
<div v-if="slots.actions" class="flex shrink-0 flex-wrap items-center gap-2">
<slot name="actions"></slot>
</div>
</div>
<div v-if="slots.toolbar" class="mt-4"> <div v-if="slots.toolbar" class="mt-4">
<slot name="toolbar"></slot> <slot name="toolbar"></slot>
</div> </div>
+15 -31
View File
@@ -100,8 +100,6 @@ const formatBytes = (value: number | string | undefined) => {
return `${(bytes / 1024 ** index).toFixed(index === 0 ? 0 : 1)} ${units[index]}` return `${(bytes / 1024 ** index).toFixed(index === 0 ? 0 : 1)} ${units[index]}`
} }
const lerp = (start: number, end: number, amount: number) => start + (end - start) * amount
const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value)) const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value))
const getDefaultWindow = () => { const getDefaultWindow = () => {
@@ -259,52 +257,38 @@ const draw = () => {
if (hoverX.value !== null && points.length && total) { if (hoverX.value !== null && points.length && total) {
const hoverRatio = clamp((hoverX.value - padLeft) / width, 0, 1) const hoverRatio = clamp((hoverX.value - padLeft) / width, 0, 1)
const sourceIndex = clamp(offset.value + hoverRatio * denominator, 0, total - 1) const sourceIndex = Math.round(clamp(offset.value + hoverRatio * denominator, 0, total - 1))
const leftIndex = Math.floor(sourceIndex) const row = props.rows[sourceIndex]
const rightIndex = Math.min(total - 1, Math.ceil(sourceIndex)) const snapX = padLeft + ((sourceIndex - offset.value) / denominator) * width
const mix = sourceIndex - leftIndex const downloads = Number(row.downloads || 0)
const leftRow = props.rows[leftIndex] const uploads = Number(row.uploads || 0)
const rightRow = props.rows[rightIndex] || leftRow const downloadTraffic = Number(row.downloadTraffic || 0)
const interpolatedX = padLeft + ((sourceIndex - offset.value) / denominator) * width const uploadTraffic = Number(row.uploadTraffic || 0)
const downloads = lerp(Number(leftRow.downloads || 0), Number(rightRow.downloads || 0), mix)
const uploads = lerp(Number(leftRow.uploads || 0), Number(rightRow.uploads || 0), mix)
const downloadTraffic = lerp(
Number(leftRow.downloadTraffic || 0),
Number(rightRow.downloadTraffic || 0),
mix
)
const uploadTraffic = lerp(
Number(leftRow.uploadTraffic || 0),
Number(rightRow.uploadTraffic || 0),
mix
)
const downloadY = padTop + height - (downloads / max) * height const downloadY = padTop + height - (downloads / max) * height
const uploadY = padTop + height - (uploads / max) * height const uploadY = padTop + height - (uploads / max) * height
const anchorY = Math.min(downloadY, uploadY) const anchorY = Math.min(downloadY, uploadY)
const placeBelow = anchorY < 104 const placeBelow = anchorY < 104
const tooltipInset = Math.min(mobile ? 116 : 132, Math.max(48, rect.width / 2 - 8)) const tooltipInset = Math.min(mobile ? 116 : 132, Math.max(48, rect.width / 2 - 8))
const dateText =
leftRow.date === rightRow.date ? leftRow.date : `${leftRow.date} - ${rightRow.date}`
ctx.strokeStyle = props.isDarkMode ? 'rgba(148, 163, 184, 0.35)' : 'rgba(91, 121, 132, 0.28)' ctx.strokeStyle = props.isDarkMode ? 'rgba(148, 163, 184, 0.35)' : 'rgba(91, 121, 132, 0.28)'
ctx.lineWidth = 1 ctx.lineWidth = 1
ctx.beginPath() ctx.beginPath()
ctx.moveTo(interpolatedX, padTop) ctx.moveTo(snapX, padTop)
ctx.lineTo(interpolatedX, padTop + height) ctx.lineTo(snapX, padTop + height)
ctx.stroke() ctx.stroke()
tooltip.value = { tooltip.value = {
left: clamp(interpolatedX, tooltipInset, rect.width - tooltipInset), left: clamp(snapX, tooltipInset, rect.width - tooltipInset),
top: placeBelow ? anchorY + 16 : anchorY - 12, top: placeBelow ? anchorY + 16 : anchorY - 12,
date: dateText, date: row.date,
downloads: Math.round(downloads), downloads,
uploads: Math.round(uploads), uploads,
downloadTraffic: formatBytes(downloadTraffic), downloadTraffic: formatBytes(downloadTraffic),
uploadTraffic: formatBytes(uploadTraffic), uploadTraffic: formatBytes(uploadTraffic),
transform: placeBelow ? 'translate(-50%, 0)' : 'translate(-50%, -100%)' transform: placeBelow ? 'translate(-50%, 0)' : 'translate(-50%, -100%)'
} }
drawHoverPoint({ x: interpolatedX, y: downloadY }, downloadColor) drawHoverPoint({ x: snapX, y: downloadY }, downloadColor)
drawHoverPoint({ x: interpolatedX, y: uploadY }, uploadColor) drawHoverPoint({ x: snapX, y: uploadY }, uploadColor)
} else { } else {
tooltip.value = null tooltip.value = null
} }
+4 -4
View File
@@ -567,10 +567,10 @@ export default {
permanent: 'Permanent' permanent: 'Permanent'
}, },
insightActions: { insightActions: {
monitor: 'Keep monitoring', monitor: 'No action needed',
extend_or_delete: 'Extend retention or clean up', extend_or_delete: 'Extend or clean up',
inspect_storage: 'Inspect storage path and file name', inspect_storage: 'Check storage info',
extend_expiration: 'Extend expiration if needed' extend_expiration: 'Extend validity'
}, },
insightReasons: { insightReasons: {
expired: 'Past expiration time', expired: 'Past expiration time',
+4 -4
View File
@@ -564,10 +564,10 @@ export default {
permanent: '永久有效' permanent: '永久有效'
}, },
insightActions: { insightActions: {
monitor: '保持观察', monitor: '无需处理',
extend_or_delete: '延长有效期或清理记录', extend_or_delete: '建议延长或清理',
inspect_storage: '检查存储路径与文件名', inspect_storage: '检查存储信息',
extend_expiration: '按需延长过期时间' extend_expiration: '建议延长有效期'
}, },
insightReasons: { insightReasons: {
expired: '已超过过期时间', expired: '已超过过期时间',
+31 -6
View File
@@ -10,11 +10,13 @@
{{ t('admin.dashboard.lastUpdated', { time: lastUpdatedText }) }} {{ t('admin.dashboard.lastUpdated', { time: lastUpdatedText }) }}
</p> </p>
</div> </div>
<BaseButton variant="secondary" :loading="isLoading" @click="fetchDashboardData"> <BaseButton variant="secondary" :loading="isDashboardRefreshing" @click="refreshDashboardPage">
<template #icon> <template #icon>
<RefreshCwIcon class="mr-2 h-4 w-4" /> <RefreshCwIcon class="mr-2 h-4 w-4" />
</template> </template>
{{ isLoading ? t('admin.dashboard.refreshing') : t('admin.dashboard.refresh') }} {{
isDashboardRefreshing ? t('admin.dashboard.refreshing') : t('admin.dashboard.refresh')
}}
</BaseButton> </BaseButton>
</div> </div>
@@ -260,13 +262,28 @@
/> />
</div> </div>
<div class="grid grid-cols-3 gap-2 sm:flex sm:justify-end"> <div class="grid grid-cols-3 gap-2 sm:flex sm:justify-end">
<BaseButton variant="outline" class="h-11 min-w-20" @click="trendCanvas?.zoom(0.75)"> <BaseButton
variant="outline"
size="sm"
class="h-9 min-w-16"
@click="trendCanvas?.zoom(0.75)"
>
放大 放大
</BaseButton> </BaseButton>
<BaseButton variant="outline" class="h-11 min-w-20" @click="trendCanvas?.zoom(1.33)"> <BaseButton
variant="outline"
size="sm"
class="h-9 min-w-16"
@click="trendCanvas?.zoom(1.33)"
>
缩小 缩小
</BaseButton> </BaseButton>
<BaseButton variant="outline" class="h-11 min-w-20" @click="trendCanvas?.resetWindow()"> <BaseButton
variant="outline"
size="sm"
class="h-9 min-w-16"
@click="trendCanvas?.resetWindow()"
>
重置 重置
</BaseButton> </BaseButton>
</div> </div>
@@ -476,6 +493,7 @@ const analyticsStart = ref(daysAgo(120))
const analyticsEnd = ref(today()) const analyticsEnd = ref(today())
const analyticsData = ref<AnalyticsData | null>(null) const analyticsData = ref<AnalyticsData | null>(null)
const analyticsError = ref('') const analyticsError = ref('')
const isAnalyticsLoading = ref(false)
const analyticsWindowText = ref('无数据') const analyticsWindowText = ref('无数据')
const analyticsDailyRows = computed(() => analyticsData.value?.daily || []) const analyticsDailyRows = computed(() => analyticsData.value?.daily || [])
const analyticsTopFiles = computed<AnalyticsFileRow[]>(() => analyticsData.value?.topFiles || []) const analyticsTopFiles = computed<AnalyticsFileRow[]>(() => analyticsData.value?.topFiles || [])
@@ -497,12 +515,19 @@ const formatBytes = (value: number | string | undefined) => formatFileSize(Numbe
const fetchAnalyticsData = async () => { const fetchAnalyticsData = async () => {
analyticsError.value = '' analyticsError.value = ''
isAnalyticsLoading.value = true
try { try {
analyticsData.value = await fetchAnalytics(analyticsStart.value, analyticsEnd.value) analyticsData.value = await fetchAnalytics(analyticsStart.value, analyticsEnd.value)
} catch (error) { } catch (error) {
analyticsError.value = error instanceof Error ? error.message : '统计加载失败' analyticsError.value = error instanceof Error ? error.message : '统计加载失败'
} finally {
isAnalyticsLoading.value = false
} }
} }
const isDashboardRefreshing = computed(() => isLoading.value || isAnalyticsLoading.value)
const refreshDashboardPage = async () => {
await Promise.all([fetchDashboardData(), fetchAnalyticsData()])
}
const healthActions = computed<DashboardHealthAction[]>(() => [ const healthActions = computed<DashboardHealthAction[]>(() => [
{ {
@@ -648,6 +673,6 @@ const PolicyRow = defineComponent({
}) })
onMounted(() => { onMounted(() => {
void Promise.all([fetchDashboardData(), fetchAnalyticsData()]) void refreshDashboardPage()
}) })
</script> </script>
+69 -28
View File
@@ -10,12 +10,6 @@
</p> </p>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<BaseButton variant="secondary" :loading="isLoading" @click="refreshFiles">
<template #icon>
<RefreshCwIcon class="mr-2 h-4 w-4" />
</template>
{{ t('fileManage.refresh') }}
</BaseButton>
<BaseButton <BaseButton
v-if="hasActiveFilters" v-if="hasActiveFilters"
variant="outline" variant="outline"
@@ -262,6 +256,14 @@
</section> </section>
<DataTable :title="t('fileManage.allFiles')" :headers="fileTableHeaders"> <DataTable :title="t('fileManage.allFiles')" :headers="fileTableHeaders">
<template #actions>
<BaseButton variant="secondary" :loading="isLoading" @click="refreshFileList">
<template #icon>
<RefreshCwIcon class="mr-2 h-4 w-4" />
</template>
{{ t('fileManage.refresh') }}
</BaseButton>
</template>
<template #toolbar> <template #toolbar>
<div <div
v-if="tableData.length > 0" v-if="tableData.length > 0"
@@ -306,7 +308,7 @@
:title="option.description" :title="option.description"
:disabled="option.disabled || !hasSelectedFiles || isBatchDeleting || isBatchUpdating" :disabled="option.disabled || !hasSelectedFiles || isBatchDeleting || isBatchUpdating"
:loading="isBatchPolicyActionRunning" :loading="isBatchPolicyActionRunning"
@click="applySelectedPolicyAction(option.action)" @click="applySelectedPolicyActionAndRefreshHistory(option.action)"
> >
<template #icon> <template #icon>
<component :is="getDetailPolicyActionIcon(option.action)" class="mr-2 h-4 w-4" /> <component :is="getDetailPolicyActionIcon(option.action)" class="mr-2 h-4 w-4" />
@@ -330,7 +332,7 @@
size="sm" size="sm"
:disabled="!hasSelectedFiles || isBatchUpdating || isBatchPolicyActionRunning" :disabled="!hasSelectedFiles || isBatchUpdating || isBatchPolicyActionRunning"
:loading="isBatchDeleting" :loading="isBatchDeleting"
@click="deleteSelectedFiles" @click="deleteSelectedFilesAndRefreshHistory"
> >
<template #icon> <template #icon>
<TrashIcon class="mr-2 h-4 w-4" /> <TrashIcon class="mr-2 h-4 w-4" />
@@ -357,7 +359,7 @@
<p class="font-medium" :class="[primaryTextClass]"> <p class="font-medium" :class="[primaryTextClass]">
{{ t('fileManage.loadError') }} {{ t('fileManage.loadError') }}
</p> </p>
<BaseButton class="mt-4" variant="secondary" @click="refreshFiles"> <BaseButton class="mt-4" variant="secondary" @click="refreshFileList">
<template #icon> <template #icon>
<RefreshCwIcon class="mr-2 h-4 w-4" /> <RefreshCwIcon class="mr-2 h-4 w-4" />
</template> </template>
@@ -437,6 +439,12 @@
> >
{{ file.displayHealthState }} {{ file.displayHealthState }}
</span> </span>
<span
class="inline-flex max-w-full items-center rounded-full px-2 py-0.5 text-xs font-medium"
:class="getInsightActionBadgeClass(file.statusInsightNextAction)"
>
{{ file.displayHealthAction }}
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -553,7 +561,7 @@
? 'bg-[#dc2626] text-white shadow-[#7f1d1d]/35 hover:bg-[#b91c1c]' ? 'bg-[#dc2626] text-white shadow-[#7f1d1d]/35 hover:bg-[#b91c1c]'
: 'bg-[#dc2626] text-white shadow-[#f87171]/35 hover:bg-[#b91c1c]' : 'bg-[#dc2626] text-white shadow-[#f87171]/35 hover:bg-[#b91c1c]'
]" ]"
@click="deleteFile(file.id)" @click="deleteFileAndRefreshHistory(file.id)"
> >
<TrashIcon class="mr-1.5 h-4 w-4" /> <TrashIcon class="mr-1.5 h-4 w-4" />
{{ t('common.delete') }} {{ t('common.delete') }}
@@ -942,12 +950,20 @@
{{ getInsightStateLabel(selectedFileDetail.statusInsightState) }} {{ getInsightStateLabel(selectedFileDetail.statusInsightState) }}
</h4> </h4>
</div> </div>
<span <div class="flex flex-wrap items-center gap-2">
class="inline-flex w-fit items-center rounded-full px-2.5 py-1 text-xs font-medium" <span
:class="getInsightBadgeClass(selectedFileDetail.statusInsightSeverity)" 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> {{ getInsightSeverityLabel(selectedFileDetail.statusInsightSeverity) }}
</span>
<span
class="inline-flex w-fit items-center rounded-full px-2.5 py-1 text-xs font-medium"
:class="getInsightActionBadgeClass(selectedFileDetail.statusInsightNextAction)"
>
{{ selectedFileDetail.displayHealthAction }}
</span>
</div>
</div> </div>
<div v-if="detailInsightReasonLabels.length > 0" class="mt-3 flex flex-wrap gap-2"> <div v-if="detailInsightReasonLabels.length > 0" class="mt-3 flex flex-wrap gap-2">
@@ -966,11 +982,11 @@
v-for="action in detailPolicyActionOptions" v-for="action in detailPolicyActionOptions"
:key="action.action" :key="action.action"
type="button" type="button"
class="flex min-h-16 items-start gap-2 rounded-lg border px-3 py-2 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-60" class="theme-2026-card-hover flex min-h-16 items-start gap-2 rounded-[30px] border px-3 py-2 text-left shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:translate-y-0 disabled:hover:shadow-md"
:class="detailPolicyActionClass" :class="[detailPolicyActionClass, cardHoverClass]"
:disabled="isDetailPolicyActionRunning || isDetailLoading || action.disabled" :disabled="isDetailPolicyActionRunning || isDetailLoading || action.disabled"
:title="action.description" :title="action.description"
@click="applyDetailPolicyAction(action.action)" @click="applyDetailPolicyActionAndRefreshHistory(action.action)"
> >
<RefreshCwIcon <RefreshCwIcon
v-if="isDetailPolicyActionRunning" v-if="isDetailPolicyActionRunning"
@@ -1185,7 +1201,7 @@
<BaseButton variant="secondary" :disabled="isSaving" @click="closeEditModal"> <BaseButton variant="secondary" :disabled="isSaving" @click="closeEditModal">
{{ t('common.cancel') }} {{ t('common.cancel') }}
</BaseButton> </BaseButton>
<BaseButton :loading="isSaving" @click="handleUpdate"> <BaseButton :loading="isSaving" @click="handleUpdateAndRefreshHistory">
<template #icon> <template #icon>
<CheckIcon class="w-4 h-4 mr-2" /> <CheckIcon class="w-4 h-4 mr-2" />
</template> </template>
@@ -1330,7 +1346,7 @@
<BaseButton variant="secondary" :disabled="isBatchUpdating" @click="closeBatchEditModal()"> <BaseButton variant="secondary" :disabled="isBatchUpdating" @click="closeBatchEditModal()">
{{ t('common.cancel') }} {{ t('common.cancel') }}
</BaseButton> </BaseButton>
<BaseButton :loading="isBatchUpdating" @click="handleBatchUpdate"> <BaseButton :loading="isBatchUpdating" @click="handleBatchUpdateAndRefreshHistory">
<template #icon> <template #icon>
<CheckIcon class="w-4 h-4 mr-2" /> <CheckIcon class="w-4 h-4 mr-2" />
</template> </template>
@@ -1525,25 +1541,25 @@ const {
copyDetailRetrieveLink, copyDetailRetrieveLink,
copyText, copyText,
clearSelection, clearSelection,
deleteFile, deleteFile: deleteFileRaw,
deleteSelectedFiles, deleteSelectedFiles: deleteSelectedFilesRaw,
deleteSelectedViewPreset, deleteSelectedViewPreset,
downloadFile, downloadFile,
exportPreviewText, exportPreviewText,
handlePageChange, handlePageChange,
handlePageSizeChange, handlePageSizeChange,
handleSearch, handleSearch,
applyDetailPolicyAction, applyDetailPolicyAction: applyDetailPolicyActionRaw,
applySelectedPolicyAction, applySelectedPolicyAction: applySelectedPolicyActionRaw,
handleBatchUpdate, handleBatchUpdate: handleBatchUpdateRaw,
handleUpdate, handleUpdate: handleUpdateRaw,
loadViewPresets, loadViewPresets,
loadFiles, loadFiles,
openBatchEditModal, openBatchEditModal,
openEditModal, openEditModal,
openFileDetail, openFileDetail,
openTextPreview, openTextPreview,
refreshFiles, refreshFiles: refreshFileList,
resetFilters, resetFilters,
saveCurrentViewPreset, saveCurrentViewPreset,
setHealthFilter, setHealthFilter,
@@ -1575,6 +1591,21 @@ const loadHistoryFiles = async () => {
isHistoryLoading.value = false isHistoryLoading.value = false
} }
} }
const runFileMutationAndRefreshHistory = async (operation: () => Promise<void>) => {
await operation()
await loadHistoryFiles()
}
const handleUpdateAndRefreshHistory = () => runFileMutationAndRefreshHistory(handleUpdateRaw)
const handleBatchUpdateAndRefreshHistory = () =>
runFileMutationAndRefreshHistory(handleBatchUpdateRaw)
const deleteFileAndRefreshHistory = (id: number) =>
runFileMutationAndRefreshHistory(() => deleteFileRaw(id))
const deleteSelectedFilesAndRefreshHistory = () =>
runFileMutationAndRefreshHistory(deleteSelectedFilesRaw)
const applySelectedPolicyActionAndRefreshHistory = (action: AdminFilePolicyAction) =>
runFileMutationAndRefreshHistory(() => applySelectedPolicyActionRaw(action))
const applyDetailPolicyActionAndRefreshHistory = (action: AdminFilePolicyAction) =>
runFileMutationAndRefreshHistory(() => applyDetailPolicyActionRaw(action))
const handleHistoryPageSizeChange = (size: number) => { const handleHistoryPageSizeChange = (size: number) => {
historyPageSize.value = size historyPageSize.value = size
historyPage.value = 1 historyPage.value = 1
@@ -1992,6 +2023,16 @@ const getInsightBadgeClass = (severity: AdminFileInsightSeverity) => {
return isDarkMode.value ? darkClasses[severity] : lightClasses[severity] return isDarkMode.value ? darkClasses[severity] : lightClasses[severity]
} }
const getInsightActionBadgeClass = (action: string) => {
if (action === 'extend_or_delete' || action === 'extend_expiration') {
return isDarkMode.value ? 'bg-amber-900/40 text-amber-200' : 'bg-amber-100 text-amber-700'
}
if (action === 'inspect_storage') {
return isDarkMode.value ? 'bg-red-900/40 text-red-200' : 'bg-red-100 text-red-700'
}
return isDarkMode.value ? 'bg-gray-700 text-gray-300' : 'bg-gray-100 text-gray-700'
}
const getTimelineDotClass = (severity: AdminFileInsightSeverity) => { const getTimelineDotClass = (severity: AdminFileInsightSeverity) => {
const classes: Record<AdminFileInsightSeverity, string> = { const classes: Record<AdminFileInsightSeverity, string> = {
success: 'bg-emerald-500', success: 'bg-emerald-500',