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
f55ba3f6
Commit
f55ba3f6
authored
Jan 12, 2026
by
IanShaw027
Browse files
fix(ops): 优化卡片标题和明细筛选逻辑
- 将"请求数"改为"请求" - SLA卡片明细只显示错误请求(kind='error') - TTFT卡片明细按延迟降序排序
parent
db51e65b
Changes
2
Hide whitespace changes
Inline
Side-by-side
frontend/src/i18n/locales/zh.ts
View file @
f55ba3f6
...
@@ -2047,7 +2047,7 @@ export default {
...
@@ -2047,7 +2047,7 @@ export default {
avg
:
'
avg
'
,
avg
:
'
avg
'
,
max
:
'
max
'
,
max
:
'
max
'
,
qps
:
'
QPS
'
,
qps
:
'
QPS
'
,
requests
:
'
请求
数
'
,
requests
:
'
请求
'
,
upstream
:
'
上游
'
,
upstream
:
'
上游
'
,
client
:
'
客户端
'
,
client
:
'
客户端
'
,
system
:
'
系统
'
,
system
:
'
系统
'
,
...
...
frontend/src/views/admin/ops/components/OpsDashboardHeader.vue
View file @
f55ba3f6
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
computed
,
onMounted
,
ref
,
watch
}
from
'
vue
'
import
{
computed
,
onMounted
,
onUnmounted
,
ref
,
watch
}
from
'
vue
'
import
{
useIntervalFn
}
from
'
@vueuse/core
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
Select
from
'
@/components/common/Select.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
HelpTooltip
from
'
@/components/common/HelpTooltip.vue
'
import
HelpTooltip
from
'
@/components/common/HelpTooltip.vue
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
{
adminAPI
}
from
'
@/api
'
import
{
adminAPI
}
from
'
@/api
'
import
type
{
OpsDashboardOverview
,
OpsWSStatus
,
OpsMetricThresholds
}
from
'
@/api/admin/ops
'
import
{
opsAPI
,
type
OpsDashboardOverview
,
type
OpsWSStatus
,
type
OpsMetricThresholds
,
type
OpsRealtimeTrafficSummary
}
from
'
@/api/admin/ops
'
import
type
{
OpsRequestDetailsPreset
}
from
'
./OpsRequestDetailsModal.vue
'
import
type
{
OpsRequestDetailsPreset
}
from
'
./OpsRequestDetailsModal.vue
'
import
{
formatNumber
}
from
'
@/utils/format
'
import
{
formatNumber
}
from
'
@/utils/format
'
...
@@ -50,6 +51,34 @@ const realtimeWindow = ref<RealtimeWindow>('1min')
...
@@ -50,6 +51,34 @@ const realtimeWindow = ref<RealtimeWindow>('1min')
const
overview
=
computed
(()
=>
props
.
overview
??
null
)
const
overview
=
computed
(()
=>
props
.
overview
??
null
)
const
systemMetrics
=
computed
(()
=>
overview
.
value
?.
system_metrics
??
null
)
const
systemMetrics
=
computed
(()
=>
overview
.
value
?.
system_metrics
??
null
)
const
REALTIME_WINDOW_MINUTES
:
Record
<
RealtimeWindow
,
number
>
=
{
'
1min
'
:
1
,
'
5min
'
:
5
,
'
30min
'
:
30
,
'
1h
'
:
60
}
const
TOOLBAR_RANGE_MINUTES
:
Record
<
string
,
number
>
=
{
'
5m
'
:
5
,
'
30m
'
:
30
,
'
1h
'
:
60
,
'
6h
'
:
6
*
60
,
'
24h
'
:
24
*
60
}
const
availableRealtimeWindows
=
computed
(()
=>
{
const
toolbarMinutes
=
TOOLBAR_RANGE_MINUTES
[
props
.
timeRange
]
??
60
return
([
'
1min
'
,
'
5min
'
,
'
30min
'
,
'
1h
'
]
as
const
).
filter
((
w
)
=>
REALTIME_WINDOW_MINUTES
[
w
]
<=
toolbarMinutes
)
})
watch
(
()
=>
props
.
timeRange
,
()
=>
{
// The realtime window must be inside the toolbar window; reset to keep UX predictable.
realtimeWindow
.
value
=
'
1min
'
}
)
// --- Filters ---
// --- Filters ---
const
groups
=
ref
<
Array
<
{
id
:
number
;
name
:
string
;
platform
:
string
}
>>
([])
const
groups
=
ref
<
Array
<
{
id
:
number
;
name
:
string
;
platform
:
string
}
>>
([])
...
@@ -186,51 +215,83 @@ function isUpstreamErrorRateAboveThreshold(upstreamErrorRatePercent: number | nu
...
@@ -186,51 +215,83 @@ function isUpstreamErrorRateAboveThreshold(upstreamErrorRatePercent: number | nu
const
totalRequestsLabel
=
computed
(()
=>
formatNumber
(
overview
.
value
?.
request_count_total
??
0
))
const
totalRequestsLabel
=
computed
(()
=>
formatNumber
(
overview
.
value
?.
request_count_total
??
0
))
const
totalTokensLabel
=
computed
(()
=>
formatNumber
(
overview
.
value
?.
token_consumed
??
0
))
const
totalTokensLabel
=
computed
(()
=>
formatNumber
(
overview
.
value
?.
token_consumed
??
0
))
const
realtimeTrafficSummary
=
ref
<
OpsRealtimeTrafficSummary
|
null
>
(
null
)
const
realtimeTrafficLoading
=
ref
(
false
)
async
function
loadRealtimeTrafficSummary
()
{
if
(
realtimeTrafficLoading
.
value
)
return
realtimeTrafficLoading
.
value
=
true
try
{
realtimeTrafficSummary
.
value
=
await
opsAPI
.
getRealtimeTrafficSummary
(
realtimeWindow
.
value
,
props
.
platform
,
props
.
groupId
)
}
catch
(
err
)
{
console
.
error
(
'
[OpsDashboardHeader] Failed to load realtime traffic summary
'
,
err
)
realtimeTrafficSummary
.
value
=
null
}
finally
{
realtimeTrafficLoading
.
value
=
false
}
}
watch
(
()
=>
[
realtimeWindow
.
value
,
props
.
platform
,
props
.
groupId
]
as
const
,
()
=>
{
loadRealtimeTrafficSummary
()
},
{
immediate
:
true
}
)
const
{
pause
:
pauseRealtimeTrafficRefresh
,
resume
:
resumeRealtimeTrafficRefresh
}
=
useIntervalFn
(
()
=>
{
loadRealtimeTrafficSummary
()
},
5000
,
{
immediate
:
false
}
)
onMounted
(()
=>
{
resumeRealtimeTrafficRefresh
()
})
onUnmounted
(()
=>
{
pauseRealtimeTrafficRefresh
()
})
const
displayRealTimeQps
=
computed
(()
=>
{
const
displayRealTimeQps
=
computed
(()
=>
{
const
v
=
realtimeTrafficSummary
.
value
?.
qps
?.
current
if
(
typeof
v
===
'
number
'
&&
Number
.
isFinite
(
v
))
return
v
const
ov
=
overview
.
value
const
ov
=
overview
.
value
if
(
!
ov
)
return
0
if
(
!
ov
)
return
0
const
useRealtime
=
props
.
wsStatus
===
'
connected
'
&&
!!
props
.
wsHasData
const
useRealtime
=
props
.
wsStatus
===
'
connected
'
&&
!!
props
.
wsHasData
const
v
=
useRealtime
?
props
.
realTimeQps
:
ov
.
qps
?.
current
const
fallback
=
useRealtime
?
props
.
realTimeQps
:
ov
.
qps
?.
current
return
typeof
v
===
'
number
'
&&
Number
.
isFinite
(
v
)
?
v
:
0
return
typeof
fallback
===
'
number
'
&&
Number
.
isFinite
(
fallback
)
?
fallback
:
0
})
})
const
displayRealTimeTps
=
computed
(()
=>
{
const
displayRealTimeTps
=
computed
(()
=>
{
const
v
=
realtimeTrafficSummary
.
value
?.
tps
?.
current
if
(
typeof
v
===
'
number
'
&&
Number
.
isFinite
(
v
))
return
v
const
ov
=
overview
.
value
const
ov
=
overview
.
value
if
(
!
ov
)
return
0
if
(
!
ov
)
return
0
const
useRealtime
=
props
.
wsStatus
===
'
connected
'
&&
!!
props
.
wsHasData
const
useRealtime
=
props
.
wsStatus
===
'
connected
'
&&
!!
props
.
wsHasData
const
v
=
useRealtime
?
props
.
realTimeTps
:
ov
.
tps
?.
current
const
fallback
=
useRealtime
?
props
.
realTimeTps
:
ov
.
tps
?.
current
return
typeof
v
===
'
number
'
&&
Number
.
isFinite
(
v
)
?
v
:
0
return
typeof
fallback
===
'
number
'
&&
Number
.
isFinite
(
fallback
)
?
fallback
:
0
})
})
// Sparkline history (keep last 60 data points)
const
realtimeQpsPeakLabel
=
computed
(()
=>
{
const
qpsHistory
=
ref
<
number
[]
>
([])
const
v
=
realtimeTrafficSummary
.
value
?.
qps
?.
peak
const
tpsHistory
=
ref
<
number
[]
>
([])
return
typeof
v
===
'
number
'
&&
Number
.
isFinite
(
v
)
?
v
.
toFixed
(
1
)
:
'
-
'
const
MAX_HISTORY_POINTS
=
60
watch
([
displayRealTimeQps
,
displayRealTimeTps
],
([
newQps
,
newTps
])
=>
{
// Add new data points
qpsHistory
.
value
.
push
(
newQps
)
tpsHistory
.
value
.
push
(
newTps
)
// Keep only last N points
if
(
qpsHistory
.
value
.
length
>
MAX_HISTORY_POINTS
)
{
qpsHistory
.
value
.
shift
()
}
if
(
tpsHistory
.
value
.
length
>
MAX_HISTORY_POINTS
)
{
tpsHistory
.
value
.
shift
()
}
})
})
const
realtimeTpsPeakLabel
=
computed
(()
=>
{
const
qpsPeakLabel
=
computed
(()
=>
{
const
v
=
realtimeTrafficSummary
.
value
?.
tps
?.
peak
const
v
=
overview
.
value
?.
qps
?.
peak
return
typeof
v
===
'
number
'
&&
Number
.
isFinite
(
v
)
?
v
.
toFixed
(
1
)
:
'
-
'
if
(
typeof
v
!==
'
number
'
)
return
'
-
'
return
v
.
toFixed
(
1
)
})
})
const
realtimeQpsAvgLabel
=
computed
(()
=>
{
const
tpsPeakLabel
=
computed
(()
=>
{
const
v
=
realtimeTrafficSummary
.
value
?.
qps
?.
avg
const
v
=
overview
.
value
?.
tps
?.
peak
return
typeof
v
===
'
number
'
&&
Number
.
isFinite
(
v
)
?
v
.
toFixed
(
1
)
:
'
-
'
if
(
typeof
v
!==
'
number
'
)
return
'
-
'
})
return
v
.
toFixed
(
1
)
const
realtimeTpsAvgLabel
=
computed
(()
=>
{
const
v
=
realtimeTrafficSummary
.
value
?.
tps
?.
avg
return
typeof
v
===
'
number
'
&&
Number
.
isFinite
(
v
)
?
v
.
toFixed
(
1
)
:
'
-
'
})
})
const
qpsAvgLabel
=
computed
(()
=>
{
const
qpsAvgLabel
=
computed
(()
=>
{
...
@@ -968,7 +1029,7 @@ function openJobsDetails() {
...
@@ -968,7 +1029,7 @@ function openJobsDetails() {
<!-- Time Window Selector -->
<!-- Time Window Selector -->
<div
class=
"flex flex-wrap gap-1"
>
<div
class=
"flex flex-wrap gap-1"
>
<button
<button
v-for=
"window in
(['1min', '5min', '30min', '1h'] as
RealtimeWindow
[])
"
v-for=
"window in
available
RealtimeWindow
s
"
:key=
"window"
:key=
"window"
type=
"button"
type=
"button"
class=
"rounded px-1.5 py-0.5 text-[9px] font-bold transition-colors sm:px-2 sm:text-[10px]"
class=
"rounded px-1.5 py-0.5 text-[9px] font-bold transition-colors sm:px-2 sm:text-[10px]"
...
@@ -1005,11 +1066,11 @@ function openJobsDetails() {
...
@@ -1005,11 +1066,11 @@ function openJobsDetails() {
<div
class=
"text-[10px] font-bold uppercase text-gray-400"
>
{{ t('admin.ops.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=
"mt-1 space-y-0.5 text-sm font-medium text-gray-600 dark:text-gray-400"
>
<div
class=
"flex items-baseline gap-1.5"
>
<div
class=
"flex items-baseline gap-1.5"
>
<span
class=
"font-black text-gray-900 dark:text-white"
>
{{
q
psPeakLabel }}
</span>
<span
class=
"font-black text-gray-900 dark:text-white"
>
{{
realtimeQ
psPeakLabel }}
</span>
<span
class=
"text-xs"
>
QPS
</span>
<span
class=
"text-xs"
>
QPS
</span>
</div>
</div>
<div
class=
"flex items-baseline gap-1.5"
>
<div
class=
"flex items-baseline gap-1.5"
>
<span
class=
"font-black text-gray-900 dark:text-white"
>
{{
t
psPeakLabel }}
</span>
<span
class=
"font-black text-gray-900 dark:text-white"
>
{{
realtimeT
psPeakLabel }}
</span>
<span
class=
"text-xs"
>
TPS
</span>
<span
class=
"text-xs"
>
TPS
</span>
</div>
</div>
</div>
</div>
...
@@ -1020,11 +1081,11 @@ function openJobsDetails() {
...
@@ -1020,11 +1081,11 @@ function openJobsDetails() {
<div
class=
"text-[10px] font-bold uppercase text-gray-400"
>
{{ t('admin.ops.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=
"mt-1 space-y-0.5 text-sm font-medium text-gray-600 dark:text-gray-400"
>
<div
class=
"flex items-baseline gap-1.5"
>
<div
class=
"flex items-baseline gap-1.5"
>
<span
class=
"font-black text-gray-900 dark:text-white"
>
{{
q
psAvgLabel }}
</span>
<span
class=
"font-black text-gray-900 dark:text-white"
>
{{
realtimeQ
psAvgLabel }}
</span>
<span
class=
"text-xs"
>
QPS
</span>
<span
class=
"text-xs"
>
QPS
</span>
</div>
</div>
<div
class=
"flex items-baseline gap-1.5"
>
<div
class=
"flex items-baseline gap-1.5"
>
<span
class=
"font-black text-gray-900 dark:text-white"
>
{{
t
psAvgLabel }}
</span>
<span
class=
"font-black text-gray-900 dark:text-white"
>
{{
realtimeT
psAvgLabel }}
</span>
<span
class=
"text-xs"
>
TPS
</span>
<span
class=
"text-xs"
>
TPS
</span>
</div>
</div>
</div>
</div>
...
@@ -1106,7 +1167,7 @@ function openJobsDetails() {
...
@@ -1106,7 +1167,7 @@ function openJobsDetails() {
<button
<button
class=
"text-[10px] font-bold text-blue-500 hover:underline"
class=
"text-[10px] font-bold text-blue-500 hover:underline"
type=
"button"
type=
"button"
@
click=
"openDetails({ title: t('admin.ops.requestDetails.title') })"
@
click=
"openDetails({ title: t('admin.ops.requestDetails.title')
, kind: 'error'
})"
>
>
{{ t('admin.ops.requestDetails.details') }}
{{ t('admin.ops.requestDetails.details') }}
</button>
</button>
...
@@ -1185,7 +1246,7 @@ function openJobsDetails() {
...
@@ -1185,7 +1246,7 @@ function openJobsDetails() {
<button
<button
class=
"text-[10px] font-bold text-blue-500 hover:underline"
class=
"text-[10px] font-bold text-blue-500 hover:underline"
type=
"button"
type=
"button"
@
click=
"openDetails({ title: 'TTFT' })"
@
click=
"openDetails({ title: 'TTFT'
, sort: 'duration_desc'
})"
>
>
{{ t('admin.ops.requestDetails.details') }}
{{ t('admin.ops.requestDetails.details') }}
</button>
</button>
...
...
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