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
0e448297
"vscode:/vscode.git/clone" did not exist on "b3463769dccdad2883094ed70ebf93db18bcf297"
Commit
0e448297
authored
Jan 13, 2026
by
yangjianbo
Browse files
Merge branch 'main' into dev
parents
9618cb56
93db889a
Changes
47
Hide whitespace changes
Inline
Side-by-side
frontend/src/views/admin/ops/components/OpsErrorDetailsModal.vue
View file @
0e448297
...
@@ -174,69 +174,75 @@ watch(
...
@@ -174,69 +174,75 @@ watch(
<
template
>
<
template
>
<BaseDialog
:show=
"show"
:title=
"modalTitle"
width=
"full"
@
close=
"close"
>
<BaseDialog
:show=
"show"
:title=
"modalTitle"
width=
"full"
@
close=
"close"
>
<!-- Filters -->
<div
class=
"flex h-full min-h-0 flex-col"
>
<div
class=
"border-b border-gray-200 pb-4 mb-4 dark:border-dark-700"
>
<!-- Filters -->
<div
class=
"grid grid-cols-1 gap-4 lg:grid-cols-12"
>
<div
class=
"mb-4 flex-shrink-0 border-b border-gray-200 pb-4 dark:border-dark-700"
>
<div
class=
"lg:col-span-5"
>
<div
class=
"grid grid-cols-1 gap-4 lg:grid-cols-12"
>
<div
class=
"relative group"
>
<div
class=
"lg:col-span-5"
>
<div
class=
"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5"
>
<div
class=
"relative group"
>
<svg
<div
class=
"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5"
>
class=
"h-4 w-4 text-gray-400 transition-colors group-focus-within:text-blue-500"
<svg
fill=
"none"
class=
"h-4 w-4 text-gray-400 transition-colors group-focus-within:text-blue-500"
viewBox=
"0 0 24 24"
fill=
"none"
stroke=
"currentColor"
viewBox=
"0 0 24 24"
>
stroke=
"currentColor"
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
stroke-width=
"2.5"
d=
"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
>
</svg>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
stroke-width=
"2.5"
d=
"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
<input
v-model=
"q"
type=
"text"
class=
"w-full rounded-2xl border-gray-200 bg-gray-50/50 py-2 pl-10 pr-4 text-sm 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"
:placeholder=
"t('admin.ops.errorDetails.searchPlaceholder')"
/>
</div>
</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>
<div
class=
"lg:col-span-2"
>
<Select
:model-value=
"phase"
:options=
"phaseSelectOptions"
class=
"w-full"
@
update:model-value=
"phase = String($event ?? '')"
/>
</div>
<div
class=
"lg:col-span-2"
>
<input
<input
v-model=
"
q
"
v-model=
"
accountIdInput
"
type=
"text"
type=
"text"
class=
"w-full rounded-2xl border-gray-200 bg-gray-50/50 py-2 pl-10 pr-4 text-sm 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"
inputmode=
"numeric"
:placeholder=
"t('admin.ops.errorDetails.searchPlaceholder')"
class=
"input w-full text-sm"
:placeholder=
"t('admin.ops.errorDetails.accountIdPlaceholder')"
/>
/>
</div>
</div>
</div>
<div
class=
"lg:col-span-2"
>
<div
class=
"lg:col-span-1 flex items-center justify-end"
>
<Select
:model-value=
"statusCode"
:options=
"statusCodeSelectOptions"
class=
"w-full"
@
update:model-value=
"statusCode = $event as any"
/>
<button
type=
"button"
class=
"btn btn-secondary btn-sm"
@
click=
"resetFilters"
>
</div>
{{
t
(
'
common.reset
'
)
}}
</button>
<div
class=
"lg:col-span-2"
>
</div>
<Select
:model-value=
"phase"
:options=
"phaseSelectOptions"
class=
"w-full"
@
update:model-value=
"phase = String($event ?? '')"
/>
</div>
</div>
</div>
<div
class=
"lg:col-span-2"
>
<!-- Body -->
<input
<div
class=
"flex min-h-0 flex-1 flex-col"
>
v-model=
"accountIdInput"
<div
class=
"mb-2 flex-shrink-0 text-xs text-gray-500 dark:text-gray-400"
>
type=
"text"
{{
t
(
'
admin.ops.errorDetails.total
'
)
}}
{{
total
}}
inputmode=
"numeric"
class=
"input w-full text-sm"
:placeholder=
"t('admin.ops.errorDetails.accountIdPlaceholder')"
/>
</div>
</div>
<div
class=
"lg:col-span-1 flex items-center justify-end"
>
<OpsErrorLogTable
<button
type=
"button"
class=
"btn btn-secondary btn-sm"
@
click=
"resetFilters"
>
class=
"min-h-0 flex-1"
{{
t
(
'
common.reset
'
)
}}
:rows=
"rows"
</button>
:total=
"total"
</div>
:loading=
"loading"
:page=
"page"
:page-size=
"pageSize"
@
openErrorDetail=
"emit('openErrorDetail', $event)"
@
update:page=
"page = $event"
@
update:pageSize=
"pageSize = $event"
/>
</div>
</div>
</div>
</div>
<!-- Body -->
<div
class=
"text-xs text-gray-500 dark:text-gray-400 mb-2"
>
{{
t
(
'
admin.ops.errorDetails.total
'
)
}}
{{
total
}}
</div>
<OpsErrorLogTable
:rows=
"rows"
:total=
"total"
:loading=
"loading"
:page=
"page"
:page-size=
"pageSize"
@
openErrorDetail=
"emit('openErrorDetail', $event)"
@
update:page=
"page = $event"
@
update:pageSize=
"pageSize = $event"
/>
</BaseDialog>
</BaseDialog>
</
template
>
</
template
>
frontend/src/views/admin/ops/components/OpsErrorLogTable.vue
View file @
0e448297
<
template
>
<
template
>
<div>
<div
class=
"flex h-full min-h-0 flex-col"
>
<div
v-if=
"loading"
class=
"flex items-center justify-center py-10"
>
<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
class=
"h-8 w-8 animate-spin rounded-full border-b-2 border-primary-600"
></div>
</div>
</div>
<div
v-else
class=
"overflow-x-auto"
>
<div
v-else
class=
"flex min-h-0 flex-1 flex-col"
>
<table
class=
"min-w-full divide-y divide-gray-200 dark:divide-dark-700"
>
<div
class=
"min-h-0 flex-1 overflow-auto"
>
<thead
class=
"sticky top-0 z-10 bg-gray-50/50 dark:bg-dark-800/50"
>
<table
class=
"min-w-full divide-y divide-gray-200 dark:divide-dark-700"
>
<tr>
<thead
class=
"sticky top-0 z-10 bg-gray-50/50 dark:bg-dark-800/50"
>
<th
<tr>
scope=
"col"
<th
class=
"whitespace-nowrap px-6 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
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>
<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>
<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.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"
>
{{
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"
>
{{
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-16 text-center text-sm text-gray-400 dark:text-dark-500"
>
{{
t
(
'
admin.ops.errorLog.noErrors
'
)
}}
</td>
</tr>
<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"
@
click=
"emit('openErrorDetail', log.id)"
@
keydown.enter.prevent=
"emit('openErrorDetail', log.id)"
@
keydown.space.prevent=
"emit('openErrorDetail', log.id)"
>
>
{{
t
(
'
admin.ops.errorLog.timeId
'
)
}}
<!-- Time & ID -->
</th>
<td
class=
"px-6 py-4"
>
<th
<div
class=
"flex flex-col gap-0.5"
>
scope=
"col"
<span
class=
"font-mono text-xs font-bold text-gray-900 dark:text-gray-200"
>
class=
"whitespace-nowrap px-6 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
{{
formatDateTime
(
log
.
created_at
).
split
(
'
'
)[
1
]
}}
>
</span>
{{
t
(
'
admin.ops.errorLog.context
'
)
}}
<span
</th>
class=
"font-mono text-[10px] text-gray-400 transition-colors group-hover:text-primary-600 dark:group-hover:text-primary-400"
<th
:title=
"log.request_id || log.client_request_id"
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"
{{
(
log
.
request_id
||
log
.
client_request_id
||
''
).
substring
(
0
,
12
)
}}
>
</span>
{{
t
(
'
admin.ops.errorLog.status
'
)
}}
</div>
</th>
</td>
<th
scope=
"col"
<!-- Context (Platform/Model) -->
class=
"px-6 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
<td
class=
"px-6 py-4"
>
>
<div
class=
"flex flex-col items-start gap-1.5"
>
{{
t
(
'
admin.ops.errorLog.message
'
)
}}
<span
</th>
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"
<th
>
scope=
"col"
{{
log
.
platform
||
'
-
'
}}
class=
"whitespace-nowrap px-6 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
</span>
>
<span
{{
t
(
'
admin.ops.errorLog.latency
'
)
}}
v-if=
"log.model"
</th>
class=
"max-w-[160px] truncate font-mono text-[10px] text-gray-500 dark:text-dark-400"
<th
:title=
"log.model"
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"
{{
log
.
model
}}
>
</span>
{{
t
(
'
admin.ops.errorLog.action
'
)
}}
<div
</th>
v-if=
"log.group_id || log.account_id"
</tr>
class=
"flex flex-wrap items-center gap-2 font-mono text-[10px] font-semibold text-gray-400 dark:text-dark-500"
</thead>
>
<tbody
class=
"divide-y divide-gray-100 dark:divide-dark-700"
>
<span
v-if=
"log.group_id"
>
{{
t
(
'
admin.ops.errorLog.grp
'
)
}}
{{
log
.
group_id
}}
</span>
<tr
v-if=
"rows.length === 0"
class=
"bg-white dark:bg-dark-900"
>
<span
v-if=
"log.account_id"
>
{{
t
(
'
admin.ops.errorLog.acc
'
)
}}
{{
log
.
account_id
}}
</span>
<td
colspan=
"6"
class=
"py-16 text-center text-sm text-gray-400 dark:text-dark-500"
>
{{
t
(
'
admin.ops.errorLog.noErrors
'
)
}}
</td>
</tr>
<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"
@
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"
>
{{
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>
</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"
>
{{
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>
</div>
</td>
<!-- Status & Severity -->
<td
class=
"px-6 py-4"
>
<div
class=
"flex flex-wrap items-center gap-2"
>
<span
:class=
"[
'inline-flex items-center rounded-lg px-2 py-1 text-xs font-black ring-1 ring-inset shadow-sm',
getStatusClass(log.status_code)
]"
>
{{
log
.
status_code
}}
</span>
<span
v-if=
"log.severity"
:class=
"['rounded-md px-2 py-0.5 text-[10px] font-black shadow-sm', 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-700 dark:text-gray-300"
: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>
<div
v-if=
"log.client_ip"
class=
"flex items-center gap-1"
>
</div>
<span
class=
"h-1 w-1 rounded-full bg-gray-300"
></span>
</td>
<span
class=
"text-[9px] font-mono font-bold text-gray-400"
>
{{
log
.
client_ip
}}
</span>
<!-- Status & Severity -->
<td
class=
"px-6 py-4"
>
<div
class=
"flex flex-wrap items-center gap-2"
>
<span
:class=
"[
'inline-flex items-center rounded-lg px-2 py-1 text-xs font-black ring-1 ring-inset shadow-sm',
getStatusClass(log.status_code)
]"
>
{{
log
.
status_code
}}
</span>
<span
v-if=
"log.severity"
:class=
"['rounded-md px-2 py-0.5 text-[10px] font-black shadow-sm', 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-700 dark:text-gray-300"
: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>
</div>
</div>
</div>
</td>
</td>
<!-- Latency -->
<!-- Latency -->
<td
class=
"px-6 py-4 text-right"
>
<td
class=
"px-6 py-4 text-right"
>
<div
class=
"flex flex-col items-end"
>
<div
class=
"flex flex-col items-end"
>
<span
class=
"font-mono text-xs font-black"
:class=
"getLatencyClass(log.latency_ms ?? null)"
>
<span
class=
"font-mono text-xs font-black"
:class=
"getLatencyClass(log.latency_ms ?? null)"
>
{{
log
.
latency_ms
!=
null
?
Math
.
round
(
log
.
latency_ms
)
+
'
ms
'
:
'
--
'
}}
{{
log
.
latency_ms
!=
null
?
Math
.
round
(
log
.
latency_ms
)
+
'
ms
'
:
'
--
'
}}
</span>
</span>
</div>
</div>
</td>
</td>
<!-- Actions -->
<!-- Actions -->
<td
class=
"px-6 py-4 text-right"
@
click.stop
>
<td
class=
"px-6 py-4 text-right"
@
click.stop
>
<button
type=
"button"
class=
"btn btn-secondary btn-sm"
@
click=
"emit('openErrorDetail', log.id)"
>
<button
type=
"button"
class=
"btn btn-secondary btn-sm"
@
click=
"emit('openErrorDetail', log.id)"
>
{{
t
(
'
admin.ops.errorLog.details
'
)
}}
{{
t
(
'
admin.ops.errorLog.details
'
)
}}
</button>
</button>
</td>
</td>
</tr>
</tr>
</tbody>
</tbody>
</table>
</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)"
/>
</div>
</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)"
/>
</div>
</div>
</
template
>
</
template
>
...
...
frontend/src/views/admin/ops/components/OpsRequestDetailsModal.vue
View file @
0e448297
...
@@ -95,6 +95,7 @@ watch(
...
@@ -95,6 +95,7 @@ watch(
(
open
)
=>
{
(
open
)
=>
{
if
(
open
)
{
if
(
open
)
{
page
.
value
=
1
page
.
value
=
1
pageSize
.
value
=
20
fetchData
()
fetchData
()
}
}
}
}
...
@@ -150,45 +151,46 @@ const kindBadgeClass = (kind: string) => {
...
@@ -150,45 +151,46 @@ const kindBadgeClass = (kind: string) => {
<
template
>
<
template
>
<BaseDialog
:show=
"modelValue"
:title=
"props.preset.title || t('admin.ops.requestDetails.title')"
width=
"full"
@
close=
"close"
>
<BaseDialog
:show=
"modelValue"
:title=
"props.preset.title || t('admin.ops.requestDetails.title')"
width=
"full"
@
close=
"close"
>
<template
#default
>
<template
#default
>
<div
class=
"flex items-center justify-between mb-4"
>
<div
class=
"flex h-full min-h-0 flex-col"
>
<div
class=
"text-xs text-gray-500 dark:text-gray-400"
>
<div
class=
"mb-4 flex flex-shrink-0 items-center justify-between"
>
{{
t
(
'
admin.ops.requestDetails.rangeLabel
'
,
{
range
:
rangeLabel
}
)
}}
<div
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.ops.requestDetails.rangeLabel
'
,
{
range
:
rangeLabel
}
)
}}
<
/div
>
<
button
type
=
"
button
"
class
=
"
btn btn-secondary btn-sm
"
@
click
=
"
fetchData
"
>
{{
t
(
'
common.refresh
'
)
}}
<
/button
>
<
/div
>
<
/div
>
<
button
type
=
"
button
"
class
=
"
btn btn-secondary btn-sm
"
@
click
=
"
fetchData
"
>
{{
t
(
'
common.refresh
'
)
}}
<
/button
>
<
/div
>
<!--
Loading
-->
<!--
Loading
-->
<
div
v
-
if
=
"
loading
"
class
=
"
flex items-center justify-center py-16
"
>
<
div
v
-
if
=
"
loading
"
class
=
"
flex flex-1 items-center justify-center py-16
"
>
<
div
class
=
"
flex flex-col items-center gap-3
"
>
<
div
class
=
"
flex flex-col items-center gap-3
"
>
<
svg
class
=
"
h-8 w-8 animate-spin text-blue-500
"
fill
=
"
none
"
viewBox
=
"
0 0 24 24
"
>
<
svg
class
=
"
h-8 w-8 animate-spin text-blue-500
"
fill
=
"
none
"
viewBox
=
"
0 0 24 24
"
>
<
circle
class
=
"
opacity-25
"
cx
=
"
12
"
cy
=
"
12
"
r
=
"
10
"
stroke
=
"
currentColor
"
stroke
-
width
=
"
4
"
><
/circle
>
<
circle
class
=
"
opacity-25
"
cx
=
"
12
"
cy
=
"
12
"
r
=
"
10
"
stroke
=
"
currentColor
"
stroke
-
width
=
"
4
"
><
/circle
>
<
path
<
path
class
=
"
opacity-75
"
class
=
"
opacity-75
"
fill
=
"
currentColor
"
fill
=
"
currentColor
"
d
=
"
M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z
"
d
=
"
M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z
"
><
/path
>
><
/path
>
<
/svg
>
<
/svg
>
<
span
class
=
"
text-sm font-medium text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
common.loading
'
)
}}
<
/span
>
<
span
class
=
"
text-sm font-medium text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
common.loading
'
)
}}
<
/span
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<!--
Table
-->
<!--
Table
-->
<
div
v
-
else
>
<
div
v
-
else
class
=
"
flex min-h-0 flex-1 flex-col
"
>
<
div
v
-
if
=
"
items.length === 0
"
class
=
"
rounded-xl border border-dashed border-gray-200 p-10 text-center dark:border-dark-700
"
>
<
div
v
-
if
=
"
items.length === 0
"
class
=
"
rounded-xl border border-dashed border-gray-200 p-10 text-center dark:border-dark-700
"
>
<
div
class
=
"
text-sm font-medium text-gray-600 dark:text-gray-300
"
>
{{
t
(
'
admin.ops.requestDetails.empty
'
)
}}
<
/div
>
<
div
class
=
"
text-sm font-medium text-gray-600 dark:text-gray-300
"
>
{{
t
(
'
admin.ops.requestDetails.empty
'
)
}}
<
/div
>
<
div
class
=
"
mt-1 text-xs text-gray-400
"
>
{{
t
(
'
admin.ops.requestDetails.emptyHint
'
)
}}
<
/div
>
<
div
class
=
"
mt-1 text-xs text-gray-400
"
>
{{
t
(
'
admin.ops.requestDetails.emptyHint
'
)
}}
<
/div
>
<
/div
>
<
/div
>
<
div
v
-
else
class
=
"
overflow-hidden rounded-xl border border-gray-200 dark:border-dark-700
"
>
<
div
v
-
else
class
=
"
flex min-h-0 flex-1 flex-col
overflow-hidden rounded-xl border border-gray-200 dark:border-dark-700
"
>
<
div
class
=
"
overflow-
x-
auto
"
>
<
div
class
=
"
min-h-0 flex-1
overflow-auto
"
>
<
table
class
=
"
min-w-full divide-y divide-gray-200 dark:divide-dark-700
"
>
<
table
class
=
"
min-w-full divide-y divide-gray-200 dark:divide-dark-700
"
>
<
thead
class
=
"
bg-gray-50 dark:bg-dark-900
"
>
<
thead
class
=
"
sticky top-0 z-10
bg-gray-50 dark:bg-dark-900
"
>
<
tr
>
<
tr
>
<
th
class
=
"
px-4 py-3 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400
"
>
<
th
class
=
"
px-4 py-3 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.ops.requestDetails.table.time
'
)
}}
{{
t
(
'
admin.ops.requestDetails.table.time
'
)
}}
...
@@ -265,15 +267,16 @@ const kindBadgeClass = (kind: string) => {
...
@@ -265,15 +267,16 @@ const kindBadgeClass = (kind: string) => {
<
/tr
>
<
/tr
>
<
/tbody
>
<
/tbody
>
<
/table
>
<
/table
>
<
/div
>
<
/div
>
<
Pagination
<
Pagination
:
total
=
"
total
"
:
total
=
"
total
"
:
page
=
"
page
"
:
page
=
"
page
"
:
page
-
size
=
"
pageSize
"
:
page
-
size
=
"
pageSize
"
@
update
:
page
=
"
handlePageChange
"
@
update
:
page
=
"
handlePageChange
"
@
update
:
pageSize
=
"
handlePageSizeChange
"
@
update
:
pageSize
=
"
handlePageSizeChange
"
/>
/>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/template
>
<
/template
>
...
...
frontend/src/views/admin/ops/components/OpsRuntimeSettingsCard.vue
View file @
0e448297
...
@@ -45,6 +45,36 @@ function validateRuntimeSettings(settings: OpsAlertRuntimeSettings): ValidationR
...
@@ -45,6 +45,36 @@ function validateRuntimeSettings(settings: OpsAlertRuntimeSettings): ValidationR
errors
.
push
(
t
(
'
admin.ops.runtime.validation.evalIntervalRange
'
))
errors
.
push
(
t
(
'
admin.ops.runtime.validation.evalIntervalRange
'
))
}
}
// Thresholds validation
const
thresholds
=
settings
.
thresholds
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
'
)
}
}
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
'
)
}
}
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 之间
'
)
}
}
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 之间
'
)
}
}
}
const
lock
=
settings
.
distributed_lock
const
lock
=
settings
.
distributed_lock
if
(
lock
?.
enabled
)
{
if
(
lock
?.
enabled
)
{
if
(
!
lock
.
key
||
lock
.
key
.
trim
().
length
<
3
)
{
if
(
!
lock
.
key
||
lock
.
key
.
trim
().
length
<
3
)
{
...
@@ -130,6 +160,15 @@ function openAlertEditor() {
...
@@ -130,6 +160,15 @@ function openAlertEditor() {
if
(
!
Array
.
isArray
(
draftAlert
.
value
.
silencing
.
entries
))
{
if
(
!
Array
.
isArray
(
draftAlert
.
value
.
silencing
.
entries
))
{
draftAlert
.
value
.
silencing
.
entries
=
[]
draftAlert
.
value
.
silencing
.
entries
=
[]
}
}
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
}
}
}
}
showAlertEditor
.
value
=
true
showAlertEditor
.
value
=
true
...
@@ -295,6 +334,81 @@ onMounted(() => {
...
@@ -295,6 +334,81 @@ onMounted(() => {
<p
class=
"mt-1 text-xs text-gray-500"
>
{{
t
(
'
admin.ops.runtime.evalIntervalHint
'
)
}}
</p>
<p
class=
"mt-1 text-xs text-gray-500"
>
{{
t
(
'
admin.ops.runtime.evalIntervalHint
'
)
}}
</p>
</div>
</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=
"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>
<input
v-model.number=
"draftAlert.thresholds.sla_percent_min"
type=
"number"
min=
"0"
max=
"100"
step=
"0.1"
class=
"input"
placeholder=
"99.5"
/>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
SLA 低于此值时将显示为红色
</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>
<input
v-model.number=
"draftAlert.thresholds.ttft_p99_ms_max"
type=
"number"
min=
"0"
step=
"100"
class=
"input"
placeholder=
"500"
/>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
TTFT P99 高于此值时将显示为红色
</p>
</div>
<div>
<div
class=
"mb-1 text-xs font-medium text-gray-600 dark:text-gray-300"
>
请求错误率最大值 (%)
</div>
<input
v-model.number=
"draftAlert.thresholds.request_error_rate_percent_max"
type=
"number"
min=
"0"
max=
"100"
step=
"0.1"
class=
"input"
placeholder=
"5"
/>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
请求错误率高于此值时将显示为红色
</p>
</div>
<div>
<div
class=
"mb-1 text-xs font-medium text-gray-600 dark:text-gray-300"
>
上游错误率最大值 (%)
</div>
<input
v-model.number=
"draftAlert.thresholds.upstream_error_rate_percent_max"
type=
"number"
min=
"0"
max=
"100"
step=
"0.1"
class=
"input"
placeholder=
"5"
/>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
上游错误率高于此值时将显示为红色
</p>
</div>
</div>
</div>
<div
class=
"rounded-2xl bg-gray-50 p-4 dark:bg-dark-700/50"
>
<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"
>
{{
t
(
'
admin.ops.runtime.silencing.title
'
)
}}
</div>
<div
class=
"mb-2 text-sm font-semibold text-gray-900 dark:text-white"
>
{{
t
(
'
admin.ops.runtime.silencing.title
'
)
}}
</div>
...
...
frontend/src/views/admin/ops/components/OpsSettingsDialog.vue
View file @
0e448297
...
@@ -6,7 +6,7 @@ import { opsAPI } from '@/api/admin/ops'
...
@@ -6,7 +6,7 @@ import { opsAPI } from '@/api/admin/ops'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
Toggle
from
'
@/components/common/Toggle.vue
'
import
Toggle
from
'
@/components/common/Toggle.vue
'
import
type
{
OpsAlertRuntimeSettings
,
EmailNotificationConfig
,
AlertSeverity
,
OpsAdvancedSettings
}
from
'
../types
'
import
type
{
OpsAlertRuntimeSettings
,
EmailNotificationConfig
,
AlertSeverity
,
OpsAdvancedSettings
,
OpsMetricThresholds
}
from
'
../types
'
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
const
appStore
=
useAppStore
()
const
appStore
=
useAppStore
()
...
@@ -29,19 +29,38 @@ const runtimeSettings = ref<OpsAlertRuntimeSettings | null>(null)
...
@@ -29,19 +29,38 @@ const runtimeSettings = ref<OpsAlertRuntimeSettings | null>(null)
const
emailConfig
=
ref
<
EmailNotificationConfig
|
null
>
(
null
)
const
emailConfig
=
ref
<
EmailNotificationConfig
|
null
>
(
null
)
// 高级设置
// 高级设置
const
advancedSettings
=
ref
<
OpsAdvancedSettings
|
null
>
(
null
)
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
})
// 加载所有配置
// 加载所有配置
async
function
loadAllSettings
()
{
async
function
loadAllSettings
()
{
loading
.
value
=
true
loading
.
value
=
true
try
{
try
{
const
[
runtime
,
email
,
advanced
]
=
await
Promise
.
all
([
const
[
runtime
,
email
,
advanced
,
thresholds
]
=
await
Promise
.
all
([
opsAPI
.
getAlertRuntimeSettings
(),
opsAPI
.
getAlertRuntimeSettings
(),
opsAPI
.
getEmailNotificationConfig
(),
opsAPI
.
getEmailNotificationConfig
(),
opsAPI
.
getAdvancedSettings
()
opsAPI
.
getAdvancedSettings
(),
opsAPI
.
getMetricThresholds
()
])
])
runtimeSettings
.
value
=
runtime
runtimeSettings
.
value
=
runtime
emailConfig
.
value
=
email
emailConfig
.
value
=
email
advancedSettings
.
value
=
advanced
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
}
}
}
catch
(
err
:
any
)
{
}
catch
(
err
:
any
)
{
console
.
error
(
'
[OpsSettingsDialog] Failed to load settings
'
,
err
)
console
.
error
(
'
[OpsSettingsDialog] Failed to load settings
'
,
err
)
appStore
.
showError
(
err
?.
response
?.
data
?.
detail
||
t
(
'
admin.ops.settings.loadFailed
'
))
appStore
.
showError
(
err
?.
response
?.
data
?.
detail
||
t
(
'
admin.ops.settings.loadFailed
'
))
...
@@ -138,6 +157,23 @@ const validation = computed(() => {
...
@@ -138,6 +157,23 @@ 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
'
)
}
if
(
metricThresholds
.
value
.
ttft_p99_ms_max
!=
null
&&
metricThresholds
.
value
.
ttft_p99_ms_max
<
0
)
{
errors
.
push
(
'
TTFT P99最大值必须大于等于0
'
)
}
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之间
'
)
}
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之间
'
)
}
return
{
valid
:
errors
.
length
===
0
,
errors
}
return
{
valid
:
errors
.
length
===
0
,
errors
}
})
})
...
@@ -153,14 +189,15 @@ async function saveAllSettings() {
...
@@ -153,14 +189,15 @@ async function saveAllSettings() {
await
Promise
.
all
([
await
Promise
.
all
([
runtimeSettings
.
value
?
opsAPI
.
updateAlertRuntimeSettings
(
runtimeSettings
.
value
)
:
Promise
.
resolve
(),
runtimeSettings
.
value
?
opsAPI
.
updateAlertRuntimeSettings
(
runtimeSettings
.
value
)
:
Promise
.
resolve
(),
emailConfig
.
value
?
opsAPI
.
updateEmailNotificationConfig
(
emailConfig
.
value
)
:
Promise
.
resolve
(),
emailConfig
.
value
?
opsAPI
.
updateEmailNotificationConfig
(
emailConfig
.
value
)
:
Promise
.
resolve
(),
advancedSettings
.
value
?
opsAPI
.
updateAdvancedSettings
(
advancedSettings
.
value
)
:
Promise
.
resolve
()
advancedSettings
.
value
?
opsAPI
.
updateAdvancedSettings
(
advancedSettings
.
value
)
:
Promise
.
resolve
(),
opsAPI
.
updateMetricThresholds
(
metricThresholds
.
value
)
])
])
appStore
.
showSuccess
(
t
(
'
admin.ops.settings.saveSuccess
'
))
appStore
.
showSuccess
(
t
(
'
admin.ops.settings.saveSuccess
'
))
emit
(
'
saved
'
)
emit
(
'
saved
'
)
emit
(
'
close
'
)
emit
(
'
close
'
)
}
catch
(
err
:
any
)
{
}
catch
(
err
:
any
)
{
console
.
error
(
'
[OpsSettingsDialog] Failed to save settings
'
,
err
)
console
.
error
(
'
[OpsSettingsDialog] Failed to save settings
'
,
err
)
appStore
.
showError
(
err
?.
response
?.
data
?.
detail
||
t
(
'
admin.ops.settings.saveFailed
'
))
appStore
.
showError
(
err
?.
response
?.
data
?.
message
||
err
?.
response
?.
data
?.
detail
||
t
(
'
admin.ops.settings.saveFailed
'
))
}
finally
{
}
finally
{
saving
.
value
=
false
saving
.
value
=
false
}
}
...
@@ -306,6 +343,77 @@ async function saveAllSettings() {
...
@@ -306,6 +343,77 @@ async function saveAllSettings() {
</div>
</div>
</div>
</div>
<!-- 指标阈值配置 -->
<div
class=
"rounded-2xl bg-gray-50 p-4 dark:bg-dark-700/50"
>
<h4
class=
"mb-3 text-sm font-semibold text-gray-900 dark:text-white"
>
{{
t
(
'
admin.ops.settings.metricThresholds
'
)
}}
</h4>
<p
class=
"mb-4 text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.ops.settings.metricThresholdsHint
'
)
}}
</p>
<div
class=
"space-y-4"
>
<div>
<label
class=
"input-label"
>
{{
t
(
'
admin.ops.settings.slaMinPercent
'
)
}}
</label>
<input
v-model.number=
"metricThresholds.sla_percent_min"
type=
"number"
min=
"0"
max=
"100"
step=
"0.1"
class=
"input"
/>
<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>
<input
v-model.number=
"metricThresholds.ttft_p99_ms_max"
type=
"number"
min=
"0"
step=
"50"
class=
"input"
/>
<p
class=
"mt-1 text-xs text-gray-500"
>
{{
t
(
'
admin.ops.settings.ttftP99MaxMsHint
'
)
}}
</p>
</div>
<div>
<label
class=
"input-label"
>
{{
t
(
'
admin.ops.settings.requestErrorRateMaxPercent
'
)
}}
</label>
<input
v-model.number=
"metricThresholds.request_error_rate_percent_max"
type=
"number"
min=
"0"
max=
"100"
step=
"0.1"
class=
"input"
/>
<p
class=
"mt-1 text-xs text-gray-500"
>
{{
t
(
'
admin.ops.settings.requestErrorRateMaxPercentHint
'
)
}}
</p>
</div>
<div>
<label
class=
"input-label"
>
{{
t
(
'
admin.ops.settings.upstreamErrorRateMaxPercent
'
)
}}
</label>
<input
v-model.number=
"metricThresholds.upstream_error_rate_percent_max"
type=
"number"
min=
"0"
max=
"100"
step=
"0.1"
class=
"input"
/>
<p
class=
"mt-1 text-xs text-gray-500"
>
{{
t
(
'
admin.ops.settings.upstreamErrorRateMaxPercentHint
'
)
}}
</p>
</div>
</div>
</div>
<!-- 高级设置 -->
<!-- 高级设置 -->
<details
class=
"rounded-2xl bg-gray-50 dark:bg-dark-700/50"
>
<details
class=
"rounded-2xl bg-gray-50 dark:bg-dark-700/50"
>
<summary
class=
"cursor-pointer p-4 text-sm font-semibold text-gray-900 dark:text-white"
>
<summary
class=
"cursor-pointer p-4 text-sm font-semibold text-gray-900 dark:text-white"
>
...
@@ -379,6 +487,48 @@ async function saveAllSettings() {
...
@@ -379,6 +487,48 @@ async function saveAllSettings() {
<Toggle
v-model=
"advancedSettings.aggregation.aggregation_enabled"
/>
<Toggle
v-model=
"advancedSettings.aggregation.aggregation_enabled"
/>
</div>
</div>
</div>
</div>
<!-- 错误过滤 -->
<div
class=
"space-y-3"
>
<h5
class=
"text-xs font-semibold text-gray-700 dark:text-gray-300"
>
错误过滤
</h5>
<div
class=
"flex items-center justify-between"
>
<div>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
忽略 count_tokens 错误
</label>
<p
class=
"mt-1 text-xs text-gray-500"
>
启用后,count_tokens 请求的错误将不计入运维监控的统计和告警中(但仍会存储在数据库中)
</p>
</div>
<Toggle
v-model=
"advancedSettings.ignore_count_tokens_errors"
/>
</div>
</div>
<!-- 自动刷新 -->
<div
class=
"space-y-3"
>
<h5
class=
"text-xs font-semibold text-gray-700 dark:text-gray-300"
>
自动刷新
</h5>
<div
class=
"flex items-center justify-between"
>
<div>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
启用自动刷新
</label>
<p
class=
"mt-1 text-xs text-gray-500"
>
自动刷新仪表板数据,启用后会定期拉取最新数据
</p>
</div>
<Toggle
v-model=
"advancedSettings.auto_refresh_enabled"
/>
</div>
<div
v-if=
"advancedSettings.auto_refresh_enabled"
>
<label
class=
"input-label"
>
刷新间隔
</label>
<Select
v-model=
"advancedSettings.auto_refresh_interval_seconds"
:options=
"[
{ value: 15, label: '15 秒' },
{ value: 30, label: '30 秒' },
{ value: 60, label: '60 秒' }
]"
/>
</div>
</div>
</div>
</div>
</details>
</details>
</div>
</div>
...
...
frontend/src/views/admin/ops/components/OpsThroughputTrendChart.vue
View file @
0e448297
...
@@ -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>
...
...
frontend/src/views/admin/ops/types.ts
View file @
0e448297
...
@@ -14,6 +14,7 @@ export type {
...
@@ -14,6 +14,7 @@ export type {
EmailNotificationConfig
,
EmailNotificationConfig
,
OpsDistributedLockSettings
,
OpsDistributedLockSettings
,
OpsAlertRuntimeSettings
,
OpsAlertRuntimeSettings
,
OpsMetricThresholds
,
OpsAdvancedSettings
,
OpsAdvancedSettings
,
OpsDataRetentionSettings
,
OpsDataRetentionSettings
,
OpsAggregationSettings
OpsAggregationSettings
...
...
Prev
1
2
3
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