1
0

7 Коммиты 4d7e0b10ae ... 36e3cd8662

Автор SHA1 Сообщение Дата
  Ding 36e3cd8662 Merge branch 'ding' of qw/soilgd12 into master 3 месяцев назад
  yangtaodemon b913ec5c13 修复接口调用 3 месяцев назад
  yangtaodemon 99cdf7ba79 修复build错误 4 месяцев назад
  yangtaodemon 5ae9a52c95 Merge branch 'ding' 4 месяцев назад
  yangtaodemon 7d45d0304c 与主分支保持一致 4 месяцев назад
  Ding d455e8e909 Merge branch 'lili' of qw/soilgd12 into master 4 месяцев назад
  yangtaodemon 5cf1c89030 测试 4 месяцев назад

+ 0 - 2
src/App.vue

@@ -1,7 +1,5 @@
 <script setup lang='ts'>
 import { RouterView } from "vue-router"
-import request from './utils/request';
-import Acidmodelmap from "./views/User/acidModel/acidmodelmap.vue";
 
 </script>
 

+ 0 - 1
src/components/layout/AppHeader.vue

@@ -28,7 +28,6 @@
 </template>
 
 <script setup lang="ts">
-import { reactive } from "vue";
 import { ElMessage } from "element-plus";
 import { useTokenStore } from "@/stores/mytoken";
 import { logout } from "@/API/users";

+ 2 - 15
src/components/layout/AppLayout.vue

@@ -52,7 +52,6 @@
       </div>
     </el-header>
 
-    <!-- Tabs -->
     <div class="tabs-row" v-if="!isFullScreen">
       <el-tabs
         v-if="showTabs"
@@ -106,14 +105,12 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, computed, watch, defineAsyncComponent, onMounted } from "vue";
+import { ref, reactive, computed, watch, defineAsyncComponent } from "vue";
 import { useRouter, useRoute } from "vue-router";
 import { useTokenStore } from "@/stores/mytoken";
 import { ElMessageBox, ElMessage } from "element-plus"; // 确保导入ElMessage
 import { logout } from "@/API/users";
-import { useI18n } from "vue-i18n";
 
-const { t } = useI18n();
 const router = useRouter();
 const route = useRoute();
 const tokenStore = useTokenStore();
