This commit is contained in:
2026-10-27 09:38:05 +07:00
parent 1ce8efeef5
commit ccf7240584
8 changed files with 147 additions and 3 deletions
+9 -1
View File
@@ -1,8 +1,9 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, onUnmounted, ref } from 'vue'
import { NAlert, NButton, NCard, NSpace, NTag, useDialog } from 'naive-ui'
import type { GameCommand, OnlineIntent, OnlineState } from '../types/online'
import { gameCommands, gameStatuses } from '../utils/game-status'
import { formatGameDuration } from '../utils/gameDuration'
const props = defineProps<{
state: OnlineState
@@ -13,6 +14,12 @@ const props = defineProps<{
const emit = defineEmits<{ action: [intent: OnlineIntent] }>()
const dialog = useDialog()
const game = computed(() => props.state.game?.key === props.selectedKey ? props.state.game : null)
const now = ref(Date.now())
const duration = computed(() => game.value
? formatGameDuration(game.value.startedAt, game.value.endedAt, now.value)
: '')
const tick = setInterval(() => { now.value = Date.now() }, 1000)
onUnmounted(() => clearInterval(tick))
const status = computed(() => game.value ? gameStatuses[game.value.status] : null)
const disabled = computed(() => props.isBusy || props.state.busy || !props.state.authenticated || !!props.state.gameError)
const availableCommands = computed(() => game.value && props.selectedKey ? gameCommands[game.value.status] : [])
@@ -52,6 +59,7 @@ function run(command: GameCommand) {
<n-tag role="status" data-testid="game-state" :type="state.gameError ? 'warning' : status?.type ?? 'info'">
{{ !selectedKey ? 'Игра не выбрана' : status?.label ?? (!state.authenticated ? 'Требуется вход' : state.gameError ? 'Нет данных' : 'Загрузка…') }}
</n-tag>
<n-tag v-if="game" type="info" data-testid="game-duration">{{ duration }}</n-tag>
</div>
</template>
<template #header-extra>
+1 -1
View File
@@ -36,7 +36,7 @@ export type OnlineIntent =
| { action: 'select-game' | 'download' | 'check'; gameId: number }
| { action: 'remove'; key: string }
export interface OnlineState {
game: { key: string; id: number; name: string; status: GameStatus } | null
game: { key: string; id: number; name: string; status: GameStatus; startedAt: string; endedAt: string } | null
gameError: string
server: string
currentGameId: number | null
+24
View File
@@ -0,0 +1,24 @@
export function formatGameDuration(startedAt: string, endedAt: string, now: number): string {
if (!startedAt) return 'Игра ещё не началась'
const start = Date.parse(startedAt)
// Совместимость с GamePage: серверные TIMESTAMP содержат местные часы как UTC.
const localNow = now - new Date(now).getTimezoneOffset() * 60000
const end = endedAt ? Date.parse(endedAt) : localNow
const diff = end - start
if (!Number.isFinite(diff)) return 'Время игры недоступно'
if (diff < 0) return 'Игра ещё не началась'
const seconds = Math.floor(diff / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)
const months = Math.floor(days / 30)
const years = Math.floor(days / 365)
if (years > 0) return `${years}г ${days % 365}д`
if (months > 0) return `${months}мес ${days % 30}д`
if (days > 0) return `${days}д ${hours % 24}ч`
if (hours > 0) return `${hours}ч ${minutes % 60}м`
if (minutes > 0) return `${minutes}м ${seconds % 60}с`
return `${seconds}с`
}