922 lines
28 KiB
Vue
922 lines
28 KiB
Vue
<template>
|
||
<div class="gl-page-content-full model-training-container">
|
||
<div class="model-step">
|
||
<el-steps :active="currentStep" simple process-status="finish">
|
||
<el-step title="数据导入" />
|
||
<el-step title="参数设置" />
|
||
<el-step title="模型训练" />
|
||
</el-steps>
|
||
</div>
|
||
<div class="content">
|
||
<div class="dispose-data" v-show="currentStep === 0">
|
||
<!-- tableName="MODEL_TRAINING_DATA" -->
|
||
<BaseTable
|
||
:head="columns"
|
||
showIndex
|
||
ref="trainDataTableRef"
|
||
:searchLimitNum="3"
|
||
:actionsWidth="tableActionsLength['2-2-2']"
|
||
hidePagination
|
||
>
|
||
<template #leftOptions>
|
||
<div class="tool">
|
||
<!-- <el-button type="primary" @click="openHandleModal">数据处理</el-button> -->
|
||
<!-- 动态可以导入多个文件,稳态只能导入一个文件 -->
|
||
<el-upload
|
||
:limit="1"
|
||
action=""
|
||
:showUploadList="false"
|
||
name="file"
|
||
:max-count="props.modelForm.algorithmType === AlgorithmType.Dynamic ? 50 : 1"
|
||
:auto-upload="false"
|
||
@change="importFileFun"
|
||
>
|
||
<el-button icon="upload" type="primary">
|
||
导入数据
|
||
</el-button>
|
||
</el-upload>
|
||
<el-button icon="setting" class="data-setting" @click="openDataSetDialogFun">训练数据设置</el-button>
|
||
</div>
|
||
|
||
</template>
|
||
<!-- 动态的多文件 -->
|
||
<!-- <template #tableTitle v-if="props.modelForm.algorithmType === AlgorithmType.Dynamic">
|
||
<template v-for="item in sourceFileList" :key="item">
|
||
<el-tag
|
||
class="tags"
|
||
:color="currentFileName === item.name ? '#2db7f5' : ''"
|
||
:closable="props.modelForm.status === ModelStatus.NoStart"
|
||
@close.stop="deleteSourceFile(item)"
|
||
@click="changeSourceFile(item)"
|
||
>
|
||
{{ item.name }}
|
||
</el-tag>
|
||
</template>
|
||
</template> -->
|
||
|
||
</BaseTable>
|
||
<!-- 稳态最大最小值的表格 -->
|
||
<attachmentTable :columns="columns" :steadyAverageData="steadyAverageData" />
|
||
<!-- 动态的曲线 -->
|
||
<!-- <div v-if="props.modelForm.algorithmType === AlgorithmType.Dynamic" class="dynamic-curve" ref="sourceChartRef"></div> -->
|
||
</div>
|
||
<div class="algorithm-select" v-show="currentStep === 1">
|
||
<param-setting ref='paramSettingRef' :param="algorithmParamForm" />
|
||
<div class="algorithm-select-box" v-if="false">
|
||
<div class="algorithm-title">算法选择</div>
|
||
<el-form :model="algorithmParamForm" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }">
|
||
<el-form-item label="线性求解">
|
||
<el-radio-group
|
||
:disabled="props.modelForm.algorithmType !== AlgorithmType.Steady"
|
||
v-model:value="algorithmParamForm.algorithm"
|
||
:options="[
|
||
{ label: '线性拟合', value: '线性拟合' },
|
||
{ label: '多项式拟合', value: '多项式拟合' },
|
||
]"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="非线性求解">
|
||
<el-radio-group
|
||
:disabled="props.modelForm.algorithmType !== AlgorithmType.Dynamic"
|
||
v-model:value="algorithmParamForm.algorithm"
|
||
:options="[
|
||
{ label: '基础神经网络NN', value: '基础神经网络NN' },
|
||
{ label: '循环神经网络RNN', value: '循环神经网络RNN' },
|
||
{ label: '长短记忆神经网络LSTM', value: '长短记忆神经网络LSTM' },
|
||
]"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
<div class="algorithm-select-box" v-if="false">
|
||
|
||
<div class="algorithm-title">参数设置</div>
|
||
<el-form :model="algorithmParamForm" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }">
|
||
<el-form-item label="激活函数定义/选择">
|
||
<el-select
|
||
show-search
|
||
:options="[{ label: 'sigmod', value: 'sigmod' }]"
|
||
v-model:value="algorithmParamForm.activateFun"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="损失函数设定">
|
||
<el-select
|
||
show-search
|
||
:options="AlgorithmOptions.lossFunction"
|
||
v-model:value="algorithmParamForm.lossFun"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="优化函数">
|
||
<el-select
|
||
show-search
|
||
:options="AlgorithmOptions.optimizeFunction"
|
||
v-model:value="algorithmParamForm.optimizeFun"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="导出模型格式">
|
||
<el-radio-group
|
||
v-model:value="algorithmParamForm.exportFormat"
|
||
:options="AlgorithmOptions.exportFormat"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="训练轮次">
|
||
<el-input-number v-model:value="algorithmParamForm.round" />
|
||
</el-form-item>
|
||
<el-form-item label="训练轮次损失打印">
|
||
<el-input-number v-model:value="algorithmParamForm.roundPrint" />
|
||
</el-form-item>
|
||
<el-form-item label="多项式拟合阶数">
|
||
<el-input-number v-model:value="algorithmParamForm.stepCounts" />
|
||
</el-form-item>
|
||
<el-form-item label="学习率">
|
||
<el-input-number
|
||
string-mode
|
||
:precision="4"
|
||
:max="0.1"
|
||
:min="0.0001"
|
||
v-model:value="algorithmParamForm.studyPercent"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="批量加载大小">
|
||
<el-input-number v-model:value="algorithmParamForm.loadSize" />
|
||
</el-form-item>
|
||
<el-form-item label="训练集比例">
|
||
<el-input-number
|
||
v-model:value="algorithmParamForm.trainingRatio"
|
||
:min="0"
|
||
:max="100"
|
||
:formatter="(value: any) => `${value}%`"
|
||
:parser="(value: any) => value.replace('%', '')"
|
||
/>
|
||
<!-- <el-input-number :max="1" :min="0" v-model:value="algorithmParamForm.trainingRatio" /> -->
|
||
</el-form-item>
|
||
<el-form-item label="是否开启数据预处理">
|
||
<el-radio-group
|
||
v-model:value="algorithmParamForm.preDisposeData"
|
||
:options="[
|
||
{ label: '开启', value: true },
|
||
{ label: '关闭', value: false },
|
||
]"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="数据处理方式">
|
||
<el-select
|
||
show-search
|
||
:options="AlgorithmOptions.disposeMethod"
|
||
v-model:value="algorithmParamForm.disposeMethod"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="数据乱序">
|
||
<el-radio-group
|
||
v-model:value="algorithmParamForm.dataNoOrder"
|
||
:options="[
|
||
{ label: '开启', value: true },
|
||
{ label: '关闭', value: false },
|
||
]"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
</div>
|
||
<div class="model-training" v-show="currentStep === 2">
|
||
<div class="training-btn">
|
||
<el-popconfirm title="是否确认终止训练?" @confirm="stopTrainingFun" v-if="props.modelForm.trainingStatus === '训练中'">
|
||
<template #reference>
|
||
<el-button type="danger">训练终止</el-button>
|
||
</template>
|
||
</el-popconfirm>
|
||
</div>
|
||
<div class="algorithm-select-box">
|
||
<el-card shadow="never">
|
||
<template #header>训练曲线</template>
|
||
<div ref="logChartRef" class="curve" id="curve"></div>
|
||
</el-card>
|
||
|
||
<el-card shadow="never">
|
||
<template #header>
|
||
<div class="log-header">
|
||
<span>最新200行日志</span>
|
||
<div class="log-btn">
|
||
<el-form :inline="true">
|
||
<el-form-item label="实时刷新">
|
||
<el-switch
|
||
v-model="logStatus"
|
||
inline-prompt
|
||
active-text="开"
|
||
inactive-text="关"
|
||
active-value="refresh"
|
||
inactive-value="noRefresh"
|
||
@change="onLogStatusChangeFun"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="">
|
||
<el-button type="primary" @click="showAllLog">全部日志</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</template>
|
||
<div class="log" ref="lastLogRef" v-html="lastLogContentHtml"></div>
|
||
</el-card>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="options">
|
||
<el-button class="step-btn" @click="handleChangeStep('pre')" :disabled="currentStep < 0">{{ $t('通用.上一步') }}</el-button>
|
||
<el-button v-if="currentStep === 1" class="step-btn" type="primary" @click="beginTrainingFun">{{ $t('数据训练.开始训练') }}</el-button>
|
||
<el-button class="step-btn" @click="handleChangeStep('next')" :disabled="currentStep >= 2"> {{ $t('通用.下一步') }}
|
||
</el-button>
|
||
</div>
|
||
<Dialog
|
||
v-model="dataSetDialogVisible"
|
||
diaTitle="训练数据设置"
|
||
:width="800"
|
||
:height="500"
|
||
@close="closeDataSetFun"
|
||
show-footer
|
||
>
|
||
<el-form :model="disposeForm" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }">
|
||
<el-form-item label="设置特征输入列">
|
||
<el-checkbox-group v-model="disposeForm.inputColumn">
|
||
<el-checkbox v-for="item in columns" :key="item.key" :label="item.title" :value="item.key" />
|
||
</el-checkbox-group>
|
||
</el-form-item>
|
||
<el-form-item label="设置标签输出列">
|
||
<el-checkbox-group v-model="disposeForm.outputColumn">
|
||
<el-checkbox v-for="item in columns" :key="item.key" :label="item.title" :value="item.key" />
|
||
</el-checkbox-group>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<div>
|
||
<el-button @click="closeDataSetFun">取消</el-button>
|
||
<el-button type="primary" @click="confirmSetColumnFun">确认</el-button>
|
||
</div>
|
||
</template>
|
||
</Dialog>
|
||
<!-- <Dialog
|
||
v-model="disposeDataDialogVisible"
|
||
diaTitle="数据处理"
|
||
:width="800"
|
||
:height="500"
|
||
@close="closeDisposeDataFun"
|
||
show-footer
|
||
>
|
||
<el-form :model="disposeForm" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }">
|
||
<el-form-item label="数据处理生效列">
|
||
<el-checkbox-group v-model:value="disposeForm.effectColumn" :options="columns" />
|
||
</el-form-item>
|
||
<el-form-item label="数据处理方式">
|
||
<el-radio-group v-model:value="disposeForm.method">
|
||
<el-radio :value="1">积分</el-radio>
|
||
<el-radio :value="2">微分</el-radio>
|
||
<el-radio :value="3">插值</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #rightfoot>
|
||
<el-button @click="closeDisposeDataFun">取消</el-button>
|
||
<el-button type="primary" @click="disposeData">确认</el-button>
|
||
</template>
|
||
</Dialog> -->
|
||
<Dialog v-model="allLogVisible" diaTitle="全部日志" :width="800" :height="500" show-footer>
|
||
<el-scrollbar ref="allLogScrollbar" class="all-log-container">
|
||
<div v-for="(lineHtml, idx) in allLogLinesHtml" :key="idx" class="log-item" v-html="lineHtml" />
|
||
</el-scrollbar>
|
||
<template #footer>
|
||
<div>
|
||
<el-button @click="closeAllLogFun">关闭</el-button>
|
||
</div>
|
||
</template>
|
||
</Dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { nextTick, onBeforeUnmount, onMounted, reactive, ref, watchEffect, computed, type PropType } from 'vue';
|
||
import attachmentTable from './components/attachmentTable.vue';
|
||
import { AlgorithmOptions, AlgorithmType, hasCommonItem, ModelStatus } from './model.ts';
|
||
import { tableActionsLength } from '@/utils/common';
|
||
import BaseTable from '@/components/common/table/baseTable.vue';
|
||
import { ElMessage } from 'element-plus';
|
||
import Dialog from '@/components/common/dialog/index.vue';
|
||
import { handleLoadDataApi, getHandleLoadDataResultApi, setTrainingDataInputOutputColumnApi, getTrainingDataInputOutputColumnApi, submitTrainingApi, getAlgorithmParamApi, getTrainingResultApi, stopTrainingApi, getModelDetailApi, getModelTrainingWebSocketUrl } from '@/api/data/dataForecast';
|
||
import paramSetting from './components/paramSetting.vue';
|
||
import { initEcharts } from '@/components/common/echartCard/echartsOptions';
|
||
import { getUserId } from '@/utils/user.ts';
|
||
|
||
const emit = defineEmits(['back', 'syncModelInfo']);
|
||
|
||
const props = defineProps({
|
||
modelForm: {
|
||
type: Object as PropType<any>,
|
||
default: null,
|
||
},
|
||
});
|
||
const currentUserId = getUserId();
|
||
const trainDataTableRef = ref();
|
||
|
||
const dataSetDialogVisible = ref(false);
|
||
|
||
const closeDataSetFun = () => {
|
||
dataSetDialogVisible.value = false;
|
||
};
|
||
|
||
// const disposeDataDialogVisible = ref(false);
|
||
// const closeDisposeDataFun = () => {
|
||
// disposeDataDialogVisible.value = false;
|
||
// };
|
||
|
||
const currentStep = ref(0);
|
||
const handleChangeStep = (type: string) => {
|
||
if (type === 'next' && currentStep.value < 2) {
|
||
if (currentStep.value === 0) {
|
||
if (!disposeForm.inputColumn.length) {
|
||
ElMessage.warning('请设置训练数据的特征输入列!');
|
||
return;
|
||
}
|
||
if (!disposeForm.outputColumn.length) {
|
||
ElMessage.warning('请设置训练数据的标签输出列!');
|
||
return;
|
||
}
|
||
// 有设置列变更才更新
|
||
// if (
|
||
// JSON.stringify({
|
||
// inputColumn: disposeForm.inputColumn,
|
||
// outputColumn: disposeForm.outputColumn,
|
||
// }) !== props.modelForm.columnParam &&
|
||
// props.modelForm.status === ModelStatus.NoStart
|
||
// ) {
|
||
// // 未开始的才能更新列信息,和数据处理信息
|
||
// // updateModelColumn({ columnParam: JSON.stringify(columnParam) });
|
||
// }
|
||
|
||
// 回显算法
|
||
// const oldParam = JSON.parse(props.modelForm.algorithmParam);
|
||
// for (const key in oldParam) {
|
||
// algorithmParamForm[key] = oldParam[key];
|
||
// }
|
||
}
|
||
|
||
if (currentStep.value === 1) {
|
||
if (props.modelForm.status === ModelStatus.NoStart) {
|
||
beginRefreshLog();
|
||
} else {
|
||
getTrainingResultFun();
|
||
}
|
||
}
|
||
currentStep.value++;
|
||
}
|
||
if (type === 'pre' && currentStep.value >= 0) {
|
||
if (currentStep.value === 0) {
|
||
emit('back');
|
||
} else {
|
||
currentStep.value--;
|
||
}
|
||
}
|
||
};
|
||
|
||
// #region 数据导入
|
||
|
||
// 定义表格列字段
|
||
const columns: any = ref([
|
||
// { dataIndex: 'loadcaseName', title: '所属分析项', width: 200, ellipsis: true },
|
||
]);
|
||
|
||
// 当前选中的文件名
|
||
const currentFileName = ref('');
|
||
|
||
// 导入训练源数据
|
||
const importFileFun = async (file: any) => {
|
||
const req = {
|
||
userId: currentUserId,
|
||
file: file.raw,
|
||
trainingModelId: props.modelForm.id,
|
||
};
|
||
const res: any = await handleLoadDataApi(req);
|
||
|
||
if (res.code === 200) {
|
||
ElMessage.success('文件上传成功,正在处理数据,请稍后...');
|
||
createConnection();
|
||
}
|
||
currentFileName.value = file.name;
|
||
};
|
||
const getImportedFileResultFun = async () => {
|
||
const req = {
|
||
modelId: props.modelForm.id,
|
||
};
|
||
const res: any = await getHandleLoadDataResultApi(req);
|
||
if (res.code === 200 && res.data) {
|
||
const sourceTitle = res.data.source_title;
|
||
const sourceData = res.data.source_data;
|
||
const averageData = res.data.average_data;
|
||
// const sourceCurveData = res.data.source_curve_data;
|
||
setTableColumnsFun(sourceTitle);
|
||
trainDataTableRef.value?.setDataFun(sourceData);
|
||
steadyAverageData.value = averageData;
|
||
// trainingSourceData.value = sourceData;
|
||
}
|
||
};
|
||
|
||
const setTableColumnsFun = (sourceTitle: any) => {
|
||
const cols: any[] = [];
|
||
sourceTitle.forEach((item: any) => {
|
||
cols.push({
|
||
key: Object.keys(item)[0],
|
||
field: Object.keys(item)[0],
|
||
title: Object.values(item)[0],
|
||
ellipsis: true,
|
||
width: 150,
|
||
isShow: true,
|
||
});
|
||
});
|
||
columns.value = cols;
|
||
};
|
||
|
||
const steadyAverageData = ref<any[]>([]);
|
||
|
||
const disposeForm = reactive({
|
||
inputColumn: [], // 输入列
|
||
outputColumn: [], // 输出列
|
||
proportion: 8, // 训练测试比列
|
||
filterAlgorithm: '', // 数据处理算法
|
||
effectColumn: [], // 生效列
|
||
method: 1, // 数据处理方式
|
||
});
|
||
|
||
// const filterAlgorithmList = [
|
||
// { label: '数据加载', value: '1' },
|
||
// { label: '低通过滤', value: '2' },
|
||
// { label: '高通过滤', value: '3' },
|
||
// { label: '高斯过滤', value: '4' },
|
||
// { label: '归一化', value: '5' },
|
||
// { label: '正则化', value: '6' },
|
||
// ];
|
||
// 设置输入输出列
|
||
const confirmSetColumnFun = async () => {
|
||
if (disposeForm.inputColumn.length === 0) {
|
||
ElMessage.warning('请设置特征输入列!');
|
||
return;
|
||
}
|
||
if (disposeForm.outputColumn.length === 0) {
|
||
ElMessage.warning('请设置标签输出列!');
|
||
return;
|
||
}
|
||
if (hasCommonItem(disposeForm.inputColumn, disposeForm.outputColumn)) {
|
||
ElMessage.warning('特征输入列和标签输出列不能出现重复属性!');
|
||
return;
|
||
}
|
||
const req = {
|
||
modelId: props.modelForm.id,
|
||
inputColumns: disposeForm.inputColumn,
|
||
outputColumns: disposeForm.outputColumn,
|
||
};
|
||
const res: any = await setTrainingDataInputOutputColumnApi(req);
|
||
if (res.code === 200) {
|
||
dataSetDialogVisible.value = false;
|
||
ElMessage.success('设置训练数据列成功!');
|
||
}
|
||
|
||
// if (disposeForm.filterAlgorithm.length === 0) {
|
||
// ElMessage.warning('请选择数据预处理算法');
|
||
// return;
|
||
// }
|
||
// openDisposeDataModal(false);
|
||
};
|
||
/** 数据处理 */
|
||
|
||
// const algorithmParamForm = reactive({
|
||
// /** 算法名 */
|
||
// algorithm: '',
|
||
// /** 激活函数定义/选择 */
|
||
// activateFun: 'sigmod',
|
||
// /** 损失函数设定 */
|
||
// lossFun: 'mse',
|
||
// /** 优化函数 */
|
||
// optimizeFun: 'sgd',
|
||
// /** 导出模型格式 */
|
||
// exportFormat: '.pt',
|
||
// /** 训练集比列 */
|
||
// trainingRatio: 80,
|
||
// /** 批量加载大小 */
|
||
// loadSize: 32,
|
||
// /** 学习率 */
|
||
// studyPercent: 0.001,
|
||
// /** 多项式拟合阶数 */
|
||
// stepCounts: 3,
|
||
// /** 训练轮次损失打印 */
|
||
// roundPrint: 10,
|
||
// /** 训练轮次 */
|
||
// round: 1000,
|
||
// /** 是否开启数据预处理 */
|
||
// preDisposeData: true,
|
||
// /** 数据处理方式 */
|
||
// disposeMethod: 'minmax',
|
||
// /** 数据乱序 */
|
||
// dataNoOrder: true,
|
||
// });
|
||
// #endregion
|
||
const algorithmParamForm = reactive<any>({});
|
||
|
||
const paramSettingRef: any = ref(null);
|
||
const beginTrainingFun = async () => {
|
||
const formData = await paramSettingRef.value?.getFormDataFun();
|
||
const req = {
|
||
userId: currentUserId,
|
||
modelId: props.modelForm.id,
|
||
...formData,
|
||
};
|
||
const res: any = await submitTrainingApi(req);
|
||
if (res.code === 200) {
|
||
ElMessage.success('模型训练已开始!');
|
||
currentStep.value = 2;
|
||
beginRefreshLog();
|
||
}
|
||
};
|
||
const stopTrainingFun = async () => {
|
||
const req = {
|
||
modelId: props.modelForm.id,
|
||
};
|
||
const res: any = await stopTrainingApi(req);
|
||
if (res.code === 200) {
|
||
ElMessage.success('训练已终止!');
|
||
}
|
||
};
|
||
// #endregion
|
||
// #region 训练板块 训练曲线
|
||
const logChartRef = ref<HTMLDivElement | null>(null);
|
||
|
||
const lastLogContent = ref<string[]>([]);
|
||
const allLogContent = ref('');
|
||
const logStatus = ref('refresh');
|
||
|
||
const lastLogRef = ref<HTMLElement | null>(null);
|
||
const allLogLines = ref<string[]>([]); // 原始每行文本(未转义)
|
||
const allLogLinesHtml = ref<string[]>([]); // 每行已转义的 html
|
||
const allLogScrollbar = ref<InstanceType<typeof Object> | null>(null);
|
||
const lastLogContentHtml = computed(() => {
|
||
return lastLogContent.value.join('<br/>');
|
||
});
|
||
const escapeHtml = (str: string) => {
|
||
return str
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
};
|
||
|
||
const renderCurveFun = (curveData: any) => {
|
||
initEcharts('curve', 'curve', 'lineChart', {
|
||
title: {
|
||
text: '',
|
||
},
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
},
|
||
legend: {
|
||
top: 0,
|
||
data: curveData.legend,
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
bottom: '3%',
|
||
containLabel: true,
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
boundaryGap: false,
|
||
data: curveData.xAxis,
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
},
|
||
series: curveData.series.map((item: any) => {
|
||
return {
|
||
name: item.name,
|
||
type: 'line',
|
||
stack: 'Total',
|
||
smooth: true,
|
||
data: item.data,
|
||
symbol: 'none',
|
||
};
|
||
}),
|
||
});
|
||
};
|
||
const scrollLastLogToBottom = () => {
|
||
nextTick(() => {
|
||
const el = lastLogRef.value || (document.querySelector('.model-training .log') as HTMLElement | null);
|
||
if (el && currentStep.value === 2) {
|
||
el.scrollTop = el.scrollHeight;
|
||
}
|
||
});
|
||
};
|
||
const scrollAllLogToBottom = () => {
|
||
nextTick(() => {
|
||
// el-scrollbar 的 wrap 元素类名为 el-scrollbar__wrap
|
||
const wrap = document.querySelector('.all-log-container .el-scrollbar__wrap') as HTMLElement | null;
|
||
if (wrap) {
|
||
wrap.scrollTop = wrap.scrollHeight;
|
||
}
|
||
});
|
||
};
|
||
const getTrainingResultFun = async () => {
|
||
const res: any = await getTrainingResultApi({ modelId: props.modelForm.id });
|
||
if (res.data) {
|
||
const raw = res.data.log ?? '';
|
||
const lines = raw.split(/\r?\n/);
|
||
const lastLines = lines.slice(Math.max(0, lines.length - 200));
|
||
lastLogContent.value = lastLines.map((l: any) => escapeHtml(l)); // 保留最近 200 行
|
||
allLogLines.value = lines;
|
||
allLogLinesHtml.value = lines.map((l: any) => escapeHtml(l));
|
||
allLogContent.value = escapeHtml(raw).replace(/\r?\n/g, '<br/>');
|
||
|
||
const curve = res.data?.result?.source_curve_data;
|
||
if (curve) {
|
||
renderCurveFun(curve);
|
||
}
|
||
scrollLastLogToBottom();
|
||
}
|
||
};
|
||
let logTimer: any = null;
|
||
const stopRefreshLog = () => {
|
||
clearInterval(logTimer);
|
||
logStatus.value = 'noRefresh';
|
||
};
|
||
|
||
const beginRefreshLog = () => {
|
||
getTrainingResultFun();
|
||
logTimer = setInterval(() => {
|
||
getTrainingResultFun();
|
||
logStatus.value = 'refresh';
|
||
}, 10000);
|
||
};
|
||
const allLogVisible = ref(false);
|
||
const showAllLog = () => {
|
||
allLogVisible.value = true;
|
||
scrollAllLogToBottom();
|
||
};
|
||
const closeAllLogFun = () => {
|
||
allLogVisible.value = false;
|
||
};
|
||
|
||
const websocketRef = ref<WebSocket | null>(null);
|
||
const createConnection = () => {
|
||
const userId = currentUserId;
|
||
const modelId = props.modelForm?.id ?? '';
|
||
|
||
const wsUrl: any = getModelTrainingWebSocketUrl(userId, modelId);
|
||
console.log('wsUrl', wsUrl);
|
||
|
||
// 关闭已有连接
|
||
try {
|
||
websocketRef.value?.close();
|
||
} catch (e) {
|
||
console.error('关闭旧的 websocket 连接失败', e);
|
||
}
|
||
|
||
const ws = new WebSocket(wsUrl);
|
||
websocketRef.value = ws;
|
||
|
||
ws.onopen = () => {
|
||
console.log('[modelTraining] websocket opened:', wsUrl);
|
||
};
|
||
|
||
ws.onmessage = (event) => {
|
||
const payload = event.data;
|
||
console.log('[modelTraining] websocket message received:', payload);
|
||
if (payload) {
|
||
const msg = JSON.parse(payload);
|
||
if (msg.type === 'connection') {
|
||
console.log('[modelTraining] websocket connection established');
|
||
} else if (msg.type === 'modelDataProcessing') {
|
||
if (msg.success) {
|
||
getImportedFileResultFun();
|
||
} else {
|
||
ElMessage.error(`${msg.message}`);
|
||
}
|
||
} else if (msg.type === 'trainingLog') {
|
||
if (msg.content) {
|
||
const newLines = msg.content.split(/\r?\n/).map((l: any) => escapeHtml(l));
|
||
lastLogContent.value.push(...newLines);
|
||
if (lastLogContent.value.length > 200) {
|
||
lastLogContent.value = lastLogContent.value.slice(-200);
|
||
}
|
||
scrollLastLogToBottom();
|
||
}
|
||
}
|
||
};
|
||
};
|
||
|
||
ws.onclose = (ev) => {
|
||
console.log('[modelTraining] websocket closed', ev);
|
||
};
|
||
|
||
ws.onerror = (err) => {
|
||
console.error('[modelTraining] websocket error', err);
|
||
};
|
||
};
|
||
const getTrainingDetailFun = async () => {
|
||
if (props.modelForm.handleStatus === '待开始') {
|
||
ElMessage.info('请先导入训练数据!');
|
||
} else if (props.modelForm.handleStatus === '处理中') {
|
||
} else if (props.modelForm.handleStatus === '成功') {
|
||
getImportedFileResultFun();
|
||
getSettingColumnsFun();
|
||
getModelDetailFun();
|
||
getAlgorithmParamFun();
|
||
} else if (props.modelForm.handleStatus === '失败') {
|
||
ElMessage.warning('训练数据处理失败,请重新导入数据!');
|
||
}
|
||
};
|
||
const getModelDetailFun = async () => {
|
||
const res: any = await getModelDetailApi({ modelId: props.modelForm.id });
|
||
if (res.code === 200 && res.data) {
|
||
emit('syncModelInfo', { ...res.data });
|
||
}
|
||
};
|
||
|
||
const getAlgorithmParamFun = async () => {
|
||
const res: any = await getAlgorithmParamApi({ modelId: props.modelForm.id });
|
||
if (res.code === 200 && res.data) {
|
||
const oldParam = res.data;
|
||
for (const key in oldParam) {
|
||
algorithmParamForm[key] = oldParam[key];
|
||
}
|
||
}
|
||
};
|
||
|
||
const getSettingColumnsFun = async () => {
|
||
const res: any = await getTrainingDataInputOutputColumnApi({ modelId: props.modelForm.id });
|
||
if (res.code === 200 && res.data) {
|
||
disposeForm.inputColumn = res.data.inputLabels || [];
|
||
disposeForm.outputColumn = res.data.outputLabels || [];
|
||
}
|
||
};
|
||
const openDataSetDialogFun = () => {
|
||
dataSetDialogVisible.value = true;
|
||
};
|
||
|
||
const onLogStatusChangeFun = (value: string) => {
|
||
if (value === 'refresh' && currentStep.value === 2) {
|
||
beginRefreshLog();
|
||
} else {
|
||
stopRefreshLog();
|
||
}
|
||
};
|
||
watchEffect(() => {
|
||
if (currentStep.value !== 2) {
|
||
stopRefreshLog();
|
||
}
|
||
});
|
||
onMounted(() => {
|
||
createConnection();
|
||
getTrainingDetailFun();
|
||
});
|
||
onBeforeUnmount(() => {
|
||
stopRefreshLog();
|
||
});
|
||
</script>
|
||
<style scoped lang="scss">
|
||
.model-training-container {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: #fff;
|
||
padding-bottom: 60px;
|
||
|
||
.model-step {
|
||
width: 500px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.btn-line {
|
||
text-align: right;
|
||
}
|
||
|
||
.tags {
|
||
margin-right: 5px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.dispose-data {
|
||
.dynamic-curve {
|
||
height: 500px;
|
||
width: 100%;
|
||
padding: 20px;
|
||
}
|
||
}
|
||
|
||
.algorithm-select {
|
||
margin: 10px;
|
||
|
||
.algorithm-select-box {
|
||
border: 1px solid #ebeef5;
|
||
|
||
.algorithm-title {
|
||
background-color: #f5f7fa;
|
||
padding: 10px;
|
||
margin-bottom: 20px;
|
||
}
|
||
}
|
||
|
||
.action-btn {
|
||
margin-top: 10px;
|
||
text-align: right;
|
||
}
|
||
}
|
||
|
||
.model-training {
|
||
margin: 10px;
|
||
|
||
.training-btn {
|
||
text-align: right;
|
||
}
|
||
|
||
.algorithm-select-box {
|
||
border: 1px solid #ebeef5;
|
||
|
||
.algorithm-title {
|
||
background-color: #f5f7fa;
|
||
padding: 10px;
|
||
margin-bottom: 20px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.curve {
|
||
width: 100%;
|
||
height: 400px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.log {
|
||
padding: 10px 20px;
|
||
|
||
.log-item {
|
||
font-size: 14px;
|
||
color: #444;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.content {
|
||
height: calc(100% - 50px);
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.options {
|
||
position: absolute;
|
||
width: 100%;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
height: 50px;
|
||
padding: 0 20px;
|
||
background-color: #fff;
|
||
box-shadow: 0 0 10px #aaa;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
|
||
.step-btn {
|
||
margin-left: 10px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.custom-number {
|
||
min-width: 50px;
|
||
}
|
||
|
||
.tool {
|
||
display: flex;
|
||
|
||
.data-setting {
|
||
margin-left: 10px;
|
||
}
|
||
}
|
||
|
||
.log-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
|
||
.log-btn {
|
||
.el-form-item {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.log {
|
||
max-height: 200px;
|
||
overflow: auto;
|
||
}
|
||
</style>
|