Files
SPDM/src/views/data/train/modelTraining.vue
2025-11-26 15:38:41 +08:00

922 lines
28 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
};
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>