Browse Source
- Implemented the assessment report feature in the model training module. - Added API endpoints for initializing, evaluating, and saving assessment reports. - Created a new Vue component for displaying assessment reports with detailed results. - Enhanced data handling for model information and assessment results, including normalization and error handling. - Introduced user interface elements for managing assessment parameters and displaying results. - Added navigation to the assessment report from the model training view.pull/60/head
7 changed files with 1246 additions and 6 deletions
@ -0,0 +1,58 @@ |
|||||
|
import type { |
||||
|
AssessCleanSummary, |
||||
|
AssessEvaluateRequest, |
||||
|
AssessEvaluateResult, |
||||
|
AssessInitQuery, |
||||
|
AssessInitResponse, |
||||
|
AssessReportDetail, |
||||
|
AssessReportSavePayload, |
||||
|
} from './model/assessReportModel' |
||||
|
import { defHttp } from '@/utils/http/axios' |
||||
|
|
||||
|
enum Api { |
||||
|
INIT = '/alert/assess-report/init', |
||||
|
REPORT = '/alert/assess-report/report', |
||||
|
EVALUATE = '/alert/assess-report/evaluate', |
||||
|
SAVE = '/alert/assess-report', |
||||
|
CLEAN_SUMMARY = '/alert/assess-report/clean/summary', |
||||
|
CLEAN_DEAD = '/alert/assess-report/clean/dead', |
||||
|
CLEAN_CONDITION_RATE = '/alert/assess-report/clean/condition-rate', |
||||
|
CLEAN_ENVELOPE_RATE = '/alert/assess-report/clean/envelope-rate', |
||||
|
CONDITION = '/alert/assess-report/condition', |
||||
|
} |
||||
|
|
||||
|
export function fetchAssessInit(params: AssessInitQuery) { |
||||
|
return defHttp.get<AssessInitResponse>({ url: Api.INIT, params }) |
||||
|
} |
||||
|
|
||||
|
export function fetchReportDetail(id: number | string) { |
||||
|
return defHttp.get<AssessReportDetail>({ url: `${Api.REPORT}/${id}` }) |
||||
|
} |
||||
|
|
||||
|
export function evaluateAssess(data: AssessEvaluateRequest) { |
||||
|
return defHttp.post<AssessEvaluateResult>({ url: Api.EVALUATE, data }) |
||||
|
} |
||||
|
|
||||
|
export function saveAssessReport(data: AssessReportSavePayload) { |
||||
|
return defHttp.post<boolean>({ url: Api.SAVE, data }) |
||||
|
} |
||||
|
|
||||
|
export function fetchCleanSummary(params: { modelId: number | string; time: string; version: string }) { |
||||
|
return defHttp.get<AssessCleanSummary>({ url: Api.CLEAN_SUMMARY, params }) |
||||
|
} |
||||
|
|
||||
|
export function fetchDeadCleanDetail(params: { pointIds: string; time: string }) { |
||||
|
return defHttp.get<{ rate: number[]; result: string[] }>({ url: Api.CLEAN_DEAD, params }) |
||||
|
} |
||||
|
|
||||
|
export function fetchConditionRate(params: { condition: string; st: string; et: string }) { |
||||
|
return defHttp.get<number[]>({ url: Api.CLEAN_CONDITION_RATE, params }) |
||||
|
} |
||||
|
|
||||
|
export function fetchEnvelopeRate(params: { condition: string; st: string; et: string }) { |
||||
|
return defHttp.get<number[]>({ url: Api.CLEAN_ENVELOPE_RATE, params }) |
||||
|
} |
||||
|
|
||||
|
export function fetchAssessCondition(algorithm: string) { |
||||
|
return defHttp.get<{ modelScore?: number; condition?: string }>({ url: Api.CONDITION, params: { algorithm } }) |
||||
|
} |
||||
@ -0,0 +1,123 @@ |
|||||
|
export interface AssessInitQuery { |
||||
|
modelId: number | string |
||||
|
version?: string |
||||
|
reportId?: number | string |
||||
|
sampleRange?: string[] |
||||
|
} |
||||
|
|
||||
|
export interface ModeRow { |
||||
|
name: string |
||||
|
content: string | number |
||||
|
} |
||||
|
|
||||
|
export interface AssessPointRow { |
||||
|
description: string |
||||
|
pointId: string |
||||
|
alarm?: boolean |
||||
|
lock?: boolean |
||||
|
dead?: boolean |
||||
|
limit?: boolean |
||||
|
lower?: number |
||||
|
upper?: number |
||||
|
unit?: string |
||||
|
tMin?: number |
||||
|
tMax?: number |
||||
|
amplitude?: number | string |
||||
|
index?: number |
||||
|
} |
||||
|
|
||||
|
export interface AssessRow extends AssessPointRow { |
||||
|
fdr?: string |
||||
|
far?: string |
||||
|
recon?: string |
||||
|
} |
||||
|
|
||||
|
export interface AssessResultRow { |
||||
|
name: string |
||||
|
content: string | number |
||||
|
} |
||||
|
|
||||
|
export interface AssessInitResponse { |
||||
|
modelName: string |
||||
|
modelInfo: Record<string, any> |
||||
|
modeRows: ModeRow[] |
||||
|
pointRows: AssessPointRow[] |
||||
|
assessRows: AssessRow[] |
||||
|
assessResult?: AssessResultRow[] |
||||
|
bottomScore?: number |
||||
|
coverage?: number |
||||
|
identifier?: string |
||||
|
timestamp?: string |
||||
|
} |
||||
|
|
||||
|
export interface AssessEvaluateRowResult { |
||||
|
pointId: string |
||||
|
index?: number |
||||
|
fdrForward: number |
||||
|
fdrReverse: number |
||||
|
farForward: number |
||||
|
farReverse: number |
||||
|
reconForward?: number |
||||
|
reconReverse?: number |
||||
|
} |
||||
|
|
||||
|
export interface AssessEvaluateRequest { |
||||
|
modelId: number | string |
||||
|
version: string |
||||
|
assessRows: AssessRow[] |
||||
|
pointRows: AssessPointRow[] |
||||
|
timeRange?: string[] |
||||
|
assessMode?: 'full' | 'single' |
||||
|
alg?: string |
||||
|
modelInfo?: string |
||||
|
rawJson?: string |
||||
|
} |
||||
|
|
||||
|
export interface AssessEvaluateResult { |
||||
|
points: AssessEvaluateRowResult[] |
||||
|
durationSeconds?: number |
||||
|
coverage?: number |
||||
|
cleanDurationSeconds?: number |
||||
|
} |
||||
|
|
||||
|
export interface AssessReportSavePayload { |
||||
|
modelId: number | string |
||||
|
version: string |
||||
|
report: Record<string, any> |
||||
|
score: number |
||||
|
coverage: number |
||||
|
valid: boolean |
||||
|
identifier: string |
||||
|
verifier?: string |
||||
|
timestamp: string |
||||
|
conditionName?: string |
||||
|
} |
||||
|
|
||||
|
export interface AssessReportDetail { |
||||
|
id: number |
||||
|
modelName: string |
||||
|
report: { |
||||
|
pointRows: AssessPointRow[] |
||||
|
modeRows: ModeRow[] |
||||
|
assessRows: AssessRow[] |
||||
|
assessResult: AssessResultRow[] |
||||
|
identifier?: string |
||||
|
time?: string |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export interface AssessCleanSummaryItem { |
||||
|
type: string |
||||
|
orgNum?: number |
||||
|
sampleNum?: number |
||||
|
sampleRate?: number |
||||
|
message?: string |
||||
|
} |
||||
|
|
||||
|
export interface AssessCleanSummary { |
||||
|
summary: AssessCleanSummaryItem[] |
||||
|
deadPoints?: AssessPointRow[] |
||||
|
conditionClauses?: string[] |
||||
|
envelopeClauses?: string[] |
||||
|
timeRange?: string[] |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
import type { AppRouteModule } from '@/router/types' |
||||
|
import { LAYOUT } from '@/router/constant' |
||||
|
|
||||
|
const assessReport: AppRouteModule = { |
||||
|
path: '/model', |
||||
|
name: 'ModelAssess', |
||||
|
component: LAYOUT, |
||||
|
redirect: '/model/assess-report', |
||||
|
meta: { |
||||
|
orderNo: 25, |
||||
|
icon: 'ant-design:file-protect-outlined', |
||||
|
title: '模型评估', |
||||
|
}, |
||||
|
children: [ |
||||
|
{ |
||||
|
path: 'assess-report/:id?', |
||||
|
name: 'AssessReport', |
||||
|
component: () => import('@/views/model/AssessReport.vue'), |
||||
|
meta: { |
||||
|
title: '评估报告', |
||||
|
currentActiveMenu: '/model', |
||||
|
hideMenu: true, |
||||
|
hideBreadcrumb: true, |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
} |
||||
|
|
||||
|
export default assessReport |
||||
@ -0,0 +1,846 @@ |
|||||
|
<script lang="ts"> |
||||
|
import type { Dayjs } from 'dayjs' |
||||
|
import dayjs from 'dayjs' |
||||
|
import { computed, defineComponent, h, onMounted, reactive, ref } from 'vue' |
||||
|
import { useRoute, useRouter } from 'vue-router' |
||||
|
import { |
||||
|
Button, |
||||
|
Card, |
||||
|
Col, |
||||
|
Descriptions, |
||||
|
Form, |
||||
|
Input, |
||||
|
InputNumber, |
||||
|
Modal, |
||||
|
RangePicker, |
||||
|
Row, |
||||
|
Space, |
||||
|
Switch, |
||||
|
Table, |
||||
|
Tag, |
||||
|
} from 'ant-design-vue' |
||||
|
import type { ColumnProps } from 'ant-design-vue/es/table' |
||||
|
import { |
||||
|
evaluateAssess, |
||||
|
fetchAssessCondition, |
||||
|
fetchCleanSummary, |
||||
|
fetchReportDetail, |
||||
|
saveAssessReport, |
||||
|
} from '@/api/alert/model/assessReport' |
||||
|
import type { |
||||
|
AssessCleanSummary, |
||||
|
AssessPointRow, |
||||
|
AssessResultRow, |
||||
|
AssessRow, |
||||
|
ModeRow, |
||||
|
} from '@/api/alert/model/model/assessReportModel' |
||||
|
import { PageWrapper } from '@/components/Page' |
||||
|
import { useMessage } from '@/hooks/web/useMessage' |
||||
|
import { useUserStore } from '@/store/modules/user' |
||||
|
import { modelInfoApi } from '@/api/alert/model/models' |
||||
|
|
||||
|
type RangeValue = [Dayjs, Dayjs] |
||||
|
|
||||
|
function defaultAssessResult(): AssessResultRow[] { |
||||
|
return [ |
||||
|
{ name: '得分', content: '--' }, |
||||
|
{ name: '工况覆盖率', content: '--' }, |
||||
|
{ name: '计算时长(s)', content: '--' }, |
||||
|
{ name: '是否投入运用', content: '--' }, |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
name: 'AssessReport', |
||||
|
components: { |
||||
|
PageWrapper, |
||||
|
ACard: Card, |
||||
|
AForm: Form, |
||||
|
AFormItem: Form.Item, |
||||
|
AInput: Input, |
||||
|
AInputNumber: InputNumber, |
||||
|
AButton: Button, |
||||
|
ASpace: Space, |
||||
|
ATable: Table, |
||||
|
ASwitch: Switch, |
||||
|
ADescriptions: Descriptions, |
||||
|
ADescriptionsItem: Descriptions.Item, |
||||
|
ARangePicker: RangePicker, |
||||
|
ATag: Tag, |
||||
|
ARow: Row, |
||||
|
ACol: Col, |
||||
|
AModal: Modal, |
||||
|
}, |
||||
|
setup() { |
||||
|
const userStore = useUserStore() |
||||
|
const route = useRoute() |
||||
|
const router = useRouter() |
||||
|
const { createMessage } = useMessage() |
||||
|
|
||||
|
const modelId = computed(() => route.params.id || route.query.id) |
||||
|
const reportId = computed(() => route.query.reportId) |
||||
|
const algorithm = computed(() => (route.query.algorithm as string) || '') |
||||
|
|
||||
|
const loading = ref(false) |
||||
|
const evaluateLoading = ref(false) |
||||
|
const submitLoading = ref(false) |
||||
|
const modelName = ref('') |
||||
|
const bottomScore = ref<number | undefined>() |
||||
|
const coverage = ref<number | undefined>() |
||||
|
const identifier = ref<string>( |
||||
|
userStore.getUserInfo?.username || userStore.getUserInfo?.nickName || userStore.getUserInfo?.userName || '', |
||||
|
) |
||||
|
const timestamp = ref<string>('') |
||||
|
const baseInfo = ref<Record<string, any>>({}) |
||||
|
const modeRows = ref<ModeRow[]>([]) |
||||
|
const pointRows = ref<AssessPointRow[]>([]) |
||||
|
const assessRows = ref<AssessRow[]>([]) |
||||
|
const assessResult = ref<AssessResultRow[]>(defaultAssessResult()) |
||||
|
const cleanSummary = ref<AssessCleanSummary>() |
||||
|
const cleanModalVisible = ref(false) |
||||
|
|
||||
|
const formModel = reactive<{ |
||||
|
version: string |
||||
|
timeRange?: RangeValue |
||||
|
description?: string |
||||
|
}>({ |
||||
|
version: ((route.query.version as string) || 'v-test').replace('%20', ' '), |
||||
|
timeRange: undefined, |
||||
|
description: '', |
||||
|
}) |
||||
|
|
||||
|
const biasForm = reactive({ |
||||
|
absolute: undefined as number | undefined, |
||||
|
relative: 0 as number | undefined, |
||||
|
percent: 15, |
||||
|
}) |
||||
|
|
||||
|
const normalizePointRow = (item: any, index: number): AssessPointRow => { |
||||
|
const tMax = item.TMax ?? item.tMax |
||||
|
const tMin = item.TMin ?? item.tMin |
||||
|
const locked = item.lock === true |
||||
|
return { |
||||
|
...item, |
||||
|
description: item.description ?? item.Description, |
||||
|
pointId: item.pointId ?? item.PointId, |
||||
|
index: item.index ?? index, |
||||
|
TMax: tMax, |
||||
|
tMax, |
||||
|
TMin: tMin, |
||||
|
tMin, |
||||
|
Lower: item.Lower ?? item.lower, |
||||
|
Upper: item.Upper ?? item.upper, |
||||
|
Unit: item.Unit ?? item.unit, |
||||
|
lock: locked, |
||||
|
// 锁定则不参与,否则默认参与预警 |
||||
|
alarm: !locked, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const buildAssessRow = (point: AssessPointRow): AssessRow => { |
||||
|
const tMax = point.TMax ?? point.tMax |
||||
|
const tMin = point.TMin ?? point.tMin |
||||
|
const amplitude |
||||
|
= point.amplitude |
||||
|
?? Number(((0.15 * ((Number(tMax) || 0) - (Number(tMin) || 0))) || 0).toFixed(2)) |
||||
|
return { |
||||
|
...point, |
||||
|
TMax: tMax, |
||||
|
tMax, |
||||
|
TMin: tMin, |
||||
|
tMin, |
||||
|
amplitude: Number.isNaN(amplitude) ? 0 : amplitude, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const hydrateAssessRow = (row: any, index: number): AssessRow => { |
||||
|
const point = pointRows.value.find(p => p.pointId === (row.pointId ?? row.PointId)) |
||||
|
const tMax = row.TMax ?? row.tMax ?? point?.TMax ?? point?.tMax |
||||
|
const tMin = row.TMin ?? row.tMin ?? point?.TMin ?? point?.tMin |
||||
|
const lower = row.Lower ?? row.lower ?? point?.Lower |
||||
|
const upper = row.Upper ?? row.upper ?? point?.Upper |
||||
|
const unit = row.Unit ?? row.unit ?? point?.Unit ?? point?.unit |
||||
|
const description = row.description ?? row.Description ?? point?.description ?? point?.Description |
||||
|
return buildAssessRow({ |
||||
|
...row, |
||||
|
index: row.index ?? index, |
||||
|
TMax: tMax, |
||||
|
tMax, |
||||
|
TMin: tMin, |
||||
|
tMin, |
||||
|
Lower: lower, |
||||
|
Upper: upper, |
||||
|
Unit: unit, |
||||
|
description, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const loadInit = async () => { |
||||
|
if (!modelId.value) { |
||||
|
createMessage.error('缺少模型ID') |
||||
|
return |
||||
|
} |
||||
|
loading.value = true |
||||
|
try { |
||||
|
if (reportId.value) { |
||||
|
const detail = await fetchReportDetail(reportId.value as string) |
||||
|
modelName.value = detail.modelName |
||||
|
modeRows.value = detail.report.modeRows || [] |
||||
|
pointRows.value = (detail.report.pointRows || []).map((item, index) => normalizePointRow(item, index)) |
||||
|
assessRows.value = (detail.report.assessRows || []).map((item, index) => hydrateAssessRow(item, index)) |
||||
|
assessResult.value = detail.report.assessResult || defaultAssessResult() |
||||
|
identifier.value = detail.report.identifier || identifier.value |
||||
|
timestamp.value = detail.report.time || '' |
||||
|
} |
||||
|
else { |
||||
|
const modelInfo = await modelInfoApi(modelId.value as string) |
||||
|
baseInfo.value = modelInfo || {} |
||||
|
modelName.value = modelInfo?.name || modelInfo?.Model_Name || '' |
||||
|
pointRows.value = (modelInfo?.pointInfo || []).map((item: any, index: number) => normalizePointRow(item, index)) |
||||
|
modeRows.value = [ |
||||
|
{ name: '主元个数', content: modelInfo?.principal ?? '' }, |
||||
|
{ name: '运行模式', content: modelInfo?.alarmmodelset?.alarmname || '全工况运行' }, |
||||
|
{ name: '模式编码', content: modelInfo?.alarmmodelset?.alarmcondition || '1=1' }, |
||||
|
{ name: '样本类型', content: '训练样本' }, |
||||
|
{ name: '样本数量', content: modelInfo?.trainTime?.length || '--' }, |
||||
|
] |
||||
|
assessRows.value |
||||
|
= pointRows.value |
||||
|
.filter(item => item.alarm) |
||||
|
.map((item, index) => buildAssessRow({ ...item, index })) || [] |
||||
|
assessResult.value = defaultAssessResult() |
||||
|
bottomScore.value = modelInfo?.model_score |
||||
|
coverage.value = modelInfo?.load_coverage |
||||
|
timestamp.value = dayjs().format('YYYY-MM-DD HH:mm:ss') |
||||
|
if (!identifier.value) { |
||||
|
identifier.value = userStore.getUserInfo?.username |
||||
|
|| userStore.getUserInfo?.nickName |
||||
|
|| userStore.getUserInfo?.userName |
||||
|
|| '' |
||||
|
} |
||||
|
} |
||||
|
if (algorithm.value) { |
||||
|
const condition = await fetchAssessCondition(algorithm.value) |
||||
|
if (condition?.modelScore !== undefined && condition.modelScore !== null) |
||||
|
bottomScore.value = Number(condition.modelScore) |
||||
|
} |
||||
|
} |
||||
|
catch (error) { |
||||
|
createMessage.error('评估信息加载失败') |
||||
|
} |
||||
|
finally { |
||||
|
loading.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const toggleAlarm = (row: AssessPointRow, checked: boolean) => { |
||||
|
row.alarm = checked |
||||
|
assessRows.value = assessRows.value.filter(item => item.pointId !== row.pointId) |
||||
|
if (checked) |
||||
|
assessRows.value.push(buildAssessRow(row)) |
||||
|
} |
||||
|
|
||||
|
const applyRound = () => { |
||||
|
assessRows.value = assessRows.value.map(item => ({ |
||||
|
...item, |
||||
|
amplitude: Math.round(Number(item.amplitude) || 0), |
||||
|
})) |
||||
|
} |
||||
|
|
||||
|
const applyAbsoluteBias = () => { |
||||
|
if (biasForm.absolute === undefined) |
||||
|
return |
||||
|
assessRows.value = assessRows.value.map(item => ({ |
||||
|
...item, |
||||
|
amplitude: biasForm.absolute, |
||||
|
})) |
||||
|
} |
||||
|
|
||||
|
const applyRelativeBias = () => { |
||||
|
if (biasForm.relative === undefined) |
||||
|
return |
||||
|
assessRows.value = assessRows.value.map(item => ({ |
||||
|
...item, |
||||
|
amplitude: Number((Number(item.amplitude || 0) + biasForm.relative!).toFixed(2)), |
||||
|
})) |
||||
|
} |
||||
|
|
||||
|
const applyPercentBias = () => { |
||||
|
assessRows.value = assessRows.value.map((item) => { |
||||
|
const point = pointRows.value.find(p => p.pointId === item.pointId) |
||||
|
const delta = point ? Number(point.TMax || point.tMax || 0) - Number(point.TMin || point.tMin || 0) : 0 |
||||
|
const amplitude = Number(((biasForm.percent / 100) * delta).toFixed(2)) |
||||
|
return { |
||||
|
...item, |
||||
|
amplitude: Number.isNaN(amplitude) ? 0 : amplitude, |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const updateAssessResult = (durationSeconds?: number, coverRange?: number) => { |
||||
|
const scoreSummary = computeScore() |
||||
|
const cover = coverRange ?? coverage.value |
||||
|
assessResult.value = [ |
||||
|
{ name: '得分', content: scoreSummary.score }, |
||||
|
{ name: '工况覆盖率', content: cover !== undefined ? `${(Number(cover) * 100).toFixed(2)}%` : '--' }, |
||||
|
{ name: '计算时长(s)', content: durationSeconds !== undefined ? durationSeconds : '--' }, |
||||
|
{ name: '是否投入运用', content: scoreSummary.state }, |
||||
|
] |
||||
|
coverage.value = coverRange ?? coverage.value |
||||
|
} |
||||
|
|
||||
|
const computeScore = () => { |
||||
|
let forward = 0 |
||||
|
let backward = 0 |
||||
|
let forwardBase = 0 |
||||
|
let backwardBase = 0 |
||||
|
assessRows.value.forEach((item) => { |
||||
|
if (!item.fdr || !item.far) |
||||
|
return |
||||
|
const fdr = item.fdr.split('/') |
||||
|
const far = item.far.split('/') |
||||
|
const fwdFdr = Number((fdr[0] || '').replace('%', '')) / 100 |
||||
|
const fwdFar = 1 - Number((far[0] || '').replace('%', '')) / 100 |
||||
|
const bwdFdr = Number((fdr[1] || '').replace('%', '')) / 100 |
||||
|
const bwdFar = 1 - Number((far[1] || '').replace('%', '')) / 100 |
||||
|
forward += (1 / (1 + Math.exp(-(fwdFdr ** 3 - 0.5) * 11)) + 1 / (1 + Math.exp(-(fwdFar ** 5 - 0.5) * 8)) - 1) * 100 |
||||
|
backward += (1 / (1 + Math.exp(-(bwdFdr ** 3 - 0.5) * 11)) + 1 / (1 + Math.exp(-(bwdFar ** 5 - 0.5) * 8)) - 1) * 100 |
||||
|
forwardBase += 100 |
||||
|
backwardBase += 100 |
||||
|
}) |
||||
|
const forwardScore = forwardBase ? (forward / forwardBase) * 100 : 0 |
||||
|
const backwardScore = backwardBase ? (backward / backwardBase) * 100 : 0 |
||||
|
const avg = (forwardScore + backwardScore) / 2 |
||||
|
const beta = 1 / (1 + Math.exp(-((assessRows.value.length / 100) ** 2))) - 0.5 |
||||
|
const finalScore = Number((avg + (100 - avg) * beta * 2).toFixed(2)) |
||||
|
const bottom = bottomScore.value ?? 0 |
||||
|
return { |
||||
|
score: finalScore, |
||||
|
state: finalScore > bottom ? '是' : '否', |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const buildLegacyPayload = (row?: AssessRow) => { |
||||
|
const points = pointRows.value |
||||
|
const total = points.length |
||||
|
const hi: number[] = Array(total).fill(0) |
||||
|
const dead: number[] = [] |
||||
|
const limit: number[] = [] |
||||
|
const uplow: string[] = [] |
||||
|
points.forEach((p) => { |
||||
|
dead.push(p.dead ? 0 : 1) |
||||
|
limit.push(p.limit ? 1 : 0) |
||||
|
uplow.push(`${p.Lower ?? p.lower ?? ''},${p.Upper ?? p.upper ?? ''}`) |
||||
|
}) |
||||
|
|
||||
|
const selected = row ? [row] : assessRows.value |
||||
|
selected.forEach((item) => { |
||||
|
const idx = item.index ?? points.findIndex(p => p.pointId === item.pointId) |
||||
|
if (idx >= 0) |
||||
|
hi[idx] = Number(item.amplitude) || 0 |
||||
|
}) |
||||
|
|
||||
|
const intervalMs = baseInfo.value?.sampling ? Number(baseInfo.value.sampling) * 1000 : 0 |
||||
|
let time = '' |
||||
|
if (formModel.timeRange && formModel.timeRange.length === 2) { |
||||
|
time = formModel.timeRange.join(',') |
||||
|
} |
||||
|
else { |
||||
|
const trainTime = baseInfo.value?.trainTime || [] |
||||
|
const pieces: string[] = [] |
||||
|
trainTime.forEach((t: any) => { |
||||
|
if (Number(t.mode) > 0 && t.st && t.et) |
||||
|
pieces.push(`${t.st},${t.et}`) |
||||
|
}) |
||||
|
time = pieces.join(';') |
||||
|
} |
||||
|
const limitValue = (() => { |
||||
|
try { |
||||
|
const m = baseInfo.value?.para?.Model_info |
||||
|
if (m && m.Kesi_99 !== undefined) |
||||
|
return Number(m.Kesi_99) |
||||
|
return 0 |
||||
|
} |
||||
|
catch (e) { |
||||
|
return 0 |
||||
|
} |
||||
|
})() |
||||
|
const condition = baseInfo.value?.alarmmodelset?.alarmcondition || '1=1' |
||||
|
|
||||
|
return { |
||||
|
Test_Data: { |
||||
|
time, |
||||
|
points: points.map(p => p.pointId).join(','), |
||||
|
interval: intervalMs, |
||||
|
}, |
||||
|
Model_id: modelId.value, |
||||
|
number_sample: selected.length || 0, |
||||
|
number_fault_variable: selected.length || 0, |
||||
|
dead: dead.join(','), |
||||
|
limit: limit.join(','), |
||||
|
low_f: hi.map(v => Number(v || 0).toFixed(4)).join(','), |
||||
|
high_f: hi.map(v => Number(v || 0).toFixed(4)).join(','), |
||||
|
condition, |
||||
|
Test_Type: 'FAI', |
||||
|
Limit_Value: limitValue, |
||||
|
uplow: uplow.join(';'), |
||||
|
number: 0, |
||||
|
expand: !(formModel.timeRange && formModel.timeRange.length === 2), |
||||
|
k: baseInfo.value?.principal, |
||||
|
version: formModel.version, |
||||
|
coverage: coverage.value, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const handleEvaluate = async (row?: AssessRow) => { |
||||
|
if (!assessRows.value.length) { |
||||
|
createMessage.warning('请先选择参与预警的参数') |
||||
|
return |
||||
|
} |
||||
|
evaluateLoading.value = true |
||||
|
try { |
||||
|
const legacyPayload = buildLegacyPayload(row) |
||||
|
const payload = { |
||||
|
modelId: modelId.value as string, |
||||
|
version: formModel.version, |
||||
|
assessRows: row ? [row] : assessRows.value, |
||||
|
pointRows: pointRows.value, |
||||
|
timeRange: formModel.timeRange |
||||
|
? formModel.timeRange.map(t => dayjs(t).format('YYYY-MM-DD HH:mm:ss')) |
||||
|
: undefined, |
||||
|
assessMode: row ? 'single' : 'full', |
||||
|
alg: algorithm.value, |
||||
|
modelInfo: baseInfo.value ? JSON.stringify(baseInfo.value) : undefined, |
||||
|
rawJson: JSON.stringify(legacyPayload), |
||||
|
} |
||||
|
const res = await evaluateAssess(payload) |
||||
|
if (res?.points) { |
||||
|
res.points.forEach((result) => { |
||||
|
assessRows.value = assessRows.value.map((item) => { |
||||
|
const match = item.pointId === result.pointId || item.index === result.index |
||||
|
if (!match) |
||||
|
return item |
||||
|
const fdrForward = (result.fdrForward * 100).toFixed(2) |
||||
|
const fdrReverse = (result.fdrReverse * 100).toFixed(2) |
||||
|
const farForward = (result.farForward * 100).toFixed(2) |
||||
|
const farReverse = (result.farReverse * 100).toFixed(2) |
||||
|
const reconForward = result.reconForward !== undefined ? result.reconForward.toFixed(2) : '--' |
||||
|
const reconReverse = result.reconReverse !== undefined ? result.reconReverse.toFixed(2) : '--' |
||||
|
return { |
||||
|
...item, |
||||
|
fdr: `${fdrForward}%/${fdrReverse}%`, |
||||
|
far: `${farForward}%/${farReverse}%`, |
||||
|
recon: `${reconForward}/${reconReverse}`, |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
updateAssessResult(res?.durationSeconds, res?.coverage) |
||||
|
if (!timestamp.value) |
||||
|
timestamp.value = dayjs().format('YYYY-MM-DD HH:mm:ss') |
||||
|
|
||||
|
createMessage.success('评估完成') |
||||
|
} |
||||
|
catch (error) { |
||||
|
createMessage.error('评估请求失败') |
||||
|
} |
||||
|
finally { |
||||
|
evaluateLoading.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const handleSubmit = async () => { |
||||
|
if (!identifier.value) { |
||||
|
createMessage.warning('验证人不能为空') |
||||
|
return |
||||
|
} |
||||
|
if (!assessRows.value.length) { |
||||
|
createMessage.warning('请至少保留一个参与预警的参数') |
||||
|
return |
||||
|
} |
||||
|
submitLoading.value = true |
||||
|
try { |
||||
|
const scoreSummary = computeScore() |
||||
|
const payload = { |
||||
|
modelId: modelId.value as string, |
||||
|
version: formModel.version, |
||||
|
report: { |
||||
|
pointRows: pointRows.value, |
||||
|
modeRows: modeRows.value, |
||||
|
assessRows: assessRows.value, |
||||
|
assessResult: assessResult.value, |
||||
|
identifier: identifier.value, |
||||
|
time: timestamp.value || dayjs().format('YYYY-MM-DD HH:mm:ss'), |
||||
|
}, |
||||
|
score: scoreSummary.score, |
||||
|
coverage: coverage.value || 0, |
||||
|
valid: scoreSummary.state === '是', |
||||
|
identifier: identifier.value, |
||||
|
verifier: '', |
||||
|
timestamp: timestamp.value || dayjs().format('YYYY-MM-DD HH:mm:ss'), |
||||
|
conditionName: modeRows.value[1]?.content as string, |
||||
|
} |
||||
|
await saveAssessReport(payload) |
||||
|
createMessage.success('评估报告已提交') |
||||
|
router.back() |
||||
|
} |
||||
|
catch (error) { |
||||
|
createMessage.error('提交失败') |
||||
|
} |
||||
|
finally { |
||||
|
submitLoading.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const handleCleanSummary = async () => { |
||||
|
if (!formModel.timeRange) { |
||||
|
createMessage.warning('请选择时间范围后再查看清洗信息') |
||||
|
return |
||||
|
} |
||||
|
try { |
||||
|
const params = { |
||||
|
modelId: modelId.value as string, |
||||
|
version: formModel.version, |
||||
|
time: formModel.timeRange.map(t => dayjs(t).format('YYYY-MM-DD HH:mm:ss')).join(','), |
||||
|
} |
||||
|
cleanSummary.value = await fetchCleanSummary(params) |
||||
|
cleanModalVisible.value = true |
||||
|
} |
||||
|
catch (error) { |
||||
|
createMessage.error('获取清洗信息失败') |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const pointColumns = computed<ColumnProps<AssessPointRow>[]>(() => [ |
||||
|
{ |
||||
|
title: ' ', |
||||
|
dataIndex: 'group', |
||||
|
width: 120, |
||||
|
align: 'center', |
||||
|
customRender: () => '模型参数', |
||||
|
}, |
||||
|
{ |
||||
|
title: '序号', |
||||
|
dataIndex: 'index', |
||||
|
width: 80, |
||||
|
align: 'center', |
||||
|
customRender: ({ record, index }) => (record.index ?? index) + 1, |
||||
|
}, |
||||
|
{ title: '参数名称', dataIndex: 'description', width: 220, align: 'center' }, |
||||
|
{ title: '参数KKS码', dataIndex: 'pointId', width: 200, align: 'center' }, |
||||
|
{ |
||||
|
title: '是否锁定', |
||||
|
dataIndex: 'lock', |
||||
|
width: 120, |
||||
|
align: 'center', |
||||
|
customRender: ({ text }) => (text ? '是' : '否'), |
||||
|
}, |
||||
|
{ |
||||
|
title: '是否参与预警', |
||||
|
dataIndex: 'alarm', |
||||
|
width: 160, |
||||
|
align: 'center', |
||||
|
customRender: ({ record }) => |
||||
|
h(Switch, { |
||||
|
checked: record.alarm, |
||||
|
disabled: record.lock, |
||||
|
onChange: (checked: boolean) => toggleAlarm(record, checked), |
||||
|
}), |
||||
|
}, |
||||
|
]) |
||||
|
|
||||
|
const assessColumns = computed<ColumnProps<AssessRow>[]>(() => [ |
||||
|
{ title: '参与预警的参数', dataIndex: 'description', width: 200, align: 'center' }, |
||||
|
{ title: '单位', dataIndex: 'unit', width: 80, align: 'center' }, |
||||
|
{ |
||||
|
title: '目标故障幅度', |
||||
|
dataIndex: 'amplitude', |
||||
|
width: 140, |
||||
|
align: 'center', |
||||
|
customRender: ({ record }) => |
||||
|
h(InputNumber, { |
||||
|
value: Number(record.amplitude) || 0, |
||||
|
min: -999999, |
||||
|
max: 999999, |
||||
|
onChange: (val: number) => { |
||||
|
record.amplitude = val |
||||
|
}, |
||||
|
}), |
||||
|
}, |
||||
|
{ title: '诊出率', dataIndex: 'fdr', width: 160, align: 'center', customRender: ({ text }) => (text || '--') }, |
||||
|
{ title: '误诊率', dataIndex: 'far', width: 160, align: 'center', customRender: ({ text }) => (text || '--') }, |
||||
|
{ title: '重构偏差', dataIndex: 'recon', width: 140, align: 'center', customRender: ({ text }) => (text || '--') }, |
||||
|
{ title: '训练样本最大值', dataIndex: 'tMax', width: 140, align: 'center' }, |
||||
|
{ title: '训练样本最小值', dataIndex: 'tMin', width: 140, align: 'center' }, |
||||
|
{ |
||||
|
title: '操作', |
||||
|
key: 'action', |
||||
|
width: 120, |
||||
|
align: 'center', |
||||
|
customRender: ({ record }) => |
||||
|
h( |
||||
|
Button, |
||||
|
{ |
||||
|
type: 'link', |
||||
|
size: 'small', |
||||
|
loading: evaluateLoading.value, |
||||
|
onClick: () => handleEvaluate(record), |
||||
|
}, |
||||
|
{ default: () => '单项评估' }, |
||||
|
), |
||||
|
}, |
||||
|
]) |
||||
|
|
||||
|
onMounted(loadInit) |
||||
|
|
||||
|
return { |
||||
|
assessColumns, |
||||
|
assessResult, |
||||
|
assessRows, |
||||
|
applyAbsoluteBias, |
||||
|
applyPercentBias, |
||||
|
applyRelativeBias, |
||||
|
applyRound, |
||||
|
baseInfo, |
||||
|
biasForm, |
||||
|
bottomScore, |
||||
|
cleanModalVisible, |
||||
|
cleanSummary, |
||||
|
computeScore, |
||||
|
coverage, |
||||
|
formModel, |
||||
|
handleCleanSummary, |
||||
|
handleEvaluate, |
||||
|
handleSubmit, |
||||
|
identifier, |
||||
|
loading, |
||||
|
modeRows, |
||||
|
modelName, |
||||
|
pointColumns, |
||||
|
pointRows, |
||||
|
toggleAlarm, |
||||
|
updateAssessResult, |
||||
|
evaluateLoading, |
||||
|
submitLoading, |
||||
|
timestamp, |
||||
|
} |
||||
|
}, |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<PageWrapper :title="`${modelName || '模型'}评估报告`" content-full-height> |
||||
|
<a-card class="mb-4" bordered> |
||||
|
<h3 class="section-title"> |
||||
|
1. 模型基本信息 |
||||
|
</h3> |
||||
|
<a-table |
||||
|
:columns="pointColumns" :data-source="pointRows" row-key="pointId" size="small" :pagination="false" |
||||
|
bordered align="center" |
||||
|
/> |
||||
|
<a-table |
||||
|
class="mode-table" |
||||
|
:columns="[ |
||||
|
{ title: '', dataIndex: 'name', width: 200, align: 'center' }, |
||||
|
{ title: '', dataIndex: 'content', align: 'center' }, |
||||
|
]" |
||||
|
:data-source="modeRows" |
||||
|
:pagination="false" |
||||
|
size="small" |
||||
|
bordered |
||||
|
row-key="name" |
||||
|
align="center" |
||||
|
/> |
||||
|
</a-card> |
||||
|
|
||||
|
<a-card class="mb-4" bordered> |
||||
|
<h3 class="section-title"> |
||||
|
2. 模型验证结果(诊出率>98%,误诊率<5%) |
||||
|
</h3> |
||||
|
<div class="bias-card"> |
||||
|
<div class="bias-actions"> |
||||
|
<a-button size="small" type="default" @click="applyRound"> |
||||
|
取整 |
||||
|
</a-button> |
||||
|
<div class="bias-group"> |
||||
|
<span class="bias-label">绝对偏置</span> |
||||
|
<a-input-number v-model:value="biasForm.absolute" size="small" :controls="false" /> |
||||
|
<a-button size="small" type="primary" ghost @click="applyAbsoluteBias"> |
||||
|
应用 |
||||
|
</a-button> |
||||
|
</div> |
||||
|
<div class="bias-group"> |
||||
|
<span class="bias-label">相对偏置</span> |
||||
|
<a-input-number v-model:value="biasForm.relative" size="small" :controls="false" /> |
||||
|
<a-button size="small" type="primary" ghost @click="applyRelativeBias"> |
||||
|
应用 |
||||
|
</a-button> |
||||
|
</div> |
||||
|
<div class="bias-group"> |
||||
|
<span class="bias-label">偏置百分比</span> |
||||
|
<a-input-number v-model:value="biasForm.percent" size="small" :controls="false" /> |
||||
|
<span class="bias-suffix">%</span> |
||||
|
<a-button size="small" type="primary" ghost @click="applyPercentBias"> |
||||
|
应用 |
||||
|
</a-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<a-table |
||||
|
:columns="assessColumns" :data-source="assessRows" row-key="pointId" size="small" bordered |
||||
|
:pagination="{ pageSize: 10 }" align="center" |
||||
|
/> |
||||
|
</a-card> |
||||
|
|
||||
|
<a-card class="mb-4" bordered> |
||||
|
<h3 class="section-title"> |
||||
|
3. 模型评估结果 |
||||
|
</h3> |
||||
|
<a-table |
||||
|
class="result-table" |
||||
|
:columns="[ |
||||
|
{ title: '指标', dataIndex: 'name', width: 200, align: 'center' }, |
||||
|
{ title: '结果', dataIndex: 'content', align: 'center' }, |
||||
|
]" |
||||
|
:data-source="assessResult" |
||||
|
:pagination="false" |
||||
|
size="small" |
||||
|
bordered |
||||
|
row-key="name" |
||||
|
align="center" |
||||
|
/> |
||||
|
<div class="footer-actions"> |
||||
|
<div class="footer-inputs"> |
||||
|
<label class="footer-label"> |
||||
|
验证人 |
||||
|
<a-input v-model:value="identifier" disabled /> |
||||
|
</label> |
||||
|
<label class="footer-label"> |
||||
|
时间 |
||||
|
<a-input v-model:value="timestamp" disabled /> |
||||
|
</label> |
||||
|
</div> |
||||
|
<div class="footer-buttons"> |
||||
|
<a-space> |
||||
|
<a-button type="primary" size="small" :loading="evaluateLoading" @click="handleEvaluate()"> |
||||
|
评估 |
||||
|
</a-button> |
||||
|
<a-button type="primary" size="small" :loading="submitLoading" @click="handleSubmit"> |
||||
|
提交 |
||||
|
</a-button> |
||||
|
</a-space> |
||||
|
</div> |
||||
|
</div> |
||||
|
</a-card> |
||||
|
|
||||
|
<a-modal v-model:open="cleanModalVisible" title="数据清洗测试" width="900px" :footer="null"> |
||||
|
<a-table |
||||
|
v-if="cleanSummary" |
||||
|
:data-source="cleanSummary.summary" |
||||
|
:pagination="false" |
||||
|
size="small" |
||||
|
row-key="type" |
||||
|
:columns="[ |
||||
|
{ title: '类型', dataIndex: 'type', align: 'center' }, |
||||
|
{ title: '采样数量', dataIndex: 'orgNum', align: 'center' }, |
||||
|
{ title: '有效样本数量', dataIndex: 'sampleNum', align: 'center' }, |
||||
|
{ |
||||
|
title: '有效样本占比', |
||||
|
dataIndex: 'sampleRate', |
||||
|
align: 'center', |
||||
|
customRender: ({ text }) => (text !== undefined ? `${Number(text * 100).toFixed(2)}%` : '--'), |
||||
|
}, |
||||
|
{ |
||||
|
title: '清洗结果', |
||||
|
dataIndex: 'message', |
||||
|
align: 'center', |
||||
|
customRender: ({ text }) => (text === '清洗失败' ? h(Tag, { color: 'red' }, () => text) : text), |
||||
|
}, |
||||
|
]" |
||||
|
align="center" |
||||
|
/> |
||||
|
</a-modal> |
||||
|
</PageWrapper> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
.section-title { |
||||
|
margin-bottom: 12px; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
.bias-card { |
||||
|
padding: 10px 12px; |
||||
|
margin-bottom: 10px; |
||||
|
background: #fafafa; |
||||
|
border: 1px solid #f0f0f0; |
||||
|
border-radius: 6px; |
||||
|
} |
||||
|
|
||||
|
.bias-actions { |
||||
|
display: flex; |
||||
|
flex-wrap: wrap; |
||||
|
gap: 12px; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.bias-group { |
||||
|
display: flex; |
||||
|
gap: 6px; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.bias-label, |
||||
|
.bias-suffix { |
||||
|
color: #666; |
||||
|
} |
||||
|
|
||||
|
.bias-group .ant-input-number { |
||||
|
width: 120px; |
||||
|
} |
||||
|
|
||||
|
.footer-actions { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 12px; |
||||
|
align-items: center; |
||||
|
margin-top: 16px; |
||||
|
} |
||||
|
|
||||
|
.footer-inputs { |
||||
|
display: flex; |
||||
|
gap: 20px; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.footer-label { |
||||
|
display: flex; |
||||
|
gap: 6px; |
||||
|
align-items: center; |
||||
|
color: #666; |
||||
|
} |
||||
|
|
||||
|
.footer-inputs .ant-input { |
||||
|
width: 200px; |
||||
|
} |
||||
|
|
||||
|
.footer-buttons { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.toolbar { |
||||
|
display: flex; |
||||
|
flex-wrap: wrap; |
||||
|
gap: 8px; |
||||
|
} |
||||
|
|
||||
|
.mode-table .ant-table-tbody > tr > td:first-child, |
||||
|
.result-table .ant-table-tbody > tr > td:first-child { |
||||
|
font-weight: 600; |
||||
|
background: #fafafa; |
||||
|
} |
||||
|
</style> |
||||
Loading…
Reference in new issue