feat: add system uptime tracking and localization support

This commit is contained in:
jxxghp
2026-06-12 15:52:22 +08:00
parent 68b0071009
commit 951d76481b
4 changed files with 165 additions and 0 deletions

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup>
import { formatDateDifference } from '@/@core/utils/formatters'
import api from '@/api'
import type { Process as SystemProcess } from '@/api/types'
import { clearCacheAndReload } from '@/composables/useVersionChecker'
import MarkdownIt from 'markdown-it'
import mdLinkAttributes from 'markdown-it-link-attributes'
@@ -37,6 +38,12 @@ md.use(mdLinkAttributes, {
// 系统环境变量
const systemEnv = ref<any>({})
// 系统运行时间的基准秒数和同步时间,用于在弹窗打开后实时递增展示。
const systemUptimeBaseSeconds = ref<number | null>(null)
const systemUptimeSyncedAt = ref(0)
const systemUptimeNow = ref(Date.now())
let systemUptimeTimer: ReturnType<typeof setInterval> | null = null
// 所有Release
const allRelease = ref<any>([])
@@ -102,6 +109,22 @@ const frontendVersionStatistics = computed(() => versionStatistic.value?.fronten
// 活跃用户统计
const activeUsers = computed(() => versionStatistic.value?.active_users ?? {})
// 系统运行秒数
const systemUptimeSeconds = computed(() => {
if (systemUptimeBaseSeconds.value === null) return null
const elapsedSeconds = Math.floor((systemUptimeNow.value - systemUptimeSyncedAt.value) / 1000)
return Math.max(0, systemUptimeBaseSeconds.value + elapsedSeconds)
})
// 友好的系统运行时间文本
const systemUptimeText = computed(() => {
if (systemUptimeSeconds.value === null) return ''
return formatUptimeDuration(systemUptimeSeconds.value)
})
/** 格式化版本安装统计数字为千分位展示。 */
function formatVersionStatisticNumber(value: unknown) {
const numberValue = Number(value ?? 0)
@@ -111,6 +134,85 @@ function formatVersionStatisticNumber(value: unknown) {
return numberValue.toLocaleString()
}
/** 将秒数保存为运行时间基准,并记录本地同步时间。 */
function syncSystemUptime(seconds: number | null) {
if (seconds === null) return
const now = Date.now()
systemUptimeBaseSeconds.value = seconds
systemUptimeSyncedAt.value = now
systemUptimeNow.value = now
}
/** 将接口返回值规范化为可展示的秒数。 */
function normalizeUptimeSeconds(value: unknown) {
const numberValue = Number(value)
if (!Number.isFinite(numberValue) || numberValue < 0) return null
return Math.floor(numberValue)
}
/** 从进程创建时间推导运行秒数;兼容秒级和毫秒级时间戳。 */
function uptimeSecondsFromCreateTime(value: unknown) {
const timestamp = Number(value)
if (!Number.isFinite(timestamp) || timestamp <= 0) return null
const timestampMs = timestamp > 1_000_000_000_000 ? timestamp : timestamp * 1000
return Math.max(0, Math.floor((Date.now() - timestampMs) / 1000))
}
/** 获取单个进程的运行秒数,优先使用创建时间以保留跨天运行时长。 */
function getProcessUptimeSeconds(process: SystemProcess) {
return uptimeSecondsFromCreateTime(process.create_time) ?? normalizeUptimeSeconds(process.run_time)
}
/** 从进程列表中挑选 MoviePilot 主进程,找不到时使用运行时间最长的进程兜底。 */
function resolveSystemUptimeSeconds(processes: SystemProcess[]) {
const availableProcesses = processes
.map(process => ({
process,
uptimeSeconds: getProcessUptimeSeconds(process),
}))
.filter((item): item is { process: SystemProcess; uptimeSeconds: number } => item.uptimeSeconds !== null)
if (!availableProcesses.length) return null
const preferredProcesses = availableProcesses.filter(({ process }) =>
/moviepilot|python|uvicorn|gunicorn|hypercorn/i.test(process.name ?? ''),
)
const targetProcesses = preferredProcesses.length ? preferredProcesses : availableProcesses
return targetProcesses.reduce((max, item) => (item.uptimeSeconds > max.uptimeSeconds ? item : max)).uptimeSeconds
}
/** 格式化单个运行时间单位。 */
function formatUptimeUnit(value: number, unit: 'day' | 'hour' | 'minute' | 'second') {
const unitKey = value === 1 ? unit : `${unit}s`
return t(`setting.about.uptimeUnits.${unitKey}`, { count: value })
}
/** 将运行秒数格式化为两段以内的友好文本例如“3天 2小时”。 */
function formatUptimeDuration(totalSeconds: number) {
const normalizedSeconds = Math.max(0, Math.floor(totalSeconds))
const days = Math.floor(normalizedSeconds / 86400)
const hours = Math.floor((normalizedSeconds % 86400) / 3600)
const minutes = Math.floor((normalizedSeconds % 3600) / 60)
const seconds = normalizedSeconds % 60
const parts: string[] = []
if (days > 0) parts.push(formatUptimeUnit(days, 'day'))
if (hours > 0) parts.push(formatUptimeUnit(hours, 'hour'))
if (minutes > 0 && parts.length < 2) parts.push(formatUptimeUnit(minutes, 'minute'))
if (!parts.length) parts.push(formatUptimeUnit(seconds, 'second'))
return parts.slice(0, 2).join(' ')
}
// 打开日志对话框
function showReleaseDialog(title: string, body: string) {
releaseDialogTitle.value = title
@@ -151,6 +253,17 @@ async function querySystemEnv() {
}
}
// 查询系统运行时间
async function querySystemUptime() {
try {
const processes: SystemProcess[] = await api.get('dashboard/processes')
syncSystemUptime(resolveSystemUptimeSeconds(processes))
} catch (error) {
console.log(error)
}
}
// 查询所有Release
async function queryAllRelease() {
try {
@@ -192,8 +305,17 @@ async function clearCache() {
onMounted(() => {
querySystemEnv()
querySystemUptime()
queryAllRelease()
querySupportingSites()
systemUptimeTimer = setInterval(() => {
if (systemUptimeBaseSeconds.value !== null) systemUptimeNow.value = Date.now()
}, 1000)
})
onBeforeUnmount(() => {
if (systemUptimeTimer) clearInterval(systemUptimeTimer)
})
</script>
@@ -321,6 +443,16 @@ onMounted(() => {
</dd>
</div>
</div>
<div v-if="systemUptimeText">
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="block text-sm font-bold">{{ t('setting.about.systemUptime') }}</dt>
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
<span class="flex-grow flex flex-row items-center truncate">
<code class="truncate">{{ systemUptimeText }}</code>
</span>
</dd>
</div>
</div>
<div>
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="block text-sm font-bold">{{ t('setting.about.supportingSites') }}</dt>

View File

@@ -1406,6 +1406,7 @@ export default {
title: 'About MoviePilot',
softwareVersion: 'Software Version',
frontendVersion: 'Frontend Version',
systemUptime: 'System Uptime',
browserVersion: 'Browser Cached Version',
authVersion: 'Auth Resource Version',
indexerVersion: 'Indexer Resource Version',
@@ -1439,6 +1440,16 @@ export default {
users: 'Users',
lastUpdated: 'Updated At',
noVersionStatisticData: 'No statistics data',
uptimeUnits: {
day: '{count} day',
days: '{count} days',
hour: '{count} hour',
hours: '{count} hours',
minute: '{count} minute',
minutes: '{count} minutes',
second: '{count} second',
seconds: '{count} seconds',
},
},
system: {
custom: 'Custom',

View File

@@ -1401,6 +1401,7 @@ export default {
title: '关于 MoviePilot',
softwareVersion: '软件版本',
frontendVersion: '前端版本',
systemUptime: '系统已运行',
browserVersion: '浏览器缓存版本',
authVersion: '认证资源版本',
indexerVersion: '站点资源版本',
@@ -1434,6 +1435,16 @@ export default {
users: '用户数',
lastUpdated: '更新时间',
noVersionStatisticData: '暂无统计数据',
uptimeUnits: {
day: '{count}天',
days: '{count}天',
hour: '{count}小时',
hours: '{count}小时',
minute: '{count}分钟',
minutes: '{count}分钟',
second: '{count}秒',
seconds: '{count}秒',
},
},
system: {
custom: '自定义',

View File

@@ -1402,6 +1402,7 @@ export default {
title: '關於 MoviePilot',
softwareVersion: '軟件版本',
frontendVersion: '前端版本',
systemUptime: '系統已運行',
browserVersion: '瀏覽器緩存版本',
authVersion: '認證資源版本',
indexerVersion: '站點資源版本',
@@ -1435,6 +1436,16 @@ export default {
users: '用戶數',
lastUpdated: '更新時間',
noVersionStatisticData: '暫無統計數據',
uptimeUnits: {
day: '{count}天',
days: '{count}天',
hour: '{count}小時',
hours: '{count}小時',
minute: '{count}分鐘',
minutes: '{count}分鐘',
second: '{count}秒',
seconds: '{count}秒',
},
},
system: {
custom: '自定義',