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
6901b64f
Commit
6901b64f
authored
Jan 17, 2026
by
cyhhao
Browse files
merge: sync upstream changes
parents
32c47b15
dae0d532
Changes
189
Hide whitespace changes
Inline
Side-by-side
frontend/src/views/admin/ops/components/OpsErrorDetailsModal.vue
View file @
6901b64f
...
...
@@ -22,23 +22,19 @@ const emit = defineEmits<{
const
{
t
}
=
useI18n
()
const
loading
=
ref
(
false
)
const
rows
=
ref
<
OpsErrorLog
[]
>
([])
const
total
=
ref
(
0
)
const
page
=
ref
(
1
)
const
pageSize
=
ref
(
2
0
)
const
pageSize
=
ref
(
1
0
)
const
q
=
ref
(
''
)
const
statusCode
=
ref
<
number
|
null
>
(
null
)
const
statusCode
=
ref
<
number
|
'
other
'
|
null
>
(
null
)
const
phase
=
ref
<
string
>
(
''
)
const
accountIdInput
=
ref
<
string
>
(
''
)
const
errorOwner
=
ref
<
string
>
(
''
)
const
viewMode
=
ref
<
'
errors
'
|
'
excluded
'
|
'
all
'
>
(
'
errors
'
)
const
accountId
=
computed
<
number
|
null
>
(()
=>
{
const
raw
=
String
(
accountIdInput
.
value
||
''
).
trim
()
if
(
!
raw
)
return
null
const
n
=
Number
.
parseInt
(
raw
,
10
)
return
Number
.
isFinite
(
n
)
&&
n
>
0
?
n
:
null
})
const
modalTitle
=
computed
(()
=>
{
return
props
.
errorType
===
'
upstream
'
?
t
(
'
admin.ops.errorDetails.upstreamErrors
'
)
:
t
(
'
admin.ops.errorDetails.requestErrors
'
)
...
...
@@ -48,20 +44,38 @@ const statusCodeSelectOptions = computed(() => {
const
codes
=
[
400
,
401
,
403
,
404
,
409
,
422
,
429
,
500
,
502
,
503
,
504
,
529
]
return
[
{
value
:
null
,
label
:
t
(
'
common.all
'
)
},
...
codes
.
map
((
c
)
=>
({
value
:
c
,
label
:
String
(
c
)
}))
...
codes
.
map
((
c
)
=>
({
value
:
c
,
label
:
String
(
c
)
})),
{
value
:
'
other
'
,
label
:
t
(
'
admin.ops.errorDetails.statusCodeOther
'
)
||
'
Other
'
}
]
})
const
ownerSelectOptions
=
computed
(()
=>
{
return
[
{
value
:
''
,
label
:
t
(
'
common.all
'
)
},
{
value
:
'
provider
'
,
label
:
t
(
'
admin.ops.errorDetails.owner.provider
'
)
||
'
provider
'
},
{
value
:
'
client
'
,
label
:
t
(
'
admin.ops.errorDetails.owner.client
'
)
||
'
client
'
},
{
value
:
'
platform
'
,
label
:
t
(
'
admin.ops.errorDetails.owner.platform
'
)
||
'
platform
'
}
]
})
const
viewModeSelectOptions
=
computed
(()
=>
{
return
[
{
value
:
'
errors
'
,
label
:
t
(
'
admin.ops.errorDetails.viewErrors
'
)
||
'
errors
'
},
{
value
:
'
excluded
'
,
label
:
t
(
'
admin.ops.errorDetails.viewExcluded
'
)
||
'
excluded
'
},
{
value
:
'
all
'
,
label
:
t
(
'
common.all
'
)
}
]
})
const
phaseSelectOptions
=
computed
(()
=>
{
const
options
=
[
{
value
:
''
,
label
:
t
(
'
common.all
'
)
},
{
value
:
'
upstream
'
,
label
:
'
upstream
'
},
{
value
:
'
network
'
,
label
:
'
network
'
},
{
value
:
'
routing
'
,
label
:
'
routing
'
},
{
value
:
'
auth
'
,
label
:
'
auth
'
},
{
value
:
'
billing
'
,
label
:
'
billing
'
},
{
value
:
'
concurrency
'
,
label
:
'
concurrency
'
},
{
value
:
'
internal
'
,
label
:
'
internal
'
}
{
value
:
'
request
'
,
label
:
t
(
'
admin.ops.errorDetails.phase.request
'
)
||
'
request
'
},
{
value
:
'
auth
'
,
label
:
t
(
'
admin.ops.errorDetails.phase.auth
'
)
||
'
auth
'
},
{
value
:
'
routing
'
,
label
:
t
(
'
admin.ops.errorDetails.phase.routing
'
)
||
'
routing
'
},
{
value
:
'
upstream
'
,
label
:
t
(
'
admin.ops.errorDetails.phase.upstream
'
)
||
'
upstream
'
},
{
value
:
'
network
'
,
label
:
t
(
'
admin.ops.errorDetails.phase.network
'
)
||
'
network
'
},
{
value
:
'
internal
'
,
label
:
t
(
'
admin.ops.errorDetails.phase.internal
'
)
||
'
internal
'
}
]
return
options
})
...
...
@@ -78,7 +92,8 @@ async function fetchErrorLogs() {
const
params
:
Record
<
string
,
any
>
=
{
page
:
page
.
value
,
page_size
:
pageSize
.
value
,
time_range
:
props
.
timeRange
time_range
:
props
.
timeRange
,
view
:
viewMode
.
value
}
const
platform
=
String
(
props
.
platform
||
''
).
trim
()
...
...
@@ -86,13 +101,19 @@ async function fetchErrorLogs() {
if
(
typeof
props
.
groupId
===
'
number
'
&&
props
.
groupId
>
0
)
params
.
group_id
=
props
.
groupId
if
(
q
.
value
.
trim
())
params
.
q
=
q
.
value
.
trim
()
if
(
typeof
statusCode
.
value
===
'
numb
er
'
)
params
.
status_codes
=
String
(
statusCode
.
value
)
if
(
typeof
accountId
.
value
===
'
number
'
)
params
.
account_id
=
accountId
.
value
if
(
statusCode
.
value
===
'
oth
er
'
)
params
.
status_codes
_other
=
'
1
'
else
if
(
typeof
statusCode
.
value
===
'
number
'
)
params
.
status_codes
=
String
(
statusCode
.
value
)
const
phaseVal
=
String
(
phase
.
value
||
''
).
trim
()
if
(
phaseVal
)
params
.
phase
=
phaseVal
const
res
=
await
opsAPI
.
listErrorLogs
(
params
)
const
ownerVal
=
String
(
errorOwner
.
value
||
''
).
trim
()
if
(
ownerVal
)
params
.
error_owner
=
ownerVal
const
res
=
props
.
errorType
===
'
upstream
'
?
await
opsAPI
.
listUpstreamErrors
(
params
)
:
await
opsAPI
.
listRequestErrors
(
params
)
rows
.
value
=
res
.
items
||
[]
total
.
value
=
res
.
total
||
0
}
catch
(
err
)
{
...
...
@@ -104,21 +125,23 @@ async function fetchErrorLogs() {
}
}
function
resetFilters
()
{
q
.
value
=
''
statusCode
.
value
=
null
phase
.
value
=
props
.
errorType
===
'
upstream
'
?
'
upstream
'
:
''
accountIdInput
.
value
=
''
page
.
value
=
1
fetchErrorLogs
()
}
function
resetFilters
()
{
q
.
value
=
''
statusCode
.
value
=
null
phase
.
value
=
props
.
errorType
===
'
upstream
'
?
'
upstream
'
:
''
errorOwner
.
value
=
''
viewMode
.
value
=
'
errors
'
page
.
value
=
1
fetchErrorLogs
()
}
watch
(
()
=>
props
.
show
,
(
open
)
=>
{
if
(
!
open
)
return
page
.
value
=
1
pageSize
.
value
=
2
0
pageSize
.
value
=
1
0
resetFilters
()
}
)
...
...
@@ -154,16 +177,7 @@ watch(
)
watch
(
()
=>
[
statusCode
.
value
,
phase
.
value
]
as
const
,
()
=>
{
if
(
!
props
.
show
)
return
page
.
value
=
1
fetchErrorLogs
()
}
)
watch
(
()
=>
accountId
.
value
,
()
=>
[
statusCode
.
value
,
phase
.
value
,
errorOwner
.
value
,
viewMode
.
value
]
as
const
,
()
=>
{
if
(
!
props
.
show
)
return
page
.
value
=
1
...
...
@@ -177,12 +191,12 @@ watch(
<div
class=
"flex h-full min-h-0 flex-col"
>
<!-- Filters -->
<div
class=
"mb-4 flex-shrink-0 border-b border-gray-200 pb-4 dark:border-dark-700"
>
<div
class=
"grid grid-cols-
1
gap-
4 lg:grid-cols-1
2"
>
<div
class=
"
lg:
col-span-
5
"
>
<div
class=
"grid grid-cols-
8
gap-2"
>
<div
class=
"col-span-
2 compact-select
"
>
<div
class=
"relative group"
>
<div
class=
"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3
.5
"
>
<div
class=
"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
>
<svg
class=
"h-
4 w-4
text-gray-400 transition-colors group-focus-within:text-blue-500"
class=
"h-
3.5 w-3.5
text-gray-400 transition-colors group-focus-within:text-blue-500"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
...
...
@@ -193,32 +207,32 @@ watch(
<input
v-model=
"q"
type=
"text"
class=
"w-full rounded-
2x
l border-gray-200 bg-gray-50/50 py-
2
pl-
10
pr-
4
text-s
m
font-medium text-gray-700 transition-all focus:border-blue-500 focus:bg-white focus:ring-
4
focus:ring-blue-500/10 dark:border-dark-700 dark:bg-dark-900 dark:text-gray-300 dark:focus:bg-dark-800"
class=
"w-full rounded-l
g
border-gray-200 bg-gray-50/50 py-
1.5
pl-
9
pr-
3
text-
x
s font-medium text-gray-700 transition-all focus:border-blue-500 focus:bg-white focus:ring-
2
focus:ring-blue-500/10 dark:border-dark-700 dark:bg-dark-900 dark:text-gray-300 dark:focus:bg-dark-800"
:placeholder=
"t('admin.ops.errorDetails.searchPlaceholder')"
/>
</div>
</div>
<div
class=
"
lg:col-span-2
"
>
<Select
:model-value=
"statusCode"
:options=
"statusCodeSelectOptions"
class=
"w-full"
@
update:model-value=
"statusCode = $event as any"
/>
<div
class=
"
compact-select
"
>
<Select
:model-value=
"statusCode"
:options=
"statusCodeSelectOptions"
@
update:model-value=
"statusCode = $event as any"
/>
</div>
<div
class=
"
lg:col-span-2
"
>
<Select
:model-value=
"phase"
:options=
"phaseSelectOptions"
class=
"w-full"
@
update:model-value=
"phase = String($event ?? '')"
/>
<div
class=
"
compact-select
"
>
<Select
:model-value=
"phase"
:options=
"phaseSelectOptions"
@
update:model-value=
"phase = String($event ?? '')"
/>
</div>
<div
class=
"lg:col-span-2"
>
<input
v-model=
"accountIdInput"
type=
"text"
inputmode=
"numeric"
class=
"input w-full text-sm"
:placeholder=
"t('admin.ops.errorDetails.accountIdPlaceholder')"
/>
<div
class=
"compact-select"
>
<Select
:model-value=
"errorOwner"
:options=
"ownerSelectOptions"
@
update:model-value=
"errorOwner = String($event ?? '')"
/>
</div>
<div
class=
"lg:col-span-1 flex items-center justify-end"
>
<button
type=
"button"
class=
"btn btn-secondary btn-sm"
@
click=
"resetFilters"
>
<div
class=
"compact-select"
>
<Select
:model-value=
"viewMode"
:options=
"viewModeSelectOptions"
@
update:model-value=
"viewMode = $event as any"
/>
</div>
<div
class=
"flex items-center justify-end"
>
<button
type=
"button"
class=
"rounded-lg bg-gray-100 px-3 py-1.5 text-xs font-semibold text-gray-700 transition-colors hover:bg-gray-200 dark:bg-dark-700 dark:text-gray-300 dark:hover:bg-dark-600"
@
click=
"resetFilters"
>
{{
t
(
'
common.reset
'
)
}}
</button>
</div>
...
...
@@ -231,18 +245,26 @@ watch(
{{
t
(
'
admin.ops.errorDetails.total
'
)
}}
{{
total
}}
</div>
<OpsErrorLogTable
class=
"min-h-0 flex-1"
:rows=
"rows"
:total=
"total"
:loading=
"loading"
:page=
"page"
:page-size=
"pageSize"
@
openErrorDetail=
"emit('openErrorDetail', $event)"
@
update:page=
"page = $event"
@
update:pageSize=
"pageSize = $event"
/>
<OpsErrorLogTable
class=
"min-h-0 flex-1"
:rows=
"rows"
:total=
"total"
:loading=
"loading"
:page=
"page"
:page-size=
"pageSize"
@
openErrorDetail=
"emit('openErrorDetail', $event)"
@
update:page=
"page = $event"
@
update:pageSize=
"pageSize = $event"
/>
</div>
</div>
</BaseDialog>
</
template
>
<
style
>
.compact-select
.select-trigger
{
@apply
py-1.5
px-3
text-xs
rounded-lg;
}
</
style
>
frontend/src/views/admin/ops/components/OpsErrorLogTable.vue
View file @
6901b64f
<
template
>
<div
class=
"flex h-full min-h-0 flex-col"
>
<div
class=
"flex h-full min-h-0 flex-col bg-white dark:bg-dark-900"
>
<!-- Loading State -->
<div
v-if=
"loading"
class=
"flex flex-1 items-center justify-center py-10"
>
<div
class=
"h-8 w-8 animate-spin rounded-full border-b-2 border-primary-600"
></div>
</div>
<!-- Table Container -->
<div
v-else
class=
"flex min-h-0 flex-1 flex-col"
>
<div
class=
"min-h-0 flex-1 overflow-auto"
>
<table
class=
"
min-
w-full
divide-y divide-gray-200 dark:divide-dark-70
0"
>
<thead
class=
"sticky top-0 z-10 bg-gray-50
/50
dark:bg-dark-800
/50
"
>
<div
class=
"min-h-0 flex-1 overflow-auto
border-b border-gray-200 dark:border-dark-700
"
>
<table
class=
"w-full
border-separate border-spacing-
0"
>
<thead
class=
"sticky top-0 z-10 bg-gray-50 dark:bg-dark-800"
>
<tr>
<th
scope=
"col"
class=
"whitespace-nowrap px-6 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.timeId
'
)
}}
<th
class=
"border-b border-gray-200 px-4 py-2.5 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:border-dark-700 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.time
'
)
}}
</th>
<th
scope=
"col"
class=
"whitespace-nowrap px-6 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.context
'
)
}}
<th
class=
"border-b border-gray-200 px-4 py-2.5 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:border-dark-700 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.type
'
)
}}
</th>
<th
scope=
"col"
class=
"whitespace-nowrap px-6 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
>
<th
class=
"border-b border-gray-200 px-4 py-2.5 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:border-dark-700 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.platform
'
)
}}
</th>
<th
class=
"border-b border-gray-200 px-4 py-2.5 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:border-dark-700 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.model
'
)
}}
</th>
<th
class=
"border-b border-gray-200 px-4 py-2.5 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:border-dark-700 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.group
'
)
}}
</th>
<th
class=
"border-b border-gray-200 px-4 py-2.5 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:border-dark-700 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.user
'
)
}}
</th>
<th
class=
"border-b border-gray-200 px-4 py-2.5 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:border-dark-700 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.status
'
)
}}
</th>
<th
scope=
"col"
class=
"px-6 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
>
<th
class=
"border-b border-gray-200 px-4 py-2.5 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:border-dark-700 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.message
'
)
}}
</th>
<th
scope=
"col"
class=
"whitespace-nowrap px-6 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.latency
'
)
}}
</th>
<th
scope=
"col"
class=
"whitespace-nowrap px-6 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
>
<th
class=
"border-b border-gray-200 px-4 py-2.5 text-right text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:border-dark-700 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.action
'
)
}}
</th>
</tr>
</thead>
<tbody
class=
"divide-y divide-gray-100 dark:divide-dark-700"
>
<tr
v-if=
"rows.length === 0"
class=
"bg-white dark:bg-dark-900"
>
<td
colspan=
"
6
"
class=
"py-1
6
text-center text-sm text-gray-400 dark:text-dark-500"
>
<tr
v-if=
"rows.length === 0"
>
<td
colspan=
"
9
"
class=
"py-1
2
text-center text-sm text-gray-400 dark:text-dark-500"
>
{{
t
(
'
admin.ops.errorLog.noErrors
'
)
}}
</td>
</tr>
...
...
@@ -57,59 +50,83 @@
<tr
v-for=
"log in rows"
:key=
"log.id"
class=
"group cursor-pointer transition-all duration-200 hover:bg-gray-50/80 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:hover:bg-dark-800/50 dark:focus:ring-offset-dark-900"
tabindex=
"0"
role=
"button"
class=
"group cursor-pointer transition-colors hover:bg-gray-50/80 dark:hover:bg-dark-800/50"
@
click=
"emit('openErrorDetail', log.id)"
@
keydown.enter.prevent=
"emit('openErrorDetail', log.id)"
@
keydown.space.prevent=
"emit('openErrorDetail', log.id)"
>
<!-- Time
& ID
-->
<td
class=
"px-
6
py-
4
"
>
<
div
class=
"flex flex-col gap-0.5
"
>
<span
class=
"font-mono text-xs font-
bold
text-gray-900 dark:text-gray-200"
>
<!-- Time -->
<td
class=
"
whitespace-nowrap
px-
4
py-
2
"
>
<
el-tooltip
:content=
"log.request_id || log.client_request_id"
placement=
"top"
:show-after=
"500
"
>
<span
class=
"font-mono text-xs font-
medium
text-gray-900 dark:text-gray-200"
>
{{
formatDateTime
(
log
.
created_at
).
split
(
'
'
)[
1
]
}}
</span>
<span
class=
"font-mono text-[10px] text-gray-400 transition-colors group-hover:text-primary-600 dark:group-hover:text-primary-400"
:title=
"log.request_id || log.client_request_id"
>
{{
(
log
.
request_id
||
log
.
client_request_id
||
''
).
substring
(
0
,
12
)
}}
</span>
</div>
</el-tooltip>
</td>
<!-- Context (Platform/Model) -->
<td
class=
"px-6 py-4"
>
<div
class=
"flex flex-col items-start gap-1.5"
>
<span
class=
"inline-flex items-center rounded-md bg-gray-100 px-2 py-0.5 text-[10px] font-bold uppercase tracking-tight text-gray-600 dark:bg-dark-700 dark:text-gray-300"
>
{{
log
.
platform
||
'
-
'
}}
</span>
<span
v-if=
"log.model"
class=
"max-w-[160px] truncate font-mono text-[10px] text-gray-500 dark:text-dark-400"
:title=
"log.model"
>
<!-- Type -->
<td
class=
"whitespace-nowrap px-4 py-2"
>
<span
:class=
"[
'inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-bold ring-1 ring-inset',
getTypeBadge(log).className
]"
>
{{
getTypeBadge
(
log
).
label
}}
</span>
</td>
<!-- Platform -->
<td
class=
"whitespace-nowrap px-4 py-2"
>
<span
class=
"inline-flex items-center rounded bg-gray-100 px-1.5 py-0.5 text-[10px] font-bold uppercase text-gray-600 dark:bg-dark-700 dark:text-gray-300"
>
{{
log
.
platform
||
'
-
'
}}
</span>
</td>
<!-- Model -->
<td
class=
"px-4 py-2"
>
<div
class=
"max-w-[120px] truncate"
:title=
"log.model"
>
<span
v-if=
"log.model"
class=
"font-mono text-[11px] text-gray-700 dark:text-gray-300"
>
{{
log
.
model
}}
</span>
<div
v-if=
"log.group_id || log.account_id"
class=
"flex flex-wrap items-center gap-2 font-mono text-[10px] font-semibold text-gray-400 dark:text-dark-500"
>
<span
v-if=
"log.group_id"
>
{{
t
(
'
admin.ops.errorLog.grp
'
)
}}
{{
log
.
group_id
}}
</span>
<span
v-if=
"log.account_id"
>
{{
t
(
'
admin.ops.errorLog.acc
'
)
}}
{{
log
.
account_id
}}
</span>
</div>
<span
v-else
class=
"text-xs text-gray-400"
>
-
</span>
</div>
</td>
<!-- Status & Severity -->
<td
class=
"px-6 py-4"
>
<div
class=
"flex flex-wrap items-center gap-2"
>
<!-- Group -->
<td
class=
"px-4 py-2"
>
<el-tooltip
v-if=
"log.group_id"
:content=
"t('admin.ops.errorLog.id') + ' ' + log.group_id"
placement=
"top"
:show-after=
"500"
>
<span
class=
"max-w-[100px] truncate text-xs font-medium text-gray-900 dark:text-gray-200"
>
{{
log
.
group_name
||
'
-
'
}}
</span>
</el-tooltip>
<span
v-else
class=
"text-xs text-gray-400"
>
-
</span>
</td>
<!-- User / Account -->
<td
class=
"px-4 py-2"
>
<template
v-if=
"isUpstreamRow(log)"
>
<el-tooltip
v-if=
"log.account_id"
:content=
"t('admin.ops.errorLog.accountId') + ' ' + log.account_id"
placement=
"top"
:show-after=
"500"
>
<span
class=
"max-w-[100px] truncate text-xs font-medium text-gray-900 dark:text-gray-200"
>
{{
log
.
account_name
||
'
-
'
}}
</span>
</el-tooltip>
<span
v-else
class=
"text-xs text-gray-400"
>
-
</span>
</
template
>
<
template
v-else
>
<el-tooltip
v-if=
"log.user_id"
:content=
"t('admin.ops.errorLog.userId') + ' ' + log.user_id"
placement=
"top"
:show-after=
"500"
>
<span
class=
"max-w-[100px] truncate text-xs font-medium text-gray-900 dark:text-gray-200"
>
{{
log
.
user_email
||
'
-
'
}}
</span>
</el-tooltip>
<span
v-else
class=
"text-xs text-gray-400"
>
-
</span>
</
template
>
</td>
<!-- Status -->
<td
class=
"whitespace-nowrap px-4 py-2"
>
<div
class=
"flex items-center gap-1.5"
>
<span
:class=
"[
'inline-flex items-center rounded
-lg
px-
2
py-
1
text-
xs
font-b
lack
ring-1 ring-inset
shadow-sm
',
'inline-flex items-center rounded px-
1.5
py-
0.5
text-
[10px]
font-b
old
ring-1 ring-inset',
getStatusClass(log.status_code)
]"
>
...
...
@@ -117,61 +134,47 @@
</span>
<span
v-if=
"log.severity"
:class=
"['rounded
-md
px-
2
py-0.5 text-[10px] font-b
lack shadow-sm
', getSeverityClass(log.severity)]"
:class=
"['rounded px-
1.5
py-0.5 text-[10px] font-b
old
', getSeverityClass(log.severity)]"
>
{{ log.severity }}
</span>
</div>
</td>
<!-- Message -->
<td
class=
"px-
6
py-
4
"
>
<div
class=
"max-w-
md lg:max-w-2xl
"
>
<p
class=
"truncate text-
xs font-semibold
text-gray-
7
00 dark:text-gray-
3
00"
:title=
"log.message"
>
<!-- Message
(Response Content)
-->
<td
class=
"px-
4
py-
2
"
>
<div
class=
"max-w-
[200px]
"
>
<p
class=
"truncate text-
[11px] font-medium
text-gray-
6
00 dark:text-gray-
4
00"
:title=
"log.message"
>
{{ formatSmartMessage(log.message) || '-' }}
</p>
<div
class=
"mt-1.5 flex flex-wrap gap-x-3 gap-y-1"
>
<div
v-if=
"log.phase"
class=
"flex items-center gap-1"
>
<span
class=
"h-1 w-1 rounded-full bg-gray-300"
></span>
<span
class=
"text-[9px] font-black uppercase tracking-tighter text-gray-400"
>
{{
log
.
phase
}}
</span>
</div>
<div
v-if=
"log.client_ip"
class=
"flex items-center gap-1"
>
<span
class=
"h-1 w-1 rounded-full bg-gray-300"
></span>
<span
class=
"text-[9px] font-mono font-bold text-gray-400"
>
{{
log
.
client_ip
}}
</span>
</div>
</div>
</div>
</td>
<!-- Latency -->
<td
class=
"px-6 py-4 text-right"
>
<div
class=
"flex flex-col items-end"
>
<span
class=
"font-mono text-xs font-black"
:class=
"getLatencyClass(log.latency_ms ?? null)"
>
{{
log
.
latency_ms
!=
null
?
Math
.
round
(
log
.
latency_ms
)
+
'
ms
'
:
'
--
'
}}
</span>
</div>
</td>
<!-- Actions -->
<td
class=
"px-6 py-4 text-right"
@
click.stop
>
<button
type=
"button"
class=
"btn btn-secondary btn-sm"
@
click=
"emit('openErrorDetail', log.id)"
>
{{
t
(
'
admin.ops.errorLog.details
'
)
}}
</button>
<td
class=
"whitespace-nowrap px-4 py-2 text-right"
@
click.stop
>
<div
class=
"flex items-center justify-end gap-3"
>
<button
type=
"button"
class=
"text-primary-600 hover:text-primary-700 dark:text-primary-400 text-xs font-bold"
@
click=
"emit('openErrorDetail', log.id)"
>
{{ t('admin.ops.errorLog.details') }}
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<Pagination
v-if=
"total > 0"
:total=
"total"
:page=
"page"
:page-size=
"pageSize"
:page-size-options=
"[10, 20, 50, 100, 200, 500]"
@
update:page=
"emit('update:page', $event)"
@
update:pageSize=
"emit('update:pageSize', $event)"
/>
<!-- Pagination -->
<div
class=
"bg-gray-50/50 dark:bg-dark-800/50"
>
<Pagination
v-if=
"total > 0"
:total=
"total"
:page=
"page"
:page-size=
"pageSize"
:page-size-options=
"[10]"
@
update:page=
"emit('update:page', $event)"
@
update:pageSize=
"emit('update:pageSize', $event)"
/>
</div>
</div>
</div>
</template>
...
...
@@ -184,6 +187,36 @@ import { getSeverityClass, formatDateTime } from '../utils/opsFormatters'
const
{
t
}
=
useI18n
()
function
isUpstreamRow
(
log
:
OpsErrorLog
):
boolean
{
const
phase
=
String
(
log
.
phase
||
''
).
toLowerCase
()
const
owner
=
String
(
log
.
error_owner
||
''
).
toLowerCase
()
return
phase
===
'
upstream
'
&&
owner
===
'
provider
'
}
function
getTypeBadge
(
log
:
OpsErrorLog
):
{
label
:
string
;
className
:
string
}
{
const
phase
=
String
(
log
.
phase
||
''
).
toLowerCase
()
const
owner
=
String
(
log
.
error_owner
||
''
).
toLowerCase
()
if
(
isUpstreamRow
(
log
))
{
return
{
label
:
t
(
'
admin.ops.errorLog.typeUpstream
'
),
className
:
'
bg-red-50 text-red-700 ring-red-600/20 dark:bg-red-900/30 dark:text-red-400 dark:ring-red-500/30
'
}
}
if
(
phase
===
'
request
'
&&
owner
===
'
client
'
)
{
return
{
label
:
t
(
'
admin.ops.errorLog.typeRequest
'
),
className
:
'
bg-amber-50 text-amber-700 ring-amber-600/20 dark:bg-amber-900/30 dark:text-amber-400 dark:ring-amber-500/30
'
}
}
if
(
phase
===
'
auth
'
&&
owner
===
'
client
'
)
{
return
{
label
:
t
(
'
admin.ops.errorLog.typeAuth
'
),
className
:
'
bg-blue-50 text-blue-700 ring-blue-600/20 dark:bg-blue-900/30 dark:text-blue-400 dark:ring-blue-500/30
'
}
}
if
(
phase
===
'
routing
'
&&
owner
===
'
platform
'
)
{
return
{
label
:
t
(
'
admin.ops.errorLog.typeRouting
'
),
className
:
'
bg-purple-50 text-purple-700 ring-purple-600/20 dark:bg-purple-900/30 dark:text-purple-400 dark:ring-purple-500/30
'
}
}
if
(
phase
===
'
internal
'
&&
owner
===
'
platform
'
)
{
return
{
label
:
t
(
'
admin.ops.errorLog.typeInternal
'
),
className
:
'
bg-gray-100 text-gray-800 ring-gray-600/20 dark:bg-dark-700 dark:text-gray-200 dark:ring-dark-500/40
'
}
}
const
fallback
=
phase
||
owner
||
t
(
'
common.unknown
'
)
return
{
label
:
fallback
,
className
:
'
bg-gray-50 text-gray-700 ring-gray-600/10 dark:bg-dark-900 dark:text-gray-300 dark:ring-dark-700
'
}
}
interface
Props
{
rows
:
OpsErrorLog
[]
total
:
number
...
...
@@ -208,14 +241,6 @@ function getStatusClass(code: number): string {
return
'
bg-gray-50 text-gray-700 ring-gray-600/20 dark:bg-gray-900/30 dark:text-gray-400 dark:ring-gray-500/30
'
}
function
getLatencyClass
(
latency
:
number
|
null
):
string
{
if
(
!
latency
)
return
'
text-gray-400
'
if
(
latency
>
10000
)
return
'
text-red-600 font-black
'
if
(
latency
>
5000
)
return
'
text-red-500 font-bold
'
if
(
latency
>
2000
)
return
'
text-orange-500 font-medium
'
return
'
text-gray-600 dark:text-gray-400
'
}
function
formatSmartMessage
(
msg
:
string
):
string
{
if
(
!
msg
)
return
''
...
...
@@ -231,10 +256,11 @@ function formatSmartMessage(msg: string): string {
}
}
if
(
msg
.
includes
(
'
context deadline exceeded
'
))
return
'
context
d
eadline
e
xceeded
'
if
(
msg
.
includes
(
'
connection refused
'
))
return
'
connection
r
efused
'
if
(
msg
.
toLowerCase
().
includes
(
'
rate limit
'
))
return
'
rate
l
imit
'
if
(
msg
.
includes
(
'
context deadline exceeded
'
))
return
t
(
'
admin.ops.errorLog.commonErrors.
context
D
eadline
E
xceeded
'
)
if
(
msg
.
includes
(
'
connection refused
'
))
return
t
(
'
admin.ops.errorLog.commonErrors.
connection
R
efused
'
)
if
(
msg
.
toLowerCase
().
includes
(
'
rate limit
'
))
return
t
(
'
admin.ops.errorLog.commonErrors.
rate
L
imit
'
)
return
msg
.
length
>
200
?
msg
.
substring
(
0
,
200
)
+
'
...
'
:
msg
}
</
script
>
</
script
>
\ No newline at end of file
frontend/src/views/admin/ops/components/OpsRequestDetailsModal.vue
View file @
6901b64f
...
...
@@ -38,7 +38,7 @@ const loading = ref(false)
const
items
=
ref
<
OpsRequestDetail
[]
>
([])
const
total
=
ref
(
0
)
const
page
=
ref
(
1
)
const
pageSize
=
ref
(
2
0
)
const
pageSize
=
ref
(
1
0
)
const
close
=
()
=>
emit
(
'
update:modelValue
'
,
false
)
...
...
@@ -95,7 +95,7 @@ watch(
(
open
)
=>
{
if
(
open
)
{
page
.
value
=
1
pageSize
.
value
=
2
0
pageSize
.
value
=
1
0
fetchData
()
}
}
...
...
frontend/src/views/admin/ops/components/OpsRuntimeSettingsCard.vue
View file @
6901b64f
...
...
@@ -50,27 +50,22 @@ function validateRuntimeSettings(settings: OpsAlertRuntimeSettings): ValidationR
if
(
thresholds
)
{
if
(
thresholds
.
sla_percent_min
!=
null
)
{
if
(
!
Number
.
isFinite
(
thresholds
.
sla_percent_min
)
||
thresholds
.
sla_percent_min
<
0
||
thresholds
.
sla_percent_min
>
100
)
{
errors
.
push
(
'
SLA 最低值必须在 0-100 之间
'
)
}
}
if
(
thresholds
.
latency_p99_ms_max
!=
null
)
{
if
(
!
Number
.
isFinite
(
thresholds
.
latency_p99_ms_max
)
||
thresholds
.
latency_p99_ms_max
<
0
)
{
errors
.
push
(
'
延迟 P99 最大值必须大于或等于 0
'
)
errors
.
push
(
t
(
'
admin.ops.runtime.validation.slaMinPercentRange
'
))
}
}
if
(
thresholds
.
ttft_p99_ms_max
!=
null
)
{
if
(
!
Number
.
isFinite
(
thresholds
.
ttft_p99_ms_max
)
||
thresholds
.
ttft_p99_ms_max
<
0
)
{
errors
.
push
(
'
TTFT P99 最大值必须大于或等于 0
'
)
errors
.
push
(
t
(
'
admin.ops.runtime.validation.ttftP99MaxRange
'
)
)
}
}
if
(
thresholds
.
request_error_rate_percent_max
!=
null
)
{
if
(
!
Number
.
isFinite
(
thresholds
.
request_error_rate_percent_max
)
||
thresholds
.
request_error_rate_percent_max
<
0
||
thresholds
.
request_error_rate_percent_max
>
100
)
{
errors
.
push
(
'
请求错误率最大值必须在 0-100 之间
'
)
errors
.
push
(
t
(
'
admin.ops.runtime.validation.requestErrorRateMaxRange
'
)
)
}
}
if
(
thresholds
.
upstream_error_rate_percent_max
!=
null
)
{
if
(
!
Number
.
isFinite
(
thresholds
.
upstream_error_rate_percent_max
)
||
thresholds
.
upstream_error_rate_percent_max
<
0
||
thresholds
.
upstream_error_rate_percent_max
>
100
)
{
errors
.
push
(
'
上游错误率最大值必须在 0-100 之间
'
)
errors
.
push
(
t
(
'
admin.ops.runtime.validation.upstreamErrorRateMaxRange
'
)
)
}
}
}
...
...
@@ -163,7 +158,6 @@ function openAlertEditor() {
if
(
!
draftAlert
.
value
.
thresholds
)
{
draftAlert
.
value
.
thresholds
=
{
sla_percent_min
:
99.5
,
latency_p99_ms_max
:
2000
,
ttft_p99_ms_max
:
500
,
request_error_rate_percent_max
:
5
,
upstream_error_rate_percent_max
:
5
...
...
@@ -335,12 +329,12 @@ onMounted(() => {
</div>
<div
class=
"rounded-2xl bg-gray-50 p-4 dark:bg-dark-700/50"
>
<div
class=
"mb-2 text-sm font-semibold text-gray-900 dark:text-white"
>
指标阈值配置
</div>
<p
class=
"mb-4 text-xs text-gray-500 dark:text-gray-400"
>
配置各项指标的告警阈值。超出阈值的指标将在看板上以红色显示。
</p>
<div
class=
"mb-2 text-sm font-semibold text-gray-900 dark:text-white"
>
{{
t
(
'
admin.ops.runtime.metricThresholds
'
)
}}
</div>
<p
class=
"mb-4 text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.ops.runtime.metricThresholdsHint
'
)
}}
</p>
<div
class=
"grid grid-cols-1 gap-4 md:grid-cols-2"
>
<div>
<div
class=
"mb-1 text-xs font-medium text-gray-600 dark:text-gray-300"
>
SLA 最低值 (%)
</div>
<div
class=
"mb-1 text-xs font-medium text-gray-600 dark:text-gray-300"
>
{{
t
(
'
admin.ops.runtime.slaMinPercent
'
)
}}
</div>
<input
v-model.number=
"draftAlert.thresholds.sla_percent_min"
type=
"number"
...
...
@@ -350,24 +344,13 @@ onMounted(() => {
class=
"input"
placeholder=
"99.5"
/>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
SLA 低于此值时将显示为红色
</p>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.ops.runtime.slaMinPercentHint
'
)
}}
</p>
</div>
<div>
<div
class=
"mb-1 text-xs font-medium text-gray-600 dark:text-gray-300"
>
延迟 P99 最大值 (ms)
</div>
<input
v-model.number=
"draftAlert.thresholds.latency_p99_ms_max"
type=
"number"
min=
"0"
step=
"100"
class=
"input"
placeholder=
"2000"
/>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
延迟 P99 高于此值时将显示为红色
</p>
</div>
<div>
<div
class=
"mb-1 text-xs font-medium text-gray-600 dark:text-gray-300"
>
TTFT P99 最大值 (ms)
</div>
<div
class=
"mb-1 text-xs font-medium text-gray-600 dark:text-gray-300"
>
{{
t
(
'
admin.ops.runtime.ttftP99MaxMs
'
)
}}
</div>
<input
v-model.number=
"draftAlert.thresholds.ttft_p99_ms_max"
type=
"number"
...
...
@@ -376,11 +359,11 @@ onMounted(() => {
class=
"input"
placeholder=
"500"
/>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
TTFT P99 高于此值时将显示为红色
</p>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.ops.runtime.ttftP99MaxMsHint
'
)
}}
</p>
</div>
<div>
<div
class=
"mb-1 text-xs font-medium text-gray-600 dark:text-gray-300"
>
请求错误率最大值 (%)
</div>
<div
class=
"mb-1 text-xs font-medium text-gray-600 dark:text-gray-300"
>
{{
t
(
'
admin.ops.runtime.requestErrorRateMaxPercent
'
)
}}
</div>
<input
v-model.number=
"draftAlert.thresholds.request_error_rate_percent_max"
type=
"number"
...
...
@@ -390,11 +373,11 @@ onMounted(() => {
class=
"input"
placeholder=
"5"
/>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
请求错误率高于此值时将显示为红色
</p>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.ops.runtime.requestErrorRateMaxPercentHint
'
)
}}
</p>
</div>
<div>
<div
class=
"mb-1 text-xs font-medium text-gray-600 dark:text-gray-300"
>
上游错误率最大值 (%)
</div>
<div
class=
"mb-1 text-xs font-medium text-gray-600 dark:text-gray-300"
>
{{
t
(
'
admin.ops.runtime.upstreamErrorRateMaxPercent
'
)
}}
</div>
<input
v-model.number=
"draftAlert.thresholds.upstream_error_rate_percent_max"
type=
"number"
...
...
@@ -404,7 +387,7 @@ onMounted(() => {
class=
"input"
placeholder=
"5"
/>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
上游错误率高于此值时将显示为红色
</p>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.ops.runtime.upstreamErrorRateMaxPercentHint
'
)
}}
</p>
</div>
</div>
</div>
...
...
@@ -424,7 +407,7 @@ onMounted(() => {
v-model=
"draftAlert.silencing.global_until_rfc3339"
type=
"text"
class=
"input font-mono text-sm"
:
placeholder=
"
t('admin.ops.runtime.silencing.untilPlaceholder')
"
placeholder=
"
2026-01-05T00:00:00Z
"
/>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.ops.runtime.silencing.untilHint
'
)
}}
</p>
</div>
...
...
@@ -496,7 +479,7 @@ onMounted(() => {
v
-
model
=
"
(entry as any).until_rfc3339
"
type
=
"
text
"
class
=
"
input font-mono text-sm
"
:
placeholder
=
"
t('admin.ops.runtime.silencing.untilPlaceholder')
"
placeholder
=
"
2026-01-05T00:00:00Z
"
/>
<
/div
>
...
...
frontend/src/views/admin/ops/components/OpsSettingsDialog.vue
View file @
6901b64f
...
...
@@ -32,7 +32,6 @@ const advancedSettings = ref<OpsAdvancedSettings | null>(null)
// 指标阈值配置
const
metricThresholds
=
ref
<
OpsMetricThresholds
>
({
sla_percent_min
:
99.5
,
latency_p99_ms_max
:
2000
,
ttft_p99_ms_max
:
500
,
request_error_rate_percent_max
:
5
,
upstream_error_rate_percent_max
:
5
...
...
@@ -53,13 +52,12 @@ async function loadAllSettings() {
advancedSettings
.
value
=
advanced
// 如果后端返回了阈值,使用后端的值;否则保持默认值
if
(
thresholds
&&
Object
.
keys
(
thresholds
).
length
>
0
)
{
metricThresholds
.
value
=
{
sla_percent_min
:
thresholds
.
sla_percent_min
??
99.5
,
latency_p99_ms_max
:
thresholds
.
latency_p99_ms_max
??
2000
,
ttft_p99_ms_max
:
thresholds
.
ttft_p99_ms_max
??
500
,
request_error_rate_percent_max
:
thresholds
.
request_error_rate_percent_max
??
5
,
upstream_error_rate_percent_max
:
thresholds
.
upstream_error_rate_percent_max
??
5
}
metricThresholds
.
value
=
{
sla_percent_min
:
thresholds
.
sla_percent_min
??
99.5
,
ttft_p99_ms_max
:
thresholds
.
ttft_p99_ms_max
??
500
,
request_error_rate_percent_max
:
thresholds
.
request_error_rate_percent_max
??
5
,
upstream_error_rate_percent_max
:
thresholds
.
upstream_error_rate_percent_max
??
5
}
}
}
catch
(
err
:
any
)
{
console
.
error
(
'
[OpsSettingsDialog] Failed to load settings
'
,
err
)
...
...
@@ -159,19 +157,16 @@ const validation = computed(() => {
// 验证指标阈值
if
(
metricThresholds
.
value
.
sla_percent_min
!=
null
&&
(
metricThresholds
.
value
.
sla_percent_min
<
0
||
metricThresholds
.
value
.
sla_percent_min
>
100
))
{
errors
.
push
(
'
SLA最低百分比必须在0-100之间
'
)
}
if
(
metricThresholds
.
value
.
latency_p99_ms_max
!=
null
&&
metricThresholds
.
value
.
latency_p99_ms_max
<
0
)
{
errors
.
push
(
'
延迟P99最大值必须大于等于0
'
)
errors
.
push
(
t
(
'
admin.ops.settings.validation.slaMinPercentRange
'
))
}
if
(
metricThresholds
.
value
.
ttft_p99_ms_max
!=
null
&&
metricThresholds
.
value
.
ttft_p99_ms_max
<
0
)
{
errors
.
push
(
'
TTFT P99最大值必须大于等于0
'
)
errors
.
push
(
t
(
'
admin.ops.settings.validation.ttftP99MaxRange
'
)
)
}
if
(
metricThresholds
.
value
.
request_error_rate_percent_max
!=
null
&&
(
metricThresholds
.
value
.
request_error_rate_percent_max
<
0
||
metricThresholds
.
value
.
request_error_rate_percent_max
>
100
))
{
errors
.
push
(
'
请求错误率最大值必须在0-100之间
'
)
errors
.
push
(
t
(
'
admin.ops.settings.validation.requestErrorRateMaxRange
'
)
)
}
if
(
metricThresholds
.
value
.
upstream_error_rate_percent_max
!=
null
&&
(
metricThresholds
.
value
.
upstream_error_rate_percent_max
<
0
||
metricThresholds
.
value
.
upstream_error_rate_percent_max
>
100
))
{
errors
.
push
(
'
上游错误率最大值必须在0-100之间
'
)
errors
.
push
(
t
(
'
admin.ops.settings.validation.upstreamErrorRateMaxRange
'
)
)
}
return
{
valid
:
errors
.
length
===
0
,
errors
}
...
...
@@ -362,17 +357,6 @@ async function saveAllSettings() {
<p
class=
"mt-1 text-xs text-gray-500"
>
{{
t
(
'
admin.ops.settings.slaMinPercentHint
'
)
}}
</p>
</div>
<div>
<label
class=
"input-label"
>
{{
t
(
'
admin.ops.settings.latencyP99MaxMs
'
)
}}
</label>
<input
v-model.number=
"metricThresholds.latency_p99_ms_max"
type=
"number"
min=
"0"
step=
"100"
class=
"input"
/>
<p
class=
"mt-1 text-xs text-gray-500"
>
{{
t
(
'
admin.ops.settings.latencyP99MaxMsHint
'
)
}}
</p>
</div>
<div>
<label
class=
"input-label"
>
{{
t
(
'
admin.ops.settings.ttftP99MaxMs
'
)
}}
</label>
...
...
@@ -488,43 +472,63 @@ async function saveAllSettings() {
</div>
</div>
<!--
错误过滤
-->
<!--
Error Filtering
-->
<div
class=
"space-y-3"
>
<h5
class=
"text-xs font-semibold text-gray-700 dark:text-gray-300"
>
错误过滤
</h5>
<h5
class=
"text-xs font-semibold text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.ops.settings.errorFiltering
'
)
}}
</h5>
<div
class=
"flex items-center justify-between"
>
<div>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
忽略 c
ount
_t
okens
错误
</label>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.ops.settings.ignoreC
ount
T
okens
Errors
'
)
}}
</label>
<p
class=
"mt-1 text-xs text-gray-500"
>
启用后,c
ount
_t
okens
请求的错误将不计入运维监控的统计和告警中(但仍会存储在数据库中)
{{
t
(
'
admin.ops.settings.ignoreC
ount
T
okens
ErrorsHint
'
)
}}
</p>
</div>
<Toggle
v-model=
"advancedSettings.ignore_count_tokens_errors"
/>
</div>
<div
class=
"flex items-center justify-between"
>
<div>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.ops.settings.ignoreContextCanceled
'
)
}}
</label>
<p
class=
"mt-1 text-xs text-gray-500"
>
{{
t
(
'
admin.ops.settings.ignoreContextCanceledHint
'
)
}}
</p>
</div>
<Toggle
v-model=
"advancedSettings.ignore_context_canceled"
/>
</div>
<div
class=
"flex items-center justify-between"
>
<div>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.ops.settings.ignoreNoAvailableAccounts
'
)
}}
</label>
<p
class=
"mt-1 text-xs text-gray-500"
>
{{
t
(
'
admin.ops.settings.ignoreNoAvailableAccountsHint
'
)
}}
</p>
</div>
<Toggle
v-model=
"advancedSettings.ignore_no_available_accounts"
/>
</div>
</div>
<!--
自动刷新
-->
<!--
Auto Refresh
-->
<div
class=
"space-y-3"
>
<h5
class=
"text-xs font-semibold text-gray-700 dark:text-gray-300"
>
自动刷新
</h5>
<h5
class=
"text-xs font-semibold text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.ops.settings.autoRefresh
'
)
}}
</h5>
<div
class=
"flex items-center justify-between"
>
<div>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
启用自动刷新
</label>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.ops.settings.enableAutoRefresh
'
)
}}
</label>
<p
class=
"mt-1 text-xs text-gray-500"
>
自动刷新仪表板数据,启用后会定期拉取最新数据
{{
t
(
'
admin.ops.settings.enableAutoRefreshHint
'
)
}}
</p>
</div>
<Toggle
v-model=
"advancedSettings.auto_refresh_enabled"
/>
</div>
<div
v-if=
"advancedSettings.auto_refresh_enabled"
>
<label
class=
"input-label"
>
刷新间隔
</label>
<label
class=
"input-label"
>
{{
t
(
'
admin.ops.settings.refreshInterval
'
)
}}
</label>
<Select
v-model=
"advancedSettings.auto_refresh_interval_seconds"
:options=
"[
{ value: 15, label:
'15 秒'
},
{ value: 30, label:
'30 秒'
},
{ value: 60, label:
'60 秒'
}
{ value: 15, label:
t('admin.ops.settings.refreshInterval15s')
},
{ value: 30, label:
t('admin.ops.settings.refreshInterval30s')
},
{ value: 60, label:
t('admin.ops.settings.refreshInterval60s')
}
]"
/>
</div>
...
...
frontend/src/views/admin/ops/components/OpsThroughputTrendChart.vue
View file @
6901b64f
...
...
@@ -61,7 +61,7 @@ const chartData = computed(() => {
labels
:
props
.
points
.
map
((
p
)
=>
formatHistoryLabel
(
p
.
bucket_start
,
props
.
timeRange
)),
datasets
:
[
{
label
:
t
(
'
admin.ops.qps
'
)
,
label
:
'
QPS
'
,
data
:
props
.
points
.
map
((
p
)
=>
p
.
qps
??
0
),
borderColor
:
colors
.
value
.
blue
,
backgroundColor
:
colors
.
value
.
blueAlpha
,
...
...
@@ -183,7 +183,7 @@ function downloadChart() {
<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-blue-500"
></span>
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>
<template
v-if=
"!props.fullscreen"
>
<button
...
...
frontend/tailwind.config.js
View file @
6901b64f
...
...
@@ -50,16 +50,19 @@ export default {
},
fontFamily
:
{
sans
:
[
'
Inter
'
,
'
system-ui
'
,
'
-apple-system
'
,
'
BlinkMacSystemFont
'
,
'
Segoe UI
'
,
'
Roboto
'
,
'
Helvetica Neue
'
,
'
Arial
'
,
'
PingFang SC
'
,
'
Hiragino Sans GB
'
,
'
Microsoft YaHei
'
,
'
sans-serif
'
],
mono
:
[
'
JetBrains Mono
'
,
'
Fira Code
'
,
'
Monaco
'
,
'
Consolas
'
,
'
monospace
'
]
mono
:
[
'
ui-monospace
'
,
'
SFMono-Regular
'
,
'
Menlo
'
,
'
Monaco
'
,
'
Consolas
'
,
'
monospace
'
]
},
boxShadow
:
{
glass
:
'
0 8px 32px rgba(0, 0, 0, 0.08)
'
,
...
...
frontend/vite.config.ts
View file @
6901b64f
...
...
@@ -58,7 +58,49 @@ export default defineConfig({
},
build
:
{
outDir
:
'
../backend/internal/web/dist
'
,
emptyOutDir
:
true
emptyOutDir
:
true
,
rollupOptions
:
{
output
:
{
/**
* 手动分包配置
* 分离第三方库并按功能合并应用代码,避免循环依赖
*/
manualChunks
(
id
:
string
)
{
if
(
id
.
includes
(
'
node_modules
'
))
{
// Vue 核心库
if
(
id
.
includes
(
'
/vue/
'
)
||
id
.
includes
(
'
/vue-router/
'
)
||
id
.
includes
(
'
/pinia/
'
)
||
id
.
includes
(
'
/@vue/
'
)
)
{
return
'
vendor-vue
'
}
// UI 工具库(较大,单独分离)
if
(
id
.
includes
(
'
/@vueuse/
'
)
||
id
.
includes
(
'
/xlsx/
'
))
{
return
'
vendor-ui
'
}
// 图表库
if
(
id
.
includes
(
'
/chart.js/
'
)
||
id
.
includes
(
'
/vue-chartjs/
'
))
{
return
'
vendor-chart
'
}
// 国际化
if
(
id
.
includes
(
'
/vue-i18n/
'
)
||
id
.
includes
(
'
/@intlify/
'
))
{
return
'
vendor-i18n
'
}
// 其他小型第三方库合并
return
'
vendor-misc
'
}
// 应用代码:按入口点自动分包,不手动干预
// 这样可以避免循环依赖,同时保持合理的 chunk 数量
}
}
}
},
server
:
{
host
:
'
0.0.0.0
'
,
...
...
frontend/vitest.config.ts
0 → 100644
View file @
6901b64f
import
{
defineConfig
,
mergeConfig
}
from
'
vitest/config
'
import
viteConfig
from
'
./vite.config
'
export
default
mergeConfig
(
viteConfig
,
defineConfig
({
test
:
{
globals
:
true
,
environment
:
'
jsdom
'
,
include
:
[
'
src/**/*.{test,spec}.{js,ts,jsx,tsx}
'
],
exclude
:
[
'
node_modules
'
,
'
dist
'
],
coverage
:
{
provider
:
'
v8
'
,
reporter
:
[
'
text
'
,
'
json
'
,
'
html
'
],
include
:
[
'
src/**/*.{js,ts,vue}
'
],
exclude
:
[
'
node_modules
'
,
'
src/**/*.d.ts
'
,
'
src/**/*.spec.ts
'
,
'
src/**/*.test.ts
'
,
'
src/main.ts
'
],
thresholds
:
{
global
:
{
statements
:
80
,
branches
:
80
,
functions
:
80
,
lines
:
80
}
}
},
setupFiles
:
[
'
./src/__tests__/setup.ts
'
]
}
})
)
Prev
1
…
6
7
8
9
10
Next
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