|
|
@@ -1,7 +1,8 @@
|
|
|
<script setup lang="ts" name="aidopS8WatchRuleWizardDialog">
|
|
|
-// CONFIG-RULE-WIZARD-BIZ-CONDITION-MVP-1:业务条件向导(5 步)
|
|
|
-// 1) 报警机制 2) 业务维度 3) 监控对象/指标 4) 判定标准 5) 基础配置 + 保存
|
|
|
-// MANUAL_REPORT 不创建 watch_rule,跳转主动提报页。
|
|
|
+// CONFIG-WIZARD-DRAFT-FRONTEND-1:5 步业务条件向导 + 后端草稿接入。
|
|
|
+// 1) 报警机制 2) 业务维度 3) 监控对象/指标 4) 判定标准 5) 基础配置
|
|
|
+// 非 MANUAL_REPORT:先 saveDraft 落库,再 generate-rule 生成正式 watch_rule。
|
|
|
+// MANUAL_REPORT:跳转主动提报,不写草稿、不写规则。
|
|
|
import { computed, reactive, ref, watch } from 'vue';
|
|
|
import { useRouter } from 'vue-router';
|
|
|
import { ElMessage } from 'element-plus';
|
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
type S8DimensionNode,
|
|
|
type S8ExceptionTypeConfigRow,
|
|
|
type S8WatchRuleCreatePayload,
|
|
|
+ type S8WizardDraftDetail,
|
|
|
} from '../../api/s8ConfigApi';
|
|
|
|
|
|
interface DataSourceLike {
|
|
|
@@ -27,11 +29,14 @@ const props = defineProps<{
|
|
|
dataSources: DataSourceLike[];
|
|
|
tenantId?: number;
|
|
|
factoryId?: number;
|
|
|
+ draftData?: S8WizardDraftDetail | null;
|
|
|
}>();
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
(e: 'update:modelValue', value: boolean): void;
|
|
|
(e: 'saved'): void;
|
|
|
+ (e: 'draftSaved'): void;
|
|
|
+ (e: 'generated'): void;
|
|
|
}>();
|
|
|
|
|
|
const router = useRouter();
|
|
|
@@ -43,6 +48,7 @@ const visible = computed({
|
|
|
|
|
|
const step = ref(0);
|
|
|
const saving = ref(false);
|
|
|
+const savingDraft = ref(false);
|
|
|
|
|
|
const MECHANISM_OPTIONS = [
|
|
|
{ value: 'DATE', label: '日期类', desc: '适用于交期、到期、超期、SLA 等时间判断' },
|
|
|
@@ -56,7 +62,6 @@ const SEVERITY_OPTIONS = [
|
|
|
{ value: 'SERIOUS', label: '严重' },
|
|
|
];
|
|
|
|
|
|
-// CONFIG-RULE-WIZARD-BIZ-CONDITION-MVP-1:业务对象 + 监控指标常量字典(前端 MVP,不依赖后端字典)
|
|
|
interface BizMetric {
|
|
|
metricCode: string;
|
|
|
metricLabel: string;
|
|
|
@@ -131,11 +136,10 @@ const form = reactive({
|
|
|
mechanism: '' as '' | 'DATE' | 'VALUE_RANGE' | 'RATIO' | 'MANUAL_REPORT',
|
|
|
stageCode: '',
|
|
|
orderFlowCode: '',
|
|
|
- objectIndex: -1, // BUSINESS_MONITOR_OPTIONS index
|
|
|
+ objectIndex: -1,
|
|
|
metricCode: '',
|
|
|
exceptionTypeCode: '',
|
|
|
severity: 'FOLLOW',
|
|
|
- // 判定标准
|
|
|
graceMinutes: 0,
|
|
|
completedStates: 'CLOSED,COMPLETED,DONE',
|
|
|
lowerBound: null as number | null,
|
|
|
@@ -144,16 +148,50 @@ const form = reactive({
|
|
|
toleranceAbs: 0,
|
|
|
toleranceRatioPct: 0,
|
|
|
targetRatio: 95,
|
|
|
- // 基础
|
|
|
ruleCode: '',
|
|
|
pollIntervalSeconds: 300,
|
|
|
triggerCountRequired: 1,
|
|
|
recoverCountRequired: 1,
|
|
|
});
|
|
|
|
|
|
+// CONFIG-WIZARD-DRAFT-FRONTEND-1:草稿身份 + 用户可改的草稿名。
|
|
|
+const currentDraftId = ref<number | string | null>(null);
|
|
|
+const currentDraftCode = ref<string>('');
|
|
|
+const draftName = ref<string>('');
|
|
|
+// 字典演进兜底:草稿回填后若按 objectType+objectLabel 找不到字典项,标记禁用 generate。
|
|
|
+const dictionaryMissing = ref(false);
|
|
|
+const fallbackObjectLabel = ref('');
|
|
|
+const fallbackMetricLabel = ref('');
|
|
|
+
|
|
|
+function pad2(n: number) { return n < 10 ? `0${n}` : `${n}`; }
|
|
|
+function nowTimestampCompact() {
|
|
|
+ const d = new Date();
|
|
|
+ return `${d.getFullYear()}${pad2(d.getMonth() + 1)}${pad2(d.getDate())}${pad2(d.getHours())}${pad2(d.getMinutes())}${pad2(d.getSeconds())}`;
|
|
|
+}
|
|
|
+function nowMmDdHhMm() {
|
|
|
+ const d = new Date();
|
|
|
+ return `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
|
|
+}
|
|
|
+function generateDraftCode(): string {
|
|
|
+ const rand = Math.random().toString(36).slice(-3).toUpperCase();
|
|
|
+ return `DRAFT-S8-${nowTimestampCompact()}-${rand}`;
|
|
|
+}
|
|
|
+function mechanismChinese(m: string): string {
|
|
|
+ return MECHANISM_OPTIONS.find((o) => o.value === m)?.label ?? '';
|
|
|
+}
|
|
|
+function generateDraftName(): string {
|
|
|
+ const mechLabel = mechanismChinese(form.mechanism);
|
|
|
+ const stageLabel = props.stageNodes.find((n) => n.nodeCode === form.stageCode)?.nodeName ?? '';
|
|
|
+ const metric = selectedMetric.value?.metricLabel ?? '';
|
|
|
+ if (mechLabel && stageLabel && metric) return `${mechLabel}·${stageLabel}·${metric}`;
|
|
|
+ if (mechLabel) return `${mechLabel}·S8 配置草稿`;
|
|
|
+ return `S8 配置草稿 ${nowMmDdHhMm()}`;
|
|
|
+}
|
|
|
+
|
|
|
function resetForm() {
|
|
|
step.value = 0;
|
|
|
saving.value = false;
|
|
|
+ savingDraft.value = false;
|
|
|
form.mechanism = '';
|
|
|
form.stageCode = '';
|
|
|
form.orderFlowCode = '';
|
|
|
@@ -173,13 +211,25 @@ function resetForm() {
|
|
|
form.pollIntervalSeconds = 300;
|
|
|
form.triggerCountRequired = 1;
|
|
|
form.recoverCountRequired = 1;
|
|
|
+ currentDraftId.value = null;
|
|
|
+ currentDraftCode.value = generateDraftCode();
|
|
|
+ draftName.value = '';
|
|
|
+ dictionaryMissing.value = false;
|
|
|
+ fallbackObjectLabel.value = '';
|
|
|
+ fallbackMetricLabel.value = '';
|
|
|
}
|
|
|
|
|
|
-watch(visible, (v) => { if (v) resetForm(); });
|
|
|
+watch(visible, (v) => {
|
|
|
+ if (!v) return;
|
|
|
+ if (props.draftData) {
|
|
|
+ hydrateFromDraft(props.draftData);
|
|
|
+ } else {
|
|
|
+ resetForm();
|
|
|
+ }
|
|
|
+});
|
|
|
|
|
|
const isManualReport = computed(() => form.mechanism === 'MANUAL_REPORT');
|
|
|
|
|
|
-// 当前机制下可选业务对象
|
|
|
const filteredObjects = computed(() => {
|
|
|
if (!form.mechanism || isManualReport.value) return [];
|
|
|
return BUSINESS_MONITOR_OPTIONS.filter((o) => o.mechanisms.includes(form.mechanism as any));
|
|
|
@@ -203,13 +253,14 @@ watch(selectedMetric, (m) => {
|
|
|
if (m) form.unit = m.unit;
|
|
|
});
|
|
|
|
|
|
-// 当机制切换时,重置后续选择
|
|
|
watch(() => form.mechanism, () => {
|
|
|
form.objectIndex = -1;
|
|
|
form.metricCode = '';
|
|
|
+ dictionaryMissing.value = false;
|
|
|
});
|
|
|
watch(() => form.objectIndex, () => {
|
|
|
form.metricCode = '';
|
|
|
+ if (form.objectIndex >= 0) dictionaryMissing.value = false;
|
|
|
});
|
|
|
|
|
|
const filteredExceptionTypes = computed(() => {
|
|
|
@@ -223,7 +274,6 @@ const exceptionTypeRow = computed(() =>
|
|
|
props.exceptionTypes.find((t) => t.typeCode === form.exceptionTypeCode) ?? null,
|
|
|
);
|
|
|
|
|
|
-// thresholdDisplay 推导
|
|
|
const thresholdDisplay = computed(() => {
|
|
|
const u = form.unit || '';
|
|
|
if (form.mechanism === 'DATE') {
|
|
|
@@ -241,7 +291,6 @@ const thresholdDisplay = computed(() => {
|
|
|
return '';
|
|
|
});
|
|
|
|
|
|
-// 显示 label
|
|
|
const mechanismLabel = computed(() => MECHANISM_OPTIONS.find((o) => o.value === form.mechanism)?.label ?? '—');
|
|
|
const stageNodeLabel = computed(() => props.stageNodes.find((n) => n.nodeCode === form.stageCode)?.nodeName ?? '—');
|
|
|
const orderFlowNodeLabel = computed(() => {
|
|
|
@@ -260,6 +309,13 @@ const ruleCodePlaceholder = computed(() => {
|
|
|
}
|
|
|
});
|
|
|
|
|
|
+const dictionaryHint = computed(() => {
|
|
|
+ if (!dictionaryMissing.value) return '';
|
|
|
+ const obj = fallbackObjectLabel.value || '原对象';
|
|
|
+ const metric = fallbackMetricLabel.value || '原指标';
|
|
|
+ return `当前指标字典已变化(原:${obj}·${metric}),请重新选择监控对象和指标后再生成正式规则。`;
|
|
|
+});
|
|
|
+
|
|
|
function pickMechanism(value: string) {
|
|
|
form.mechanism = value as typeof form.mechanism;
|
|
|
}
|
|
|
@@ -277,7 +333,6 @@ function nextStep() {
|
|
|
if (!form.metricCode) { ElMessage.warning('请选择监控指标'); return; }
|
|
|
step.value = 3;
|
|
|
} else if (step.value === 3) {
|
|
|
- // 判定标准校验
|
|
|
if (form.mechanism === 'DATE') {
|
|
|
const states = form.completedStates.split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
|
|
|
if (states.length === 0) { ElMessage.warning('完成状态至少填一个'); return; }
|
|
|
@@ -291,6 +346,8 @@ function nextStep() {
|
|
|
if (!form.exceptionTypeCode) { ElMessage.warning('请选择异常类型'); return; }
|
|
|
const def = exceptionTypeRow.value?.severityDefault;
|
|
|
if (def) form.severity = def;
|
|
|
+ // 进入第 5 步前,若 draftName 为空,自动生成默认值;用户仍可在 step 4 编辑。
|
|
|
+ if (!draftName.value.trim()) draftName.value = generateDraftName();
|
|
|
step.value = 4;
|
|
|
}
|
|
|
}
|
|
|
@@ -298,7 +355,7 @@ function prevStep() { if (step.value > 0) step.value -= 1; }
|
|
|
|
|
|
function ruleTypeFromMechanism(mech: string): string {
|
|
|
if (mech === 'DATE') return 'TIMEOUT';
|
|
|
- return 'OUT_OF_RANGE'; // VALUE_RANGE / RATIO 共用 OOR evaluator
|
|
|
+ return 'OUT_OF_RANGE';
|
|
|
}
|
|
|
|
|
|
function buildParamsJson(): string {
|
|
|
@@ -341,7 +398,6 @@ function buildParamsJson(): string {
|
|
|
return JSON.stringify(params);
|
|
|
}
|
|
|
if (form.mechanism === 'RATIO') {
|
|
|
- // RATIO 只写固定 lowerBound(目标比例),禁止写 lowerBoundField/upperBoundField
|
|
|
return JSON.stringify({
|
|
|
...common,
|
|
|
measuredValueField: m?.measuredValueField ?? 'measured_value',
|
|
|
@@ -357,7 +413,6 @@ function buildExpression(): string {
|
|
|
if (form.mechanism === 'DATE') {
|
|
|
return "SELECT 'DEMO-OBJECT-001' AS related_object_code, 'DEMO-OBJECT-001' AS source_object_id, 'DEMO-OBJECT-001' AS related_object_name, DATE_SUB(NOW(), INTERVAL 1 HOUR) AS due_at, 'PENDING' AS status WHERE 1=0";
|
|
|
}
|
|
|
- // VALUE_RANGE / RATIO
|
|
|
return "SELECT 'DEMO-OBJECT-001' AS related_object_code, 'DEMO-OBJECT-001' AS source_object_id, 'DEMO-OBJECT-001' AS related_object_name, 0 AS measured_value WHERE 1=0";
|
|
|
}
|
|
|
|
|
|
@@ -367,20 +422,174 @@ function pickDataSourceId(): number | null {
|
|
|
return (enabledOne ?? props.dataSources[0]).id;
|
|
|
}
|
|
|
|
|
|
-async function save() {
|
|
|
- if (!form.ruleCode.trim()) { ElMessage.warning('规则编码不能为空'); return; }
|
|
|
- if (!form.stageCode) { ElMessage.warning('阶段维度未选择'); return; }
|
|
|
- if (!form.exceptionTypeCode) { ElMessage.warning('异常类型未选择'); return; }
|
|
|
- if (form.objectIndex < 0 || !form.metricCode) { ElMessage.warning('监控对象或监控指标未选择'); return; }
|
|
|
- if (form.pollIntervalSeconds <= 0) { ElMessage.warning('轮询间隔必须大于 0'); return; }
|
|
|
+// CONFIG-WIZARD-DRAFT-FRONTEND-1:序列化 / 反序列化向导态。
|
|
|
+function serializeWizardState(): string {
|
|
|
+ const obj = selectedObject.value;
|
|
|
+ const m = selectedMetric.value;
|
|
|
+ return JSON.stringify({
|
|
|
+ version: 1,
|
|
|
+ step: step.value,
|
|
|
+ form: {
|
|
|
+ mechanism: form.mechanism,
|
|
|
+ stageCode: form.stageCode,
|
|
|
+ orderFlowCode: form.orderFlowCode,
|
|
|
+ exceptionTypeCode: form.exceptionTypeCode,
|
|
|
+ severity: form.severity,
|
|
|
+ objectType: obj?.objectType ?? '',
|
|
|
+ objectLabel: obj?.objectLabel ?? '',
|
|
|
+ metricCode: form.metricCode,
|
|
|
+ metricLabel: m?.metricLabel ?? '',
|
|
|
+ unit: form.unit,
|
|
|
+ graceMinutes: form.graceMinutes,
|
|
|
+ completedStates: form.completedStates,
|
|
|
+ lowerBound: form.lowerBound,
|
|
|
+ upperBound: form.upperBound,
|
|
|
+ toleranceAbs: form.toleranceAbs,
|
|
|
+ toleranceRatioPct: form.toleranceRatioPct,
|
|
|
+ targetRatio: form.targetRatio,
|
|
|
+ ruleCode: form.ruleCode,
|
|
|
+ pollIntervalSeconds: form.pollIntervalSeconds,
|
|
|
+ triggerCountRequired: form.triggerCountRequired,
|
|
|
+ recoverCountRequired: form.recoverCountRequired,
|
|
|
+ },
|
|
|
+ labels: {
|
|
|
+ mechanismLabel: mechanismLabel.value,
|
|
|
+ stageLabel: stageNodeLabel.value,
|
|
|
+ objectLabel: obj?.objectLabel ?? '',
|
|
|
+ metricLabel: m?.metricLabel ?? '',
|
|
|
+ thresholdDisplay: thresholdDisplay.value,
|
|
|
+ },
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function hydrateFromDraft(draft: S8WizardDraftDetail) {
|
|
|
+ resetForm();
|
|
|
+ currentDraftId.value = draft.id;
|
|
|
+ currentDraftCode.value = draft.draftCode;
|
|
|
+ draftName.value = draft.draftName ?? '';
|
|
|
+ let parsed: any = null;
|
|
|
+ try { parsed = JSON.parse(draft.wizardJson); } catch { parsed = null; }
|
|
|
+ if (!parsed || typeof parsed !== 'object') {
|
|
|
+ ElMessage.warning('草稿内容损坏,已重置');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (parsed.version && parsed.version !== 1) {
|
|
|
+ ElMessage.warning(`草稿版本 ${parsed.version} 不被当前前端支持,请新建`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const f = parsed.form ?? {};
|
|
|
+ const labels = parsed.labels ?? {};
|
|
|
+
|
|
|
+ // 1) 先恢复 mechanism(filteredObjects 依赖它)
|
|
|
+ if (f.mechanism) form.mechanism = f.mechanism;
|
|
|
+ if (f.stageCode) form.stageCode = f.stageCode;
|
|
|
+ if (f.orderFlowCode) form.orderFlowCode = f.orderFlowCode;
|
|
|
+ if (f.exceptionTypeCode) form.exceptionTypeCode = f.exceptionTypeCode;
|
|
|
+ if (f.severity) form.severity = f.severity;
|
|
|
+
|
|
|
+ // 2) 反查 BUSINESS_MONITOR_OPTIONS:按 (objectType, objectLabel) 在当前机制下定位 objectIndex
|
|
|
+ const candidates = BUSINESS_MONITOR_OPTIONS.filter((o) => o.mechanisms.includes(form.mechanism as any));
|
|
|
+ const idx = candidates.findIndex((o) => o.objectType === f.objectType && o.objectLabel === f.objectLabel);
|
|
|
+ if (idx >= 0) {
|
|
|
+ form.objectIndex = idx;
|
|
|
+ const metricExists = candidates[idx].metrics.some((mm) => mm.metricCode === f.metricCode && mm.mechanism === form.mechanism);
|
|
|
+ if (metricExists) form.metricCode = f.metricCode;
|
|
|
+ else dictionaryMissing.value = true;
|
|
|
+ } else {
|
|
|
+ // 字典缺失 — 保留 label 显示,禁止 generate
|
|
|
+ dictionaryMissing.value = true;
|
|
|
+ fallbackObjectLabel.value = f.objectLabel || labels.objectLabel || '';
|
|
|
+ fallbackMetricLabel.value = f.metricLabel || labels.metricLabel || '';
|
|
|
+ }
|
|
|
|
|
|
- const dataSourceId = pickDataSourceId();
|
|
|
- if (dataSourceId == null) { ElMessage.error('缺少数据源,无法创建规则'); return; }
|
|
|
+ if (typeof f.unit === 'string') form.unit = f.unit;
|
|
|
+ if (typeof f.graceMinutes === 'number') form.graceMinutes = f.graceMinutes;
|
|
|
+ if (typeof f.completedStates === 'string') form.completedStates = f.completedStates;
|
|
|
+ if (f.lowerBound !== undefined) form.lowerBound = f.lowerBound;
|
|
|
+ if (f.upperBound !== undefined) form.upperBound = f.upperBound;
|
|
|
+ if (typeof f.toleranceAbs === 'number') form.toleranceAbs = f.toleranceAbs;
|
|
|
+ if (typeof f.toleranceRatioPct === 'number') form.toleranceRatioPct = f.toleranceRatioPct;
|
|
|
+ if (typeof f.targetRatio === 'number') form.targetRatio = f.targetRatio;
|
|
|
+ if (typeof f.ruleCode === 'string') form.ruleCode = f.ruleCode;
|
|
|
+ if (typeof f.pollIntervalSeconds === 'number') form.pollIntervalSeconds = f.pollIntervalSeconds;
|
|
|
+ if (typeof f.triggerCountRequired === 'number') form.triggerCountRequired = f.triggerCountRequired;
|
|
|
+ if (typeof f.recoverCountRequired === 'number') form.recoverCountRequired = f.recoverCountRequired;
|
|
|
+
|
|
|
+ const wantStep = typeof parsed.step === 'number' ? parsed.step : (typeof draft.currentStep === 'number' ? draft.currentStep : 0);
|
|
|
+ step.value = Math.max(0, Math.min(4, wantStep));
|
|
|
+}
|
|
|
|
|
|
- const obj = selectedObject.value;
|
|
|
- if (!obj) { ElMessage.warning('监控对象异常'); return; }
|
|
|
+function buildDraftPayload(): {
|
|
|
+ tenantId: number;
|
|
|
+ factoryId: number;
|
|
|
+ draftCode: string;
|
|
|
+ draftName: string;
|
|
|
+ wizardJson: string;
|
|
|
+ currentStep: number;
|
|
|
+ mechanism: string;
|
|
|
+ stageCode: string;
|
|
|
+ orderFlowCode: string | null;
|
|
|
+ exceptionTypeCode: string;
|
|
|
+} {
|
|
|
+ const name = draftName.value.trim() || generateDraftName();
|
|
|
+ return {
|
|
|
+ tenantId: props.tenantId ?? 1,
|
|
|
+ factoryId: props.factoryId ?? 1,
|
|
|
+ draftCode: currentDraftCode.value || generateDraftCode(),
|
|
|
+ draftName: name,
|
|
|
+ wizardJson: serializeWizardState(),
|
|
|
+ currentStep: step.value,
|
|
|
+ mechanism: form.mechanism,
|
|
|
+ stageCode: form.stageCode,
|
|
|
+ orderFlowCode: form.orderFlowCode || null,
|
|
|
+ exceptionTypeCode: form.exceptionTypeCode,
|
|
|
+ };
|
|
|
+}
|
|
|
|
|
|
- const payload: S8WatchRuleCreatePayload = {
|
|
|
+async function saveDraft(): Promise<boolean> {
|
|
|
+ if (isManualReport.value) {
|
|
|
+ ElMessage.info('主动提报无需保存草稿');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ const payload = buildDraftPayload();
|
|
|
+ savingDraft.value = true;
|
|
|
+ try {
|
|
|
+ if (currentDraftId.value == null) {
|
|
|
+ const created = await s8ConfigApi.wizardDrafts.create(payload);
|
|
|
+ currentDraftId.value = created.id;
|
|
|
+ currentDraftCode.value = created.draftCode;
|
|
|
+ draftName.value = created.draftName ?? payload.draftName;
|
|
|
+ ElMessage.success('草稿已保存');
|
|
|
+ } else {
|
|
|
+ const updated = await s8ConfigApi.wizardDrafts.update(currentDraftId.value, {
|
|
|
+ draftName: payload.draftName,
|
|
|
+ wizardJson: payload.wizardJson,
|
|
|
+ currentStep: payload.currentStep,
|
|
|
+ mechanism: payload.mechanism || null,
|
|
|
+ stageCode: payload.stageCode || null,
|
|
|
+ orderFlowCode: payload.orderFlowCode,
|
|
|
+ exceptionTypeCode: payload.exceptionTypeCode || null,
|
|
|
+ });
|
|
|
+ draftName.value = updated.draftName ?? payload.draftName;
|
|
|
+ ElMessage.success('草稿已更新');
|
|
|
+ }
|
|
|
+ emit('draftSaved');
|
|
|
+ return true;
|
|
|
+ } catch (e: any) {
|
|
|
+ const msg = e?.response?.data?.message ?? e?.message ?? '保存草稿失败';
|
|
|
+ ElMessage.error(msg);
|
|
|
+ return false;
|
|
|
+ } finally {
|
|
|
+ savingDraft.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function buildRulePayload(): S8WatchRuleCreatePayload | null {
|
|
|
+ const dataSourceId = pickDataSourceId();
|
|
|
+ if (dataSourceId == null) { ElMessage.error('缺少数据源,无法生成规则'); return null; }
|
|
|
+ const obj = selectedObject.value;
|
|
|
+ if (!obj) { ElMessage.warning('监控对象异常'); return null; }
|
|
|
+ return {
|
|
|
tenantId: props.tenantId ?? 1,
|
|
|
factoryId: props.factoryId ?? 1,
|
|
|
ruleCode: form.ruleCode.trim(),
|
|
|
@@ -401,15 +610,45 @@ async function save() {
|
|
|
paramsJson: buildParamsJson(),
|
|
|
expression: buildExpression(),
|
|
|
};
|
|
|
+}
|
|
|
|
|
|
+function validateGenerate(): boolean {
|
|
|
+ if (!form.mechanism || isManualReport.value) { ElMessage.warning('未选择报警机制'); return false; }
|
|
|
+ if (!form.stageCode) { ElMessage.warning('阶段维度未选择'); return false; }
|
|
|
+ if (form.objectIndex < 0 || !form.metricCode) { ElMessage.warning('监控对象或监控指标未选择'); return false; }
|
|
|
+ if (!form.exceptionTypeCode) { ElMessage.warning('异常类型未选择'); return false; }
|
|
|
+ if (!form.ruleCode.trim()) { ElMessage.warning('规则编码不能为空'); return false; }
|
|
|
+ if (form.pollIntervalSeconds <= 0) { ElMessage.warning('轮询间隔必须大于 0'); return false; }
|
|
|
+ if (form.mechanism === 'DATE') {
|
|
|
+ const states = form.completedStates.split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
|
|
|
+ if (states.length === 0) { ElMessage.warning('完成状态至少填一个'); return false; }
|
|
|
+ }
|
|
|
+ if (form.mechanism === 'VALUE_RANGE') {
|
|
|
+ if (form.lowerBound == null && form.upperBound == null) { ElMessage.warning('上下限至少填一个'); return false; }
|
|
|
+ }
|
|
|
+ if (form.mechanism === 'RATIO') {
|
|
|
+ if (!(form.targetRatio > 0 && form.targetRatio <= 100)) { ElMessage.warning('目标比例须在 0–100 之间'); return false; }
|
|
|
+ }
|
|
|
+ if (dictionaryMissing.value) { ElMessage.warning(dictionaryHint.value || '指标字典缺失,请重新选择'); return false; }
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+async function generateRuleFromDraft() {
|
|
|
+ if (!validateGenerate()) return;
|
|
|
saving.value = true;
|
|
|
try {
|
|
|
- await s8ConfigApi.watchRules.create(payload);
|
|
|
- ElMessage.success('规则已创建,默认停用');
|
|
|
+ const ok = await saveDraft();
|
|
|
+ if (!ok || currentDraftId.value == null) { saving.value = false; return; }
|
|
|
+ const payload = buildRulePayload();
|
|
|
+ if (!payload) { saving.value = false; return; }
|
|
|
+ payload.enabled = false; // 双保险
|
|
|
+ const result = await s8ConfigApi.wizardDrafts.generateRule(currentDraftId.value, { rulePayload: payload });
|
|
|
+ ElMessage.success(`正式规则已生成,默认停用(id=${result.ruleId})`);
|
|
|
+ emit('generated');
|
|
|
emit('saved');
|
|
|
visible.value = false;
|
|
|
} catch (e: any) {
|
|
|
- const msg = e?.response?.data?.message ?? e?.message ?? '创建失败';
|
|
|
+ const msg = e?.response?.data?.message ?? e?.message ?? '生成规则失败';
|
|
|
ElMessage.error(msg);
|
|
|
} finally {
|
|
|
saving.value = false;
|
|
|
@@ -423,7 +662,7 @@ function goManualReport() {
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
- <el-dialog v-model="visible" title="新建监控配置" width="680px" :close-on-click-modal="false" destroy-on-close>
|
|
|
+ <el-dialog v-model="visible" :title="currentDraftId ? '继续编辑草稿' : '新建监控配置'" width="680px" :close-on-click-modal="false" destroy-on-close>
|
|
|
<el-steps :active="isManualReport ? 0 : step" finish-status="success" simple style="margin-bottom: 16px">
|
|
|
<el-step title="报警机制" />
|
|
|
<el-step title="业务维度" />
|
|
|
@@ -432,6 +671,15 @@ function goManualReport() {
|
|
|
<el-step title="基础配置" />
|
|
|
</el-steps>
|
|
|
|
|
|
+ <el-alert
|
|
|
+ v-if="dictionaryMissing && !isManualReport"
|
|
|
+ type="warning"
|
|
|
+ show-icon
|
|
|
+ :closable="false"
|
|
|
+ style="margin-bottom: 12px"
|
|
|
+ :title="dictionaryHint"
|
|
|
+ />
|
|
|
+
|
|
|
<!-- Step 0: 报警机制 -->
|
|
|
<div v-if="step === 0">
|
|
|
<div class="wizard-mech-grid">
|
|
|
@@ -480,7 +728,7 @@ function goManualReport() {
|
|
|
<el-select v-model="form.objectIndex" placeholder="选择监控对象" style="width: 100%">
|
|
|
<el-option
|
|
|
v-for="(o, idx) in filteredObjects"
|
|
|
- :key="`${o.objectType}-${idx}`"
|
|
|
+ :key="`${o.objectType}-${o.objectLabel}-${idx}`"
|
|
|
:label="o.objectLabel"
|
|
|
:value="idx"
|
|
|
/>
|
|
|
@@ -503,7 +751,6 @@ function goManualReport() {
|
|
|
<!-- Step 3: 判定标准 -->
|
|
|
<div v-else-if="step === 3">
|
|
|
<el-form label-position="top" size="small">
|
|
|
- <!-- DATE -->
|
|
|
<template v-if="form.mechanism === 'DATE'">
|
|
|
<el-alert type="info" :closable="false" show-icon title="判定逻辑">
|
|
|
<template #default>到期后仍未到达「完成状态」即触发。</template>
|
|
|
@@ -516,7 +763,6 @@ function goManualReport() {
|
|
|
<span class="wizard-hint">超过到期时间后再等待 N 分钟才触发,0 表示立即触发</span>
|
|
|
</el-form-item>
|
|
|
</template>
|
|
|
- <!-- VALUE_RANGE -->
|
|
|
<template v-else-if="form.mechanism === 'VALUE_RANGE'">
|
|
|
<el-alert type="info" :closable="false" show-icon title="判定逻辑">
|
|
|
<template #default>低于下限或高于上限即触发;至少填写下限或上限之一。</template>
|
|
|
@@ -537,7 +783,6 @@ function goManualReport() {
|
|
|
<el-input-number v-model="form.toleranceRatioPct" :min="0" :max="100" :step="1" />
|
|
|
</el-form-item>
|
|
|
</template>
|
|
|
- <!-- RATIO -->
|
|
|
<template v-else-if="form.mechanism === 'RATIO'">
|
|
|
<el-alert type="info" :closable="false" show-icon title="判定逻辑">
|
|
|
<template #default>实际比例低于目标比例即触发。</template>
|
|
|
@@ -572,8 +817,8 @@ function goManualReport() {
|
|
|
<el-descriptions-item label="报警机制">{{ mechanismLabel }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="所属阶段">{{ stageNodeLabel }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="订单流程">{{ orderFlowNodeLabel }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="监控对象">{{ selectedObject?.objectLabel || '—' }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="监控指标">{{ selectedMetric?.metricLabel || '—' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="监控对象">{{ selectedObject?.objectLabel || fallbackObjectLabel || '—' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="监控指标">{{ selectedMetric?.metricLabel || fallbackMetricLabel || '—' }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="判定标准">{{ thresholdDisplay }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="异常类型">{{ exceptionTypeLabel }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="严重度">{{ severityLabel }}</el-descriptions-item>
|
|
|
@@ -584,6 +829,10 @@ function goManualReport() {
|
|
|
</el-descriptions>
|
|
|
|
|
|
<el-form label-position="top" size="small">
|
|
|
+ <el-form-item label="草稿名称">
|
|
|
+ <el-input v-model="draftName" placeholder="留空则自动生成" maxlength="128" show-word-limit />
|
|
|
+ <span class="wizard-hint">草稿名仅用于草稿配置列表展示,可随时修改</span>
|
|
|
+ </el-form-item>
|
|
|
<el-form-item label="规则编码" required>
|
|
|
<el-input v-model="form.ruleCode" :placeholder="ruleCodePlaceholder" maxlength="64" show-word-limit />
|
|
|
</el-form-item>
|
|
|
@@ -608,8 +857,9 @@ function goManualReport() {
|
|
|
<template v-else>
|
|
|
<el-button v-if="step > 0" @click="prevStep">上一步</el-button>
|
|
|
<el-button @click="visible = false">取消</el-button>
|
|
|
+ <el-button :loading="savingDraft" @click="saveDraft">保存草稿</el-button>
|
|
|
<el-button v-if="step < 4" type="primary" @click="nextStep">下一步</el-button>
|
|
|
- <el-button v-else type="primary" :loading="saving" @click="save">保存(停用)</el-button>
|
|
|
+ <el-button v-else type="primary" :loading="saving" :disabled="dictionaryMissing" @click="generateRuleFromDraft">生成正式规则</el-button>
|
|
|
</template>
|
|
|
</template>
|
|
|
</el-dialog>
|