<c:forEach>标签报isValid()==false错问题

博主遇到了关于JSTL标签的错误,并提及给出的图是正确写法,围绕JSTL标签的使用展开。

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

今天遇到了这样一个错,关于jstl标签的错误,上图是正确写法。


<template> <el-dialog :title="!dataForm.id ? '新增电池信息' : '修改电池信息'" :visible.sync="visible" width="90%" top="5vh" custom-class="battery-form-dialog" > <!-- 使用标签页布局 --> <el-tabs v-model="activeTab" type="card" tab-position="top"> <!-- 电池基本信息 --> <el-tab-pane label="基本信息" name="1"> <div class="form-grid"> <el-form-item label="电池ID" prop="batteryId"> <el-input v-model="dataForm.batteryId" disabled> <i slot="prefix" class="el-icon-files"></i> </el-input> </el-form-item> <el-form-item label="电压(V)" prop="batteryVoltage"> <el-input v-model="dataForm.batteryVoltage" type="number" disabled> <i slot="prefix" class="el-icon-data-line"></i> </el-input> </el-form-item> <el-form-item label="电流(A)" prop="batteryCurrent"> <el-input v-model="dataForm.batteryCurrent" type="number" disabled> <i slot="prefix" class="el-icon-sort"></i> </el-input> </el-form-item> <el-form-item label="SOC(%)" prop="batterySoc"> <el-input v-model="dataForm.batterySoc" type="number" min="0" max="100" disabled> <i slot="prefix" class="el-icon-pie-chart"></i> </el-input> </el-form-item> <el-form-item label="硬件版本" prop="batteryHardVersion"> <el-input v-model="dataForm.batteryHardVersion" disabled> <i slot="prefix" class="el-icon-cpu"></i> </el-input> </el-form-item> <el-form-item label="软件版本" prop="batterySoftVersion"> <el-input v-model="dataForm.batterySoftVersion" disabled> <i slot="prefix" class="el-icon-setting"></i> </el-input> </el-form-item> </div> </el-tab-pane> <!-- 电池状态信息 --> <el-tab-pane label="状态信息" name="2"> <div class="form-grid"> <el-form-item label="工作模式" prop="batteryWorkMode"> <el-select v-model="dataForm.batteryWorkMode" disabled> <el-option label="放电模式 (0x01)" value="1"></el-option> <el-option label="充电模式 (0x10)" value="16"></el-option> <el-option label="保护模式 (0x21)" value="33"></el-option> <el-option label="待机无输出模式 (0x30)" value="48"></el-option> <el-option label="待机预放电模式 (0x31)" value="49"></el-option> <el-option label="故障需返厂 (0xFF)" value="255"></el-option> </el-select> </el-form-item> <el-form-item label="电池类型" prop="batteryKind"> <el-select v-model="dataForm.batteryKind"> <el-option label="运营" value="0"></el-option> <el-option label="售后" value="1"></el-option> <el-option label="内测" value="2"></el-option> <el-option label="废" value="3"></el-option> </el-select> </el-form-item> <el-form-item label="保护状态码" prop="batteryProtectCode"> <el-tooltip effect="dark" placement="top"> <div slot="content"> bit0: 放电低温 | bit1: 放电高温 | bit2: 充电高温<br> bit3: 充电低温 | bit4: 放电过流 | bit5: 放电欠压<br> bit6: 充电过流 | bit7: 充电过压 | bit8: 短路<br> bit9: 温差过大 | bit10: 压差过大 | bit11: 智能充电通信超时<br> bit12: IC保护 | bit13: 预放电失败 | bit14: BMS与Tracker通讯超时<br> bit15: Gsensor故障 | bit16~31: 保留 </div> <el-input v-model="dataForm.batteryProtectCode" disabled> <i slot="prefix" class="el-icon-warning"></i> </el-input> </el-tooltip> </el-form-item> <el-form-item label="误状态码" prop="batteryErrorCode"> <el-tooltip effect="dark" placement="top"> <div slot="content"> 低8位故障状态码:<br> bit0: 保留 | bit1: 采样线故障 | bit2: 温度传感器损坏<br> bit3: IC损坏 | bit4: 充电MOS损坏 | bit5: 放电MOS损坏<br> bit6: 失衡 | bit7: 失效<br> 高8位工作状态:<br> bit8: G sensor损坏 | bit9: BMS与Tracker通讯故障 </div> <el-input v-model="dataForm.batteryErrorCode" disabled> <i slot="prefix" class="el-icon-warning-outline"></i> </el-input> </el-tooltip> </el-form-item> </div> </el-tab-pane> <!-- 温度电压信息 --> <el-tab-pane label="温度电压" name="3"> <div class="form-grid"> <el-form-item label="最高温度(℃)" prop="batteryTemperatureMax"> <el-input v-model="dataForm.batteryTemperatureMax" type="number" disabled> <i slot="prefix" class="el-icon-sunny"></i> </el-input> </el-form-item> <el-form-item label="最低温度(℃)" prop="batteryTemperatureMin"> <el-input v-model="dataForm.batteryTemperatureMin" type="number" disabled> <i slot="prefix" class="el-icon-cold-drink"></i> </el-input> </el-form-item> <el-form-item label="最高电压(V)" prop="batteryVoltageMax"> <el-input v-model="dataForm.batteryVoltageMax" type="number" disabled> <i slot="prefix" class="el-icon-top"></i> </el-input> </el-form-item> <el-form-item label="最低电压(V)" prop="batteryVoltageMin"> <el-input v-model="dataForm.batteryVoltageMin" type="number" disabled> <i slot="prefix" class="el-icon-bottom"></i> </el-input> </el-form-item> </div> </el-tab-pane> <!-- 设备信息 --> <el-tab-pane label="设备信息" name="4"> <div class="form-grid"> <el-form-item label="设备型号" prop="model"> <el-input v-model="dataForm.model" disabled> <i slot="prefix" class="el-icon-mobile"></i> </el-input> </el-form-item> <el-form-item label="制造商" prop="manufacture"> <el-input v-model="dataForm.manufacture" disabled> <i slot="prefix" class="el-icon-office-building"></i> </el-input> </el-form-item> <el-form-item label="IMEI" prop="imei"> <el-input v-model="dataForm.imei" disabled> <i slot="prefix" class="el-icon-phone"></i> </el-input> </el-form-item> <el-form-item label="IMSI" prop="imsi"> <el-input v-model="dataForm.imsi" disabled> <i slot="prefix" class="el-icon-sim-card"></i> </el-input> </el-form-item> <el-form-item label="ICCID" prop="iccid"> <el-input v-model="dataForm.iccid" disabled> <i slot="prefix" class="el-icon-credit-card"></i> </el-input> </el-form-item> </div> </el-tab-pane> <!-- GPS定位信息 --> <el-tab-pane label="GPS定位" name="5"> <div class="form-grid"> <el-form-item label="经度" prop="longitude"> <el-input v-model="dataForm.longitude" type="number" disabled> <i slot="prefix" class="el-icon-place"></i> </el-input> </el-form-item> <el-form-item label="经度方向" prop="longitudeDirection"> <el-select v-model="dataForm.longitudeDirection" disabled> <el-option label="东经" value="E"></el-option> <el-option label="西经" value="W"></el-option> </el-select> </el-form-item> <el-form-item label="纬度" prop="latitude"> <el-input v-model="dataForm.latitude" type="number" disabled> <i slot="prefix" class="el-icon-place"></i> </el-input> </el-form-item> <el-form-item label="纬度方向" prop="latitudeDirection"> <el-select v-model="dataForm.latitudeDirection" disabled> <el-option label="北纬" value="N"></el-option> <el-option label="南纬" value="S"></el-option> </el-select> </el-form-item> </div> </el-tab-pane> <!-- 业务信息 --> <el-tab-pane label="业务信息" name="6"> <div class="form-grid"> <el-form-item label="业务模式" prop="businessmode"> <el-select v-model="dataForm.businessmode" disabled> <el-option v-for="mode in businessModes" :key="mode.value" :label="mode.label" :value="mode.value"></el-option> </el-select> </el-form-item> <el-form-item label="处理状态" prop="handled"> <el-select v-model="dataForm.handled" disabled> <el-option label="已处理" :value="1"></el-option> <el-option label="待处理" :value="0"></el-option> </el-select> </el-form-item> <el-form-item label="执行方式" prop="todoNow"> <el-select v-model="dataForm.todoNow" disabled> <el-option label="立即执行" :value="1"></el-option> <el-option label="等待执行" :value="0"></el-option> </el-select> </el-form-item> <el-form-item label="需要回复" prop="needack"> <el-select v-model="dataForm.needack" disabled> <el-option label="是" :value="1"></el-option> <el-option label="否" :value="0"></el-option> </el-select> </el-form-item> <el-form-item label="消息类型" prop="flag"> <el-select v-model="dataForm.flag" disabled> <el-option label="设备主动请求或上" :value="1"></el-option> <el-option label="平台主动推送或下指令" :value="2"></el-option> <el-option label="平台指令反馈的结果" :value="3"></el-option> </el-select> </el-form-item> </div> </el-tab-pane> <!-- 其他信息 --> <el-tab-pane label="其他信息" name="7"> <div class="form-grid"> <el-form-item label="原始数据" prop="payload"> <el-input v-model="dataForm.payload" type="textarea" :rows="4" disabled></el-input> </el-form-item> </div> </el-tab-pane> </el-tabs> <span slot="footer" class="dialog-footer"> <el-button @click="visible = false">取消</el-button> <el-button type="primary" @click="dataFormSubmit()">确定</el-button> </span> </el-dialog> </template> <script> export default { data() { return { visible: false, activeTab: '1', dataForm: { id: 0, batteryId: '', batteryVoltage: '', batteryCurrent: '', batterySoc: '', batteryHardVersion: '', batterySoftVersion: '', batteryWorkMode: '', batteryKind: "0", batteryProtectCode: '', batteryErrorCode: '', batteryTemperatureMax: '', batteryTemperatureMin: '', batteryVoltageMax: '', batteryVoltageMin: '', mosStatus: '', mosTemp: '', batteryCycleTimes: '', steadyStatus: '', cellVoltage: '', model: '', manufacture: '', imei: '', imsi: '', iccid: '', trackerHardwareVersion: '', trackerSoftwareVersion: '', csq: '', networkType: '', locationMode: '', longitude: '', longitudeDirection: '', latitude: '', latitudeDirection: '', gpsSpeed: '', gpsSignal: '', satelliteNum: '', accuracy: '', flag: '', clientId: '', topic: '', productKey: '', handled: '', todoNow: '', needack: '', businessmode: '', uploadTime: '', createTime: '', updateTime: '', payload: '' }, businessModes: [ { value: 1, label: "关机模式" }, { value: 2, label: "运营模式" }, { value: 3, label: "运输模式" }, { value: 4, label: "通讯充电模式" }, { value: 5, label: "盲充模式" }, { value: 6, label: "存储模式" }, { value: 7, label: "搜寻模式" }, { value: 8, label: "返厂模式" } ], /* dataRule: { batteryId: [ { required: true, message: '不能为空', trigger: 'blur' } ], batteryVoltage: [ { required: true, message: '不能为空', trigger: 'blur' } ], batteryCurrent: [ { required: true, message: '不能为空', trigger: 'blur' } ], batterySoc: [ { required: true, message: '不能为空', trigger: 'blur' } ], batteryHardVersion: [ { required: true, message: '不能为空', trigger: 'blur' } ], batterySoftVersion: [ { required: true, message: '不能为空', trigger: 'blur' } ], batteryWorkMode: [ { required: true, message: '0x01(1):放电模式 0x10(16):充电模式 0x21(33):保护模式 0x30(48):待机无输出模式 0x31(49):待机预放电模式 0xFF(255):故障需返厂不能为空', trigger: 'blur' } ], batteryProtectCode: [ { required: true, message: '保护状态 bit16~bit 31 保留 bit15:Gsensor 故障 bit14:BMS 与 Tracker通讯超时 bit13: 预放电失败 bit12: IC保护 bit11: 智能充电通信超时 bit10:压差过大 bit9:温差过大 bit8:短路 bit7:充电过压 bit6:充电过流 bit5:放电欠压 bit4:放电过流 bit3:充电低温 bit2:充电高温 bit1:放电高温 bit0:放电低温不能为空', trigger: 'blur' } ], batteryErrorCode: [ { required: true, message: '转成2进制, 总共为16位,高8位为电池包工作状态,低8位为故障状态码 bit9BMS 与 Tracker通讯故障,bit8BMS 板 G sensor损坏,(8/9无效)bit7:失效,bit6:失衡,bit5:放电 MOS 损坏,bit4:充电 MOS 损坏,bit3IC 损坏,bit2:温度传感器损坏,bit1:采样线断线,虚焊等故障 保留,置 0,bit0:保留不能为空', trigger: 'blur' } ], batteryTemperatureMax: [ { required: true, message: '不能为空', trigger: 'blur' } ], batteryTemperatureMin: [ { required: true, message: '不能为空', trigger: 'blur' } ], batteryVoltageMax: [ { required: true, message: '不能为空', trigger: 'blur' } ], batteryVoltageMin: [ { required: true, message: '不能为空', trigger: 'blur' } ], mosStatus: [ { required: true, message: '不能为空', trigger: 'blur' } ], mosTemp: [ { required: true, message: '不能为空', trigger: 'blur' } ], batteryCycleTimes: [ { required: true, message: '不能为空', trigger: 'blur' } ], steadyStatus: [ { required: true, message: '不能为空', trigger: 'blur' } ], cellVoltage: [ { required: true, message: '不能为空', trigger: 'blur' } ], model: [ { required: true, message: '不能为空', trigger: 'blur' } ], manufacture: [ { required: true, message: '不能为空', trigger: 'blur' } ], imei: [ { required: true, message: '不能为空', trigger: 'blur' } ], imsi: [ { required: true, message: '不能为空', trigger: 'blur' } ], iccid: [ { required: true, message: '不能为空', trigger: 'blur' } ], trackerHardwareVersion: [ { required: true, message: '不能为空', trigger: 'blur' } ], trackerSoftwareVersion: [ { required: true, message: '不能为空', trigger: 'blur' } ], csq: [ { required: true, message: '不能为空', trigger: 'blur' } ], networkType: [ { required: true, message: '不能为空', trigger: 'blur' } ], locationMode: [ { required: true, message: '不能为空', trigger: 'blur' } ], longitude: [ { required: true, message: '不能为空', trigger: 'blur' } ], longitudeDirection: [ { required: true, message: '不能为空', trigger: 'blur' } ], latitude: [ { required: true, message: '不能为空', trigger: 'blur' } ], latitudeDirection: [ { required: true, message: '不能为空', trigger: 'blur' } ], gpsSpeed: [ { required: true, message: '不能为空', trigger: 'blur' } ], gpsSignal: [ { required: true, message: '不能为空', trigger: 'blur' } ], satelliteNum: [ { required: true, message: '不能为空', trigger: 'blur' } ], accuracy: [ { required: true, message: '不能为空', trigger: 'blur' } ], flag: [ { required: true, message: '1:设备主动请求或上 2:平台主动推送或下指令 3:平台指令反馈的结果不能为空', trigger: 'blur' } ], clientId: [ { required: true, message: 'MQTT发布消息所需的clientId不能为空', trigger: 'blur' } ], topic: [ { required: true, message: 'MQTT主题不能为空', trigger: 'blur' } ], productKey: [ { required: true, message: '主题名称中需要的productKey不能为空', trigger: 'blur' } ], handled: [ { required: true, message: '配置是否处理 1:已处理 0:待处理不能为空', trigger: 'blur' } ], todoNow: [ { required: true, message: '是否立即执行 0:等待执行 1:立即执行不能为空', trigger: 'blur' } ], needack: [ { required: true, message: '是否回复不能为空', trigger: 'blur' } ], businessmode: [ { required: true, message: '业务模式 1:关机模式 2:运营模式 3:运输模式 4:通讯充电模式 5:盲充模式 6:存储模式 7:搜寻模式 8:返厂模式不能为空', trigger: 'blur' } ], uploadTime: [ { required: true, message: '不能为空', trigger: 'blur' } ], createTime: [ { required: true, message: '不能为空', trigger: 'blur' } ], updateTime: [ { required: true, message: '更新时间不能为空', trigger: 'blur' } ], payload: [ { required: true, message: '原始数据不能为空', trigger: 'blur' } ] } */ } }, computed: { // 处理状态标签 handledLabel() { return this.dataForm.handled === 1 ? "已处理" : this.dataForm.handled === 0 ? "待处理" : ""; }, // 执行方式标签 todoNowLabel() { return this.dataForm.todoNow === 1 ? "立即执行" : this.dataForm.todoNow === 0 ? "等待执行" : ""; }, // 需要回复标签 needackLabel() { return this.dataForm.needack === 1 ? "是" : this.dataForm.needack === 0 ? "否" : ""; }, // 业务模式标签 businessModeLabel() { const mode = this.businessModes.find(m => m.value == this.dataForm.businessmode); return mode ? mode.label : ""; }, // 消息类型标签 flagLabel() { switch (this.dataForm.flag) { case 1: return "设备主动请求或上"; case 2: return "平台主动推送或下指令"; case 3: return "平台指令反馈的结果"; default: return ""; } } }, methods: { init (id) { this.dataForm.id = id || 0 this.visible = true this.$nextTick(() => { this.$refs['dataForm'].resetFields() if (this.dataForm.id) { this.$http({ url: this.$http.adornUrl(`/maya/mybatteryinfo/info/${this.dataForm.id}`), method: 'get', params: this.$http.adornParams() }).then(({data}) => { if (data && data.code === 0) { this.dataForm.batteryId = data.myBatteryInfo.batteryId this.dataForm.batteryVoltage = data.myBatteryInfo.batteryVoltage this.dataForm.batteryCurrent = data.myBatteryInfo.batteryCurrent this.dataForm.batterySoc = data.myBatteryInfo.batterySoc this.dataForm.batteryHardVersion = data.myBatteryInfo.batteryHardVersion this.dataForm.batterySoftVersion = data.myBatteryInfo.batterySoftVersion this.dataForm.batteryWorkMode = data.myBatteryInfo.batteryWorkMode this.dataForm.batteryKind = String(data.myBatteryInfo.batteryKind); this.dataForm.batteryProtectCode = data.myBatteryInfo.batteryProtectCode this.dataForm.batteryErrorCode = data.myBatteryInfo.batteryErrorCode this.dataForm.batteryTemperatureMax = data.myBatteryInfo.batteryTemperatureMax this.dataForm.batteryTemperatureMin = data.myBatteryInfo.batteryTemperatureMin this.dataForm.batteryVoltageMax = data.myBatteryInfo.batteryVoltageMax this.dataForm.batteryVoltageMin = data.myBatteryInfo.batteryVoltageMin this.dataForm.mosStatus = data.myBatteryInfo.mosStatus this.dataForm.mosTemp = data.myBatteryInfo.mosTemp this.dataForm.batteryCycleTimes = data.myBatteryInfo.batteryCycleTimes this.dataForm.steadyStatus = data.myBatteryInfo.steadyStatus this.dataForm.cellVoltage = data.myBatteryInfo.cellVoltage this.dataForm.model = data.myBatteryInfo.model this.dataForm.manufacture = data.myBatteryInfo.manufacture this.dataForm.imei = data.myBatteryInfo.imei this.dataForm.imsi = data.myBatteryInfo.imsi this.dataForm.iccid = data.myBatteryInfo.iccid this.dataForm.trackerHardwareVersion = data.myBatteryInfo.trackerHardwareVersion this.dataForm.trackerSoftwareVersion = data.myBatteryInfo.trackerSoftwareVersion this.dataForm.csq = data.myBatteryInfo.csq this.dataForm.networkType = data.myBatteryInfo.networkType this.dataForm.locationMode = data.myBatteryInfo.locationMode this.dataForm.longitude = data.myBatteryInfo.longitude this.dataForm.longitudeDirection = data.myBatteryInfo.longitudeDirection this.dataForm.latitude = data.myBatteryInfo.latitude this.dataForm.latitudeDirection = data.myBatteryInfo.latitudeDirection this.dataForm.gpsSpeed = data.myBatteryInfo.gpsSpeed this.dataForm.gpsSignal = data.myBatteryInfo.gpsSignal this.dataForm.satelliteNum = data.myBatteryInfo.satelliteNum this.dataForm.accuracy = data.myBatteryInfo.accuracy this.dataForm.flag = data.myBatteryInfo.flag this.dataForm.clientId = data.myBatteryInfo.clientId this.dataForm.topic = data.myBatteryInfo.topic this.dataForm.productKey = data.myBatteryInfo.productKey this.dataForm.handled = data.myBatteryInfo.handled this.dataForm.todoNow = data.myBatteryInfo.todoNow this.dataForm.needack = data.myBatteryInfo.needack this.dataForm.businessmode = data.myBatteryInfo.businessmode this.dataForm.uploadTime = data.myBatteryInfo.uploadTime this.dataForm.createTime = data.myBatteryInfo.createTime this.dataForm.updateTime = data.myBatteryInfo.updateTime this.dataForm.payload = data.myBatteryInfo.payload } }) } }) }, // 表单提交 dataFormSubmit () { this.$refs['dataForm'].validate((valid) => { if (valid) { this.$http({ url: this.$http.adornUrl(`/maya/mybatteryinfo/${!this.dataForm.id ? 'save' : 'update'}`), method: 'post', data: this.$http.adornData({ 'id': this.dataForm.id || undefined, 'batteryId': this.dataForm.batteryId, 'batteryVoltage': this.dataForm.batteryVoltage, 'batteryCurrent': this.dataForm.batteryCurrent, 'batterySoc': this.dataForm.batterySoc, 'batteryHardVersion': this.dataForm.batteryHardVersion, 'batterySoftVersion': this.dataForm.batterySoftVersion, 'batteryWorkMode': this.dataForm.batteryWorkMode, 'batteryKind': Number(this.dataForm.batteryKind), 'batteryProtectCode': this.dataForm.batteryProtectCode, 'batteryErrorCode': this.dataForm.batteryErrorCode, 'batteryTemperatureMax': this.dataForm.batteryTemperatureMax, 'batteryTemperatureMin': this.dataForm.batteryTemperatureMin, 'batteryVoltageMax': this.dataForm.batteryVoltageMax, 'batteryVoltageMin': this.dataForm.batteryVoltageMin, 'mosStatus': this.dataForm.mosStatus, 'mosTemp': this.dataForm.mosTemp, 'batteryCycleTimes': this.dataForm.batteryCycleTimes, 'steadyStatus': this.dataForm.steadyStatus, 'cellVoltage': this.dataForm.cellVoltage, 'model': this.dataForm.model, 'manufacture': this.dataForm.manufacture, 'imei': this.dataForm.imei, 'imsi': this.dataForm.imsi, 'iccid': this.dataForm.iccid, 'trackerHardwareVersion': this.dataForm.trackerHardwareVersion, 'trackerSoftwareVersion': this.dataForm.trackerSoftwareVersion, 'csq': this.dataForm.csq, 'networkType': this.dataForm.networkType, 'locationMode': this.dataForm.locationMode, 'longitude': this.dataForm.longitude, 'longitudeDirection': this.dataForm.longitudeDirection, 'latitude': this.dataForm.latitude, 'latitudeDirection': this.dataForm.latitudeDirection, 'gpsSpeed': this.dataForm.gpsSpeed, 'gpsSignal': this.dataForm.gpsSignal, 'satelliteNum': this.dataForm.satelliteNum, 'accuracy': this.dataForm.accuracy, 'flag': this.dataForm.flag, 'clientId': this.dataForm.clientId, 'topic': this.dataForm.topic, 'productKey': this.dataForm.productKey, 'handled': this.dataForm.handled, 'todoNow': this.dataForm.todoNow, 'needack': this.dataForm.needack, 'businessmode': this.dataForm.businessmode, 'uploadTime': this.dataForm.uploadTime, 'createTime': this.dataForm.createTime, 'updateTime': this.dataForm.updateTime, 'payload': this.dataForm.payload }) }).then(({data}) => { if (data && data.code === 0) { this.$message({ message: '操作成功', type: 'success', duration: 1500, onClose: () => { this.visible = false this.$emit('refreshDataList') } }) } else { this.$message.error(data.msg) } }) } }) } } } </script> <style> .battery-form-dialog { font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; color: #1a1a1a; } /* 标签页样式优化 */ .battery-form-dialog .el-tabs__item { font-weight: 600 !important; font-size: 15px !important; padding: 0 20px !important; height: 40px; line-height: 40px; color: #555; } .battery-form-dialog .el-tabs__item.is-active { color: #1a73e8 !important; border-bottom: 3px solid #1a73e8 !important; } .battery-form-dialog .el-tabs__header { margin-bottom: 20px; } /* 网格布局 - 横向排列 */ .form-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px 30px; padding: 15px; } /* 表单标签优化 */ .battery-form-dialog .el-form-item__label { display: block; font-weight: 600 !important; color: #1a1a1a !important; font-size: 14px !important; padding-bottom: 6px !important; margin-bottom: 0 !important; letter-spacing: 0.5px; } /* 输入框优化 */ .battery-form-dialog .el-input__inner, .battery-form-dialog .el-textarea__inner, .battery-form-dialog .el-select .el-input__inner { font-size: 15px !important; color: #222 !important; font-weight: 500 !important; border: 1px solid #dcdfe6 !important; background-color: #f8fafc !important; height: 40px; line-height: 40px; border-radius: 4px; } /* 文本域样式 */ .battery-form-dialog .el-textarea__inner { min-height: 100px; line-height: 1.5; padding: 10px 15px; font-family: monospace; } /* 禁用状态优化 */ .battery-form-dialog .el-input.is-disabled .el-input__inner, .battery-form-dialog .el-textarea.is-disabled .el-textarea__inner, .battery-form-dialog .el-select.is-disabled .el-input__inner { color: #444 !important; background-color: #f0f4f8 !important; border-color: #e4e7ed !important; opacity: 1; } /* 图标颜色优化 */ .battery-form-dialog .el-input__prefix { color: #1a73e8 !important; font-size: 16px; display: flex; align-items: center; padding-left: 8px; } /* 按钮优化 */ .battery-form-dialog .el-button { font-weight: 600; padding: 10px 20px; border-radius: 4px; font-size: 14px; min-width: 100px; transition: all 0.3s; } .battery-form-dialog .el-button--default { border-color: #dcdfe6; } .battery-form-dialog .el-button--primary { background-color: #1a73e8; border-color: #1a73e8; } .battery-form-dialog .el-button--primary:hover { background-color: #0d62c9; border-color: #0d62c9; } /* 标题优化 */ .battery-form-dialog .el-dialog__title { font-size: 18px !important; font-weight: 700 !important; color: #1a1a1a !important; letter-spacing: 0.5px; } /* 提示工具样式 */ .battery-form-dialog .el-tooltip__popper { max-width: 400px; font-size: 13px; line-height: 1.6; background-color: #2c3e50; color: #ecf0f1; } .battery-form-dialog .el-tooltip__popper[x-placement^="top"] .popper__arrow { border-top-color: #2c3e50; } /* 误提示样式 */ .battery-form-dialog .el-form-item__error { color: #f56c6c; font-size: 12px; padding-top: 4px; } /* 响应式调整 */ @media (max-width: 1200px) { .form-grid { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); } } @media (max-width: 992px) { .form-grid { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); } } @media (max-width: 768px) { .battery-form-dialog { width: 95% !important; } .form-grid { grid-template-columns: 1fr; } .battery-form-dialog .el-tabs__item { font-size: 13px !important; padding: 0 10px !important; } } </style>修复该页面,并给我完整胡代码
最新发布
07-22
<template> <el-form ref="ruleFormRef" style="margin-top: 20px;max-width: 600px" :model="ruleForm" :rules="rules" label-width="auto" :disabled="pageMode === 'view'"> <el-form-item label="产品名称:" prop="name"> <el-input v-model="ruleForm.name" /> </el-form-item> <el-form-item label="产品描述:" prop="description"> <el-input v-model="ruleForm.description" type="textarea"/> </el-form-item> <el-form-item label="产品分类:" prop="category" style="width: 50%;"> <el-select v-model="ruleForm.category" placeholder="请选择分类"> <el-option v-for="item in categorys" :label="item.name" :value="item.id" :key="item.id" /> </el-select> </el-form-item> <el-form-item label="产品单价:" prop="price"> <el-input v-model.number="ruleForm.price" type="number" style="width: 240px" /> </el-form-item> <el-form-item label="产品库存:" prop="stock"> <el-input v-model.number="ruleForm.stock" type="number" style="width: 240px" /> </el-form-item> <el-form-item label="生产厂家:" prop="manufacturer"> <el-input v-model="ruleForm.manufacturer" /> </el-form-item> <el-form-item label="产地:" required> <el-col :span="12"> <el-form-item prop="originProvince"> <el-select v-model="ruleForm.originProvince" placeholder="省" style="flex: 1; margin-right: 10px"> <el-option v-for="item in originProvinces" :key="item.id" :label="item.name" :value="item.id" /> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item prop="originCity"> <el-select v-model="ruleForm.originCity" placeholder="市" style="flex: 1 " :disabled="!ruleForm.originProvince"> <el-option v-for="item in originCitys" :key="item.id" :label="item.name" :value="item.id" /> </el-select> </el-form-item> </el-col> </el-form-item> <el-form-item label="发货地:" required> <el-col :span="12"> <el-form-item prop="shippingProvince"> <el-select v-model="ruleForm.shippingProvince" placeholder="省" style="flex: 1; margin-right: 10px"> <el-option v-for="item in originProvinces" :key="item.id" :label="item.name" :value="item.id" /> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item prop="shippingCity"> <el-select v-model="ruleForm.shippingCity" placeholder="市" style="flex: 1" :disabled="!ruleForm.shippingProvince"> <el-option v-for="item in shippingCities" :key="item.id" :label="item.name" :value="item.id" /> </el-select> </el-form-item> </el-col> </el-form-item> <el-form-item label="生产日期" required> <el-col :span="11"> <el-form-item prop="createAt"> <el-date-picker v-model="ruleForm.createAt" type="date" aria-label="Pick a date" placeholder="Pick a date" style="width: 100%" /> </el-form-item> </el-col> </el-form-item> <el-form-item> <!-- 登录按钮:仅在默认模式下显示 --> <el-button v-if="pageMode === 'default'" style="margin-left: 18px;" type="primary" @click="submitForm(ruleFormRef, ruleForm)"> 登录 </el-button> <!-- 更新按钮:仅在编辑模式下显示 --> <el-button v-if="pageMode === 'edit'" style="margin-left: 30px;" type="primary" @click="updateForm(ruleFormRef)"> 更新 </el-button> <!-- 重置按钮:在编辑或默认模式下都显示 --> <el-button v-if="pageMode === 'edit' || pageMode === 'default'" style="margin-left: 30px;" type="primary" @click="resetForm(ruleFormRef)"> 重置 </el-button> </el-form-item> </el-form> </template> <script setup> import { ref, reactive, onMounted } from 'vue'; import productApi from '@/api/products' import placeApi from '@/api/place' import { useRouter } from 'vue-router' import { ElLoading, ElMessage } from 'element-plus' const pageMode = ref('') // 可选值: 'view', 'edit', 'default' const router = useRouter(); const ruleFormRef = ref() const categorys = ref([])// 产品分类 const originProvinces = ref([]) // 省份数据 const originCitys = ref([]) // 城市数据 const shippingCities = ref([]) // 发货地城市数据 const ruleForm = reactive({ name: '', description: '', category: '', price: '', stock: '', manufacturer: '', originProvince: '', originCity: '', shippingProvince: '', shippingCity: '', createAt: '', user: 'aaa', delivery: false, type: [], }) const rules = reactive({ name: [ { required: true, message: '请输入产品名称', trigger: 'blur' }, { max: 10, message: '长度超过10位', trigger: 'blur' }, ], description: [ { required: true, message: '请输入产品描述', trigger: 'blur' }, { max: 200, message: '长度超过200位', trigger: 'blur' }, ], category: [ { required: true, message: '请选择产品分类', trigger: 'change', }, ], price: [ { required: true, message: '请输入产品单价', trigger: ['blur', 'change'] }, { validator: (rule, value, callback) => { if (value === null || value === '') { callback(new Error('请输入产品单价')) } else if (value <= 0) { callback(new Error('单价必须大于 0')) } else if (value > 10000) { callback(new Error('单价不能大于 10000')) } else { callback() } }, trigger: ['blur', 'change'] } ], stock: [ { required: true, message: '请输入产品库存', trigger: ['blur', 'change'] }, { validator: (rule, value, callback) => { if (value === null || value === '') { callback(new Error('请输入商品单价')) } else if (value <= 0) { callback(new Error('单价必须大于 0')) } else if (value > 10000) { callback(new Error('单价不能大于 10000')) } else { callback() } }, trigger: ['blur', 'change'] } ], manufacturer: [ { required: true, message: '请输入生产厂家', trigger: 'blur' }, ], originProvince: { validator: (rule, value, callback) => { if (!value) { callback(new Error('请选择产地省份')) } else { callback() } }, trigger: 'change' }, originCity:{ validator: (rule, value, callback) => { if (!value) { callback(new Error('请选择产地城市')) } else { callback() } }, trigger: 'change' }, shippingProvince: { validator: (rule, value, callback) => { if (!value) { callback(new Error('请选择发货省份')) } else { callback() } }, trigger: 'change' }, shippingCity:{ validator: (rule, value, callback) => { if (!value) { callback(new Error('请选择发货城市')) } else { callback() } }, trigger: 'change' }, createAt: [ { type: 'date', required: true, message: '请输入日期', trigger: 'change', }, ], }) // 重置表单操作 const resetForm = (formEl) => { if (!formEl) return formEl.resetFields() } //产品分类 function getCategory() { productApi.getCategory().then(res => { categorys.value = res.data; }) } // 获取商品详情 function getProductDetail(productId) { productApi.getById(productId).then(res => { const data = res.data // 先赋值整个对象 Object.assign(ruleForm, data) }).catch(error => { console.error('获取商品详情失败:', error); }); } // 获取省份 function getProvinces() { placeApi.getProvince().then(res => { originProvinces.value = res.data }).catch(error => { console.error('获取省份失败:', error) }) } // 监听产地省份变化,加载城市 watch( () => ruleForm.originProvince, (newVal) => { if (newVal) { placeApi.getCity(newVal).then(res => { originCitys.value = res.data }).catch(() => { originCitys.value = [] }) } else { // 如果省份为空,则清空城市列表和当前选中的城市字段 originCitys.value = [] ruleForm.originCity = '' } } ) // 监听发货地省份变化,加载城市 watch( () => ruleForm.shippingProvince, (newVal) => { if (newVal) { placeApi.getCity(newVal).then(res => { shippingCities.value = res.data }).catch(() => { shippingCities.value = [] }) } else { // 如果省份为空,则清空城市列表和当前选中的城市字段 shippingCities.value = [] ruleForm.shippingCity = '' } } ) // 更新操作 const updateForm = async (formEl) => { if (!formEl) return const loading = ElLoading.service({ fullscreen: true }) await formEl.validate((valid, fields) => { if (valid) { console.log('开始提交...') // 调用 API 更新商品信息 productApi.update(ruleForm).then(res => { ElMessage.success('更新成功') router.push('/home/test1') // 可选跳转回列表页 }).catch(error => { console.error('更新失败:', error) ElMessage.error('更新失败,请重试') }).finally(() => { loading.close() }) } else { console.log('表单验证失败:', fields) ElMessage.error('请检查输入内容') loading.close() return false } }) } // 登录操作 const submitForm = async (formEl, ruleForm) => { if (!formEl) return await formEl.validate((valid, fields) => { if (valid) { console.log('开始提交商品...') const loading = ElLoading.service({ fullscreen: true }) productApi.addProduct(ruleForm).then(res => { ElMessage.success('登录成功') router.push('/home/test1') }).catch(error => { console.error('登录失败:', error) ElMessage.error('登录失败,请重试') }).finally(() => { loading.close() }) } else { console.log('表单验证失败:', fields) ElMessage.error('请检查输入内容') loading.close() return false } }) } onMounted(() => { getCategory() getProvinces() // 获取当前路由信息 const route = router.currentRoute.value // 判断页面是查看、编辑还是新增模式 const type = route.query.type const productId = route.params.id || route.query.id if (type === 'view') { pageMode.value = 'view' // 查看模式:不显示按钮 } else if (type === 'edit') { pageMode.value = 'edit' // 编辑模式:显示“更新”、“重置” } else { pageMode.value = 'default' // 默认模式:显示“登录”、“重置” } // 如果存在商品 ID,则调用接口获取商品详情并填充表单 if (productId) { getProductDetail(productId) } }) </script> <style> .location-selectors { display: flex; justify-content: space-between; width: 100%; } </style>  现在我想不用element组件写,我想自己写子组件实现表单的同样的样式和功能
07-16
#include "o3_JTR.h" #include "DatabaseManager.h" TemperatureGauge::TemperatureGauge(QWidget *parent) :QWidget(parent) { QVBoxLayout* layout = new QVBoxLayout(this); layout->setContentsMargins(2,2,2,2); layout->setSpacing(2); gauge = new QProgressBar(); tempLabel = new QLabel(this); gauge->setRange(0,300); gauge->setValue(0); gauge->setOrientation(Qt::Vertical); gauge->setTextVisible(false); gauge->setStyleSheet("QProgressBar{border:2px solid #888;border-radius:5px;background:white;}" "QProgressBar::chunk{background:qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #4CAF50, stop:0.5 #FFC107, stop:1 #F44336);border-radius:3px;}"); tempLabel->setAlignment(Qt::AlignCenter); tempLabel->setStyleSheet("font-size:12px;"); layout->addWidget(gauge,1); layout->addWidget(tempLabel); } void TemperatureGauge::setTemperatureDirect(float temp) { gauge->setValue(temp); tempLabel->setText(QString("%1°C").arg(temp)); gauge->setStyleSheet("QProgressBar::chunk{background:#c9ffa7;border-radius:3px;}"); } void TemperatureGauge::setTemperature(float temp) { gauge->setValue(temp); tempLabel->setText(QString("%1°C").arg(temp)); gauge->setStyleSheet("QProgressBar::chunk{background:#c9ffa7;border-radius:3px;}"); } JTR::JTR(QWidget *parent) :QWidget(parent),dbManager(DatabaseManager::instance()) { setUpUI(); // 立即创建条形图 createBarChart(); //定时刷新数据 refreshTimer = new QTimer(this); connect(refreshTimer, &QTimer::timeout, this, &JTR::refreshData); refreshTimer->start(2000); //每2秒刷新一次 // //延迟2秒后开启动画 // QTimer::singleShot(2000, this, &JTR::startAnimation); } // void JTR::startAnimation() // { // if(animation) // { // movingRect->move(0, 10); //重置位置 // animation->start(); //开启动画 // } // } //显示温度,进去板子数量,出来板子数量 void JTR::setUpUI() { QHBoxLayout* mainLayout = new QHBoxLayout(this); // 改为水平布局 mainLayout->setContentsMargins(10, 10, 10, 10); mainLayout->setSpacing(15); //左侧区域 - 温度显示 QWidget* tempWidget = new QWidget(); createTemperatureDisplay(tempWidget); //修改为在tempWidget上创建温度显示 //右侧区域 - 日志显示 QWidget* logWidget = new QWidget(); createLogDisplay(logWidget); //设置左右比例约为3:2 mainLayout->addWidget(tempWidget, 3); mainLayout->addWidget(logWidget, 2); this->setLayout(mainLayout); //初始化出板数定时器 (5s刷新一次) boardCountTimer = new QTimer(this); connect(boardCountTimer, &QTimer::timeout, this, &JTR::refreshBoardCount); boardCountTimer->start(5000); //5s QTimer::singleShot(0, this, &JTR::refreshBoardCount); //立即刷新一次 } void JTR::createLogDisplay(QWidget* parent) { QVBoxLayout* layout = new QVBoxLayout(parent); layout->setContentsMargins(5, 5, 5, 5); layout->setSpacing(10); QLabel* titleLabel = new QLabel("操作日志"); titleLabel->setStyleSheet("font-weight: bold; font-size: 14px;"); layout->addWidget(titleLabel); logDisplay = new QTextEdit(); logDisplay->setReadOnly(true); logDisplay->setStyleSheet( "font-family: 'Courier New';" "font-size: 12px;" "background-color: white;" "border: 1px solid #ccc;" ); layout->addWidget(logDisplay,1); //拉伸因子为1 //出板数显示区域 QHBoxLayout* boardCountLayout = new QHBoxLayout(); boardCountLabel = new QLabel("出板数:"); boardCountLabel->setStyleSheet("font-weight: bold; font-size: 14px;"); boardCountDisplay = new QTextEdit(); boardCountDisplay->setReadOnly(true); boardCountDisplay->setFixedHeight(30); boardCountDisplay->setStyleSheet( "font-size: 14px;" "background-color: white;" "border: 1px solid #ccc;" ); boardCountLayout->addWidget(boardCountLabel); boardCountLayout->addWidget(boardCountDisplay, 1); //设置拉伸因子 layout->addLayout(boardCountLayout); } //获取当天日期字符串 QString JTR::getTodayDateString() { return QDateTime::currentDateTime().toString("yyyy-MM-dd"); } void JTR::createBarChart() { QSqlDatabase db = dbManager.getDatabase(); QString today = getTodayDateString(); //获取今天的第一条数据的年月日 QString productionDateStr = "生产日期:" + today; QDateTime startTime = QDateTime::fromString(today + " 07:00:00", "yyyy-MM-dd hh:mm:ss"); //固定为当天7:00 QDateTime endTime = QDateTime::fromString(today + " 23:00:00", "yyyy-MM-dd hh:mm:ss"); //固定为当天23:00 //查询保持不变 QString queryStr = QString( "SELECT " "CONCAT(LPAD(HOUR(record_time), 2, '0'), ':', " "IF(MINUTE(record_time) < 30, '00', '30')) AS time_slot, " "MAX(board_out_count) - MIN(board_out_count) + 1 AS board_count " "FROM board_count " "WHERE DATE(record_time) = '%1' " "AND TIME(record_time) BETWEEN '07:00:00' AND '23:00:00' " "GROUP BY HOUR(record_time), FLOOR(MINUTE(record_time)/30) " "ORDER BY time_slot" ).arg(today); QSqlQuery query(db); if(!query.exec(queryStr)) { qDebug() << "条形图查询失败:" << query.lastError().text(); // 即使查询失败也创建空图表,保证UI显示 createEmptyBarChart(productionDateStr); return; } //创建时间槽列表(7:00-23:00,半小时一个槽) QStringList timeSlots; QMap<QString, int> timeSlotCounts; //生成所有可能的时间槽并初始化为0 for(int hour = 7; hour <= 23; hour++) { timeSlots << QString("%1:00").arg(hour, 2, 10, QChar('0')); timeSlotCounts[QString("%1:00").arg(hour, 2, 10, QChar('0'))] = 0; if(hour < 23) { //23点不添加30分(避免超过23:00) timeSlots << QString("%1:30").arg(hour, 2, 10, QChar('0')); timeSlotCounts[QString("%1:30").arg(hour, 2, 10, QChar('0'))] = 0; } } //填充查询结果 bool hasData = false; while(query.next()) { QString timeSlot = query.value("time_slot").toString(); int count = query.value("board_count").toInt(); timeSlotCounts[timeSlot] = count; if(count > 0) hasData = true; } //如果没有数据,创建空图表 if(!hasData) { createEmptyBarChart(productionDateStr); return; } //创建条形图 QChart* barChart = new QChart(); barChart->setAnimationOptions(QChart::SeriesAnimations); barChart->legend()->hide(); //设置标题样式 QFont titleFont = barChart->titleFont(); titleFont.setPointSize(10); titleFont.setBold(true); barChart->setTitleFont(titleFont); //设置标题内容 barChart->setTitle(QString("生产数量统计(半小时) - %1").arg(productionDateStr)); QBarSeries *series = new QBarSeries(); QBarSet *set = new QBarSet("生产数量"); set->setBrush(QBrush(QColor("#4CAF50"))); int maxCount = 0; foreach(const QString &slot, timeSlots) { int count = timeSlotCounts[slot]; *set << count; if(count > maxCount) { maxCount = count; } } maxCount = ((maxCount / 10) + 1) * 10; if(maxCount < 15) maxCount = 15; series->append(set); barChart->addSeries(series); //===== 使用 QCategoryAxis ===== QCategoryAxis *axisX = new QCategoryAxis(); axisX->setMin(0); axisX->setLabelsPosition(QCategoryAxis::AxisLabelsPositionOnValue); //创建时间标签(7:00-23:00,每半小时一个刻度) QStringList timeLabels; QList<double> tickPositions; for(int hour = 7; hour <= 23; hour++) { //整点标签和位置 timeLabels << QString("%1:00").arg(hour, 2, 10, QChar('0')); tickPositions << (hour - 7) * 2; //转换为0-based索引(每半小时=1单位) //半点标签和位置(23:00除外) if(hour < 23) { timeLabels << QString("%1:30").arg(hour, 2, 10, QChar('0')); tickPositions << (hour - 7) * 2 + 1; } } //设置分类边界(关键修正:直接使用刻度位置) for(int i = 0; i < timeLabels.size(); i++) { axisX->append(timeLabels[i], tickPositions[i] - 0.5); } //设置轴范围(覆盖所有时间点) axisX->setRange(0, tickPositions.last()); //其他设置 axisX->setTitleText("时间"); axisX->setLabelsAngle(-90); //设置Y轴(数量) QValueAxis *axisY = new QValueAxis(); axisY->setRange(0, maxCount); axisY->setTitleText("数量"); axisY->setTickCount((maxCount / 5) + 1); axisY->setLabelFormat("%d"); //将轴添加到图表并将系列附加到轴 barChart->addAxis(axisX, Qt::AlignBottom); barChart->addAxis(axisY, Qt::AlignLeft); series->attachAxis(axisX); series->attachAxis(axisY); //在条的顶部添加值标签 series->setLabelsVisible(true); series->setLabelsFormat("@value"); series->setLabelsPosition(QAbstractBarSeries::LabelsCenter); //调整图表边距,确保标签显示完整 barChart->setMargins(QMargins(30, 10, 30, 30)); //设置条形宽度 series->setBarWidth(0.9); //适当调整条形宽度 //将图表应用于视图 if(barChartView->chart()) { delete barChartView->chart(); } barChartView->setChart(barChart); barChartView->setRenderHint(QPainter::Antialiasing); } // 创建空条形图 void JTR::createEmptyBarChart(const QString& title) { QChart* barChart = new QChart(); barChart->setAnimationOptions(QChart::SeriesAnimations); barChart->legend()->hide(); QFont titleFont = barChart->titleFont(); titleFont.setPointSize(10); titleFont.setBold(true); barChart->setTitleFont(titleFont); barChart->setTitle(QString("生产数量统计(半小时) - %1").arg(title)); QBarSeries *series = new QBarSeries(); QBarSet *set = new QBarSet("生产数量"); set->setBrush(QBrush(QColor("#4CAF50"))); // 添加空数据 for(int i = 0; i < 32; i++) // 7:00-23:00共32个半小时段 { *set << 0; } series->append(set); barChart->addSeries(series); // 创建X轴 QCategoryAxis *axisX = new QCategoryAxis(); axisX->setMin(0); axisX->setLabelsPosition(QCategoryAxis::AxisLabelsPositionOnValue); QStringList timeLabels; QList<double> tickPositions; for(int hour = 7; hour <= 23; hour++) { timeLabels << QString("%1:00").arg(hour, 2, 10, QChar('0')); tickPositions << (hour - 7) * 2; if(hour < 23) { timeLabels << QString("%1:30").arg(hour, 2, 10, QChar('0')); tickPositions << (hour - 7) * 2 + 1; } } for(int i = 0; i < timeLabels.size(); i++) { axisX->append(timeLabels[i], tickPositions[i] - 0.5); } axisX->setRange(0, tickPositions.last()); axisX->setTitleText("时间"); axisX->setLabelsAngle(-90); // 创建Y轴 QValueAxis *axisY = new QValueAxis(); axisY->setRange(0, 10); // 默认范围0-10 axisY->setTitleText("数量"); axisY->setTickCount(3); axisY->setLabelFormat("%d"); barChart->addAxis(axisX, Qt::AlignBottom); barChart->addAxis(axisY, Qt::AlignLeft); series->attachAxis(axisX); series->attachAxis(axisY); barChart->setMargins(QMargins(30, 10, 30, 30)); if(barChartView->chart()) { delete barChartView->chart(); } barChartView->setChart(barChart); barChartView->setRenderHint(QPainter::Antialiasing); } void JTR::updateBarChart() { createBarChart(); // 直接调用现有的创建函数 } //创建温区 void JTR::createTemperatureDisplay(QWidget* parent) { QVBoxLayout* vLayout = new QVBoxLayout(parent); vLayout->setSpacing(15); //创建上层温区 QHBoxLayout* upperHeaderLayout = new QHBoxLayout(); upperHeaderLayout->addSpacing(0); //增加间距对齐 for(int i = 1;i < 11;++i) { QLabel* numLabel = new QLabel(QString::number(i)); numLabel->setAlignment(Qt::AlignCenter); numLabel->setFixedWidth(50); upperHeaderLayout->addWidget(numLabel); } vLayout->addLayout(upperHeaderLayout,1); //创建上层温区 QGroupBox* upGroup = new QGroupBox("上层温区"); QHBoxLayout* upLayout = new QHBoxLayout(); upLayout->setSpacing(15); for(int i =0;i < 10;++i) { TemperatureGauge* gauge = new TemperatureGauge(); gauge->setFixedSize(60,150); upLayout->addWidget(gauge); upGauges.append(gauge); } upGroup->setLayout(upLayout); vLayout->addWidget(upGroup,2); // //创建动画区域 // animationWidget = new QWidget(); // animationWidget->setFixedHeight(30); // animationWidget->setStyleSheet("background-color:white;"); // animLayout = new QHBoxLayout(animationWidget); // animLayout->setContentsMargins(0,0,0,0); // animLayout->setSpacing(0); // //创建十个动画片段 // for(int i = 0;i < 10;++i) // { // QWidget* segment = new QWidget(); // segment->setStyleSheet("background-color:transparent;border-right:1px dashed #888"); // segment->setFixedWidth(60); // animLayout->addWidget(segment); // } // //创建移动的矩形 // movingRect = new QWidget(animationWidget); // movingRect->setStyleSheet("background-color: #4CAF50;"); // movingRect->setFixedSize(60, 10); // movingRect->move(0, 10); // //设置动画 // animation = new QPropertyAnimation(movingRect,"pos",this); // animation->setDuration(300000); //通过时间 // animation->setStartValue(QPoint(0,10)); // animation->setEndValue(QPoint(1400, 10)); //间距 // animation->setEasingCurve(QEasingCurve::Linear); // animation->setLoopCount(-1); // vLayout->addWidget(animationWidget,1); //创建下层温区 QGroupBox* lowerGroup = new QGroupBox("下层温区"); QHBoxLayout* lowerLayout = new QHBoxLayout(); lowerLayout->setSpacing(15); for(int i = 0;i < 10;++i) { TemperatureGauge* gauge = new TemperatureGauge(); gauge->setFixedSize(60,150); lowerLayout->addWidget(gauge); lowGauges.append(gauge); } lowerGroup->setLayout(lowerLayout); vLayout->addWidget(lowerGroup,2); //添加条形图 QGroupBox* barChartGroup = new QGroupBox("生产数量统计(半小时)"); barChartGroup->setStyleSheet("font:600 12pt '宋体';"); QVBoxLayout* barChartLayout = new QVBoxLayout(barChartGroup); //创建条形图 barChartView = new QChartView(this); barChartView->setRenderHint(QPainter::Antialiasing); barChartLayout->addWidget(barChartView); vLayout->addWidget(barChartGroup,4); //添加主布局 QVBoxLayout *mainLayout = qobject_cast<QVBoxLayout*>(this->layout()); if(mainLayout) { mainLayout->insertLayout(0,vLayout); } createBarChart(); } void JTR::refreshData() { if(!DatabaseManager::instance().isConnected()) { qDebug() << "数据库未连接"; return; } //刷新温度数据 refreshTemperatureData(); //刷新日志数据 refreshLogData(); } void JTR::refreshLogData() { QSqlQuery query(DatabaseManager::instance().getDatabase()); bool querySuccess = query.exec("SELECT time, roleE, roleC, content FROM logs WHERE date(time) = CURRENT_DATE ORDER BY time ASC"); if(!querySuccess) { qDebug() << "日志查询失败:" << query.lastError().text(); return; } QString logText; while(query.next()) { QDateTime time = query.value(0).toDateTime(); QString roleE = query.value(1).toString(); QString roleC = query.value(2).toString(); QString content = query.value(3).toString(); //格式化日志行 logText += QString("%1\t%2\t%3\t%4\n") .arg(time.toString("yyyy-MM-dd hh:mm:ss")) .arg(roleE, -8) //左对齐,占8字符宽度 .arg(roleC, -6) //左对齐,占6字符宽度 .arg(content); } //只有当内容变化时才更新,避免频繁刷新 if(logDisplay->toPlainText() != logText) { logDisplay->setPlainText(logText); logDisplay->moveCursor(QTextCursor::Start); //滚动到顶部 } } // void JTR::refreshTemperatureData() // { // //获取最新的20条记录 // QSqlQuery query(DatabaseManager::instance().getDatabase()); // bool querySuccess = query.exec("SELECT zone_name, zone_level, actual_value, record_time FROM temperature_data " // "WHERE zone_level IN ('上层', '下层') " // "ORDER BY record_time DESC LIMIT 20"); // if(!querySuccess) // { // qDebug() << "查询失败:" << query.lastError().text(); // return; // } // //初始化温度数组 // QVector<float> upTemps(10,0.0f); // QVector<float> lowTemps(10,0.0f); // bool needUpdate = false; // //QDateTime latestTime; // while(query.next()) // { // QString zoneName = query.value(0).toString(); // QString zoneLevel = query.value(1).toString(); // float temp = query.value(2).toFloat(); // //解析温区位置和编号 // QRegularExpression re("(\\d+)"); // QRegularExpressionMatch match = re.match(zoneName); // if(match.hasMatch()) // { // int zoneNum = match.captured(1).toInt(); // if(zoneNum >= 1 && zoneNum <= 10) // { // if(zoneLevel == "上层") // { // if(upTemps[zoneNum - 1] == 0.0f) // 只取第一个有效值 // { // upTemps[zoneNum - 1] = temp; // // 检查是否需要更新 // if(lastUpTemps.size() > zoneNum - 1 && // qAbs(lastUpTemps[zoneNum - 1] - temp) > 0.1f) // { // needUpdate = true; // } // } // } // else if(zoneLevel == "下层") // { // if(lowTemps[zoneNum - 1] == 0.0f) // 只取第一个有效值 // { // lowTemps[zoneNum - 1] = temp; // // 检查是否需要更新 // if(lastLowTemps.size() > zoneNum - 1 && // qAbs(lastLowTemps[zoneNum - 1] - temp) > 0.1f) // { // needUpdate = true; // } // } // } // } // } // } // // 如果没有变化且不是第一次更新,则不执行更新 // if(!needUpdate && !lastUpTemps.isEmpty()) // { // return; // } // // 更新显示 - 使用直接设置方法避免闪烁 // for(int i = 0; i < upGauges.size() && i < upTemps.size(); ++i) // { // if(lastUpTemps.size() <= i || qAbs(lastUpTemps[i] - upTemps[i]) > 0.1f) // { // upGauges[i]->setTemperatureDirect(upTemps[i]); // } // } // for(int i = 0; i < lowGauges.size() && i < lowTemps.size(); ++i) // { // if(lastLowTemps.size() <= i || qAbs(lastLowTemps[i] - lowTemps[i]) > 0.1f) // { // lowGauges[i]->setTemperatureDirect(lowTemps[i]); // } // } // // 保存当前温度值 // lastUpTemps = upTemps; // lastLowTemps = lowTemps; // } void JTR::refreshTemperatureData() { if(!DatabaseManager::instance().isConnected()) { qDebug() << "数据库未连接"; return; } // 初始化温度数组,使用上一次的值或0 QVector<float> upTemps = lastUpTemps.isEmpty() ? QVector<float>(10, 0.0f) : lastUpTemps; QVector<float> lowTemps = lastLowTemps.isEmpty() ? QVector<float>(10, 0.0f) : lastLowTemps; bool upUpdated = false; bool lowUpdated = false; // 获取最新的温度数据(扩大查询范围确保能获取到所有温区数据) QSqlQuery query(DatabaseManager::instance().getDatabase()); bool querySuccess = query.exec("SELECT zone_name, zone_level, actual_value, record_time FROM temperature_data " "WHERE zone_level IN ('上层', '下层') " "ORDER BY record_time DESC LIMIT 40"); // 增加查询数量 if(!querySuccess) { qDebug() << "温度查询失败:" << query.lastError().text(); return; } // 使用QSet记录已经处理过的温区,避免重复处理 QSet<QString> processedZones; while(query.next()) { QString zoneName = query.value(0).toString(); QString zoneLevel = query.value(1).toString(); float temp = query.value(2).toFloat(); QString zoneKey = zoneLevel + zoneName; // 创建唯一键 // 如果已经处理过这个温区,跳过 if(processedZones.contains(zoneKey)) continue; processedZones.insert(zoneKey); // 解析温区编号 QRegularExpression re("(\\d+)"); QRegularExpressionMatch match = re.match(zoneName); if(!match.hasMatch()) continue; int zoneNum = match.captured(1).toInt(); if(zoneNum < 1 || zoneNum > 10) continue; if(zoneLevel == "上层") { if(upTemps[zoneNum - 1] != temp) { upTemps[zoneNum - 1] = temp; upUpdated = true; } } else if(zoneLevel == "下层") { if(lowTemps[zoneNum - 1] != temp) { lowTemps[zoneNum - 1] = temp; lowUpdated = true; } } } // 更新上层温区显示 if(upUpdated || lastUpTemps.isEmpty()) { for(int i = 0; i < qMin(upGauges.size(), upTemps.size()); ++i) { upGauges[i]->setTemperatureDirect(upTemps[i]); } } // 更新下层温区显示 if(lowUpdated || lastLowTemps.isEmpty()) { for(int i = 0; i < qMin(lowGauges.size(), lowTemps.size()); ++i) { lowGauges[i]->setTemperatureDirect(lowTemps[i]); } } // 保存当前温度值 lastUpTemps = upTemps; lastLowTemps = lowTemps; qDebug() << "温度更新完成 - 上层:" << upTemps << "下层:" << lowTemps; } void JTR::refreshBoardCount() { if(!DatabaseManager::instance().isConnected()) { qDebug() << "数据库未连接"; return; } QSqlQuery query(DatabaseManager::instance().getDatabase()); bool querySuccess = query.exec( "SELECT board_out_count, record_time FROM board_count " "ORDER BY record_time DESC LIMIT 1" ); if(!querySuccess) { qDebug() << "出板数查询失败:" << query.lastError().text(); return; } if(query.next()) { int count = query.value(0).toInt(); QDateTime recordTime = query.value(1).toDateTime(); // 只更新比当前显示更新的数据 if(!lastBoardCountTime.isValid() || recordTime > lastBoardCountTime) { boardCountDisplay->setPlainText(QString::number(count)); lastBoardCountTime = recordTime; // 出板数更新后也更新条形图 updateBarChart(); qDebug() << "更新出板数显示:" << count << "时间:" << recordTime.toString(); } }else { boardCountDisplay->setPlainText("0"); } } 现在这个更新条形图的时候会闪烁更新,可能是因为每次更新会把原来的图像清除掉,然后显示新的数据图像,但是我需要不是这样的,我需要的是在原来的图像的基础上变化为新的图像,这样看起来比较流畅
07-18
在我的后端中还有两个文件,一个是new_algorithm.py:import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.neighbors import KNeighborsClassifier from sklearn.svm import SVC from sklearn.ensemble import RandomForestClassifier from sklearn.tree import DecisionTreeClassifier from sklearn.neural_network import MLPClassifier from sklearn.metrics import accuracy_score, classification_report from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline from sklearn.base import clone import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import os import re from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer import seaborn as sns # 添加数据插补器 from sklearn.impute import SimpleImputer def check_chinese_font_support(): """检查系统是否支持中文字体""" chinese_fonts = ['SimHei', 'WenQuanYi Micro Hei', 'Heiti TC', 'Microsoft YaHei', 'SimSun'] for font in chinese_fonts: try: plt.rcParams["font.family"] = font # 测试字体是否可用 fig, ax = plt.subplots(figsize=(1, 1)) ax.text(0.5, 0.5, '测试', fontsize=12) plt.close(fig) print(f"系统支持中文字体: {font}") return True except: continue print("系统不支持中文字体,将使用英文标签") plt.rcParams["font.family"] = ['Arial', 'sans-serif'] return False class GasSensorDataAnalyzer: """有害气体分类数据加载与预处理类""" def __init__(self): # 基础气体标签 self.gas_labels = { 'acetone': 0, 'toluene': 1, 'methanol': 2, 'formaldehyde': 3, 'ethanol': 4 } # 混合气体标签生成(每个混合气体用唯一数字标识) self.mixture_labels = self._generate_mixture_labels() # 合并所有气体标签 self.all_gas_labels = {**self.gas_labels, **self.mixture_labels} # 中英文气体名称映射 self.gas_names = { 'acetone': {'cn': '丙酮', 'en': 'Acetone'}, 'toluene': {'cn': '甲苯', 'en': 'Toluene'}, 'methanol': {'cn': '甲醇', 'en': 'Methanol'}, 'formaldehyde': {'cn': '甲醛', 'en': 'Formaldehyde'}, 'ethanol': {'cn': '乙醇', 'en': 'Ethanol'}, 'toluene+formaldehyde': {'cn': '甲苯+甲醛', 'en': 'Toluene+Formaldehyde'}, 'methanol+toluene+formaldehyde': {'cn': '甲醇+甲苯+甲醛', 'en': 'Methanol+Toluene+Formaldehyde'} # 可以根据需要添加更多混合气体的名称映射 } # 传感器类型映射 self.sensor_types = { 'MP2': 0, 'MP3B': 1, 'MP503': 2, 'MP801': 3, 'MQ2': 4, 'MQ7B': 5 } # 初始化多维度类别映射 self.multi_dimension_labels = {} self.next_label_id = 0 # 传感器中英文名称映射 self.sensor_names = { 'MP2': {'cn': 'MP2', 'en': 'MP2'}, 'MP3B': {'cn': 'MP3B', 'en': 'MP3B'}, 'MP503': {'cn': 'MP503', 'en': 'MP503'}, 'MP801': {'cn': 'MP801', 'en': 'MP801'}, 'MQ2': {'cn': 'MQ2', 'en': 'MQ2'}, 'MQ7B': {'cn': 'MQ7B', 'en': 'MQ7B'} } def _generate_mixture_labels(self): """生成混合气体的标签映射""" # 定义可能的混合气体组合 mixtures = [ 'toluene+formaldehyde', 'methanol+toluene+formaldehyde' # 可以根据需要添加更多混合气体组合 ] # 为每个混合气体分配唯一标签(从基础气体标签之后开始) next_label = max(self.gas_labels.values()) + 1 return {mixture: next_label + i for i, mixture in enumerate(mixtures)} def get_or_create_multi_dimension_label(self, sensor_type, gas_type, concentration): """ 获取或创建多维度类别标签 参数: - sensor_type: 传感器类型 - gas_type: 气体类型 - concentration: 浓度值 返回: - 标签ID和标签名称 """ # 创建唯一键 key = f"{sensor_type}_{gas_type}_{concentration}ppm" # 如果键不存在,创建新标签 if key not in self.multi_dimension_labels: self.multi_dimension_labels[key] = self.next_label_id self.next_label_id += 1 # 返回标签ID和标签名称 label_id = self.multi_dimension_labels[key] # 创建中英文标签名称 sensor_name_cn = self.sensor_names.get(sensor_type, {}).get('cn', sensor_type) sensor_name_en = self.sensor_names.get(sensor_type, {}).get('en', sensor_type) gas_name_cn = self.gas_names.get(gas_type, {}).get('cn', gas_type) gas_name_en = self.gas_names.get(gas_type, {}).get('en', gas_type) label_name_cn = f"{sensor_name_cn}_{gas_name_cn}_{concentration}ppm" label_name_en = f"{sensor_name_en}_{gas_name_en}_{concentration}ppm" return label_id, { 'cn': label_name_cn, 'en': label_name_en } def load_single_gas_data(self, file_path, gas_type, concentration, sensor_type): """ 加载单一气体数据 参数: - file_path: 文件路径 - gas_type: 气体类型 (如 'acetone', 'toluene' 等) - concentration: 浓度值 (如 20, 30, 50 等) - sensor_type: 传感器类型 (如 'MP2', 'MP801' 等) """ try: if not os.path.exists(file_path): raise FileNotFoundError(f"文件不存在: {file_path}") df = pd.read_excel(file_path, sheet_name='Sheet1', index_col=0) X = df.values # 尝试将数据转换为 float 类型 try: X = X.astype(float) except ValueError: print("警告: 数据中包含非数值类型,将过滤掉非数值类型的数据") numeric_mask = np.vectorize(np.isreal)(X) X = X[numeric_mask].reshape(-1, df.shape[1]) # 检查并告NaN值 nan_count = np.isnan(X).sum() if nan_count > 0: print(f"警告: 数据中包含 {nan_count} 个NaN值") # 可选:替换NaN值为0 # X = np.nan_to_num(X, nan=0.0) # 创建多维度标签 label_id, label_name = self.get_or_create_multi_dimension_label( sensor_type, gas_type, concentration ) # 为所有样本分配相同的标签 y = np.full(len(X), label_id, dtype=int) print(f"已加载 {label_name['cn']} 数据: {len(X)} 样本, 特征维度: {X.shape[1]}") return X, y except Exception as e: print(f"加载数据时出: {e}") return None, None def load_multiple_gas_data(self, file_paths, gas_types, concentrations, sensor_types): """ 加载多个气体数据并合并 参数: - file_paths: 文件路径列表 - gas_types: 气体类型列表 (如 ['acetone', 'toluene'] 等) - concentrations: 浓度值列表 (如 [20, 30] 等) - sensor_types: 传感器类型列表 (如 ['MP2', 'MP801'] 等) """ X_all = [] y_all = [] feature_dimensions = [] # 用于记录每个数据集的特征维度 for file_path, gas_type, concentration, sensor_type in zip( file_paths, gas_types, concentrations, sensor_types ): X, y = self.load_single_gas_data(file_path, gas_type, concentration, sensor_type) if X is not None and len(X) > 0: X_all.append(X) y_all.append(y) feature_dimensions.append(X.shape[1]) if not X_all: print("没有加载到有效数据") return None, None # 检查所有数据集的特征维度是否一致 unique_dimensions = np.unique(feature_dimensions) if len(unique_dimensions) > 1: print(f"警告: 检测到不同的特征维度: {unique_dimensions}") print("这可能导致合并数据时出。请检查您的Excel文件是否具有相同的列数。") # 找出最常见的维度 from collections import Counter dimension_counts = Counter(feature_dimensions) most_common_dimension = dimension_counts.most_common(1)[0][0] print(f"最常见的特征维度是: {most_common_dimension}") # 过滤掉特征维度不匹配的数据 filtered_X_all = [] filtered_y_all = [] for i, X in enumerate(X_all): if X.shape[1] == most_common_dimension: filtered_X_all.append(X) filtered_y_all.append(y_all[i]) else: print(f"忽略特征维度不匹配的数据集: {file_paths[i]} (维度: {X.shape[1]})") if not filtered_X_all: print("没有找到特征维度匹配的数据集") return None, None X_all = filtered_X_all y_all = filtered_y_all # 合并所有数据 X_combined = np.vstack(X_all) y_combined = np.concatenate(y_all) # 检查合并后的数据中是否存在NaN值 total_nan = np.isnan(X_combined).sum() if total_nan > 0: print(f"警告: 合并后的数据中包含 {total_nan} 个NaN值,占比: {total_nan/(X_combined.size):.4f}") print(f"NaN值在样本中的分布: {np.isnan(X_combined).any(axis=1).sum()} 个样本包含NaN值") print(f"NaN值在特征中的分布: {np.isnan(X_combined).any(axis=0).sum()} 个特征包含NaN值") print(f"合并后的数据: {len(X_combined)} 样本,{len(np.unique(y_combined))} 个类别,特征维度: {X_combined.shape[1]}") return X_combined, y_combined def load_dataset(self, file_path, gas_type, concentration, sensor_type): """加载单一数据集并返回""" return self.load_single_gas_data(file_path, gas_type, concentration, sensor_type) class AlgorithmSelector: """多算法选择与训练类""" def __init__(self, use_chinese=True): # 算法名称映射 self.algorithm_names = { 'knn': {'cn': 'K-近邻算法', 'en': 'K-Nearest Neighbors'}, 'svm': {'cn': '支持向量机', 'en': 'Support Vector Machine'}, 'random_forest': {'cn': '随机森林', 'en': 'Random Forest'}, 'decision_tree': {'cn': '决策树', 'en': 'Decision Tree'}, 'neural_network': {'cn': '神经网络', 'en': 'Neural Network'} } # 算法配置 self.algorithms = { 'knn': { 'model': KNeighborsClassifier(), 'params': {'n_neighbors': 5, 'metric': 'euclidean'} }, 'svm': { 'model': SVC(), 'params': {'kernel': 'rbf', 'C': 1.0, 'probability': True} }, 'random_forest': { 'model': RandomForestClassifier(), 'params': {'n_estimators': 100, 'random_state': 42} }, 'decision_tree': { 'model': DecisionTreeClassifier(), 'params': {'max_depth': None, 'random_state': 42} }, 'neural_network': { 'model': MLPClassifier(), 'params': { 'neural_network__hidden_layer_sizes': (100, 50), # 注意前缀 'neural_network__max_iter': 500, 'neural_network__random_state': 42} } } # 算法是否需要标准化 self.needs_scaling = { 'knn': True, 'svm': True, 'random_forest': False, 'decision_tree': False, 'neural_network': True } # 是否使用中文 self.use_chinese = use_chinese def set_algorithm_params(self, algorithm_name, params): """设置算法参数""" if algorithm_name in self.algorithms: # 为Pipeline正确格式化参数名称 formatted_params = {f"{algorithm_name}__{k}": v for k, v in params.items()} self.algorithms[algorithm_name]['params'] = formatted_params else: raise ValueError(f"不支持的算法: {algorithm_name}") def train_models(self, X, y, test_size=0.2, random_state=42): """ 训练所有算法并返回结果 返回: - 包含训练好的模型及其性能的字典 """ # 检查类别数量 unique_classes = np.unique(y) num_classes = len(unique_classes) if num_classes < 2: print(f"警告: 数据集中只有 {num_classes} 个类别,某些算法可能无法训练") print(f"单一类别值: {unique_classes[0]}") # 跳过SVM算法,因为它需要至少两个类别 algorithms_to_train = [name for name in self.algorithms if name != 'svm'] print(f"由于单类别数据,将跳过 SVM 算法,仅训练: {', '.join([self.algorithm_names[name]['cn'] for name in algorithms_to_train])}") # 在单一数据集上划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=test_size, random_state=random_state ) # 标记这是单类别数据 is_single_class_data = True else: # 在多类别数据集上划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=test_size, random_state=random_state, stratify=y ) algorithms_to_train = list(self.algorithms.keys()) is_single_class_data = False # 检查数据类型并确保可以安全转换为数值类型 try: # 尝试将数据转换为float类型 X_train_numeric = X_train.astype(float) X_test_numeric = X_test.astype(float) # 检查训练数据中是否存在NaN值 train_nan = np.isnan(X_train_numeric).sum() if train_nan > 0: print(f"警告: 训练数据中包含 {train_nan} 个NaN值,占比: {train_nan/(X_train_numeric.size):.4f}") print(f"NaN值在训练样本中的分布: {np.isnan(X_train_numeric).any(axis=1).sum()} 个样本包含NaN值") print(f"NaN值在训练特征中的分布: {np.isnan(X_train_numeric).any(axis=0).sum()} 个特征包含NaN值") # 检查测试数据中是否存在NaN值 test_nan = np.isnan(X_test_numeric).sum() if test_nan > 0: print(f"警告: 测试数据中包含 {test_nan} 个NaN值,占比: {test_nan/(X_test_numeric.size):.4f}") print(f"NaN值在测试样本中的分布: {np.isnan(X_test_numeric).any(axis=1).sum()} 个样本包含NaN值") print(f"NaN值在测试特征中的分布: {np.isnan(X_test_numeric).any(axis=0).sum()} 个特征包含NaN值") except ValueError as e: print(f"警告: 无法将数据转换为数值类型,跳过NaN值检查: {e}") results = {} for name in algorithms_to_train: algo = self.algorithms[name] # 获取算法名称(根据是否支持中文选择) algo_name = self.algorithm_names[name]['cn'] if self.use_chinese else self.algorithm_names[name]['en'] try: print(f"\n训练 {algo_name}...") # 创建模型管道 if self.needs_scaling[name]: # 为需要标准化的算法创建包含三个步骤的Pipeline model = Pipeline([ ('imputer', SimpleImputer(strategy='mean')), # 使用均值填充缺失值 ('scaler', StandardScaler()), (name, clone(algo['model'])) ]) else: # 为不需要标准化的算法创建包含两个步骤的Pipeline model = Pipeline([ ('imputer', SimpleImputer(strategy='mean')), # 使用均值填充缺失值 (name, clone(algo['model'])) ]) # 为决策树和随机森林直接设置参数,不使用Pipeline参数设置方式 if name in ['decision_tree', 'random_forest']: # 获取算法实例 algo_instance = model.named_steps[name] # 直接设置参数 for param, value in algo['params'].items(): setattr(algo_instance, param, value) else: # 为其他算法使用Pipeline参数设置方式 model.set_params(**algo['params']) # 训练模型 model.fit(X_train, y_train) # 评估模型 train_accuracy = model.score(X_train, y_train) test_accuracy = model.score(X_test, y_test) y_pred = model.predict(X_test) print(f"训练集准确率: {train_accuracy:.4f}") print(f"测试集准确率: {test_accuracy:.4f}") print("分类:") print(classification_report(y_test, y_pred)) results[name] = { 'name': algo_name, 'model': model, 'train_accuracy': train_accuracy, 'test_accuracy': test_accuracy, 'y_pred': y_pred, 'X_test': X_test, 'y_test': y_test, 'unique_labels': np.unique(y_test), 'is_single_class': is_single_class_data } except Exception as e: print(f"训练 {algo_name} 时发生: {e}") results[name] = { 'name': algo_name, 'error': str(e), 'is_single_class': is_single_class_data } # 为跳过的SVM算法添加结果记录 if 'svm' not in algorithms_to_train: svm_name = self.algorithm_names['svm']['cn'] if self.use_chinese else self.algorithm_names['svm']['en'] results['svm'] = { 'name': svm_name, 'error': "由于单类别数据,跳过SVM算法", 'is_single_class': is_single_class_data } return results def compare_algorithms(self, results): """比较不同算法的性能""" # 过滤掉训练失败的算法 valid_results = {name: result for name, result in results.items() if 'test_accuracy' in result} if not valid_results: print("没有算法成功训练,无法生成比较图。") return None names = [valid_results[name]['name'] for name in valid_results] accuracies = [valid_results[name]['test_accuracy'] for name in valid_results] plt.figure(figsize=(12, 6)) bars = plt.bar(names, accuracies, color='skyblue') # 根据是否支持中文选择标题 title = "不同算法的测试集准确率比较" if self.use_chinese else "Comparison of Test Set Accuracies for Different Algorithms" x_label = "算法" if self.use_chinese else "Algorithm" y_label = "准确率" if self.use_chinese else "Accuracy" plt.ylim(0, 1.05) plt.title(title) plt.xlabel(x_label) plt.ylabel(y_label) # 添加数值标签 for bar in bars: height = bar.get_height() plt.text(bar.get_x() + bar.get_width()/2., height + 0.01, f'{height:.4f}', ha='center', va='bottom') plt.xticks(rotation=45, ha='right') plt.tight_layout() return plt def plot_confusion_matrix(self, results, gas_data_loader, use_chinese=True, rotate_labels=45, fig_width=12, fig_height=10, font_size=10): """ 绘制混淆矩阵 参数: - results: 包含算法结果的字典 - gas_data_loader: 气体数据加载器实例 - use_chinese: 是否使用中文 - rotate_labels: 标签旋转角度,默认为45度 - fig_width: 图形的宽度,默认为12 - fig_height: 图形的高度,默认为10 - font_size: 字体大小,默认为10 """ # 过滤掉训练失败的算法 valid_results = {name: result for name, result in results.items() if 'test_accuracy' in result} if not valid_results: print("没有算法成功训练,无法生成混淆矩阵。") return None # 获取所有算法中出现的唯一标签 all_unique_labels = set() for name, result in valid_results.items(): all_unique_labels.update(result['unique_labels']) all_unique_labels = sorted(list(all_unique_labels)) # 创建标签名称映射 label_names = [] for label in all_unique_labels: # 尝试查找对应的多维度标签名称 label_name = None for key, label_id in gas_data_loader.multi_dimension_labels.items(): if label_id == label: # 获取标签名称而不是标签ID label_name = gas_data_loader.get_or_create_multi_dimension_label( key.split('_')[0], # 传感器类型 key.split('_')[1], # 气体类型 int(key.split('_')[2].replace('ppm', '')) # 浓度值 )[1] # 获取第二个返回值,即标签名称字典 break # 如果找到,使用对应的标签名称 if label_name and isinstance(label_name, dict): if use_chinese: label_names.append(label_name.get('cn', f"类别 {label}")) else: label_names.append(label_name.get('en', f"Class {label}")) else: # 如果没有找到,使用默认标签名称 label_names.append(f"类别 {label}" if use_chinese else f"Class {label}") for name, result in valid_results.items(): plt.figure(figsize=(fig_width, fig_height)) cm = confusion_matrix(result['y_test'], result['y_pred'], labels=all_unique_labels) disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=label_names) disp.plot(cmap=plt.cm.Blues) # 根据是否支持中文选择标题 title = f"{result['name']} 混淆矩阵" if use_chinese else f"{result['name']} Confusion Matrix" # 如果是单类别数据,添加说明 if result.get('is_single_class', False): title += " (单类别数据)" plt.title(title) # 旋转x轴标签 plt.xticks(rotation=rotate_labels, ha='right', rotation_mode='anchor', fontsize=font_size) plt.yticks(fontsize=font_size) plt.tight_layout() return plt def detect_dataset_type(dataset_path): """ 自动检测数据集类型:单一数据集或多数据集文件夹 参数: - dataset_path: 数据集路径 返回: - dataset_type: 'single' 或 'multiple' - file_paths: 文件路径列表 - gas_types: 气体类型列表 - concentrations: 浓度值列表 - sensor_types: 传感器类型列表 """ # 检查路径是否存在 if not os.path.exists(dataset_path): raise FileNotFoundError(f"路径不存在: {dataset_path}") # 检查是文件还是文件夹 if os.path.isfile(dataset_path): # 单一文件处理 file_paths = [dataset_path] # 从文件名提取传感器类型、气体类型和浓度 file_name = os.path.basename(dataset_path) sensor_type = extract_sensor_type(file_name) gas_type = extract_gas_type(file_name) concentration = extract_concentration(file_name) gas_types = [gas_type] concentrations = [concentration] sensor_types = [sensor_type] print(f"检测到单一数据集: {file_name}") print(f"传感器类型: {sensor_type}, 气体类型: {gas_type}, 浓度: {concentration}ppm") return 'single', file_paths, gas_types, concentrations, sensor_types elif os.path.isdir(dataset_path): # 文件夹处理 - 查找所有Excel文件 excel_files = [f for f in os.listdir(dataset_path) if f.endswith(('.xlsx', '.xls'))] if not excel_files: raise ValueError(f"文件夹中没有找到Excel文件: {dataset_path}") file_paths = [] gas_types = [] concentrations = [] sensor_types = [] for file in excel_files: file_path = os.path.join(dataset_path, file) file_paths.append(file_path) # 从文件名提取传感器类型、气体类型和浓度 sensor_type = extract_sensor_type(file) gas_type = extract_gas_type(file) concentration = extract_concentration(file) gas_types.append(gas_type) concentrations.append(concentration) sensor_types.append(sensor_type) print(f"找到数据集文件: {file}") print(f"传感器类型: {sensor_type}, 气体类型: {gas_type}, 浓度: {concentration}ppm") print(f"总共找到 {len(file_paths)} 个数据集文件") return 'multiple', file_paths, gas_types, concentrations, sensor_types else: raise ValueError(f"无法识别的路径: {dataset_path}") def extract_sensor_type(file_name): """从文件名提取传感器类型""" # 定义传感器类型的正则表达式模式 sensor_patterns = { 'MP2': r'(^MP2[^a-zA-Z0-9]|MP2$)', 'MP3B': r'(^MP3B[^a-zA-Z0-9]|MP3B$)', 'MP503': r'(^MP503[^a-zA-Z0-9]|MP503$)', 'MP801': r'(^MP801[^a-zA-Z0-9]|MP801$)', 'MQ2': r'(^MQ2[^a-zA-Z0-9]|MQ2$)', 'MQ7B': r'(^MQ7B[^a-zA-Z0-9]|MQ7B$)' } # 转换为大写以提高匹配率 file_name_upper = file_name.upper() # 尝试匹配传感器类型 for sensor_type, pattern in sensor_patterns.items(): if re.search(pattern, file_name_upper): return sensor_type # 如果没有匹配到,返回默认值 print(f"警告: 无法从文件名 '{file_name}' 中提取传感器类型,使用默认值 'MP2'") return 'MP2' def extract_gas_type(file_name): """从文件名提取气体类型""" # 定义基础气体类型的中英文名称映射 gas_name_mapping = { 'bingtong': 'acetone', '丙酮': 'acetone', 'jiaben': 'toluene', '甲苯': 'toluene', 'jiachun': 'methanol', '甲醇': 'methanol', 'jiaquan': 'formaldehyde', '甲醛': 'formaldehyde', 'yichun': 'ethanol', '乙醇': 'ethanol' } # 去除文件扩展名 file_name_without_ext = os.path.splitext(file_name)[0] # 按照固定格式"传感器_气体名称_浓度"分割文件名 parts = file_name_without_ext.split('_') # 确保有足够的部分 if len(parts) < 3: print(f"警告: 文件名格式不符合预期: {file_name}") return 'acetone' # 获取气体名称部分 gas_name_part = parts[1] # 检查是否为混合气体 if '+' in gas_name_part or '+' in gas_name_part: # 处理混合气体 # 统一分隔符 gas_name_part = gas_name_part.replace('+', '+') gas_components = gas_name_part.split('+') # 转换为标准气体名称 standard_gas_names = [] for component in gas_components: # 先尝试中文名称映射 standard_name = gas_name_mapping.get(component, None) if standard_name: standard_gas_names.append(standard_name) else: # 如果是英文名称,直接添加 if component.lower() in ['acetone', 'toluene', 'methanol', 'formaldehyde', 'ethanol']: standard_gas_names.append(component.lower()) else: print(f"警告: 无法识别的气体成分: {component}") # 按字母顺序排序以确保一致性 standard_gas_names.sort() # 组合成混合气体名称 if len(standard_gas_names) > 1: return '+'.join(standard_gas_names) elif len(standard_gas_names) == 1: return standard_gas_names[0] # 处理单一气体 # 先尝试中文名称映射 standard_name = gas_name_mapping.get(gas_name_part, None) if standard_name: return standard_name # 如果是英文名称,直接返回小写形式 if gas_name_part.lower() in ['acetone', 'toluene', 'methanol', 'formaldehyde', 'ethanol']: return gas_name_part.lower() # 如果没有匹配到,返回默认值 print(f"警告: 无法从文件名 '{file_name}' 中提取气体类型,使用默认值 'acetone'") return 'acetone' def extract_concentration(file_name): """从文件名提取浓度值""" # 去除文件扩展名 file_name_without_ext = os.path.splitext(file_name)[0] # 按照固定格式"传感器_气体名称_浓度"分割文件名 parts = file_name_without_ext.split('_') # 确保有足够的部分 if len(parts) < 3: print(f"警告: 文件名格式不符合预期: {file_name}") return 20 # 获取浓度部分 concentration_part = parts[2] # 提取数字部分 match = re.search(r'(\d+)', concentration_part) if match: return int(match.group(1)) # 如果没有匹配到,返回默认值 print(f"警告: 无法从文件名 '{file_name}' 中提取浓度值,使用默认值 20ppm") return 20 def main(): """主函数""" # 检查中文字体支持 chinese_supported = check_chinese_font_support() # 创建数据加载器 data_loader = GasSensorDataAnalyzer() # 定义数据集路径 dataset_path = r"C:\Users\Cong\Desktop\作业\项目\六通道2混合\2_MP2" try: # 自动检测数据集类型 dataset_type, file_paths, gas_types, concentrations, sensor_types = detect_dataset_type(dataset_path) # 根据检测结果加载数据 if dataset_type == 'single': # 加载单一数据集 X, y = data_loader.load_dataset(file_paths[0], gas_types[0], concentrations[0], sensor_types[0]) else: # 加载多个数据集并合并 X, y = data_loader.load_multiple_gas_data(file_paths, gas_types, concentrations, sensor_types) if X is None or len(X) == 0: print("No valid data available for training. Please check file paths and formats.") return print(f"加载的数据集总样本数: {len(X)}") print(f"数据集中的类别数量: {len(np.unique(y))}") # 创建算法选择器,根据中文字体支持情况决定是否使用中文 selector = AlgorithmSelector(use_chinese=chinese_supported) # 自定义参数配置示例 selector.set_algorithm_params('knn', {'n_neighbors': 3, 'metric': 'manhattan'}) selector.set_algorithm_params('svm', {'C': 0.8, 'kernel': 'linear'}) selector.set_algorithm_params('neural_network', {'hidden_layer_sizes': (150, 75)}) # 训练所有算法 results = selector.train_models(X, y) # 比较算法性能 plt1 = selector.compare_algorithms(results) if plt1: plt1.savefig('algorithm_comparison.png') plt1.close() # 绘制混淆矩阵 plt2 = selector.plot_confusion_matrix(results, data_loader, use_chinese=chinese_supported, rotate_labels=45,fig_width=20, fig_height=20, font_size=8) if plt2: plt2.savefig('confusion_matrix.png') plt2.close() print("\n算法比较结果已保存为 'algorithm_comparison.png'") print("混淆矩阵已保存为 'confusion_matrix.png'") except Exception as e: print(f"程序执行过程中发生: {e}") if __name__ == "__main__": main()还有一个是tempcoderunnerfile.py文件:@app.route('/upload', methods=['POST']) def upload_file(): """处理文件上传""" if 'files' not in request.files: return jsonify({'error': 'No file part'}), 400 files = request.files.getlist('files') gas_type = request.form.get('gas_type', 'acetone') concentration = int(request.form.get('concentration', 20)) if not files or files[0].filename == '': return jsonify({'error': 'No selected file'}), 400 datasets = [] for file in files: if file and allowed_file(file.filename): # 保存临时文件 file_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename) file.save(file_path) # 加载数据 data = data_loader.load_single_gas_data(file_path, gas_type, concentration) datasets.append(data) # 删除临时文件 os.remove(file_path) # 合并数据集 X, y = data_loader.combine_datasets(datasets) if X is None or len(X) == 0: return jsonify({'error': 'No valid data loaded'}), 400 # 保存合并后的数据 df = pd.DataFrame(X) df['label'] = y file_path = os.path.join(app.config['UPLOAD_FOLDER'], 'temp_data.xlsx') df.to_excel(file_path, index=False) return jsonify({ 'status': 'success', 'sample_count': len(X), 'feature_count': X.shape[1], 'gas_type': gas_type, 'concentration': concentration })请根据这两个文件重修修改app.py文件,确保他在algorithmselection呈现的页面中当我选择两个文件进入的时候能够分析数据
06-22
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值