Browse Source

feat(model): 添加评估报告样本选择功能

- 在模型训练页面增加样本选择弹窗配置
- 支持训练样本和时间范围两种样本来源
- 实现样本数量输入和有效性校验
- 优化评估报告页面样式和布局
- 调整表格和卡片组件的紧凑显示样式
- 更新图表网格和尺寸配置
- 添加VSCode JSON格式化配置
pull/80/head
chenjiale 1 month ago
parent
commit
60d439f864
  1. 5
      .vscode/settings.json
  2. 89
      src/views/model/AssessReport.vue
  3. 275
      src/views/model/train/index.vue

5
.vscode/settings.json

@ -187,5 +187,8 @@
"enable": true "enable": true
}, },
"terminal.integrated.scrollback": 10000, "terminal.integrated.scrollback": 10000,
"nuxt.isNuxtApp": false "nuxt.isNuxtApp": false,
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
}
} }

89
src/views/model/AssessReport.vue

@ -112,6 +112,29 @@ export default defineComponent({
description: '', description: '',
}) })
const queryTimeRange = (() => {
const start = route.query.startTime as string
const end = route.query.endTime as string
if (start && end) {
const st = dayjs(start)
const et = dayjs(end)
if (st.isValid() && et.isValid())
return [st, et] as RangeValue
}
return undefined
})()
if (queryTimeRange)
formModel.timeRange = queryTimeRange
const sampleType = ref<string>((route.query.sampleType as string) || (queryTimeRange ? 'time' : 'train'))
const sampleCountFromQuery = ref<number | undefined>(undefined)
const rawSampleCount = route.query.sampleCount as string
if (rawSampleCount !== undefined) {
const parsed = Number(rawSampleCount)
if (Number.isFinite(parsed))
sampleCountFromQuery.value = parsed
}
const biasForm = reactive({ const biasForm = reactive({
absolute: undefined as number | undefined, absolute: undefined as number | undefined,
relative: 0 as number | undefined, relative: 0 as number | undefined,
@ -320,12 +343,14 @@ export default defineComponent({
selectedVersion.value = modelInfo?.version || modelInfo?.Cur_Version || formModel.version selectedVersion.value = modelInfo?.version || modelInfo?.Cur_Version || formModel.version
pointRows.value = (modelInfo?.pointInfo || []).map((item: any, index: number) => normalizePointRow(item, index)) pointRows.value = (modelInfo?.pointInfo || []).map((item: any, index: number) => normalizePointRow(item, index))
const sampleCount = sumTrainMode(modelInfo?.trainTime) const sampleCount = sumTrainMode(modelInfo?.trainTime)
const finalSampleCount = sampleCountFromQuery.value ?? sampleCount
const sampleLabel = sampleType.value === 'time' ? '时间选择' : '训练样本'
modeRows.value = [ modeRows.value = [
{ name: '主元个数', content: modelInfo?.principal ?? '' }, { name: '主元个数', content: modelInfo?.principal ?? '' },
{ name: '运行模式', content: modelInfo?.alarmmodelset?.alarmname || '全工况运行' }, { name: '运行模式', content: modelInfo?.alarmmodelset?.alarmname || '全工况运行' },
{ name: '模式编码', content: modelInfo?.alarmmodelset?.alarmcondition || '1=1' }, { name: '模式编码', content: modelInfo?.alarmmodelset?.alarmcondition || '1=1' },
{ name: '样本类型', content: '训练样本' }, { name: '样本类型', content: sampleLabel },
{ name: '样本数量', content: sampleCount ?? '--' }, { name: '样本数量', content: finalSampleCount ?? '--' },
] ]
assess.value assess.value
= pointRows.value = pointRows.value
@ -488,6 +513,7 @@ export default defineComponent({
} }
})() })()
const condition = baseInfo.value?.alarmmodelset?.alarmcondition || '1=1' const condition = baseInfo.value?.alarmmodelset?.alarmcondition || '1=1'
const selectedSampleCount = sampleCountFromQuery.value ?? selected.length
return { return {
Test_Data: { Test_Data: {
@ -496,7 +522,7 @@ export default defineComponent({
interval: intervalMs, interval: intervalMs,
}, },
Model_id: modelId.value, Model_id: modelId.value,
number_sample: selected.length || 0, number_sample: selectedSampleCount || 0,
number_fault_variable: selected.length || 0, number_fault_variable: selected.length || 0,
dead: dead.join(','), dead: dead.join(','),
limit: limit.join(','), limit: limit.join(','),
@ -506,7 +532,7 @@ export default defineComponent({
Test_Type: 'FAI', Test_Type: 'FAI',
Limit_Value: limitValue, Limit_Value: limitValue,
uplow: uplow.join(';'), uplow: uplow.join(';'),
number: 0, number: selectedSampleCount || 0,
expand: !(formModel.timeRange && formModel.timeRange.length === 2), expand: !(formModel.timeRange && formModel.timeRange.length === 2),
k: baseInfo.value?.principal, k: baseInfo.value?.principal,
version: formModel.version, version: formModel.version,
@ -730,12 +756,13 @@ export default defineComponent({
</script> </script>
<template> <template>
<PageWrapper :title="`${modelName || '模型'}评估报告`" content-full-height> <PageWrapper class="report-wrapper" :title="`${modelName || '模型'}评估报告`" content-full-height>
<a-card class="mb-4" bordered> <a-card class="mb-4 compact-card" bordered>
<h3 class="section-title"> <h3 class="section-title">
1. 模型基本信息 1. 模型基本信息
</h3> </h3>
<a-table <a-table
class="compact-table"
:columns="basicColumns" :columns="basicColumns"
:data-source="basicRows" :data-source="basicRows"
:row-key="record => record.key || record.pointId || record.name" :row-key="record => record.key || record.pointId || record.name"
@ -746,7 +773,7 @@ export default defineComponent({
/> />
</a-card> </a-card>
<a-card class="mb-4" bordered> <a-card class="mb-4 compact-card" bordered>
<h3 class="section-title"> <h3 class="section-title">
2. 模型验证结果诊出率>98%误诊率&lt;5% 2. 模型验证结果诊出率>98%误诊率&lt;5%
</h3> </h3>
@ -780,12 +807,13 @@ export default defineComponent({
</div> </div>
</div> </div>
<a-table <a-table
class="compact-table"
:columns="assessColumns" :data-source="assess" row-key="pointId" size="small" bordered :columns="assessColumns" :data-source="assess" row-key="pointId" size="small" bordered
:pagination="{ pageSize: 10 }" align="center" :pagination="{ pageSize: 10 }" align="center"
/> />
</a-card> </a-card>
<a-card class="mb-4" bordered> <a-card class="mb-4 compact-card" bordered>
<h3 class="section-title"> <h3 class="section-title">
3. 模型评估结果 3. 模型评估结果
</h3> </h3>
@ -857,14 +885,19 @@ export default defineComponent({
</template> </template>
<style scoped> <style scoped>
.report-wrapper {
padding: 12px;
}
.section-title { .section-title {
margin-bottom: 12px; margin: 0 0 8px;
font-weight: 600; font-weight: 600;
font-size: 16px;
} }
.bias-card { .bias-card {
padding: 10px 12px; padding: 8px 10px;
margin-bottom: 10px; margin-bottom: 8px;
background: #fafafa; background: #fafafa;
border: 1px solid #f0f0f0; border: 1px solid #f0f0f0;
border-radius: 6px; border-radius: 6px;
@ -873,13 +906,13 @@ export default defineComponent({
.bias-actions { .bias-actions {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 12px; gap: 8px;
align-items: center; align-items: center;
} }
.bias-group { .bias-group {
display: flex; display: flex;
gap: 6px; gap: 4px;
align-items: center; align-items: center;
} }
@ -895,14 +928,14 @@ export default defineComponent({
.footer-actions { .footer-actions {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 8px;
align-items: center; align-items: center;
margin-top: 16px; margin-top: 12px;
} }
.footer-inputs { .footer-inputs {
display: flex; display: flex;
gap: 20px; gap: 12px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
@ -916,7 +949,7 @@ export default defineComponent({
} }
.footer-inputs .ant-input { .footer-inputs .ant-input {
width: 200px; width: 160px;
} }
.footer-buttons { .footer-buttons {
@ -931,6 +964,28 @@ export default defineComponent({
gap: 8px; gap: 8px;
} }
.compact-card {
margin-bottom: 12px;
}
.compact-card :deep(.ant-card-head) {
min-height: 40px;
padding: 0 12px;
}
.compact-card :deep(.ant-card-head-title) {
padding: 10px 0;
}
.compact-card :deep(.ant-card-body) {
padding: 12px;
}
.compact-table :deep(.ant-table-thead > tr > th),
.compact-table :deep(.ant-table-tbody > tr > td) {
padding: 6px 8px;
}
.mode-table .ant-table-tbody > tr > td:first-child, .mode-table .ant-table-tbody > tr > td:first-child,
.result-table .ant-table-tbody > tr > td:first-child { .result-table .ant-table-tbody > tr > td:first-child {
font-weight: 600; font-weight: 600;

275
src/views/model/train/index.vue

@ -25,6 +25,7 @@ import {
Steps, Steps,
Table, Table,
Tabs, Tabs,
Radio,
} from 'ant-design-vue' } from 'ant-design-vue'
import VueECharts from 'vue-echarts' import VueECharts from 'vue-echarts'
import PointTransfer from '../components/PointTransfer.vue' import PointTransfer from '../components/PointTransfer.vue'
@ -76,6 +77,8 @@ export default defineComponent({
ACol: Col, ACol: Col,
ASelect: Select, ASelect: Select,
ASpace: Space, ASpace: Space,
ARadio: Radio,
ARadioGroup: Radio.Group,
Icon, Icon,
PointTransfer, PointTransfer,
}, },
@ -151,16 +154,17 @@ export default defineComponent({
}) })
const [pointTable] = useTable({ const [pointTable] = useTable({
columns: pointTableSchema, columns: pointTableSchema,
pagination: true, pagination: false,
dataSource: pointData, dataSource: pointData,
scroll: { y: 300 }, scroll: { y: 240 },
}) })
const trainTime = computed(() => model.value?.trainTime || []) const trainTime = computed(() => model.value?.trainTime || [])
const [trainTimeTable] = useTable({ const [trainTimeTable] = useTable({
columns: sampleInfoTableSchema, columns: sampleInfoTableSchema,
dataSource: trainTime, dataSource: trainTime,
scroll: { y: 300 }, pagination: false,
scroll: { y: 240 },
}) })
const effectiveSampleCount = computed(() => { const effectiveSampleCount = computed(() => {
@ -351,10 +355,11 @@ export default defineComponent({
}, },
grid: [ grid: [
{ {
left: 60, left: 24,
right: 50, right: 16,
bottom: 50, bottom: 30,
top: 30, top: 18,
containLabel: true,
}, },
], ],
} }
@ -856,6 +861,51 @@ export default defineComponent({
reportNameSelectVisible.value = false reportNameSelectVisible.value = false
} }
const assessConfigVisible = ref(false)
const assessSource = ref<'train' | 'time'>('train')
const assessTimeRange = ref<RangeValue | null>(null)
const assessSampleCount = ref<number | null>(null)
function openAssessConfig() {
assessSource.value = 'train'
assessTimeRange.value = null
assessSampleCount.value = effectiveSampleCount.value || 0
assessConfigVisible.value = true
}
function handleAssessConfigOk() {
if (assessSource.value === 'train' && (!model.value?.trainTime || model.value.trainTime.length === 0)) {
createMessage.warning('暂无训练样本,请选择时间范围')
return
}
if (assessSource.value === 'time') {
if (!assessTimeRange.value || assessTimeRange.value.length !== 2) {
createMessage.warning('请选择时间范围')
return
}
}
const version = selectedVersion.value || model.value?.Cur_Version || 'v-test'
const algorithm = model.value?.algorithm || 'PCA'
const queryParts = [
`version=${encodeURIComponent(version)}`,
`algorithm=${algorithm}`,
]
if (assessSource.value === 'time' && assessTimeRange.value) {
const [start, end] = assessTimeRange.value
queryParts.push('sampleType=time')
queryParts.push(`startTime=${encodeURIComponent(dayjs(start).format('YYYY-MM-DD HH:mm:ss'))}`)
queryParts.push(`endTime=${encodeURIComponent(dayjs(end).format('YYYY-MM-DD HH:mm:ss'))}`)
}
else {
queryParts.push('sampleType=train')
}
if (assessSampleCount.value !== null && assessSampleCount.value !== undefined && assessSampleCount.value !== '') {
queryParts.push(`sampleCount=${assessSampleCount.value}`)
}
assessConfigVisible.value = false
go(`/model/assess-report/${id}?${queryParts.join('&')}`)
}
async function openBottomModal() { async function openBottomModal() {
if (!model.value?.para) { if (!model.value?.para) {
createMessage.error('模型未训练,无法下装') createMessage.error('模型未训练,无法下装')
@ -1016,9 +1066,13 @@ export default defineComponent({
function goAssessReport(reportId?: number | null | Event) { function goAssessReport(reportId?: number | null | Event) {
// PointerEvent // PointerEvent
const rid = (reportId && typeof reportId === 'object') ? null : reportId const rid = (reportId && typeof reportId === 'object') ? null : reportId
if (!rid) {
openAssessConfig()
return
}
const version = selectedVersion.value || model.value?.Cur_Version || 'v-test' const version = selectedVersion.value || model.value?.Cur_Version || 'v-test'
const algorithm = model.value?.algorithm || 'PCA' const algorithm = model.value?.algorithm || 'PCA'
const extra = rid ? `&reportId=${rid}` : '' const extra = `&reportId=${rid}`
go(`/model/assess-report/${id}?version=${encodeURIComponent(version)}&algorithm=${algorithm}${extra}`) go(`/model/assess-report/${id}?version=${encodeURIComponent(version)}&algorithm=${algorithm}${extra}`)
} }
@ -1083,15 +1137,20 @@ export default defineComponent({
showTrainActions, showTrainActions,
canEditModel, canEditModel,
effectiveSampleCount, effectiveSampleCount,
assessConfigVisible,
assessSource,
assessTimeRange,
assessSampleCount,
handleAssessConfigOk,
} }
}, },
}) })
</script> </script>
<template> <template>
<PageWrapper content-background> <PageWrapper class="train-page" content-background>
<div> <div>
<a-card title="模型信息" :bordered="false"> <a-card title="模型信息" :bordered="false" class="compact-card">
<a-descriptions size="small" :column="4" bordered> <a-descriptions size="small" :column="4" bordered>
<a-descriptions-item label="模型名称"> <a-descriptions-item label="模型名称">
{{ model?.name }} {{ model?.name }}
@ -1149,7 +1208,7 @@ export default defineComponent({
{{ model?.modifiedTime || "暂无" }} {{ model?.modifiedTime || "暂无" }}
</a-descriptions-item> </a-descriptions-item>
</a-descriptions> </a-descriptions>
<a-divider /> <a-divider class="compact-divider" />
<a-descriptions size="small" :column="4" bordered> <a-descriptions size="small" :column="4" bordered>
<a-descriptions-item label="算法"> <a-descriptions-item label="算法">
{{ model?.algorithm || "PCA" }} {{ model?.algorithm || "PCA" }}
@ -1187,17 +1246,17 @@ export default defineComponent({
<a-card <a-card
title="模式" title="模式"
:bordered="false" :bordered="false"
style="margin-top: 16px; margin-bottom: -20px" class="compact-card mode-card"
> >
<a-button size="large" :disabled="!canEditModel" @click="openEditMode"> <a-button size="large" :disabled="!canEditModel" @click="openEditMode">
{{ model?.alarmmodelset?.alarmname || '全工况运行' }} {{ model?.alarmmodelset?.alarmname || '全工况运行' }}
</a-button> </a-button>
</a-card> </a-card>
<a-card :bordered="false"> <a-card :bordered="false" class="compact-card tab-card">
<a-tabs v-model:active-key="activeKey"> <a-tabs v-model:active-key="activeKey">
<a-tab-pane key="1" tab="训练采样时间"> <a-tab-pane key="1" tab="训练采样时间">
<BasicTable @register="trainTimeTable"> <BasicTable class="compact-table" @register="trainTimeTable">
<template #action="{ record, index }"> <template #action="{ record, index }">
<a-button <a-button
v-if="showTrainActions" v-if="showTrainActions"
@ -1212,7 +1271,7 @@ export default defineComponent({
</BasicTable> </BasicTable>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="2" tab="测点参数"> <a-tab-pane key="2" tab="测点参数">
<BasicTable @register="pointTable"> <BasicTable class="compact-table" @register="pointTable">
<template #action="{ record, index }"> <template #action="{ record, index }">
<a-button <a-button
v-if="showTrainActions" v-if="showTrainActions"
@ -1228,9 +1287,9 @@ export default defineComponent({
</a-tabs> </a-tabs>
</a-card> </a-card>
<a-card title="智能训练" :bordered="false"> <a-card title="智能训练" :bordered="false" class="compact-card">
<div style="display: flex; align-items: center"> <div class="train-toolbar">
<a-form layout="inline" style="flex: 1"> <a-form layout="inline" class="train-toolbar-form">
<a-form-item label="模型预览时间范围"> <a-form-item label="模型预览时间范围">
<a-range-picker <a-range-picker
v-model:value="historyTime" v-model:value="historyTime"
@ -1242,7 +1301,6 @@ export default defineComponent({
<a-button <a-button
v-if="showTrainActions" v-if="showTrainActions"
type="primary" type="primary"
style="margin-left: auto"
@click="trainModel" @click="trainModel"
> >
模型训练 模型训练
@ -1250,7 +1308,7 @@ export default defineComponent({
<a-button <a-button
v-if="showTrainActions" v-if="showTrainActions"
type="primary" type="primary"
style="margin-left: 10px" class="ml-8"
@click="clearModel" @click="clearModel"
> >
清除训练结果 清除训练结果
@ -1258,7 +1316,7 @@ export default defineComponent({
<a-button <a-button
v-if="showTrainActions" v-if="showTrainActions"
type="primary" type="primary"
style="margin-left: 6px" class="ml-8"
@click="openEditModel" @click="openEditModel"
> >
修改模型 修改模型
@ -1266,13 +1324,13 @@ export default defineComponent({
<a-button <a-button
v-if="showTrainActions" v-if="showTrainActions"
danger danger
style="margin-left: 10px" class="ml-8"
@click="openBottomModal" @click="openBottomModal"
> >
下装 下装
</a-button> </a-button>
</div> </div>
<a-divider /> <a-divider class="compact-divider" />
<a-spin :spinning="spinning" size="large"> <a-spin :spinning="spinning" size="large">
<div <div
v-for="(item, index) in historyList" v-for="(item, index) in historyList"
@ -1280,7 +1338,7 @@ export default defineComponent({
class="echart-box" class="echart-box"
style="width: 100%" style="width: 100%"
> >
<a-card :bordered="false" style="margin-bottom: 16px"> <a-card :bordered="false" class="compact-card inner-card">
<template #title> <template #title>
<span style="font-size: 20px">{{ item.name }}</span> <span style="font-size: 20px">{{ item.name }}</span>
</template> </template>
@ -1288,7 +1346,7 @@ export default defineComponent({
:ref="(el) => setEchartsRef(el, index)" :ref="(el) => setEchartsRef(el, index)"
:option="getOption(item)" :option="getOption(item)"
autoresize autoresize
style="width: 100%; height: 200px" style="width: 100%; height: 180px"
@finished="() => onChartFinished(index)" @finished="() => onChartFinished(index)"
@brush-selected="onBrushSelected" @brush-selected="onBrushSelected"
/> />
@ -1297,6 +1355,48 @@ export default defineComponent({
</a-spin> </a-spin>
</a-card> </a-card>
</div> </div>
<a-modal
v-model:open="assessConfigVisible"
title="样本选择"
width="520px"
@ok="handleAssessConfigOk"
@cancel="() => { assessConfigVisible = false }"
>
<a-space direction="vertical" size="middle" style="width: 100%">
<div class="assess-option">
<a-radio-group v-model:value="assessSource">
<a-radio value="train">
训练样本
</a-radio>
<a-radio value="time">
时间选择
</a-radio>
</a-radio-group>
</div>
<div class="assess-option">
<span class="assess-label">时间范围</span>
<a-range-picker
v-model:value="assessTimeRange"
show-time
format="YYYY-MM-DD HH:mm:ss"
style="flex: 1"
:disabled="assessSource !== 'time'"
/>
</div>
<div class="assess-option">
<span class="assess-label">样本数量</span>
<a-input-number
v-model:value="assessSampleCount"
:min="0"
:max="effectiveSampleCount || undefined"
:controls="false"
style="flex: 1"
placeholder="请输入样本数量"
/>
<span class="assess-hint">样本选择范围0-{{ effectiveSampleCount }}</span>
</div>
</a-space>
</a-modal>
<a-modal <a-modal
v-model:open="reportModalVisible" v-model:open="reportModalVisible"
title="选择评估报告" title="选择评估报告"
@ -1455,4 +1555,129 @@ export default defineComponent({
.el-table .el-table__header th { .el-table .el-table__header th {
font-weight: bold; font-weight: bold;
} }
.train-page {
padding: 8px;
background: #f6f7fb;
}
.assess-option {
display: flex;
align-items: center;
gap: 8px;
}
.assess-label {
width: 72px;
color: #4b5563;
}
.assess-hint {
color: #9098a4;
font-size: 12px;
}
.compact-card {
margin-top: -10px;
margin-bottom: 8px;
border-color: #d9dfe7;
border-radius: 8px;
box-shadow: 0 2px 6px rgb(0 0 0 / 3%);
}
.compact-card :deep(.ant-card-head) {
min-height: 32px;
padding: 0 8px;
background: linear-gradient(90deg, #f9fafc 0%, #f6f8fb 100%);
}
.compact-card :deep(.ant-card-head-title) {
padding: 6px 0;
font-weight: 600;
color: #1d2733;
}
.compact-card :deep(.ant-card-body) {
padding: 8px;
}
.compact-card :deep(.ant-descriptions-item-label),
.compact-card :deep(.ant-descriptions-item-content) {
padding: 4px 6px;
font-size: 13px;
}
.compact-divider {
margin: 8px 0;
}
.compact-table :deep(.ant-table-thead > tr > th),
.compact-table :deep(.ant-table-tbody > tr > td) {
padding: 4px 6px;
font-size: 13px;
}
.compact-table :deep(.ant-table-thead > tr > th) {
color: #2a2f36;
background: #f2f4f8;
}
.mode-card {
margin-top: 2px;
margin-bottom: -24px;
}
.mode-card :deep(.ant-card-body) {
padding: 2px 8px 4px;
}
.mode-card :deep(.ant-btn-lg) {
height: 34px;
padding: 0 14px;
font-size: 14px;
line-height: 34px;
}
.tab-card :deep(.ant-card-body) {
padding: 4px 6px 4px;
}
.train-toolbar {
display: flex;
gap: 6px;
align-items: center;
}
.train-toolbar-form {
flex: 1;
}
.train-toolbar :deep(.ant-form-item) {
margin-bottom: 6px;
}
.ml-8 {
margin-left: 6px;
}
.echart-box {
margin-bottom: 8px;
}
.inner-card :deep(.ant-card-body) {
padding: 8px 8px 6px;
}
.inner-card :deep(.ant-card-head-title) {
font-size: 15px;
}
.train-toolbar :deep(.ant-btn) {
font-size: 13px;
}
.train-toolbar :deep(.ant-input),
.train-toolbar :deep(.ant-picker) {
font-size: 13px;
}
</style> </style>

Loading…
Cancel
Save