diff --git a/src/pages/dashboard.vue b/src/pages/dashboard.vue index e0051c7e..007a141c 100644 --- a/src/pages/dashboard.vue +++ b/src/pages/dashboard.vue @@ -217,8 +217,8 @@ const dashboardConfigs = ref([ name: t('dashboard.network'), key: '', attrs: {}, - cols: { cols: 12, md: 6 }, - rows: 17, + cols: { cols: 12, sm: 3, md: 4 }, + rows: DASHBOARD_RESOURCE_CHART_ROWS, elements: [], }, { diff --git a/src/utils/schedulerVisual.ts b/src/utils/schedulerVisual.ts new file mode 100644 index 00000000..08271437 --- /dev/null +++ b/src/utils/schedulerVisual.ts @@ -0,0 +1,68 @@ +import type { ScheduleInfo } from '@/api/types' + +// 定时服务在捷径和仪表板中共用的视觉配置。 +export type SchedulerVisual = { + color: string + icon: string + rgb: string +} + +// 已知定时服务的视觉匹配规则。 +type SchedulerVisualRule = SchedulerVisual & { + ids?: string[] + names?: string[] + providers?: string[] +} + +const schedulerVisualRules: SchedulerVisualRule[] = [ + { ids: ['cookiecloud'], names: ['CookieCloud'], icon: 'mdi-cloud-sync-outline', color: '#3f8cff', rgb: '63, 140, 255' }, + { ids: ['mediaserver_sync'], names: ['媒体服务器'], icon: 'mdi-television-play', color: '#42c336', rgb: '66, 195, 54' }, + { ids: ['new_subscribe_search', 'subscribe_search'], names: ['订阅搜索', '新增订阅搜索'], icon: 'mdi-magnify', color: '#e91e63', rgb: '233, 30, 99' }, + { ids: ['subscribe_tmdb'], names: ['订阅元数据'], icon: 'mdi-database-search-outline', color: '#9b6cf3', rgb: '155, 108, 243' }, + { ids: ['subscribe_refresh'], names: ['订阅刷新'], icon: 'mdi-refresh', color: '#25b6c8', rgb: '37, 182, 200' }, + { ids: ['subscribe_follow'], names: ['订阅分享'], icon: 'mdi-share-variant-outline', color: '#ff704d', rgb: '255, 112, 77' }, + { ids: ['transfer'], names: ['下载文件整理', '文件整理'], icon: 'mdi-folder-move-outline', color: '#3f8cff', rgb: '63, 140, 255' }, + { ids: ['random_wallpager'], names: ['壁纸'], icon: 'mdi-image-outline', color: '#9b6cf3', rgb: '155, 108, 243' }, + { ids: ['scheduler_job'], names: ['公共定时服务'], icon: 'mdi-clock-outline', color: '#42c336', rgb: '66, 195, 54' }, + { ids: ['clear_cache'], names: ['缓存清理'], icon: 'mdi-delete-sweep-outline', color: '#ffad1f', rgb: '255, 173, 31' }, + { ids: ['data_cleanup'], names: ['数据表清理'], icon: 'mdi-database-remove-outline', color: '#ff704d', rgb: '255, 112, 77' }, + { ids: ['user_auth'], names: ['用户认证'], icon: 'mdi-account-check-outline', color: '#9b6cf3', rgb: '155, 108, 243' }, + { ids: ['sitedata_refresh'], names: ['站点数据'], icon: 'mdi-web-refresh', color: '#25b6c8', rgb: '37, 182, 200' }, + { ids: ['recommend_refresh'], names: ['推荐缓存'], icon: 'mdi-star-outline', color: '#ffad1f', rgb: '255, 173, 31' }, + { ids: ['plugin_market_refresh'], names: ['插件市场'], icon: 'mdi-puzzle-outline', color: '#ff704d', rgb: '255, 112, 77' }, + { ids: ['subscribe_calendar_cache'], names: ['订阅日历'], icon: 'mdi-calendar-refresh-outline', color: '#3f8cff', rgb: '63, 140, 255' }, + { ids: ['full_gc'], names: ['内存回收'], icon: 'mdi-memory', color: '#25b6c8', rgb: '37, 182, 200' }, + { ids: ['agent_heartbeat'], names: ['智能体'], icon: 'mdi-robot-outline', color: '#9b6cf3', rgb: '155, 108, 243' }, + { ids: ['usage_report'], names: ['统计上报'], icon: 'mdi-chart-line', color: '#42c336', rgb: '66, 195, 54' }, + { ids: ['workflow'], providers: ['工作流'], icon: 'mdi-source-branch', color: '#3f8cff', rgb: '63, 140, 255' }, + { ids: ['plugin'], icon: 'mdi-puzzle-outline', color: '#ff704d', rgb: '255, 112, 77' }, +] + +const schedulerFallbackVisual: SchedulerVisual = { + icon: 'mdi-timer-cog-outline', + color: '#25b6c8', + rgb: '37, 182, 200', +} + +/** 判断视觉规则中的任一文本是否命中任务信息。 */ +function hasSchedulerRuleMatch(values: string[] | undefined, target: string) { + if (!values?.length) return false + + return values.some(value => target.includes(value.toLocaleLowerCase())) +} + +/** 使用任务 ID、名称和提供者返回统一的定时服务图标与主题色。 */ +export function getSchedulerVisual(scheduler: ScheduleInfo): SchedulerVisual { + const schedulerId = (scheduler.id || '').toLocaleLowerCase() + const schedulerName = (scheduler.name || '').toLocaleLowerCase() + const schedulerProvider = (scheduler.provider || '').toLocaleLowerCase() + const matchedRule = schedulerVisualRules.find(rule => { + return ( + hasSchedulerRuleMatch(rule.ids, schedulerId) || + hasSchedulerRuleMatch(rule.names, schedulerName) || + hasSchedulerRuleMatch(rule.providers, schedulerProvider) + ) + }) + + return matchedRule ?? schedulerFallbackVisual +} diff --git a/src/views/dashboard/AnalyticsNetwork.vue b/src/views/dashboard/AnalyticsNetwork.vue index d123591a..46d803ef 100644 --- a/src/views/dashboard/AnalyticsNetwork.vue +++ b/src/views/dashboard/AnalyticsNetwork.vue @@ -34,11 +34,11 @@ const variableTheme = controlledComputed( // 时间序列 - 上行和下行流量 const series = ref([ { - name: '上行流量', + name: t('dashboard.upload'), data: [0], }, { - name: '下行流量', + name: t('dashboard.download'), data: [0], }, ]) @@ -57,14 +57,27 @@ const animatedCurrentDownloadText = computed( () => `${formatDashboardFileSize(animatedCurrentDownload.value, 2, currentDownload.value)}/s`, ) +// 根据最近上、下行峰值自动选择图表刻度,低流量时仍保留可读的区域高度。 +const networkChartMax = computed(() => { + const peak = Math.max(...series.value.flatMap(item => item.data), currentUpload.value, currentDownload.value) + if (peak <= 0) return 1024 + + const unit = 1024 ** Math.max(0, Math.floor(Math.log(peak) / Math.log(1024))) + + return Math.max(unit, Math.ceil(peak / unit) * unit) +}) + const chartOptions = controlledComputed( - () => vuetifyTheme.name.value, + () => `${vuetifyTheme.name.value}:${networkChartMax.value}`, () => { + const axisLabelColor = `rgba(${hexToRgb(currentTheme.value['on-surface'])},${variableTheme.value['medium-emphasis-opacity']})` + return { chart: { parentHeightOffset: 0, toolbar: { show: false }, animations: { enabled: false }, + foreColor: axisLabelColor, }, tooltip: { enabled: false, @@ -82,7 +95,7 @@ const chartOptions = controlledComputed( }, padding: { top: -10, - left: -7, + left: 8, right: 5, bottom: 5, }, @@ -93,10 +106,13 @@ const chartOptions = controlledComputed( curve: 'smooth', }, colors: [currentTheme.value.warning, currentTheme.value.info], + fill: { + opacity: [0.2, 0.12], + }, markers: { size: 6, offsetY: 4, - offsetX: -2, + offsetX: 4, strokeWidth: 3, colors: ['transparent'], strokeColors: 'transparent', @@ -116,20 +132,31 @@ const chartOptions = controlledComputed( ], hover: { size: 7 }, }, + dataLabels: { + enabled: false, + }, xaxis: { labels: { show: false }, axisTicks: { show: false }, axisBorder: { show: false }, }, yaxis: { - labels: { show: false }, + labels: { + show: true, + minWidth: 52, + formatter: (value: number) => + `${formatDashboardFileSize(value, value >= 1024 ** 2 ? 1 : 0, networkChartMax.value)}/s`, + style: { + colors: axisLabelColor, + fontSize: '10px', + }, + }, + tickAmount: 2, + max: networkChartMax.value, + min: 0, }, legend: { - show: true, - position: 'top', - horizontalAlign: 'left', - fontSize: '12px', - fontFamily: 'inherit', + show: false, }, } }, @@ -175,21 +202,16 @@ useKeepAliveRefresh(refresh)