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