From 930e9ee55c283ce09a570f663026fc90afd48cf3 Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Thu, 15 Jan 2026 19:50:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(ops):=20=E6=B7=BB=E5=8A=A0=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E6=97=B6=E9=97=B4=E8=8C=83=E5=9B=B4=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 功能特性: - 在时间段选择器中增加"自定义"选项 - 点击后弹出对话框,支持选择任意时间范围 - 使用 HTML5 datetime-local 输入框,体验友好 - 自定义时显示格式化的时间范围标签(MM-DD HH:mm ~ MM-DD HH:mm) - 默认初始化为最近1小时 技术实现: - 扩展 TimeRange 类型支持 'custom' - 添加 customStartTime 和 customEndTime 状态管理 - 创建 buildApiParams 辅助函数统一处理 API 参数 - 当选择自定义时,使用 start_time 和 end_time 参数替代 time_range - 更新所有相关 API 调用支持自定义时间范围 国际化: - 添加"自定义"、"开始时间"、"结束时间"翻译 --- frontend/src/i18n/locales/zh.ts | 7 +- frontend/src/views/admin/ops/OpsDashboard.vue | 79 +++++------- .../ops/components/OpsDashboardHeader.vue | 120 ++++++++++++++---- 3 files changed, 135 insertions(+), 71 deletions(-) diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index daf39939..30f8df51 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -2117,7 +2117,12 @@ export default { '6h': '近6小时', '24h': '近24小时', '7d': '近7天', - '30d': '近30天' + '30d': '近30天', + custom: '自定义' + }, + customTimeRange: { + startTime: '开始时间', + endTime: '结束时间' }, fullscreen: { enter: '进入全屏' diff --git a/frontend/src/views/admin/ops/OpsDashboard.vue b/frontend/src/views/admin/ops/OpsDashboard.vue index d33f0f64..d8a31931 100644 --- a/frontend/src/views/admin/ops/OpsDashboard.vue +++ b/frontend/src/views/admin/ops/OpsDashboard.vue @@ -23,10 +23,13 @@ :auto-refresh-enabled="autoRefreshEnabled" :auto-refresh-countdown="autoRefreshCountdown" :fullscreen="isFullscreen" + :custom-start-time="customStartTime" + :custom-end-time="customEndTime" @update:time-range="onTimeRangeChange" @update:platform="onPlatformChange" @update:group="onGroupChange" @update:query-mode="onQueryModeChange" + @update:custom-time-range="onCustomTimeRangeChange" @refresh="fetchData" @open-request-details="handleOpenRequestDetails" @open-error-details="openErrorDetails" @@ -148,8 +151,8 @@ const { t } = useI18n() const opsEnabled = computed(() => adminSettingsStore.opsMonitoringEnabled) -type TimeRange = '5m' | '30m' | '1h' | '6h' | '24h' -const allowedTimeRanges = new Set(['5m', '30m', '1h', '6h', '24h']) +type TimeRange = '5m' | '30m' | '1h' | '6h' | '24h' | 'custom' +const allowedTimeRanges = new Set(['5m', '30m', '1h', '6h', '24h', 'custom']) type QueryMode = 'auto' | 'raw' | 'preagg' const allowedQueryModes = new Set(['auto', 'raw', 'preagg']) @@ -163,6 +166,8 @@ const timeRange = ref('1h') const platform = ref('') const groupId = ref(null) const queryMode = ref('auto') +const customStartTime = ref(null) +const customEndTime = ref(null) const QUERY_KEYS = { timeRange: 'tr', @@ -420,6 +425,11 @@ function onTimeRangeChange(v: string | number | boolean | null) { timeRange.value = v as TimeRange } +function onCustomTimeRangeChange(startTime: string, endTime: string) { + customStartTime.value = startTime + customEndTime.value = endTime +} + function onSettingsSaved() { loadThresholds() fetchData() @@ -458,18 +468,25 @@ function openError(id: number) { showErrorModal.value = true } +function buildApiParams() { + const params: any = { + platform: platform.value || undefined, + group_id: groupId.value ?? undefined, + mode: queryMode.value + } + if (timeRange.value === 'custom' && customStartTime.value && customEndTime.value) { + params.start_time = customStartTime.value + params.end_time = customEndTime.value + } else { + params.time_range = timeRange.value + } + return params +} + async function refreshOverviewWithCancel(fetchSeq: number, signal: AbortSignal) { if (!opsEnabled.value) return try { - const data = await opsAPI.getDashboardOverview( - { - time_range: timeRange.value, - platform: platform.value || undefined, - group_id: groupId.value ?? undefined, - mode: queryMode.value - }, - { signal } - ) + const data = await opsAPI.getDashboardOverview(buildApiParams(), { signal }) if (fetchSeq !== dashboardFetchSeq) return overview.value = data } catch (err: any) { @@ -483,15 +500,7 @@ async function refreshThroughputTrendWithCancel(fetchSeq: number, signal: AbortS if (!opsEnabled.value) return loadingTrend.value = true try { - const data = await opsAPI.getThroughputTrend( - { - time_range: timeRange.value, - platform: platform.value || undefined, - group_id: groupId.value ?? undefined, - mode: queryMode.value - }, - { signal } - ) + const data = await opsAPI.getThroughputTrend(buildApiParams(), { signal }) if (fetchSeq !== dashboardFetchSeq) return throughputTrend.value = data } catch (err: any) { @@ -509,15 +518,7 @@ async function refreshLatencyHistogramWithCancel(fetchSeq: number, signal: Abort if (!opsEnabled.value) return loadingLatency.value = true try { - const data = await opsAPI.getLatencyHistogram( - { - time_range: timeRange.value, - platform: platform.value || undefined, - group_id: groupId.value ?? undefined, - mode: queryMode.value - }, - { signal } - ) + const data = await opsAPI.getLatencyHistogram(buildApiParams(), { signal }) if (fetchSeq !== dashboardFetchSeq) return latencyHistogram.value = data } catch (err: any) { @@ -535,15 +536,7 @@ async function refreshErrorTrendWithCancel(fetchSeq: number, signal: AbortSignal if (!opsEnabled.value) return loadingErrorTrend.value = true try { - const data = await opsAPI.getErrorTrend( - { - time_range: timeRange.value, - platform: platform.value || undefined, - group_id: groupId.value ?? undefined, - mode: queryMode.value - }, - { signal } - ) + const data = await opsAPI.getErrorTrend(buildApiParams(), { signal }) if (fetchSeq !== dashboardFetchSeq) return errorTrend.value = data } catch (err: any) { @@ -561,15 +554,7 @@ async function refreshErrorDistributionWithCancel(fetchSeq: number, signal: Abor if (!opsEnabled.value) return loadingErrorDistribution.value = true try { - const data = await opsAPI.getErrorDistribution( - { - time_range: timeRange.value, - platform: platform.value || undefined, - group_id: groupId.value ?? undefined, - mode: queryMode.value - }, - { signal } - ) + const data = await opsAPI.getErrorDistribution(buildApiParams(), { signal }) if (fetchSeq !== dashboardFetchSeq) return errorDistribution.value = data } catch (err: any) { diff --git a/frontend/src/views/admin/ops/components/OpsDashboardHeader.vue b/frontend/src/views/admin/ops/components/OpsDashboardHeader.vue index 8e868bba..b36055e0 100644 --- a/frontend/src/views/admin/ops/components/OpsDashboardHeader.vue +++ b/frontend/src/views/admin/ops/components/OpsDashboardHeader.vue @@ -26,6 +26,8 @@ interface Props { autoRefreshEnabled?: boolean autoRefreshCountdown?: number fullscreen?: boolean + customStartTime?: string | null + customEndTime?: string | null } interface Emits { @@ -33,6 +35,7 @@ interface Emits { (e: 'update:group', value: number | null): void (e: 'update:timeRange', value: string): void (e: 'update:queryMode', value: string): void + (e: 'update:customTimeRange', startTime: string, endTime: string): void (e: 'refresh'): void (e: 'openRequestDetails', preset?: OpsRequestDetailsPreset): void (e: 'openErrorDetails', kind: 'request' | 'upstream'): void @@ -85,6 +88,23 @@ watch( // --- Filters --- +const showCustomTimeRangeDialog = ref(false) +const customStartTimeInput = ref('') +const customEndTimeInput = ref('') + +function formatCustomTimeRangeLabel(startTime: string, endTime: string): string { + const start = new Date(startTime) + const end = new Date(endTime) + const formatDate = (d: Date) => { + const month = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + const hour = String(d.getHours()).padStart(2, '0') + const minute = String(d.getMinutes()).padStart(2, '0') + return `${month}-${day} ${hour}:${minute}` + } + return `${formatDate(start)} ~ ${formatDate(end)}` +} + const groups = ref>([]) const platformOptions = computed(() => [ @@ -100,7 +120,13 @@ const timeRangeOptions = computed(() => [ { value: '30m', label: t('admin.ops.timeRange.30m') }, { value: '1h', label: t('admin.ops.timeRange.1h') }, { value: '6h', label: t('admin.ops.timeRange.6h') }, - { value: '24h', label: t('admin.ops.timeRange.24h') } + { value: '24h', label: t('admin.ops.timeRange.24h') }, + { + value: 'custom', + label: props.timeRange === 'custom' && props.customStartTime && props.customEndTime + ? `${t('admin.ops.timeRange.custom')} (${formatCustomTimeRangeLabel(props.customStartTime, props.customEndTime)})` + : t('admin.ops.timeRange.custom') + } ]) const queryModeOptions = computed(() => [ @@ -149,7 +175,32 @@ function handleGroupChange(val: string | number | boolean | null) { } function handleTimeRangeChange(val: string | number | boolean | null) { - emit('update:timeRange', String(val || '1h')) + const newValue = String(val || '1h') + if (newValue === 'custom') { + // 初始化为最近1小时 + const now = new Date() + const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000) + customStartTimeInput.value = oneHourAgo.toISOString().slice(0, 16) + customEndTimeInput.value = now.toISOString().slice(0, 16) + showCustomTimeRangeDialog.value = true + } else { + emit('update:timeRange', newValue) + } +} + +function handleCustomTimeRangeConfirm() { + if (!customStartTimeInput.value || !customEndTimeInput.value) return + const startTime = new Date(customStartTimeInput.value).toISOString() + const endTime = new Date(customEndTimeInput.value).toISOString() + emit('update:timeRange', 'custom') + emit('update:customTimeRange', startTime, endTime) + showCustomTimeRangeDialog.value = false +} + +function handleCustomTimeRangeCancel() { + showCustomTimeRangeDialog.value = false + // 如果当前不是 custom,不需要做任何事 + // 如果当前是 custom,保持不变 } function handleQueryModeChange(val: string | number | boolean | null) { @@ -164,11 +215,6 @@ function openErrorDetails(kind: 'request' | 'upstream') { emit('openErrorDetails', kind) } -const updatedAtLabel = computed(() => { - if (!props.lastUpdated) return t('common.unknown') - return props.lastUpdated.toLocaleTimeString() -}) - // --- Threshold checking helpers --- type ThresholdLevel = 'normal' | 'warning' | 'critical' @@ -829,25 +875,11 @@ function handleToolbarRefresh() { · - {{ t('common.refresh') }}: {{ updatedAtLabel }} + {{ t('common.refresh') }}: {{ props.lastUpdated ? props.lastUpdated.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }).replace(/\//g, '-') : t('common.unknown') }} - - @@ -1534,5 +1566,47 @@ function handleToolbarRefresh() { + + + +
+
+ + +
+
+ + +
+
+ + +
+
+