Skip to content

Commit 58e9065

Browse files
refactor(usage): 错误表提取共享徽章工具与 IP 批量工具条,后端排序解析归并 SetSort
- 新增 utils/errorBadges.ts(状态码/请求类型徽章、排序 key 映射)与 IpGeoBatchToolbar 组件,两张错误表接入 - OpsErrorLogFilter.SetSort 统一 admin/用户端 sort 参数解析 - 范围限定功能自身文件,UsageTable 基线保持不动 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 54cce18 commit 58e9065

7 files changed

Lines changed: 146 additions & 110 deletions

File tree

backend/internal/handler/admin/ops_handler.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,7 @@ func NewOpsHandler(opsService *service.OpsService) *OpsHandler {
7777
// Column whitelist and order normalization live in the repository; unknown
7878
// values degrade to the default (created_at DESC), mirroring the usage list.
7979
func applyOpsErrorSortParams(c *gin.Context, filter *service.OpsErrorLogFilter) {
80-
filter.SortBy = strings.TrimSpace(c.Query("sort_by"))
81-
filter.SortOrder = strings.TrimSpace(c.Query("sort_order"))
80+
filter.SetSort(c.Query("sort_by"), c.Query("sort_order"))
8281
}
8382

8483
// GET /api/v1/admin/ops/errors

backend/internal/handler/usage_handler.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,8 +323,7 @@ func (h *UsageHandler) ListErrors(c *gin.Context) {
323323
}
324324

325325
// 排序对齐用量明细:列白名单与方向归一在 repo 层,非法值回退 created_at DESC。
326-
filter.SortBy = strings.TrimSpace(c.Query("sort_by"))
327-
filter.SortOrder = strings.TrimSpace(c.Query("sort_order"))
326+
filter.SetSort(c.Query("sort_by"), c.Query("sort_order"))
328327

329328
result, err := h.opsService.ListUserErrorRequests(c.Request.Context(), subject.UserID, filter)
330329
if err != nil {

backend/internal/service/ops_models.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package service
22

3-
import "time"
3+
import (
4+
"strings"
5+
"time"
6+
)
47

58
type OpsSystemLog struct {
69
ID int64 `json:"id"`
@@ -166,6 +169,13 @@ type OpsErrorLogFilter struct {
166169
SortOrder string
167170
}
168171

172+
// SetSort normalizes raw sort_by/sort_order query values into the filter.
173+
// Shared by the admin and user-facing error list handlers.
174+
func (f *OpsErrorLogFilter) SetSort(sortBy, sortOrder string) {
175+
f.SortBy = strings.TrimSpace(sortBy)
176+
f.SortOrder = strings.TrimSpace(sortOrder)
177+
}
178+
169179
type OpsErrorLogList struct {
170180
Errors []*OpsErrorLog `json:"errors"`
171181
Total int `json:"total"`
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<template>
2+
<div
3+
v-if="uniqueIps.length > 0"
4+
class="flex flex-shrink-0 items-center justify-end gap-2 border-b border-gray-200 px-4 py-2 dark:border-dark-700"
5+
>
6+
<span v-if="pendingCount > 0" class="text-xs text-gray-500 dark:text-gray-400">
7+
{{ t('usage.ipGeo.pending', { count: pendingCount }) }}
8+
</span>
9+
<button
10+
type="button"
11+
class="inline-flex items-center gap-1 rounded px-2 py-1 text-xs font-medium text-primary-600 transition-colors hover:bg-primary-50 disabled:cursor-not-allowed disabled:opacity-50 dark:text-primary-400 dark:hover:bg-primary-900/30"
12+
:disabled="loading || pendingCount === 0"
13+
@click="run"
14+
>
15+
{{ loading ? t('usage.ipGeo.batchFetching') : t('usage.ipGeo.batchFetch') }}
16+
</button>
17+
</div>
18+
</template>
19+
20+
<script setup lang="ts">
21+
import { computed, ref } from 'vue'
22+
import { useI18n } from 'vue-i18n'
23+
import { fetchBatch, getEntry } from '@/utils/ipGeoLookup'
24+
25+
// 当前页 IP 批量地理查询工具条:传入原始 IP 列表(可含空值),内部去重;
26+
// 无 IP 时自身不渲染。批量失败 emit failed,由使用方弹提示。
27+
const props = defineProps<{
28+
ips: Array<string | null | undefined>
29+
}>()
30+
31+
const emit = defineEmits<{
32+
(e: 'failed'): void
33+
}>()
34+
35+
const { t } = useI18n()
36+
37+
const uniqueIps = computed(() =>
38+
Array.from(new Set(props.ips.filter((ip): ip is string => Boolean(ip))))
39+
)
40+
41+
const pendingCount = computed(() =>
42+
uniqueIps.value.filter((ip) => {
43+
const status = getEntry(ip).status
44+
return status === 'idle' || status === 'error'
45+
}).length
46+
)
47+
48+
const loading = ref(false)
49+
50+
const run = async () => {
51+
loading.value = true
52+
try {
53+
const ok = await fetchBatch(uniqueIps.value)
54+
if (!ok) emit('failed')
55+
} finally {
56+
loading.value = false
57+
}
58+
}
59+
</script>

frontend/src/components/user/UserErrorRequestsTable.vue

Lines changed: 14 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,7 @@
11
<template>
22
<div class="flex min-h-0 flex-1 flex-col">
33
<div class="card flex min-h-0 flex-1 flex-col overflow-hidden">
4-
<div
5-
v-if="currentPageIps.length > 0"
6-
class="flex flex-shrink-0 items-center justify-end gap-2 border-b border-gray-200 px-4 py-2 dark:border-dark-700"
7-
>
8-
<span v-if="pendingIpCount > 0" class="text-xs text-gray-500 dark:text-gray-400">
9-
{{ t('usage.ipGeo.pending', { count: pendingIpCount }) }}
10-
</span>
11-
<button
12-
type="button"
13-
class="inline-flex items-center gap-1 rounded px-2 py-1 text-xs font-medium text-primary-600 transition-colors hover:bg-primary-50 disabled:cursor-not-allowed disabled:opacity-50 dark:text-primary-400 dark:hover:bg-primary-900/30"
14-
:disabled="ipGeoBatchLoading || pendingIpCount === 0"
15-
@click="handleBatchFetchIpGeo"
16-
>
17-
{{ ipGeoBatchLoading ? t('usage.ipGeo.batchFetching') : t('usage.ipGeo.batchFetch') }}
18-
</button>
19-
</div>
4+
<IpGeoBatchToolbar :ips="rows.map((r) => r.client_ip)" @failed="emit('ipGeoBatchFailed')" />
205

216
<DataTable
227
:columns="columns"
@@ -143,8 +128,15 @@ import EmptyState from '@/components/common/EmptyState.vue'
143128
import Pagination from '@/components/common/Pagination.vue'
144129
import UserErrorDetailModal from '@/components/user/UserErrorDetailModal.vue'
145130
import IpGeoCell from '@/components/common/IpGeoCell.vue'
131+
import IpGeoBatchToolbar from '@/components/common/IpGeoBatchToolbar.vue'
146132
import { formatDateTime } from '@/utils/format'
147-
import { fetchBatch, getEntry } from '@/utils/ipGeoLookup'
133+
import {
134+
mapErrorSortKey,
135+
numericRequestTypeKind,
136+
requestTypeBadgeClass,
137+
requestTypeLabelKey,
138+
statusCodeBadgeClass,
139+
} from '@/utils/errorBadges'
148140
import type { UserErrorRequest } from '@/types'
149141
import type { Column } from '@/components/common/types'
150142
@@ -165,9 +157,8 @@ const emit = defineEmits<{
165157
(e: 'sort', sortBy: string, sortOrder: 'asc' | 'desc'): void
166158
}>()
167159
168-
// 列 key → 后端 sort_by:status 列实际按 status_code 排序
169160
function onSort(key: string, order: 'asc' | 'desc') {
170-
emit('sort', key === 'status' ? 'status_code' : key, order)
161+
emit('sort', mapErrorSortKey(key), order)
171162
}
172163
173164
const { t } = useI18n()
@@ -195,37 +186,10 @@ const columns = computed<Column[]>(() =>
195186
: allColumns.value
196187
)
197188
198-
// 类型徽章:request_type 1/2/3 → 同步/流式/WS;缺失时按 stream 布尔回退。
199-
// 配色对齐 UsageTable getRequestTypeBadgeClass(stream 蓝、sync 灰、ws 紫)。
200189
function requestTypeBadge(row: UserErrorRequest): { label: string; className: string } | null {
201-
const rt = row.request_type ?? (row.stream == null ? 0 : row.stream ? 2 : 1)
202-
if (rt === 3) return { label: t('usage.ws'), className: 'bg-violet-100 text-violet-800 dark:bg-violet-900 dark:text-violet-200' }
203-
if (rt === 2) return { label: t('usage.stream'), className: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' }
204-
if (rt === 1) return { label: t('usage.sync'), className: 'bg-gray-100 text-gray-800 dark:bg-dark-700 dark:text-gray-200' }
205-
return null
206-
}
207-
208-
const ipGeoBatchLoading = ref(false)
209-
210-
const currentPageIps = computed(() =>
211-
Array.from(new Set(props.rows.map((row) => row.client_ip).filter((ip): ip is string => Boolean(ip))))
212-
)
213-
214-
const pendingIpCount = computed(() =>
215-
currentPageIps.value.filter((ip) => {
216-
const status = getEntry(ip).status
217-
return status === 'idle' || status === 'error'
218-
}).length
219-
)
220-
221-
const handleBatchFetchIpGeo = async () => {
222-
ipGeoBatchLoading.value = true
223-
try {
224-
const ok = await fetchBatch(currentPageIps.value)
225-
if (!ok) emit('ipGeoBatchFailed')
226-
} finally {
227-
ipGeoBatchLoading.value = false
228-
}
190+
const kind = numericRequestTypeKind(row.request_type, row.stream)
191+
if (!kind) return null
192+
return { label: t(requestTypeLabelKey(kind)), className: requestTypeBadgeClass(kind) }
229193
}
230194
231195
const showDetail = ref(false)
@@ -236,11 +200,5 @@ function openDetail(id: number) {
236200
showDetail.value = true
237201
}
238202
239-
// 状态码配色对齐管理端错误表(OpsErrorLogTable.getStatusClass)
240-
function statusClass(code: number) {
241-
if (code >= 500) return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
242-
if (code === 429) return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'
243-
if (code >= 400) return 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200'
244-
return 'bg-gray-100 text-gray-800 dark:bg-dark-700 dark:text-gray-200'
245-
}
203+
const statusClass = statusCodeBadgeClass
246204
</script>

frontend/src/utils/errorBadges.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* 错误请求/用量明细共享的徽章配色与列映射。
3+
* 配色统一 bg-X-100/text-X-800 体系,与 UsageTable 一致。
4+
*/
5+
6+
import type { UsageRequestType } from '@/types'
7+
8+
export type UsageRequestKind = UsageRequestType
9+
10+
/** 状态码徽章:≥500 红、429 紫、≥400 琥珀、其余灰 */
11+
export function statusCodeBadgeClass(code: number): string {
12+
if (code >= 500) return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
13+
if (code === 429) return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'
14+
if (code >= 400) return 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200'
15+
return 'bg-gray-100 text-gray-800 dark:bg-dark-700 dark:text-gray-200'
16+
}
17+
18+
/** 请求类型徽章配色(cyber 红、ws 紫、stream 蓝、sync 灰、未知琥珀) */
19+
export function requestTypeBadgeClass(kind: UsageRequestKind): string {
20+
if (kind === 'cyber') return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
21+
if (kind === 'ws_v2') return 'bg-violet-100 text-violet-800 dark:bg-violet-900 dark:text-violet-200'
22+
if (kind === 'stream') return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
23+
if (kind === 'sync') return 'bg-gray-100 text-gray-800 dark:bg-dark-700 dark:text-gray-200'
24+
return 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200'
25+
}
26+
27+
/** 请求类型 i18n 键(展示方自行 t()) */
28+
export function requestTypeLabelKey(kind: UsageRequestKind): string {
29+
if (kind === 'cyber') return 'usage.cyber'
30+
if (kind === 'ws_v2') return 'usage.ws'
31+
if (kind === 'stream') return 'usage.stream'
32+
if (kind === 'sync') return 'usage.sync'
33+
return 'usage.unknown'
34+
}
35+
36+
/**
37+
* 数字 request_type(1 同步/2 流式/3 WS)→ kind;
38+
* 缺失时按 stream 布尔回退,两者都缺返回 null(展示为 -)。
39+
*/
40+
export function numericRequestTypeKind(
41+
requestType?: number | null,
42+
stream?: boolean | null
43+
): UsageRequestKind | null {
44+
const rt = requestType ?? (stream == null ? 0 : stream ? 2 : 1)
45+
if (rt === 3) return 'ws_v2'
46+
if (rt === 2) return 'stream'
47+
if (rt === 1) return 'sync'
48+
return null
49+
}
50+
51+
/** 错误表列 key → 后端 sort_by(status 列实际按 status_code 排序) */
52+
export function mapErrorSortKey(key: string): string {
53+
return key === 'status' ? 'status_code' : key
54+
}

frontend/src/views/admin/ops/components/OpsErrorLogTable.vue

Lines changed: 6 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,7 @@
11
<template>
22
<div class="flex h-full min-h-0 flex-col">
33
<div class="card flex min-h-0 flex-1 flex-col overflow-hidden">
4-
<div
5-
v-if="currentPageIps.length > 0"
6-
class="flex flex-shrink-0 items-center justify-end gap-2 border-b border-gray-200 px-4 py-2 dark:border-dark-700"
7-
>
8-
<span v-if="pendingIpCount > 0" class="text-xs text-gray-500 dark:text-gray-400">
9-
{{ t('usage.ipGeo.pending', { count: pendingIpCount }) }}
10-
</span>
11-
<button
12-
type="button"
13-
class="inline-flex items-center gap-1 rounded px-2 py-1 text-xs font-medium text-primary-600 transition-colors hover:bg-primary-50 disabled:cursor-not-allowed disabled:opacity-50 dark:text-primary-400 dark:hover:bg-primary-900/30"
14-
:disabled="ipGeoBatchLoading || pendingIpCount === 0"
15-
@click="handleBatchFetchIpGeo"
16-
>
17-
{{ ipGeoBatchLoading ? t('usage.ipGeo.batchFetching') : t('usage.ipGeo.batchFetch') }}
18-
</button>
19-
</div>
4+
<IpGeoBatchToolbar :ips="rows.map((r) => r.client_ip)" @failed="emit('ipGeoBatchFailed')" />
205

216
<DataTable
227
:columns="columns"
@@ -194,17 +179,18 @@
194179
</template>
195180

196181
<script setup lang="ts">
197-
import { computed, ref } from 'vue'
182+
import { computed } from 'vue'
198183
import { useI18n } from 'vue-i18n'
199184
import DataTable from '@/components/common/DataTable.vue'
200185
import EmptyState from '@/components/common/EmptyState.vue'
201186
import Pagination from '@/components/common/Pagination.vue'
202187
import IpGeoCell from '@/components/common/IpGeoCell.vue'
188+
import IpGeoBatchToolbar from '@/components/common/IpGeoBatchToolbar.vue'
203189
import type { OpsErrorLog } from '@/api/admin/ops'
204190
import type { Column } from '@/components/common/types'
205191
import { getSeverityClass, formatDateTime } from '../utils/opsFormatters'
206-
import { fetchBatch, getEntry } from '@/utils/ipGeoLookup'
207192
import { mapErrorCategory } from '@/utils/errorCategory'
193+
import { mapErrorSortKey, statusCodeBadgeClass } from '@/utils/errorBadges'
208194
209195
const { t } = useI18n()
210196
@@ -313,40 +299,11 @@ interface Emits {
313299
const props = defineProps<Props>()
314300
const emit = defineEmits<Emits>()
315301
316-
// 列 key → 后端 sort_by:status 列实际按 status_code 排序
317302
function onSort(key: string, order: 'asc' | 'desc') {
318-
emit('sort', key === 'status' ? 'status_code' : key, order)
319-
}
320-
321-
const ipGeoBatchLoading = ref(false)
322-
323-
const currentPageIps = computed(() =>
324-
Array.from(new Set(props.rows.map((row) => row.client_ip).filter((ip): ip is string => Boolean(ip))))
325-
)
326-
327-
const pendingIpCount = computed(() =>
328-
currentPageIps.value.filter((ip) => {
329-
const status = getEntry(ip).status
330-
return status === 'idle' || status === 'error'
331-
}).length
332-
)
333-
334-
const handleBatchFetchIpGeo = async () => {
335-
ipGeoBatchLoading.value = true
336-
try {
337-
const ok = await fetchBatch(currentPageIps.value)
338-
if (!ok) emit('ipGeoBatchFailed')
339-
} finally {
340-
ipGeoBatchLoading.value = false
341-
}
303+
emit('sort', mapErrorSortKey(key), order)
342304
}
343305
344-
function getStatusClass(code: number): string {
345-
if (code >= 500) return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
346-
if (code === 429) return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'
347-
if (code >= 400) return 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200'
348-
return 'bg-gray-100 text-gray-800 dark:bg-dark-700 dark:text-gray-200'
349-
}
306+
const getStatusClass = statusCodeBadgeClass
350307
351308
function formatSmartMessage(msg: string): string {
352309
if (!msg) return ''

0 commit comments

Comments
 (0)