Browse Source

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

- 在模型训练页面增加样本选择弹窗配置
- 支持训练样本和时间范围两种样本来源
- 实现样本数量输入和有效性校验
- 优化评估报告页面样式和布局
- 调整表格和卡片组件的紧凑显示样式
- 更新图表网格和尺寸配置
- 添加VSCode JSON格式化配置
pull/80/head
chenjiale 4 weeks 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
},
"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: '',
})
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({
absolute: undefined as number | undefined,
relative: 0 as number | undefined,
@ -320,12 +343,14 @@ export default defineComponent({
selectedVersion.value = modelInfo?.version || modelInfo?.Cur_Version || formModel.version
pointRows.value = (modelInfo?.pointInfo || []).map((item: any, index: number) => normalizePointRow(item, index))
const sampleCount = sumTrainMode(modelInfo?.trainTime)
const finalSampleCount = sampleCountFromQuery.value ?? sampleCount
const sampleLabel = sampleType.value === 'time' ? '时间选择' : '训练样本'
modeRows.value = [
{ name: '主元个数', content: modelInfo?.principal ?? '' },
{ name: '运行模式', content: modelInfo?.alarmmodelset?.alarmname || '全工况运行' },
{ name: '模式编码', content: modelInfo?.alarmmodelset?.alarmcondition || '1=1' },
{ name: '样本类型', content: '训练样本' },
{ name: '样本数量', content: sampleCount ?? '--' },
{ name: '样本类型', content: sampleLabel },
{ name: '样本数量', content: finalSampleCount ?? '--' },
]
assess.value
= pointRows.value
@ -488,6 +513,7 @@ export default defineComponent({
}
})()
const condition = baseInfo.value?.alarmmodelset?.alarmcondition || '1=1'
const selectedSampleCount = sampleCountFromQuery.value ?? selected.length
return {
Test_Data: {
@ -496,7 +522,7 @@ export default defineComponent({
interval: intervalMs,
},
Model_id: modelId.value,
number_sample: selected.length || 0,
number_sample: selectedSampleCount || 0,
number_fault_variable: selected.length || 0,
dead: dead.join(','),
limit: limit.join(','),
@ -506,7 +532,7 @@ export default defineComponent({
Test_Type: 'FAI',
Limit_Value: limitValue,
uplow: uplow.join(';'),
number: 0,
number: selectedSampleCount || 0,
expand: !(formModel.timeRange && formModel.timeRange.length === 2),
k: baseInfo.value?.principal,
version: formModel.version,
@ -730,12 +756,13 @@ export default defineComponent({
</script>
<template>
<PageWrapper :title="`${modelName || '模型'}评估报告`" content-full-height>
<a-card class="mb-4" bordered>
<PageWrapper class="report-wrapper" :title="`${modelName || '模型'}评估报告`" content-full-height>
<a-card class="mb-4 compact-card" bordered>
<h3 class="section-title">
1. 模型基本信息
</h3>
<a-table
class="compact-table"
:columns="basicColumns"
:data-source="basicRows"
:row-key="record => record.key || record.pointId || record.name"
@ -746,7 +773,7 @@ export default defineComponent({
/>
</a-card>
<a-card class="mb-4" bordered>
<a-card class="mb-4 compact-card" bordered>
<h3 class="section-title">
2. 模型验证结果诊出率>98%误诊率&lt;5%
</h3>
@ -780,12 +807,13 @@ export default defineComponent({
</div>
</div>
<a-table
class="compact-table"
:columns="assessColumns" :data-source="assess" row-key="pointId" size="small" bordered
:pagination="{ pageSize: 10 }" align="center"
/>
</a-card>
<a-card class="mb-4" bordered>
<a-card class="mb-4 compact-card" bordered>
<h3 class="section-title">
3. 模型评估结果
</h3>
@ -857,14 +885,19 @@ export default defineComponent({
</template>
<style scoped>
.report-wrapper {
padding: 12px;
}
.section-title {
margin-bottom: 12px;
margin: 0 0 8px;
font-weight: 600;
font-size: 16px;
}
.bias-card {
padding: 10px 12px;
margin-bottom: 10px;
padding: 8px 10px;
margin-bottom: 8px;
background: #fafafa;
border: 1px solid #f0f0f0;
border-radius: 6px;
@ -873,13 +906,13 @@ export default defineComponent({
.bias-actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
gap: 8px;
align-items: center;
}
.bias-group {
display: flex;
gap: 6px;
gap: 4px;
align-items: center;
}
@ -895,14 +928,14 @@ export default defineComponent({
.footer-actions {
display: flex;
flex-direction: column;
gap: 12px;
gap: 8px;
align-items: center;
margin-top: 16px;
margin-top: 12px;
}
.footer-inputs {
display: flex;
gap: 20px;
gap: 12px;
align-items: center;
justify-content: center;
width: 100%;
@ -916,7 +949,7 @@ export default defineComponent({
}
.footer-inputs .ant-input {
width: 200px;
width: 160px;
}
.footer-buttons {
@ -931,6 +964,28 @@ export default defineComponent({
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,
.result-table .ant-table-tbody > tr > td:first-child {
font-weight: 600;

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

@ -25,6 +25,7 @@ import {
Steps,
Table,
Tabs,
Radio,
} from 'ant-design-vue'
import VueECharts from 'vue-echarts'
import PointTransfer from '../components/PointTransfer.vue'
@ -76,6 +77,8 @@ export default defineComponent({
ACol: Col,
ASelect: Select,
ASpace: Space,
ARadio: Radio,
ARadioGroup: Radio.Group,
Icon,
PointTransfer,
},
@ -151,16 +154,17 @@ export default defineComponent({
})
const [pointTable] = useTable({
columns: pointTableSchema,
pagination: true,
pagination: false,
dataSource: pointData,
scroll: { y: 300 },
scroll: { y: 240 },
})
const trainTime = computed(() => model.value?.trainTime || [])
const [trainTimeTable] = useTable({
columns: sampleInfoTableSchema,
dataSource: trainTime,
scroll: { y: 300 },
pagination: false,
scroll: { y: 240 },
})
const effectiveSampleCount = computed(() => {
@ -351,10 +355,11 @@ export default defineComponent({
},
grid: [
{
left: 60,
right: 50,
bottom: 50,
top: 30,
left: 24,
right: 16,
bottom: 30,
top: 18,
containLabel: true,
},
],
}
@ -856,6 +861,51 @@ export default defineComponent({
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() {
if (!model.value?.para) {
createMessage.error('模型未训练,无法下装')
@ -1016,9 +1066,13 @@ export default defineComponent({
function goAssessReport(reportId?: number | null | Event) {
// PointerEvent
const rid = (reportId && typeof reportId === 'object') ? null : reportId
if (!rid) {
openAssessConfig()
return
}
const version = selectedVersion.value || model.value?.Cur_Version || 'v-test'
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}`)
}
@ -1083,15 +1137,20 @@ export default defineComponent({
showTrainActions,
canEditModel,
effectiveSampleCount,
assessConfigVisible,
assessSource,
assessTimeRange,
assessSampleCount,
handleAssessConfigOk,
}
},
})
</script>
<template>
<PageWrapper content-background>
<PageWrapper class="train-page" content-background>
<div>
<a-card title="模型信息" :bordered="false">
<a-card title="模型信息" :bordered="false" class="compact-card">
<a-descriptions size="small" :column="4" bordered>
<a-descriptions-item label="模型名称">
{{ model?.name }}
@ -1149,7 +1208,7 @@ export default defineComponent({
{{ model?.modifiedTime || "暂无" }}
</a-descriptions-item>
</a-descriptions>
<a-divider />
<a-divider class="compact-divider" />
<a-descriptions size="small" :column="4" bordered>
<a-descriptions-item label="算法">
{{ model?.algorithm || "PCA" }}
@ -1187,17 +1246,17 @@ export default defineComponent({
<a-card
title="模式"
:bordered="false"
style="margin-top: 16px; margin-bottom: -20px"
class="compact-card mode-card"
>
<a-button size="large" :disabled="!canEditModel" @click="openEditMode">
{{ model?.alarmmodelset?.alarmname || '全工况运行' }}
</a-button>
</a-card>
<a-card :bordered="false">
<a-card :bordered="false" class="compact-card tab-card">
<a-tabs v-model:active-key="activeKey">
<a-tab-pane key="1" tab="训练采样时间">
<BasicTable @register="trainTimeTable">
<BasicTable class="compact-table" @register="trainTimeTable">
<template #action="{ record, index }">
<a-button
v-if="showTrainActions"
@ -1212,7 +1271,7 @@ export default defineComponent({
</BasicTable>
</a-tab-pane>
<a-tab-pane key="2" tab="测点参数">
<BasicTable @register="pointTable">
<BasicTable class="compact-table" @register="pointTable">
<template #action="{ record, index }">
<a-button
v-if="showTrainActions"
@ -1228,9 +1287,9 @@ export default defineComponent({
</a-tabs>
</a-card>
<a-card title="智能训练" :bordered="false">
<div style="display: flex; align-items: center">
<a-form layout="inline" style="flex: 1">
<a-card title="智能训练" :bordered="false" class="compact-card">
<div class="train-toolbar">
<a-form layout="inline" class="train-toolbar-form">
<a-form-item label="模型预览时间范围">
<a-range-picker
v-model:value="historyTime"
@ -1242,7 +1301,6 @@ export default defineComponent({
<a-button
v-if="showTrainActions"
type="primary"
style="margin-left: auto"
@click="trainModel"
>
模型训练
@ -1250,7 +1308,7 @@ export default defineComponent({
<a-button
v-if="showTrainActions"
type="primary"
style="margin-left: 10px"
class="ml-8"
@click="clearModel"
>
清除训练结果
@ -1258,7 +1316,7 @@ export default defineComponent({
<a-button
v-if="showTrainActions"
type="primary"
style="margin-left: 6px"
class="ml-8"
@click="openEditModel"
>
修改模型
@ -1266,13 +1324,13 @@ export default defineComponent({
<a-button
v-if="showTrainActions"
danger
style="margin-left: 10px"
class="ml-8"
@click="openBottomModal"
>
下装
</a-button>
</div>
<a-divider />
<a-divider class="compact-divider" />
<a-spin :spinning="spinning" size="large">
<div
v-for="(item, index) in historyList"
@ -1280,7 +1338,7 @@ export default defineComponent({
class="echart-box"
style="width: 100%"
>
<a-card :bordered="false" style="margin-bottom: 16px">
<a-card :bordered="false" class="compact-card inner-card">
<template #title>
<span style="font-size: 20px">{{ item.name }}</span>
</template>
@ -1288,7 +1346,7 @@ export default defineComponent({
:ref="(el) => setEchartsRef(el, index)"
:option="getOption(item)"
autoresize
style="width: 100%; height: 200px"
style="width: 100%; height: 180px"
@finished="() => onChartFinished(index)"
@brush-selected="onBrushSelected"
/>
@ -1297,6 +1355,48 @@ export default defineComponent({
</a-spin>
</a-card>
</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
v-model:open="reportModalVisible"
title="选择评估报告"
@ -1455,4 +1555,129 @@ export default defineComponent({
.el-table .el-table__header th {
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>

Loading…
Cancel
Save