瀏覽代碼

上传视频,模型可视化动态获取

tangbengaoyuan 2 月之前
父節點
當前提交
a972f5855a
共有 28 個文件被更改,包括 494 次插入665 次删除
  1. 29 0
      flask/app.py
  2. 二進制
      flask/software_intro.db
  3. 二進制
      flask/uploads/02093a92-0220-47fb-8105-44bf04d754df.png
  4. 二進制
      flask/uploads/05da5c85-46d1-43f8-bc7c-7086af62f71f.png
  5. 二進制
      flask/uploads/0cd34b73-d259-4dc3-a649-9d55162b9c49.jpg
  6. 二進制
      flask/uploads/10c81720-e713-4673-9df3-57e3c60c6eab.png
  7. 二進制
      flask/uploads/1de04420-9a47-4f85-a0bb-c27eaa79c263.png
  8. 二進制
      flask/uploads/43d2f03e-1dd1-4f8e-a813-20b9d184c44b.png
  9. 二進制
      flask/uploads/4cf27afa-60d5-4292-81ca-e8b510bf6d55.png
  10. 二進制
      flask/uploads/6960fa36-ad00-4856-b2d9-f8f10f08bc29.png
  11. 二進制
      flask/uploads/6e192688-76cc-4694-90e6-875434d258a7.png
  12. 二進制
      flask/uploads/7d0a8a32-97d6-42bc-8679-d99195d44951.png
  13. 二進制
      flask/uploads/977c4192-b373-4615-93b2-7f0abb4e0694.png
  14. 二進制
      flask/uploads/993411e6-352f-462c-aa11-325be2096bac.png
  15. 二進制
      flask/uploads/a32557e1-4e2e-4ae6-9395-1a86258ff91d.png
  16. 二進制
      flask/uploads/bd56314c-9543-42ff-85c1-e1e83e45756b.png
  17. 二進制
      flask/uploads/c49dbcb0-a7c9-4bd6-9e7c-c72659090005.png
  18. 二進制
      flask/uploads/d10d108f-bdd6-414d-ac4d-8f6b50a43c41.png
  19. 二進制
      flask/uploads/e4cfbd13-6f8b-45cd-b005-f6a019fd8128.png
  20. 二進制
      flask/uploads/f16c82a8-fa7e-4ca7-b172-f71c502d1238.png
  21. 二進制
      flask/uploads/f50da3f8-a6f1-4313-901f-06ec12c2538b.png
  22. 二進制
      flask/video_uploads/f1285d0a-a4d3-4a83-aad4-32028b85dcbc.mp4
  23. 4 13
      src/views/menu/IntroUpdateModal.vue
  24. 44 0
      src/views/menu/Introduce.vue
  25. 315 0
      src/views/menu/ModelIterationVisualization.vue
  26. 38 2
      src/views/menu/RichTextEditor.vue
  27. 32 324
      src/views/menu/SoilAcidReductionIterativeEvolution.vue
  28. 32 326
      src/views/menu/SoilAcidificationIterativeEvolution.vue

+ 29 - 0
flask/app.py

@@ -12,7 +12,13 @@ UPLOAD_FOLDER = 'uploads'
 if not os.path.exists(UPLOAD_FOLDER):
     os.makedirs(UPLOAD_FOLDER)
 
+# 视频上传目录
+VIDEO_UPLOAD_FOLDER = 'video_uploads'
+if not os.path.exists(VIDEO_UPLOAD_FOLDER):
+    os.makedirs(VIDEO_UPLOAD_FOLDER)
+
 app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
+app.config['VIDEO_UPLOAD_FOLDER'] = VIDEO_UPLOAD_FOLDER
 
 # 封装数据库连接函数
 def get_db_connection():
@@ -63,6 +69,7 @@ def update_software_intro(id):
         data = request.get_json()
         title = data.get('title')
         intro = data.get('intro')
+        print(f"Received title: {title}, intro: {intro}")
 
         conn = get_db_connection()
         cursor = conn.cursor()
@@ -87,11 +94,33 @@ def upload_image():
         return jsonify({'imageUrl': imageUrl})
     return jsonify({'error': '未找到图片文件'}), 400
 
+
+# 处理视频上传的路由
+@app.route('/upload-video', methods=['POST'])
+def upload_video():
+    file = request.files['video']
+    if file:
+        try:
+            filename = str(uuid.uuid4()) + '.' + file.filename.rsplit('.', 1)[1].lower()
+            file.save(os.path.join(app.config['VIDEO_UPLOAD_FOLDER'], filename))
+            videoUrl = f'http://127.0.0.1:5000/video_uploads/{filename}'
+            return jsonify({'videoUrl': videoUrl})
+        except Exception as e:
+            print(f"视频保存失败: {e}")
+            return jsonify({'error': '视频保存失败'}), 500
+    return jsonify({'error': '未找到视频文件'}), 400
+
     # 配置静态资源服务
 @app.route('/uploads/<path:filename>')
 def serve_image(filename):
     uploads_folder = os.path.join(app.root_path, 'uploads')
     return send_from_directory(uploads_folder, filename)
 
+# 配置视频静态资源服务
+@app.route('/video_uploads/<path:filename>')
+def serve_video(filename):
+    video_uploads_folder = os.path.join(app.root_path, 'video_uploads')
+    return send_from_directory(video_uploads_folder, filename)
+
 if __name__ == '__main__':
     app.run(debug=True)

二進制
flask/software_intro.db


二進制
flask/uploads/02093a92-0220-47fb-8105-44bf04d754df.png


二進制
flask/uploads/05da5c85-46d1-43f8-bc7c-7086af62f71f.png


二進制
flask/uploads/0cd34b73-d259-4dc3-a649-9d55162b9c49.jpg


二進制
flask/uploads/10c81720-e713-4673-9df3-57e3c60c6eab.png


二進制
flask/uploads/1de04420-9a47-4f85-a0bb-c27eaa79c263.png


