Android官方文档—APP资源(Resource Types) (Menu)

本文详细介绍了Android应用程序中菜单资源的定义和使用方式,包括菜单、菜单项和菜单组的XML语法,以及如何在Java代码中引用这些资源。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

菜单资源

菜单资源定义了可以使用MenuInflater充气的应用程序菜单(选项菜单,上下文菜单或子菜单)。

有关使用菜单的指南,请参阅菜单开发人员指南。

文件位置:

res/menu/filename.xml

文件名将用作资源ID。

编译资源数据类型:
    指向Menu(或子类)资源的资源指针。

资源参考:

    在Java中:R.menu.filename
    在XML中:@ [package:] menu.filename

语法:



    <?xml version="1.0" encoding="utf-8"?>
    <menu xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:id="@[+][package:]id/resource_name"
              android:title="string"
              android:titleCondensed="string"
              android:icon="@[package:]drawable/drawable_resource_name"
              android:onClick="method name"
              android:showAsAction=["ifRoom" | "never" | "withText" | "always" | "collapseActionView"]
              android:actionLayout="@[package:]layout/layout_resource_name"
              android:actionViewClass="class name"
              android:actionProviderClass="class name"
              android:alphabeticShortcut="string"
              android:numericShortcut="string"
              android:checkable=["true" | "false"]
              android:visible=["true" | "false"]
              android:enabled=["true" | "false"]
              android:menuCategory=["container" | "system" | "secondary" | "alternative"]
              android:orderInCategory="integer" />
        <group android:id="@[+][package:]id/resource name"
               android:checkableBehavior=["none" | "all" | "single"]
               android:visible=["true" | "false"]
               android:enabled=["true" | "false"]
               android:menuCategory=["container" | "system" | "secondary" | "alternative"]
               android:orderInCategory="integer" >
            <item />
        </group>
        <item >
            <menu>
              <item />
            </menu>
        </item>
    </menu>

元素:

<menu>

必要。这必须是根节点。包含<item>和/或<group>元素。

属性:

xmlns:android

XML命名空间。必要。定义XML命名空间,该命名空间必须为“http://schemas.android.com/apk/res/android”。

<item>

菜单项。可能包含<menu>元素(用于子菜单)。必须是<menu>或<group>元素的子元素。

属性:

android:id

资源ID。唯一的资源ID。要为此项目创建新的资源ID,请使用以下格式:“@ + id / name”。加号表示应将其创建为新ID。

android:title

字符串资源。菜单标题为字符串资源或原始字符串。

android:titleCondensed

字符串资源。精简标题作为字符串资源或原始字符串。此标题用于正常标题太长的情况。

android:icon

可绘制的资源。要用作菜单项图标的图像。

android:onClick

方法名称。单击此菜单项时调用的方法。该方法必须在activity中声明为public,并接受MenuItem作为其唯一参数,该参数指示单击的项目。此方法优先于onOptionsItemSelected()的标准回调。请参见底部的示例。

警告:如果使用ProGuard(或类似工具)对代码进行模糊处理,请确保通过重命名排除在此属性中指定的方法,因为它可能会破坏功能。

在API Level 11中引入。

android:showAsAction

关键词。此项目何时以及如何在应用栏中显示为操作项。仅当活动包含应用栏时,菜单项才会显示为操作项。有效值:

ValueDescription
ifRoom如果有空间,只将此项目放在应用栏中。如果标记为“ifRoom”的所有项目都没有空间,则orderInCategory值最低的项目将显示为操作,其余项目将显示在溢出菜单中。
withText还包括带有操作项的标题文本(由android:title定义)。您可以将此值与其他一个值一起作为标志集包含在内,方法是将它们与管道分开。
never切勿将此项目放在应用栏中。而是在应用栏的溢出菜单中列出该项目。
always始终将此项目放在应用栏中。避免使用此项,除非项目始终出现在操作栏中至关重要。将多个项目设置为始终显示为操作项可能会导致它们与应用栏中的其他UI重叠。
collapseActionView与此操作项关联的操作视图(由android:actionLayout或android:actionViewClass声明)是可折叠的。
在API级别14中引入。

有关更多信息,请参阅添加App Bar培训课程。

在API Level 11中引入。

android:actionLayout

布局资源。用作动作视图的布局。

有关更多信息,请参阅操作视图和操作提供程序。

在API Level 11中引入。

android:actionViewClass

类名称。 View的完全限定类名,用作操作视图。例如,“android.widget.SearchView”将SearchView用作动作视图。

有关更多信息,请参阅操作视图和操作提供程序。

警告:如果使用ProGuard(或类似工具)对代码进行模糊处理,请确保通过重命名排除在此属性中指定的类,因为它可能会破坏功能。

android:actionProviderClass

类名称。 ActionProvider的完全限定类名,用于代替操作项。例如,“android.widget.ShareActionProvider”使用ShareActionProvider。

有关更多信息,请参阅操作视图和操作提供程序。

警告:如果使用ProGuard(或类似工具)对代码进行模糊处理,请确保通过重命名排除在此属性中指定的类,因为它可能会破坏功能。

在API级别14中引入。

android:alphabeticShortcut

字符。字母快捷键的字符。

android:numericShortcut

整数。数字快捷键的数字。

android:checkable

布尔。如果项目是可选中的,则为“true”。

android:checked

布尔。如果默认选中该项,则为“true”。

android:visible

布尔。如果项目默认可见,则为“true”。

android:enabled

布尔。如果默认启用该项,则为“true”。

android:menuCategory

关键词。与菜单CATEGORY_ *常量对应的值,用于定义项目的优先级。有效值:

ValueDescription
container对于属于容器的项目。
system对于系统提供的项目。
secondary对于用户提供的辅助(不经常使用)选项的项目。
alternative对于对当前显示的数据执行替代操作的项目。

android:orderInCategory

整数。组内的项目“重要性”的顺序。

<group>

菜单组(用于创建共享特征的项目集合,例如它们是可见的,启用的还是可检查的)。包含一个或多个<item>元素。必须是<menu>元素的子元素。

属性:

android:id

资源ID。唯一的资源ID。要为此项目创建新的资源ID,请使用以下格式:“@ + id / name”。加号表示应将其创建为新ID。

android:checkableBehavior

关键词。组的可检查行为的类型。有效值:

ValueDescription
none不可检查
all可以检查所有项目(使用复选框)
single只能检查一个项目(使用单选按钮)

android:visible

布尔。如果该组可见,则为“true”。

android:enabled

布尔。如果启用了组,则为“true”。

android:menuCategory

关键词。与菜单CATEGORY_ *常量对应的值,用于定义组的优先级。有效值:

ValueDescription
container对于属于容器的组。
system对于系统提供的组。
secondary对于用户提供的辅助(不经常使用)选项的组。
alternative对于对当前显示的数据执行替代操作的组。

android:orderInCategory

整数。类别中项目的默认顺序。

例子:

保存在res / menu / example_menu.xml的XML文件:

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/item1"
          android:title="@string/item1"
          android:icon="@drawable/group_item1_icon"
          android:showAsAction="ifRoom|withText"/>
    <group android:id="@+id/group">
        <item android:id="@+id/group_item1"
              android:onClick="onGroupItemClick"
              android:title="@string/group_item1"
              android:icon="@drawable/group_item1_icon" />
        <item android:id="@+id/group_item2"
              android:onClick="onGroupItemClick"
              android:title="@string/group_item2"
              android:icon="@drawable/group_item2_icon" />
    </group>
    <item android:id="@+id/submenu"
          android:title="@string/submenu_title"
          android:showAsAction="ifRoom|withText" >
        <menu>
            <item android:id="@+id/submenu_item1"
                  android:title="@string/submenu_item1" />
        </menu>
    </item>
