|
|
@@ -20,6 +20,13 @@ const exceptionTypes = ref<S8ExceptionTypeConfigRow[]>([]);
|
|
|
const drawerOpen = ref(false);
|
|
|
const editingRow = ref<S8WatchRuleConfigRow | null>(null);
|
|
|
|
|
|
+// A-03: filter state
|
|
|
+const filterSceneCode = ref('');
|
|
|
+const filterSeverity = ref('');
|
|
|
+
|
|
|
+// B-05: advanced fields collapse
|
|
|
+const advancedExpanded = ref(false);
|
|
|
+
|
|
|
interface TimeoutForm {
|
|
|
dueAtField: string;
|
|
|
statusField: string;
|
|
|
@@ -29,13 +36,14 @@ interface TimeoutForm {
|
|
|
graceMinutes: number;
|
|
|
exceptionTypeCode: string;
|
|
|
}
|
|
|
+// A-06: toleranceRatio renamed to toleranceRatioPct (stores 0–100 in UI, converts to 0–1 on save)
|
|
|
interface ShortageForm {
|
|
|
targetQtyField: string;
|
|
|
actualQtyField: string;
|
|
|
objectCodeField: string;
|
|
|
objectIdField: string;
|
|
|
toleranceAbs: number;
|
|
|
- toleranceRatio: number;
|
|
|
+ toleranceRatioPct: number;
|
|
|
exceptionTypeCode: string;
|
|
|
}
|
|
|
interface OutOfRangeForm {
|
|
|
@@ -47,7 +55,7 @@ interface OutOfRangeForm {
|
|
|
lowerBoundField: string;
|
|
|
upperBoundField: string;
|
|
|
toleranceAbs: number;
|
|
|
- toleranceRatio: number;
|
|
|
+ toleranceRatioPct: number;
|
|
|
exceptionTypeCode: string;
|
|
|
}
|
|
|
|
|
|
@@ -66,7 +74,7 @@ const shortageForm = reactive<ShortageForm>({
|
|
|
objectCodeField: '',
|
|
|
objectIdField: '',
|
|
|
toleranceAbs: 0,
|
|
|
- toleranceRatio: 0,
|
|
|
+ toleranceRatioPct: 0,
|
|
|
exceptionTypeCode: '',
|
|
|
});
|
|
|
const outOfRangeForm = reactive<OutOfRangeForm>({
|
|
|
@@ -78,7 +86,7 @@ const outOfRangeForm = reactive<OutOfRangeForm>({
|
|
|
lowerBoundField: '',
|
|
|
upperBoundField: '',
|
|
|
toleranceAbs: 0,
|
|
|
- toleranceRatio: 0,
|
|
|
+ toleranceRatioPct: 0,
|
|
|
exceptionTypeCode: '',
|
|
|
});
|
|
|
const enabledForm = ref(true);
|
|
|
@@ -131,6 +139,36 @@ function stopAutoRefresh() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// A-03: filtered rows
|
|
|
+const filteredRows = computed(() => {
|
|
|
+ let r = rows.value;
|
|
|
+ if (filterSceneCode.value) r = r.filter(x => x.sceneCode === filterSceneCode.value);
|
|
|
+ if (filterSeverity.value) r = r.filter(x => (x.severity ?? '').toUpperCase() === filterSeverity.value);
|
|
|
+ return r;
|
|
|
+});
|
|
|
+
|
|
|
+const sceneCodeOptions = computed(() =>
|
|
|
+ [...new Set(rows.value.map(r => r.sceneCode).filter((c): c is string => !!c))].sort(),
|
|
|
+);
|
|
|
+
|
|
|
+// A-03: severity helpers
|
|
|
+function severityLabel(s?: string | null): string {
|
|
|
+ switch ((s ?? '').toUpperCase()) {
|
|
|
+ case 'HIGH': return '高';
|
|
|
+ case 'MEDIUM': return '中';
|
|
|
+ case 'LOW': return '低';
|
|
|
+ default: return s || '—';
|
|
|
+ }
|
|
|
+}
|
|
|
+function severityTagType(s?: string | null): 'danger' | 'warning' | 'info' | '' {
|
|
|
+ switch ((s ?? '').toUpperCase()) {
|
|
|
+ case 'HIGH': return 'danger';
|
|
|
+ case 'MEDIUM': return 'warning';
|
|
|
+ case 'LOW': return 'info';
|
|
|
+ default: return '';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
const ruleTypeOf = (row: S8WatchRuleConfigRow | null): S8WatchRuleType | '' =>
|
|
|
(row?.ruleType ?? '').toUpperCase();
|
|
|
|
|
|
@@ -139,11 +177,12 @@ const formTitle = computed(() => {
|
|
|
return `编辑规则参数 — ${editingRow.value.ruleCode}`;
|
|
|
});
|
|
|
|
|
|
+// A-01: OUT_OF_RANGE label changed from '数据超差' to '数值超限'
|
|
|
const ruleTypeLabel = (t?: string | null) => {
|
|
|
switch ((t ?? '').toUpperCase()) {
|
|
|
case 'TIMEOUT': return '超时';
|
|
|
case 'SHORTAGE': return '数量不足';
|
|
|
- case 'OUT_OF_RANGE': return '数据超差';
|
|
|
+ case 'OUT_OF_RANGE': return '数值超限';
|
|
|
default: return t || '—';
|
|
|
}
|
|
|
};
|
|
|
@@ -154,6 +193,101 @@ const exceptionTypeOptions = computed(() =>
|
|
|
.map((t) => ({ label: `${t.typeName} (${t.typeCode})`, value: t.typeCode })),
|
|
|
);
|
|
|
|
|
|
+// A-07: exception type name lookup
|
|
|
+function exceptionTypeName(code: string): string {
|
|
|
+ const found = exceptionTypes.value.find(t => t.typeCode === code);
|
|
|
+ return found ? found.typeName : code;
|
|
|
+}
|
|
|
+
|
|
|
+// A-07: rule summary in plain Chinese
|
|
|
+const ruleSummary = computed<{ text: string; complete: boolean }>(() => {
|
|
|
+ const row = editingRow.value;
|
|
|
+ if (!row) return { text: '', complete: false };
|
|
|
+ const t = ruleTypeOf(row);
|
|
|
+ if (t === 'TIMEOUT') {
|
|
|
+ const f = timeoutForm;
|
|
|
+ if (!f.dueAtField || !f.statusField || !f.exceptionTypeCode) {
|
|
|
+ return { text: '超时规则(参数不完整,请填写必填字段)', complete: false };
|
|
|
+ }
|
|
|
+ const typeName = exceptionTypeName(f.exceptionTypeCode);
|
|
|
+ const grace = f.graceMinutes > 0 ? `,宽限 ${f.graceMinutes} 分钟` : '';
|
|
|
+ return {
|
|
|
+ text: `检测 "${f.dueAtField}" 是否超期${grace},触发"${typeName}"异常`,
|
|
|
+ complete: true,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ if (t === 'SHORTAGE') {
|
|
|
+ const f = shortageForm;
|
|
|
+ if (!f.targetQtyField || !f.actualQtyField || !f.exceptionTypeCode) {
|
|
|
+ return { text: '数量不足规则(参数不完整,请填写必填字段)', complete: false };
|
|
|
+ }
|
|
|
+ const typeName = exceptionTypeName(f.exceptionTypeCode);
|
|
|
+ return {
|
|
|
+ text: `"${f.actualQtyField}" 低于 "${f.targetQtyField}"(绝对容差 ${f.toleranceAbs},比例容差 ${f.toleranceRatioPct}%),触发"${typeName}"异常`,
|
|
|
+ complete: true,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ if (t === 'OUT_OF_RANGE') {
|
|
|
+ const f = outOfRangeForm;
|
|
|
+ if (!f.measuredValueField || !f.exceptionTypeCode) {
|
|
|
+ return { text: '数值超限规则(参数不完整,请填写必填字段)', complete: false };
|
|
|
+ }
|
|
|
+ const typeName = exceptionTypeName(f.exceptionTypeCode);
|
|
|
+ const boundDesc = f.lowerBoundField || f.upperBoundField
|
|
|
+ ? `行内字段限(${[f.lowerBoundField && `下限字段: ${f.lowerBoundField}`, f.upperBoundField && `上限字段: ${f.upperBoundField}`].filter(Boolean).join(',')})`
|
|
|
+ : `固定区间 [${f.lowerBound ?? '—'}, ${f.upperBound ?? '—'}]`;
|
|
|
+ return {
|
|
|
+ text: `"${f.measuredValueField}" 超出 ${boundDesc},触发"${typeName}"异常`,
|
|
|
+ complete: true,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return { text: '规则类型未知', complete: false };
|
|
|
+});
|
|
|
+
|
|
|
+// A-05: lastError Chinese mapping
|
|
|
+function mapLastError(raw: string | null | undefined): string {
|
|
|
+ if (!raw) return '—';
|
|
|
+ const s = raw.trim();
|
|
|
+ if (/params_json.*invalid/i.test(s) || /json.*parse/i.test(s)) return '配置参数格式错误,请重新保存';
|
|
|
+ if (/connection.*refused/i.test(s) || /cannot.*connect/i.test(s) || /连接.*失败/i.test(s)) return '数据库连接失败';
|
|
|
+ if (/query.*timeout|execution.*timeout|command.*timeout|sql.*timeout/i.test(s)) return '查询超时';
|
|
|
+ if (/column.*not.*found/i.test(s) || /invalid.*column/i.test(s) || /列名.*无效/i.test(s)) return '字段名配置有误,列不存在';
|
|
|
+ if (/table.*not.*found/i.test(s) || /object.*not.*found/i.test(s)) return '数据源表不存在';
|
|
|
+ if (/permission.*denied/i.test(s) || /access.*denied/i.test(s)) return '数据库权限不足';
|
|
|
+ return s.length > 60 ? s.substring(0, 60) + '…' : s;
|
|
|
+}
|
|
|
+
|
|
|
+// A-06: toleranceRatio conversion helpers
|
|
|
+function ratioToPercent(v: number | null | undefined): number {
|
|
|
+ return parseFloat(((v ?? 0) * 100).toFixed(4));
|
|
|
+}
|
|
|
+function percentToRatio(v: number | null | undefined): number {
|
|
|
+ return parseFloat(((v ?? 0) / 100).toFixed(4));
|
|
|
+}
|
|
|
+
|
|
|
+// B-01: pre-save validation
|
|
|
+function validateBeforeSave(): string | null {
|
|
|
+ const t = ruleTypeOf(editingRow.value);
|
|
|
+ if (t === 'TIMEOUT') {
|
|
|
+ if (!timeoutForm.dueAtField.trim()) return '到期时间字段名不能为空';
|
|
|
+ if (!timeoutForm.statusField.trim()) return '状态字段名不能为空';
|
|
|
+ if (!timeoutForm.exceptionTypeCode) return '请选择异常类型';
|
|
|
+ } else if (t === 'SHORTAGE') {
|
|
|
+ if (!shortageForm.targetQtyField.trim()) return '目标数量字段名不能为空';
|
|
|
+ if (!shortageForm.actualQtyField.trim()) return '实际数量字段名不能为空';
|
|
|
+ if (!shortageForm.exceptionTypeCode) return '请选择异常类型';
|
|
|
+ if (shortageForm.toleranceRatioPct < 0 || shortageForm.toleranceRatioPct > 100) return '比例容差须在 0–100% 范围内';
|
|
|
+ } else if (t === 'OUT_OF_RANGE') {
|
|
|
+ if (!outOfRangeForm.measuredValueField.trim()) return '测量值字段名不能为空';
|
|
|
+ if (!outOfRangeForm.exceptionTypeCode) return '请选择异常类型';
|
|
|
+ if (outOfRangeForm.toleranceRatioPct < 0 || outOfRangeForm.toleranceRatioPct > 100) return '比例容差须在 0–100% 范围内';
|
|
|
+ const noUpper = outOfRangeForm.upperBound == null && !outOfRangeForm.upperBoundField.trim();
|
|
|
+ const noLower = outOfRangeForm.lowerBound == null && !outOfRangeForm.lowerBoundField.trim();
|
|
|
+ if (noUpper && noLower) return '请至少配置一个上限或下限(固定值或行内字段)';
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
async function loadRows() {
|
|
|
loading.value = true;
|
|
|
try {
|
|
|
@@ -186,13 +320,13 @@ function resetForms() {
|
|
|
Object.assign(shortageForm, {
|
|
|
targetQtyField: '', actualQtyField: '',
|
|
|
objectCodeField: '', objectIdField: '',
|
|
|
- toleranceAbs: 0, toleranceRatio: 0, exceptionTypeCode: '',
|
|
|
+ toleranceAbs: 0, toleranceRatioPct: 0, exceptionTypeCode: '',
|
|
|
});
|
|
|
Object.assign(outOfRangeForm, {
|
|
|
measuredValueField: '', objectCodeField: '', objectIdField: '',
|
|
|
lowerBound: null, upperBound: null,
|
|
|
lowerBoundField: '', upperBoundField: '',
|
|
|
- toleranceAbs: 0, toleranceRatio: 0, exceptionTypeCode: '',
|
|
|
+ toleranceAbs: 0, toleranceRatioPct: 0, exceptionTypeCode: '',
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@@ -230,7 +364,8 @@ function loadFormFromRow(row: S8WatchRuleConfigRow) {
|
|
|
objectCodeField: parsed.objectCodeField ?? '',
|
|
|
objectIdField: parsed.objectIdField ?? '',
|
|
|
toleranceAbs: typeof parsed.toleranceAbs === 'number' ? parsed.toleranceAbs : 0,
|
|
|
- toleranceRatio: typeof parsed.toleranceRatio === 'number' ? parsed.toleranceRatio : 0,
|
|
|
+ // A-06: convert stored 0–1 to 0–100 for display
|
|
|
+ toleranceRatioPct: ratioToPercent(typeof parsed.toleranceRatio === 'number' ? parsed.toleranceRatio : 0),
|
|
|
exceptionTypeCode: parsed.exceptionTypeCode ?? '',
|
|
|
});
|
|
|
} else if (t === 'OUT_OF_RANGE') {
|
|
|
@@ -252,7 +387,8 @@ function loadFormFromRow(row: S8WatchRuleConfigRow) {
|
|
|
lowerBoundField: parsed.lowerBoundField ?? '',
|
|
|
upperBoundField: parsed.upperBoundField ?? '',
|
|
|
toleranceAbs: typeof parsed.toleranceAbs === 'number' ? parsed.toleranceAbs : 0,
|
|
|
- toleranceRatio: typeof parsed.toleranceRatio === 'number' ? parsed.toleranceRatio : 0,
|
|
|
+ // A-06: convert stored 0–1 to 0–100 for display
|
|
|
+ toleranceRatioPct: ratioToPercent(typeof parsed.toleranceRatio === 'number' ? parsed.toleranceRatio : 0),
|
|
|
exceptionTypeCode: parsed.exceptionTypeCode ?? 'EQUIP_FAULT',
|
|
|
});
|
|
|
}
|
|
|
@@ -261,6 +397,7 @@ function loadFormFromRow(row: S8WatchRuleConfigRow) {
|
|
|
function openEdit(row: S8WatchRuleConfigRow) {
|
|
|
editingRow.value = row;
|
|
|
loadFormFromRow(row);
|
|
|
+ advancedExpanded.value = false;
|
|
|
// S8-SCHED-SAVE-SPLIT-1:写入快照。schedule 走数值归一化;params 走 buildPayloadParamsJson
|
|
|
// 自身的稳定 key 顺序,因此打开时与保存时两次调用即可字节级比较。
|
|
|
originalScheduleSnapshot.value = {
|
|
|
@@ -319,7 +456,8 @@ function buildPayloadParamsJson(): string | null {
|
|
|
objectCodeField: shortageForm.objectCodeField,
|
|
|
objectIdField: shortageForm.objectIdField,
|
|
|
toleranceAbs: Number(shortageForm.toleranceAbs) || 0,
|
|
|
- toleranceRatio: Number(shortageForm.toleranceRatio) || 0,
|
|
|
+ // A-06: convert 0–100 back to 0–1 for storage
|
|
|
+ toleranceRatio: percentToRatio(shortageForm.toleranceRatioPct),
|
|
|
exceptionTypeCode: shortageForm.exceptionTypeCode,
|
|
|
});
|
|
|
}
|
|
|
@@ -333,7 +471,8 @@ function buildPayloadParamsJson(): string | null {
|
|
|
lowerBoundField: outOfRangeForm.lowerBoundField || undefined,
|
|
|
upperBoundField: outOfRangeForm.upperBoundField || undefined,
|
|
|
toleranceAbs: Number(outOfRangeForm.toleranceAbs) || 0,
|
|
|
- toleranceRatio: Number(outOfRangeForm.toleranceRatio) || 0,
|
|
|
+ // A-06: convert 0–100 back to 0–1 for storage
|
|
|
+ toleranceRatio: percentToRatio(outOfRangeForm.toleranceRatioPct),
|
|
|
exceptionTypeCode: outOfRangeForm.exceptionTypeCode,
|
|
|
});
|
|
|
}
|
|
|
@@ -342,6 +481,12 @@ function buildPayloadParamsJson(): string | null {
|
|
|
|
|
|
async function save() {
|
|
|
if (!editingRow.value) return;
|
|
|
+ // B-01: validate before submitting
|
|
|
+ const validationError = validateBeforeSave();
|
|
|
+ if (validationError) {
|
|
|
+ ElMessage.error(validationError);
|
|
|
+ return;
|
|
|
+ }
|
|
|
const scheduleChanged = hasScheduleChanged();
|
|
|
const paramsChanged = hasParamsChanged();
|
|
|
if (!scheduleChanged && !paramsChanged) {
|
|
|
@@ -402,10 +547,8 @@ async function save() {
|
|
|
if (scheduleChanged && paramsChanged) {
|
|
|
if (scheduleErr) {
|
|
|
ElMessage.error(`调度设置保存失败,规则参数未提交:${apiMsg(scheduleErr)}`);
|
|
|
- // 双方都未落库,列表无需刷新;抽屉保持打开供用户重试。
|
|
|
} else if (paramsErr) {
|
|
|
ElMessage.error(`调度设置已保存,但规则参数保存失败,请检查后重试:${apiMsg(paramsErr)}`);
|
|
|
- // schedule 已落库,刷新列表展示新 schedule;抽屉保持打开避免误以为全部成功。
|
|
|
await loadRows();
|
|
|
} else {
|
|
|
ElMessage.success('保存成功');
|
|
|
@@ -544,11 +687,22 @@ onDeactivated(() => stopAutoRefresh());
|
|
|
|
|
|
<template>
|
|
|
<AidopDemoShell title="监视规则配置" subtitle="S8 / 配置 / 监视规则(参数模板化编辑 + 调度运行态;列表 30 秒自动刷新)">
|
|
|
+ <!-- A-03: toolbar with scene/severity filters -->
|
|
|
<div class="rule-toolbar">
|
|
|
<el-button size="small" @click="loadRows" :loading="loading">手动刷新</el-button>
|
|
|
+ <el-select v-model="filterSceneCode" size="small" clearable placeholder="场景筛选" style="width: 140px">
|
|
|
+ <el-option v-for="c in sceneCodeOptions" :key="c" :label="c" :value="c" />
|
|
|
+ </el-select>
|
|
|
+ <el-select v-model="filterSeverity" size="small" clearable placeholder="严重度筛选" style="width: 120px">
|
|
|
+ <el-option label="高" value="HIGH" />
|
|
|
+ <el-option label="中" value="MEDIUM" />
|
|
|
+ <el-option label="低" value="LOW" />
|
|
|
+ </el-select>
|
|
|
<span class="rule-toolbar__hint">每 30 秒自动刷新;编辑抽屉打开期间暂停刷新</span>
|
|
|
</div>
|
|
|
- <el-table v-loading="loading" :data="rows" border stripe size="small">
|
|
|
+
|
|
|
+ <!-- A-02: updated labels; A-03: filteredRows + sceneCode/severity columns -->
|
|
|
+ <el-table v-loading="loading" :data="filteredRows" border stripe size="small">
|
|
|
<el-table-column prop="ruleCode" label="规则编码" width="180" />
|
|
|
<el-table-column label="规则类型" width="100">
|
|
|
<template #default="{ row }">
|
|
|
@@ -557,6 +711,15 @@ onDeactivated(() => stopAutoRefresh());
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
+ <el-table-column prop="sceneCode" label="场景" width="120" show-overflow-tooltip />
|
|
|
+ <el-table-column label="严重度" width="80">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag v-if="row.severity" :type="severityTagType(row.severity)" size="small">
|
|
|
+ {{ severityLabel(row.severity) }}
|
|
|
+ </el-tag>
|
|
|
+ <span v-else>—</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
<el-table-column label="启用" width="80">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag :type="row.enabled ? 'success' : 'info'" size="small">
|
|
|
@@ -564,14 +727,14 @@ onDeactivated(() => stopAutoRefresh());
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="轮询间隔" width="100">
|
|
|
+ <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 label="持续异常次数" width="115">
|
|
|
+ <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 label="持续正常次数" width="115">
|
|
|
+ <template #default="{ row }">连续 {{ row.recoverCountRequired ?? 1 }} 次</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="上次执行" width="160">
|
|
|
<template #default="{ row }">
|
|
|
@@ -603,10 +766,11 @@ onDeactivated(() => stopAutoRefresh());
|
|
|
<el-table-column label="耗时" width="80">
|
|
|
<template #default="{ row }">{{ row.lastDurationMs != null ? `${row.lastDurationMs}ms` : '—' }}</template>
|
|
|
</el-table-column>
|
|
|
+ <!-- A-05: mapLastError() applied -->
|
|
|
<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>
|
|
|
+ <span class="rule-error">{{ mapLastError(row.lastError) }}</span>
|
|
|
</el-tooltip>
|
|
|
<span v-else>—</span>
|
|
|
</template>
|
|
|
@@ -625,105 +789,249 @@ onDeactivated(() => stopAutoRefresh());
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
|
|
|
- <el-drawer v-model="drawerOpen" :title="formTitle" size="520px" direction="rtl" :before-close="(done: () => void) => { closeDrawer(); done(); }">
|
|
|
+ <el-drawer v-model="drawerOpen" :title="formTitle" size="540px" direction="rtl" :before-close="(done: () => void) => { closeDrawer(); done(); }">
|
|
|
<div v-if="editingRow" class="rule-edit">
|
|
|
+
|
|
|
+ <!-- A-07: rule summary block -->
|
|
|
+ <div class="rule-summary" :class="{ 'rule-summary--incomplete': !ruleSummary.complete }">
|
|
|
+ <span class="rule-summary__label">规则摘要:</span>{{ ruleSummary.text }}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-divider content-position="left">基本信息</el-divider>
|
|
|
<el-descriptions :column="1" size="small" border class="rule-meta">
|
|
|
<el-descriptions-item label="规则编码">{{ editingRow.ruleCode }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="规则类型">{{ ruleTypeLabel(editingRow.ruleType) }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="场景">{{ editingRow.sceneCode }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="源对象">{{ editingRow.sourceObjectType ?? editingRow.watchObjectType ?? '—' }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="表达式(只读)">
|
|
|
- <pre class="rule-expr">{{ editingRow.expression || '—' }}</pre>
|
|
|
+ <!-- A-04: severity readonly display -->
|
|
|
+ <el-descriptions-item label="严重度">
|
|
|
+ <el-tag v-if="editingRow.severity" :type="severityTagType(editingRow.severity)" size="small">{{ severityLabel(editingRow.severity) }}</el-tag>
|
|
|
+ <span v-else>—</span>
|
|
|
+ <span class="rule-hint">如需调整严重度,请联系实施人员</span>
|
|
|
</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="场景">{{ editingRow.sceneCode ?? '—' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="源对象">{{ editingRow.sourceObjectType ?? editingRow.watchObjectType ?? '—' }}</el-descriptions-item>
|
|
|
</el-descriptions>
|
|
|
|
|
|
- <el-divider content-position="left">参数</el-divider>
|
|
|
+ <el-divider content-position="left">触发条件</el-divider>
|
|
|
|
|
|
+ <!-- TIMEOUT form -->
|
|
|
<el-form v-if="ruleTypeOf(editingRow) === 'TIMEOUT'" label-position="top" size="small">
|
|
|
- <el-form-item label="dueAtField(到期时间字段名)">
|
|
|
- <el-input v-model="timeoutForm.dueAtField" />
|
|
|
+ <el-form-item>
|
|
|
+ <template #label>
|
|
|
+ 到期时间字段名
|
|
|
+ <el-tooltip content="填写 SQL 结果集中表示到期时间的列名,如 due_date、expected_finish_time" placement="top">
|
|
|
+ <span class="rule-field-hint">(?)</span>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ <el-input v-model="timeoutForm.dueAtField" placeholder="如 due_date" />
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="statusField(状态字段名)">
|
|
|
- <el-input v-model="timeoutForm.statusField" />
|
|
|
+ <el-form-item>
|
|
|
+ <template #label>
|
|
|
+ 状态字段名
|
|
|
+ <el-tooltip content="填写 SQL 结果集中表示当前状态的列名,如 status、order_status" placement="top">
|
|
|
+ <span class="rule-field-hint">(?)</span>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ <el-input v-model="timeoutForm.statusField" placeholder="如 status" />
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="completedStates(完成状态,逗号分隔)">
|
|
|
+ <!-- B-04: completedStates risk tip -->
|
|
|
+ <div class="rule-tip">
|
|
|
+ 注意:<strong>完成状态值</strong>必须与数据库中实际值完全匹配(区分大小写)。拼写错误将导致规则持续命中,产生大量误报异常。
|
|
|
+ </div>
|
|
|
+ <el-form-item>
|
|
|
+ <template #label>
|
|
|
+ 完成状态值(逗号分隔)
|
|
|
+ <el-tooltip content="填写表示「已完成」的状态枚举值,多个用逗号分隔。须与数据库实际存储值完全一致(区分大小写)" placement="top">
|
|
|
+ <span class="rule-field-hint">(?)</span>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
<el-input v-model="timeoutForm.completedStates" placeholder="如 DONE,CLOSED" />
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="objectCodeField">
|
|
|
- <el-input v-model="timeoutForm.objectCodeField" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="objectIdField">
|
|
|
- <el-input v-model="timeoutForm.objectIdField" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="graceMinutes(宽限分钟,≥0)">
|
|
|
+ <el-form-item label="宽限分钟(≥0)">
|
|
|
<el-input-number v-model="timeoutForm.graceMinutes" :min="0" />
|
|
|
+ <span class="rule-hint">超过到期时间后再等待 N 分钟才触发,0 表示立即触发</span>
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="exceptionTypeCode">
|
|
|
- <el-select v-model="timeoutForm.exceptionTypeCode" filterable placeholder="选择异常类型">
|
|
|
+ <el-form-item label="异常类型">
|
|
|
+ <el-select v-model="timeoutForm.exceptionTypeCode" filterable clearable placeholder="选择异常类型" style="width: 100%">
|
|
|
<el-option v-for="o in exceptionTypeOptions" :key="o.value" :label="o.label" :value="o.value" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
+ <!-- B-05: advanced fields collapse -->
|
|
|
+ <div class="rule-advanced-toggle" @click="advancedExpanded = !advancedExpanded">
|
|
|
+ {{ advancedExpanded ? '▲ 收起高级字段' : '▼ 展开高级字段(对象编码/ID 映射)' }}
|
|
|
+ </div>
|
|
|
+ <template v-if="advancedExpanded">
|
|
|
+ <el-form-item>
|
|
|
+ <template #label>
|
|
|
+ 对象编码字段名
|
|
|
+ <el-tooltip content="SQL 结果集中用于关联业务对象编码的列名,通常无需修改" placement="top">
|
|
|
+ <span class="rule-field-hint">(?)</span>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ <el-input v-model="timeoutForm.objectCodeField" placeholder="如 order_code" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <template #label>
|
|
|
+ 对象 ID 字段名
|
|
|
+ <el-tooltip content="SQL 结果集中用于关联业务对象主键的列名,通常无需修改" placement="top">
|
|
|
+ <span class="rule-field-hint">(?)</span>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ <el-input v-model="timeoutForm.objectIdField" placeholder="如 order_id" />
|
|
|
+ </el-form-item>
|
|
|
+ </template>
|
|
|
</el-form>
|
|
|
|
|
|
+ <!-- SHORTAGE form -->
|
|
|
<el-form v-else-if="ruleTypeOf(editingRow) === 'SHORTAGE'" label-position="top" size="small">
|
|
|
- <el-form-item label="targetQtyField">
|
|
|
- <el-input v-model="shortageForm.targetQtyField" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="actualQtyField">
|
|
|
- <el-input v-model="shortageForm.actualQtyField" />
|
|
|
+ <!-- B-02: AND logic -->
|
|
|
+ <el-alert type="info" :closable="false" show-icon title="触发逻辑说明">
|
|
|
+ <template #default>
|
|
|
+ 缺料触发为 <strong>AND 逻辑</strong>:实际缺口必须同时超过绝对容差 <em>且</em> 超过比例容差,才触发异常;任一未超过则不触发。
|
|
|
+ </template>
|
|
|
+ </el-alert>
|
|
|
+ <el-form-item>
|
|
|
+ <template #label>
|
|
|
+ 目标数量字段名
|
|
|
+ <el-tooltip content="填写 SQL 结果集中表示计划/目标数量的列名,如 plan_qty、target_quantity" placement="top">
|
|
|
+ <span class="rule-field-hint">(?)</span>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ <el-input v-model="shortageForm.targetQtyField" placeholder="如 plan_qty" />
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="objectCodeField">
|
|
|
- <el-input v-model="shortageForm.objectCodeField" />
|
|
|
+ <el-form-item>
|
|
|
+ <template #label>
|
|
|
+ 实际数量字段名
|
|
|
+ <el-tooltip content="填写 SQL 结果集中表示实际/到货数量的列名,如 actual_qty、received_qty" placement="top">
|
|
|
+ <span class="rule-field-hint">(?)</span>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ <el-input v-model="shortageForm.actualQtyField" placeholder="如 actual_qty" />
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="objectIdField">
|
|
|
- <el-input v-model="shortageForm.objectIdField" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="toleranceAbs(绝对容差,≥0)">
|
|
|
+ <el-form-item label="绝对容差(≥0)">
|
|
|
<el-input-number v-model="shortageForm.toleranceAbs" :min="0" :step="0.1" />
|
|
|
+ <span class="rule-hint">缺口不超过此值时不触发</span>
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="toleranceRatio(比例容差,0–1)">
|
|
|
- <el-input-number v-model="shortageForm.toleranceRatio" :min="0" :max="1" :step="0.01" />
|
|
|
+ <el-form-item label="比例容差(0–100%)">
|
|
|
+ <el-input-number v-model="shortageForm.toleranceRatioPct" :min="0" :max="100" :step="1" />
|
|
|
+ <span class="rule-hint">% 缺口占目标数量的比例不超过此值时不触发;两项均须超过才触发</span>
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="exceptionTypeCode">
|
|
|
- <el-select v-model="shortageForm.exceptionTypeCode" filterable placeholder="选择异常类型">
|
|
|
+ <el-form-item label="异常类型">
|
|
|
+ <el-select v-model="shortageForm.exceptionTypeCode" filterable clearable placeholder="选择异常类型" style="width: 100%">
|
|
|
<el-option v-for="o in exceptionTypeOptions" :key="o.value" :label="o.label" :value="o.value" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
+ <!-- B-05: advanced fields collapse -->
|
|
|
+ <div class="rule-advanced-toggle" @click="advancedExpanded = !advancedExpanded">
|
|
|
+ {{ advancedExpanded ? '▲ 收起高级字段' : '▼ 展开高级字段(对象编码/ID 映射)' }}
|
|
|
+ </div>
|
|
|
+ <template v-if="advancedExpanded">
|
|
|
+ <el-form-item>
|
|
|
+ <template #label>
|
|
|
+ 对象编码字段名
|
|
|
+ <el-tooltip content="SQL 结果集中用于关联业务对象编码的列名,通常无需修改" placement="top">
|
|
|
+ <span class="rule-field-hint">(?)</span>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ <el-input v-model="shortageForm.objectCodeField" placeholder="如 order_code" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <template #label>
|
|
|
+ 对象 ID 字段名
|
|
|
+ <el-tooltip content="SQL 结果集中用于关联业务对象主键的列名,通常无需修改" placement="top">
|
|
|
+ <span class="rule-field-hint">(?)</span>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ <el-input v-model="shortageForm.objectIdField" placeholder="如 order_id" />
|
|
|
+ </el-form-item>
|
|
|
+ </template>
|
|
|
</el-form>
|
|
|
|
|
|
+ <!-- OUT_OF_RANGE form -->
|
|
|
<el-form v-else-if="ruleTypeOf(editingRow) === 'OUT_OF_RANGE'" label-position="top" size="small">
|
|
|
- <el-form-item label="measuredValueField">
|
|
|
- <el-input v-model="outOfRangeForm.measuredValueField" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="objectCodeField">
|
|
|
- <el-input v-model="outOfRangeForm.objectCodeField" />
|
|
|
+ <!-- B-02: AND logic -->
|
|
|
+ <el-alert type="info" :closable="false" show-icon title="触发逻辑说明">
|
|
|
+ <template #default>
|
|
|
+ 超限触发为 <strong>AND 逻辑</strong>:超限幅度必须同时超过绝对容差 <em>且</em> 超过比例容差,才触发异常;任一未超过则不触发。
|
|
|
+ </template>
|
|
|
+ </el-alert>
|
|
|
+ <!-- B-03: bound priority -->
|
|
|
+ <el-alert type="warning" :closable="false" show-icon title="上下限优先级">
|
|
|
+ <template #default>
|
|
|
+ <strong>行内字段优先</strong>:若填写了行内上/下限字段名,则优先取该行对应列的值作为限值;固定上下限仅在字段名为空时生效。
|
|
|
+ </template>
|
|
|
+ </el-alert>
|
|
|
+ <el-form-item>
|
|
|
+ <template #label>
|
|
|
+ 测量值字段名
|
|
|
+ <el-tooltip content="填写 SQL 结果集中表示被监测数值的列名,如 measured_value、sensor_reading" placement="top">
|
|
|
+ <span class="rule-field-hint">(?)</span>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ <el-input v-model="outOfRangeForm.measuredValueField" placeholder="如 measured_value" />
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="objectIdField">
|
|
|
- <el-input v-model="outOfRangeForm.objectIdField" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="upperBound(固定上限,可空)">
|
|
|
+ <el-form-item label="固定上限(可空)">
|
|
|
<el-input-number v-model="outOfRangeForm.upperBound" :step="0.1" />
|
|
|
+ <span class="rule-hint">行内上限字段不为空时此值不生效</span>
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="lowerBound(固定下限,可空)">
|
|
|
+ <el-form-item label="固定下限(可空)">
|
|
|
<el-input-number v-model="outOfRangeForm.lowerBound" :step="0.1" />
|
|
|
+ <span class="rule-hint">行内下限字段不为空时此值不生效</span>
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="upperBoundField(行内上限字段,可空)">
|
|
|
- <el-input v-model="outOfRangeForm.upperBoundField" />
|
|
|
+ <el-form-item>
|
|
|
+ <template #label>
|
|
|
+ 行内上限字段名(可空)
|
|
|
+ <el-tooltip content="若每行有自己的上限列,填写该列名,优先级高于固定上限" placement="top">
|
|
|
+ <span class="rule-field-hint">(?)</span>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ <el-input v-model="outOfRangeForm.upperBoundField" placeholder="如 upper_limit_col" />
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="lowerBoundField(行内下限字段,可空)">
|
|
|
- <el-input v-model="outOfRangeForm.lowerBoundField" />
|
|
|
+ <el-form-item>
|
|
|
+ <template #label>
|
|
|
+ 行内下限字段名(可空)
|
|
|
+ <el-tooltip content="若每行有自己的下限列,填写该列名,优先级高于固定下限" placement="top">
|
|
|
+ <span class="rule-field-hint">(?)</span>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ <el-input v-model="outOfRangeForm.lowerBoundField" placeholder="如 lower_limit_col" />
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="toleranceAbs(绝对容差,≥0)">
|
|
|
+ <el-form-item label="绝对容差(≥0)">
|
|
|
<el-input-number v-model="outOfRangeForm.toleranceAbs" :min="0" :step="0.1" />
|
|
|
+ <span class="rule-hint">超限幅度不超过此值时不触发</span>
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="toleranceRatio(比例容差,0–1)">
|
|
|
- <el-input-number v-model="outOfRangeForm.toleranceRatio" :min="0" :max="1" :step="0.01" />
|
|
|
+ <el-form-item label="比例容差(0–100%)">
|
|
|
+ <el-input-number v-model="outOfRangeForm.toleranceRatioPct" :min="0" :max="100" :step="1" />
|
|
|
+ <span class="rule-hint">% 超限幅度占限值的比例不超过此值时不触发;两项均须超过才触发</span>
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="exceptionTypeCode">
|
|
|
- <el-select v-model="outOfRangeForm.exceptionTypeCode" filterable placeholder="选择异常类型">
|
|
|
+ <el-form-item label="异常类型">
|
|
|
+ <el-select v-model="outOfRangeForm.exceptionTypeCode" filterable clearable placeholder="选择异常类型" style="width: 100%">
|
|
|
<el-option v-for="o in exceptionTypeOptions" :key="o.value" :label="o.label" :value="o.value" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
+ <!-- B-05: advanced fields collapse -->
|
|
|
+ <div class="rule-advanced-toggle" @click="advancedExpanded = !advancedExpanded">
|
|
|
+ {{ advancedExpanded ? '▲ 收起高级字段' : '▼ 展开高级字段(对象编码/ID 映射)' }}
|
|
|
+ </div>
|
|
|
+ <template v-if="advancedExpanded">
|
|
|
+ <el-form-item>
|
|
|
+ <template #label>
|
|
|
+ 对象编码字段名
|
|
|
+ <el-tooltip content="SQL 结果集中用于关联业务对象编码的列名,通常无需修改" placement="top">
|
|
|
+ <span class="rule-field-hint">(?)</span>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ <el-input v-model="outOfRangeForm.objectCodeField" placeholder="如 device_code" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <template #label>
|
|
|
+ 对象 ID 字段名
|
|
|
+ <el-tooltip content="SQL 结果集中用于关联业务对象主键的列名,通常无需修改" placement="top">
|
|
|
+ <span class="rule-field-hint">(?)</span>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ <el-input v-model="outOfRangeForm.objectIdField" placeholder="如 device_id" />
|
|
|
+ </el-form-item>
|
|
|
+ </template>
|
|
|
</el-form>
|
|
|
|
|
|
<el-alert v-else type="warning" :closable="false" show-icon>
|
|
|
@@ -731,18 +1039,21 @@ onDeactivated(() => stopAutoRefresh());
|
|
|
<pre class="rule-raw">{{ rawParamsJson || '(空)' }}</pre>
|
|
|
</el-alert>
|
|
|
|
|
|
- <el-divider content-position="left">调度设置</el-divider>
|
|
|
+ <el-divider content-position="left">检测 SQL(只读)</el-divider>
|
|
|
+ <pre class="rule-expr">{{ editingRow.expression || '—' }}</pre>
|
|
|
+
|
|
|
+ <el-divider content-position="left">检测调度</el-divider>
|
|
|
<el-form label-position="top" size="small">
|
|
|
- <el-form-item label="轮询间隔(poll_interval_seconds,60–86400 秒)">
|
|
|
+ <el-form-item label="检测频率(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-form-item label="持续异常次数(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-form-item label="持续正常次数(1–10)">
|
|
|
<el-input-number v-model="scheduleForm.recoverCountRequired" :min="1" :max="10" />
|
|
|
<span class="rule-hint">连续未命中 N 次后才标 recovered</span>
|
|
|
</el-form-item>
|
|
|
@@ -771,10 +1082,30 @@ onDeactivated(() => stopAutoRefresh());
|
|
|
.rule-meta {
|
|
|
margin-bottom: 12px;
|
|
|
}
|
|
|
+.rule-summary {
|
|
|
+ background: var(--el-color-primary-light-9);
|
|
|
+ border-left: 3px solid var(--el-color-primary);
|
|
|
+ padding: 8px 12px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 13px;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ line-height: 1.6;
|
|
|
+}
|
|
|
+.rule-summary--incomplete {
|
|
|
+ background: var(--el-color-warning-light-9);
|
|
|
+ border-left-color: var(--el-color-warning);
|
|
|
+}
|
|
|
+.rule-summary__label {
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+}
|
|
|
.rule-expr {
|
|
|
white-space: pre-wrap;
|
|
|
word-break: break-all;
|
|
|
- margin: 0;
|
|
|
+ margin: 0 0 8px;
|
|
|
+ padding: 8px;
|
|
|
+ background: var(--el-fill-color-light);
|
|
|
+ border-radius: 4px;
|
|
|
font-family: var(--el-font-family-mono, monospace);
|
|
|
font-size: 12px;
|
|
|
color: var(--el-text-color-secondary);
|
|
|
@@ -799,6 +1130,7 @@ onDeactivated(() => stopAutoRefresh());
|
|
|
align-items: center;
|
|
|
gap: 12px;
|
|
|
margin-bottom: 8px;
|
|
|
+ flex-wrap: wrap;
|
|
|
}
|
|
|
.rule-toolbar__hint {
|
|
|
font-size: 12px;
|
|
|
@@ -809,8 +1141,34 @@ onDeactivated(() => stopAutoRefresh());
|
|
|
color: var(--el-text-color-secondary);
|
|
|
margin-left: 8px;
|
|
|
}
|
|
|
+.rule-field-hint {
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--el-color-primary);
|
|
|
+ cursor: pointer;
|
|
|
+ margin-left: 4px;
|
|
|
+}
|
|
|
.rule-error {
|
|
|
color: var(--el-color-danger);
|
|
|
font-size: 12px;
|
|
|
}
|
|
|
+.rule-tip {
|
|
|
+ background: var(--el-color-warning-light-9);
|
|
|
+ border: 1px solid var(--el-color-warning-light-5);
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 8px 12px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--el-text-color-regular);
|
|
|
+ margin-bottom: 12px;
|
|
|
+ line-height: 1.6;
|
|
|
+}
|
|
|
+.rule-advanced-toggle {
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--el-color-primary);
|
|
|
+ cursor: pointer;
|
|
|
+ margin: 8px 0 4px;
|
|
|
+ user-select: none;
|
|
|
+}
|
|
|
+.rule-advanced-toggle:hover {
|
|
|
+ text-decoration: underline;
|
|
|
+}
|
|
|
</style>
|