feat(ops): 优化警报规则和设置的成功提示信息
- 添加警报规则保存成功提示:"警报规则保存成功" - 添加警报规则删除成功提示:"警报规则删除成功" - 添加运维监控设置保存成功提示:"运维监控设置保存成功" - 替换通用的"操作成功"提示为具体的业务提示 - 失败时显示后端返回的详细错误信息 相关文件: - frontend/src/i18n/locales/zh.ts - frontend/src/views/admin/ops/components/OpsAlertRulesCard.vue - frontend/src/views/admin/ops/components/OpsSettingsDialog.vue
This commit is contained in:
@@ -154,6 +154,7 @@ export default {
|
|||||||
saving: '保存中...',
|
saving: '保存中...',
|
||||||
selectedCount: '(已选 {count} 个)',
|
selectedCount: '(已选 {count} 个)',
|
||||||
refresh: '刷新',
|
refresh: '刷新',
|
||||||
|
settings: '设置',
|
||||||
notAvailable: '不可用',
|
notAvailable: '不可用',
|
||||||
now: '现在',
|
now: '现在',
|
||||||
unknown: '未知',
|
unknown: '未知',
|
||||||
@@ -2205,13 +2206,16 @@ export default {
|
|||||||
loading: '加载中...',
|
loading: '加载中...',
|
||||||
empty: '暂无告警规则',
|
empty: '暂无告警规则',
|
||||||
loadFailed: '加载告警规则失败',
|
loadFailed: '加载告警规则失败',
|
||||||
|
saveSuccess: '警报规则保存成功',
|
||||||
saveFailed: '保存告警规则失败',
|
saveFailed: '保存告警规则失败',
|
||||||
|
deleteSuccess: '警报规则删除成功',
|
||||||
deleteFailed: '删除告警规则失败',
|
deleteFailed: '删除告警规则失败',
|
||||||
create: '新建规则',
|
create: '新建规则',
|
||||||
createTitle: '新建告警规则',
|
createTitle: '新建告警规则',
|
||||||
editTitle: '编辑告警规则',
|
editTitle: '编辑告警规则',
|
||||||
deleteConfirmTitle: '确认删除该规则?',
|
deleteConfirmTitle: '确认删除该规则?',
|
||||||
deleteConfirmMessage: '将删除该规则及其关联的告警事件,是否继续?',
|
deleteConfirmMessage: '将删除该规则及其关联的告警事件,是否继续?',
|
||||||
|
manage: '预警规则',
|
||||||
metrics: {
|
metrics: {
|
||||||
successRate: '成功率 (%)',
|
successRate: '成功率 (%)',
|
||||||
errorRate: '错误率 (%)',
|
errorRate: '错误率 (%)',
|
||||||
@@ -2350,6 +2354,42 @@ export default {
|
|||||||
accountHealthThresholdRange: '账号健康错误率阈值必须在 0 到 100 之间'
|
accountHealthThresholdRange: '账号健康错误率阈值必须在 0 到 100 之间'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
settings: {
|
||||||
|
title: '运维监控设置',
|
||||||
|
loadFailed: '加载设置失败',
|
||||||
|
saveSuccess: '运维监控设置保存成功',
|
||||||
|
saveFailed: '保存设置失败',
|
||||||
|
dataCollection: '数据采集',
|
||||||
|
evaluationInterval: '评估间隔(秒)',
|
||||||
|
evaluationIntervalHint: '检测任务的执行频率,建议保持默认',
|
||||||
|
alertConfig: '预警配置',
|
||||||
|
enableAlert: '开启预警',
|
||||||
|
alertRecipients: '预警接收邮箱',
|
||||||
|
emailPlaceholder: '输入邮箱地址',
|
||||||
|
recipientsHint: '若为空,系统将使用第一个管理员邮箱作为默认收件人',
|
||||||
|
minSeverity: '最低级别',
|
||||||
|
reportConfig: '评估报告配置',
|
||||||
|
enableReport: '开启评估报告',
|
||||||
|
reportRecipients: '评估报告接收邮箱',
|
||||||
|
dailySummary: '每日摘要',
|
||||||
|
weeklySummary: '每周摘要',
|
||||||
|
advancedSettings: '高级设置',
|
||||||
|
dataRetention: '数据保留策略',
|
||||||
|
enableCleanup: '启用数据清理',
|
||||||
|
cleanupSchedule: '清理计划(Cron)',
|
||||||
|
cleanupScheduleHint: '例如:0 2 * * * 表示每天凌晨2点',
|
||||||
|
errorLogRetentionDays: '错误日志保留天数',
|
||||||
|
minuteMetricsRetentionDays: '分钟指标保留天数',
|
||||||
|
hourlyMetricsRetentionDays: '小时指标保留天数',
|
||||||
|
retentionDaysHint: '建议保留7-90天,过长会占用存储空间',
|
||||||
|
aggregation: '预聚合任务',
|
||||||
|
enableAggregation: '启用预聚合任务',
|
||||||
|
aggregationHint: '预聚合可提升长时间窗口查询性能',
|
||||||
|
validation: {
|
||||||
|
title: '请先修正以下问题',
|
||||||
|
retentionDaysRange: '保留天数必须在1-365天之间'
|
||||||
|
}
|
||||||
|
},
|
||||||
concurrency: {
|
concurrency: {
|
||||||
title: '并发 / 排队',
|
title: '并发 / 排队',
|
||||||
byPlatform: '按平台',
|
byPlatform: '按平台',
|
||||||
@@ -2383,10 +2423,12 @@ export default {
|
|||||||
accountError: '异常'
|
accountError: '异常'
|
||||||
},
|
},
|
||||||
tooltips: {
|
tooltips: {
|
||||||
|
totalRequests: '当前时间窗口内的总请求数和Token消耗量。',
|
||||||
throughputTrend: '当前窗口内的请求/QPS 与 token/TPS 趋势。',
|
throughputTrend: '当前窗口内的请求/QPS 与 token/TPS 趋势。',
|
||||||
latencyHistogram: '成功请求的延迟分布(毫秒)。',
|
latencyHistogram: '成功请求的延迟分布(毫秒)。',
|
||||||
errorTrend: '错误趋势(SLA 口径排除业务限制;上游错误率排除 429/529)。',
|
errorTrend: '错误趋势(SLA 口径排除业务限制;上游错误率排除 429/529)。',
|
||||||
errorDistribution: '按状态码统计的错误分布。',
|
errorDistribution: '按状态码统计的错误分布。',
|
||||||
|
upstreamErrors: '上游服务返回的错误,包括API提供商的错误响应(排除429/529限流错误)。',
|
||||||
goroutines:
|
goroutines:
|
||||||
'Go 运行时的协程数量(轻量级线程)。没有绝对“安全值”,建议以历史基线为准。经验参考:<2000 常见;2000-8000 需关注;>8000 且伴随队列/延迟上升时,优先排查阻塞/泄漏。',
|
'Go 运行时的协程数量(轻量级线程)。没有绝对“安全值”,建议以历史基线为准。经验参考:<2000 常见;2000-8000 需关注;>8000 且伴随队列/延迟上升时,优先排查阻塞/泄漏。',
|
||||||
cpu: 'CPU 使用率,显示系统处理器的负载情况。',
|
cpu: 'CPU 使用率,显示系统处理器的负载情况。',
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ async function save() {
|
|||||||
draft.value = null
|
draft.value = null
|
||||||
editingId.value = null
|
editingId.value = null
|
||||||
await load()
|
await load()
|
||||||
appStore.showSuccess(t('common.success'))
|
appStore.showSuccess(t('admin.ops.alertRules.saveSuccess'))
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('[OpsAlertRulesCard] Failed to save rule', err)
|
console.error('[OpsAlertRulesCard] Failed to save rule', err)
|
||||||
appStore.showError(err?.response?.data?.detail || t('admin.ops.alertRules.saveFailed'))
|
appStore.showError(err?.response?.data?.detail || t('admin.ops.alertRules.saveFailed'))
|
||||||
@@ -160,7 +160,7 @@ async function confirmDelete() {
|
|||||||
showDeleteConfirm.value = false
|
showDeleteConfirm.value = false
|
||||||
pendingDelete.value = null
|
pendingDelete.value = null
|
||||||
await load()
|
await load()
|
||||||
appStore.showSuccess(t('common.success'))
|
appStore.showSuccess(t('admin.ops.alertRules.deleteSuccess'))
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('[OpsAlertRulesCard] Failed to delete rule', err)
|
console.error('[OpsAlertRulesCard] Failed to delete rule', err)
|
||||||
appStore.showError(err?.response?.data?.detail || t('admin.ops.alertRules.deleteFailed'))
|
appStore.showError(err?.response?.data?.detail || t('admin.ops.alertRules.deleteFailed'))
|
||||||
|
|||||||
395
frontend/src/views/admin/ops/components/OpsSettingsDialog.vue
Normal file
395
frontend/src/views/admin/ops/components/OpsSettingsDialog.vue
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { opsAPI } from '@/api/admin/ops'
|
||||||
|
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||||
|
import Select from '@/components/common/Select.vue'
|
||||||
|
import Toggle from '@/components/common/Toggle.vue'
|
||||||
|
import type { OpsAlertRuntimeSettings, EmailNotificationConfig, AlertSeverity, OpsAdvancedSettings } from '../types'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
show: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: []
|
||||||
|
saved: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
// 运行时设置
|
||||||
|
const runtimeSettings = ref<OpsAlertRuntimeSettings | null>(null)
|
||||||
|
// 邮件通知配置
|
||||||
|
const emailConfig = ref<EmailNotificationConfig | null>(null)
|
||||||
|
// 高级设置
|
||||||
|
const advancedSettings = ref<OpsAdvancedSettings | null>(null)
|
||||||
|
|
||||||
|
// 加载所有配置
|
||||||
|
async function loadAllSettings() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const [runtime, email, advanced] = await Promise.all([
|
||||||
|
opsAPI.getAlertRuntimeSettings(),
|
||||||
|
opsAPI.getEmailNotificationConfig(),
|
||||||
|
opsAPI.getAdvancedSettings()
|
||||||
|
])
|
||||||
|
runtimeSettings.value = runtime
|
||||||
|
emailConfig.value = email
|
||||||
|
advancedSettings.value = advanced
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[OpsSettingsDialog] Failed to load settings', err)
|
||||||
|
appStore.showError(err?.response?.data?.detail || t('admin.ops.settings.loadFailed'))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听弹窗打开
|
||||||
|
watch(() => props.show, (show) => {
|
||||||
|
if (show) {
|
||||||
|
loadAllSettings()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 邮件输入
|
||||||
|
const alertRecipientInput = ref('')
|
||||||
|
const reportRecipientInput = ref('')
|
||||||
|
|
||||||
|
// 严重级别选项
|
||||||
|
const severityOptions: Array<{ value: AlertSeverity | ''; label: string }> = [
|
||||||
|
{ value: '', label: t('admin.ops.email.minSeverityAll') },
|
||||||
|
{ value: 'critical', label: t('common.critical') },
|
||||||
|
{ value: 'warning', label: t('common.warning') },
|
||||||
|
{ value: 'info', label: t('common.info') }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 验证邮箱
|
||||||
|
function isValidEmailAddress(email: string): boolean {
|
||||||
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加收件人
|
||||||
|
function addRecipient(target: 'alert' | 'report') {
|
||||||
|
if (!emailConfig.value) return
|
||||||
|
const raw = (target === 'alert' ? alertRecipientInput.value : reportRecipientInput.value).trim()
|
||||||
|
if (!raw) return
|
||||||
|
|
||||||
|
if (!isValidEmailAddress(raw)) {
|
||||||
|
appStore.showError(t('common.invalidEmail'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = raw.toLowerCase()
|
||||||
|
const list = target === 'alert' ? emailConfig.value.alert.recipients : emailConfig.value.report.recipients
|
||||||
|
if (!list.includes(normalized)) {
|
||||||
|
list.push(normalized)
|
||||||
|
}
|
||||||
|
if (target === 'alert') alertRecipientInput.value = ''
|
||||||
|
else reportRecipientInput.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除收件人
|
||||||
|
function removeRecipient(target: 'alert' | 'report', email: string) {
|
||||||
|
if (!emailConfig.value) return
|
||||||
|
const list = target === 'alert' ? emailConfig.value.alert.recipients : emailConfig.value.report.recipients
|
||||||
|
const idx = list.indexOf(email)
|
||||||
|
if (idx >= 0) list.splice(idx, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证
|
||||||
|
const validation = computed(() => {
|
||||||
|
const errors: string[] = []
|
||||||
|
|
||||||
|
// 验证运行时设置
|
||||||
|
if (runtimeSettings.value) {
|
||||||
|
const evalSeconds = runtimeSettings.value.evaluation_interval_seconds
|
||||||
|
if (!Number.isFinite(evalSeconds) || evalSeconds < 1 || evalSeconds > 86400) {
|
||||||
|
errors.push(t('admin.ops.runtime.validation.evalIntervalRange'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证邮件配置
|
||||||
|
if (emailConfig.value) {
|
||||||
|
if (emailConfig.value.alert.enabled && emailConfig.value.alert.recipients.length === 0) {
|
||||||
|
errors.push(t('admin.ops.email.validation.alertRecipientsRequired'))
|
||||||
|
}
|
||||||
|
if (emailConfig.value.report.enabled && emailConfig.value.report.recipients.length === 0) {
|
||||||
|
errors.push(t('admin.ops.email.validation.reportRecipientsRequired'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证高级设置
|
||||||
|
if (advancedSettings.value) {
|
||||||
|
const { error_log_retention_days, minute_metrics_retention_days, hourly_metrics_retention_days } = advancedSettings.value.data_retention
|
||||||
|
if (error_log_retention_days < 1 || error_log_retention_days > 365) {
|
||||||
|
errors.push(t('admin.ops.settings.validation.retentionDaysRange'))
|
||||||
|
}
|
||||||
|
if (minute_metrics_retention_days < 1 || minute_metrics_retention_days > 365) {
|
||||||
|
errors.push(t('admin.ops.settings.validation.retentionDaysRange'))
|
||||||
|
}
|
||||||
|
if (hourly_metrics_retention_days < 1 || hourly_metrics_retention_days > 365) {
|
||||||
|
errors.push(t('admin.ops.settings.validation.retentionDaysRange'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: errors.length === 0, errors }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 保存所有配置
|
||||||
|
async function saveAllSettings() {
|
||||||
|
if (!validation.value.valid) {
|
||||||
|
appStore.showError(validation.value.errors[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
runtimeSettings.value ? opsAPI.updateAlertRuntimeSettings(runtimeSettings.value) : Promise.resolve(),
|
||||||
|
emailConfig.value ? opsAPI.updateEmailNotificationConfig(emailConfig.value) : Promise.resolve(),
|
||||||
|
advancedSettings.value ? opsAPI.updateAdvancedSettings(advancedSettings.value) : Promise.resolve()
|
||||||
|
])
|
||||||
|
appStore.showSuccess(t('admin.ops.settings.saveSuccess'))
|
||||||
|
emit('saved')
|
||||||
|
emit('close')
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[OpsSettingsDialog] Failed to save settings', err)
|
||||||
|
appStore.showError(err?.response?.data?.detail || t('admin.ops.settings.saveFailed'))
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BaseDialog :show="show" :title="t('admin.ops.settings.title')" width="extra-wide" @close="emit('close')">
|
||||||
|
<div v-if="loading" class="py-10 text-center text-sm text-gray-500">
|
||||||
|
{{ t('common.loading') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="runtimeSettings && emailConfig && advancedSettings" class="space-y-6">
|
||||||
|
<!-- 验证错误 -->
|
||||||
|
<div v-if="!validation.valid" class="rounded-lg border border-amber-200 bg-amber-50 p-3 text-xs text-amber-800 dark:border-amber-900/50 dark:bg-amber-900/20 dark:text-amber-200">
|
||||||
|
<div class="font-bold">{{ t('admin.ops.settings.validation.title') }}</div>
|
||||||
|
<ul class="mt-1 list-disc space-y-1 pl-4">
|
||||||
|
<li v-for="msg in validation.errors" :key="msg">{{ msg }}</li>
|
||||||
|
</ul>
|
||||||
|
</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.dataCollection') }}</h4>
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('admin.ops.settings.evaluationInterval') }}</label>
|
||||||
|
<input
|
||||||
|
v-model.number="runtimeSettings.evaluation_interval_seconds"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="86400"
|
||||||
|
class="input"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-gray-500">{{ t('admin.ops.settings.evaluationIntervalHint') }}</p>
|
||||||
|
</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.alertConfig') }}</h4>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<label class="font-medium text-gray-900 dark:text-white">{{ t('admin.ops.settings.enableAlert') }}</label>
|
||||||
|
</div>
|
||||||
|
<Toggle v-model="emailConfig.alert.enabled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="emailConfig.alert.enabled">
|
||||||
|
<label class="input-label">{{ t('admin.ops.settings.alertRecipients') }}</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input
|
||||||
|
v-model="alertRecipientInput"
|
||||||
|
type="email"
|
||||||
|
class="input"
|
||||||
|
:placeholder="t('admin.ops.settings.emailPlaceholder')"
|
||||||
|
@keydown.enter.prevent="addRecipient('alert')"
|
||||||
|
/>
|
||||||
|
<button class="btn btn-secondary whitespace-nowrap" type="button" @click="addRecipient('alert')">
|
||||||
|
{{ t('common.add') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 flex flex-wrap gap-2">
|
||||||
|
<span
|
||||||
|
v-for="email in emailConfig.alert.recipients"
|
||||||
|
:key="email"
|
||||||
|
class="inline-flex items-center gap-2 rounded-full bg-blue-100 px-3 py-1 text-xs font-medium text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
|
||||||
|
>
|
||||||
|
{{ email }}
|
||||||
|
<button type="button" class="text-blue-700/80 hover:text-blue-900" @click="removeRecipient('alert', email)">×</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.ops.settings.recipientsHint') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="emailConfig.alert.enabled">
|
||||||
|
<label class="input-label">{{ t('admin.ops.settings.minSeverity') }}</label>
|
||||||
|
<Select v-model="emailConfig.alert.min_severity" :options="severityOptions" />
|
||||||
|
</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.reportConfig') }}</h4>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<label class="font-medium text-gray-900 dark:text-white">{{ t('admin.ops.settings.enableReport') }}</label>
|
||||||
|
</div>
|
||||||
|
<Toggle v-model="emailConfig.report.enabled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="emailConfig.report.enabled">
|
||||||
|
<label class="input-label">{{ t('admin.ops.settings.reportRecipients') }}</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input
|
||||||
|
v-model="reportRecipientInput"
|
||||||
|
type="email"
|
||||||
|
class="input"
|
||||||
|
:placeholder="t('admin.ops.settings.emailPlaceholder')"
|
||||||
|
@keydown.enter.prevent="addRecipient('report')"
|
||||||
|
/>
|
||||||
|
<button class="btn btn-secondary whitespace-nowrap" type="button" @click="addRecipient('report')">
|
||||||
|
{{ t('common.add') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 flex flex-wrap gap-2">
|
||||||
|
<span
|
||||||
|
v-for="email in emailConfig.report.recipients"
|
||||||
|
:key="email"
|
||||||
|
class="inline-flex items-center gap-2 rounded-full bg-blue-100 px-3 py-1 text-xs font-medium text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
|
||||||
|
>
|
||||||
|
{{ email }}
|
||||||
|
<button type="button" class="text-blue-700/80 hover:text-blue-900" @click="removeRecipient('report', email)">×</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.ops.settings.recipientsHint') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="emailConfig.report.enabled" class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('admin.ops.settings.dailySummary') }}</label>
|
||||||
|
<Toggle v-model="emailConfig.report.daily_summary_enabled" />
|
||||||
|
</div>
|
||||||
|
<div v-if="emailConfig.report.daily_summary_enabled">
|
||||||
|
<input v-model="emailConfig.report.daily_summary_schedule" type="text" class="input" placeholder="0 9 * * *" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('admin.ops.settings.weeklySummary') }}</label>
|
||||||
|
<Toggle v-model="emailConfig.report.weekly_summary_enabled" />
|
||||||
|
</div>
|
||||||
|
<div v-if="emailConfig.report.weekly_summary_enabled">
|
||||||
|
<input v-model="emailConfig.report.weekly_summary_schedule" type="text" class="input" placeholder="0 9 * * 1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 高级设置 -->
|
||||||
|
<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">
|
||||||
|
{{ t('admin.ops.settings.advancedSettings') }}
|
||||||
|
</summary>
|
||||||
|
<div class="space-y-4 px-4 pb-4">
|
||||||
|
<!-- 数据保留策略 -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<h5 class="text-xs font-semibold text-gray-700 dark:text-gray-300">{{ t('admin.ops.settings.dataRetention') }}</h5>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('admin.ops.settings.enableCleanup') }}</label>
|
||||||
|
<Toggle v-model="advancedSettings.data_retention.cleanup_enabled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="advancedSettings.data_retention.cleanup_enabled">
|
||||||
|
<label class="input-label">{{ t('admin.ops.settings.cleanupSchedule') }}</label>
|
||||||
|
<input
|
||||||
|
v-model="advancedSettings.data_retention.cleanup_schedule"
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
placeholder="0 2 * * *"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-gray-500">{{ t('admin.ops.settings.cleanupScheduleHint') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('admin.ops.settings.errorLogRetentionDays') }}</label>
|
||||||
|
<input
|
||||||
|
v-model.number="advancedSettings.data_retention.error_log_retention_days"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="365"
|
||||||
|
class="input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('admin.ops.settings.minuteMetricsRetentionDays') }}</label>
|
||||||
|
<input
|
||||||
|
v-model.number="advancedSettings.data_retention.minute_metrics_retention_days"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="365"
|
||||||
|
class="input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('admin.ops.settings.hourlyMetricsRetentionDays') }}</label>
|
||||||
|
<input
|
||||||
|
v-model.number="advancedSettings.data_retention.hourly_metrics_retention_days"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="365"
|
||||||
|
class="input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-500">{{ t('admin.ops.settings.retentionDaysHint') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 预聚合任务 -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<h5 class="text-xs font-semibold text-gray-700 dark:text-gray-300">{{ t('admin.ops.settings.aggregation') }}</h5>
|
||||||
|
|
||||||
|
<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.enableAggregation') }}</label>
|
||||||
|
<p class="mt-1 text-xs text-gray-500">{{ t('admin.ops.settings.aggregationHint') }}</p>
|
||||||
|
</div>
|
||||||
|
<Toggle v-model="advancedSettings.aggregation.aggregation_enabled" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button class="btn btn-secondary" @click="emit('close')">{{ t('common.cancel') }}</button>
|
||||||
|
<button class="btn btn-primary" :disabled="saving || !validation.valid" @click="saveAllSettings">
|
||||||
|
{{ saving ? t('common.saving') : t('common.save') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</BaseDialog>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user