fix: 模型/告警数据持久化恢复(localStorage 与旧版同 key 自动迁移)+ 全部分页 proxy 兜底防空表
This commit is contained in:
@@ -1,4 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 告警确认(issue #173 [B7] 重构;#D2 样式对齐;2026-08-06 数据回退修复)
|
||||
*
|
||||
* 对齐旧版 web/admin/admin.js:告警状态机持久化在 localStorage
|
||||
* (key 与旧版相同:iaop.admin.alerts.v1),确认操作写回,
|
||||
* 同源下旧版页面的确认记录自动迁移可见。
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, VbenButton } from '@vben/common-ui';
|
||||
@@ -7,11 +14,42 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { ask } from '#/api/iaop/infer';
|
||||
|
||||
const alerts = ref([
|
||||
{ id: 1, severity: 'P0', title: '还原炉 TiCl₄ 纯度连续 3 点低于 99.9%', device: 'RF-01', status: 'pending' },
|
||||
{ id: 2, severity: 'P1', title: '氯化炉炉压偏离设定 ±50Pa', device: 'CLF-01', status: 'pending' },
|
||||
{ id: 3, severity: 'P2', title: '蒸馏塔液位波动增大', device: 'D-03', status: 'acked' },
|
||||
]);
|
||||
interface AlertItem {
|
||||
id: number;
|
||||
severity: string;
|
||||
title: string;
|
||||
device: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const ALERTS_KEY = 'iaop.admin.alerts.v1'; // 与旧版 admin.js 相同,同源共享
|
||||
|
||||
function seedAlerts(): AlertItem[] {
|
||||
return [
|
||||
{ id: 1, severity: 'P0', title: '还原炉 TiCl₄ 纯度连续 3 点低于 99.9%', device: 'RF-01', status: 'pending' },
|
||||
{ id: 2, severity: 'P1', title: '氯化炉炉压偏离设定 ±50Pa', device: 'CLF-01', status: 'pending' },
|
||||
{ id: 3, severity: 'P2', title: '蒸馏塔液位波动增大', device: 'D-03', status: 'acked' },
|
||||
];
|
||||
}
|
||||
|
||||
function loadAlerts(): AlertItem[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(ALERTS_KEY);
|
||||
if (raw) {
|
||||
const arr = JSON.parse(raw);
|
||||
if (Array.isArray(arr) && arr.length > 0) return arr;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
const seed = seedAlerts();
|
||||
saveAlerts(seed);
|
||||
return seed;
|
||||
}
|
||||
|
||||
function saveAlerts(list: AlertItem[]) {
|
||||
try { localStorage.setItem(ALERTS_KEY, JSON.stringify(list)); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const alerts = ref<AlertItem[]>(loadAlerts());
|
||||
const sevColor: Record<string, string> = { P0: 'red', P1: 'orange', P2: 'blue' };
|
||||
const explain = ref('');
|
||||
|
||||
@@ -19,6 +57,7 @@ const gridOptions: VxeTableGridOptions = {
|
||||
rowConfig: { keyField: 'id' },
|
||||
height: 'auto',
|
||||
stripe: true,
|
||||
pagerConfig: { enabled: true, pageSize: 10 },
|
||||
columns: [
|
||||
{ title: '级别', field: 'severity', width: 90, slots: { default: 'severity' } },
|
||||
{ title: '告警', field: 'title', minWidth: 260 },
|
||||
@@ -27,16 +66,20 @@ const gridOptions: VxeTableGridOptions = {
|
||||
{ title: '操作', field: 'action', width: 210, slots: { default: 'action' } },
|
||||
],
|
||||
proxy: {
|
||||
query: async ({ page }) => ({
|
||||
items: alerts.value.slice((page.currentPage - 1) * page.pageSize, page.currentPage * page.pageSize),
|
||||
total: alerts.value.length,
|
||||
}),
|
||||
query: async ({ page } = {}) => {
|
||||
const cur = page?.currentPage ?? 1;
|
||||
const size = page?.pageSize ?? alerts.value.length || 10;
|
||||
return {
|
||||
items: alerts.value.slice((cur - 1) * size, cur * size),
|
||||
total: alerts.value.length,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid] = useVbenVxeGrid({ gridOptions });
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
async function explainAlert(a: { title: string }) {
|
||||
async function explainAlert(a: AlertItem) {
|
||||
try {
|
||||
const r = await ask(`请解释告警并给出处置建议:${a.title}`, { maxTokens: 200 });
|
||||
explain.value = r.text;
|
||||
@@ -44,8 +87,11 @@ async function explainAlert(a: { title: string }) {
|
||||
explain.value = '推理服务不可用,无法生成解释。';
|
||||
}
|
||||
}
|
||||
function ack(a: { status: string }) {
|
||||
|
||||
function ack(a: AlertItem) {
|
||||
a.status = 'acked';
|
||||
saveAlerts(alerts.value); // 旧版行为:确认状态落盘
|
||||
gridApi.query();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -63,6 +109,6 @@ function ack(a: { status: string }) {
|
||||
<VbenButton size="small" variant="solid" :disabled="row.status === 'acked'" @click="ack(row)">确认</VbenButton>
|
||||
</template>
|
||||
</Grid>
|
||||
<a-alert v-if="explain" type="info" style="margin-top: 12px" :message="explain" />
|
||||
<a-alert v-if="explain" type="info" class="mt-3" :message="explain" />
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@@ -31,11 +31,14 @@ const gridOptions: VxeTableGridOptions = {
|
||||
{ title: '结果', field: 'result', width: 90, slots: { default: 'result' } },
|
||||
{ title: '详情', field: 'detail', minWidth: 240 },
|
||||
],
|
||||
pagerConfig: { enabled: true, pageSize: 10 },
|
||||
proxy: {
|
||||
query: async ({ page }) => {
|
||||
query: async ({ page } = {}) => {
|
||||
const cur = page?.currentPage ?? 1;
|
||||
const size = page?.pageSize ?? 10;
|
||||
const rows = filtered();
|
||||
return {
|
||||
items: rows.slice((page.currentPage - 1) * page.pageSize, page.currentPage * page.pageSize),
|
||||
items: rows.slice((cur - 1) * size, cur * size),
|
||||
total: rows.length,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -17,11 +17,16 @@ const gridOptions: VxeTableGridOptions = {
|
||||
{ title: '章节数', field: 'sections', width: 120 },
|
||||
{ title: '索引状态', field: 'indexed', width: 140, slots: { default: 'indexed' } },
|
||||
],
|
||||
pagerConfig: { enabled: true, pageSize: 10 },
|
||||
proxy: {
|
||||
query: async ({ page }) => ({
|
||||
items: docs.value.slice((page.currentPage - 1) * page.pageSize, page.currentPage * page.pageSize),
|
||||
total: docs.value.length,
|
||||
}),
|
||||
query: async ({ page } = {}) => {
|
||||
const cur = page?.currentPage ?? 1;
|
||||
const size = page?.pageSize ?? docs.value.length || 10;
|
||||
return {
|
||||
items: docs.value.slice((cur - 1) * size, cur * size),
|
||||
total: docs.value.length,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 模型管理(issue #172 [B6] 重构;#D2 样式对齐;2026-08-06 数据回退修复)
|
||||
*
|
||||
* 数据语义对齐旧版 web/admin/admin.js:
|
||||
* - 模型注册表持久化在 localStorage(key 与旧版相同:iaop.admin.models.v1),
|
||||
* 同源(39.101.182.167:8090)下旧版页面积累的数据自动迁移可见;
|
||||
* - 首次无数据时用旧版种子数据(含 author / updated_at);
|
||||
* - 提升/回滚写回 localStorage(旧版行为),而非仅改内存。
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, VbenButton } from '@vben/common-ui';
|
||||
@@ -6,42 +15,87 @@ import { Page, VbenButton } from '@vben/common-ui';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
const models = ref([
|
||||
{ name: 'quality_forecast', version: 'v2.1.0', stage: 'prod', desc: 'TiCl4 纯度预测(GBDT)' },
|
||||
{ name: 'quality_forecast', version: 'v2.2.0', stage: 'staging', desc: 'TiCl4 纯度预测(DNN)' },
|
||||
{ name: 'anomaly_detection', version: 'v1.4.0', stage: 'prod', desc: '炉温异常检测(iForest)' },
|
||||
{ name: 'cross_process_optimizer', version: 'v1.1.0', stage: 'dev', desc: '跨工序寻优' },
|
||||
]);
|
||||
interface ModelItem {
|
||||
name: string;
|
||||
version: string;
|
||||
stage: string;
|
||||
author: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
const MODELS_KEY = 'iaop.admin.models.v1'; // 与旧版 admin.js 相同,同源共享
|
||||
|
||||
function seedModels(): ModelItem[] {
|
||||
return [
|
||||
{ name: 'quality_forecast', version: '1.2.0', stage: 'prod', author: 'bot_dev1', updated_at: '2026-08-01T09:00:00Z' },
|
||||
{ name: 'anomaly_detection', version: '1.0.3', stage: 'staging', author: 'bot_dev1', updated_at: '2026-08-02T09:00:00Z' },
|
||||
{ name: 'process_optimizer', version: '0.9.1', stage: 'dev', author: 'engineer', updated_at: '2026-08-03T09:00:00Z' },
|
||||
{ name: 'cross_process_optimizer', version: '0.5.0', stage: 'dev', author: 'engineer', updated_at: '2026-08-04T09:00:00Z' },
|
||||
];
|
||||
}
|
||||
|
||||
function loadModels(): ModelItem[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(MODELS_KEY);
|
||||
if (raw) {
|
||||
const arr = JSON.parse(raw);
|
||||
if (Array.isArray(arr) && arr.length > 0) return arr;
|
||||
}
|
||||
} catch { /* 隐私模式等忽略 */ }
|
||||
const seed = seedModels();
|
||||
saveModels(seed);
|
||||
return seed;
|
||||
}
|
||||
|
||||
function saveModels(list: ModelItem[]) {
|
||||
try { localStorage.setItem(MODELS_KEY, JSON.stringify(list)); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const models = ref<ModelItem[]>(loadModels());
|
||||
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
rowConfig: { keyField: 'name' },
|
||||
height: 'auto',
|
||||
stripe: true,
|
||||
pagerConfig: { enabled: true, pageSize: 10 },
|
||||
columns: [
|
||||
{ title: '模型', field: 'name', minWidth: 180 },
|
||||
{ title: '版本', field: 'version', width: 120 },
|
||||
{ title: '阶段', field: 'stage', width: 110, slots: { default: 'stage' } },
|
||||
{ title: '说明', field: 'desc', minWidth: 220 },
|
||||
{ title: '版本', field: 'version', width: 110 },
|
||||
{ title: '阶段', field: 'stage', width: 100, slots: { default: 'stage' } },
|
||||
{ title: '作者', field: 'author', width: 110 },
|
||||
{ title: '更新时间', field: 'updated_at', width: 130, slots: { default: 'updated' } },
|
||||
{ title: '操作', field: 'action', width: 170, slots: { default: 'action' } },
|
||||
],
|
||||
proxy: {
|
||||
query: async ({ page }) => ({
|
||||
items: models.value.slice((page.currentPage - 1) * page.pageSize, page.currentPage * page.pageSize),
|
||||
total: models.value.length,
|
||||
}),
|
||||
query: async ({ page } = {}) => {
|
||||
// page 可能缺省(分页未就绪时的首次查询),兜底返回全量
|
||||
const cur = page?.currentPage ?? 1;
|
||||
const size = page?.pageSize ?? models.value.length || 10;
|
||||
return {
|
||||
items: models.value.slice((cur - 1) * size, cur * size),
|
||||
total: models.value.length,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
function promote(m: { stage: string }) {
|
||||
if (m.stage === 'staging') m.stage = 'prod';
|
||||
else if (m.stage === 'dev') m.stage = 'staging';
|
||||
function touch(m: ModelItem) {
|
||||
m.updated_at = new Date().toISOString();
|
||||
saveModels(models.value);
|
||||
gridApi.query();
|
||||
}
|
||||
function rollback(m: { stage: string }) {
|
||||
|
||||
function promote(m: ModelItem) {
|
||||
if (m.stage === 'dev') m.stage = 'staging';
|
||||
else if (m.stage === 'staging') m.stage = 'prod';
|
||||
touch(m);
|
||||
}
|
||||
function rollback(m: ModelItem) {
|
||||
if (m.stage === 'prod') m.stage = 'staging';
|
||||
gridApi.query();
|
||||
else if (m.stage === 'staging') m.stage = 'dev';
|
||||
touch(m);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -51,9 +105,12 @@ function rollback(m: { stage: string }) {
|
||||
<template #stage="{ row }">
|
||||
<a-tag :color="row.stage === 'prod' ? 'green' : row.stage === 'staging' ? 'orange' : 'blue'">{{ row.stage }}</a-tag>
|
||||
</template>
|
||||
<template #updated="{ row }">
|
||||
{{ (row.updated_at || '').slice(0, 10) }}
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<VbenButton size="small" :disabled="row.stage === 'prod'" @click="promote(row)">提升</VbenButton>
|
||||
<VbenButton size="small" variant="outline" :disabled="row.stage !== 'prod'" @click="rollback(row)">回滚</VbenButton>
|
||||
<VbenButton size="small" variant="outline" :disabled="row.stage === 'dev'" @click="rollback(row)">回滚</VbenButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
@@ -18,11 +18,16 @@ const gridOptions: VxeTableGridOptions<Record<string, string>> = {
|
||||
height: 'auto',
|
||||
stripe: true,
|
||||
columns: HEADERS.map((h) => ({ title: h, field: h, minWidth: 90 })),
|
||||
pagerConfig: { enabled: true, pageSize: 10 },
|
||||
proxy: {
|
||||
query: async ({ page }) => ({
|
||||
items: preview.value.slice((page.currentPage - 1) * page.pageSize, page.currentPage * page.pageSize),
|
||||
total: preview.value.length,
|
||||
}),
|
||||
query: async ({ page } = {}) => {
|
||||
const cur = page?.currentPage ?? 1;
|
||||
const size = page?.pageSize ?? 10;
|
||||
return {
|
||||
items: preview.value.slice((cur - 1) * size, cur * size),
|
||||
total: preview.value.length,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -24,11 +24,16 @@ const gridOptions: VxeTableGridOptions = {
|
||||
{ title: '作者', field: 'author', width: 110 },
|
||||
{ title: '说明', field: 'note', minWidth: 200 },
|
||||
],
|
||||
pagerConfig: { enabled: true, pageSize: 10 },
|
||||
proxy: {
|
||||
query: async ({ page }) => ({
|
||||
items: rows.value.slice((page.currentPage - 1) * page.pageSize, page.currentPage * page.pageSize),
|
||||
total: rows.value.length,
|
||||
}),
|
||||
query: async ({ page } = {}) => {
|
||||
const cur = page?.currentPage ?? 1;
|
||||
const size = page?.pageSize ?? 10;
|
||||
return {
|
||||
items: rows.value.slice((cur - 1) * size, cur * size),
|
||||
total: rows.value.length,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user