Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
bb1fd54d
Unverified
Commit
bb1fd54d
authored
Jan 13, 2026
by
Wesley Liddick
Committed by
GitHub
Jan 13, 2026
Browse files
Merge pull request #257 from Edric-Li/feat/ops-fullscreen-scrollbar
feat(ops): 添加运维监控全屏模式 & 优化滚动条
parents
3b71bc3d
d02e1db0
Changes
6
Hide whitespace changes
Inline
Side-by-side
frontend/src/i18n/locales/en.ts
View file @
bb1fd54d
...
...
@@ -1943,6 +1943,9 @@ export default {
'
6h
'
:
'
Last 6 hours
'
,
'
24h
'
:
'
Last 24 hours
'
},
fullscreen
:
{
enter
:
'
Enter Fullscreen
'
},
diagnosis
:
{
title
:
'
Smart Diagnosis
'
,
footer
:
'
Automated diagnostic suggestions based on current metrics
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
bb1fd54d
...
...
@@ -2088,6 +2088,9 @@ export default {
'
6h
'
:
'
近6小时
'
,
'
24h
'
:
'
近24小时
'
},
fullscreen
:
{
enter
:
'
进入全屏
'
},
diagnosis
:
{
title
:
'
智能诊断
'
,
footer
:
'
基于当前指标的自动诊断建议
'
,
...
...
frontend/src/style.css
View file @
bb1fd54d
...
...
@@ -19,7 +19,22 @@
@apply
min-h-screen;
}
/* 自定义滚动条 */
/* 自定义滚动条 - 默认隐藏,悬停或滚动时显示 */
*
{
scrollbar-width
:
thin
;
scrollbar-color
:
transparent
transparent
;
}
*
:hover
,
*
:focus-within
{
scrollbar-color
:
rgba
(
156
,
163
,
175
,
0.5
)
transparent
;
}
.dark
*
:hover
,
.dark
*
:focus-within
{
scrollbar-color
:
rgba
(
75
,
85
,
99
,
0.5
)
transparent
;
}
::-webkit-scrollbar
{
@apply
h-2
w-2;
}
...
...
@@ -29,10 +44,15 @@
}
::-webkit-scrollbar-thumb
{
@apply
rounded-full
bg-gray-300
dark
:
bg-dark-600
;
@apply
rounded-full
bg-transparent;
transition
:
background-color
0.2s
ease
;
}
*
:hover::-webkit-scrollbar-thumb
{
@apply
bg-gray-300/50
dark
:
bg-dark-600
/
50
;
}
::-webkit-scrollbar-thumb:hover
{
*
:hover
::-webkit-scrollbar-thumb:hover
{
@apply
bg-gray-400
dark
:
bg-dark-500
;
}
...
...
frontend/src/views/admin/ops/OpsDashboard.vue
View file @
bb1fd54d
<
template
>
<
AppLayout
>
<div
class=
"space-y-6 pb-12"
>
<
component
:is=
"isFullscreen ? 'div' : AppLayout"
:class=
"isFullscreen ? 'flex min-h-screen flex-col justify-center bg-gray-50 dark:bg-dark-950' : ''"
>
<div
:
class=
"
[isFullscreen ? 'p-4 md:p-6' : '', '
space-y-6 pb-12
']
"
>
<div
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"
...
...
@@ -22,6 +22,7 @@
:thresholds=
"metricThresholds"
:auto-refresh-enabled=
"autoRefreshEnabled"
:auto-refresh-countdown=
"autoRefreshCountdown"
:fullscreen=
"isFullscreen"
@
update:time-range=
"onTimeRangeChange"
@
update:platform=
"onPlatformChange"
@
update:group=
"onGroupChange"
...
...
@@ -31,6 +32,8 @@
@
open-error-details=
"openErrorDetails"
@
open-settings=
"showSettingsDialog = true"
@
open-alert-rules=
"showAlertRulesCard = true"
@
enter-fullscreen=
"enterFullscreen"
@
exit-fullscreen=
"exitFullscreen"
/>
<!-- Row: Concurrency + Throughput -->
...
...
@@ -45,6 +48,7 @@
:top-groups=
"throughputTrend?.top_groups ?? []"
:loading=
"loadingTrend"
:time-range=
"timeRange"
:fullscreen=
"isFullscreen"
@
select-platform=
"handleThroughputSelectPlatform"
@
select-group=
"handleThroughputSelectGroup"
@
open-details=
"handleOpenRequestDetails"
...
...
@@ -72,36 +76,37 @@
<!-- Alert Events -->
<OpsAlertEventsCard
v-if=
"opsEnabled && !(loading && !hasLoadedOnce)"
/>
<!-- Settings Dialog -->
<OpsSettingsDialog
:show=
"showSettingsDialog"
@
close=
"showSettingsDialog = false"
@
saved=
"onSettingsSaved"
/>
<!-- Settings Dialog (hidden in fullscreen mode) -->
<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"
>
<OpsAlertRulesCard
/>
</BaseDialog>
<BaseDialog
:show=
"showAlertRulesCard"
:title=
"t('admin.ops.alertRules.title')"
width=
"extra-wide"
@
close=
"showAlertRulesCard = false"
>
<OpsAlertRulesCard
/>
</BaseDialog>
<OpsErrorDetailsModal
:show=
"showErrorDetails"
:time-range=
"timeRange"
:platform=
"platform"
:group-id=
"groupId"
:error-type=
"errorDetailsType"
@
update:show=
"showErrorDetails = $event"
@
openErrorDetail=
"openError"
/>
<OpsErrorDetailsModal
:show=
"showErrorDetails"
:time-range=
"timeRange"
:platform=
"platform"
:group-id=
"groupId"
:error-type=
"errorDetailsType"
@
update:show=
"showErrorDetails = $event"
@
openErrorDetail=
"openError"
/>
<OpsErrorDetailModal
v-model:show=
"showErrorModal"
:error-id=
"selectedErrorId"
/>
<OpsErrorDetailModal
v-model:show=
"showErrorModal"
:error-id=
"selectedErrorId"
/>
<OpsRequestDetailsModal
v-model=
"showRequestDetails"
:time-range=
"timeRange"
:preset=
"requestDetailsPreset"
:platform=
"platform"
:group-id=
"groupId"
@
openErrorDetail=
"openError"
/>
<OpsRequestDetailsModal
v-model=
"showRequestDetails"
:time-range=
"timeRange"
:preset=
"requestDetailsPreset"
:platform=
"platform"
:group-id=
"groupId"
@
openErrorDetail=
"openError"
/>
</
template
>
</div>
</
AppLayou
t>
</
componen
t>
</template>
<
script
setup
lang=
"ts"
>
...
...
@@ -163,12 +168,36 @@ const QUERY_KEYS = {
timeRange
:
'
tr
'
,
platform
:
'
platform
'
,
groupId
:
'
group_id
'
,
queryMode
:
'
mode
'
queryMode
:
'
mode
'
,
fullscreen
:
'
fullscreen
'
}
as
const
const
isApplyingRouteQuery
=
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
dashboardFetchSeq
=
0
...
...
@@ -603,6 +632,9 @@ watch(
)
onMounted
(
async
()
=>
{
// Fullscreen mode: listen for ESC key
window
.
addEventListener
(
'
keydown
'
,
handleKeydown
)
await
adminSettingsStore
.
fetch
()
if
(
!
adminSettingsStore
.
opsMonitoringEnabled
)
{
await
router
.
replace
(
'
/admin/settings
'
)
...
...
@@ -637,6 +669,7 @@ async function loadThresholds() {
}
onUnmounted
(()
=>
{
window
.
removeEventListener
(
'
keydown
'
,
handleKeydown
)
abortDashboardFetch
()
pauseAutoRefresh
()
pauseCountdown
()
...
...
frontend/src/views/admin/ops/components/OpsDashboardHeader.vue
View file @
bb1fd54d
...
...
@@ -25,6 +25,7 @@ interface Props {
thresholds
?:
OpsMetricThresholds
|
null
// 阈值配置
autoRefreshEnabled
?:
boolean
autoRefreshCountdown
?:
number
fullscreen
?:
boolean
}
interface
Emits
{
...
...
@@ -37,6 +38,8 @@ interface Emits {
(
e
:
'
openErrorDetails
'
,
kind
:
'
request
'
|
'
upstream
'
):
void
(
e
:
'
openSettings
'
):
void
(
e
:
'
openAlertRules
'
):
void
(
e
:
'
enterFullscreen
'
):
void
(
e
:
'
exitFullscreen
'
):
void
}
const
props
=
defineProps
<
Props
>
()
...
...
@@ -391,15 +394,15 @@ const healthScoreClass = computed(() => {
return
'
text-red-500
'
})
const
circleSize
=
100
const
strokeWidth
=
8
const
radius
=
(
circleSize
-
strokeWidth
)
/
2
const
circumference
=
2
*
Math
.
PI
*
radius
const
circleSize
=
computed
(()
=>
props
.
fullscreen
?
140
:
100
)
const
strokeWidth
=
computed
(()
=>
props
.
fullscreen
?
10
:
8
)
const
radius
=
computed
(()
=>
(
circleSize
.
value
-
strokeWidth
.
value
)
/
2
)
const
circumference
=
computed
(()
=>
2
*
Math
.
PI
*
radius
.
value
)
const
dashOffset
=
computed
(()
=>
{
if
(
isSystemIdle
.
value
)
return
0
if
(
healthScoreValue
.
value
==
null
)
return
0
const
score
=
Math
.
max
(
0
,
Math
.
min
(
100
,
healthScoreValue
.
value
))
return
circumference
-
(
score
/
100
)
*
circumference
return
circumference
.
value
-
(
score
/
100
)
*
circumference
.
value
})
interface
DiagnosisItem
{
...
...
@@ -814,7 +817,7 @@ function handleToolbarRefresh() {
</
script
>
<
template
>
<div
class=
"flex flex-col gap-4 rounded-3xl bg-white
p-6
shadow-sm ring-1 ring-gray-900/5 dark:bg-dark-800 dark:ring-dark-700"
>
<div
:
class=
"
['
flex flex-col gap-4 rounded-3xl bg-white shadow-sm ring-1 ring-gray-900/5 dark:bg-dark-800 dark:ring-dark-700
', props.fullscreen ? 'p-8' : 'p-6']
"
>
<!-- Top Toolbar -->
<div
class=
"flex flex-wrap items-center justify-between gap-4 border-b border-gray-100 pb-4 dark:border-dark-700"
>
<div>
...
...
@@ -830,7 +833,7 @@ function handleToolbarRefresh() {
{{
t
(
'
admin.ops.title
'
)
}}
</h1>
<div
class=
"mt-1 flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400"
>
<div
v-if=
"!props.fullscreen"
class=
"mt-1 flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400"
>
<span
class=
"flex items-center gap-1.5"
:title=
"props.loading ? t('admin.ops.loadingText') : t('admin.ops.ready')"
>
<span
class=
"relative flex h-2 w-2"
>
<span
class=
"relative inline-flex h-2 w-2 rounded-full"
:class=
"props.loading ? 'bg-gray-400' : 'bg-green-500'"
></span>
...
...
@@ -863,28 +866,30 @@ function handleToolbarRefresh() {
</div>
<div
class=
"flex flex-wrap items-center gap-3"
>
<Select
:model-value=
"platform"
:options=
"platformOptions"
class=
"w-full sm:w-[140px]"
@
update:model-value=
"handlePlatformChange"
/>
<Select
:model-value=
"groupId"
:options=
"groupOptions"
class=
"w-full sm:w-[160px]"
@
update:model-value=
"handleGroupChange"
/>
<div
class=
"mx-1 hidden h-4 w-[1px] bg-gray-200 dark:bg-dark-700 sm:block"
></div>
<Select
:model-value=
"timeRange"
:options=
"timeRangeOptions"
class=
"relative w-full sm:w-[150px]"
@
update:model-value=
"handleTimeRangeChange"
/>
<
template
v-if=
"!props.fullscreen"
>
<Select
:model-value=
"platform"
:options=
"platformOptions"
class=
"w-full sm:w-[140px]"
@
update:model-value=
"handlePlatformChange"
/>
<Select
:model-value=
"groupId"
:options=
"groupOptions"
class=
"w-full sm:w-[160px]"
@
update:model-value=
"handleGroupChange"
/>
<div
class=
"mx-1 hidden h-4 w-[1px] bg-gray-200 dark:bg-dark-700 sm:block"
></div>
<Select
:model-value=
"timeRange"
:options=
"timeRangeOptions"
class=
"relative w-full sm:w-[150px]"
@
update:model-value=
"handleTimeRangeChange"
/>
</
template
>
<Select
v-if=
"false"
...
...
@@ -895,6 +900,7 @@ function handleToolbarRefresh() {
/>
<button
v-if=
"!props.fullscreen"
type=
"button"
class=
"flex h-8 w-8 items-center justify-center rounded-lg bg-gray-100 text-gray-500 transition-colors hover:bg-gray-200 dark:bg-dark-700 dark:text-gray-400 dark:hover:bg-dark-600"
:disabled=
"loading"
...
...
@@ -911,9 +917,11 @@ function handleToolbarRefresh() {
</svg>
</button>
<div
class=
"mx-1 hidden h-4 w-[1px] bg-gray-200 dark:bg-dark-700 sm:block"
></div>
<div
v-if=
"!props.fullscreen"
class=
"mx-1 hidden h-4 w-[1px] bg-gray-200 dark:bg-dark-700 sm:block"
></div>
<!-- Alert Rules Button (hidden in fullscreen) -->
<button
v-if=
"!props.fullscreen"
type=
"button"
class=
"flex h-8 items-center gap-1.5 rounded-lg bg-blue-100 px-3 text-xs font-bold text-blue-700 transition-colors hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:hover:bg-blue-900/50"
:title=
"t('admin.ops.alertRules.title')"
...
...
@@ -925,7 +933,9 @@ function handleToolbarRefresh() {
<span
class=
"hidden sm:inline"
>
{{ t('admin.ops.alertRules.manage') }}
</span>
</button>
<!-- Settings Button (hidden in fullscreen) -->
<button
v-if=
"!props.fullscreen"
type=
"button"
class=
"flex h-8 items-center gap-1.5 rounded-lg bg-gray-100 px-3 text-xs font-bold text-gray-700 transition-colors hover:bg-gray-200 dark:bg-dark-700 dark:text-gray-300 dark:hover:bg-dark-600"
:title=
"t('admin.ops.settings.title')"
...
...
@@ -937,13 +947,26 @@ function handleToolbarRefresh() {
</svg>
<span
class=
"hidden sm:inline"
>
{{ t('common.settings') }}
</span>
</button>
<!-- Enter Fullscreen Button (hidden in fullscreen mode) -->
<button
v-if=
"!props.fullscreen"
type=
"button"
class=
"flex h-8 w-8 items-center justify-center rounded-lg bg-gray-100 text-gray-700 transition-colors hover:bg-gray-200 dark:bg-dark-700 dark:text-gray-300 dark:hover:bg-dark-600"
:title=
"t('admin.ops.fullscreen.enter')"
@
click=
"emit('enterFullscreen')"
>
<svg
class=
"h-4 w-4"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
stroke-width=
"2"
d=
"M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
/>
</svg>
</button>
</div>
</div>
<div
v-if=
"overview"
class=
"grid grid-cols-1 gap-6 lg:grid-cols-12"
>
<!-- Left: Health + Realtime -->
<div
class=
"rounded-2xl bg-gray-50
p-4
dark:bg-dark-900 lg:col-span-5"
>
<div
class=
"grid grid-cols-1 gap-6 md:grid-cols-[200px_1fr] md:items-center"
>
<div
:
class=
"
['
rounded-2xl bg-gray-50 dark:bg-dark-900 lg:col-span-5
', props.fullscreen ? 'p-6' : 'p-4']
"
>
<div
class=
"grid
h-full
grid-cols-1 gap-6 md:grid-cols-[200px_1fr] md:items-center"
>
<!-- 1) Health Score -->
<div
class=
"group relative flex cursor-pointer flex-col items-center justify-center rounded-xl py-2 transition-all hover:bg-white/60 dark:hover:bg-dark-800/60 md:border-r md:border-gray-200 md:pr-6 dark:md:border-dark-700"
...
...
@@ -1026,14 +1049,14 @@ function handleToolbarRefresh() {
</svg>
<div
class=
"absolute flex flex-col items-center"
>
<span
class=
"text-3xl
font-black
"
:class=
"
healthScoreClass"
>
<span
:
class=
"
[props.fullscreen ? 'text-5xl' : '
text-3xl
', '
font-black
',
healthScoreClass
]
"
>
{{ isSystemIdle ? t('admin.ops.idleStatus') : (overview.health_score ?? '--') }}
</span>
<span
class=
"text-[10px]
font-bold uppercase tracking-wider text-gray-400"
>
{{ t('admin.ops.health') }}
</span>
<span
:
class=
"
[props.fullscreen ? 'text-xs' : '
text-[10px]
', '
font-bold uppercase tracking-wider text-gray-400
']
"
>
{{ t('admin.ops.health') }}
</span>
</div>
</div>
<div
class=
"mt-4 text-center"
>
<div
class=
"mt-4 text-center"
v-if=
"!props.fullscreen"
>
<div
class=
"flex items-center justify-center gap-1 text-xs font-medium text-gray-500"
>
{{ t('admin.ops.healthCondition') }}
<HelpTooltip
:content=
"t('admin.ops.healthHelp')"
/>
...
...
@@ -1051,7 +1074,7 @@ function handleToolbarRefresh() {
</div>
<!-- 2) Realtime Traffic -->
<div
class=
"flex flex-col justify-center py-2"
>
<div
class=
"flex
h-full
flex-col justify-center py-2"
>
<div
class=
"mb-3 flex flex-wrap items-center justify-between gap-2"
>
<div
class=
"flex items-center gap-2"
>
<div
class=
"relative flex h-3 w-3 shrink-0"
>
...
...
@@ -1059,7 +1082,7 @@ function handleToolbarRefresh() {
<span
class=
"relative inline-flex h-3 w-3 rounded-full bg-blue-500"
></span>
</div>
<h3
class=
"text-xs font-bold uppercase tracking-wider text-gray-400"
>
{{ t('admin.ops.realtime.title') }}
</h3>
<HelpTooltip
:content=
"t('admin.ops.tooltips.qps')"
/>
<HelpTooltip
v-if=
"!props.fullscreen"
:content=
"t('admin.ops.tooltips.qps')"
/>
</div>
<!-- Time Window Selector -->
...
...
@@ -1079,18 +1102,18 @@ function handleToolbarRefresh() {
</div>
</div>
<div
class=
"space-y-3"
>
<div
:
class=
"
props.fullscreen ? 'space-y-4' : '
space-y-3
'
"
>
<!-- Row 1: Current -->
<div>
<div
class=
"text-[10px]
font-bold uppercase text-gray-400"
>
{{ t('admin.ops.current') }}
</div>
<div
:
class=
"
[props.fullscreen ? 'text-xs' : '
text-[10px]
', '
font-bold uppercase text-gray-400
']
"
>
{{ t('admin.ops.current') }}
</div>
<div
class=
"mt-1 flex flex-wrap items-baseline gap-x-4 gap-y-2"
>
<div
class=
"flex items-baseline gap-1.5"
>
<span
class=
"text-xl
font-black text-gray-900 dark:text-white
sm:text-2xl
"
>
{{ displayRealTimeQps.toFixed(1) }}
</span>
<span
class=
"text-xs
font-bold text-gray-500"
>
QPS
</span>
<span
:
class=
"
[props.fullscreen ? '
text-
4
xl
' : 'text-xl sm:text-2xl', '
font-black text-gray-900 dark:text-white
']
"
>
{{ displayRealTimeQps.toFixed(1) }}
</span>
<span
:
class=
"
[props.fullscreen ? 'text-sm' : '
text-xs
', '
font-bold text-gray-500
']
"
>
QPS
</span>
</div>
<div
class=
"flex items-baseline gap-1.5"
>
<span
class=
"text-xl
font-black text-gray-900 dark:text-white
sm:text-2xl
"
>
{{ displayRealTimeTps.toFixed(1) }}
</span>
<span
class=
"text-xs
font-bold text-gray-500"
>
TPS
</span>
<span
:
class=
"
[props.fullscreen ? '
text-
4
xl
' : 'text-xl sm:text-2xl', '
font-black text-gray-900 dark:text-white
']
"
>
{{ displayRealTimeTps.toFixed(1) }}
</span>
<span
:
class=
"
[props.fullscreen ? 'text-sm' : '
text-xs
', '
font-bold text-gray-500
']
"
>
TPS
</span>
</div>
</div>
</div>
...
...
@@ -1099,8 +1122,8 @@ function handleToolbarRefresh() {
<div
class=
"grid grid-cols-2 gap-3"
>
<!-- Peak -->
<div>
<div
class=
"text-[10px]
font-bold uppercase text-gray-400"
>
{{ t('admin.ops.peak') }}
</div>
<div
class=
"mt-1 space-y-0.5
text-sm
font-medium text-gray-600 dark:text-gray-400"
>
<div
:
class=
"
[props.fullscreen ? 'text-xs' : '
text-[10px]
', '
font-bold uppercase text-gray-400
']
"
>
{{ t('admin.ops.peak') }}
</div>
<div
:
class=
"
[props.fullscreen ? 'text-base' : 'text-sm', '
mt-1 space-y-0.5 font-medium text-gray-600 dark:text-gray-400
']
"
>
<div
class=
"flex items-baseline gap-1.5"
>
<span
class=
"font-black text-gray-900 dark:text-white"
>
{{ realtimeQpsPeakLabel }}
</span>
<span
class=
"text-xs"
>
QPS
</span>
...
...
@@ -1114,8 +1137,8 @@ function handleToolbarRefresh() {
<!-- Average -->
<div>
<div
class=
"text-[10px]
font-bold uppercase text-gray-400"
>
{{ t('admin.ops.average') }}
</div>
<div
class=
"mt-1 space-y-0.5
text-sm
font-medium text-gray-600 dark:text-gray-400"
>
<div
:
class=
"
[props.fullscreen ? 'text-xs' : '
text-[10px]
', '
font-bold uppercase text-gray-400
']
"
>
{{ t('admin.ops.average') }}
</div>
<div
:
class=
"
[props.fullscreen ? 'text-base' : 'text-sm', '
mt-1 space-y-0.5 font-medium text-gray-600 dark:text-gray-400
']
"
>
<div
class=
"flex items-baseline gap-1.5"
>
<span
class=
"font-black text-gray-900 dark:text-white"
>
{{ realtimeQpsAvgLabel }}
</span>
<span
class=
"text-xs"
>
QPS
</span>
...
...
@@ -1156,15 +1179,16 @@ function handleToolbarRefresh() {
</div>
<!-- Right: 6 cards (3 cols x 2 rows) -->
<div
class=
"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:col-span-7 lg:grid-cols-3"
>
<div
class=
"grid
h-full
grid-cols-1
content-center
gap-4 sm:grid-cols-2 lg:col-span-7 lg:grid-cols-3"
>
<!-- Card 1: Requests -->
<div
class=
"rounded-2xl bg-gray-50 p-4 dark:bg-dark-900"
>
<div
class=
"flex items-center justify-between"
>
<div
class=
"flex items-center gap-1"
>
<span
class=
"text-[10px] font-bold uppercase text-gray-400"
>
{{ t('admin.ops.requestsTitle') }}
</span>
<HelpTooltip
:content=
"t('admin.ops.tooltips.totalRequests')"
/>
<HelpTooltip
v-if=
"!props.fullscreen"
:content=
"t('admin.ops.tooltips.totalRequests')"
/>
</div>
<button
v-if=
"!props.fullscreen"
class=
"text-[10px] font-bold text-blue-500 hover:underline"
type=
"button"
@
click=
"openDetails({ title: t('admin.ops.requestDetails.title') })"
...
...
@@ -1197,10 +1221,11 @@ function handleToolbarRefresh() {
<div
class=
"flex items-center justify-between"
>
<div
class=
"flex items-center gap-2"
>
<span
class=
"text-[10px] font-bold uppercase text-gray-400"
>
SLA
</span>
<HelpTooltip
:content=
"t('admin.ops.tooltips.sla')"
/>
<HelpTooltip
v-if=
"!props.fullscreen"
:content=
"t('admin.ops.tooltips.sla')"
/>
<span
class=
"h-1.5 w-1.5 rounded-full"
:class=
"isSLABelowThreshold(slaPercent) ? 'bg-red-500' : (slaPercent ?? 0) >= 99.5 ? 'bg-green-500' : 'bg-yellow-500'"
></span>
</div>
<button
v-if=
"!props.fullscreen"
class=
"text-[10px] font-bold text-blue-500 hover:underline"
type=
"button"
@
click=
"openDetails({ title: t('admin.ops.requestDetails.title'), kind: 'error' })"
...
...
@@ -1227,9 +1252,10 @@ function handleToolbarRefresh() {
<div
class=
"flex items-center justify-between"
>
<div
class=
"flex items-center gap-1"
>
<span
class=
"text-[10px] font-bold uppercase text-gray-400"
>
{{ t('admin.ops.latencyDuration') }}
</span>
<HelpTooltip
:content=
"t('admin.ops.tooltips.latency')"
/>
<HelpTooltip
v-if=
"!props.fullscreen"
:content=
"t('admin.ops.tooltips.latency')"
/>
</div>
<button
v-if=
"!props.fullscreen"
class=
"text-[10px] font-bold text-blue-500 hover:underline"
type=
"button"
@
click=
"openDetails({ title: t('admin.ops.latencyDuration'), sort: 'duration_desc', min_duration_ms: Math.max(Number(durationP99Ms ?? 0), 0) })"
...
...
@@ -1277,9 +1303,10 @@ function handleToolbarRefresh() {
<div
class=
"flex items-center justify-between"
>
<div
class=
"flex items-center gap-1"
>
<span
class=
"text-[10px] font-bold uppercase text-gray-400"
>
TTFT
</span>
<HelpTooltip
:content=
"t('admin.ops.tooltips.ttft')"
/>
<HelpTooltip
v-if=
"!props.fullscreen"
:content=
"t('admin.ops.tooltips.ttft')"
/>
</div>
<button
v-if=
"!props.fullscreen"
class=
"text-[10px] font-bold text-blue-500 hover:underline"
type=
"button"
@
click=
"openDetails({ title: 'TTFT', sort: 'duration_desc' })"
...
...
@@ -1327,9 +1354,9 @@ function handleToolbarRefresh() {
<div
class=
"flex items-center justify-between"
>
<div
class=
"flex items-center gap-1"
>
<span
class=
"text-[10px] font-bold uppercase text-gray-400"
>
{{ t('admin.ops.requestErrors') }}
</span>
<HelpTooltip
:content=
"t('admin.ops.tooltips.errors')"
/>
<HelpTooltip
v-if=
"!props.fullscreen"
:content=
"t('admin.ops.tooltips.errors')"
/>
</div>
<button
class=
"text-[10px] font-bold text-blue-500 hover:underline"
type=
"button"
@
click=
"openErrorDetails('request')"
>
<button
v-if=
"!props.fullscreen"
class=
"text-[10px] font-bold text-blue-500 hover:underline"
type=
"button"
@
click=
"openErrorDetails('request')"
>
{{ t('admin.ops.requestDetails.details') }}
</button>
</div>
...
...
@@ -1353,9 +1380,9 @@ function handleToolbarRefresh() {
<div
class=
"flex items-center justify-between"
>
<div
class=
"flex items-center gap-1"
>
<span
class=
"text-[10px] font-bold uppercase text-gray-400"
>
{{ t('admin.ops.upstreamErrors') }}
</span>
<HelpTooltip
:content=
"t('admin.ops.tooltips.upstreamErrors')"
/>
<HelpTooltip
v-if=
"!props.fullscreen"
:content=
"t('admin.ops.tooltips.upstreamErrors')"
/>
</div>
<button
class=
"text-[10px] font-bold text-blue-500 hover:underline"
type=
"button"
@
click=
"openErrorDetails('upstream')"
>
<button
v-if=
"!props.fullscreen"
class=
"text-[10px] font-bold text-blue-500 hover:underline"
type=
"button"
@
click=
"openErrorDetails('upstream')"
>
{{ t('admin.ops.requestDetails.details') }}
</button>
</div>
...
...
@@ -1383,12 +1410,12 @@ function handleToolbarRefresh() {
<div
class=
"rounded-xl bg-gray-50 p-3 dark:bg-dark-900"
>
<div
class=
"flex items-center gap-1"
>
<div
class=
"text-[10px] font-bold uppercase tracking-wider text-gray-400"
>
CPU
</div>
<HelpTooltip
:content=
"t('admin.ops.tooltips.cpu')"
/>
<HelpTooltip
v-if=
"!props.fullscreen"
:content=
"t('admin.ops.tooltips.cpu')"
/>
</div>
<div
class=
"mt-1 text-lg font-black"
:class=
"cpuPercentClass"
>
{{ cpuPercentValue == null ? '-' : `${cpuPercentValue.toFixed(1)}%` }}
</div>
<div
class=
"mt-1 text-[10px] text-gray-500 dark:text-gray-400"
>
<div
v-if=
"!props.fullscreen"
class=
"mt-1 text-[10px] text-gray-500 dark:text-gray-400"
>
{{ t('common.warning') }} 80% · {{ t('common.critical') }} 95%
</div>
</div>
...
...
@@ -1397,12 +1424,12 @@ function handleToolbarRefresh() {
<div
class=
"rounded-xl bg-gray-50 p-3 dark:bg-dark-900"
>
<div
class=
"flex items-center gap-1"
>
<div
class=
"text-[10px] font-bold uppercase tracking-wider text-gray-400"
>
MEM
</div>
<HelpTooltip
:content=
"t('admin.ops.tooltips.memory')"
/>
<HelpTooltip
v-if=
"!props.fullscreen"
:content=
"t('admin.ops.tooltips.memory')"
/>
</div>
<div
class=
"mt-1 text-lg font-black"
:class=
"memPercentClass"
>
{{ memPercentValue == null ? '-' : `${memPercentValue.toFixed(1)}%` }}
</div>
<div
class=
"mt-1 text-[10px] text-gray-500 dark:text-gray-400"
>
<div
v-if=
"!props.fullscreen"
class=
"mt-1 text-[10px] text-gray-500 dark:text-gray-400"
>
{{
systemMetrics?.memory_used_mb == null || systemMetrics?.memory_total_mb == null
? '-'
...
...
@@ -1415,12 +1442,12 @@ function handleToolbarRefresh() {
<div
class=
"rounded-xl bg-gray-50 p-3 dark:bg-dark-900"
>
<div
class=
"flex items-center gap-1"
>
<div
class=
"text-[10px] font-bold uppercase tracking-wider text-gray-400"
>
DB
</div>
<HelpTooltip
:content=
"t('admin.ops.tooltips.db')"
/>
<HelpTooltip
v-if=
"!props.fullscreen"
:content=
"t('admin.ops.tooltips.db')"
/>
</div>
<div
class=
"mt-1 text-lg font-black"
:class=
"dbMiddleClass"
>
{{ dbMiddleLabel }}
</div>
<div
class=
"mt-1 text-[10px] text-gray-500 dark:text-gray-400"
>
<div
v-if=
"!props.fullscreen"
class=
"mt-1 text-[10px] text-gray-500 dark:text-gray-400"
>
{{ t('admin.ops.conns') }} {{ dbConnOpenValue ?? '-' }} / {{ dbMaxOpenConnsValue ?? '-' }}
· {{ t('admin.ops.active') }} {{ dbConnActiveValue ?? '-' }}
· {{ t('admin.ops.idle') }} {{ dbConnIdleValue ?? '-' }}
...
...
@@ -1432,12 +1459,12 @@ function handleToolbarRefresh() {
<div
class=
"rounded-xl bg-gray-50 p-3 dark:bg-dark-900"
>
<div
class=
"flex items-center gap-1"
>
<div
class=
"text-[10px] font-bold uppercase tracking-wider text-gray-400"
>
Redis
</div>
<HelpTooltip
:content=
"t('admin.ops.tooltips.redis')"
/>
<HelpTooltip
v-if=
"!props.fullscreen"
:content=
"t('admin.ops.tooltips.redis')"
/>
</div>
<div
class=
"mt-1 text-lg font-black"
:class=
"redisMiddleClass"
>
{{ redisMiddleLabel }}
</div>
<div
class=
"mt-1 text-[10px] text-gray-500 dark:text-gray-400"
>
<div
v-if=
"!props.fullscreen"
class=
"mt-1 text-[10px] text-gray-500 dark:text-gray-400"
>
{{ t('admin.ops.conns') }} {{ redisConnTotalValue ?? '-' }} / {{ redisPoolSizeValue ?? '-' }}
<span
v-if=
"redisConnActiveValue != null"
>
· {{ t('admin.ops.active') }} {{ redisConnActiveValue }}
</span>
<span
v-if=
"redisConnIdleValue != null"
>
· {{ t('admin.ops.idle') }} {{ redisConnIdleValue }}
</span>
...
...
@@ -1448,12 +1475,12 @@ function handleToolbarRefresh() {
<div
class=
"rounded-xl bg-gray-50 p-3 dark:bg-dark-900"
>
<div
class=
"flex items-center gap-1"
>
<div
class=
"text-[10px] font-bold uppercase tracking-wider text-gray-400"
>
{{ t('admin.ops.goroutines') }}
</div>
<HelpTooltip
:content=
"t('admin.ops.tooltips.goroutines')"
/>
<HelpTooltip
v-if=
"!props.fullscreen"
:content=
"t('admin.ops.tooltips.goroutines')"
/>
</div>
<div
class=
"mt-1 text-lg font-black"
:class=
"goroutineStatusClass"
>
{{ goroutineStatusLabel }}
</div>
<div
class=
"mt-1 text-[10px] text-gray-500 dark:text-gray-400"
>
<div
v-if=
"!props.fullscreen"
class=
"mt-1 text-[10px] text-gray-500 dark:text-gray-400"
>
{{ t('admin.ops.current') }}
<span
class=
"font-mono"
>
{{ goroutineCountValue ?? '-' }}
</span>
· {{ t('common.warning') }}
<span
class=
"font-mono"
>
{{ goroutinesWarnThreshold }}
</span>
· {{ t('common.critical') }}
<span
class=
"font-mono"
>
{{ goroutinesCriticalThreshold }}
</span>
...
...
@@ -1468,9 +1495,9 @@ function handleToolbarRefresh() {
<div
class=
"flex items-center justify-between gap-2"
>
<div
class=
"flex items-center gap-1"
>
<div
class=
"text-[10px] font-bold uppercase tracking-wider text-gray-400"
>
{{ t('admin.ops.jobs') }}
</div>
<HelpTooltip
:content=
"t('admin.ops.tooltips.jobs')"
/>
<HelpTooltip
v-if=
"!props.fullscreen"
:content=
"t('admin.ops.tooltips.jobs')"
/>
</div>
<button
class=
"text-[10px] font-bold text-blue-500 hover:underline"
type=
"button"
@
click=
"openJobsDetails"
>
<button
v-if=
"!props.fullscreen"
class=
"text-[10px] font-bold text-blue-500 hover:underline"
type=
"button"
@
click=
"openJobsDetails"
>
{{ t('admin.ops.requestDetails.details') }}
</button>
</div>
...
...
@@ -1479,7 +1506,7 @@ function handleToolbarRefresh() {
{{ jobsStatusLabel }}
</div>
<div
class=
"mt-1 text-[10px] text-gray-500 dark:text-gray-400"
>
<div
v-if=
"!props.fullscreen"
class=
"mt-1 text-[10px] text-gray-500 dark:text-gray-400"
>
{{ t('common.total') }}
<span
class=
"font-mono"
>
{{ jobHeartbeats.length }}
</span>
· {{ t('common.warning') }}
<span
class=
"font-mono"
>
{{ jobsWarnCount }}
</span>
</div>
...
...
frontend/src/views/admin/ops/components/OpsThroughputTrendChart.vue
View file @
bb1fd54d
...
...
@@ -19,6 +19,7 @@ interface Props {
timeRange
:
string
byPlatform
?:
OpsThroughputPlatformBreakdownItem
[]
topGroups
?:
OpsThroughputGroupBreakdownItem
[]
fullscreen
?:
boolean
}
const
props
=
defineProps
<
Props
>
()
...
...
@@ -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"
/>
</svg>
{{
t
(
'
admin.ops.throughputTrend
'
)
}}
<HelpTooltip
:content=
"t('admin.ops.tooltips.throughputTrend')"
/>
<HelpTooltip
v-if=
"!props.fullscreen"
:content=
"t('admin.ops.tooltips.throughputTrend')"
/>
</h3>
<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-green-500"
></span>
{{
t
(
'
admin.ops.tpsK
'
)
}}
</span>
<button
type=
"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"
:disabled=
"state !== 'ready'"
:title=
"t('admin.ops.requestDetails.title')"
@
click=
"emit('openDetails')"
>
{{
t
(
'
admin.ops.requestDetails.details
'
)
}}
</button>
<button
type=
"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"
:disabled=
"state !== 'ready'"
:title=
"t('admin.ops.charts.resetZoomHint')"
@
click=
"resetZoom"
>
{{
t
(
'
admin.ops.charts.resetZoom
'
)
}}
</button>
<button
type=
"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"
:disabled=
"state !== 'ready'"
:title=
"t('admin.ops.charts.downloadChartHint')"
@
click=
"downloadChart"
>
{{
t
(
'
admin.ops.charts.downloadChart
'
)
}}
</button>
<template
v-if=
"!props.fullscreen"
>
<button
type=
"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"
:disabled=
"state !== 'ready'"
:title=
"t('admin.ops.requestDetails.title')"
@
click=
"emit('openDetails')"
>
{{
t
(
'
admin.ops.requestDetails.details
'
)
}}
</button>
<button
type=
"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"
:disabled=
"state !== 'ready'"
:title=
"t('admin.ops.charts.resetZoomHint')"
@
click=
"resetZoom"
>
{{
t
(
'
admin.ops.charts.resetZoom
'
)
}}
</button>
<button
type=
"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"
:disabled=
"state !== 'ready'"
:title=
"t('admin.ops.charts.downloadChartHint')"
@
click=
"downloadChart"
>
{{
t
(
'
admin.ops.charts.downloadChart
'
)
}}
</button>
</
template
>
</div>
</div>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment