feat(ops): 添加自定义时间范围选择功能
功能特性: - 在时间段选择器中增加"自定义"选项 - 点击后弹出对话框,支持选择任意时间范围 - 使用 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 调用支持自定义时间范围 国际化: - 添加"自定义"、"开始时间"、"结束时间"翻译
This commit is contained in:
@@ -2117,7 +2117,12 @@ export default {
|
|||||||
'6h': '近6小时',
|
'6h': '近6小时',
|
||||||
'24h': '近24小时',
|
'24h': '近24小时',
|
||||||
'7d': '近7天',
|
'7d': '近7天',
|
||||||
'30d': '近30天'
|
'30d': '近30天',
|
||||||
|
custom: '自定义'
|
||||||
|
},
|
||||||
|
customTimeRange: {
|
||||||
|
startTime: '开始时间',
|
||||||
|
endTime: '结束时间'
|
||||||
},
|
},
|
||||||
fullscreen: {
|
fullscreen: {
|
||||||
enter: '进入全屏'
|
enter: '进入全屏'
|
||||||
|
|||||||
@@ -23,10 +23,13 @@
|
|||||||
:auto-refresh-enabled="autoRefreshEnabled"
|
:auto-refresh-enabled="autoRefreshEnabled"
|
||||||
:auto-refresh-countdown="autoRefreshCountdown"
|
:auto-refresh-countdown="autoRefreshCountdown"
|
||||||
:fullscreen="isFullscreen"
|
:fullscreen="isFullscreen"
|
||||||
|
:custom-start-time="customStartTime"
|
||||||
|
:custom-end-time="customEndTime"
|
||||||
@update:time-range="onTimeRangeChange"
|
@update:time-range="onTimeRangeChange"
|
||||||
@update:platform="onPlatformChange"
|
@update:platform="onPlatformChange"
|
||||||
@update:group="onGroupChange"
|
@update:group="onGroupChange"
|
||||||
@update:query-mode="onQueryModeChange"
|
@update:query-mode="onQueryModeChange"
|
||||||
|
@update:custom-time-range="onCustomTimeRangeChange"
|
||||||
@refresh="fetchData"
|
@refresh="fetchData"
|
||||||
@open-request-details="handleOpenRequestDetails"
|
@open-request-details="handleOpenRequestDetails"
|
||||||
@open-error-details="openErrorDetails"
|
@open-error-details="openErrorDetails"
|
||||||
@@ -148,8 +151,8 @@ const { t } = useI18n()
|
|||||||
|
|
||||||
const opsEnabled = computed(() => adminSettingsStore.opsMonitoringEnabled)
|
const opsEnabled = computed(() => adminSettingsStore.opsMonitoringEnabled)
|
||||||
|
|
||||||
type TimeRange = '5m' | '30m' | '1h' | '6h' | '24h'
|
type TimeRange = '5m' | '30m' | '1h' | '6h' | '24h' | 'custom'
|
||||||
const allowedTimeRanges = new Set<TimeRange>(['5m', '30m', '1h', '6h', '24h'])
|
const allowedTimeRanges = new Set<TimeRange>(['5m', '30m', '1h', '6h', '24h', 'custom'])
|
||||||
|
|
||||||
type QueryMode = 'auto' | 'raw' | 'preagg'
|
type QueryMode = 'auto' | 'raw' | 'preagg'
|
||||||
const allowedQueryModes = new Set<QueryMode>(['auto', 'raw', 'preagg'])
|
const allowedQueryModes = new Set<QueryMode>(['auto', 'raw', 'preagg'])
|
||||||
@@ -163,6 +166,8 @@ const timeRange = ref<TimeRange>('1h')
|
|||||||
const platform = ref<string>('')
|
const platform = ref<string>('')
|
||||||
const groupId = ref<number | null>(null)
|
const groupId = ref<number | null>(null)
|
||||||
const queryMode = ref<QueryMode>('auto')
|
const queryMode = ref<QueryMode>('auto')
|
||||||
|
const customStartTime = ref<string | null>(null)
|
||||||
|
const customEndTime = ref<string | null>(null)
|
||||||
|
|
||||||
const QUERY_KEYS = {
|
const QUERY_KEYS = {
|
||||||
timeRange: 'tr',
|
timeRange: 'tr',
|
||||||
@@ -420,6 +425,11 @@ function onTimeRangeChange(v: string | number | boolean | null) {
|
|||||||
timeRange.value = v as TimeRange
|
timeRange.value = v as TimeRange
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onCustomTimeRangeChange(startTime: string, endTime: string) {
|
||||||
|
customStartTime.value = startTime
|
||||||
|
customEndTime.value = endTime
|
||||||
|
}
|
||||||
|
|
||||||
function onSettingsSaved() {
|
function onSettingsSaved() {
|
||||||
loadThresholds()
|
loadThresholds()
|
||||||
fetchData()
|
fetchData()
|
||||||
@@ -458,18 +468,25 @@ function openError(id: number) {
|
|||||||
showErrorModal.value = true
|
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) {
|
async function refreshOverviewWithCancel(fetchSeq: number, signal: AbortSignal) {
|
||||||
if (!opsEnabled.value) return
|
if (!opsEnabled.value) return
|
||||||
try {
|
try {
|
||||||
const data = await opsAPI.getDashboardOverview(
|
const data = await opsAPI.getDashboardOverview(buildApiParams(), { signal })
|
||||||
{
|
|
||||||
time_range: timeRange.value,
|
|
||||||
platform: platform.value || undefined,
|
|
||||||
group_id: groupId.value ?? undefined,
|
|
||||||
mode: queryMode.value
|
|
||||||
},
|
|
||||||
{ signal }
|
|
||||||
)
|
|
||||||
if (fetchSeq !== dashboardFetchSeq) return
|
if (fetchSeq !== dashboardFetchSeq) return
|
||||||
overview.value = data
|
overview.value = data
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -483,15 +500,7 @@ async function refreshThroughputTrendWithCancel(fetchSeq: number, signal: AbortS
|
|||||||
if (!opsEnabled.value) return
|
if (!opsEnabled.value) return
|
||||||
loadingTrend.value = true
|
loadingTrend.value = true
|
||||||
try {
|
try {
|
||||||
const data = await opsAPI.getThroughputTrend(
|
const data = await opsAPI.getThroughputTrend(buildApiParams(), { signal })
|
||||||
{
|
|
||||||
time_range: timeRange.value,
|
|
||||||
platform: platform.value || undefined,
|
|
||||||
group_id: groupId.value ?? undefined,
|
|
||||||
mode: queryMode.value
|
|
||||||
},
|
|
||||||
{ signal }
|
|
||||||
)
|
|
||||||
if (fetchSeq !== dashboardFetchSeq) return
|
if (fetchSeq !== dashboardFetchSeq) return
|
||||||
throughputTrend.value = data
|
throughputTrend.value = data
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -509,15 +518,7 @@ async function refreshLatencyHistogramWithCancel(fetchSeq: number, signal: Abort
|
|||||||
if (!opsEnabled.value) return
|
if (!opsEnabled.value) return
|
||||||
loadingLatency.value = true
|
loadingLatency.value = true
|
||||||
try {
|
try {
|
||||||
const data = await opsAPI.getLatencyHistogram(
|
const data = await opsAPI.getLatencyHistogram(buildApiParams(), { signal })
|
||||||
{
|
|
||||||
time_range: timeRange.value,
|
|
||||||
platform: platform.value || undefined,
|
|
||||||
group_id: groupId.value ?? undefined,
|
|
||||||
mode: queryMode.value
|
|
||||||
},
|
|
||||||
{ signal }
|
|
||||||
)
|
|
||||||
if (fetchSeq !== dashboardFetchSeq) return
|
if (fetchSeq !== dashboardFetchSeq) return
|
||||||
latencyHistogram.value = data
|
latencyHistogram.value = data
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -535,15 +536,7 @@ async function refreshErrorTrendWithCancel(fetchSeq: number, signal: AbortSignal
|
|||||||
if (!opsEnabled.value) return
|
if (!opsEnabled.value) return
|
||||||
loadingErrorTrend.value = true
|
loadingErrorTrend.value = true
|
||||||
try {
|
try {
|
||||||
const data = await opsAPI.getErrorTrend(
|
const data = await opsAPI.getErrorTrend(buildApiParams(), { signal })
|
||||||
{
|
|
||||||
time_range: timeRange.value,
|
|
||||||
platform: platform.value || undefined,
|
|
||||||
group_id: groupId.value ?? undefined,
|
|
||||||
mode: queryMode.value
|
|
||||||
},
|
|
||||||
{ signal }
|
|
||||||
)
|
|
||||||
if (fetchSeq !== dashboardFetchSeq) return
|
if (fetchSeq !== dashboardFetchSeq) return
|
||||||
errorTrend.value = data
|
errorTrend.value = data
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -561,15 +554,7 @@ async function refreshErrorDistributionWithCancel(fetchSeq: number, signal: Abor
|
|||||||
if (!opsEnabled.value) return
|
if (!opsEnabled.value) return
|
||||||
loadingErrorDistribution.value = true
|
loadingErrorDistribution.value = true
|
||||||
try {
|
try {
|
||||||
const data = await opsAPI.getErrorDistribution(
|
const data = await opsAPI.getErrorDistribution(buildApiParams(), { signal })
|
||||||
{
|
|
||||||
time_range: timeRange.value,
|
|
||||||
platform: platform.value || undefined,
|
|
||||||
group_id: groupId.value ?? undefined,
|
|
||||||
mode: queryMode.value
|
|
||||||
},
|
|
||||||
{ signal }
|
|
||||||
)
|
|
||||||
if (fetchSeq !== dashboardFetchSeq) return
|
if (fetchSeq !== dashboardFetchSeq) return
|
||||||
errorDistribution.value = data
|
errorDistribution.value = data
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ interface Props {
|
|||||||
autoRefreshEnabled?: boolean
|
autoRefreshEnabled?: boolean
|
||||||
autoRefreshCountdown?: number
|
autoRefreshCountdown?: number
|
||||||
fullscreen?: boolean
|
fullscreen?: boolean
|
||||||
|
customStartTime?: string | null
|
||||||
|
customEndTime?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
@@ -33,6 +35,7 @@ interface Emits {
|
|||||||
(e: 'update:group', value: number | null): void
|
(e: 'update:group', value: number | null): void
|
||||||
(e: 'update:timeRange', value: string): void
|
(e: 'update:timeRange', value: string): void
|
||||||
(e: 'update:queryMode', value: string): void
|
(e: 'update:queryMode', value: string): void
|
||||||
|
(e: 'update:customTimeRange', startTime: string, endTime: string): void
|
||||||
(e: 'refresh'): void
|
(e: 'refresh'): void
|
||||||
(e: 'openRequestDetails', preset?: OpsRequestDetailsPreset): void
|
(e: 'openRequestDetails', preset?: OpsRequestDetailsPreset): void
|
||||||
(e: 'openErrorDetails', kind: 'request' | 'upstream'): void
|
(e: 'openErrorDetails', kind: 'request' | 'upstream'): void
|
||||||
@@ -85,6 +88,23 @@ watch(
|
|||||||
|
|
||||||
// --- Filters ---
|
// --- 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<Array<{ id: number; name: string; platform: string }>>([])
|
const groups = ref<Array<{ id: number; name: string; platform: string }>>([])
|
||||||
|
|
||||||
const platformOptions = computed(() => [
|
const platformOptions = computed(() => [
|
||||||
@@ -100,7 +120,13 @@ const timeRangeOptions = computed(() => [
|
|||||||
{ value: '30m', label: t('admin.ops.timeRange.30m') },
|
{ value: '30m', label: t('admin.ops.timeRange.30m') },
|
||||||
{ value: '1h', label: t('admin.ops.timeRange.1h') },
|
{ value: '1h', label: t('admin.ops.timeRange.1h') },
|
||||||
{ value: '6h', label: t('admin.ops.timeRange.6h') },
|
{ 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(() => [
|
const queryModeOptions = computed(() => [
|
||||||
@@ -149,7 +175,32 @@ function handleGroupChange(val: string | number | boolean | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleTimeRangeChange(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) {
|
function handleQueryModeChange(val: string | number | boolean | null) {
|
||||||
@@ -164,11 +215,6 @@ function openErrorDetails(kind: 'request' | 'upstream') {
|
|||||||
emit('openErrorDetails', kind)
|
emit('openErrorDetails', kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedAtLabel = computed(() => {
|
|
||||||
if (!props.lastUpdated) return t('common.unknown')
|
|
||||||
return props.lastUpdated.toLocaleTimeString()
|
|
||||||
})
|
|
||||||
|
|
||||||
// --- Threshold checking helpers ---
|
// --- Threshold checking helpers ---
|
||||||
type ThresholdLevel = 'normal' | 'warning' | 'critical'
|
type ThresholdLevel = 'normal' | 'warning' | 'critical'
|
||||||
|
|
||||||
@@ -829,25 +875,11 @@ function handleToolbarRefresh() {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span>{{ t('common.refresh') }}: {{ updatedAtLabel }}</span>
|
<span>{{ 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') }}</span>
|
||||||
|
|
||||||
<template v-if="props.autoRefreshEnabled && props.autoRefreshCountdown !== undefined">
|
<template v-if="props.autoRefreshEnabled && props.autoRefreshCountdown !== undefined">
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span class="flex items-center gap-1">
|
<span>剩余 {{ props.autoRefreshCountdown }}s</span>
|
||||||
<svg class="h-3 w-3 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>
|
|
||||||
<path class="opacity-75" 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"></path>
|
|
||||||
</svg>
|
|
||||||
<span>{{ t('admin.ops.settings.autoRefreshCountdown', { seconds: props.autoRefreshCountdown }) }}</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-if="systemMetrics">
|
|
||||||
<span>·</span>
|
|
||||||
<span>
|
|
||||||
{{ t('admin.ops.collectedAt') }} {{ formatTimeShort(systemMetrics.created_at) }}
|
|
||||||
({{ t('admin.ops.window') }} {{ systemMetrics.window_minutes }}m)
|
|
||||||
</span>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1534,5 +1566,47 @@ function handleToolbarRefresh() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
||||||
|
<!-- Custom Time Range Dialog -->
|
||||||
|
<BaseDialog :show="showCustomTimeRangeDialog" :title="t('admin.ops.timeRange.custom')" width="narrow" @close="handleCustomTimeRangeCancel">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
{{ t('admin.ops.customTimeRange.startTime') }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="customStartTimeInput"
|
||||||
|
type="datetime-local"
|
||||||
|
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-dark-600 dark:bg-dark-800 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
{{ t('admin.ops.customTimeRange.endTime') }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="customEndTimeInput"
|
||||||
|
type="datetime-local"
|
||||||
|
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-dark-600 dark:bg-dark-800 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-lg bg-gray-100 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-200 dark:bg-dark-700 dark:text-gray-300 dark:hover:bg-dark-600"
|
||||||
|
@click="handleCustomTimeRangeCancel"
|
||||||
|
>
|
||||||
|
{{ t('common.cancel') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-lg bg-blue-500 px-4 py-2 text-sm font-medium text-white hover:bg-blue-600"
|
||||||
|
@click="handleCustomTimeRangeConfirm"
|
||||||
|
>
|
||||||
|
{{ t('common.confirm') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseDialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user