b60c003ec2
完善 2026 主题的移动端与桌面端交互体验,修复多项可访问性和信息展示问题。 - 允许浏览器缩放,补充焦点样式、按钮名称、表单标签和对话框键盘焦点管理\n- 支持 reduced-motion,扩大公共页、后台导航、记录列表和分页控件的触控区域\n- 优化系统设置分区导航、密码显示、字段语义和底部粘性保存栏\n- 压缩移动文件列表操作区,改善桌面表格滚动可达性和统计值截断\n- 优化仪表盘指标、趋势摘要、进度条语义及中英文文案
189 lines
6.8 KiB
TypeScript
189 lines
6.8 KiB
TypeScript
import { computed, ref, reactive } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { StatsService } from '@/services'
|
|
import type { DashboardData, DashboardHealthSummary, DashboardViewData } from '@/types'
|
|
import { formatFileSize, getErrorMessage } from '@/utils/common'
|
|
|
|
type UseDashboardStatsOptions = {
|
|
loadFailedMessage?: string
|
|
}
|
|
|
|
const emptyDashboardData = (): DashboardViewData => ({
|
|
hasExtendedStats: false,
|
|
totalFiles: 0,
|
|
storageUsed: 0,
|
|
yesterdayCount: 0,
|
|
todayCount: 0,
|
|
yesterdaySize: 0,
|
|
todaySize: 0,
|
|
sysUptime: null,
|
|
activeCount: 0,
|
|
expiredCount: 0,
|
|
textCount: 0,
|
|
fileCount: 0,
|
|
chunkedCount: 0,
|
|
usedCount: 0,
|
|
storageBackend: '-',
|
|
uploadSizeLimit: 0,
|
|
openUpload: 0,
|
|
enableChunk: 0,
|
|
maxSaveSeconds: 0,
|
|
healthAttentionCount: 0,
|
|
healthDangerCount: 0,
|
|
healthWarningCount: 0,
|
|
expiringSoonCount: 0,
|
|
storageIssueCount: 0,
|
|
neverRetrievedCount: 0,
|
|
healthyCount: 0,
|
|
permanentCount: 0,
|
|
storageUsedText: '0 Bytes',
|
|
yesterdaySizeText: '0 Bytes',
|
|
todaySizeText: '0 Bytes',
|
|
uploadSizeLimitText: '0 Bytes',
|
|
sysUptimeText: '-',
|
|
activeRatio: 0,
|
|
textRatio: 0,
|
|
fileRatio: 0,
|
|
healthyRatio: 0,
|
|
healthAttentionRatio: 0,
|
|
todaySizeRatio: 0
|
|
})
|
|
|
|
const toNumber = (value: number | string | null | undefined) => Number(value || 0)
|
|
|
|
const clampRatio = (value: number) => Math.max(0, Math.min(100, Math.round(value)))
|
|
|
|
const hasOwn = (target: object, key: string) => Object.prototype.hasOwnProperty.call(target, key)
|
|
|
|
const formatDuration = (startTimestamp: number | null, dayLabel: string, hourLabel: string) => {
|
|
if (!startTimestamp) return '-'
|
|
const uptime = Date.now() - startTimestamp
|
|
const days = Math.floor(uptime / (24 * 60 * 60 * 1000))
|
|
const hours = Math.floor((uptime % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000))
|
|
return `${days}${dayLabel} ${hours}${hourLabel}`
|
|
}
|
|
|
|
const healthSummaryKeys: (keyof DashboardHealthSummary)[] = [
|
|
'healthAttentionCount',
|
|
'healthDangerCount',
|
|
'healthWarningCount',
|
|
'expiringSoonCount',
|
|
'storageIssueCount',
|
|
'neverRetrievedCount',
|
|
'healthyCount',
|
|
'permanentCount'
|
|
]
|
|
|
|
const normalizeHealthSummary = (detail: DashboardData): DashboardHealthSummary => ({
|
|
healthAttentionCount: toNumber(
|
|
detail.healthSummary?.healthAttentionCount ?? detail.healthAttentionCount
|
|
),
|
|
healthDangerCount: toNumber(detail.healthSummary?.healthDangerCount ?? detail.healthDangerCount),
|
|
healthWarningCount: toNumber(
|
|
detail.healthSummary?.healthWarningCount ?? detail.healthWarningCount
|
|
),
|
|
expiringSoonCount: toNumber(detail.healthSummary?.expiringSoonCount ?? detail.expiringSoonCount),
|
|
storageIssueCount: toNumber(detail.healthSummary?.storageIssueCount ?? detail.storageIssueCount),
|
|
neverRetrievedCount: toNumber(
|
|
detail.healthSummary?.neverRetrievedCount ?? detail.neverRetrievedCount
|
|
),
|
|
healthyCount: toNumber(detail.healthSummary?.healthyCount ?? detail.healthyCount),
|
|
permanentCount: toNumber(detail.healthSummary?.permanentCount ?? detail.permanentCount)
|
|
})
|
|
|
|
export function useDashboardStats(options: UseDashboardStatsOptions = {}) {
|
|
const { t, locale } = useI18n()
|
|
const dashboardData = reactive<DashboardViewData>(emptyDashboardData())
|
|
const isLoading = ref(false)
|
|
const errorMessage = ref('')
|
|
const lastUpdatedAt = ref<Date | null>(null)
|
|
const lastUpdatedText = computed(() =>
|
|
lastUpdatedAt.value
|
|
? new Intl.DateTimeFormat(locale.value, { dateStyle: 'medium', timeStyle: 'short' }).format(lastUpdatedAt.value)
|
|
: '-'
|
|
)
|
|
|
|
const fetchDashboardData = async () => {
|
|
isLoading.value = true
|
|
errorMessage.value = ''
|
|
|
|
try {
|
|
const response = await StatsService.getDashboard()
|
|
if (!response.detail) {
|
|
throw new Error('No dashboard data')
|
|
}
|
|
|
|
const detail = response.detail
|
|
dashboardData.totalFiles = toNumber(detail.totalFiles)
|
|
dashboardData.storageUsed = toNumber(detail.storageUsed)
|
|
dashboardData.yesterdayCount = toNumber(detail.yesterdayCount)
|
|
dashboardData.todayCount = toNumber(detail.todayCount)
|
|
dashboardData.yesterdaySize = toNumber(detail.yesterdaySize)
|
|
dashboardData.todaySize = toNumber(detail.todaySize)
|
|
dashboardData.sysUptime = detail.sysUptime
|
|
dashboardData.hasExtendedStats = hasOwn(detail, 'activeCount')
|
|
dashboardData.activeCount = dashboardData.hasExtendedStats
|
|
? toNumber(detail.activeCount)
|
|
: dashboardData.totalFiles
|
|
dashboardData.expiredCount = toNumber(detail.expiredCount)
|
|
dashboardData.textCount = toNumber(detail.textCount)
|
|
dashboardData.fileCount = toNumber(detail.fileCount)
|
|
dashboardData.chunkedCount = toNumber(detail.chunkedCount)
|
|
dashboardData.usedCount = toNumber(detail.usedCount)
|
|
dashboardData.storageBackend = detail.storageBackend || '-'
|
|
dashboardData.uploadSizeLimit = toNumber(detail.uploadSizeLimit)
|
|
dashboardData.openUpload = toNumber(detail.openUpload)
|
|
dashboardData.enableChunk = toNumber(detail.enableChunk)
|
|
dashboardData.maxSaveSeconds = toNumber(detail.maxSaveSeconds)
|
|
const healthSummary = normalizeHealthSummary(detail)
|
|
healthSummaryKeys.forEach((key) => {
|
|
dashboardData[key] = healthSummary[key]
|
|
})
|
|
|
|
dashboardData.storageUsedText = formatFileSize(dashboardData.storageUsed)
|
|
dashboardData.yesterdaySizeText = formatFileSize(dashboardData.yesterdaySize)
|
|
dashboardData.todaySizeText = formatFileSize(dashboardData.todaySize)
|
|
dashboardData.uploadSizeLimitText = formatFileSize(dashboardData.uploadSizeLimit)
|
|
dashboardData.sysUptimeText = formatDuration(
|
|
dashboardData.sysUptime,
|
|
t('common.dayShort'),
|
|
t('common.hourShort')
|
|
)
|
|
dashboardData.activeRatio = dashboardData.totalFiles
|
|
? clampRatio((dashboardData.activeCount / dashboardData.totalFiles) * 100)
|
|
: 0
|
|
dashboardData.textRatio = dashboardData.totalFiles
|
|
? clampRatio((dashboardData.textCount / dashboardData.totalFiles) * 100)
|
|
: 0
|
|
dashboardData.fileRatio = dashboardData.totalFiles
|
|
? clampRatio((dashboardData.fileCount / dashboardData.totalFiles) * 100)
|
|
: 0
|
|
dashboardData.healthyRatio = dashboardData.totalFiles
|
|
? clampRatio((dashboardData.healthyCount / dashboardData.totalFiles) * 100)
|
|
: 0
|
|
dashboardData.healthAttentionRatio = dashboardData.totalFiles
|
|
? clampRatio((dashboardData.healthAttentionCount / dashboardData.totalFiles) * 100)
|
|
: 0
|
|
dashboardData.todaySizeRatio = dashboardData.uploadSizeLimit
|
|
? clampRatio((dashboardData.todaySize / dashboardData.uploadSizeLimit) * 100)
|
|
: 0
|
|
lastUpdatedAt.value = new Date()
|
|
} catch (error) {
|
|
errorMessage.value = getErrorMessage(
|
|
error,
|
|
options.loadFailedMessage || 'Failed to load dashboard data'
|
|
)
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
return {
|
|
dashboardData,
|
|
errorMessage,
|
|
fetchDashboardData,
|
|
isLoading,
|
|
lastUpdatedText
|
|
}
|
|
}
|