Commit 0da51507 authored by Edric Li's avatar Edric Li
Browse files

feat(ops): 添加运维监控全屏模式

- 支持通过 URL 参数 ?fullscreen=1 进入全屏模式
- 全屏模式下隐藏非必要 UI 元素(选择器、按钮、提示等)
- 增大健康评分圆环和字体以提升可读性
- 支持 ESC 键退出全屏
- 添加全屏按钮的 i18n 翻译
parent 3b71bc3d
...@@ -1943,6 +1943,9 @@ export default { ...@@ -1943,6 +1943,9 @@ export default {
'6h': 'Last 6 hours', '6h': 'Last 6 hours',
'24h': 'Last 24 hours' '24h': 'Last 24 hours'
}, },
fullscreen: {
enter: 'Enter Fullscreen'
},
diagnosis: { diagnosis: {
title: 'Smart Diagnosis', title: 'Smart Diagnosis',
footer: 'Automated diagnostic suggestions based on current metrics', footer: 'Automated diagnostic suggestions based on current metrics',
......
...@@ -2088,6 +2088,9 @@ export default { ...@@ -2088,6 +2088,9 @@ export default {
'6h': '近6小时', '6h': '近6小时',
'24h': '近24小时' '24h': '近24小时'
}, },
fullscreen: {
enter: '进入全屏'
},
diagnosis: { diagnosis: {
title: '智能诊断', title: '智能诊断',
footer: '基于当前指标的自动诊断建议', footer: '基于当前指标的自动诊断建议',
......
<template> <template>
<AppLayout> <component :is="isFullscreen ? 'div' : AppLayout" :class="isFullscreen ? 'flex min-h-screen flex-col justify-center bg-gray-50 dark:bg-dark-950' : ''">
<div class="space-y-6 pb-12"> <div :class="[isFullscreen ? 'p-4 md:p-6' : '', 'space-y-6 pb-12']">
<div <div
v-if="errorMessage" v-if="errorMessage"
class="rounded-2xl bg-red-50 p-4 text-sm text-red-600 dark:bg-red-900/20 dark:text-red-400" class="rounded-2xl bg-red-50 p-4 text-sm text-red-600 dark:bg-red-900/20 dark:text-red-400"
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
:thresholds="metricThresholds" :thresholds="metricThresholds"
:auto-refresh-enabled="autoRefreshEnabled" :auto-refresh-enabled="autoRefreshEnabled"
:auto-refresh-countdown="autoRefreshCountdown" :auto-refresh-countdown="autoRefreshCountdown"
:fullscreen="isFullscreen"
@update:time-range="onTimeRangeChange" @update:time-range="onTimeRangeChange"
@update:platform="onPlatformChange" @update:platform="onPlatformChange"
@update:group="onGroupChange" @update:group="onGroupChange"
...@@ -31,6 +32,8 @@ ...@@ -31,6 +32,8 @@
@open-error-details="openErrorDetails" @open-error-details="openErrorDetails"
@open-settings="showSettingsDialog = true" @open-settings="showSettingsDialog = true"
@open-alert-rules="showAlertRulesCard = true" @open-alert-rules="showAlertRulesCard = true"
@enter-fullscreen="enterFullscreen"
@exit-fullscreen="exitFullscreen"
/> />
<!-- Row: Concurrency + Throughput --> <!-- Row: Concurrency + Throughput -->
...@@ -45,6 +48,7 @@ ...@@ -45,6 +48,7 @@
:top-groups="throughputTrend?.top_groups ?? []" :top-groups="throughputTrend?.top_groups ?? []"
:loading="loadingTrend" :loading="loadingTrend"
:time-range="timeRange" :time-range="timeRange"
:fullscreen="isFullscreen"
@select-platform="handleThroughputSelectPlatform" @select-platform="handleThroughputSelectPlatform"
@select-group="handleThroughputSelectGroup" @select-group="handleThroughputSelectGroup"
@open-details="handleOpenRequestDetails" @open-details="handleOpenRequestDetails"
...@@ -72,36 +76,37 @@ ...@@ -72,36 +76,37 @@
<!-- Alert Events --> <!-- Alert Events -->
<OpsAlertEventsCard v-if="opsEnabled && !(loading && !hasLoadedOnce)" /> <OpsAlertEventsCard v-if="opsEnabled && !(loading && !hasLoadedOnce)" />
<!-- Settings Dialog --> <!-- Settings Dialog (hidden in fullscreen mode) -->
<OpsSettingsDialog :show="showSettingsDialog" @close="showSettingsDialog = false" @saved="onSettingsSaved" /> <template v-if="!isFullscreen">
<OpsSettingsDialog :show="showSettingsDialog" @close="showSettingsDialog = false" @saved="onSettingsSaved" />
<!-- Alert Rules Dialog --> <BaseDialog :show="showAlertRulesCard" :title="t('admin.ops.alertRules.title')" width="extra-wide" @close="showAlertRulesCard = false">
<BaseDialog :show="showAlertRulesCard" :title="t('admin.ops.alertRules.title')" width="extra-wide" @close="showAlertRulesCard = false"> <OpsAlertRulesCard />
<OpsAlertRulesCard /> </BaseDialog>
</BaseDialog>
<OpsErrorDetailsModal <OpsErrorDetailsModal
:show="showErrorDetails" :show="showErrorDetails"
:time-range="timeRange" :time-range="timeRange"
:platform="platform" :platform="platform"
:group-id="groupId" :group-id="groupId"
:error-type="errorDetailsType" :error-type="errorDetailsType"
@update:show="showErrorDetails = $event" @update:show="showErrorDetails = $event"
@openErrorDetail="openError" @openErrorDetail="openError"
/> />
<OpsErrorDetailModal v-model:show="showErrorModal" :error-id="selectedErrorId" /> <OpsErrorDetailModal v-model:show="showErrorModal" :error-id="selectedErrorId" />
<OpsRequestDetailsModal <OpsRequestDetailsModal
v-model="showRequestDetails" v-model="showRequestDetails"
:time-range="timeRange" :time-range="timeRange"
:preset="requestDetailsPreset" :preset="requestDetailsPreset"
:platform="platform" :platform="platform"
:group-id="groupId" :group-id="groupId"
@openErrorDetail="openError" @openErrorDetail="openError"
/> />
</template>
</div> </div>
</AppLayout> </component>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
...@@ -163,12 +168,36 @@ const QUERY_KEYS = { ...@@ -163,12 +168,36 @@ const QUERY_KEYS = {
timeRange: 'tr', timeRange: 'tr',
platform: 'platform', platform: 'platform',
groupId: 'group_id', groupId: 'group_id',
queryMode: 'mode' queryMode: 'mode',
fullscreen: 'fullscreen'
} as const } as const
const isApplyingRouteQuery = ref(false) const isApplyingRouteQuery = ref(false)
const isSyncingRouteQuery = ref(false) const isSyncingRouteQuery = ref(false)
// Fullscreen mode
const isFullscreen = computed(() => {
const val = route.query[QUERY_KEYS.fullscreen]
return val === '1' || val === 'true'
})
function exitFullscreen() {
const nextQuery = { ...route.query }
delete nextQuery[QUERY_KEYS.fullscreen]
router.replace({ query: nextQuery })
}
function enterFullscreen() {
const nextQuery = { ...route.query, [QUERY_KEYS.fullscreen]: '1' }
router.replace({ query: nextQuery })
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape' && isFullscreen.value) {
exitFullscreen()
}
}
let dashboardFetchController: AbortController | null = null let dashboardFetchController: AbortController | null = null
let dashboardFetchSeq = 0 let dashboardFetchSeq = 0
...@@ -603,6 +632,9 @@ watch( ...@@ -603,6 +632,9 @@ watch(
) )
onMounted(async () => { onMounted(async () => {
// Fullscreen mode: listen for ESC key
window.addEventListener('keydown', handleKeydown)
await adminSettingsStore.fetch() await adminSettingsStore.fetch()
if (!adminSettingsStore.opsMonitoringEnabled) { if (!adminSettingsStore.opsMonitoringEnabled) {
await router.replace('/admin/settings') await router.replace('/admin/settings')
...@@ -637,6 +669,7 @@ async function loadThresholds() { ...@@ -637,6 +669,7 @@ async function loadThresholds() {
} }
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('keydown', handleKeydown)
abortDashboardFetch() abortDashboardFetch()
pauseAutoRefresh() pauseAutoRefresh()
pauseCountdown() pauseCountdown()
......
...@@ -19,6 +19,7 @@ interface Props { ...@@ -19,6 +19,7 @@ interface Props {
timeRange: string timeRange: string
byPlatform?: OpsThroughputPlatformBreakdownItem[] byPlatform?: OpsThroughputPlatformBreakdownItem[]
topGroups?: OpsThroughputGroupBreakdownItem[] topGroups?: OpsThroughputGroupBreakdownItem[]
fullscreen?: boolean
} }
const props = defineProps<Props>() const props = defineProps<Props>()
...@@ -179,38 +180,40 @@ function downloadChart() { ...@@ -179,38 +180,40 @@ function downloadChart() {
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg> </svg>
{{ t('admin.ops.throughputTrend') }} {{ t('admin.ops.throughputTrend') }}
<HelpTooltip :content="t('admin.ops.tooltips.throughputTrend')" /> <HelpTooltip v-if="!props.fullscreen" :content="t('admin.ops.tooltips.throughputTrend')" />
</h3> </h3>
<div class="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400"> <div class="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
<span class="flex items-center gap-1"><span class="h-2 w-2 rounded-full bg-blue-500"></span>{{ t('admin.ops.qps') }}</span> <span class="flex items-center gap-1"><span class="h-2 w-2 rounded-full bg-blue-500"></span>{{ t('admin.ops.qps') }}</span>
<span class="flex items-center gap-1"><span class="h-2 w-2 rounded-full bg-green-500"></span>{{ t('admin.ops.tpsK') }}</span> <span class="flex items-center gap-1"><span class="h-2 w-2 rounded-full bg-green-500"></span>{{ t('admin.ops.tpsK') }}</span>
<button <template v-if="!props.fullscreen">
type="button" <button
class="ml-2 inline-flex items-center rounded-lg border border-gray-200 bg-white px-2 py-1 text-[11px] font-semibold text-gray-600 hover:bg-gray-50 disabled:opacity-50 dark:border-dark-700 dark:bg-dark-900 dark:text-gray-300 dark:hover:bg-dark-800" type="button"
:disabled="state !== 'ready'" class="ml-2 inline-flex items-center rounded-lg border border-gray-200 bg-white px-2 py-1 text-[11px] font-semibold text-gray-600 hover:bg-gray-50 disabled:opacity-50 dark:border-dark-700 dark:bg-dark-900 dark:text-gray-300 dark:hover:bg-dark-800"
:title="t('admin.ops.requestDetails.title')" :disabled="state !== 'ready'"
@click="emit('openDetails')" :title="t('admin.ops.requestDetails.title')"
> @click="emit('openDetails')"
{{ t('admin.ops.requestDetails.details') }} >
</button> {{ t('admin.ops.requestDetails.details') }}
<button </button>
type="button" <button
class="ml-2 inline-flex items-center rounded-lg border border-gray-200 bg-white px-2 py-1 text-[11px] font-semibold text-gray-600 hover:bg-gray-50 disabled:opacity-50 dark:border-dark-700 dark:bg-dark-900 dark:text-gray-300 dark:hover:bg-dark-800" type="button"
:disabled="state !== 'ready'" class="ml-2 inline-flex items-center rounded-lg border border-gray-200 bg-white px-2 py-1 text-[11px] font-semibold text-gray-600 hover:bg-gray-50 disabled:opacity-50 dark:border-dark-700 dark:bg-dark-900 dark:text-gray-300 dark:hover:bg-dark-800"
:title="t('admin.ops.charts.resetZoomHint')" :disabled="state !== 'ready'"
@click="resetZoom" :title="t('admin.ops.charts.resetZoomHint')"
> @click="resetZoom"
{{ t('admin.ops.charts.resetZoom') }} >
</button> {{ t('admin.ops.charts.resetZoom') }}
<button </button>
type="button" <button
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-2 py-1 text-[11px] font-semibold text-gray-600 hover:bg-gray-50 disabled:opacity-50 dark:border-dark-700 dark:bg-dark-900 dark:text-gray-300 dark:hover:bg-dark-800" type="button"
:disabled="state !== 'ready'" class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-2 py-1 text-[11px] font-semibold text-gray-600 hover:bg-gray-50 disabled:opacity-50 dark:border-dark-700 dark:bg-dark-900 dark:text-gray-300 dark:hover:bg-dark-800"
:title="t('admin.ops.charts.downloadChartHint')" :disabled="state !== 'ready'"
@click="downloadChart" :title="t('admin.ops.charts.downloadChartHint')"
> @click="downloadChart"
{{ t('admin.ops.charts.downloadChart') }} >
</button> {{ t('admin.ops.charts.downloadChart') }}
</button>
</template>
</div> </div>
</div> </div>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment