|
|
@@ -1,5 +1,5 @@
|
|
|
<script setup lang="ts" name="aidopS8WatchRuleConfig">
|
|
|
-import { computed, onMounted, reactive, ref } from 'vue';
|
|
|
+import { computed, onActivated, onDeactivated, onMounted, onUnmounted, reactive, ref } from 'vue';
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
|
import AidopDemoShell from '../../components/AidopDemoShell.vue';
|
|
|
import {
|
|
|
@@ -84,6 +84,42 @@ const outOfRangeForm = reactive<OutOfRangeForm>({
|
|
|
const enabledForm = ref(true);
|
|
|
const rawParamsJson = ref<string>('');
|
|
|
|
|
|
+// S8-SCHED-FRONTEND-1:调度参数编辑表单。
|
|
|
+interface ScheduleForm {
|
|
|
+ pollIntervalSeconds: number;
|
|
|
+ triggerCountRequired: number;
|
|
|
+ recoverCountRequired: number;
|
|
|
+}
|
|
|
+const scheduleForm = reactive<ScheduleForm>({
|
|
|
+ pollIntervalSeconds: 300,
|
|
|
+ triggerCountRequired: 1,
|
|
|
+ recoverCountRequired: 1,
|
|
|
+});
|
|
|
+const POLL_INTERVAL_PRESETS = [
|
|
|
+ { label: '1 分钟', value: 60 },
|
|
|
+ { label: '5 分钟', value: 300 },
|
|
|
+ { label: '15 分钟', value: 900 },
|
|
|
+ { label: '30 分钟', value: 1800 },
|
|
|
+ { label: '1 小时', value: 3600 },
|
|
|
+ { label: '4 小时', value: 14400 },
|
|
|
+];
|
|
|
+
|
|
|
+// S8-SCHED-FRONTEND-1:30s 自动刷新;onMounted/onActivated 启动;onUnmounted/onDeactivated 清理。
|
|
|
+const REFRESH_INTERVAL_MS = 30000;
|
|
|
+let refreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
+function startAutoRefresh() {
|
|
|
+ stopAutoRefresh();
|
|
|
+ refreshTimer = setInterval(() => {
|
|
|
+ if (document.visibilityState === 'visible' && !drawerOpen.value) loadRows();
|
|
|
+ }, REFRESH_INTERVAL_MS);
|
|
|
+}
|
|
|
+function stopAutoRefresh() {
|
|
|
+ if (refreshTimer != null) {
|
|
|
+ clearInterval(refreshTimer);
|
|
|
+ refreshTimer = null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
const ruleTypeOf = (row: S8WatchRuleConfigRow | null): S8WatchRuleType | '' =>
|
|
|
(row?.ruleType ?? '').toUpperCase();
|
|
|
|
|
|
@@ -153,6 +189,9 @@ function loadFormFromRow(row: S8WatchRuleConfigRow) {
|
|
|
resetForms();
|
|
|
enabledForm.value = !!row.enabled;
|
|
|
rawParamsJson.value = row.paramsJson ?? '';
|
|
|
+ scheduleForm.pollIntervalSeconds = clampInt(row.pollIntervalSeconds ?? 300, 60, 86400, 300);
|
|
|
+ scheduleForm.triggerCountRequired = clampInt(row.triggerCountRequired ?? 1, 1, 10, 1);
|
|
|
+ scheduleForm.recoverCountRequired = clampInt(row.recoverCountRequired ?? 1, 1, 10, 1);
|
|
|
|
|
|
if (!row.paramsJson) return;
|
|
|
let parsed: Record<string, any>;
|
|
|
@@ -268,11 +307,17 @@ async function save() {
|
|
|
if (!editingRow.value) return;
|
|
|
saving.value = true;
|
|
|
try {
|
|
|
- const payload = {
|
|
|
+ // 调度参数走专用 schedule 端点;params_json / rule_type / expression 不会被触碰。
|
|
|
+ await s8ConfigApi.watchRules.updateSchedule(editingRow.value.id, {
|
|
|
+ pollIntervalSeconds: clampInt(scheduleForm.pollIntervalSeconds, 60, 86400, 300),
|
|
|
+ triggerCountRequired: clampInt(scheduleForm.triggerCountRequired, 1, 10, 1),
|
|
|
+ recoverCountRequired: clampInt(scheduleForm.recoverCountRequired, 1, 10, 1),
|
|
|
+ });
|
|
|
+ // params_json + enabled 仍走原 params 端点,保持模板化保存语义。
|
|
|
+ await s8ConfigApi.watchRules.updateParams(editingRow.value.id, {
|
|
|
paramsJson: buildPayloadParamsJson(),
|
|
|
enabled: enabledForm.value,
|
|
|
- };
|
|
|
- await s8ConfigApi.watchRules.updateParams(editingRow.value.id, payload);
|
|
|
+ });
|
|
|
ElMessage.success('保存成功');
|
|
|
closeDrawer();
|
|
|
await loadRows();
|
|
|
@@ -283,6 +328,77 @@ async function save() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// S8-SCHED-FRONTEND-1:行操作。run-now / pause / resume 调用专用端点,操作完成后立即刷新列表。
|
|
|
+async function runNow(row: S8WatchRuleConfigRow) {
|
|
|
+ try {
|
|
|
+ await s8ConfigApi.watchRules.runNow(row.id);
|
|
|
+ ElMessage.success('已排队,最长 1 分钟内执行');
|
|
|
+ await loadRows();
|
|
|
+ } catch (e: any) {
|
|
|
+ ElMessage.error(e?.response?.data?.message ?? e?.message ?? '排队失败');
|
|
|
+ }
|
|
|
+}
|
|
|
+async function pauseRule(row: S8WatchRuleConfigRow) {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(`确认暂停规则 ${row.ruleCode}?暂停后下一轮调度不再执行此规则。`, '提示', { type: 'warning' });
|
|
|
+ } catch { return; }
|
|
|
+ try {
|
|
|
+ await s8ConfigApi.watchRules.pause(row.id);
|
|
|
+ ElMessage.success('已暂停');
|
|
|
+ await loadRows();
|
|
|
+ } catch (e: any) {
|
|
|
+ ElMessage.error(e?.response?.data?.message ?? e?.message ?? '暂停失败');
|
|
|
+ }
|
|
|
+}
|
|
|
+async function resumeRule(row: S8WatchRuleConfigRow) {
|
|
|
+ try {
|
|
|
+ await s8ConfigApi.watchRules.resume(row.id);
|
|
|
+ ElMessage.success('已恢复,并将在下一轮调度中执行');
|
|
|
+ await loadRows();
|
|
|
+ } catch (e: any) {
|
|
|
+ ElMessage.error(e?.response?.data?.message ?? e?.message ?? '恢复失败');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function clampInt(raw: number | string | null | undefined, min: number, max: number, fallback: number) {
|
|
|
+ const n = Number(raw);
|
|
|
+ if (!Number.isFinite(n)) return fallback;
|
|
|
+ const i = Math.floor(n);
|
|
|
+ if (i < min) return min;
|
|
|
+ if (i > max) return max;
|
|
|
+ return i;
|
|
|
+}
|
|
|
+
|
|
|
+function isPaused(row: S8WatchRuleConfigRow) {
|
|
|
+ if (!row.pausedUntil) return false;
|
|
|
+ return new Date(row.pausedUntil).getTime() > Date.now();
|
|
|
+}
|
|
|
+function isRunning(row: S8WatchRuleConfigRow) {
|
|
|
+ if (!row.lockUntil) return false;
|
|
|
+ return new Date(row.lockUntil).getTime() > Date.now();
|
|
|
+}
|
|
|
+function pollIntervalLabel(seconds: number | null | undefined) {
|
|
|
+ if (!seconds || seconds <= 0) return '—';
|
|
|
+ const preset = POLL_INTERVAL_PRESETS.find((p) => p.value === seconds);
|
|
|
+ if (preset) return preset.label;
|
|
|
+ if (seconds < 60) return `${seconds} 秒`;
|
|
|
+ if (seconds < 3600) return `${Math.round(seconds / 60)} 分钟`;
|
|
|
+ return `${Math.round(seconds / 3600)} 小时`;
|
|
|
+}
|
|
|
+function lastStatusTagType(status: string | null | undefined): 'success' | 'danger' | 'info' | '' {
|
|
|
+ switch ((status ?? '').toUpperCase()) {
|
|
|
+ case 'SUCCESS': return 'success';
|
|
|
+ case 'FAILED': return 'danger';
|
|
|
+ case 'SKIPPED': return 'info';
|
|
|
+ default: return '';
|
|
|
+ }
|
|
|
+}
|
|
|
+function nextRunDisplay(row: S8WatchRuleConfigRow) {
|
|
|
+ if (isPaused(row)) return '已暂停';
|
|
|
+ if (isRunning(row)) return '执行中';
|
|
|
+ return row.nextRunAt ?? '—';
|
|
|
+}
|
|
|
+
|
|
|
async function toggleEnabled(row: S8WatchRuleConfigRow) {
|
|
|
const next = !row.enabled;
|
|
|
const action = next ? '启用' : '停用';
|
|
|
@@ -304,37 +420,89 @@ async function toggleEnabled(row: S8WatchRuleConfigRow) {
|
|
|
onMounted(() => {
|
|
|
loadRows();
|
|
|
loadExceptionTypes();
|
|
|
+ startAutoRefresh();
|
|
|
});
|
|
|
+onUnmounted(() => stopAutoRefresh());
|
|
|
+onActivated(() => startAutoRefresh());
|
|
|
+onDeactivated(() => stopAutoRefresh());
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
- <AidopDemoShell title="监视规则配置" subtitle="S8 / 配置 / 监视规则(仅模板化参数编辑,不开放 SQL 编辑)">
|
|
|
+ <AidopDemoShell title="监视规则配置" subtitle="S8 / 配置 / 监视规则(参数模板化编辑 + 调度运行态;列表 30 秒自动刷新)">
|
|
|
+ <div class="rule-toolbar">
|
|
|
+ <el-button size="small" @click="loadRows" :loading="loading">手动刷新</el-button>
|
|
|
+ <span class="rule-toolbar__hint">每 30 秒自动刷新;编辑抽屉打开期间暂停刷新</span>
|
|
|
+ </div>
|
|
|
<el-table v-loading="loading" :data="rows" border stripe size="small">
|
|
|
- <el-table-column prop="ruleCode" label="规则编码" width="170" />
|
|
|
- <el-table-column label="规则类型" width="120">
|
|
|
+ <el-table-column prop="ruleCode" label="规则编码" width="180" />
|
|
|
+ <el-table-column label="规则类型" width="100">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag :type="row.ruleType ? 'success' : 'info'" size="small">
|
|
|
{{ ruleTypeLabel(row.ruleType) }}
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column prop="sceneCode" label="场景" width="160" />
|
|
|
- <el-table-column prop="sourceObjectType" label="源对象" width="120">
|
|
|
- <template #default="{ row }">{{ row.sourceObjectType ?? row.watchObjectType ?? '—' }}</template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="启用" width="90">
|
|
|
+ <el-table-column label="启用" width="80">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag :type="row.enabled ? 'success' : 'info'" size="small">
|
|
|
{{ row.enabled ? '启用' : '停用' }}
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column prop="updatedAt" label="最近更新" width="170">
|
|
|
- <template #default="{ row }">{{ row.updatedAt ?? row.createdAt ?? '—' }}</template>
|
|
|
+ <el-table-column label="轮询间隔" width="100">
|
|
|
+ <template #default="{ row }">{{ pollIntervalLabel(row.pollIntervalSeconds) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="触发阈值" width="110">
|
|
|
+ <template #default="{ row }">连续 {{ row.triggerCountRequired ?? 1 }} 次命中</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="恢复阈值" width="120">
|
|
|
+ <template #default="{ row }">连续 {{ row.recoverCountRequired ?? 1 }} 次未命中</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="上次执行" width="160">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tooltip v-if="row.lastRunAt" :content="String(row.lastRunAt)" placement="top">
|
|
|
+ <span>{{ row.lastRunAt }}</span>
|
|
|
+ </el-tooltip>
|
|
|
+ <span v-else>—</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="下次执行" width="160">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag v-if="isPaused(row)" type="warning" size="small">已暂停</el-tag>
|
|
|
+ <el-tag v-else-if="isRunning(row)" type="info" size="small">执行中</el-tag>
|
|
|
+ <span v-else>{{ nextRunDisplay(row) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="上次状态" width="100">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag v-if="row.lastStatus" :type="lastStatusTagType(row.lastStatus)" size="small">{{ row.lastStatus }}</el-tag>
|
|
|
+ <span v-else>—</span>
|
|
|
+ </template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="操作" width="180" fixed="right">
|
|
|
+ <el-table-column label="失败次数" width="90">
|
|
|
<template #default="{ row }">
|
|
|
- <el-button size="small" type="primary" link @click="openEdit(row)">编辑参数</el-button>
|
|
|
+ <el-badge v-if="(row.consecutiveFailureCount ?? 0) > 0" :value="row.consecutiveFailureCount" type="danger" />
|
|
|
+ <span v-else>0</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="耗时" width="80">
|
|
|
+ <template #default="{ row }">{{ row.lastDurationMs != null ? `${row.lastDurationMs}ms` : '—' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="错误摘要" min-width="180" show-overflow-tooltip>
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tooltip v-if="row.lastError" :content="String(row.lastError)" placement="top">
|
|
|
+ <span class="rule-error">{{ row.lastError }}</span>
|
|
|
+ </el-tooltip>
|
|
|
+ <span v-else>—</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="320" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button size="small" type="primary" link @click="openEdit(row)">编辑</el-button>
|
|
|
+ <el-button size="small" type="success" link :disabled="!row.enabled || isPaused(row) || isRunning(row)" @click="runNow(row)">立即执行</el-button>
|
|
|
+ <el-button v-if="!isPaused(row)" size="small" type="warning" link @click="pauseRule(row)">暂停</el-button>
|
|
|
+ <el-button v-else size="small" type="success" link @click="resumeRule(row)">恢复</el-button>
|
|
|
+ <el-button v-if="!isPaused(row) && (row.consecutiveFailureCount ?? 0) > 0" size="small" type="success" link @click="resumeRule(row)">重置失败</el-button>
|
|
|
<el-button size="small" :type="row.enabled ? 'warning' : 'success'" link @click="toggleEnabled(row)">
|
|
|
{{ row.enabled ? '停用' : '启用' }}
|
|
|
</el-button>
|
|
|
@@ -448,6 +616,23 @@ onMounted(() => {
|
|
|
<pre class="rule-raw">{{ rawParamsJson || '(空)' }}</pre>
|
|
|
</el-alert>
|
|
|
|
|
|
+ <el-divider content-position="left">调度设置</el-divider>
|
|
|
+ <el-form label-position="top" size="small">
|
|
|
+ <el-form-item label="轮询间隔(poll_interval_seconds,60–86400 秒)">
|
|
|
+ <el-select v-model="scheduleForm.pollIntervalSeconds" allow-create filterable placeholder="选择或输入秒数">
|
|
|
+ <el-option v-for="o in POLL_INTERVAL_PRESETS" :key="o.value" :label="o.label" :value="o.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="触发阈值(trigger_count_required,1–10)">
|
|
|
+ <el-input-number v-model="scheduleForm.triggerCountRequired" :min="1" :max="10" />
|
|
|
+ <span class="rule-hint">连续命中 N 次后才建单</span>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="恢复阈值(recover_count_required,1–10)">
|
|
|
+ <el-input-number v-model="scheduleForm.recoverCountRequired" :min="1" :max="10" />
|
|
|
+ <span class="rule-hint">连续未命中 N 次后才标 recovered</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
<el-divider content-position="left">启用</el-divider>
|
|
|
<el-form label-position="top" size="small">
|
|
|
<el-form-item label="启用状态">
|
|
|
@@ -494,4 +679,23 @@ onMounted(() => {
|
|
|
border-top: 1px solid var(--el-border-color-light);
|
|
|
text-align: right;
|
|
|
}
|
|
|
+.rule-toolbar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+.rule-toolbar__hint {
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
+}
|
|
|
+.rule-hint {
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
+ margin-left: 8px;
|
|
|
+}
|
|
|
+.rule-error {
|
|
|
+ color: var(--el-color-danger);
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
</style>
|