|
@@ -1,11 +1,8 @@
|
|
|
-<script setup lang='ts'>
|
|
|
|
|
|
|
+<script setup lang="ts">
|
|
|
import { ref, onMounted, nextTick, onUnmounted, watch } from 'vue';
|
|
import { ref, onMounted, nextTick, onUnmounted, watch } from 'vue';
|
|
|
import VueEcharts from 'vue-echarts';
|
|
import VueEcharts from 'vue-echarts';
|
|
|
import 'echarts';
|
|
import 'echarts';
|
|
|
import { api5000 } from '../../../utils/request';
|
|
import { api5000 } from '../../../utils/request';
|
|
|
-import { useI18n } from 'vue-i18n';
|
|
|
|
|
-
|
|
|
|
|
-const { t } = useI18n()
|
|
|
|
|
|
|
|
|
|
interface HistoryDataItem {
|
|
interface HistoryDataItem {
|
|
|
dataset_id: number;
|
|
dataset_id: number;
|
|
@@ -35,36 +32,36 @@ interface ScatterDataResponse {
|
|
|
const props = defineProps({
|
|
const props = defineProps({
|
|
|
showLineChart: {
|
|
showLineChart: {
|
|
|
type: Boolean,
|
|
type: Boolean,
|
|
|
- default: true
|
|
|
|
|
|
|
+ default: true,
|
|
|
},
|
|
},
|
|
|
showInitScatterChart: {
|
|
showInitScatterChart: {
|
|
|
type: Boolean,
|
|
type: Boolean,
|
|
|
- default: true
|
|
|
|
|
|
|
+ default: true,
|
|
|
},
|
|
},
|
|
|
showMidScatterChart: {
|
|
showMidScatterChart: {
|
|
|
type: Boolean,
|
|
type: Boolean,
|
|
|
- default: true
|
|
|
|
|
|
|
+ default: true,
|
|
|
},
|
|
},
|
|
|
showFinalScatterChart: {
|
|
showFinalScatterChart: {
|
|
|
type: Boolean,
|
|
type: Boolean,
|
|
|
- default: true
|
|
|
|
|
|
|
+ default: true,
|
|
|
},
|
|
},
|
|
|
lineChartPathParam: {
|
|
lineChartPathParam: {
|
|
|
type: String,
|
|
type: String,
|
|
|
- default: 'reduce'
|
|
|
|
|
|
|
+ default: 'reflux',
|
|
|
},
|
|
},
|
|
|
initScatterModelId: {
|
|
initScatterModelId: {
|
|
|
type: Number,
|
|
type: Number,
|
|
|
- default: 6
|
|
|
|
|
|
|
+ default: 6,
|
|
|
},
|
|
},
|
|
|
midScatterModelId: {
|
|
midScatterModelId: {
|
|
|
type: Number,
|
|
type: Number,
|
|
|
- default: 7
|
|
|
|
|
|
|
+ default: 7,
|
|
|
},
|
|
},
|
|
|
finalScatterModelId: {
|
|
finalScatterModelId: {
|
|
|
type: Number,
|
|
type: Number,
|
|
|
- default: 17
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ default: 17,
|
|
|
|
|
+ },
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// 定义响应式变量
|
|
// 定义响应式变量
|
|
@@ -84,29 +81,37 @@ const learningCurveImageUrl = ref('');
|
|
|
const dataIncreaseCurveImageUrl = ref('');
|
|
const dataIncreaseCurveImageUrl = ref('');
|
|
|
const selectedModelType = ref('rf'); // 默认选择随机森林
|
|
const selectedModelType = ref('rf'); // 默认选择随机森林
|
|
|
|
|
|
|
|
|
|
+// 新增:土地类型选择
|
|
|
|
|
+const selectedLandType = ref('dryland'); // 默认选择旱地
|
|
|
|
|
+
|
|
|
// 模型类型选项
|
|
// 模型类型选项
|
|
|
const modelTypeOptions = [
|
|
const modelTypeOptions = [
|
|
|
- { label: t('ModelIteration.randomForest'), value: 'rf' },
|
|
|
|
|
- { label: t('ModelIteration.xgboost'), value: 'xgbr' },
|
|
|
|
|
- { label: t('ModelIteration.gradientBoosting'), value: 'gbst' },
|
|
|
|
|
|
|
+ { label: '随机森林', value: 'rf' },
|
|
|
|
|
+ { label: 'XGBoost', value: 'xgbr' },
|
|
|
|
|
+ { label: '梯度提升', value: 'gbst' },
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
|
|
+// 土地类型验证
|
|
|
|
|
+const validateLandType = (landType: string): boolean => {
|
|
|
|
|
+ return ['dryland', 'paddy_field'].includes(landType);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
// 计算数据范围的函数
|
|
// 计算数据范围的函数
|
|
|
const calculateDataRange = (data: [number, number][]) => {
|
|
const calculateDataRange = (data: [number, number][]) => {
|
|
|
- const xValues = data.map(item => item[0]);
|
|
|
|
|
- const yValues = data.map(item => item[1]);
|
|
|
|
|
|
|
+ const xValues = data.map((item) => item[0]);
|
|
|
|
|
+ const yValues = data.map((item) => item[1]);
|
|
|
return {
|
|
return {
|
|
|
xMin: Math.min(...xValues),
|
|
xMin: Math.min(...xValues),
|
|
|
xMax: Math.max(...xValues),
|
|
xMax: Math.max(...xValues),
|
|
|
yMin: Math.min(...yValues),
|
|
yMin: Math.min(...yValues),
|
|
|
- yMax: Math.max(...yValues)
|
|
|
|
|
|
|
+ yMax: Math.max(...yValues),
|
|
|
};
|
|
};
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// 获取折线图数据
|
|
// 获取折线图数据
|
|
|
const fetchLineData = async () => {
|
|
const fetchLineData = async () => {
|
|
|
try {
|
|
try {
|
|
|
- const response = await api5000.get<HistoryDataResponse>(`/get-model-history/${props.lineChartPathParam}`);
|
|
|
|
|
|
|
+ const response = await api5000.get<HistoryDataResponse>(`/get-model-history/${props.lineChartPathParam}`);
|
|
|
const data = response.data;
|
|
const data = response.data;
|
|
|
|
|
|
|
|
const timestamps = data.timestamps;
|
|
const timestamps = data.timestamps;
|
|
@@ -120,36 +125,36 @@ const fetchLineData = async () => {
|
|
|
performanceScores[item.model_name].push(score);
|
|
performanceScores[item.model_name].push(score);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- const series = Object.keys(performanceScores).map(modelName => ({
|
|
|
|
|
|
|
+ const series = Object.keys(performanceScores).map((modelName) => ({
|
|
|
name: modelName,
|
|
name: modelName,
|
|
|
type: 'line',
|
|
type: 'line',
|
|
|
- data: performanceScores[modelName]
|
|
|
|
|
|
|
+ data: performanceScores[modelName],
|
|
|
}));
|
|
}));
|
|
|
|
|
|
|
|
ecLineOption.value = {
|
|
ecLineOption.value = {
|
|
|
tooltip: {
|
|
tooltip: {
|
|
|
- trigger: 'axis'
|
|
|
|
|
|
|
+ trigger: 'axis',
|
|
|
},
|
|
},
|
|
|
legend: {
|
|
legend: {
|
|
|
- data: Object.keys(performanceScores)
|
|
|
|
|
|
|
+ data: Object.keys(performanceScores),
|
|
|
},
|
|
},
|
|
|
grid: {
|
|
grid: {
|
|
|
- left: '3%',
|
|
|
|
|
- right: '17%',
|
|
|
|
|
|
|
+ left: '22%',
|
|
|
|
|
+ right: '22%',
|
|
|
bottom: '3%',
|
|
bottom: '3%',
|
|
|
- containLabel: true
|
|
|
|
|
|
|
+ containLabel: true,
|
|
|
},
|
|
},
|
|
|
xAxis: {
|
|
xAxis: {
|
|
|
- name: t('ModelIteration.modelIteration'),
|
|
|
|
|
|
|
+ name: '模型迭代',
|
|
|
type: 'category',
|
|
type: 'category',
|
|
|
boundaryGap: false,
|
|
boundaryGap: false,
|
|
|
- data: timestamps.map((_, index) => `${index + 1}代`)
|
|
|
|
|
|
|
+ data: timestamps.map((_, index) => `${index + 1}代`),
|
|
|
},
|
|
},
|
|
|
yAxis: {
|
|
yAxis: {
|
|
|
name: 'Score (R^2)',
|
|
name: 'Score (R^2)',
|
|
|
- type: 'value'
|
|
|
|
|
|
|
+ type: 'value',
|
|
|
},
|
|
},
|
|
|
- series
|
|
|
|
|
|
|
+ series,
|
|
|
};
|
|
};
|
|
|
console.log('ecLineOption updated:', ecLineOption.value);
|
|
console.log('ecLineOption updated:', ecLineOption.value);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
@@ -163,7 +168,10 @@ const fetchScatterData = async (modelId: number, optionRef: any) => {
|
|
|
const response = await api5000.get<ScatterDataResponse>(`/model-scatter-data/${modelId}`);
|
|
const response = await api5000.get<ScatterDataResponse>(`/model-scatter-data/${modelId}`);
|
|
|
const data = response.data;
|
|
const data = response.data;
|
|
|
|
|
|
|
|
- const scatterData = data.scatter_data;
|
|
|
|
|
|
|
+ // 将数据点格式化为两位小数,并明确指定类型为[number, number][]
|
|
|
|
|
+ const scatterData: [number, number][] = data.scatter_data.map(
|
|
|
|
|
+ ([x, y]) => [parseFloat(x.toFixed(2)), parseFloat(y.toFixed(2))] as [number, number]
|
|
|
|
|
+ );
|
|
|
const range = calculateDataRange(scatterData);
|
|
const range = calculateDataRange(scatterData);
|
|
|
const padding = 0.1;
|
|
const padding = 0.1;
|
|
|
const xMin = range.xMin - Math.abs(range.xMin * padding);
|
|
const xMin = range.xMin - Math.abs(range.xMin * padding);
|
|
@@ -177,29 +185,36 @@ const fetchScatterData = async (modelId: number, optionRef: any) => {
|
|
|
tooltip: {
|
|
tooltip: {
|
|
|
trigger: 'axis',
|
|
trigger: 'axis',
|
|
|
axisPointer: {
|
|
axisPointer: {
|
|
|
- type: 'cross'
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ type: 'cross',
|
|
|
|
|
+ },
|
|
|
|
|
+ formatter: function (params: any) {
|
|
|
|
|
+ if (params.length > 0) {
|
|
|
|
|
+ const data = params[0].data;
|
|
|
|
|
+ return `True Values: ${data[0]}<br/>Predicted Values: ${data[1]}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ return '';
|
|
|
|
|
+ },
|
|
|
},
|
|
},
|
|
|
legend: {
|
|
legend: {
|
|
|
- data: ['True vs Predicted']
|
|
|
|
|
|
|
+ data: ['True vs Predicted'],
|
|
|
},
|
|
},
|
|
|
grid: {
|
|
grid: {
|
|
|
- left: '3%',
|
|
|
|
|
- right: '22%',
|
|
|
|
|
|
|
+ left: '15%',
|
|
|
|
|
+ right: '15%',
|
|
|
bottom: '3%',
|
|
bottom: '3%',
|
|
|
- containLabel: true
|
|
|
|
|
|
|
+ containLabel: true,
|
|
|
},
|
|
},
|
|
|
xAxis: {
|
|
xAxis: {
|
|
|
name: 'True Values',
|
|
name: 'True Values',
|
|
|
type: 'value',
|
|
type: 'value',
|
|
|
- min: min,
|
|
|
|
|
- max: max
|
|
|
|
|
|
|
+ min: parseFloat(min.toFixed(2)),
|
|
|
|
|
+ max: parseFloat(max.toFixed(2)),
|
|
|
},
|
|
},
|
|
|
yAxis: {
|
|
yAxis: {
|
|
|
name: 'Predicted Values',
|
|
name: 'Predicted Values',
|
|
|
type: 'value',
|
|
type: 'value',
|
|
|
min: parseFloat(min.toFixed(2)),
|
|
min: parseFloat(min.toFixed(2)),
|
|
|
- max: parseFloat(max.toFixed(2))
|
|
|
|
|
|
|
+ max: parseFloat(max.toFixed(2)),
|
|
|
},
|
|
},
|
|
|
series: [
|
|
series: [
|
|
|
{
|
|
{
|
|
@@ -209,23 +224,23 @@ const fetchScatterData = async (modelId: number, optionRef: any) => {
|
|
|
symbolSize: 10,
|
|
symbolSize: 10,
|
|
|
itemStyle: {
|
|
itemStyle: {
|
|
|
color: '#1f77b4',
|
|
color: '#1f77b4',
|
|
|
- opacity: 0.7
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ opacity: 0.7,
|
|
|
|
|
+ },
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
|
name: 'Trendline',
|
|
name: 'Trendline',
|
|
|
type: 'line',
|
|
type: 'line',
|
|
|
data: [
|
|
data: [
|
|
|
[min, min],
|
|
[min, min],
|
|
|
- [max, max]
|
|
|
|
|
|
|
+ [max, max],
|
|
|
],
|
|
],
|
|
|
lineStyle: {
|
|
lineStyle: {
|
|
|
type: 'dashed',
|
|
type: 'dashed',
|
|
|
color: '#ff7f0e',
|
|
color: '#ff7f0e',
|
|
|
- width: 2
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- ]
|
|
|
|
|
|
|
+ width: 2,
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
};
|
|
};
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('获取散点图数据失败:', error);
|
|
console.error('获取散点图数据失败:', error);
|
|
@@ -235,14 +250,21 @@ const fetchScatterData = async (modelId: number, optionRef: any) => {
|
|
|
// 新增:获取学习曲线图片
|
|
// 新增:获取学习曲线图片
|
|
|
const fetchLearningCurveImage = async () => {
|
|
const fetchLearningCurveImage = async () => {
|
|
|
try {
|
|
try {
|
|
|
|
|
+ // 验证土地类型参数
|
|
|
|
|
+ if (!validateLandType(selectedLandType.value)) {
|
|
|
|
|
+ console.error('无效的土地类型参数:', selectedLandType.value);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const response = await api5000.get('/latest-learning-curve', {
|
|
const response = await api5000.get('/latest-learning-curve', {
|
|
|
params: {
|
|
params: {
|
|
|
- data_type: 'reduce',
|
|
|
|
|
- model_type: selectedModelType.value
|
|
|
|
|
|
|
+ data_type: 'reflux',
|
|
|
|
|
+ model_type: selectedModelType.value,
|
|
|
|
|
+ land_type: selectedLandType.value,
|
|
|
},
|
|
},
|
|
|
- responseType: 'blob'
|
|
|
|
|
|
|
+ responseType: 'blob',
|
|
|
});
|
|
});
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const blob = new Blob([response.data], { type: 'image/png' });
|
|
const blob = new Blob([response.data], { type: 'image/png' });
|
|
|
learningCurveImageUrl.value = URL.createObjectURL(blob);
|
|
learningCurveImageUrl.value = URL.createObjectURL(blob);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
@@ -253,13 +275,20 @@ const fetchLearningCurveImage = async () => {
|
|
|
// 新增:获取数据增长曲线图片
|
|
// 新增:获取数据增长曲线图片
|
|
|
const fetchDataIncreaseCurveImage = async () => {
|
|
const fetchDataIncreaseCurveImage = async () => {
|
|
|
try {
|
|
try {
|
|
|
|
|
+ // 验证土地类型参数
|
|
|
|
|
+ if (!validateLandType(selectedLandType.value)) {
|
|
|
|
|
+ console.error('无效的土地类型参数:', selectedLandType.value);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const response = await api5000.get('/latest-data-increase-curve', {
|
|
const response = await api5000.get('/latest-data-increase-curve', {
|
|
|
params: {
|
|
params: {
|
|
|
- data_type: 'reduce',
|
|
|
|
|
|
|
+ data_type: 'reflux',
|
|
|
|
|
+ land_type: selectedLandType.value,
|
|
|
},
|
|
},
|
|
|
- responseType: 'blob'
|
|
|
|
|
|
|
+ responseType: 'blob',
|
|
|
});
|
|
});
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const blob = new Blob([response.data], { type: 'image/png' });
|
|
const blob = new Blob([response.data], { type: 'image/png' });
|
|
|
dataIncreaseCurveImageUrl.value = URL.createObjectURL(blob);
|
|
dataIncreaseCurveImageUrl.value = URL.createObjectURL(blob);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
@@ -273,6 +302,14 @@ watch(selectedModelType, () => {
|
|
|
fetchDataIncreaseCurveImage();
|
|
fetchDataIncreaseCurveImage();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+// 监听土地类型变化,重新获取图片和散点图数据
|
|
|
|
|
+watch(selectedLandType, () => {
|
|
|
|
|
+ fetchLearningCurveImage();
|
|
|
|
|
+ fetchDataIncreaseCurveImage();
|
|
|
|
|
+ const modelId = selectedLandType.value === 'dryland' ? 35 : 36;
|
|
|
|
|
+ fetchScatterData(modelId, ecInitScatterOption);
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
// 定义调整图表大小的函数
|
|
// 定义调整图表大小的函数
|
|
|
const resizeCharts = () => {
|
|
const resizeCharts = () => {
|
|
|
nextTick(() => {
|
|
nextTick(() => {
|
|
@@ -285,7 +322,10 @@ const resizeCharts = () => {
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
onMounted(async () => {
|
|
|
if (props.showLineChart) await fetchLineData();
|
|
if (props.showLineChart) await fetchLineData();
|
|
|
- if (props.showInitScatterChart) await fetchScatterData(props.initScatterModelId, ecInitScatterOption);
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 根据土地类型选择初始散点图模型ID
|
|
|
|
|
+ const initialModelId = selectedLandType.value === 'dryland' ? 35 : 36;
|
|
|
|
|
+ if (props.showInitScatterChart) await fetchScatterData(initialModelId, ecInitScatterOption);
|
|
|
if (props.showMidScatterChart) await fetchScatterData(props.midScatterModelId, ecMidScatterOption);
|
|
if (props.showMidScatterChart) await fetchScatterData(props.midScatterModelId, ecMidScatterOption);
|
|
|
if (props.showFinalScatterChart) await fetchScatterData(props.finalScatterModelId, ecFinalScatterOption);
|
|
if (props.showFinalScatterChart) await fetchScatterData(props.finalScatterModelId, ecFinalScatterOption);
|
|
|
|
|
|
|
@@ -303,7 +343,7 @@ onMounted(async () => {
|
|
|
// 组件卸载时移除事件监听器
|
|
// 组件卸载时移除事件监听器
|
|
|
onUnmounted(() => {
|
|
onUnmounted(() => {
|
|
|
window.removeEventListener('resize', resizeCharts);
|
|
window.removeEventListener('resize', resizeCharts);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 清理Blob URL
|
|
// 清理Blob URL
|
|
|
if (learningCurveImageUrl.value) {
|
|
if (learningCurveImageUrl.value) {
|
|
|
URL.revokeObjectURL(learningCurveImageUrl.value);
|
|
URL.revokeObjectURL(learningCurveImageUrl.value);
|
|
@@ -316,30 +356,26 @@ onUnmounted(() => {
|
|
|
|
|
|
|
|
<template>
|
|
<template>
|
|
|
<div class="container">
|
|
<div class="container">
|
|
|
- <template v-if="showInitScatterChart">
|
|
|
|
|
- <!-- 散点图 -->
|
|
|
|
|
- <h2 class="chart-header">{{ $t('ModelIteration.scatterPlotTitle') }}</h2>
|
|
|
|
|
- <div class="chart-container">
|
|
|
|
|
- <VueEcharts :option="ecInitScatterOption" ref="ecInitScatterOptionRef" />
|
|
|
|
|
|
|
+ <!-- 主标题区域,包含标题和土地类型按钮组 -->
|
|
|
|
|
+ <div class="main-title-row">
|
|
|
|
|
+ <h1 class="main-title">模型迭代可视化</h1>
|
|
|
|
|
+ <div class="land-type-buttons">
|
|
|
|
|
+ <button :class="['land-button', 'dryland-button', selectedLandType === 'dryland' ? 'active' : '']"
|
|
|
|
|
+ @click="selectedLandType = 'dryland'">
|
|
|
|
|
+ 旱地
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button :class="['land-button', 'paddy-button', selectedLandType === 'paddy_field' ? 'active' : '']"
|
|
|
|
|
+ @click="selectedLandType = 'paddy_field'">
|
|
|
|
|
+ 水田
|
|
|
|
|
+ </button>
|
|
|
</div>
|
|
</div>
|
|
|
- </template>
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <h2 class="chart-header">{{ $t('ModelIteration.modelPerformanceAnalysis') }}</h2>
|
|
|
|
|
- <!-- 模型性能分析部分 -->
|
|
|
|
|
- <div class="analysis-section">
|
|
|
|
|
- <!-- 数据增长曲线模块 -->
|
|
|
|
|
- <div class="chart-module">
|
|
|
|
|
- <h2 class="chart-header">{{ $t('ModelIteration.dataIncreaseCurve') }}</h2>
|
|
|
|
|
- <div class="image-chart-item">
|
|
|
|
|
- <div class="image-container">
|
|
|
|
|
- <img v-if="dataIncreaseCurveImageUrl" :src="dataIncreaseCurveImageUrl" :alt="$t('ModelIteration.dataIncreaseCurve')" >
|
|
|
|
|
- <div v-else class="image-placeholder">{{ $t('ModelIteration.loading') }}</div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <!-- 学习曲线模块 -->
|
|
|
|
|
- <div class="chart-module">
|
|
|
|
|
- <h2 class="chart-header">{{ $t('ModelIteration.learningCurve') }}</h2>
|
|
|
|
|
|
|
+ <!-- 学习曲线模块 - 移到最上方 -->
|
|
|
|
|
+ <div class="chart-module">
|
|
|
|
|
+ <!-- 学习曲线标题和模型选择器在同一行 -->
|
|
|
|
|
+ <div class="chart-title-row">
|
|
|
|
|
+ <h2 class="chart-header">学习曲线</h2>
|
|
|
<div class="model-selector-wrapper">
|
|
<div class="model-selector-wrapper">
|
|
|
<select v-model="selectedModelType" class="model-select">
|
|
<select v-model="selectedModelType" class="model-select">
|
|
|
<option v-for="option in modelTypeOptions" :key="option.value" :value="option.value">
|
|
<option v-for="option in modelTypeOptions" :key="option.value" :value="option.value">
|
|
@@ -347,14 +383,35 @@ onUnmounted(() => {
|
|
|
</option>
|
|
</option>
|
|
|
</select>
|
|
</select>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="image-chart-item">
|
|
|
|
|
- <div class="image-container">
|
|
|
|
|
- <img v-if="learningCurveImageUrl" :src="learningCurveImageUrl" :alt="$t('ModelIteration.learningCurve')" >
|
|
|
|
|
- <div v-else class="image-placeholder">{{ $t('ModelIteration.loading') }}</div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="chart-container">
|
|
|
|
|
+ <div class="image-container">
|
|
|
|
|
+ <img v-if="learningCurveImageUrl" :src="learningCurveImageUrl" alt="学习曲线" />
|
|
|
|
|
+ <div v-else class="image-placeholder">加载中...</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 数据增长曲线模块 - 移到中间 -->
|
|
|
|
|
+ <div class="chart-module">
|
|
|
|
|
+ <h2 class="chart-header">数据增长曲线</h2>
|
|
|
|
|
+ <div class="chart-container">
|
|
|
|
|
+ <div class="image-container">
|
|
|
|
|
+ <img v-if="dataIncreaseCurveImageUrl" :src="dataIncreaseCurveImageUrl" alt="数据增长曲线" />
|
|
|
|
|
+ <div v-else class="image-placeholder">加载中...</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 散点图模块 - 移到最下方 -->
|
|
|
|
|
+ <template v-if="showInitScatterChart">
|
|
|
|
|
+ <div class="chart-module">
|
|
|
|
|
+ <h2 class="chart-header">散点图</h2>
|
|
|
|
|
+ <div class="chart-container">
|
|
|
|
|
+ <VueEcharts :option="ecInitScatterOption" ref="ecInitScatterOptionRef" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
@@ -363,19 +420,37 @@ onUnmounted(() => {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- justify-content: center;
|
|
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
- height: 100%;
|
|
|
|
|
- gap: 20px;
|
|
|
|
|
|
|
+ gap: 30px;
|
|
|
color: #000;
|
|
color: #000;
|
|
|
background-color: white;
|
|
background-color: white;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 主标题样式 */
|
|
|
|
|
+.main-title {
|
|
|
|
|
+ font-size: 28px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ color: #2c3e50;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ margin: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.chart-header {
|
|
.chart-header {
|
|
|
font-size: 18px;
|
|
font-size: 18px;
|
|
|
font-weight: bold;
|
|
font-weight: bold;
|
|
|
- margin-bottom: 10px;
|
|
|
|
|
color: #2c3e50;
|
|
color: #2c3e50;
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 图表标题行 - 用于放置标题和模型选择器 */
|
|
|
|
|
+.chart-title-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+ gap: 20px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.sub-title {
|
|
.sub-title {
|
|
@@ -383,42 +458,45 @@ onUnmounted(() => {
|
|
|
font-weight: 700;
|
|
font-weight: 700;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.chart-container {
|
|
|
|
|
|
|
+.chart-module {
|
|
|
width: 85%;
|
|
width: 85%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.chart-container {
|
|
|
|
|
+ width: 100%;
|
|
|
height: 450px;
|
|
height: 450px;
|
|
|
- margin: 0 auto;
|
|
|
|
|
- margin-bottom: 20px;
|
|
|
|
|
background-color: white;
|
|
background-color: white;
|
|
|
border-radius: 8px;
|
|
border-radius: 8px;
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.VueEcharts {
|
|
.VueEcharts {
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
height: 100%;
|
|
height: 100%;
|
|
|
- margin: 0 10px;
|
|
|
|
|
background-color: white;
|
|
background-color: white;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/* 新增:图片图表样式 */
|
|
|
|
|
-.image-charts-section {
|
|
|
|
|
- width: 90%;
|
|
|
|
|
- margin: 30px auto;
|
|
|
|
|
- padding: 20px;
|
|
|
|
|
|
|
+/* 图片容器样式 - 与散点图尺寸一致 */
|
|
|
|
|
+.image-container {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 450px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
background-color: #f8f9fa;
|
|
background-color: #f8f9fa;
|
|
|
- border-radius: 8px;
|
|
|
|
|
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.model-type-selector {
|
|
|
|
|
- margin: 20px 0;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.model-type-selector label {
|
|
|
|
|
- margin-right: 10px;
|
|
|
|
|
- font-weight: bold;
|
|
|
|
|
- color: #2c3e50;
|
|
|
|
|
|
|
+/* 模型选择器样式 */
|
|
|
|
|
+.model-selector-wrapper {
|
|
|
|
|
+ margin: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.model-select {
|
|
.model-select {
|
|
@@ -427,39 +505,15 @@ onUnmounted(() => {
|
|
|
border-radius: 4px;
|
|
border-radius: 4px;
|
|
|
background-color: white;
|
|
background-color: white;
|
|
|
font-size: 14px;
|
|
font-size: 14px;
|
|
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: border-color 0.3s ease;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.image-charts-container {
|
|
|
|
|
- display: grid;
|
|
|
|
|
- grid-template-columns: 1fr 1fr;
|
|
|
|
|
- gap: 30px;
|
|
|
|
|
- margin-top: 20px;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.image-chart-item {
|
|
|
|
|
- background-color: white;
|
|
|
|
|
- padding: 20px;
|
|
|
|
|
- border-radius: 8px;
|
|
|
|
|
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.image-chart-title {
|
|
|
|
|
- font-size: 16px;
|
|
|
|
|
- font-weight: bold;
|
|
|
|
|
- margin-bottom: 15px;
|
|
|
|
|
- color: #2c3e50;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.image-container {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 400px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- background-color: #f8f9fa;
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
|
|
+.model-select:focus {
|
|
|
|
|
+ outline: none;
|
|
|
|
|
+ border-color: #3498db;
|
|
|
|
|
+ box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.image-container img {
|
|
.image-container img {
|
|
@@ -494,19 +548,153 @@ onUnmounted(() => {
|
|
|
color: #333;
|
|
color: #333;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/* 土地类型按钮样式 */
|
|
|
|
|
+.main-title-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.land-type-buttons {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.land-button {
|
|
|
|
|
+ padding: 8px 16px;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dryland-button {
|
|
|
|
|
+ background-color: #f0f0f0;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ border: 1px solid #ddd;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dryland-button.active {
|
|
|
|
|
+ background-color: #d4edda;
|
|
|
|
|
+ color: #155724;
|
|
|
|
|
+ border-color: #c3e6cb;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.paddy-button {
|
|
|
|
|
+ background-color: #f0f0f0;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ border: 1px solid #ddd;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.paddy-button.active {
|
|
|
|
|
+ background-color: #cce7ff;
|
|
|
|
|
+ color: #004085;
|
|
|
|
|
+ border-color: #b3d7ff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.land-button:not(.active) {
|
|
|
|
|
+ background-color: #e9ecef;
|
|
|
|
|
+ color: #6c757d;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 主标题行样式 */
|
|
|
|
|
+.main-title-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ width: 85%;
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 15px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 土地类型按钮组样式 */
|
|
|
|
|
+.land-type-buttons {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.land-button {
|
|
|
|
|
+ padding: 10px 20px;
|
|
|
|
|
+ border: 2px solid transparent;
|
|
|
|
|
+ border-radius: 25px;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ outline: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dryland-button {
|
|
|
|
|
+ background-color: #8B4513;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.paddy-button {
|
|
|
|
|
+ background-color: #1E90FF;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.land-button:not(.active) {
|
|
|
|
|
+ background-color: #ddd;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.land-button.active {
|
|
|
|
|
+ border-color: #333;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.land-button:hover:not(.active) {
|
|
|
|
|
+ background-color: #ccc;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/* 响应式设计 */
|
|
/* 响应式设计 */
|
|
|
@media (max-width: 768px) {
|
|
@media (max-width: 768px) {
|
|
|
- .image-charts-container {
|
|
|
|
|
- grid-template-columns: 1fr;
|
|
|
|
|
|
|
+ .container {
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ gap: 20px;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- .image-container {
|
|
|
|
|
- height: 300px;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ .main-title {
|
|
|
|
|
+ font-size: 24px;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- .chart-container {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ .chart-module {
|
|
|
width: 95%;
|
|
width: 95%;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .main-title-row {
|
|
|
|
|
+ width: 95%;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .chart-container,
|
|
|
|
|
+ .image-container {
|
|
|
height: 400px;
|
|
height: 400px;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ .chart-title-row {
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .model-selector-wrapper {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .model-select {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
-</style>
|
|
|
|
|
|
|
+</style>
|