</menu>

以下应用程序代码从onCreateOptionsMenu(Menu)回调中膨胀菜单,并声明其中两个项目的点击回调:

public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.example_menu, menu);
    return true;
}

public void onGroupItemClick(MenuItem item) {
    // One of the group items (using the onClick attribute) was clicked
    // The item parameter passed here indicates which item it is
    // All other menu item clicks are handled by onOptionsItemSelected()
}

 

<think>我们将在前端(algorithmselection.vue)和后端(app.py)进行修改,以实现连接。下面是完整的修改后的文件内容。###修改后的algorithmselection.vue文件主要修改点:1.在`uploadFiles`方法中,将上传请求的URL改为`http://localhost:5000/upload`(根据实际部署情况,可能需要调整)2.在`analyzeData`方法中,将分析请求的URL改为`http://localhost:5000/analyze`3.在上传请求中,添加对错误响应的处理(检查response.ok)4.更新数据概览的显示(使用后端返回的气体类型和浓度)注意:由于后端返回的`gas_types`是一个数组,我们需要将其显示出来。同时,我们使用后端返回的`sample_count`更新样本数量。```vue<template><divclass="layout"><!--顶部导航栏--><navclass="navbarnavbar-expand-lgnavbar-darkbg-primarymb-4"><divclass="container"><aclass="navbar-brand"href="#"><iclass="fafa-line-chartme-2"></i>气体传感器数据分析平台</a><spanclass="logout"@click="handleLogout">退出</span></div></nav><divclass="main-layout"><!--左侧边栏--><asideclass="sidebar"><ulclass="menu"><li@click="navigateTo('algorithms')">上传并选择算法</li><li@click="navigateTo('gas-info')">气体信息查询</li></ul></aside><!--右侧主内容区--><divclass="main-content"><divclass="container-fluid"><divclass="row"><!--左侧面板:数据上传和算法设置--><divclass="col-md-4"><!--数据上传板块--><divclass="cardmb-4"><divclass="card-headerbg-primarytext-white"><h5class="mb-0">数据上传</h5></div><divclass="card-body"><formid="uploadForm"enctype="multipart/form-data"@submit.prevent="uploadFiles"><divclass="mb-3"><labelfor="sensorType1"class="form-label">传感器类型1</label><selectclass="form-select"id="sensorType1"v-model="sensorType1"><optionvalue="MP2">MP2</option><optionvalue="MP3B">MP3B</option><optionvalue="MP503">MP503</option><optionvalue="MP801">MP801</option><optionvalue="MQ2">MQ2</option><optionvalue="MQ7B">MQ7B</option></select></div><divclass="mb-3"><labelfor="gasType1"class="form-label">气体类型1</label><selectclass="form-select"id="gasType1"v-model="gasType1"><optionvalue="acetone">丙酮(Acetone)</option><optionvalue="toluene">甲苯(Toluene)</option><optionvalue="methanol">甲醇(Methanol)</option><optionvalue="formaldehyde">甲醛(Formaldehyde)</option><optionvalue="ethanol">乙醇(Ethanol)</option><optionvalue="toluene+formaldehyde">甲苯+甲醛(toluene+formaldehyde)</option><optionvalue="methanol+toluene">甲醇+甲苯(methanol+toluene)</option></select></div><divclass="mb-3"><labelfor="concentration1"class="form-label">浓度1(ppm)</label><inputtype="number"class="form-control"id="concentration1"v-model="concentration1"min="1"></div><divclass="mb-3"><labelfor="sensorType2"class="form-label">传感器类型2</label><selectclass="form-select"id="sensorType2"v-model="sensorType2"><optionvalue="MP2">MP2</option><optionvalue="MP3B">MP3B</option><optionvalue="MP503">MP503</option><optionvalue="MP801">MP801</option><optionvalue="MQ2">MQ2</option><optionvalue="MQ7B">MQ7B</option></select></div><divclass="mb-3"><labelfor="gasType2"class="form-label">气体类型2</label><selectclass="form-select"id="gasType2"v-model="gasType2"><optionvalue="acetone">丙酮(Acetone)</option><optionvalue="toluene">甲苯(Toluene)</option><optionvalue="methanol">甲醇(Methanol)</option><optionvalue="formaldehyde">甲醛(Formaldehyde)</option><optionvalue="ethanol">乙醇(Ethanol)</option><optionvalue="toluene+formaldehyde">甲苯+甲醛(toluene+formaldehyde)</option><optionvalue="methanol+toluene">甲醇+甲苯(methanol+toluene)</option></select></div><divclass="mb-3"><labelfor="concentration2"class="form-label">浓度2(ppm)</label><inputtype="number"class="form-control"id="concentration2"v-model="concentration2"min="1"></div><divclass="mb-3"><labelclass="form-label">选择数据文件</label><divclass="row"><divclass="col-md-6"><labelclass="form-labelsmall">选择单个或多个文件</label><inputclass="form-control"type="file"id="fileUpload"multiple></div><divclass="col-md-6"><labelclass="form-labelsmall">选择整个文件夹</label><inputclass="form-control"type="file"id="folderUpload"webkitdirectorydirectorymultiple></div></div></div><buttontype="submit"class="btnbtn-primaryw-100"><iclass="fafa-uploadme-2"></i>上传数据</button></form><divid="uploadStatus"class="mt-3":class="uploadStatusClass">{{uploadStatusMessage}}</div></div></div><!--算法参数设置板块--><divclass="card"><divclass="card-headerbg-primarytext-white"><h5class="mb-0">算法参数设置</h5></div><divclass="card-body"><divclass="mb-3"><labelclass="form-label">K-近邻算法(KNN)</label><divclass="row"><divclass="col-6"><labelclass="form-label">邻居数量</label><inputtype="range"class="form-range"min="1"max="20"v-model="knnNeighbors"id="knnNeighbors"><spanid="knnNeighborsValue"class="badgebg-secondary">{{knnNeighbors}}</span></div><divclass="col-6"><labelclass="form-label">距离度量</label><selectclass="form-select"id="knnMetric"v-model="knnMetric"><optionvalue="euclidean">欧氏距离</option><optionvalue="manhattan">曼哈顿距离</option></select></div></div></div><divclass="mb-3"><labelclass="form-label">支持向量机(SVM)</label><divclass="row"><divclass="col-6"><labelclass="form-label">正则化参数$C$</label><inputtype="range"class="form-range"min="0.1"max="10"step="0.1"v-model="svmC"id="svmC"><spanid="svmCValue"class="badgebg-secondary">{{svmC}}</span></div><divclass="col-6"><labelclass="form-label">核函数</label><selectclass="form-select"id="svmKernel"v-model="svmKernel"><optionvalue="rbf">RBF</option><optionvalue="linear">线性</option><optionvalue="poly">多项式</option></select></div></div></div><divclass="mb-3"><labelclass="form-label">随机森林(RandomForest)</label><divclass="row"><divclass="col-6"><labelclass="form-label">树的数量</label><inputtype="range"class="form-range"min="10"max="200"step="10"v-model="rfEstimators"id="rfEstimators"><spanid="rfEstimatorsValue"class="badgebg-secondary">{{rfEstimators}}</span></div><divclass="col-6"><labelclass="form-label">最大深度</label><selectclass="form-select"id="rfMaxDepth"v-model="rfMaxDepth"><optionvalue="None">无限制</option><optionvalue="5">5</option><optionvalue="10">10</option></select></div></div></div><buttonid="analyzeBtn"class="btnbtn-successw-100mt-3"@click="analyzeData":disabled="analyzing"><iclass="fa":class="analyzing?'fa-spinnerfa-spin':'fa-play'"></i>{{analyzing?'分析中...':'开始分析'}}</button></div></div></div><!--右侧面板:数据概览、性能比较和详细结果--><divclass="col-md-8"><!--数据概览板块--><divclass="cardmb-4"><divclass="card-headerbg-primarytext-white"><h5class="mb-0">数据概览</h5></div><divclass="card-body"><divclass="rowtext-center"><divclass="col-md-4"><divclass="p-3bg-lightrounded"><h6>气体类型</h6><h4id="gasTypeDisplay">{{gasTypeDisplay}}</h4></div></div><divclass="col-md-4"><divclass="p-3bg-lightrounded"><h6>浓度(ppm)</h6><h4id="concentrationDisplay">{{concentrationDisplay}}</h4></div></div><divclass="col-md-4"><divclass="p-3bg-lightrounded"><h6>样本数量</h6><h4id="sampleCountDisplay">{{sampleCountDisplay}}</h4></div></div></div></div></div><!--算法性能比较板块--><divclass="cardmb-4"><divclass="card-headerbg-primarytext-white"><h5class="mb-0">算法性能比较</h5></div><divclass="card-body"><canvasid="accuracyChart"height="300"></canvas></div></div><!--详细结果板块--><divclass="card"><divclass="card-headerbg-primarytext-white"><h5class="mb-0">详细结果</h5></div><divclass="card-body"><divclass="table-responsive"><tableclass="tabletable-striped"><thead><tr><th>算法</th><th>训练集准确率</th><th>测试集准确率</th><th>操作</th></tr></thead><tbodyid="resultsTable"><templatev-for="(result,algoName)inresults":key="algoName"><!--主行--><trclass="main-row"><td>{{result.name}}</td><td>{{(result.train_accuracy*100).toFixed(2)}}%</td><td>{{(result.test_accuracy*100).toFixed(2)}}%</td><td><buttonclass="btnbtn-smbtn-outline-primaryview-details"@click="toggleDetails(algoName)">{{showDetails[algoName]?'隐藏详情':'查看详情'}}</button></td></tr><!--详情行--><trv-if="showDetails[algoName]"class="detail-row"><tdcolspan="4"><divclass="report-container"><pre>{{result.classification_report}}</pre></div></td></tr></template></tbody></table></div></div></div></div></div></div></div></div></div></template><script>importChartfrom'chart.js/auto';exportdefault{data(){return{//数据上传相关sensorType1:'MP2',gasType1:'acetone',concentration1:20,sensorType2:'MP2',gasType2:'acetone',concentration2:20,uploadStatusMessage:'',uploadStatusClass:'',//数据概览gasTypeDisplay:'-',concentrationDisplay:'-',sampleCountDisplay:'-',//算法参数knnNeighbors:5,knnMetric:'euclidean',svmC:1.0,svmKernel:'rbf',rfEstimators:100,rfMaxDepth:'None',//分析结果analyzing:false,results:{},showDetails:{},//图表实例accuracyChart:null};},mounted(){//初始化图表this.initAccuracyChart();},methods:{navigateTo(page){this.$router.push(`/${page}`);},handleLogout(){this.$router.push('/login');},//初始化准确率图表initAccuracyChart(){constctx=document.getElementById('accuracyChart').getContext('2d');this.accuracyChart=newChart(ctx,{type:'bar',data:{labels:[],datasets:[{label:'测试集准确率',data:[],backgroundColor:'rgba(54,162,235,0.7)',borderColor:'rgba(54,162,235,1)',borderWidth:1}]},options:{responsive:true,scales:{y:{beginAtZero:true,max:1,ticks:{callback:function(value){return(value*100).toFixed(0)+'%';}}}},plugins:{tooltip:{callbacks:{label:function(context){return'准确率:'+(context.raw*100).toFixed(2)+'%';}}}}}});},//上传文件asyncuploadFiles(){constfileInput=document.getElementById('fileUpload');constfolderInput=document.getElementById('folderUpload');//合并所有选择的文件constfiles=Array.from(fileInput.files).concat(Array.from(folderInput.files));if(files.length===0){this.showUploadStatus('请选择至少一个文件或文件夹','error');return;}//显示上传文件信息this.showUploadStatus(`正在上传${files.length}个文件...`,'info');constformData=newFormData();for(constfileoffiles){formData.append('files',file);}formData.append('gas_type1',this.gasType1);formData.append('concentration1',this.concentration1);formData.append('sensor_type1',this.sensorType1);formData.append('gas_type2',this.gasType2);formData.append('concentration2',this.concentration2);formData.append('sensor_type2',this.sensorType2);try{constresponse=awaitfetch('http://localhost:5000/upload',{method:'POST',body:formData});//检查响应状态if(!response.ok){consterrorData=awaitresponse.json();thrownewError(errorData.error||'上传失败');}constdata=awaitresponse.json();if(data.error){this.showUploadStatus(data.error,'error');}else{this.showUploadStatus(`成功上传${files.length}个文件,加载了${data.sample_count}个样本`,'success');//更新数据概览//使用后端返回的气体类型和浓度this.gasTypeDisplay=data.gas_types.join(',');this.concentrationDisplay=data.concentrations.join(',')+'ppm';this.sampleCountDisplay=data.sample_count;//清空输入框fileInput.value='';folderInput.value='';}}catch(error){this.showUploadStatus('上传失败:'+error.message,'error');}},//显示上传状态showUploadStatus(message,type){this.uploadStatusMessage=message;this.uploadStatusClass=type==='error'?'alertalert-danger':type==='success'?'alertalert-success':'alertalert-info';},//分析数据asyncanalyzeData(){this.analyzing=true;//获取算法参数constparams={knn:{n_neighbors:this.knnNeighbors,metric:this.knnMetric},svm:{C:this.svmC,kernel:this.svmKernel},random_forest:{n_estimators:this.rfEstimators,max_depth:this.rfMaxDepth==='None'?null:parseInt(this.rfMaxDepth)}};try{constresponse=awaitfetch('http://localhost:5000/analyze',{method:'POST',headers:{'Content-Type':'application/json',},body:JSON.stringify({params:params,gas_type1:this.gasType1,concentration1:this.concentration1,gas_type2:this.gasType2,concentration2:this.concentration2})});//检查响应状态if(!response.ok){consterrorData=awaitresponse.json();thrownewError(errorData.error||'分析失败');}constdata=awaitresponse.json();if(data.error){thrownewError(data.error);}//更新图表和结果this.updateAccuracyChart(data.results);this.results=data.results;//重置详情展开状态this.showDetails={};for(constalgoNameindata.results){this.$set(this.showDetails,algoName,false);}}catch(error){this.showUploadStatus('分析失败:'+error.message,'error');}finally{this.analyzing=false;}},//更新准确率图表updateAccuracyChart(results){constlabels=[];constaccuracies=[];for(constalgoNameinresults){labels.push(results[algoName].name);accuracies.push(results[algoName].test_accuracy);}this.accuracyChart.data.labels=labels;this.accuracyChart.data.datasets[0].data=accuracies;this.accuracyChart.update();},//切换详情显示toggleDetails(algoName){this.$set(this.showDetails,algoName,!this.showDetails[algoName]);}}};</script><stylescoped>/*页面框架样式*/.layout{display:flex;flex-direction:column;height:100vh;background-color:#f8f9fa;}.navbar{padding:0.5rem1rem;box-shadow:02px4pxrgba(0,0,0,0.1);}.logout{color:white;cursor:pointer;padding:0.5rem;margin-left:auto;}.main-layout{display:flex;flex:1;overflow:hidden;}.sidebar{width:220px;background-color:#d5d5d5;padding:30px20px;box-shadow:2px010pxrgba(0,0,0,0.05);}.menuli{list-style:none;padding:12px;margin:10px0;cursor:pointer;border-radius:8px;transition:background0.2s;font-size:16px;}.menuli:hover{background-color:#dcdcdc;}/*主内容区样式*/.main-content{flex:1;background-color:#f6f9ff;overflow-y:auto;padding:15px;}/*卡片样式*/.card{box-shadow:04px6pxrgba(0,0,0,0.1);transition:all0.3sease;border-radius:8px;border:1pxsolid#dee2e6;margin-bottom:1.5rem;}.card:hover{box-shadow:010px15pxrgba(0,0,0,0.1);}.card-header{padding:0.75rem1.25rem;font-size:1rem;}.badge{font-size:0.9em;padding:0.35em0.65em;}#uploadStatus{min-height:50px;padding:10px;border-radius:5px;}.alert{padding:10px;margin-bottom:10px;border-radius:5px;}.alert-success{background-color:#d1e7dd;color:#0f5132;}.alert-danger{background-color:#f8d7da;color:#842029;}.alert-info{background-color:#cff4fc;color:#055160;}.tableth{background-color:#f8f9fa;}/*详情行样式*/.detail-row{background-color:#f8f9fa;border-left:4pxsolid#0d6efd;transition:all0.3sease;}.detail-row:hover{background-color:#e9ecef;}.detail-row.report-container{padding:15px;}.detail-rowpre{white-space:pre-wrap;word-wrap:break-word;font-size:0.9rem;padding:10px;margin:0;max-height:300px;overflow-y:auto;background-color:#fff;border-radius:4px;border:1pxsolid#dee2e6;font-family:'CourierNew',monospace;line-height:1.4;}/*表格行悬停效果*/#resultsTabletr.main-row{cursor:pointer;transition:background-color0.2s;}#resultsTabletr.main-row:hover{background-color:rgba(13,110,253,0.05);}/*按钮样式优化*/.btn{padding:0.375rem0.75rem;font-size:0.9rem;}.view-details{transition:all0.3s;padding:3px8px;font-size:0.85rem;}.view-details:hover{background-color:#0d6efd;color:white;}/*响应式调整*/@media(max-width:992px){.main-layout{flex-direction:column;}.sidebar{width:100%;padding:15px;height:auto;}.main-content{padding:10px;}}@media(max-width:768px){.col-md-4,.col-md-8{width:100%;max-width:100%;flex:00100%;}.detail-rowpre{max-height:200px;font-size:0.8rem;}.view-details{font-size:0.75rem;}}</style>```###修改后的app.py文件主要修改点:1.添加CORS支持,允许跨域请求2.修改上传路由,返回气体类型、浓度和样本数量3.在分析路由中,从临时文件中读取数据4.添加错误处理```pythonfromflaskimportFlask,render_template,request,jsonify,send_from_directoryfromflask_corsimportCORS#导入CORSfromnew_algorithmimportGasSensorDataAnalyzer,AlgorithmSelector,detect_dataset_typeimporttempfileimportosimportpandasaspdimportnumpyasnpimportjsonimportshutilimporttracebackimportloggingimportrefromsklearn.metricsimportclassification_reportapp=Flask(__name__)CORS(app)#允许跨域请求app.config['UPLOAD_FOLDER']=tempfile.mkdtemp()#配置日志logging.basicConfig(level=logging.DEBUG,format='%(asctime)s-%(name)s-%(levelname)s-%(message)s')logger=logging.getLogger('gas_sensor_app')#初始化数据加载器和算法选择器data_loader=GasSensorDataAnalyzer()selector=AlgorithmSelector(use_chinese=True)defsanitize_filename(filename):"""清理文件名中的非法字符和路径分隔符保留中文、字母、数字、点、连字符和下划线"""#替换所有路径分隔符为下划线filename=re.sub(r'[\\/]','_',filename)#保留中文、字母、数字、点、连字符和下划线filename=re.sub(r'[^\w\u4e00-\u9fff\.-]','_',filename)returnfilenamedefallowed_file(filename):"""检查文件扩展名是否允许"""ALLOWED_EXTENSIONS={'xlsx','csv','txt'}return'.'infilenameand\filename.rsplit('.',1)[1].lower()inALLOWED_EXTENSIONS@app.route('/')defindex():"""主页面"""returnrender_template('index.html')@app.route('/upload',methods=['POST'])defupload_file():"""处理文件上传并自动检测数据集类型"""temp_dir=Nonetry:logger.info("收到文件上传请求")if'files'notinrequest.files:logger.warning("请求中缺少文件部分")returnjsonify({'error':'Nofilepart'}),400files=request.files.getlist('files')ifnotfilesorfiles[0].filename=='':logger.warning("未选择任何文件")returnjsonify({'error':'Noselectedfile'}),400#创建临时目录来保存上传的文件temp_dir=tempfile.mkdtemp(dir=app.config['UPLOAD_FOLDER'])logger.debug(f"创建临时目录:{temp_dir}")file_paths=[]forfileinfiles:iffileandallowed_file(file.filename):#清理文件名中的非法字符safe_filename=sanitize_filename(file.filename)#保存临时文件file_path=os.path.join(temp_dir,safe_filename)logger.debug(f"保存文件:{file_path}")#确保目录存在os.makedirs(os.path.dirname(file_path),exist_ok=True)file.save(file_path)file_paths.append(file_path)logger.info(f"成功保存{len(file_paths)}个文件")#获取表单数据gas_type1=request.form.get('gas_type1','acetone')concentration1=request.form.get('concentration1','20')sensor_type1=request.form.get('sensor_type1','MP2')gas_type2=request.form.get('gas_type2','acetone')concentration2=request.form.get('concentration2','20')sensor_type2=request.form.get('sensor_type2','MP2')#使用detect_dataset_type分析文件夹logger.debug("开始检测数据集类型...")dataset_type,detected_paths,gas_types,concentrations,sensor_types=detect_dataset_type(temp_dir,gas_types=[gas_type1,gas_type2],concentrations=[concentration1,concentration2],sensor_types=[sensor_type1,sensor_type2])logger.info(f"检测到数据集类型:{dataset_type}")logger.debug(f"气体类型:{gas_types}")logger.debug(f"浓度:{concentrations}")logger.debug(f"传感器类型:{sensor_types}")#使用数据加载器加载数据logger.debug("开始加载数据...")X,y=data_loader.load_multiple_gas_data(detected_paths,gas_types,concentrations,sensor_types)ifXisNoneorlen(X)==0:logger.error("加载数据失败:没有有效数据")returnjsonify({'error':'Novaliddataloaded'}),400logger.info(f"成功加载数据:{len(X)}个样本,{X.shape[1]}个特征")#保存合并后的数据df=pd.DataFrame(X)df['label']=yfile_path=os.path.join(app.config['UPLOAD_FOLDER'],'temp_data.xlsx')logger.debug(f"保存合并数据到:{file_path}")df.to_excel(file_path,index=False)#获取气体名称映射gas_names={'acetone':'丙酮(Acetone)','toluene':'甲苯(Toluene)','methanol':'甲醇(Methanol)','formaldehyde':'甲醛(Formaldehyde)','ethanol':'乙醇(Ethanol)','toluene+formaldehyde':'甲苯+甲醛(Toluene+Formaldehyde)','methanol+toluene':'甲醇+甲苯(methanol+toluene)','methanol+toluene+formaldehyde':'甲醇+甲苯+甲醛(Methanol+Toluene+Formaldehyde)'}#准备返回结果response={'status':'success','sample_count':len(X),'feature_count':X.shape[1],'gas_types':[gas_names.get(gt,gt)forgtingas_types],'concentrations':concentrations,'sensor_types':sensor_types,'dataset_type':dataset_type}logger.info("文件上传处理成功")returnjsonify(response)exceptExceptionase:logger.error(f"文件上传出错:{str(e)}")logger.error(traceback.format_exc())returnjsonify({'error':'Internalservererror','message':str(e),'traceback':traceback.format_exc()}),500finally:#清理临时文件iftemp_dir:try:logger.debug(f"清理临时目录:{temp_dir}")shutil.rmtree(temp_dir,ignore_errors=True)exceptExceptionase:logger.warning(f"清理临时目录时出错:{str(e)}")@app.route('/analyze',methods=['POST'])defanalyze_data():"""执行数据分析"""try:logger.info("收到数据分析请求")data=request.get_json()ifnotdata:logger.warning("请求中没有数据")returnjsonify({'error':'Nodataprovided'}),400#从请求中获取参数params=data.get('params',{})logger.debug(f"算法参数:{params}")#设置算法参数foralgo_name,algo_paramsinparams.items():selector.set_algorithm_params(algo_name,algo_params)#加载数据file_path=os.path.join(app.config['UPLOAD_FOLDER'],'temp_data.xlsx')logger.debug(f"从{file_path}加载数据")ifnotos.path.exists(file_path):logger.error(f"数据文件不存在:{file_path}")returnjsonify({'error':'Datafilenotfound'}),404df=pd.read_excel(file_path)if'label'notindf.columns:logger.error("数据文件中缺少'label'列")returnjsonify({'error':"Datafilemissing'label'column"}),400X=df.drop('label',axis=1).valuesy=df['label'].valuesifXisNoneorlen(X)==0:logger.error("没有可用于分析的数据")returnjsonify({'error':'Nodataavailableforanalysis'}),400logger.info(f"开始训练模型,样本数:{len(X)}")#训练模型results=selector.train_models(X,y)logger.info("模型训练完成")#准备返回结果response={'status':'success','results':{},'sample_count':len(X),}foralgo_name,resultinresults.items():#生成文本格式的分类报告report_text=""if'y_test'inresultand'y_pred'inresult:try:report_text=classification_report(result['y_test'],result['y_pred'])exceptExceptionase:report_text=f"生成分类报告出错:{str(e)}"elif'error'inresult:report_text=result['error']response['results'][algo_name]={'name':result['name'],'train_accuracy':result.get('train_accuracy',0),'test_accuracy':result.get('test_accuracy',0),'classification_report':report_text}returnjsonify(response)exceptExceptionase:logger.error(f"数据分析出错:{str(e)}")logger.error(traceback.format_exc())returnjsonify({'error':'Internalservererror','message':str(e),'traceback':traceback.format_exc()}),500@app.route('/static/<path:filename>')defserve_static(filename):"""提供静态文件"""returnsend_from_directory('static',filename)#全局错误处理@app.errorhandler(404)defnot_found_error(error):returnjsonify({'error':'Resourcenotfound'}),404@app.errorhandler(500)definternal_error(error):returnjsonify({'error':'Internalservererror','message':str(error),'traceback':traceback.format_exc()}),500if__name__=='__main__':app.run(debug=True,port=5000)```###注意事项:1.后端需要安装`flask-cors`:`pipinstallflask-cors`2.确保`new_algorithm.py`中的`GasSensorDataAnalyzer`和`AlgorithmSelector`类已经实现,并且`detect_dataset_type`函数可用。3.前端和后端运行在不同的端口(前端默认`5173`,后端`5000`),所以需要配置跨域。###运行步骤:1.启动后端:`pythonapp.py`2.启动前端:在`gas-classification-frontend`目录下运行`npmrundev`这样,前端就可以与后端进行通信了。§§相关问题§§:1.如何解决Vue和Flask开发时的跨域问题?2.在生产环境中如何部署Vue+Flask应用?3.如何优化大文件上传的性能和用户体验?4.Vue中如何处理FormData文件上传的最佳实践?</think>以下是修改后的完整 `algorithmselection.vue` 和 `app.py` 文件,已实现前后端连接: ### 修改后的完整 algorithmselection.vue ```vue <template> <div class="layout"> <!-- 顶部导航栏 --> <nav class="navbar navbar-expand-lg navbar-dark bg-primary mb-4"> <div class="container"> <a class="navbar-brand" href="#"> <i class="fa fa-line-chart me-2"></i>气体传感器数据分析平台 </a> <span class="logout" @click="handleLogout">退出</span> </div> </nav> <div class="main-layout"> <!-- 左侧边栏 --> <aside class="sidebar"> <ul class="menu"> <li @click="navigateTo('algorithms')">上传并选择算法</li> <li @click="navigateTo('gas-info')">气体信息查询</li> </ul> </aside> <!-- 右侧主内容区 --> <div class="main-content"> <div class="container-fluid"> <div class="row"> <!-- 左侧面板:数据上传和算法设置 --> <div class="col-md-4"> <!-- 数据上传板块 --> <div class="card mb-4"> <div class="card-header bg-primary text-white"> <h5 class="mb-0">数据上传</h5> </div> <div class="card-body"> <form id="uploadForm" enctype="multipart/form-data" @submit.prevent="uploadFiles"> <div class="mb-3"> <label for="sensorType1" class="form-label">传感器类型 1</label> <select class="form-select" id="sensorType1" v-model="sensorType1"> <option value="MP2">MP2</option> <option value="MP3B">MP3B</option> <option value="MP503">MP503</option> <option value="MP801">MP801</option> <option value="MQ2">MQ2</option> <option value="MQ7B">MQ7B</option> </select> </div> <div class="mb-3"> <label for="gasType1" class="form-label">气体类型 1</label> <select class="form-select" id="gasType1" v-model="gasType1"> <option value="acetone">丙酮 (Acetone)</option> <option value="toluene">甲苯 (Toluene)</option> <option value="methanol">甲醇 (Methanol)</option> <option value="formaldehyde">甲醛 (Formaldehyde)</option> <option value="ethanol">乙醇 (Ethanol)</option> <option value="toluene+formaldehyde">甲苯+甲醛 (toluene+formaldehyde)</option> <option value="methanol+toluene">甲醇+甲苯 (methanol+toluene)</option> </select> </div> <div class="mb-3"> <label for="concentration1" class="form-label">浓度 1 (ppm)</label> <input type="number" class="form-control" id="concentration1" v-model="concentration1" min="1"> </div> <div class="mb-3"> <label for="sensorType2" class="form-label">传感器类型 2</label> <select class="form-select" id="sensorType2" v-model="sensorType2"> <option value="MP2">MP2</option> <option value="MP3B">MP3B</option> <option value="MP503">MP503</option> <option value="MP801">MP801</option> <option value="MQ2">MQ2</option> <option value="MQ7B">MQ7B</option> </select> </div> <div class="mb-3"> <label for="gasType2" class="form-label">气体类型 2</label> <select class="form-select" id="gasType2" v-model="gasType2"> <option value="acetone">丙酮 (Acetone)</option> <option value="toluene">甲苯 (Toluene)</option> <option value="methanol">甲醇 (Methanol)</option> <option value="formaldehyde">甲醛 (Formaldehyde)</option> <option value="ethanol">乙醇 (Ethanol)</option> <option value="toluene+formaldehyde">甲苯+甲醛 (toluene+formaldehyde)</option> <option value="methanol+toluene">甲醇+甲苯 (methanol+toluene)</option> </select> </div> <div class="mb-3"> <label for="concentration2" class="form-label">浓度 2 (ppm)</label> <input type="number" class="form-control" id="concentration2" v-model="concentration2" min="1"> </div> <div class="mb-3"> <label class="form-label">选择数据文件</label> <div class="row"> <div class="col-md-6"> <label class="form-label small">选择单个或多个文件</label> <input class="form-control" type="file" id="fileUpload" multiple> </div> <div class="col-md-6"> <label class="form-label small">选择整个文件夹</label> <input class="form-control" type="file" id="folderUpload" webkitdirectory directory multiple> </div> </div> </极 <button type="submit" class="btn btn-primary w-100"> <i class="fa fa-upload me-2"></i>上传数据 </button> </form> <div id="uploadStatus" class="mt-3" :class="uploadStatusClass">{{ uploadStatusMessage }}</div> </div> </div> <!-- 算法参数设置板块 --> <div class="card"> <div class="card-header bg-primary text-white"> <h5 class="mb-0">算法参数设置</h极 </div> <div class="card-body"> <div class="mb-3"> <label class="form-label">K-近邻算法 (KNN)</label> <div class="row"> <div class="col-6"> <label class="form-label">邻居数量</label> <input type="range" class="form-range" min="1" max="20" v-model="knnNeighbors" id="knnNeighbors"> <span id="knnNeighborsValue" class="badge bg-secondary">{{ knnNeighbors }}</span> </div> <div class="col-6"> <label class="form-label">距离度量</label> <select class="form-select" id="knnMetric" v-model="knnMetric"> <option value="euclidean">欧氏距离</option> <option value="manhattan">曼哈顿距离</option> </select> </div> </div> </div> <div class="mb-3"> <label class="form-label">支持向量机 (SVM)</label> <div class="row"> <div class="col-6"> <label class="form-label">正则化参数 $ C$</label> <input type="range" class="form-range" min="0.1" max="10" step="0.1" v-model="svmC" id="svmC"> <span id="svmCValue" class="bad极 bg-secondary">{{ svmC }}</span> </div> <div class="col-6"> <label class="form-label">核函数</label> <select class="form-select" id="svmKernel" v-model="svmKernel"> <option value="rbf">RBF</option> <option value="linear">线性</option> <option value="poly">多项式</option> </select> </div> </div> </div> <div class="mb-3"> <label class="form-label">随机森林 (Random Forest)</label> <div class="row"> <div class="col-6"> <label class="form-label">树的数量</label> <input type="range" class="form-range" min="10" max="200" step="10" v-model="rfEstimators" id="rfEstimators"> <span id="rfEstimatorsValue" class="badge bg-secondary">{{ rfEstimators }}</span> </div> <div class="col-6"> <label class="form-label">最大深度</label> <select class="form-select" id="rfMaxDepth" v-model="rfMaxDepth"> <option value="None">无限制</option> <option value="5">5</option> <option value="10">10</option> </select> </div> </div> </div> <button id="analyzeBtn" class="btn btn-success w-100 mt-3" @click="analyzeData" :disabled="analyzing"> <i class="fa" :class="analyzing ? 'fa-spinner fa-spin' : 'fa-play'"></i> {{ analyzing ? '分析中...' : '开始分析' }} </button> </div> </div> </div> <!-- 右侧面板:数据概览、性能比较和详细结果 --> <div class="col-md-8"> <!-- 数据概览板块 --> <div class="card mb-4"> <div class="card-header bg-primary text-white"> <h5 class="mb-0">数据概览</h5> </div> <div class="card-body"> <div class="row text-center"> <div class="col-md-4"> <div class="p-3 bg-light rounded"> <h6>气体类型</h6> <h4 id="gasTypeDisplay">{{ gasTypeDisplay }}</h4> </div> </div> <div class="col-md-4"> <div class="p-3 bg-light rounded"> <h6>浓度 (ppm)</h6> <h4 id="concentrationDisplay">{{ concentrationDisplay }}</h4> </div> </div> <div class="col-md-4"> <div class="p-3 bg-light rounded"> <h6>样本数量</h6> <h4 id="sampleCountDisplay">{{ sampleCountDisplay }}</h4> </div> </div> </div> </div> </div> <!-- 算法性能比较板块 --> <div class="card mb-4"> <div class="card-header bg-primary text-white"> <h5 class="mb-0">算法性能比较</h5> </div> <div class="card-body"> <canvas id="accuracyChart" height="300"></canvas> </div> </div> <!-- 详细结果板块 --> <div class="card"> <div class="card-header bg-primary text-white"> <h5 class="mb-0">详细结果</h5> </div> <div class="card-body"> <div class="table-responsive"> <table class="table table-striped"> <thead> <tr> <th>算法</th> <th>训练集准确率</th> <th>测试集准确率</th> <th>操作</th> </tr> </thead> <tbody id="resultsTable"> <template v-for="(result, algoName) in results" :key="algoName"> <!-- 主行 --> <tr class="main-row"> <td>{{ result.name }}</td> <td>{{ (result.train_accuracy * 100).toFixed(2) }}%</td> <td>{{ (result.test_accuracy * 100).toFixed(2) }}%</td> <td> <button class="btn btn-sm btn-outline-primary view-details" @click="toggleDetails(algoName)"> {{ showDetails[algoName] ? '隐藏详情' : '查看详情' }} </button> </td> </tr> <!-- 详情行 --> <tr v-if="showDetails[algoName]" class="detail-row"> <td colspan="4"> <div class="report-container"> <pre>{{ result.classification_report }}</pre> </div> </td> </tr> </template> </tbody> </table> </div> </div> </div> </div> </div> </div> </div> </div> </div> </template> <script> import Chart from 'chart.js/auto'; export default { data() { return { // 数据上传相关 sensorType1: 'MP2', gasType1: 'acetone', concentration1: 20, sensorType2: 'MP2', gasType2: 'acetone', concentration2: 20, uploadStatusMessage: '', uploadStatusClass: '', // 数据概览 gasTypeDisplay: '-', concentrationDisplay: '-', sampleCountDisplay: '-', // 算法参数 knnNeighbors: 5, knnMetric: 'euclidean', svmC: 1.0, svmKernel: 'r极', rfEstimators: 100, rfMaxDepth: 'None', // 分析结果 analyzing: false, results: {}, showDetails: {}, // 图表实例 accuracyChart: null }; }, mounted() { // 初始化图表 this.initAccuracyChart(); }, methods: { navigateTo(page) { this.$router.push(`/${page}`); }, handleLogout() { this.$router.push('/login'); }, // 初始化准确率图表 initAccuracyChart() { const ctx = document.getElementById('accuracyChart').getContext('2d'); this.accuracyChart = new Chart(ctx, { type: 'bar', data: { labels: [], datasets: [{ label: '测试集准确率', data: [], backgroundColor: 'rgba(54, 162, 235, 0.7)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 }] }, options: { responsive: true, scales: { y: { beginAtZero: true, max: 1, ticks: { callback: function(value) { return (value * 100).toFixed(0) + '%'; } } } }, plugins: { tooltip: { callbacks: { label: function(context) { return '准确率: ' + (context.raw * 100).toFixed(2) + '%'; } } } } } }); }, // 上传文件 async uploadFiles() { const fileInput = document.getElementById('fileUpload'); const folderInput = document.getElementById('folderUpload'); // 合并所有选择的文件 const files = Array.from(fileInput.files).concat(Array.from(folderInput.files)); if (files.length === 0) { this.showUploadStatus('请选择至少一个文件或文件夹', 'error'); return; } this.showUploadStatus(`正在上传 ${files.length} 个文件...`, 'info'); const formData = new FormData(); for (const file of files) { formData.append('files', file); } formData.append('gas_type1', this.gasType1); formData.append('concentration1', this.concentration1); formData.append('sensor_type1', this.sensorType1); formData.append('gas_type2', this.gasType2); formData.append('concentration2', this.concentration2); formData.append('sensor_type2', this.sensorType2); try { const response = await fetch('http://localhost:5000/upload', { method: 'POST', body: formData }); // 检查响应状态 if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || '上传失败'); } const data = await response.json(); if (data.error) { this.showUploadStatus(data.error, 'error'); } else { this.showUploadStatus(`成功上传 ${files.length} 个文件,加载了 ${data.sample_count} 个样本`, 'success'); // 更新数据概览 this.gasTypeDisplay = data.gas_types.join(', '); this.concentrationDisplay = data.concentrations.join(', ') + ' ppm'; this.sampleCountDisplay = data.sample_count; // 清空输入框 fileInput.value = ''; folderInput.value = ''; } } catch (error) { this.showUploadStatus('上传失败: ' + error.message, 'error'); } }, // 显示上传状态 showUploadStatus(message, type) { this.uploadStatusMessage = message; this.uploadStatusClass = type === 'error' ? 'alert alert-danger' : type === 'success' ? 'alert alert-success' : 'alert alert-info'; }, // 分析数据 async analyzeData() { this.analyzing = true; // 获取算法参数 const params = { knn: { n_neighbors: this.knnNeighbors, metric: this.knnMetric }, svm: { C: this.svmC, kernel: this.svmKernel }, random_forest: { n_estimators: this.rfEstimators, max_depth: this.rfMaxDepth === 'None' ? null : parseInt(this.rfMaxDepth) } }; try { const response = await fetch('http://localhost:5000/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ params: params, gas_type1: this.gasType1, concentration1: this.concentration1, gas_type2: this.gasType2, concentration2: this.concentration2 }) }); // 检查响应状态 if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || '分析失败'); } const data = await response.json(); if (data.error) { throw new Error(data.error); } // 更新图表和结果 this.updateAccuracyChart(data.results); this.results = data.results; // 重置详情展开状态 this.showDetails = {}; for (const algoName in data.results) { this.$set(this.showDetails, algoName, false); } } catch (error) { this.showUploadStatus('分析失败: ' + error.message, 'error'); } finally { this.analyzing = false; } }, // 更新准确率图表 updateAccuracyChart(results) { const labels = []; const accuracies = []; for (const algoName in results) { labels.push(results[algoName].name); accuracies.push(results[algoName].test_accuracy); } this.accuracyChart.data.labels = labels; this.accuracyChart.data.datasets[0].data = accuracies; this.accuracyChart.update(); }, // 切换详情显示 toggleDetails(algoName) { this.$set(this.showDetails, algoName, !this.showDetails[algoName]); } } }; </script> <style scoped> /* 页面框架样式 */ .layout { display: flex; flex-direction: column; height: 100vh; background-color: #f8f9fa; } .navbar { padding: 0.5rem 1rem; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .logout { color: white; cursor: pointer; padding: 0.5rem; margin-left: auto; } .main-layout { display: flex; flex: 1; overflow: hidden; } .sidebar { width: 220px; background-color: #d5d5d5; padding: 30px 20px; box-shadow: 2px 0 10px rgba(0, 0, 0, 0.05); } .menu li { list-style: none; padding: 12px; margin: 10px 0; cursor: pointer; border-radius: 8px; transition: background 0.2s; font-size: 16px; } .menu li:hover { background-color: #dcdcdc; } /* 主内容区样式 */ .main-content { flex: 1; background-color: #f6f9ff; overflow-y: auto; padding: 15px; } /* 卡片样式 */ .card { box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); transition: all 0.3s ease; border-radius: 8px; border: 1px solid #dee2e6; margin-bottom: 1.5rem; } .card:hover { box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1); } .card-header { padding: 0.75rem 1.25rem; font-size: 1rem; } .badge { font-size: 0.9em; padding: 0.35em 0.65em; } #uploadStatus { min-height: 50px; padding: 10px; border-radius: 5px; } .alert { padding: 10px; margin-bottom: 10px; border-radius: 5px; } .alert-success { background-color: #d1e7dd; color: #0f5132; } .alert-danger { background-color: #f8d7da; color: #842029; } .alert-info { background-color: #cff4fc; color: #055160; } .table th { background-color: #f8f9fa; } /* 详情行样式 */ .detail-row { background-color: #f8f9fa; border-left: 4px solid #0d6efd; transition: all 0.3s ease; } .detail-row:hover { background-color: #e9ecef; } .detail-row .report-container { padding: 15px; } .detail-row pre { white-space: pre-wrap; word-wrap: break-word; font-size: 0.9rem; padding: 10px; margin: 0; max-height: 300px; overflow-y: auto; background-color: #fff; border-radius: 4px; border: 1px solid #dee2e6; font-family: 'Courier New', monospace; line-height: 1.4; } /* 表格行悬停效果 */ #resultsTable tr.main-row { cursor: pointer; transition: background-color 0.2s; } #resultsTable tr.main-row:hover { background-color: rgba(13, 110, 253, 0.05); } /* 按钮样式优化 */ .btn { padding: 0.375rem 0.75rem; font-size: 0.9rem; } .view-details { transition: all 0.3s; padding: 3px 8px; font-size: 0.85rem; } .view-details:hover { background-color: #0d6efd; color: white; } /* 响应式调整 */ @media (max-width: 992px) { .main-layout { flex-direction: column; } .sidebar { width: 100%; padding: 15px; height: auto; } .main-content { padding: 10px; } } @media (max-width: 768px) { .col-md-4, .col-md-8 { width: 100%; max-width: 100%; flex: 0 0 100%; } .detail-row pre { max-height: 200极 font-size: 0.8rem; } .view-details { font-size: 0.75rem; } } </style> ``` ### 修改后的完整 app.py ```python from flask import Flask, render_template, request, jsonify, send_from_directory from flask_cors import CORS from new_algorithm import GasSensorDataAnalyzer, AlgorithmSelector, detect_dataset_type import tempfile import os import pandas as pd import numpy as np import json import shutil import traceback import logging import re from sklearn.metrics import classification_report app = Flask(__name__) CORS(app) # 添加CORS支持 app.config['UPLOAD_FOLDER'] = tempfile.mkdtemp() # 配置日志 logging.basicConfig( level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger('gas_sensor_app') # 初始化数据加载器和算法选择器 data_loader = GasSensorDataAnalyzer() selector = AlgorithmSelector(use_chinese=True) def sanitize_filename(filename): """ 清理文件名中的非法字符和路径分隔符 保留中文、字母、数字、点、连字符和下划线 """ # 替换所有路径分隔符为下划线 filename = re.sub(r'[\\/]', '_', filename) # 保留中文、字母、数字、点、连字符和下划线 filename = re.sub(r'[^\w\u4e00-\u9fff\.-]', '_', filename) return filename def allowed_file(filename): """检查文件扩展名是否允许""" ALLOWED_EXTENSIONS = {'xlsx', 'csv', 'txt'} return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS @app.route('/') def index(): """主页面""" return render_template('index.html') @app.route('/upload', methods=['POST']) def upload_file(): """处理文件上传并自动检测数据集类型""" temp_dir = None try: logger.info("收到文件上传请求") if 'files' not in request.files: logger.warning("请求中缺少文件部分") return jsonify({'error': 'No file part'}), 400 files = request.files.getlist('files') if not files or files[0].filename == '': logger.warning("未选择任何文件") return jsonify({'error': 'No selected file'}), 400 # 创建临时目录来保存上传的文件 temp_dir = tempfile.mkdtemp(dir=app.config['UPLOAD_FOLDER']) logger.debug(f"创建临时目录: {temp_dir}") file_paths = [] for file in files: if file and allowed_file(file.filename): # 清理文件名中的非法字符 safe_filename = sanitize_filename(file.filename) # 保存临时文件 file_path = os.path.join(temp_dir, safe_filename) logger.debug(f"保存文件: {file_path}") # 确保目录存在 os.makedirs(os.path.dirname(file_path), exist_ok=True) file.save(file_path) file_paths.append(file_path) logger.info(f"成功保存 {len(file_paths)} 个文件") # 获取表单数据 gas_type1 = request.form.get('gas_type1', 'acetone') concentration1 = request.form.get('concentration1', '20') sensor_type1 = request.form.get('sensor_type1', 'MP2') gas_type2 = request.form.get('gas_type2', 'acetone') concentration2 = request.form.get('concentration2', '20') sensor_type2 = request.form.get('sensor_type2', 'MP2') # 使用detect_dataset_type分析文件夹 logger.debug("开始检测数据集类型...") dataset_type, detected_paths, gas_types, concentrations, sensor_types = detect_dataset_type( temp_dir, gas_types=[gas_type1, gas_type2], concentrations=[concentration1, concentration2], sensor_types=[sensor_type1, sensor_type2] ) logger.info(f"检测到数据集类型: {dataset_type}") logger.debug(f"气体类型: {gas_types}") logger.debug(f"浓度: {concentrations}") logger.debug(f"传感器类型: {sensor_types}") # 使用数据加载器加载数据 logger.debug("开始加载数据...") X, y = data_loader.load_multiple_gas_data(detected_paths, gas_types, concentrations, sensor_types) if X is None or len(X) == 0: logger.error("加载数据失败: 没有有效数据") return jsonify({'error': 'No valid data loaded'}), 400 logger.info(f"成功加载数据: {len(X)} 个样本, {X.shape[1]} 个特征") # 保存合并后的数据 df = pd.DataFrame(X) df['label'] = y file_path = os.path.join(app.config['UPLOAD_FOLDER'], 'temp_data.xlsx') logger.debug(f"保存合并数据到: {file_path}") df.to_excel(file_path, index=False) # 获取气体名称映射 gas_names = { 'acetone': '丙酮 (Acetone)', 'toluene': '甲苯 (Toluene)', 'methanol': '甲醇 (Methanol)', 'formaldehyde': '甲醛 (Formaldehyde)', 'ethanol': '乙醇 (Ethanol)', 'toluene+formaldehyde': '甲苯+甲醛 (Toluene+Formaldehyde)', 'methanol+toluene':'甲醇+甲苯 (methanol+toluene)', 'methanol+toluene+formaldehyde': '甲醇+甲苯+甲醛 (Methanol+Toluene+Formaldehyde)' } # 准备返回结果 response = { 'status': 'success', 'sample_count': len(X), 'feature_count': X.shape[1], 'gas_types': [gas_names.get(gt, gt) for gt in gas_types], 'concentrations': concentrations, 'sensor_types': sensor_types, 'dataset_type': dataset_type } logger.info("文件上传处理成功") return jsonify(response) except Exception as e: logger.error(f"文件上传出错: {str(e)}") logger.error(traceback.format_exc()) return jsonify({ 'error': 'Internal server error', 'message': str(e), 'traceback': traceback.format_exc() }), 500 finally: # 清理临时文件 if temp_dir: try: logger.debug(f"清理临时目录: {temp_dir}") shutil.rmtree(temp_dir, ignore_errors=True) except Exception as e: logger.warning(f"清理临时目录时出错: {str(e)}") @app.route('/analyze', methods=['POST']) def analyze_data(): """执行数据分析""" try: logger.info("收到数据分析请求") data = request.get_json() if not data: logger.warning("请求中没有数据") return jsonify({'error': 'No data provided'}), 400 # 从请求中获取参数 params = data.get('params', {}) logger.debug(f"算法参数: {params}") # 设置算法参数 for algo_name, algo_params in params.items(): selector.set_algorithm_params(algo_name, algo_params) # 加载数据 file_path = os.path.join(app.config['UPLOAD_FOLDER'], 'temp_data.xlsx') logger.debug(f"从 {file_path} 加载数据") if not os.path.exists(file_path): logger.error(f"数据文件不存在: {file_path}") return jsonify({'error': 'Data file not found'}), 404 df = pd.read_excel(file_path) if 'label' not in df.columns: logger.error("数据文件中缺少 'label' 列") return jsonify({'error': "Data file missing 'label' column"}), 400 X = df.drop('label', axis=1).values y = df['label'].values if X is None or len(X) == 0: logger.error("没有可用于分析的数据") return jsonify({'error': 'No data available for analysis'}), 400 logger.info(f"开始训练模型, 样本数: {len(X)}") # 训练模型 results = selector.train_models(X, y) logger.info("模型训练完成") # 准备返回结果 response = { 'status': 'success', 'results': {}, 'sample_count': len(X), } for algo_name, result in results.items(): # 生成文本格式的分类报告 report_text = "" if 'y_test' in result and 'y_pred' in result: try: report_text = classification_report(result['y_test'], result['y_pred']) except Exception as e: report_text = f"生成分类报告出错: {str(e)}" elif 'error' in result: report_text = result['error'] response['results'][algo_name] = { 'name': result['name'], 'train_accuracy': result.get('train_accuracy', 0), 'test_accuracy': result.get('test_accuracy', 0), 'classification_report': report_text } return jsonify(response) except Exception as e: logger.error(f"数据分析出错: {str(e)}") logger.error(traceback.format_exc()) return jsonify({ 'error': 'Internal server error', 'message': str(e), 'traceback': traceback.format_exc() }), 500 @app.route('/static/<path:filename>') def serve_static(filename): """提供静态文件""" return send_from_directory('static', filename) # 全局错误处理 @app.errorhandler(404) def not_found_error(error): return jsonify({'error': 'Resource not found'}), 404 @app.errorhandler(500) def internal_error(error): return jsonify({ 'error': 'Internal server error', 'message': str(error), 'traceback': traceback.format_exc() }), 500 if __name__ == '__main__': app.run(debug=True, port=5000) ``` ### 关键修改点总结: #### 前端修改: 1. **API端点更新**: - 上传接口:`fetch('http://localhost:5000/upload')` - 分析接口:`fetch('http://localhost:5000/analyze')` 2. **错误处理增强**: ```javascript if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || '上传失败'); } ``` 3. **数据概览显示**: ```javascript // 使用后端返回的数据 this.gasTypeDisplay = data.gas_types.join(', '); this.concentrationDisplay = data.concentrations.join(', ') + ' ppm'; this.sampleCountDisplay = data.sample_count; ``` 4. **文件上传优化**: - 简化状态消息 - 清空文件输入框 #### 后端修改: 1. **CORS支持**: ```python from flask_cors import CORS CORS(app) ``` 2. **表单数据处理**: ```python gas_type1 = request.form.get('gas_type1', 'acetone') concentration1 = request.form.get('concentration1', '20') ``` 3. **数据集检测参数传递**: ```python dataset_type, detected_paths, gas_types, concentrations, sensor_types = detect_dataset_type( temp_dir, gas_types=[gas_type1, gas_type2], concentrations=[concentration1, concentration2], sensor_types=[sensor_type1, sensor_type2] ) ``` 4. **响应格式优化**: ```python response = { 'status': 'success', 'sample_count': len(X), 'feature_count': X.shape[1], 'gas_types': [gas_names.get(gt, gt) for gt in gas_types], 'concentrations': concentrations, 'sensor_types': sensor_types, 'dataset_type': dataset_type } ``` ### 使用说明: 1. 安装依赖: ```bash pip install flask flask-cors pandas openpyxl ``` 2. 启动后端: ```bash python app.py ``` 3. 启动前端: ```bash npm run dev ``` 4. 确保 `new_algorithm.py` 模块存在并包含 `GasSensorDataAnalyzer`, `AlgorithmSelector`, `detect_dataset_type` 等类和方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值