二進制
flask/uploads/43d2f03e-1dd1-4f8e-a813-20b9d184c44b.png


二進制
flask/uploads/4cf27afa-60d5-4292-81ca-e8b510bf6d55.png


二進制
flask/uploads/6960fa36-ad00-4856-b2d9-f8f10f08bc29.png


二進制
flask/uploads/6e192688-76cc-4694-90e6-875434d258a7.png


二進制
flask/uploads/7d0a8a32-97d6-42bc-8679-d99195d44951.png


二進制
flask/uploads/977c4192-b373-4615-93b2-7f0abb4e0694.png


二進制
flask/uploads/993411e6-352f-462c-aa11-325be2096bac.png


二進制
flask/uploads/a32557e1-4e2e-4ae6-9395-1a86258ff91d.png


二進制
flask/uploads/bd56314c-9543-42ff-85c1-e1e83e45756b.png


二進制
flask/uploads/c49dbcb0-a7c9-4bd6-9e7c-c72659090005.png


二進制
flask/uploads/d10d108f-bdd6-414d-ac4d-8f6b50a43c41.png


二進制
flask/uploads/e4cfbd13-6f8b-45cd-b005-f6a019fd8128.png


二進制
flask/uploads/f16c82a8-fa7e-4ca7-b172-f71c502d1238.png


二進制
flask/uploads/f50da3f8-a6f1-4313-901f-06ec12c2538b.png


二進制
flask/video_uploads/f1285d0a-a4d3-4a83-aad4-32028b85dcbc.mp4


+ 4 - 13
src/views/menu/IntroUpdateModal.vue

@@ -52,11 +52,8 @@ const fetchData = async () => {
     isLoading.value = true;
     const response = await axios.get(`http://127.0.0.1:5000/software-intro/${props.targetId}`);
     const { title, intro } = response.data;
-    let processedIntro = intro;
-    // 处理多余的换行符
-    processedIntro = processedIntro.replace(/(\r\n)+/g, '\r\n').trim();
-    // 把换行符转换为 <br> 标签,适配富文本编辑器
-    processedIntro = processedIntro.replace(/\r\n/g, '<br>');
+    // 仅将换行符替换为 <br>,避免影响视频标签
+    let processedIntro = intro.replace(/\r\n/g, '<br>');
     updatedIntro.value.title = title;
     updatedIntro.value.intro = processedIntro;
   } catch (err: any) {
@@ -87,14 +84,8 @@ watch(() => props.targetId, (newTargetId, oldTargetId) => {
 const updateIntro = async () => {
   isUpdating.value = true;
   try {
-    // 处理富文本内容,将 <br> 标签替换回 \r\n
-    let processedIntro = updatedIntro.value.intro;
-    processedIntro = processedIntro.replace(/<br>/gi, '\r\n');
-    // 处理富文本内容,去除 <p> 和 <br> 标签
-    processedIntro = processedIntro.replace(/<p>/g, '').replace(/<\/p>/g, '\r\n');
-    // 去除多余的换行符
-    processedIntro = processedIntro.replace(/(\r\n)+/g, '\r\n').trim();
-
+    // 将 <br> 标签替换回换行符
+    let processedIntro = updatedIntro.value.intro.replace(/<br>/gi, '\r\n');
     const dataToSend = {
       title: updatedIntro.value.title,
       intro: processedIntro

+ 44 - 0
src/views/menu/Introduce.vue

@@ -63,7 +63,43 @@ onMounted(async () => {
   } finally {
     isLoading.value = false;
   }
+
+  // 页面加载完成后,监听视频播放事件
+  const container = document.querySelector('.software-intro-container');
+  if (container) {
+    container.addEventListener('play', (event) => {
+      if (event.target instanceof HTMLVideoElement) {
+        const video = event.target;
+        setVideoStyle(video);
+      }
+    }, true);
+
+    // 监听窗口大小变化事件,确保视频在窗口大小改变时也能适应
+    window.addEventListener('resize', () => {
+      const videos = container.querySelectorAll('video');
+      videos.forEach((video) => {
+        if (video instanceof HTMLVideoElement) {
+          setVideoStyle(video);
+        }
+      });
+    });
+  }
 });
+
+// 封装设置视频样式的函数
+const setVideoStyle = (video: HTMLVideoElement) => {
+  const containerWidth = (document.querySelector('.software-intro-container') as HTMLElement).offsetWidth;
+  const maxWidth = containerWidth * 0.8; // 最大宽度为容器宽度的 80%
+  if (video.videoWidth > maxWidth) {
+    video.style.width = `${maxWidth}px`;
+    video.style.height = 'auto';
+  } else {
+    video.style.width = 'auto';
+    video.style.height = 'auto';
+  }
+  video.style.maxWidth = '80%';
+  video.style.objectFit = 'contain';
+};
 </script>
 
 <style scoped lang="scss">
@@ -131,6 +167,14 @@ p {
   margin-bottom: 10px;
 }
 
+/* 控制视频尺寸 */
+video {
+  width: 80%;
+  max-width: 80%;
+  height: auto;
+  object-fit: contain;
+}
+
 /* 响应式设计 */
 @media (max-width: 768px) {
   .software-intro-container {

+ 315 - 0
src/views/menu/ModelIterationVisualization.vue

@@ -0,0 +1,315 @@
+<template>
+    <div class="container">
+      <template v-if="showLineChart">
+        <!-- 折线图表头 -->
+        <div class="chart-container">
+          <VueEcharts :option="ecLineOption" ref="ecLineOptionRef" />
+        </div>
+      </template>
+      <template v-if="showInitScatterChart">
+        <!-- 初代散点图表头 -->
+        <h2 class="chart-header">初代散点图</h2>
+        <div class="chart-container">
+          <VueEcharts :option="ecInitScatterOption" ref="ecInitScatterOptionRef" />
+        </div>
+      </template>
+      <template v-if="showMidScatterChart">
+        <!-- 中间代散点图表头 -->
+        <h2 class="chart-header">中间代散点图</h2>
+        <div class="chart-container">
+          <VueEcharts :option="ecMidScatterOption" ref="ecMidScatterOptionRef" />
+        </div>
+      </template>
+      <template v-if="showFinalScatterChart">
+        <!-- 最终代散点图表头 -->
+        <h2 class="chart-header">最终代散点图</h2>
+        <div class="chart-container">
+          <VueEcharts :option="ecFinalScatterOption" ref="ecFinalScatterOptionRef" />
+        </div>
+      </template>
+    </div>
+  </template>
+  
+  <script setup lang='ts'>
+  import { ref, onMounted, nextTick, onUnmounted, defineProps } from 'vue';
+  import VueEcharts from 'vue-echarts';
+  import 'echarts';
+  import request from '../../utils/request';
+  
+  interface HistoryDataItem {
+    dataset_id: number;
+    row_count: number;
+    model_id: number;
+    model_name: string;
+    performance_score: number;
+    timestamp: string;
+  }
+  
+  interface HistoryDataResponse {
+    data_type: string;
+    timestamps: string[];
+    row_counts: number[];
+    performance_scores: number[];
+    model_details: HistoryDataItem[];
+  }
+  
+  interface ScatterDataResponse {
+    scatter_data: [number, number][];
+    r2_score: number;
+    y_range: [number, number];
+    model_name: string;
+    model_type: string;
+  }
+  
+  const props = defineProps({
+    showLineChart: {
+      type: Boolean,
+      default: true
+    },
+    showInitScatterChart: {
+      type: Boolean,
+      default: true
+    },
+    showMidScatterChart: {
+      type: Boolean,
+      default: true
+    },
+    showFinalScatterChart: {
+      type: Boolean,
+      default: true
+    },
+    lineChartPathParam: {
+      type: String,
+      default: 'reduce'
+    },
+    initScatterModelId: {
+      type: Number,
+      default: 6
+    },
+    midScatterModelId: {
+      type: Number,
+      default: 7
+    },
+    finalScatterModelId: {
+      type: Number,
+      default: 17
+    }
+  });
+  
+  // 定义响应式变量
+  const ecLineOption = ref({});
+  const ecInitScatterOption = ref({});
+  const ecMidScatterOption = ref({});
+  const ecFinalScatterOption = ref({});
+  
+  // 定义图表引用
+  const ecLineOptionRef = ref<InstanceType<typeof VueEcharts>>();
+  const ecInitScatterOptionRef = ref<InstanceType<typeof VueEcharts>>();
+  const ecMidScatterOptionRef = ref<InstanceType<typeof VueEcharts>>();
+  const ecFinalScatterOptionRef = ref<InstanceType<typeof VueEcharts>>();
+  
+  // 计算数据范围的函数
+  const calculateDataRange = (data: [number, number][]) => {
+    const xValues = data.map(item => item[0]);
+    const yValues = data.map(item => item[1]);
+    return {
+      xMin: Math.min(...xValues),
+      xMax: Math.max(...xValues),
+      yMin: Math.min(...yValues),
+      yMax: Math.max(...yValues)
+    };
+  };
+  
+  // 获取折线图数据
+  const fetchLineData = async () => {
+    try {
+      const response = await request.get<HistoryDataResponse>(`/get-model-history/${props.lineChartPathParam}`);
+      const data = response.data;
+  
+      const timestamps = data.timestamps;
+      const performanceScores: Record<string, number[]> = {};
+  
+      data.model_details.forEach((item: HistoryDataItem) => {
+        const score = Number(item.performance_score);
+        if (!performanceScores[item.model_name]) {
+          performanceScores[item.model_name] = [];
+        }
+        performanceScores[item.model_name].push(score);
+      });
+  
+      const series = Object.keys(performanceScores).map(modelName => ({
+        name: modelName,
+        type: 'line',
+        data: performanceScores[modelName]
+      }));
+  
+      ecLineOption.value = {
+        tooltip: {
+          trigger: 'axis'
+        },
+        legend: {
+          data: Object.keys(performanceScores)
+        },
+        grid: {
+          left: '3%',
+          right: '17%',
+          bottom: '3%',
+          containLabel: true
+        },
+        xAxis: {
+          name: '模型迭代',
+          type: 'category',
+          boundaryGap: false,
+          data: timestamps.map((_, index) => `${index + 1}代`)
+        },
+        yAxis: {
+          name: 'Score (R^2)',
+          type: 'value'
+        },
+        series
+      };
+      console.log('ecLineOption updated:', ecLineOption.value);
+    } catch (error) {
+      console.error('获取折线图数据失败:', error);
+    }
+  };
+  
+  // 获取散点图数据
+  const fetchScatterData = async (modelId: number, optionRef: any) => {
+    try {
+      const response = await request.get<ScatterDataResponse>(`/model-scatter-data/${modelId}`);
+      const data = response.data;
+  
+      const scatterData = data.scatter_data;
+      const range = calculateDataRange(scatterData);
+      const padding = 0.1;
+      const xMin = range.xMin - Math.abs(range.xMin * padding);
+      const xMax = range.xMax + Math.abs(range.xMax * padding);
+      const yMin = range.yMin - Math.abs(range.yMin * padding);
+      const yMax = range.yMax + Math.abs(range.yMax * padding);
+      const min = Math.min(xMin, yMin);
+      const max = Math.max(xMax, yMax);
+  
+      optionRef.value = {
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            type: 'cross'
+          }
+        },
+        legend: {
+          data: ['True vs Predicted']
+        },
+        grid: {
+          left: '3%',
+          right: '22%',
+          bottom: '3%',
+          containLabel: true
+        },
+        xAxis: {
+          name: 'True Values',
+          type: 'value',
+          min: min,
+          max: max
+        },
+        yAxis: {
+          name: 'Predicted Values',
+          type: 'value',
+          min: parseFloat(min.toFixed(2)),
+          max: parseFloat(max.toFixed(2))
+        },
+        series: [
+          {
+            name: 'True vs Predicted',
+            type: 'scatter',
+            data: scatterData,
+            symbolSize: 10,
+            itemStyle: {
+              color: '#1f77b4',
+              opacity: 0.7
+            }
+          },
+          {
+            name: 'Trendline',
+            type: 'line',
+            data: [
+              [min, min],
+              [max, max]
+            ],
+            lineStyle: {
+              type: 'dashed',
+              color: '#ff7f0e',
+              width: 2
+            }
+          }
+        ]
+      };
+    } catch (error) {
+      console.error('获取散点图数据失败:', error);
+    }
+  };
+  
+  // 定义调整图表大小的函数
+  const resizeCharts = () => {
+    nextTick(() => {
+      if (props.showLineChart) ecLineOptionRef.value?.resize();
+      if (props.showInitScatterChart) ecInitScatterOptionRef.value?.resize();
+      if (props.showMidScatterChart) ecMidScatterOptionRef.value?.resize();
+      if (props.showFinalScatterChart) ecFinalScatterOptionRef.value?.resize();
+    });
+  };
+  
+  onMounted(async () => {
+    if (props.showLineChart) await fetchLineData();
+    if (props.showInitScatterChart) await fetchScatterData(props.initScatterModelId, ecInitScatterOption);
+    if (props.showMidScatterChart) await fetchScatterData(props.midScatterModelId, ecMidScatterOption);
+    if (props.showFinalScatterChart) await fetchScatterData(props.finalScatterModelId, ecFinalScatterOption);
+  
+    // 页面加载完成后调整图表大小
+    resizeCharts();
+  
+    // 监听窗口大小变化,调整图表大小
+    window.addEventListener('resize', resizeCharts);
+  });
+  
+  // 组件卸载时移除事件监听器
+  onUnmounted(() => {
+    window.removeEventListener('resize', resizeCharts);
+  });
+  </script>
+  
+  <style scoped>
+  .container {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    width: 100%;
+    height: 100%;
+    gap: 20px;
+  }
+  
+  .chart-header {
+    font-size: 18px;
+    font-weight: bold;
+    margin-bottom: 10px;
+  }
+  
+  .sub-title {
+    font-size: 14px;
+    font-weight: 700;
+  }
+  
+  .chart-container {
+    width: 85%;
+    height: 450px;
+    margin: 0 auto;
+    margin-bottom: 20px;
+  }
+  
+  .VueEcharts {
+    width: 100%;
+    height: 100%;
+    margin: 0 10px;
+  }
+  </style>    

+ 38 - 2
src/views/menu/RichTextEditor.vue

@@ -19,7 +19,7 @@
 </template>
 
 <script setup>
-import { ref, defineProps, defineEmits, watch } from 'vue';
+import { ref, defineProps, defineEmits, watch, onMounted } from 'vue';
 import '@wangeditor/editor/dist/css/style.css';
 import { onBeforeUnmount, shallowRef } from 'vue';
 import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
@@ -60,18 +60,32 @@ editorConfig.value.MENU_CONF['uploadImage'] = {
 // 自定义视频上传
 editorConfig.value.MENU_CONF['uploadVideo'] = {
   async customUpload(file, insertFn) {
-    console.log('上传视频', file);
+    try {
+      const formData = new FormData();
+      formData.append('video', file);
+      const response = await axios.post('http://127.0.0.1:5000/upload-video', formData, {
+        headers: {
+          'Content-Type': 'multipart/form-data'
+        }
+      });
+      const videoUrl = response.data.videoUrl;
+      insertFn(videoUrl, 'video');
+    } catch (error) {
+      console.error('视频上传失败:', error);
+    }
   },
 };
 
 const handleCreated = (editor) => {
   editorRef.value = editor;
   console.log(editorConfig.value.MENU_CONF, 'editorConfig.value');
+  console.log('Editor created:', editor);
 };
 
 const customPaste = (editor, event, callback) => {
   const text = event.clipboardData.getData('text/plain');
   if (text) {
+    console.log('Pasted text:', text);
     editor.insertText(text);
     event.preventDefault();
     callback(false);
@@ -95,6 +109,21 @@ watch(localValue, (newValue, oldValue) => {
     updateValue();
   }
 });
+
+onMounted(() => {
+  const editorElement = document.querySelector('.rich-text-editor');
+  if (editorElement) {
+    editorElement.addEventListener('play', (event) => {
+      if (event.target instanceof HTMLVideoElement) {
+        const video = event.target;
+        video.style.width = '80%';
+        video.style.maxWidth = '80%';
+        video.style.height = 'auto';
+        video.style.objectFit = 'contain';
+      }
+    }, true);
+  }
+});
 </script>
 
 <style scoped lang="scss">
@@ -178,5 +207,12 @@ watch(localValue, (newValue, oldValue) => {
   &::-webkit-scrollbar-track {
     background-color: #f1f1f1;
   }
+
+  video {
+    width: 80%;
+    max-width: 80%;
+    height: auto;
+    object-fit: contain;
+  }
 }
 </style>

+ 32 - 324
src/views/menu/SoilAcidReductionIterativeEvolution.vue

@@ -1,333 +1,41 @@
 <template>
-  <div class="container">
-    <div class="chart-container">
-      <VueEcharts :option="ecLineOption" ref="ecLineOptionRef" />
-    </div>
-    <p class="sub-title">初代散点图</p>
-    <div class="chart-container">
-      <VueEcharts :option="ecInitScatterOption" ref="ecInitScatterOptionRef" />
-    </div>
-    <p class="sub-title">中间代散点图</p>
-    <div class="chart-container">
-      <VueEcharts :option="ecMidScatterOption" ref="ecMidScatterOptionRef" />
-    </div>
-    <p class="sub-title">最终代散点图</p>
-    <div class="chart-container">
-      <VueEcharts :option="ecFinalScatterOption" ref="ecFinalScatterOptionRef" />
-    </div>
+  <div id="app">
+    <ModelIterationVisualization
+      :showLineChart="showLineChart"
+      :showInitScatterChart="showInitScatterChart"
+      :showMidScatterChart="showMidScatterChart"
+      :showFinalScatterChart="showFinalScatterChart"
+      :lineChartPathParam="lineChartPathParam"
+      :initScatterModelId="initScatterModelId"
+      :midScatterModelId="midScatterModelId"
+      :finalScatterModelId="finalScatterModelId"
+    />
   </div>
 </template>
 
-<script setup lang='ts' name=''>
-import {ref} from 'vue'
-import VueEcharts from 'vue-echarts';
-import 'echarts';
-
-const ecLineOptionRef = ref<InstanceType<typeof VueEcharts> | null>(null);
-const ecInitScatterOptionRef = ref<InstanceType<typeof VueEcharts> | null>(null);
-const ecMidScatterOptionRef = ref<InstanceType<typeof VueEcharts> | null>(null);
-const ecFinalScatterOptionRef = ref<InstanceType<typeof VueEcharts> | null>(null);
-
-// 计算数据范围的函数
-const calculateDataRange = (data) => {
-  const xValues = data.map(item => item[0]);
-  const yValues = data.map(item => item[1]);
-  return {
-    xMin: Math.min(...xValues),
-    xMax: Math.max(...xValues),
-    yMin: Math.min(...yValues),
-    yMax: Math.max(...yValues)
-  };
-};
-
-// 反酸模型  初代  散点图
-const getInitScatterOption=()=>{
-
-  const scatterData = [[0.12931, 0.10315928999999999], [0.066298, 0.10226514000000003], [0.057692, 0.10226514000000003], [0.072464, 0.10339078000000003], [0.146414, 0.10000317000000003], [0.087263, 0.09199080000000004], [0.096983, 0.09947644], [0.070505, 0.10142657000000004], [0.052497, 0.09956722000000007], [0.166515, 0.10179712], [0.063291, 0.10339078000000003]];
-
-  const range = calculateDataRange(scatterData);
-  const padding = 0.1;
-  const xMin = range.xMin - Math.abs(range.xMin * padding);
-  const xMax = range.xMax + Math.abs(range.xMax * padding);
-  const yMin = range.yMin - Math.abs(range.yMin * padding);
-  const yMax = range.yMax + Math.abs(range.yMax * padding);
-  const min = Math.min(xMin, yMin)
-  const max = Math.max(xMax, yMax)
-  return {
-    tooltip: {
-      trigger: 'axis',
-      axisPointer: {
-        type: 'cross'
-      }
-    },
-    legend: {
-      data: ['True vs Predicted']
-    },
-    grid: {
-      left: '4%',
-      right: '22%',
-      bottom: '3%',
-      containLabel: true
-    },
-    xAxis: {
-      name: 'True Values',
-      type: 'value',
-      min: min,
-      max: max
-    },
-    yAxis: {
-      name: 'Predicted Values',
-      type: 'value',
-      min: parseFloat(min.toFixed(2)),
-      max: parseFloat(max.toFixed(2))
-    },
-    series: [
-      {
-        name: 'True vs Predicted',
-        type: 'scatter',
-        data: scatterData,
-        symbolSize: 10,
-        itemStyle: {
-          color: '#1f77b4',
-          opacity: 0.7
-        }
-      },
-      {
-        name: 'Trendline',
-        type: 'line',
-        data: [
-          [min, min],
-          [max, max]
-        ],
-        lineStyle: {
-          type: 'dashed',
-          color: '#ff7f0e',
-          width: 2
-        }
-      }
-    ]
-  };
-}
-// 反酸模型  中间代  散点图
-const getMidScatterOption=()=>{
-  const scatterData = [[0.12931, 0.09595878000000002], [0.066298, 0.10344700000000003], [0.057692, 0.10344700000000003], [0.072464, 0.09011803], [0.146414, 0.0853504500000001], [0.087263, 0.07407179000000012], [0.096983, 0.10581049999999995], [0.070505, 0.09876380000000004], [0.052497, 0.08568465000000008], [0.166515, 0.13348440999999997], [0.063291, 0.09011803]];
-
-  const range = calculateDataRange(scatterData);
-  const padding = 0.1;
-  const xMin = range.xMin - Math.abs(range.xMin * padding);
-  const xMax = range.xMax + Math.abs(range.xMax * padding);
-  const yMin = range.yMin - Math.abs(range.yMin * padding);
-  const yMax = range.yMax + Math.abs(range.yMax * padding);
-  const min = Math.min(xMin, yMin)
-  const max = Math.max(xMax, yMax)
-  return {
-    tooltip: {
-      trigger: 'axis',
-      axisPointer: {
-        type: 'cross'
-      }
-    },
-    legend: {
-      data: ['True vs Predicted']
-    },
-    grid: {
-      left: '4%',
-      right: '22%',
-      bottom: '3%',
-      containLabel: true
-    },
-    xAxis: {
-      name: 'True Values',
-      type: 'value',
-      min: min,
-      max: max
-    },
-    yAxis: {
-      name: 'Predicted Values',
-      type: 'value',
-      min: parseFloat(min.toFixed(2)),
-      max: parseFloat(max.toFixed(2))
-    },
-    series: [
-      {
-        name: 'True vs Predicted',
-        type: 'scatter',
-        data: scatterData,
-        symbolSize: 10,
-        itemStyle: {
-          color: '#1f77b4',
-          opacity: 0.7
-        }
-      },
-      {
-        name: 'Trendline',
-        type: 'line',
-        data: [
-          [min, min],
-          [max, max]
-        ],
-        lineStyle: {
-          type: 'dashed',
-          color: '#ff7f0e',
-          width: 2
-        }
-      }
-    ]
-  };
-}
-// 反酸模型  最终代  散点图
-const getFinalScatterOption=() => {
-
-  const scatterData = [[0.12931, 0.1144982841836412], [0.066298, 0.07733075000000009],
-    [0.057692, 0.07784833000000009], [0.072464, 0.09429906104591025],
-    [0.146414, 0.11774272000000004], [0.087263, 0.07587641627546172],
-    [0.096983, 0.12344755999999983], [0.070505, 0.06424743000000006],
-    [0.052497, 0.07665224568865422], [0.166515, 0.15591779999999988],
-    [0.063291, 0.09275175104591024]];
-
-  const range = calculateDataRange(scatterData);
-  const padding = 0.1;
-  const xMin = range.xMin - Math.abs(range.xMin * padding);
-  const xMax = range.xMax + Math.abs(range.xMax * padding);
-  const yMin = range.yMin - Math.abs(range.yMin * padding);
-  const yMax = range.yMax + Math.abs(range.yMax * padding);
-  const min = Math.min(xMin, yMin)
-  const max = Math.max(xMax, yMax)
-  return {
-    tooltip: {
-      trigger: 'axis',
-      axisPointer: {
-        type: 'cross'
-      }
-    },
-    legend: {
-      data: ['True vs Predicted']
-    },
-    grid: {
-      left: '4%',
-      right: '22%',
-      bottom: '3%',
-      containLabel: true
-    },
-    xAxis: {
-      name: 'True Values',
-      type: 'value',
-      min: min,
-      max: max
-    },
-    yAxis: {
-      name: 'Predicted Values',
-      type: 'value',
-      min: parseFloat(min.toFixed(2)),
-      max: parseFloat(max.toFixed(2))
-    },
-    series: [
-      {
-        name: 'True vs Predicted',
-        type: 'scatter',
-        data: scatterData,
-        symbolSize: 10,
-        itemStyle: {
-          color: '#1f77b4',
-          opacity: 0.7
-        }
-      },
-      {
-        name: 'Trendline',
-        type: 'line',
-        data: [
-          [min, min],
-          [max, max]
-        ],
-        lineStyle: {
-          type: 'dashed',
-          color: '#ff7f0e',
-          width: 2
-        }
-      }
-    ]
-  };
-}
-
-//获取折线图配置
-const getLineOption=()=>{
-  return{
-    tooltip:{
-      trigger:'axis'
-    },
-    legend:{
-      data:['Random Forest', 'XGBoost', 'Gradient Boosting']  // 模型名称
-    },
-    grid: {
-      left: '3%',
-      right: '17%',
-      bottom: '3%',
-      containLabel: true
-    },
-    xAxis: {
-      name: '模型迭代',
-      type: 'category',
-      boundaryGap: false,
-      data: ['1代','2代','3代','4代','5代','6代','7代','8代','9代']  // train_sizes按10%递增
-    },
-    yAxis: {
-      name: 'Score (R^2)',
-      type: 'value'
-    },
-    series: [
-      {
-        name: 'Random Forest',
-        type: 'line',
-        data: [-0.07014332070467688, 0.16174446067644666, 0.45539363923645115, 0.42496898634299696, 0.4538427686692541, 0.418902219699232, 0.3944331740214546, 0.5036773554206873, 0.6231911298905934, 0.7017514238881619]
-      },
-      {
-        name: 'XGBoost',
-        type: 'line',
-        data: [-0.5854699949971149, 0.12632204933742897, 0.5552415957727412, 0.36024885147666674, 0.3983302490476677, 0.11693728875627551, 0.41317321044147026, 0.7833174829404705, 0.6626050730438451, 0.8168196535959388]
-      },
-      {
-        name: 'Gradient Boosting',
-        type: 'line',
-        data: [-0.2099996080229254, 0.2604127444757308, 0.5544068626708099, 0.37487088504748367, 0.32309657868722486, 0.24740671740183884, 0.4949107161199925, 0.694806526591159, 0.6719430144475025, 0.7889677514137288]
-      }
-    ]
-  }
-}
-
-const ecLineOption=ref(getLineOption());
-const ecInitScatterOption = ref(getInitScatterOption());
-const ecMidScatterOption = ref(getMidScatterOption());
-const ecFinalScatterOption = ref(getFinalScatterOption());
+<script setup lang='ts'>
+import { ref } from 'vue';
+import ModelIterationVisualization from './ModelIterationVisualization.vue';
 
+const showLineChart = ref(true);
+const showInitScatterChart = ref(true);
+const showMidScatterChart = ref(true);
+const showFinalScatterChart = ref(true);
 
+// 直接在代码里定义好参数
+const lineChartPathParam = ref('reduce'); 
+const initScatterModelId = ref(24);
+const midScatterModelId = ref(25);
+const finalScatterModelId = ref(26);
 </script>
 
-
 <style scoped>
-.container {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  width: 100%;
-  height: 100%;
-  gap: 20px;
-}
-
-.sub-title {
-  font-size: 14px;
-  font-weight: 700;
-}
-
-.chart-container {
-  width: 75%;
-  height: 450px;
-  margin-bottom: 20px;
-}
-
-.VueEcharts {
-  width: 100%;
-  height: 100%;
-  margin: 0 10px;
-}
-</style>
+#app {
+  font-family: Avenir, Helvetica, Arial, sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  text-align: center;
+  color: #2c3e50;
+  margin-top: 60px;
+}
+</style>    

+ 32 - 326
src/views/menu/SoilAcidificationIterativeEvolution.vue

@@ -1,335 +1,41 @@
 <template>
-  <div class="container">
-    <div class="chart-container">
-      <VueEcharts :option="ecLineOption" ref="ecLineOptionRef" />
-    </div>
-    <p class="sub-title">初代散点图</p>
-    <div class="chart-container">
-      <VueEcharts :option="ecInitScatterOption" ref="ecInitScatterOptionRef" />
-    </div>
-    <p class="sub-title">中间代散点图</p>
-    <div class="chart-container">
-      <VueEcharts :option="ecMidScatterOption" ref="ecMidScatterOptionRef" />
-    </div>
-    <p class="sub-title">最终代散点图</p>
-    <div class="chart-container">
-      <VueEcharts :option="ecFinalScatterOption" ref="ecFinalScatterOptionRef" />
-    </div>
+  <div id="app">
+    <ModelIterationVisualization
+      :showLineChart="showLineChart"
+      :showInitScatterChart="showInitScatterChart"
+      :showMidScatterChart="showMidScatterChart"
+      :showFinalScatterChart="showFinalScatterChart"
+      :lineChartPathParam="lineChartPathParam"
+      :initScatterModelId="initScatterModelId"
+      :midScatterModelId="midScatterModelId"
+      :finalScatterModelId="finalScatterModelId"
+    />
   </div>
 </template>
 
-<script setup lang='ts' name=''>
-import {ref} from 'vue'
-import VueEcharts from 'vue-echarts';
-import 'echarts';
-
-const ecLineOptionRef = ref<InstanceType<typeof VueEcharts> | null>(null);
-const ecInitScatterOptionRef = ref<InstanceType<typeof VueEcharts> | null>(null);
-const ecMidScatterOptionRef = ref<InstanceType<typeof VueEcharts> | null>(null);
-const ecFinalScatterOptionRef = ref<InstanceType<typeof VueEcharts> | null>(null);
-
-// 计算数据范围的函数
-const calculateDataRange = (data) => {
-  const xValues = data.map(item => item[0]);
-  const yValues = data.map(item => item[1]);
-  return {
-    xMin: Math.min(...xValues),
-    xMax: Math.max(...xValues),
-    yMin: Math.min(...yValues),
-    yMax: Math.max(...yValues)
-  };
-};
-
-// 反酸模型  初代  散点图
-const getInitScatterOption=()=>{
-
-  const scatterData = [[-0.003333333333333854, -0.4181333333333324], [-0.1733333333333329, -0.26733333333333265], [-0.6233333333333331, -0.3718666666666661], [-0.7088888888888892, -0.3854666666666661], [-0.3366666666666669, -0.3998666666666657], [-0.8888888888888887, -0.36439999999999934], [-0.5633333333333326, -0.6207999999999997], [-0.7333333333333325, -0.36026666666666607], [-0.3366666666666663, -0.29213333333333275], [-1.176666666666666, -0.6095999999999993], [-0.7122222222222225, -0.3807999999999989], [-0.7699999999999996, -0.3718666666666661]];
-
-  const range = calculateDataRange(scatterData);
-  const padding = 0.1;
-  const xMin = range.xMin - Math.abs(range.xMin * padding);
-  const xMax = range.xMax + Math.abs(range.xMax * padding);
-  const yMin = range.yMin - Math.abs(range.yMin * padding);
-  const yMax = range.yMax + Math.abs(range.yMax * padding);
-  const min = Math.min(xMin, yMin)
-  const max = Math.max(xMax, yMax)
-
-  return {
-    tooltip: {
-      trigger: 'axis',
-      axisPointer: {
-        type: 'cross'
-      }
-    },
-    legend: {
-      data: ['True vs Predicted']
-    },
-    grid: {
-      left: '3%',
-      right: '22%',
-      bottom: '3%',
-      containLabel: true
-    },
-    xAxis: {
-      name: 'True Values',
-      type: 'value',
-      min: min,
-      max: max
-    },
-    yAxis: {
-      name: 'Predicted Values',
-      type: 'value',
-      min: parseFloat(min.toFixed(2)),
-      max: parseFloat(max.toFixed(2))
-    },
-    series: [
-      {
-        name: 'True vs Predicted',
-        type: 'scatter',
-        data: scatterData,
-        symbolSize: 10,
-        itemStyle: {
-          color: '#1f77b4',
-          opacity: 0.7
-        }
-      },
-      {
-        name: 'Trendline',
-        type: 'line',
-        data: [
-          [min, min],
-          [max, max]
-        ],
-        lineStyle: {
-          type: 'dashed',
-          color: '#ff7f0e',
-          width: 2
-        }
-      }
-    ]
-  };
-}
-// 反酸模型  中间代  散点图
-const getMidScatterOption=()=>{
-  const scatterData = [[-0.003333333333333854, -0.5483999999999993], [-0.1733333333333329, -0.2093333333333331], [-0.6233333333333331, -0.5090000000000002], [-0.7088888888888892, -0.4281333333333331], [-0.3366666666666669, -0.4691333333333336], [-0.8888888888888887, -0.49643333333333267], [-0.5633333333333326, -0.7191999999999996], [-0.7333333333333325, -0.5024666666666666], [-0.3366666666666663, -0.37796666666666623], [-1.176666666666666, -0.6415666666666656], [-0.7122222222222225, -0.43299999999999966], [-0.7699999999999996, -0.499966666666667]];
-
-  const range = calculateDataRange(scatterData);
-  const padding = 0.1;
-  const xMin = range.xMin - Math.abs(range.xMin * padding);
-  const xMax = range.xMax + Math.abs(range.xMax * padding);
-  const yMin = range.yMin - Math.abs(range.yMin * padding);
-  const yMax = range.yMax + Math.abs(range.yMax * padding);
-  const min = Math.min(xMin, yMin)
-  const max = Math.max(xMax, yMax)
-  return {
-    tooltip: {
-      trigger: 'axis',
-      axisPointer: {
-        type: 'cross'
-      }
-    },
-    legend: {
-      data: ['True vs Predicted']
-    },
-    grid: {
-      left: '3%',
-      right: '22%',
-      bottom: '3%',
-      containLabel: true
-    },
-    xAxis: {
-      name: 'True Values',
-      type: 'value',
-      min: min,
-      max: max
-    },
-    yAxis: {
-      name: 'Predicted Values',
-      type: 'value',
-      min: parseFloat(min.toFixed(2)),
-      max: parseFloat(max.toFixed(2))
-    },
-    series: [
-      {
-        name: 'True vs Predicted',
-        type: 'scatter',
-        data: scatterData,
-        symbolSize: 10,
-        itemStyle: {
-          color: '#1f77b4',
-          opacity: 0.7
-        }
-      },
-      {
-        name: 'Trendline',
-        type: 'line',
-        data: [
-          [min, min],
-          [max, max]
-        ],
-        lineStyle: {
-          type: 'dashed',
-          color: '#ff7f0e',
-          width: 2
-        }
-      }
-    ]
-  };
-}
-// 反酸模型  最终代  散点图
-const getFinalScatterOption=() => {
-
-  const scatterData = [[-0.003333333333333854, -0.45726666666666654], [-0.1733333333333329, -0.1726333333333331],
-    [-0.6233333333333331, -0.5226666666666667], [-0.7088888888888892, -0.4791888888888889],
-    [-0.3366666666666669, -0.3630666666666673], [-0.8888888888888887, -0.48272222222222183],
-    [-0.5633333333333326, -0.7492444444444444], [-0.7333333333333325, -0.5572666666666672],
-    [-0.3366666666666663, -0.29379999999999984], [-1.176666666666666, -0.8544111111111106],
-    [-0.7122222222222225, -0.4959777777777775], [-0.7699999999999996, -0.6149666666666669]];
-
-  const range = calculateDataRange(scatterData);
-  const padding = 0.1;
-  const xMin = range.xMin - Math.abs(range.xMin * padding);
-  const xMax = range.xMax + Math.abs(range.xMax * padding);
-  const yMin = range.yMin - Math.abs(range.yMin * padding);
-  const yMax = range.yMax + Math.abs(range.yMax * padding);
-  const min = Math.min(xMin, yMin)
-  const max = Math.max(xMax, yMax)
-
-  return {
-    tooltip: {
-      trigger: 'axis',
-      axisPointer: {
-        type: 'cross'
-      }
-    },
-    legend: {
-      data: ['True vs Predicted']
-    },
-    grid: {
-      left: '3%',
-      right: '22%',
-      bottom: '3%',
-      containLabel: true
-    },
-    xAxis: {
-      name: 'True Values',
-      type: 'value',
-      min: min,
-      max: max
-    },
-    yAxis: {
-      name: 'Predicted Values',
-      type: 'value',
-      min: parseFloat(min.toFixed(2)),
-      max: parseFloat(max.toFixed(2))
-    },
-    series: [
-      {
-        name: 'True vs Predicted',
-        type: 'scatter',
-        data: scatterData,
-        symbolSize: 10,
-        itemStyle: {
-          color: '#1f77b4',
-          opacity: 0.7
-        }
-      },
-      {
-        name: 'Trendline',
-        type: 'line',
-        data: [
-          [min, min],
-          [max, max]
-        ],
-        lineStyle: {
-          type: 'dashed',
-          color: '#ff7f0e',
-          width: 2
-        }
-      }
-    ]
-  };
-}
-
-//获取折线图配置
-const getLineOption=()=>{
-  return{
-    tooltip:{
-      trigger:'axis'
-    },
-    legend:{
-      data:['Random Forest', 'XGBoost', 'Gradient Boosting']  // 模型名称
-    },
-    grid: {
-      left: '3%',
-      right: '17%',
-      bottom: '3%',
-      containLabel: true
-    },
-    xAxis: {
-      name: '模型迭代',
-      type: 'category',
-      boundaryGap: false,
-      data: ['1代','2代','3代','4代','5代','6代','7代','8代','9代']  // train_sizes按10%递增
-    },
-    yAxis: {
-      name: 'Score (R^2)',
-      type: 'value'
-    },
-    series: [
-      {
-        name: 'Random Forest',
-        type: 'line',
-        data: [-0.17101591951095463, -0.556719360354051, -0.04083550751401055, -0.20858221504075436, 0.07297292282221035, 0.19857845644421734, 0.28407131176770184, 0.27979356883596496, 0.36904808817286416, 0.4183018571701477]  // 使用您的实际R2分数数据
-      },
-      {
-        name: 'XGBoost',
-        type: 'line',
-        data: [-1.1811781145886937, -1.5645641005612534, -0.12619079632263497, 0.03324096120721032, 0.06969290639267578, 0.12375262461601955, 0.5331670468884062, 0.49454793164801647, 0.31904329339597803, 0.2712670704381914]
-      },
-      {
-        name: 'Gradient Boosting',
-        type: 'line',
-        data: [-0.8583039298789095, -1.073316171952042, 0.09659300885027666, 0.06097833957434784, 0.191975498544109, 0.3718334600546489, 0.3948098332187753, 0.4398778520728397, 0.452609022210963, 0.41484634172723023]
-      }
-    ]
-  }
-}
-
-const ecLineOption=ref(getLineOption());
-const ecInitScatterOption = ref(getInitScatterOption());
-const ecMidScatterOption = ref(getMidScatterOption());
-const ecFinalScatterOption = ref(getFinalScatterOption());
+<script setup lang='ts'>
+import { ref } from 'vue';
+import ModelIterationVisualization from './ModelIterationVisualization.vue';
 
+const showLineChart = ref(true);
+const showInitScatterChart = ref(true);
+const showMidScatterChart = ref(true);
+const showFinalScatterChart = ref(true);
 
+// 直接在代码里定义好参数
+const lineChartPathParam = ref('reflux'); 
+const initScatterModelId = ref(17);
+const midScatterModelId = ref(18);
+const finalScatterModelId = ref(19);
 </script>
 
-
 <style scoped>
-.container {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  width: 100%;
-  height: 100%;
-  gap: 20px;
-}
-
-.sub-title {
-  font-size: 14px;
-  font-weight: 700;
-}
-
-.chart-container {
-  width: 75%;
-  height: 450px;
-  margin-bottom: 20px;
-}
-
-.VueEcharts {
-  width: 100%;
-  height: 100%;
-  margin: 0 10px;
-}
-</style>
+#app {
+  font-family: Avenir, Helvetica, Arial, sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  text-align: center;
+  color: #2c3e50;
+  margin-top: 60px;
+}
+</style>