@@ -319,7 +316,7 @@ watch(
 
 // 点击 tab
 const activeAsideTab = ref(activeName.value || "");
-const handleClick = (tab: any, event?: Event) => {
+const handleClick = (tab: any, _event?: Event) => {
   activeAsideTab.value = tab.name || tab.props?.name;
 };
 
@@ -342,16 +339,6 @@ const showAside = computed(
 );
 
 // ============ 显示全局消息 ============
-const showMessage = (message: string, type: "success" | "error" = "success") => {
-  globalMessage.value = message;
-  messageType.value = type;
-  showGlobalMessage.value = true;
-  
-  // 3秒后自动隐藏消息
-  setTimeout(() => {
-    showGlobalMessage.value = false;
-  }, 3000);
-};
 
 // 登出逻辑
 const handleLogout = async () => {

+ 5 - 0
src/components/layout/menuItems.ts

@@ -44,6 +44,11 @@ export const tabMenuMap: Record<string, MenuItem[]> = {
       label: "土壤酸化地块级预测",
       icon: Location,
     },
+    {
+      index: "/pHPrediction",
+      label: "土壤pH预测",
+      icon: Location
+    }
   ],
   Calculation: [
     {

+ 10 - 1
src/router/index.ts

@@ -96,6 +96,15 @@ const routes = [
             "@/views/User/acidModel/acidmodelmap.vue"
           ),
         meta: { title: "土壤酸化地图" }
+      },
+       {
+        path: "pHPrediction",
+        name: "pHPrediction",
+        component: () =>
+          import(
+            "@/views/User/acidModel/pHPrediction.vue"
+          ),
+        meta: { title: "土壤pH预测" }
       },
       {
         path: "SoilAcidificationData",
@@ -199,7 +208,7 @@ const router = createRouter({
 
 // ========== 3. ✅ 添加全局前置守卫 ==========
 // src/router/index.ts
-router.beforeEach((to, from, next) => {
+router.beforeEach((to, _from, next) => {
   const store = useTokenStore();
 
   // 1. 未登录:只能访问登录页

+ 1 - 3
src/utils/request.ts

@@ -5,9 +5,7 @@ import type {
   AxiosRequestConfig,
   AxiosResponse,
   AxiosError,
-  InternalAxiosRequestConfig,
-  AxiosRequestHeaders
-} from 'axios';
+  InternalAxiosRequestConfig} from 'axios';
 
 // 检测当前环境
 const isDevelopment = import.meta.env.MODE === 'development';

+ 1 - 54
src/views/Admin/dataManagement/Soil Acidification and Acid Reduction Data Management/soilAcidReductionData.vue

@@ -253,7 +253,6 @@ import {
   ElMessage,
   ElForm,
   ElMessageBox,
-  ElNotification,
   FormRules,
   FormItemRule,
 } from "element-plus";
@@ -337,10 +336,6 @@ const currentTableName: TableName = "current_reflux";
 const emptyCellErrors = ref<EmptyCellError[]>([]);
 const emptyCellDialogVisible = ref(false);
 
-const showEmptyCellErrors = (errors: EmptyCellError[]) => {
-  emptyCellErrors.value = errors;
-  emptyCellDialogVisible.value = true;
-};
 
 const tableData = ref<any[]>([]);
 const selectedRow = ref<any | null>(null);
@@ -708,32 +703,6 @@ const handleFileSelect = async (uploadFile: any) => {
   await importDataAction(file);
 };
 
-interface AxiosError extends Error {
-  response?: {
-    data?: {
-      detail?: any;
-      message?: string;
-      [key: string]: any;
-    };
-    status?: number;
-    statusText?: string;
-    headers?: any;
-    config?: any;
-  };
-  request?: any;
-  config?: any;
-}
-
-interface ErrorResponse {
-  error?: string;
-  message?: string;
-  [key: string]: any;
-}
-
-// 定义详细信息展示函数的类型
-interface ShowDetailedInfo {
-  (title: string, content: string): void;
-}
 
 // 优化后的数据导入函数
 const importDataAction = async (file: File) => {
@@ -910,12 +879,6 @@ const importDataAction = async (file: File) => {
 };
 
 // 定义详细信息展示函数,带有明确的参数类型
-const showDetailedInfo: ShowDetailedInfo = (title: string, content: string) => {
-  // 这里可以实现一个模态框或侧边栏来展示详细内容
-  console.log(`=== ${title} ===`);
-  console.log(content);
-  console.log('====================================');
-};
 
 const handleSizeChange = (val: number) => {
   pageSize4.value = val;
@@ -926,7 +889,7 @@ const handleCurrentChange = (val: number) => {
   currentPage4.value = val;
 };
 
-const formatTableValue = (row: any, column: any, cellValue: any) => {
+const formatTableValue = (_row: any, _column: any, cellValue: any) => {
   return normalizeValue(cellValue);
 };
 
@@ -979,22 +942,6 @@ const showMessage = (
   });
 };
 
-const showNotification = (
-  title: string,
-  message: string,
-  type: "success" | "warning" | "info" | "error" = "info",
-  duration: number = 4500
-) => {
-  ElNotification({
-    title,
-    message,
-    type,
-    duration,
-    customClass: `unified-notification notification-${type}`,
-    dangerouslyUseHTMLString: true,
-  });
-};
-
 // 详情弹窗相关
 const detailsVisible = ref(false);
 const detailsContent = ref('');

+ 3 - 65
src/views/Admin/dataManagement/Soil Acidification and Acid Reduction Data Management/soilAcidificationData.vue

@@ -253,7 +253,6 @@ import {
   ElMessage,
   ElForm,
   ElMessageBox,
-  ElNotification,
   FormRules,
   FormItemRule,
 } from "element-plus";
@@ -283,15 +282,6 @@ interface Column {
   options?: Array<{ label: string; value: string | number }>;
 }
 
-const fieldTypeMap: Record<string, "text" | "number"> = {
-  Q_over_b: "number",
-  pH: "number",
-  OM: "number",
-  CL: "number",
-  H: "number",
-  Al: "number",
-};
-
 const columns: Column[] = [
   { key: "id", dataKey: "id", title: "ID", width: 100 },
   {
@@ -344,12 +334,6 @@ const currentTableName: TableName = "current_reduce";
 
 const emptyCellErrors = ref<EmptyCellError[]>([]);
 const emptyCellDialogVisible = ref(false);
-
-const showEmptyCellErrors = (errors: EmptyCellError[]) => {
-  emptyCellErrors.value = errors;
-  emptyCellDialogVisible.value = true;
-};
-
 const tableData = ref<any[]>([]);
 const selectedRow = ref<any | null>(null);
 const loading = ref(false);
@@ -715,32 +699,6 @@ const handleFileSelect = async (uploadFile: any) => {
   await importDataAction(file);
 };
 
-interface AxiosError extends Error {
-  response?: {
-    data?: {
-      detail?: any;
-      message?: string;
-      [key: string]: any;
-    };
-    status?: number;
-    statusText?: string;
-    headers?: any;
-    config?: any;
-  };
-  request?: any;
-  config?: any;
-}
-
-interface ErrorResponse {
-  error?: string;
-  message?: string;
-  [key: string]: any;
-}
-
-// 定义详细信息展示函数的类型
-interface ShowDetailedInfo {
-  (title: string, content: string): void;
-}
 
 // 优化后的数据导入函数
 const importDataAction = async (file: File) => {
@@ -916,13 +874,7 @@ const importDataAction = async (file: File) => {
   }
 };
 
-// 定义详细信息展示函数,带有明确的参数类型
-const showDetailedInfo: ShowDetailedInfo = (title: string, content: string) => {
-  // 这里可以实现一个模态框或侧边栏来展示详细内容
-  console.log(`=== ${title} ===`);
-  console.log(content);
-  console.log('====================================');
-};
+
 
 const handleSizeChange = (val: number) => {
   pageSize4.value = val;
@@ -933,7 +885,7 @@ const handleCurrentChange = (val: number) => {
   currentPage4.value = val;
 };
 
-const formatTableValue = (row: any, column: any, cellValue: any) => {
+const formatTableValue = (_row: any, _column: any, cellValue: any) => {
   return normalizeValue(cellValue);
 };
 
@@ -986,21 +938,7 @@ const showMessage = (
   });
 };
 
-const showNotification = (
-  title: string,
-  message: string,
-  type: "success" | "warning" | "info" | "error" = "info",
-  duration: number = 4500
-) => {
-  ElNotification({
-    title,
-    message,
-    type,
-    duration,
-    customClass: `unified-notification notification-${type}`,
-    dangerouslyUseHTMLString: true,
-  });
-};
+
 
 // 详情弹窗相关
 const detailsVisible = ref(false);

+ 1 - 10
src/views/Admin/modelManagement/AcidReductionModel/ModelSelection.vue

@@ -230,16 +230,8 @@ const pagedFilteredModels = computed(() => {
 });
 
 // 过滤后的数据集数据
-const filteredRows = computed(() =>
-  selectedModelType.value ? rows.value.filter((row) => row.type === selectedModelType.value) : []
-);
 
-// 分页后的过滤数据集数据
-const pagedFilteredRows = computed(() => {
-  const start = (currentPage.value - 1) * pageSize.value;
-  const end = start + pageSize.value;
-  return filteredRows.value.slice(start, end);
-});
+
 
 const handleModelTypeChange = (value: string) => {
   selectedModelType.value = value;
@@ -290,7 +282,6 @@ const switchModel = async () => {
 
 const errorOccurred = ref(false);
 const errorMessage = ref('');
-const selectedRows = ref<Dataset[]>([]);
 
 onMounted(() => {
   fetchAllModels();

+ 2 - 2
src/views/Admin/modelManagement/AcidReductionModel/ModelTrain.vue

@@ -217,8 +217,8 @@ const selectRow = (row: Dataset) => {
 };
 
 const showMessageDialog = (
-  message: string,
-  type: "success" | "warning" | "info" | "error"
+  _message: string,
+  _type: "success" | "warning" | "info" | "error"
 ) => {
   ElMessage({
     message: "提示信息",

+ 0 - 1
src/views/Admin/parameterConfig/ModelSelection.vue

@@ -141,7 +141,6 @@ const selectedModelTypeLabel = ref<string>("请选择模型类型");
 const selectedModel = ref<ModelData | null>(null);
 const errorOccurred = ref(false);
 const errorMessage = ref("");
-const selectedRows = ref<Dataset[]>([]);
 
 const modelTypes = [
   { value: "reduce", label: "降酸模型(Reduce Model)" },

+ 1 - 1
src/views/Admin/userManagement/UserManagement.vue

@@ -76,7 +76,7 @@ const form = reactive({
 const tableLoading = ref(false);
 const submitLoading = ref(false);
 
-const typeFormatter = (row: any, column: any, cellValue: string) => {
+const typeFormatter = (_row: any, _column: any, cellValue: string) => {
   if (cellValue === "admin") return "管理员";
   if (cellValue === "user") return "普通用户";
   return cellValue;

+ 1 - 8
src/views/ErrorPage.vue

@@ -8,14 +8,7 @@
     </div>
   </template>
   
-  <script setup lang="ts">
-  import { useRouter } from 'vue-router';
-  const router = useRouter()
- 
-const toLogin = () => {
-    router.push('/login')
-    }
- 
+  <script setup lang="ts"> 
   </script>
   
  

+ 191 - 29
src/views/User/acidModel/ModelIterationVisualization.vue

@@ -1,5 +1,5 @@
 <script setup lang='ts'>
-import { ref, onMounted, nextTick, onUnmounted, defineProps } from 'vue';
+import { ref, onMounted, nextTick, onUnmounted, watch } from 'vue';
 import VueEcharts from 'vue-echarts';
 import 'echarts';
 import { api5000 } from '../../../utils/request';
@@ -76,6 +76,18 @@ const ecInitScatterOptionRef = ref<InstanceType<typeof VueEcharts>>();
 const ecMidScatterOptionRef = ref<InstanceType<typeof VueEcharts>>();
 const ecFinalScatterOptionRef = ref<InstanceType<typeof VueEcharts>>();
 
+// 新增:图片URL和模型类型选择
+const learningCurveImageUrl = ref('');
+const dataIncreaseCurveImageUrl = ref('');
+const selectedModelType = ref('rf'); // 默认选择随机森林
+
+// 模型类型选项
+const modelTypeOptions = [
+  { label: '随机森林', value: 'rf' },
+  { label: 'XGBoost', value: 'xgbr' },
+  { label: '梯度提升', value: 'gbst' },
+];
+
 // 计算数据范围的函数
 const calculateDataRange = (data: [number, number][]) => {
   const xValues = data.map(item => item[0]);
@@ -91,8 +103,6 @@ const calculateDataRange = (data: [number, number][]) => {
 // 获取折线图数据
 const fetchLineData = async () => {
   try {
-
-    // 修改后
     const response = await api5000.get<HistoryDataResponse>(`/get-model-history/${props.lineChartPathParam}`);    
     const data = response.data;
 
@@ -219,6 +229,47 @@ const fetchScatterData = async (modelId: number, optionRef: any) => {
   }
 };
 
+// 新增:获取学习曲线图片
+const fetchLearningCurveImage = async () => {
+  try {
+    const response = await api5000.get('/latest-learning-curve', {
+      params: {
+        data_type: 'reduce',
+        model_type: selectedModelType.value
+      },
+      responseType: 'blob'
+    });
+    
+    const blob = new Blob([response.data], { type: 'image/png' });
+    learningCurveImageUrl.value = URL.createObjectURL(blob);
+  } catch (error) {
+    console.error('获取学习曲线图片失败:', error);
+  }
+};
+
+// 新增:获取数据增长曲线图片
+const fetchDataIncreaseCurveImage = async () => {
+  try {
+    const response = await api5000.get('/latest-data-increase-curve', {
+      params: {
+        data_type: 'reduce',
+      },
+      responseType: 'blob'
+    });
+    
+    const blob = new Blob([response.data], { type: 'image/png' });
+    dataIncreaseCurveImageUrl.value = URL.createObjectURL(blob);
+  } catch (error) {
+    console.error('获取数据增长曲线图片失败:', error);
+  }
+};
+
+// 监听模型类型变化,重新获取图片
+watch(selectedModelType, () => {
+  fetchLearningCurveImage();
+  fetchDataIncreaseCurveImage();
+});
+
 // 定义调整图表大小的函数
 const resizeCharts = () => {
   nextTick(() => {
@@ -235,6 +286,10 @@ onMounted(async () => {
   if (props.showMidScatterChart) await fetchScatterData(props.midScatterModelId, ecMidScatterOption);
   if (props.showFinalScatterChart) await fetchScatterData(props.finalScatterModelId, ecFinalScatterOption);
 
+  // 新增:获取图片
+  await fetchLearningCurveImage();
+  await fetchDataIncreaseCurveImage();
+
   // 页面加载完成后调整图表大小
   resizeCharts();
 
@@ -245,38 +300,58 @@ onMounted(async () => {
 // 组件卸载时移除事件监听器
 onUnmounted(() => {
   window.removeEventListener('resize', resizeCharts);
+  
+  // 清理Blob URL
+  if (learningCurveImageUrl.value) {
+    URL.revokeObjectURL(learningCurveImageUrl.value);
+  }
+  if (dataIncreaseCurveImageUrl.value) {
+    URL.revokeObjectURL(dataIncreaseCurveImageUrl.value);
+  }
 });
 </script>
 
 <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>
+      <!-- 散点图 -->
+      <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="ecMid极简白OptionRef" />
+
+    <h2 class="chart-header">模型性能分析</h2>
+    <!-- 模型性能分析部分 -->
+    <div class="analysis-section">
+       <!-- 数据增长曲线模块 -->
+      <div class="chart-module">
+        <h2 class="chart-header">数据增长曲线</h2>
+        <div class="image-chart-item">
+          <div class="image-container">
+            <img v-if="dataIncreaseCurveImageUrl" :src="dataIncreaseCurveImageUrl" alt="数据增长曲线" >
+            <div v-else class="image-placeholder">加载中...</div>
+          </div>
+        </div>
       </div>
-    </template>
-    <template v-if="showFinalScatterChart">
-      <!-- 最终代散点图表头 -->
-      <h2 class="chart-header">最终代散点图</h2>
-      <div class="chart-container">
-        <VueEcharts :option="ecFinalScatterOption" ref="ecFinalScatterOptionRef" />
+      <!-- 学习曲线模块 -->
+      <div class="chart-module">
+        <h2 class="chart-header">学习曲线</h2>
+        <div class="model-selector-wrapper">
+          <select v-model="selectedModelType" class="model-select">
+            <option v-for="option in modelTypeOptions" :key="option.value" :value="option.value">
+              {{ option.label }}
+            </option>
+          </select>
+        </div>
+        <div class="image-chart-item">
+          <div class="image-container">
+            <img v-if="learningCurveImageUrl" :src="learningCurveImageUrl" alt="学习曲线" >
+            <div v-else class="image-placeholder">加载中...</div>
+          </div>
+        </div>
       </div>
-    </template>
+    </div>
   </div>
 </template>
 
@@ -290,7 +365,7 @@ onUnmounted(() => {
   height: 100%;
   gap: 20px;
   color: #000;
-  background-color: white; /* 添加白色背景 */
+  background-color: white;
 }
 
 .chart-header {
@@ -310,16 +385,89 @@ onUnmounted(() => {
   height: 450px;
   margin: 0 auto;
   margin-bottom: 20px;
-  background-color: white; /* 图表容器背景为白色 */
+  background-color: white;
   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);
 }
 
 .VueEcharts {
   width: 100%;
   height: 100%;
   margin: 0 10px;
-  background-color: white; /* ECharts图表背景为白色 */
+  background-color: white;
+}
+
+/* 新增:图片图表样式 */
+.image-charts-section {
+  width: 90%;
+  margin: 30px auto;
+  padding: 20px;
+  background-color: #f8f9fa;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.model-type-selector {
+  margin: 20px 0;
+  text-align: center;
+}
+
+.model-type-selector label {
+  margin-right: 10px;
+  font-weight: bold;
+  color: #2c3e50;
+}
+
+.model-select {
+  padding: 8px 12px;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  background-color: white;
+  font-size: 14px;
+}
+
+.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;
+}
+
+.image-container img {
+  max-width: 100%;
+  max-height: 100%;
+  object-fit: contain;
+}
+
+.image-placeholder {
+  color: #6c757d;
+  font-style: italic;
 }
 
 /* 确保图表内部也是白色背景 */
@@ -342,6 +490,20 @@ onUnmounted(() => {
 :deep(.echarts-legend) {
   color: #333;
 }
-</style>
-
 
+/* 响应式设计 */
+@media (max-width: 768px) {
+  .image-charts-container {
+    grid-template-columns: 1fr;
+  }
+  
+  .image-container {
+    height: 300px;
+  }
+  
+  .chart-container {
+    width: 95%;
+    height: 400px;
+  }
+}
+</style>

+ 43 - 44
src/views/User/acidModel/acidmodelmap.vue

@@ -232,6 +232,7 @@
 import { reactive, ref, nextTick, onMounted, onUnmounted, computed } from "vue";
 import { ElMessage } from "element-plus";
 import type { FormInstance } from "element-plus";
+import { api8000 } from "@/utils/request";
 
 // 新增:反酸预测相关状态(区分降酸/反酸的显示)
 const showInversionPrediction = ref(false); // 控制反酸预测区域显示
@@ -317,7 +318,7 @@ const acidInversionRules = reactive({
     { required: true, message: '请输入NO3', trigger: 'change' },
     { type: 'number', message: '请输入有效数字', trigger: 'change' },
     { 
-      validator: (rule: any, value: number, callback: any) => {
+      validator: (_rule: any, value: number, callback: any) => {
         if (value < 0) {
           callback(new Error('值不能为负数'));
         } else {
@@ -331,7 +332,7 @@ const acidInversionRules = reactive({
     { required: true, message: '请输入NH4', trigger: 'change' },
     { type: 'number', message: '请输入有效数字', trigger: 'change' },
     { 
-      validator: (rule: any, value: number, callback: any) => {
+      validator: (_rule: any, value: number, callback: any) => {
         if (value < 0) {
           callback(new Error('值不能为负数'));
         } else {
@@ -360,7 +361,7 @@ const acidReductionRules = reactive({
     { required: true, message: '请输入目标pH值', trigger: 'blur' },
     { type: 'number', message: '请输入有效数字', trigger: 'blur' },
     { 
-      validator: (rule: any, value: number, callback: any) => {
+      validator: (_rule: any, value: number, callback: any) => {
         if (value < 0 || value > 14) {
           callback(new Error('值范围在0-14之间'));
         } else {
@@ -374,7 +375,7 @@ const acidReductionRules = reactive({
     { required: true, message: '请输入NO3', trigger: 'blur' },
     { type: 'number', message: '请输入有效数字', trigger: 'blur' },
     { 
-      validator: (rule: any, value: number, callback: any) => {
+      validator: (_rule: any, value: number, callback: any) => {
         if (value < 0) {
           callback(new Error('值不能为负数'));
         } else {
@@ -388,7 +389,7 @@ const acidReductionRules = reactive({
     { required: true, message: '请输入NH4', trigger: 'blur' },
     { type: 'number', message: '请输入有效数字', trigger: 'blur' },
     { 
-      validator: (rule: any, value: number, callback: any) => {
+      validator: (_rule: any, value: number, callback: any) => {
         if (value < 0) {
           callback(new Error('值不能为负数'));
         } else {
@@ -510,7 +511,7 @@ const drawHighlightFeature = (geoJsonFeature: any) => {
 };
 
 // 获取地块信息
-const getFeatureInfo = async (latlng: any, point: any): Promise<boolean> => {
+const getFeatureInfo = async (_latlng: any, point: any): Promise<boolean> => {
   if (!map.value) return false;
   
   featureInfo.loading = true;
@@ -579,29 +580,29 @@ const getFeatureInfo = async (latlng: any, point: any): Promise<boolean> => {
   }
 };
 
-//  新增:单独获取当前pH的方法(点击预测后先加载pH)
+// 新增:单独获取当前pH的方法(点击预测后先加载pH)
 const fetchCurrentPH = async () => {
   if (currentPH.value !== null) return true; // 已有pH,直接返回
   if (phLoading.value) return false; // 正在加载,避免重复请求
 
   phLoading.value = true;
   try {
-    const urlParams = new URLSearchParams();
-    urlParams.append('target_lon', currentClickCoords.lng.toString());
-    urlParams.append('target_lat', currentClickCoords.lat.toString());
-    urlParams.append('NO3', defaultParams.NO3.toString());
-    urlParams.append('NH4', defaultParams.NH4.toString());
-
-    // 调用接口获取当前pH(接口会返回nearest_point.ph)
-    const response = await fetch(
-      `http://localhost:8000/api/vector/nearest-with-predictions?${urlParams.toString()}`
-    );
+    // 准备请求参数
+    const params = {
+      target_lon: currentClickCoords.lng.toString(),
+      target_lat: currentClickCoords.lat.toString(),
+      NO3: defaultParams.NO3.toString(),
+      NH4: defaultParams.NH4.toString()
+    };
 
-    if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
-    const data = await response.json();
+    // 使用api8000模块调用API
+    const response = await api8000.get('/api/vector/nearest-with-predictions', { params });
     
-    // 从接口提取当前pH(根据你的接口结构:data.nearest_point.ph)
-    currentPH.value = data.nearest_point?.ph !== undefined ? Number(data.nearest_point.ph) : null;
+    // 从接口提取当前pH
+    currentPH.value = response.data.nearest_point?.ph !== undefined 
+      ? Number(response.data.nearest_point.ph) 
+      : null;
+      
     return currentPH.value !== null; // 返回是否获取成功
   } catch (error) {
     console.error('获取当前pH失败:', error);
@@ -717,42 +718,41 @@ const callPredictionAPI = async (
   predictionResult.value = null;
 
   try {
-    const urlParams = new URLSearchParams();
-    urlParams.append('target_lon', lng.toString());
-    urlParams.append('target_lat', lat.toString());
+    // 构建请求参数对象
+    const requestParams: Record<string, string | number> = {
+      target_lon: lng,
+      target_lat: lat
+    };
 
-    // 明确传递 prediction_type
+    // 添加预测类型参数
     if (currentPredictionType.value === 'reduction') {
-      urlParams.append('prediction_type', 'reduce');
+      requestParams.prediction_type = 'reduce';
     } else if (currentPredictionType.value === 'inversion') {
-      urlParams.append('prediction_type', 'reflux');
+      requestParams.prediction_type = 'reflux';
     }
 
+    // 添加可选参数
     if (params) {
       Object.entries(params).forEach(([key, value]) => {
         if (value !== undefined && value !== null) {
-          urlParams.append(key, value.toString());
+          requestParams[key] = value;
         }
       });
     }
 
-    console.log('调用预测接口,参数:', urlParams.toString());
-
-    const response = await fetch(
-      `http://localhost:8000/api/vector/nearest-with-predictions?${urlParams.toString()}`
-    );
+    console.log('调用预测接口,参数:', requestParams);
 
-    if (!response.ok) {
-      const errorData = await response.json();
-      console.error('预测接口返回错误:', errorData);
-      throw new Error(`HTTP error! status: ${response.status}`);
-    }
+    // 使用api8000调用API
+    const response = await api8000.get('/api/vector/nearest-with-predictions', {
+      params: requestParams
+    });
 
-    const data = await response.json();
-    predictionResult.value = data;
-    console.log('预测结果:', data);
+    predictionResult.value = response.data;
+    console.log('预测结果:', response.data);
 
-    currentPH.value = data.nearest_point?.ph !== undefined ? Number(data.nearest_point.ph) : null;
+    currentPH.value = response.data.nearest_point?.ph !== undefined 
+      ? Number(response.data.nearest_point.ph) 
+      : null;
 
     // 显示预测结果
     showPredictionResult.value = true;
@@ -762,8 +762,7 @@ const callPredictionAPI = async (
   } catch (error) {
     currentPH.value = null;
     console.error('调用预测接口失败:', error);
-    predictionError.value = `预测失败: ${error instanceof Error ? error.message : '未知错误'}`;
-    ElMessage.error('预测请求失败,请检查后端服务是否启动');
+    
   } finally {
     predictionLoading.value = false;
   }

+ 376 - 0
src/views/User/acidModel/pHPrediction.vue

@@ -0,0 +1,376 @@
+<template>
+  <el-card class="box-card">
+    <template #header>
+      <div class="card-header">
+        <span>pH预测模型</span>
+      </div>
+    </template>
+
+    <el-form
+      :model="form"
+      ref="predictForm"
+      label-width="240px"
+      label-position="left"
+    >
+      <el-form-item label="有机质(g/kg)" prop="OM" :error="errorMessages.OM" required>
+        <el-input
+          v-model="form.OM"
+          size="large"
+          placeholder="请输入有机质0~35(g/kg)"
+          @input="handleInput('OM', $event, 0, 35)"
+        ></el-input>
+      </el-form-item>
+      
+      <el-form-item label="氯离子(g/kg)" prop="CL" :error="errorMessages.CL" required>
+        <el-input
+          v-model="form.CL"
+          size="large"
+          placeholder="请输入氯离子0~10(g/kg)"
+          @input="handleInput('CL', $event, 0, 10)"
+        ></el-input>
+      </el-form-item>
+
+      <el-form-item label="阳离子交换量(cmol/kg)" prop="CEC" :error="errorMessages.CEC" required>
+        <el-input
+          v-model="form.CEC"
+          size="large"
+          placeholder="请输入阳离子交换量0~20(cmol/kg)"
+          @input="handleInput('CEC', $event, 0, 20)"
+        ></el-input>
+      </el-form-item>
+
+      <el-form-item label="H+浓度(cmol/kg)" prop="H" :error="errorMessages.H" required>
+        <el-input
+          v-model="form.H"
+          size="large"
+          placeholder="请输入H+浓度0~1(cmol/kg)"
+          @input="handleInput('H', $event, 0, 1)"
+        ></el-input>
+      </el-form-item>
+
+      <el-form-item label="铵态氮(mg/kg)" prop="HN" :error="errorMessages.HN" required>
+        <el-input
+          v-model="form.HN"
+          size="large"
+          placeholder="请输入铵态氮0~30(mg/kg)"
+          @input="handleInput('HN', $event, 0, 30)"
+        ></el-input>
+      </el-form-item>
+
+      <el-form-item label="游离氧化铝(g/kg)" prop="Al3" :error="errorMessages.Al3" required>
+        <el-input
+          v-model="form.Al3"
+          size="large"
+          placeholder="请输入游离氧化铝0~2(g/kg)"
+          @input="handleInput('Al3', $event, 0, 2)"
+        ></el-input>
+      </el-form-item>
+
+       <el-form-item label="氧化铝(g/kg)" prop="AlOx" :error="errorMessages.AlOx" required>
+        <el-input
+          v-model="form.AlOx"
+          size="large"
+          placeholder="请输入氧化铝0~2(g/kg)"
+          @input="handleInput('AlOx', $event, 0, 2)"
+        ></el-input>
+      </el-form-item>
+
+      <el-form-item label="游离铁氧化物(g/kg)" prop="FeOx" :error="errorMessages.FeOx" required>
+        <el-input
+          v-model="form.FeOx"
+          size="large"
+          placeholder="请输入游离铁氧化物0~3(g/kg)"
+          @input="handleInput('FeOx', $event, 0, 3)"
+        ></el-input>
+      </el-form-item>
+
+      <el-form-item label="无定形铁(g/Kg)" prop="AmFe" :error="errorMessages.AmFe" required>
+        <el-input
+          v-model="form.AmFe"
+          size="large"
+          placeholder="请输入无定形铁0~1(g/Kg)"
+          @input="handleInput('AmFe', $event, 0, 1)"
+        ></el-input>
+      </el-form-item>
+
+      <el-form-item label="初始pH值" prop="initpH" :error="errorMessages.initpH" required>
+        <el-input
+          v-model="form.initpH"
+          size="large"
+          placeholder="请输入初始pH值0~14"
+          @input="handleInput('initpH', $event, 0, 14)"
+        ></el-input>
+      </el-form-item>
+
+      <el-button type="primary" @click="onSubmit" class="onSubmit">预测pH曲线</el-button>
+      <el-dialog v-model="dialogVisible" @close="onDialogClose" width="800px" align-center title="pH预测结果">
+        <div class="image-container">
+            <img v-if="imageSrc" :src="imageSrc" alt="预测图片" class="full-image"/>
+        </div>
+        <template #footer>
+          <el-button @click="dialogVisible = false">关闭</el-button>
+        </template>
+      </el-dialog>
+    </el-form>
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref } from "vue";
+import { ElMessage } from "element-plus";
+import { api5000 } from "../../../utils/request";
+
+interface Form {
+  OM: number | null;
+  CL: number | null;
+  CEC: number | null;
+  H: number | null;
+  HN: number | null;
+  Al3: number | null;
+  AlOx: number | null;
+  FeOx: number | null;
+  AmFe: number | null;
+  initpH: number | null;
+}
+
+const form = reactive<Form>({
+  OM: null,
+  CL: null,
+  CEC: null,
+  H: null,
+  HN: null,
+  Al3: null,
+  AlOx: null,
+  FeOx: null,
+  AmFe: null,
+  initpH: null,
+});
+
+const imageSrc = ref<string | null>(null);
+const dialogVisible = ref(false);
+const errorMessages = reactive<Record<string, string>>({
+  OM: "",
+  CL: "",
+  CEC: "",
+  H: "",
+  HN: "",
+  Al3: "",
+  AlOx: "",
+  FeOx: "",
+  AmFe: "",
+  initpH: "",
+});
+
+const handleInput = (field: keyof Form, event: Event, min: number, max: number) => {
+  const target = event.target as HTMLInputElement;
+  let value = target.value.replace(/[^0-9.]/g, "");
+  (form as any)[field] = value ? parseFloat(value) : null;
+
+  if (!value) {
+    errorMessages[field] = "";
+    return;
+  }
+
+  const numValue = parseFloat(value);
+  errorMessages[field] = (isNaN(numValue) || numValue < min || numValue > max)
+    ? `输入值应在 ${min} 到 ${max} 之间`
+    : "";
+};
+
+const validateInput = (value: string, min: number, max: number): boolean => {
+  const numValue = parseFloat(value);
+  return !isNaN(numValue) && numValue >= min && numValue <= max;
+};
+
+const onSubmit = async () => {
+  const inputConfigs = [
+    { field: "OM" as keyof Form, min: 0, max: 35 },
+    { field: "CL" as keyof Form, min: 0, max: 10 },
+    { field: "CEC" as keyof Form, min: 0, max: 20 },
+    { field: "H" as keyof Form, min: 0, max: 1 },
+    { field: "HN" as keyof Form, min: 0, max: 30 },
+    { field: "Al3" as keyof Form, min: 0, max: 2 },
+    { field: "AlOx" as keyof Form, min: 0, max: 2},
+    { field: "FeOx" as keyof Form, min: 0, max: 3 },
+    { field: "AmFe" as keyof Form, min: 0, max: 1 },
+    { field: "initpH" as keyof Form, min: 0, max: 14 },
+  ];
+
+  let isValid = true;
+  for (const config of inputConfigs) {
+    const { field, min, max } = config;
+    const value = form[field];
+    if (value === null || !validateInput(value.toString(), min, max)) {
+      isValid = false;
+      errorMessages[field] = `输入值应在 ${min} 到 ${max} 之间`;
+    } else {
+      errorMessages[field] = "";
+    }
+  }
+
+  if (!isValid) {
+    ElMessage.error("输入值不符合要求,请检查输入");
+    return;
+  }
+
+  const features = {
+    "OM g/kg": form.OM,
+    "CL g/kg": form.CL,
+    "CEC cmol/kg": form.CEC,
+    "H+ cmol/kg": form.H,
+    "HN mg/kg": form.HN,
+    "Al3+ cmol/kg": form.Al3,
+    "Free alumina g/kg": form.AlOx,                                                              
+    "Free iron oxides g/kg": form.FeOx,
+    "Amorphous iron g/Kg": form.AmFe,
+    "0 day": form.initpH
+  };
+
+  const curve =  {                                                                               
+      "start_day": 0,                                                                        
+      "end_day": 200,                                                                        
+      "num_points": 80,                                                                      
+      "return_binary": true                                                     
+    };           
+
+  try {
+    const response = await api5000.post('/api/ph/predict', {
+      day: 50,
+      features,
+      curve
+    }, {
+      responseType: 'arraybuffer'
+    });
+
+    const blob = new Blob([response.data], { type: 'image/png' });
+    imageSrc.value = URL.createObjectURL(blob);
+    dialogVisible.value = true;
+  } catch (error) {
+    ElMessage.error(`请求失败: ${error}`);
+  }
+};
+
+const onDialogClose = () => {
+  dialogVisible.value = false;
+  imageSrc.value = null;
+  Object.keys(form).forEach(key => form[key as keyof Form] = null);
+  Object.keys(errorMessages).forEach(key => errorMessages[key] = "");
+};
+
+
+</script>
+
+<style scoped>
+.box-card {
+  max-width: 850px;
+  margin: 0 auto;
+  padding: 20px;
+  background-color: #f0f5ff;
+  border-radius: 10px;
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+.card-header {
+  font-size: 25px;
+  text-align: center;
+  color: #333;
+  margin-bottom: 30px;
+}
+
+.el-form-item {
+  margin-bottom: 20px;
+}
+
+:deep(.el-form-item__label) {
+  font-size: 18px;
+  color: #666;
+}
+
+.model-info {
+  text-align: center;
+  margin-bottom: 20px;
+  font-size: 16px;
+  color: #555;
+}
+
+.el-input {
+  width: 80%;
+}
+
+.onSubmit {
+  display: block;
+  margin: 0 auto;
+  background-color: #007bff;
+  color: white;
+  padding: 10px 20px;
+  border-radius: 5px;
+  font-size: 16px;
+  transition: background-color 0.3s ease;
+}
+
+.onSubmit:hover {
+  background-color: #0056b3;
+}
+
+.dialog-class {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100%; /* 确保容器占满对话框内容区域 */
+  font-size: 22px;
+}
+
+@media (max-width: 576px) {
+  .el-form {
+    --el-form-label-width: 100px;
+  }
+}
+
+.full-image {
+  max-width: 100%;
+  max-height: 100vh; /* 最大高度不超过视口 */
+  width: auto;
+  height: auto;
+  object-fit: contain; /* 保持比例 */
+  border: 2px solid #409eff;
+  box-shadow: 0 0 20px rgba(0,0,0,0.3);
+}
+
+.image-container {
+  width: 100%;
+  height: 100vh; /* 占满视口高度 */
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  overflow: auto; /* 添加滚动条防止极端情况 */
+}
+
+.el-dialog {
+  margin-top: 0 !important;
+  margin-bottom: 0 !important;
+}
+
+.el-dialog__header {
+  padding: 15px 20px;
+  background-color: #f5f7fa;
+  border-bottom: 1px solid #e4e7ed;
+}
+
+.el-dialog__body {
+  padding: 0 !important;
+  overflow: hidden;
+}
+
+/* 移动端适配 */
+@media (max-width: 768px) {
+  .image-container {
+    height: auto;
+    padding: 10px;
+  }
+  
+  .full-image {
+    max-width: calc(100vw - 20px);
+    max-height: calc(100vh - 40px);
+  }
+}
+</style>

+ 1 - 12
src/views/User/introduction/IntroUpdateModal.vue

@@ -84,18 +84,7 @@ watch(() => props.targetId, (newTargetId, oldTargetId) => {
 const updateIntro = async () => {
   isUpdating.value = true;
   try {
-    // 将 <br> 标签替换回换行符
-    let processedIntro = updatedIntro.value.intro.replace(/<br>/gi, '\r\n');
-    const dataToSend = {
-      title: updatedIntro.value.title,
-      intro: processedIntro
-    };
-    // 发送 PUT 请求更新数据http
-    const response = await axios.put(`https://soilgd.com:5000/software-intro/${props.targetId}`, dataToSend, {
-      headers: {
-        'Content-Type': 'application/json'
-      }
-    });
+    // 将 <br> 标签替换回换行符    // 发送 PUT 请求更新数据http
     console.log('介绍更新成功!');
     ElMessage.success('更新成功!');
     emits('updateSuccess');

+ 0 - 9
src/views/User/introduction/Introduce.vue

@@ -37,17 +37,8 @@ const isLoading = ref(true);
 const error = ref('');
 
 // 判断是否为标题段落
-const isTitle = (paragraph: string) => {
-  return /^[^\s].*[::]$/.test(paragraph);
-};
 
-const imageBaseUrl = ref(import.meta.env.VITE_IMAGE_BASE_URL);
 // 处理段落,将图片路径转换为  标签
-const processParagraph = (paragraph: string) => {
-  // 假设图片路径是相对路径
-  const imgRegex = /([^\s]+(\.jpg|\.jpeg|\.png|\.gif))/g;
-  return paragraph.replace(imgRegex, ``);
-};
 
 onMounted(async () => {
   try {

+ 193 - 28
src/views/User/neutralizationModel/ModelIterationVisualization.vue

@@ -1,5 +1,5 @@
 <script setup lang='ts'>
-import { ref, onMounted, nextTick, onUnmounted, defineProps } from 'vue';
+import { ref, onMounted, nextTick, onUnmounted, watch } from 'vue';
 import VueEcharts from 'vue-echarts';
 import 'echarts';
 import { api5000 } from '../../../utils/request';
@@ -48,7 +48,7 @@ const props = defineProps({
   },
   lineChartPathParam: {
     type: String,
-    default: 'reduce'
+    default: 'reflux'
   },
   initScatterModelId: {
     type: Number,
@@ -76,6 +76,19 @@ const ecInitScatterOptionRef = ref<InstanceType<typeof VueEcharts>>();
 const ecMidScatterOptionRef = ref<InstanceType<typeof VueEcharts>>();
 const ecFinalScatterOptionRef = ref<InstanceType<typeof VueEcharts>>();
 
+// 新增:图片URL和模型类型选择
+const learningCurveImageUrl = ref('');
+const dataIncreaseCurveImageUrl = ref('');
+const selectedModelType = ref('rf'); // 默认选择随机森林
+
+// 模型类型选项
+const modelTypeOptions = [
+  { label: '随机森林', value: 'rf' },
+  { label: 'XGBoost', value: 'xgbr' },
+  { label: '梯度提升', value: 'gbst' },
+];
+
+
 // 计算数据范围的函数
 const calculateDataRange = (data: [number, number][]) => {
   const xValues = data.map(item => item[0]);
@@ -217,6 +230,46 @@ const fetchScatterData = async (modelId: number, optionRef: any) => {
   }
 };
 
+// 新增:获取学习曲线图片
+const fetchLearningCurveImage = async () => {
+  try {
+    const response = await api5000.get('/latest-learning-curve', {
+      params: {
+        data_type: 'reflux',
+        model_type: selectedModelType.value
+      },
+      responseType: 'blob'
+    });
+    
+    const blob = new Blob([response.data], { type: 'image/png' });
+    learningCurveImageUrl.value = URL.createObjectURL(blob);
+  } catch (error) {
+    console.error('获取学习曲线图片失败:', error);
+  }
+};
+
+// 新增:获取数据增长曲线图片
+const fetchDataIncreaseCurveImage = async () => {
+  try {
+    const response = await api5000.get('/latest-data-increase-curve', {
+      params: {
+        data_type: 'reflux',
+      },
+      responseType: 'blob'
+    });
+    
+    const blob = new Blob([response.data], { type: 'image/png' });
+    dataIncreaseCurveImageUrl.value = URL.createObjectURL(blob);
+  } catch (error) {
+    console.error('获取数据增长曲线图片失败:', error);
+  }
+};
+
+// 监听模型类型变化,重新获取图片
+watch(selectedModelType, () => {
+  fetchLearningCurveImage();
+  fetchDataIncreaseCurveImage();
+});
 // 定义调整图表大小的函数
 const resizeCharts = () => {
   nextTick(() => {
@@ -233,6 +286,10 @@ onMounted(async () => {
   if (props.showMidScatterChart) await fetchScatterData(props.midScatterModelId, ecMidScatterOption);
   if (props.showFinalScatterChart) await fetchScatterData(props.finalScatterModelId, ecFinalScatterOption);
 
+  // 新增:获取图片
+  await fetchLearningCurveImage();
+  await fetchDataIncreaseCurveImage();
+
   // 页面加载完成后调整图表大小
   resizeCharts();
 
@@ -243,37 +300,58 @@ onMounted(async () => {
 // 组件卸载时移除事件监听器
 onUnmounted(() => {
   window.removeEventListener('resize', resizeCharts);
+
+  // 清理Blob URL
+  if (learningCurveImageUrl.value) {
+    URL.revokeObjectURL(learningCurveImageUrl.value);
+  }
+  if (dataIncreaseCurveImageUrl.value) {
+    URL.revokeObjectURL(dataIncreaseCurveImageUrl.value);
+  }
 });
 </script>
+
 <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>
+      <!-- 散点图 -->
+      <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="ecMid极简白OptionRef" />
+
+   <h2 class="chart-header">模型性能分析</h2>
+    <!-- 模型性能分析部分 -->
+    <div class="analysis-section">
+       <!-- 数据增长曲线模块 -->
+      <div class="chart-module">
+        <h2 class="chart-header">数据增长曲线</h2>
+        <div class="image-chart-item">
+          <div class="image-container">
+            <img v-if="dataIncreaseCurveImageUrl" :src="dataIncreaseCurveImageUrl" alt="数据增长曲线" >
+            <div v-else class="image-placeholder">加载中...</div>
+          </div>
+        </div>
       </div>
-    </template>
-    <template v-if="showFinalScatterChart">
-      <!-- 最终代散点图表头 -->
-      <h2 class="chart-header">最终代散点图</h2>
-      <div class="chart-container">
-        <VueEcharts :option="ecFinalScatterOption" ref="ecFinalScatterOptionRef" />
+      <!-- 学习曲线模块 -->
+      <div class="chart-module">
+        <h2 class="chart-header">学习曲线</h2>
+        <div class="model-selector-wrapper">
+          <select v-model="selectedModelType" class="model-select">
+            <option v-for="option in modelTypeOptions" :key="option.value" :value="option.value">
+              {{ option.label }}
+            </option>
+          </select>
+        </div>
+        <div class="image-chart-item">
+          <div class="image-container">
+            <img v-if="learningCurveImageUrl" :src="learningCurveImageUrl" alt="学习曲线" >
+            <div v-else class="image-placeholder">加载中...</div>
+          </div>
+        </div>
       </div>
-    </template>
+    </div>
   </div>
 </template>
 
@@ -287,7 +365,7 @@ onUnmounted(() => {
   height: 100%;
   gap: 20px;
   color: #000;
-  background-color: white; /* 添加白色背景 */
+  background-color: white;
 }
 
 .chart-header {
@@ -307,16 +385,89 @@ onUnmounted(() => {
   height: 450px;
   margin: 0 auto;
   margin-bottom: 20px;
-  background-color: white; /* 图表容器背景为白色 */
+  background-color: white;
   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);
 }
 
 .VueEcharts {
   width: 100%;
   height: 100%;
   margin: 0 10px;
-  background-color: white; /* ECharts图表背景为白色 */
+  background-color: white;
+}
+
+/* 新增:图片图表样式 */
+.image-charts-section {
+  width: 90%;
+  margin: 30px auto;
+  padding: 20px;
+  background-color: #f8f9fa;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.model-type-selector {
+  margin: 20px 0;
+  text-align: center;
+}
+
+.model-type-selector label {
+  margin-right: 10px;
+  font-weight: bold;
+  color: #2c3e50;
+}
+
+.model-select {
+  padding: 8px 12px;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  background-color: white;
+  font-size: 14px;
+}
+
+.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;
+}
+
+.image-container img {
+  max-width: 100%;
+  max-height: 100%;
+  object-fit: contain;
+}
+
+.image-placeholder {
+  color: #6c757d;
+  font-style: italic;
 }
 
 /* 确保图表内部也是白色背景 */
@@ -339,6 +490,20 @@ onUnmounted(() => {
 :deep(.echarts-legend) {
   color: #333;
 }
-</style>
-
 
+/* 响应式设计 */
+@media (max-width: 768px) {
+  .image-charts-container {
+    grid-template-columns: 1fr;
+  }
+  
+  .image-container {
+    height: 300px;
+  }
+  
+  .chart-container {
+    width: 95%;
+    height: 400px;
+  }
+}
+</style>

+ 1 - 1
src/views/login/loginView.vue

@@ -158,7 +158,7 @@
 </template>
 
 <script setup lang="ts">
-import { reactive, ref, computed, watch, onMounted } from "vue";
+import { reactive, ref, computed, watch } from "vue";
 import { ElForm, ElMessage } from "element-plus";
 import type { FormRules } from "element-plus";
 import { login, register } from "@/API/users";

+ 1 - 1
src/views/packaged/PaginationComponent.vue

@@ -13,7 +13,7 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, watch, onMounted } from 'vue';
+import { ref, watch } from 'vue';
 
 // 定义 props
 interface Props {