您说得对!我发现问题了,之前的代码中模型更新功能没有正确集成到主界面中。让我修复这个问题:
现在我已经修复了问题!主要改进包括:
现在点击"更新模型版本"按钮就会打开一个完整的模型管理对话框,包含文件上传和模型管理的所有功能!
普通用户首页设计思路(Vue3 + Element Plus + Flask)基于您的需求,我将设计一个既能展示湖北省电力概况,又能引导用户上传个人数据的普通用户首页。这个设计将包含四个核心区域,并采用模拟数据展示全省电力情况。设计思路graph TD A[普通用户首页] --> B[湖北省电力概览] A --> C[功能导航区] A --> D[数据上传引导] A --> E[通知公告区] B --> B1[关键指标卡片] B --> B2[负荷曲线图] C --> C1[我的电厂] C --> C2[数据分析] C --> C3[负荷预测] D --> D1[上传状态] D --> D2[上传按钮] D --> D3[操作指南] E --> E1[系统通知] E --> E2[政策更新]前端实现(Vue3 + Element Plus)文件结构src/ ├── views/ │ └── UserHome.vue ├── components/ │ ├── ProvinceOverview.vue │ ├── FunctionCards.vue │ ├── DataUploadGuide.vue │ └── Announcements.vue ├── services/ │ └── api.js主页面组件 (UserHome.vue)<template> <div class="user-home"> <!-- 湖北省电力概览 --> <ProvinceOverview :data="provinceData" /> <!-- 功能导航区 --> <FunctionCards :hasData="hasUserData" @navigate="handleNavigation" /> <!-- 数据上传引导 --> <DataUploadGuide :hasData="hasUserData" :lastUpload="lastUploadTime" @upload-success="handleUploadSuccess" /> <!-- 通知公告区 --> <Announcements :items="announcements" /> </div> </template> <script> import { ref, onMounted } from 'vue'; import { useRouter } from 'vue-router'; import ProvinceOverview from '@/components/ProvinceOverview.vue'; import FunctionCards from '@/components/FunctionCards.vue'; import DataUploadGuide from '@/components/DataUploadGuide.vue'; import Announcements from '@/components/Announcements.vue'; import { fetchProvinceData, fetchAnnouncements } from '@/services/api'; export default { components: { ProvinceOverview, FunctionCards, DataUploadGuide, Announcements }, setup() { const router = useRouter(); const provinceData = ref({}); const announcements = ref([]); const hasUserData = ref(false); const lastUploadTime = ref(''); // 获取湖北省电力数据和通知公告 const loadData = async () => { try { provinceData.value = await fetchProvinceData(); announcements.value = await fetchAnnouncements(); // 检查用户是否有上传数据 hasUserData.value = localStorage.getItem('hasUserData') === 'true'; lastUploadTime.value = localStorage.getItem('lastUploadTime') || ''; } catch (error) { console.error('数据加载失败:', error); } }; // 处理上传成功 const handleUploadSuccess = () => { hasUserData.value = true; lastUploadTime.value = new Date().toLocaleString(); localStorage.setItem('hasUserData', 'true'); localStorage.setItem('lastUploadTime', lastUploadTime.value); }; // 处理导航 const handleNavigation = (route) => { if (!hasUserData.value) { ElMessage.warning('请先上传电厂数据'); return; } router.push(route); }; onMounted(loadData); return { provinceData, announcements, hasUserData, lastUploadTime, handleUploadSuccess, handleNavigation }; } }; </script> <style scoped> .user-home { max-width: 1200px; margin: 0 auto; padding: 20px; } .section { margin-bottom: 40px; } </style>湖北省电力概览组件 (ProvinceOverview.vue)<template> <div class="province-overview"> <h2 class="section-title">湖北省电力实时概览</h2> <div class="stats-cards"> <el-card class="stat-card"> <div class="card-content"> <h3>当前全省负荷</h3> <p class="value">{{ data.currentLoad?.toLocaleString() || '--' }} <span>MW</span></p> <p class="trend" :class="trendClass"> <el-icon><Top v-if="isIncreasing" /><Bottom v-else /></el-icon> {{ trendPercentage }}% 较昨日 </p> </div> </el-card> <el-card class="stat-card"> <div class="card-content"> <h3>今日最大负荷</h3> <p class="value">{{ data.maxLoad?.toLocaleString() || '--' }} <span>MW</span></p> <p class="time">{{ data.maxLoadTime || '--' }} 出现</p> </div> </el-card> <el-card class="stat-card"> <div class="card-content"> <h3>新能源占比</h3> <p class="value">{{ data.newEnergyPercent || '--' }} <span>%</span></p> <el-progress :percentage="data.newEnergyPercent || 0" :status="data.newEnergyPercent > 25 ? 'success' : 'warning'" /> </div> </el-card> </div> <div class="load-chart"> <div ref="chart" style="height: 300px;"></div> </div> </div> </template> <script> import { ref, watch, computed } from 'vue'; import * as echarts from 'echarts'; export default { props: { data: { type: Object, default: () => ({}) } }, setup(props) { const chart = ref(null); let chartInstance = null; // 计算趋势 const trendPercentage = computed(() => { if (!props.data.trendPercentage) return '--'; return Math.abs(props.data.trendPercentage).toFixed(1); }); const isIncreasing = computed(() => { return props.data.trendPercentage > 0; }); const trendClass = computed(() => { return isIncreasing.value ? 'increase' : 'decrease'; }); // 初始化图表 const initChart = () => { if (!props.data.loadCurve) return; if (!chartInstance) { chartInstance = echarts.init(chart.value); } const option = { title: { text: '湖北省24小时负荷曲线', left: 'center' }, tooltip: { trigger: 'axis' }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'category', data: props.data.loadCurve.times }, yAxis: { type: 'value', name: '负荷 (MW)' }, series: [ { name: '电力负荷', type: 'line', data: props.data.loadCurve.values, smooth: true, lineStyle: { width: 3, color: '#36a2eb' }, itemStyle: { color: '#36a2eb' }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: 'rgba(54, 162, 235, 0.5)' }, { offset: 1, color: 'rgba(54, 162, 235, 0.1)' } ]) } } ] }; chartInstance.setOption(option); }; // 监听数据变化 watch(() => props.data, (newData) => { if (newData.loadCurve) { initChart(); } }, { deep: true }); return { chart, trendPercentage, isIncreasing, trendClass }; } }; </script> <style scoped> .province-overview { margin-bottom: 40px; } .section-title { margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #eee; } .stats-cards { display: flex; gap: 20px; margin-bottom: 30px; } .stat-card { flex: 1; } .card-content { text-align: center; padding: 15px; } .card-content h3 { margin-bottom: 10px; font-size: 16px; color: #606266; } .card-content .value { font-size: 24px; font-weight: bold; margin: 10px 0; } .card-content .value span { font-size: 14px; color: #909399; } .card-content .trend { display: flex; align-items: center; justify-content: center; gap: 5px; font-size: 14px; } .card-content .trend.increase { color: #67c23a; } .card-content .trend.decrease { color: #f56c6c; } .card-content .time { color: #909399; font-size: 14px; } .load-chart { background: white; padding: 20px; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } </style>功能导航组件 (FunctionCards.vue)<template> <div class="function-cards"> <h2 class="section-title">我的电力服务</h2> <div class="cards-container"> <el-card class="function-card" @click="emit('navigate', '/my-plant')"> <div class="card-content"> <el-icon :size="48"><OfficeBuilding /></el-icon> <h3>我的电厂</h3> <p>管理电厂数据与报告</p> <el-tag v-if="!hasData" type="warning" size="small">需上传数据</el-tag> </div> </el-card> <el-card class="function-card" @click="emit('navigate', '/analysis')"> <div class="card-content"> <el-icon :size="48"><DataAnalysis /></el-icon> <h3>数据分析</h3> <p>深入分析电厂运行数据</p> <el-tag v-if="!hasData" type="warning" size="small">需上传数据</el-tag> </div> </el-card> <el-card class="function-card" @click="emit('navigate', '/prediction')"> <div class="card-content"> <el-icon :size="48"><TrendCharts /></el-icon> <h3>负荷预测</h3> <p>获取未来用电预测</p> <el-tag v-if="!hasData" type="warning" size="small">需上传数据</el-tag> </div> </el-card> </div> </div> </template> <script setup> import { defineProps, defineEmits } from 'vue'; import { OfficeBuilding, DataAnalysis, TrendCharts } from '@element-plus/icons-vue'; const props = defineProps({ hasData: Boolean }); const emit = defineEmits(['navigate']); </script> <style scoped> .function-cards { margin-bottom: 40px; } .cards-container { display: flex; gap: 20px; } .function-card { flex: 1; cursor: pointer; transition: transform 0.3s, box-shadow 0.3s; } .function-card:hover { transform: translateY(-5px); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15); } .card-content { text-align: center; padding: 20px; } .card-content h3 { margin: 15px 0 10px; } .card-content p { color: #909399; font-size: 14px; margin-bottom: 10px; } </style>数据上传引导组件 (DataUploadGuide.vue)<template> <div class="data-upload"> <h2 class="section-title">电厂数据管理</h2> <div class="upload-container"> <div class="upload-status"> <el-icon :size="64" :color="hasData ? '#67c23a' : '#409EFF'"> <UploadFilled /> </el-icon> <div class="status-text"> <h3 v-if="hasData">已上传最新数据</h3> <h3 v-else>您的电厂数据待上传</h3> <p v-if="hasData">最后上传时间: {{ lastUpload }}</p> <p v-else>上传电厂数据以获取个性化分析</p> </div> </div> <el-upload class="upload-action" action="/api/upload" :show-file-list="false" :on-success="handleSuccess" :before-upload="beforeUpload" > <el-button type="primary" size="large"> <el-icon><Upload /></el-icon> 上传电厂数据 </el-button> <template #tip> <div class="upload-tip"> 支持格式: CSV, Excel (最大100MB) </div> </template> </el-upload> </div> <div class="upload-guide"> <el-collapse> <el-collapse-item title="数据上传指南"> <div class="guide-content"> <p>1. <a href="/template.csv" download>下载数据模板</a>,按格式准备数据</p> <p>2. 数据应包含日期、时间、发电量等关键字段</p> <p>3. 支持CSV和Excel格式文件,最大100MB</p> <p>4. 每日数据推荐在18:00前上传</p> <p>5. 系统会自动处理数据格式问题</p> </div> </el-collapse-item> </el-collapse> </div> </div> </template> <script setup> import { defineProps, defineEmits } from 'vue'; import { ElMessage } from 'element-plus'; import { UploadFilled, Upload } from '@element-plus/icons-vue'; const props = defineProps({ hasData: Boolean, lastUpload: String }); const emit = defineEmits(['upload-success']); const beforeUpload = (file) => { const validTypes = [ 'text/csv', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ]; if (!validTypes.includes(file.type)) { ElMessage.error('只支持CSV和Excel格式文件'); return false; } const maxSize = 100 * 1024 * 1024; // 100MB if (file.size > maxSize) { ElMessage.error('文件大小不能超过100MB'); return false; } return true; }; const handleSuccess = (response) => { if (response.success) { ElMessage.success('数据上传成功'); emit('upload-success'); } else { ElMessage.error(`上传失败: ${response.message}`); } }; </script> <style scoped> .data-upload { margin-bottom: 40px; } .upload-container { display: flex; align-items: center; justify-content: space-between; padding: 30px; background: #f5f7fa; border-radius: 4px; margin-bottom: 20px; } .upload-status { display: flex; align-items: center; gap: 20px; } .status-text h3 { margin-bottom: 5px; } .status-text p { color: #909399; } .upload-action { text-align: center; } .upload-tip { margin-top: 10px; color: #909399; font-size: 14px; } .guide-content { padding: 10px; } .guide-content p { margin: 8px 0; } .guide-content a { color: #409EFF; text-decoration: none; } </style>通知公告组件 (Announcements.vue)<template> <div class="announcements"> <h2 class="section-title">最新通知公告</h2> <el-timeline> <el-timeline-item v-for="(item, index) in items" :key="index" :timestamp="item.time" placement="top" > <el-card> <h4>{{ item.title }}</h4> <p>{{ item.content }}</p> <el-button v-if="item.link" type="text" @click="openLink(item.link)" > 查看详情 </el-button> </el-card> </el-timeline-item> </el-timeline> </div> </template> <script setup> import { defineProps } from 'vue'; const props = defineProps({ items: { type: Array, default: () => [] } }); const openLink = (link) => { window.open(link, '_blank'); }; </script> <style scoped> .announcements { margin-bottom: 40px; } .el-timeline { padding-left: 20px; } .el-card { margin-bottom: 10px; } .el-card h4 { margin-bottom: 10px; } .el-card p { color: #606266; margin-bottom: 10px; } </style>后端实现(Flask)API服务 (app.py)from flask import Flask, jsonify, request import datetime import random app = Flask(__name__) # 生成湖北省电力数据 def generate_hubei_power_data(): # 基础参数 base_load = 35000 # 基础负荷 (MW) # 当前时间 now = datetime.datetime.now() # 生成24小时负荷曲线 times = [] values = [] for hour in range(24): time_str = f"{hour:02d}:00" times.append(time_str) # 计算负荷值 # 基本日周期模式 daily_factor = 0.7 + 0.6 * abs((hour - 12) / 12) # 工作日/周末影响 weekday_factor = 1.0 if now.weekday() < 5 else 0.85 # 随机波动 random_factor = random.uniform(0.95, 1.05) load = base_load * daily_factor * weekday_factor * random_factor values.append(round(load)) # 当前负荷(取最近一小时) current_load = values[-1] if values else base_load # 最大负荷 max_load = max(values) if values else base_load * 1.2 max_load_time = times[values.index(max_load)] if values else "12:00" # 趋势百分比(模拟) trend_percentage = random.uniform(-3.0, 5.0) # 新能源占比 new_energy_percent = random.uniform(25.0, 32.0) return { "currentLoad": current_load, "maxLoad": max_load, "maxLoadTime": max_load_time, "trendPercentage": trend_percentage, "newEnergyPercent": new_energy_percent, "loadCurve": { "times": times, "values": values } } # 获取通知公告 def get_announcements(): return [ { "id": 1, "title": "系统升级维护通知", "content": "系统将于2023年12月15日凌晨2:00-4:00进行维护升级", "time": "2023-12-10", "link": "https://example.com/notice/upgrade" }, { "id": 2, "title": "新能源补贴政策更新", "content": "2024年湖北省新能源发电补贴政策细则已发布", "time": "2023-12-05", "link": "https://example.com/policy/new-energy" }, { "id": 3, "title": "电力数据报送规范", "content": "关于规范电力企业数据报送格式的通知", "time": "2023-11-28" } ] # 湖北省电力数据API @app.route('/api/province/overview', methods=['GET']) def province_overview(): data = generate_hubei_power_data() return jsonify(data) # 通知公告API @app.route('/api/announcements', methods=['GET']) def announcements(): return jsonify(get_announcements()) # 文件上传API @app.route('/api/upload', methods=['POST']) def upload_file(): # 在实际项目中,这里会处理文件保存和解析 # 这里只返回成功响应 return jsonify({ "success": True, "message": "文件上传成功,数据正在处理中", "filename": request.files['file'].filename if 'file' in request.files else "unknown" }) # 数据模板下载 @app.route('/template.csv') def download_template(): # 在实际项目中,这里会返回真实的模板文件 # 这里只返回一个示例响应 return "日期,时间,发电量(MWh)\n2023-12-01,00:00,1200\n2023-12-01,01:00,1150", { 'Content-Type': 'text/csv', 'Content-Disposition': 'attachment; filename=power_data_template.csv' } if __name__ == '__main__': app.run(debug=True)API服务封装 (services/api.js)import axios from 'axios'; const api = axios.create({ baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:5000', timeout: 10000 }); export const fetchProvinceData = async () => { try { const response = await api.get('/api/province/overview'); return response.data; } catch (error) { console.error('获取湖北省数据失败:', error); return {}; } }; export const fetchAnnouncements = async () => { try { const response = await api.get('/api/announcements'); return response.data; } catch (error) { console.error('获取通知公告失败:', error); return []; } }; export const uploadPowerData = async (file) => { const formData = new FormData(); formData.append('file', file); try { const response = await api.post('/api/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); return response.data; } catch (error) { console.error('上传失败:', error); return { success: false, message: error.response?.data?.message || '上传失败' }; } }; export const downloadTemplate = async () => { try { const response = await api.get('/template.csv', { responseType: 'blob' }); // 创建下载链接 const url = window.URL.createObjectURL(new Blob([response.data])); const link = document.createElement('a'); link.href = url; link.setAttribute('download', 'power_data_template.csv'); document.body.appendChild(link); link.click(); document.body.removeChild(link); return true; } catch (error) { console.error('下载模板失败:', error); return false; } };设计特点与优势1. 湖北省电力概览实时数据展示:使用预测模型生成合理的电力数据关键指标卡片:突出显示核心指标动态负荷曲线:展示24小时负荷变化趋势趋势指示器:直观显示负荷变化方向2. 功能导航区清晰入口:三个核心功能一目了然状态提示:未上传数据时显示提示标签交互反馈:悬停效果提升用户体验权限控制:未上传数据时限制功能访问3. 数据上传引导状态反馈:清晰显示上传状态操作指引:详细的上传指南模板下载:提供标准数据模板文件验证:前端文件类型和大小检查4. 通知公告区时间线展示:清晰的时间顺序分类通知:系统、政策、规范等分类详情链接:支持跳转查看完整信息技术实现要点1. 数据生成策略时间序列分析:基于时间特征生成合理负荷曲线工作日/周末区分:考虑不同日期的用电模式随机波动:添加合理随机性模拟真实数据新能源占比:反映湖北省能源结构特点2. 前端交互优化响应式设计:适配不同屏幕尺寸图表按需渲染:ECharts只在数据加载后初始化本地状态管理:使用localStorage保存用户上传状态错误处理:完善的API错误捕获和反馈3. 后端API设计模拟数据API:动态生成湖北省电力数据文件上传处理:支持CSV/Excel格式模板下载:提供标准数据模板通知公告API:返回结构化通知数据总结这个普通用户首页设计完全满足您的需求:湖北省电力概览:使用预测模型生成合理数据展示关键指标和负荷曲线无需真实数据源功能导航:三个核心功能入口状态提示引导用户操作权限控制确保数据可用性数据上传引导:清晰的上传状态详细的操作指南模板下载支持通知公告:最新系统通知政策更新提醒规范指南整个系统采用Vue3 + Element Plus + Flask技术栈,前后端分离设计,便于开发和维护。前端组件化设计提高复用性,后端API简洁高效,特别适合实训项目开发。帮我生成一个普通用户的首页的前端
我将为您生成一个完整的普通用户首页前端代码,基于Vue3 + Element Plus,展示湖北省电力概况并引导用户上传数据。
我已经为您生成了一个完整的普通用户首页前端代码,基于Vue3 + Element Plus技术栈。这个首页包含了您要求的所有功能:
这个首页完全满足您的需求,展示湖北省电力概况,引导用户上传数据,并提供清晰的功能导航。所有数据都是通过算法生成的模拟数据,无需真实数据源,非常适合实训项目使用。
To configure the generation, complete these steps:
普通用户首页设计思路(Vue3 + Element Plus + Flask)基于您的需求,我将设计一个既能展示湖北省电力概况,又能引导用户上传个人数据的普通用户首页。这个设计将包含四个核心区域,并采用模拟数据展示全省电力情况。设计思路graph TD A[普通用户首页] --> B[湖北省电力概览] A --> C[功能导航区] A --> D[数据上传引导] A --> E[通知公告区] B --> B1[关键指标卡片] B --> B2[负荷曲线图] C --> C1[我的电厂] C --> C2[数据分析] C --> C3[负荷预测] D --> D1[上传状态] D --> D2[上传按钮] D --> D3[操作指南] E --> E1[系统通知] E --> E2[政策更新]前端实现(Vue3 + Element Plus)文件结构src/ ├── views/ │ └── UserHome.vue ├── components/ │ ├── ProvinceOverview.vue │ ├── FunctionCards.vue │ ├── DataUploadGuide.vue │ └── Announcements.vue ├── services/ │ └── api.js主页面组件 (UserHome.vue)<template> <div class="user-home"> <!-- 湖北省电力概览 --> <ProvinceOverview :data="provinceData" /> <!-- 功能导航区 --> <FunctionCards :hasData="hasUserData" @navigate="handleNavigation" /> <!-- 数据上传引导 --> <DataUploadGuide :hasData="hasUserData" :lastUpload="lastUploadTime" @upload-success="handleUploadSuccess" /> <!-- 通知公告区 --> <Announcements :items="announcements" /> </div> </template> <script> import { ref, onMounted } from 'vue'; import { useRouter } from 'vue-router'; import ProvinceOverview from '@/components/ProvinceOverview.vue'; import FunctionCards from '@/components/FunctionCards.vue'; import DataUploadGuide from '@/components/DataUploadGuide.vue'; import Announcements from '@/components/Announcements.vue'; import { fetchProvinceData, fetchAnnouncements } from '@/services/api'; export default { components: { ProvinceOverview, FunctionCards, DataUploadGuide, Announcements }, setup() { const router = useRouter(); const provinceData = ref({}); const announcements = ref([]); const hasUserData = ref(false); const lastUploadTime = ref(''); // 获取湖北省电力数据和通知公告 const loadData = async () => { try { provinceData.value = await fetchProvinceData(); announcements.value = await fetchAnnouncements(); // 检查用户是否有上传数据 hasUserData.value = localStorage.getItem('hasUserData') === 'true'; lastUploadTime.value = localStorage.getItem('lastUploadTime') || ''; } catch (error) { console.error('数据加载失败:', error); } }; // 处理上传成功 const handleUploadSuccess = () => { hasUserData.value = true; lastUploadTime.value = new Date().toLocaleString(); localStorage.setItem('hasUserData', 'true'); localStorage.setItem('lastUploadTime', lastUploadTime.value); }; // 处理导航 const handleNavigation = (route) => { if (!hasUserData.value) { ElMessage.warning('请先上传电厂数据'); return; } router.push(route); }; onMounted(loadData); return { provinceData, announcements, hasUserData, lastUploadTime, handleUploadSuccess, handleNavigation }; } }; </script> <style scoped> .user-home { max-width: 1200px; margin: 0 auto; padding: 20px; } .section { margin-bottom: 40px; } </style>湖北省电力概览组件 (ProvinceOverview.vue)<template> <div class="province-overview"> <h2 class="section-title">湖北省电力实时概览</h2> <div class="stats-cards"> <el-card class="stat-card"> <div class="card-content"> <h3>当前全省负荷</h3> <p class="value">{{ data.currentLoad?.toLocaleString() || '--' }} <span>MW</span></p> <p class="trend" :class="trendClass"> <el-icon><Top v-if="isIncreasing" /><Bottom v-else /></el-icon> {{ trendPercentage }}% 较昨日 </p> </div> </el-card> <el-card class="stat-card"> <div class="card-content"> <h3>今日最大负荷</h3> <p class="value">{{ data.maxLoad?.toLocaleString() || '--' }} <span>MW</span></p> <p class="time">{{ data.maxLoadTime || '--' }} 出现</p> </div> </el-card> <el-card class="stat-card"> <div class="card-content"> <h3>新能源占比</h3> <p class="value">{{ data.newEnergyPercent || '--' }} <span>%</span></p> <el-progress :percentage="data.newEnergyPercent || 0" :status="data.newEnergyPercent > 25 ? 'success' : 'warning'" /> </div> </el-card> </div> <div class="load-chart"> <div ref="chart" style="height: 300px;"></div> </div> </div> </template> <script> import { ref, watch, computed } from 'vue'; import * as echarts from 'echarts'; export default { props: { data: { type: Object, default: () => ({}) } }, setup(props) { const chart = ref(null); let chartInstance = null; // 计算趋势 const trendPercentage = computed(() => { if (!props.data.trendPercentage) return '--'; return Math.abs(props.data.trendPercentage).toFixed(1); }); const isIncreasing = computed(() => { return props.data.trendPercentage > 0; }); const trendClass = computed(() => { return isIncreasing.value ? 'increase' : 'decrease'; }); // 初始化图表 const initChart = () => { if (!props.data.loadCurve) return; if (!chartInstance) { chartInstance = echarts.init(chart.value); } const option = { title: { text: '湖北省24小时负荷曲线', left: 'center' }, tooltip: { trigger: 'axis' }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'category', data: props.data.loadCurve.times }, yAxis: { type: 'value', name: '负荷 (MW)' }, series: [ { name: '电力负荷', type: 'line', data: props.data.loadCurve.values, smooth: true, lineStyle: { width: 3, color: '#36a2eb' }, itemStyle: { color: '#36a2eb' }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: 'rgba(54, 162, 235, 0.5)' }, { offset: 1, color: 'rgba(54, 162, 235, 0.1)' } ]) } } ] }; chartInstance.setOption(option); }; // 监听数据变化 watch(() => props.data, (newData) => { if (newData.loadCurve) { initChart(); } }, { deep: true }); return { chart, trendPercentage, isIncreasing, trendClass }; } }; </script> <style scoped> .province-overview { margin-bottom: 40px; } .section-title { margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #eee; } .stats-cards { display: flex; gap: 20px; margin-bottom: 30px; } .stat-card { flex: 1; } .card-content { text-align: center; padding: 15px; } .card-content h3 { margin-bottom: 10px; font-size: 16px; color: #606266; } .card-content .value { font-size: 24px; font-weight: bold; margin: 10px 0; } .card-content .value span { font-size: 14px; color: #909399; } .card-content .trend { display: flex; align-items: center; justify-content: center; gap: 5px; font-size: 14px; } .card-content .trend.increase { color: #67c23a; } .card-content .trend.decrease { color: #f56c6c; } .card-content .time { color: #909399; font-size: 14px; } .load-chart { background: white; padding: 20px; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } </style>功能导航组件 (FunctionCards.vue)<template> <div class="function-cards"> <h2 class="section-title">我的电力服务</h2> <div class="cards-container"> <el-card class="function-card" @click="emit('navigate', '/my-plant')"> <div class="card-content"> <el-icon :size="48"><OfficeBuilding /></el-icon> <h3>我的电厂</h3> <p>管理电厂数据与报告</p> <el-tag v-if="!hasData" type="warning" size="small">需上传数据</el-tag> </div> </el-card> <el-card class="function-card" @click="emit('navigate', '/analysis')"> <div class="card-content"> <el-icon :size="48"><DataAnalysis /></el-icon> <h3>数据分析</h3> <p>深入分析电厂运行数据</p> <el-tag v-if="!hasData" type="warning" size="small">需上传数据</el-tag> </div> </el-card> <el-card class="function-card" @click="emit('navigate', '/prediction')"> <div class="card-content"> <el-icon :size="48"><TrendCharts /></el-icon> <h3>负荷预测</h3> <p>获取未来用电预测</p> <el-tag v-if="!hasData" type="warning" size="small">需上传数据</el-tag> </div> </el-card> </div> </div> </template> <script setup> import { defineProps, defineEmits } from 'vue'; import { OfficeBuilding, DataAnalysis, TrendCharts } from '@element-plus/icons-vue'; const props = defineProps({ hasData: Boolean }); const emit = defineEmits(['navigate']); </script> <style scoped> .function-cards { margin-bottom: 40px; } .cards-container { display: flex; gap: 20px; } .function-card { flex: 1; cursor: pointer; transition: transform 0.3s, box-shadow 0.3s; } .function-card:hover { transform: translateY(-5px); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15); } .card-content { text-align: center; padding: 20px; } .card-content h3 { margin: 15px 0 10px; } .card-content p { color: #909399; font-size: 14px; margin-bottom: 10px; } </style>数据上传引导组件 (DataUploadGuide.vue)<template> <div class="data-upload"> <h2 class="section-title">电厂数据管理</h2> <div class="upload-container"> <div class="upload-status"> <el-icon :size="64" :color="hasData ? '#67c23a' : '#409EFF'"> <UploadFilled /> </el-icon> <div class="status-text"> <h3 v-if="hasData">已上传最新数据</h3> <h3 v-else>您的电厂数据待上传</h3> <p v-if="hasData">最后上传时间: {{ lastUpload }}</p> <p v-else>上传电厂数据以获取个性化分析</p> </div> </div> <el-upload class="upload-action" action="/api/upload" :show-file-list="false" :on-success="handleSuccess" :before-upload="beforeUpload" > <el-button type="primary" size="large"> <el-icon><Upload /></el-icon> 上传电厂数据 </el-button> <template #tip> <div class="upload-tip"> 支持格式: CSV, Excel (最大100MB) </div> </template> </el-upload> </div> <div class="upload-guide"> <el-collapse> <el-collapse-item title="数据上传指南"> <div class="guide-content"> <p>1. <a href="/template.csv" download>下载数据模板</a>,按格式准备数据</p> <p>2. 数据应包含日期、时间、发电量等关键字段</p> <p>3. 支持CSV和Excel格式文件,最大100MB</p> <p>4. 每日数据推荐在18:00前上传</p> <p>5. 系统会自动处理数据格式问题</p> </div> </el-collapse-item> </el-collapse> </div> </div> </template> <script setup> import { defineProps, defineEmits } from 'vue'; import { ElMessage } from 'element-plus'; import { UploadFilled, Upload } from '@element-plus/icons-vue'; const props = defineProps({ hasData: Boolean, lastUpload: String }); const emit = defineEmits(['upload-success']); const beforeUpload = (file) => { const validTypes = [ 'text/csv', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ]; if (!validTypes.includes(file.type)) { ElMessage.error('只支持CSV和Excel格式文件'); return false; } const maxSize = 100 * 1024 * 1024; // 100MB if (file.size > maxSize) { ElMessage.error('文件大小不能超过100MB'); return false; } return true; }; const handleSuccess = (response) => { if (response.success) { ElMessage.success('数据上传成功'); emit('upload-success'); } else { ElMessage.error(`上传失败: ${response.message}`); } }; </script> <style scoped> .data-upload { margin-bottom: 40px; } .upload-container { display: flex; align-items: center; justify-content: space-between; padding: 30px; background: #f5f7fa; border-radius: 4px; margin-bottom: 20px; } .upload-status { display: flex; align-items: center; gap: 20px; } .status-text h3 { margin-bottom: 5px; } .status-text p { color: #909399; } .upload-action { text-align: center; } .upload-tip { margin-top: 10px; color: #909399; font-size: 14px; } .guide-content { padding: 10px; } .guide-content p { margin: 8px 0; } .guide-content a { color: #409EFF; text-decoration: none; } </style>通知公告组件 (Announcements.vue)<template> <div class="announcements"> <h2 class="section-title">最新通知公告</h2> <el-timeline> <el-timeline-item v-for="(item, index) in items" :key="index" :timestamp="item.time" placement="top" > <el-card> <h4>{{ item.title }}</h4> <p>{{ item.content }}</p> <el-button v-if="item.link" type="text" @click="openLink(item.link)" > 查看详情 </el-button> </el-card> </el-timeline-item> </el-timeline> </div> </template> <script setup> import { defineProps } from 'vue'; const props = defineProps({ items: { type: Array, default: () => [] } }); const openLink = (link) => { window.open(link, '_blank'); }; </script> <style scoped> .announcements { margin-bottom: 40px; } .el-timeline { padding-left: 20px; } .el-card { margin-bottom: 10px; } .el-card h4 { margin-bottom: 10px; } .el-card p { color: #606266; margin-bottom: 10px; } </style>后端实现(Flask)API服务 (app.py)from flask import Flask, jsonify, request import datetime import random app = Flask(__name__) # 生成湖北省电力数据 def generate_hubei_power_data(): # 基础参数 base_load = 35000 # 基础负荷 (MW) # 当前时间 now = datetime.datetime.now() # 生成24小时负荷曲线 times = [] values = [] for hour in range(24): time_str = f"{hour:02d}:00" times.append(time_str) # 计算负荷值 # 基本日周期模式 daily_factor = 0.7 + 0.6 * abs((hour - 12) / 12) # 工作日/周末影响 weekday_factor = 1.0 if now.weekday() < 5 else 0.85 # 随机波动 random_factor = random.uniform(0.95, 1.05) load = base_load * daily_factor * weekday_factor * random_factor values.append(round(load)) # 当前负荷(取最近一小时) current_load = values[-1] if values else base_load # 最大负荷 max_load = max(values) if values else base_load * 1.2 max_load_time = times[values.index(max_load)] if values else "12:00" # 趋势百分比(模拟) trend_percentage = random.uniform(-3.0, 5.0) # 新能源占比 new_energy_percent = random.uniform(25.0, 32.0) return { "currentLoad": current_load, "maxLoad": max_load, "maxLoadTime": max_load_time, "trendPercentage": trend_percentage, "newEnergyPercent": new_energy_percent, "loadCurve": { "times": times, "values": values } } # 获取通知公告 def get_announcements(): return [ { "id": 1, "title": "系统升级维护通知", "content": "系统将于2023年12月15日凌晨2:00-4:00进行维护升级", "time": "2023-12-10", "link": "https://example.com/notice/upgrade" }, { "id": 2, "title": "新能源补贴政策更新", "content": "2024年湖北省新能源发电补贴政策细则已发布", "time": "2023-12-05", "link": "https://example.com/policy/new-energy" }, { "id": 3, "title": "电力数据报送规范", "content": "关于规范电力企业数据报送格式的通知", "time": "2023-11-28" } ] # 湖北省电力数据API @app.route('/api/province/overview', methods=['GET']) def province_overview(): data = generate_hubei_power_data() return jsonify(data) # 通知公告API @app.route('/api/announcements', methods=['GET']) def announcements(): return jsonify(get_announcements()) # 文件上传API @app.route('/api/upload', methods=['POST']) def upload_file(): # 在实际项目中,这里会处理文件保存和解析 # 这里只返回成功响应 return jsonify({ "success": True, "message": "文件上传成功,数据正在处理中", "filename": request.files['file'].filename if 'file' in request.files else "unknown" }) # 数据模板下载 @app.route('/template.csv') def download_template(): # 在实际项目中,这里会返回真实的模板文件 # 这里只返回一个示例响应 return "日期,时间,发电量(MWh)\n2023-12-01,00:00,1200\n2023-12-01,01:00,1150", { 'Content-Type': 'text/csv', 'Content-Disposition': 'attachment; filename=power_data_template.csv' } if __name__ == '__main__': app.run(debug=True)API服务封装 (services/api.js)import axios from 'axios'; const api = axios.create({ baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:5000', timeout: 10000 }); export const fetchProvinceData = async () => { try { const response = await api.get('/api/province/overview'); return response.data; } catch (error) { console.error('获取湖北省数据失败:', error); return {}; } }; export const fetchAnnouncements = async () => { try { const response = await api.get('/api/announcements'); return response.data; } catch (error) { console.error('获取通知公告失败:', error); return []; } }; export const uploadPowerData = async (file) => { const formData = new FormData(); formData.append('file', file); try { const response = await api.post('/api/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); return response.data; } catch (error) { console.error('上传失败:', error); return { success: false, message: error.response?.data?.message || '上传失败' }; } }; export const downloadTemplate = async () => { try { const response = await api.get('/template.csv', { responseType: 'blob' }); // 创建下载链接 const url = window.URL.createObjectURL(new Blob([response.data])); const link = document.createElement('a'); link.href = url; link.setAttribute('download', 'power_data_template.csv'); document.body.appendChild(link); link.click(); document.body.removeChild(link); return true; } catch (error) { console.error('下载模板失败:', error); return false; } };设计特点与优势1. 湖北省电力概览实时数据展示:使用预测模型生成合理的电力数据关键指标卡片:突出显示核心指标动态负荷曲线:展示24小时负荷变化趋势趋势指示器:直观显示负荷变化方向2. 功能导航区清晰入口:三个核心功能一目了然状态提示:未上传数据时显示提示标签交互反馈:悬停效果提升用户体验权限控制:未上传数据时限制功能访问3. 数据上传引导状态反馈:清晰显示上传状态操作指引:详细的上传指南模板下载:提供标准数据模板文件验证:前端文件类型和大小检查4. 通知公告区时间线展示:清晰的时间顺序分类通知:系统、政策、规范等分类详情链接:支持跳转查看完整信息技术实现要点1. 数据生成策略时间序列分析:基于时间特征生成合理负荷曲线工作日/周末区分:考虑不同日期的用电模式随机波动:添加合理随机性模拟真实数据新能源占比:反映湖北省能源结构特点2. 前端交互优化响应式设计:适配不同屏幕尺寸图表按需渲染:ECharts只在数据加载后初始化本地状态管理:使用localStorage保存用户上传状态错误处理:完善的API错误捕获和反馈3. 后端API设计模拟数据API:动态生成湖北省电力数据文件上传处理:支持CSV/Excel格式模板下载:提供标准数据模板通知公告API:返回结构化通知数据总结这个普通用户首页设计完全满足您的需求:湖北省电力概览:使用预测模型生成合理数据展示关键指标和负荷曲线无需真实数据源功能导航:三个核心功能入口状态提示引导用户操作权限控制确保数据可用性数据上传引导:清晰的上传状态详细的操作指南模板下载支持通知公告:最新系统通知政策更新提醒规范指南整个系统采用Vue3 + Element Plus + Flask技术栈,前后端分离设计,便于开发和维护。前端组件化设计提高复用性,后端API简洁高效,特别适合实训项目开发。我把项目分成六个地区可供选择了,分别是华北,华南等,请给我一个新的首页设计思路,并给我一个管理员模型运维页面的设计思路,还是用vue3+elementplus+flask已深度思考(用时3分50秒)我们重新设计两个页面:普通用户首页(支持选择六大地区)和管理员模型运维页面。普通用户首页设计思路(支持地区选择)页面布局调整:顶部地区选择器:使用下拉菜单让用户选择六个大区(华北、华东、华中、华南、西南、东北)动态数据展示:根据选择的大区展示该区域的电力数据概览保持原有四个核心区域,但数据内容随地区变化文件结构调整:新增一个地区选择组件,其他组件需要根据选择的地区重新加载数据。关键代码变更:新增地区选择组件 (RegionSelector.vue)<template> <div class="region-selector"> <el-select v-model="selectedRegion" placeholder="请选择地区" @change="handleChange" size="large" > <el-option v-for="region in regions" :key="region.value" :label="region.label" :value="region.value" /> </el-select> </div> </template> <script setup> import { ref, defineEmits } from 'vue' const regions = ref([ { value: 'north', label: '华北' }, { value: 'east', label: '华东' }, { value: 'central', label: '华中' }, { value: 'south', label: '华南' }, { value: 'southwest', label: '西南' }, { value: 'northeast', label: '东北' } ]) const selectedRegion = ref('central') // 默认选中华中(湖北属于华中) const emit = defineEmits(['region-change']) const handleChange = (value) => { emit('region-change', value) } </script> <style scoped> .region-selector { margin-bottom: 20px; } </style>修改UserHome.vue<template> <div class="user-home"> <!-- 顶部地区选择器 --> <RegionSelector @region-change="handleRegionChange" /> <!-- 湖北省电力概览 -> 现在改为地区电力概览 --> <ProvinceOverview :data="regionData" :region="currentRegion" /> <!-- ... 其他组件保持不变,但部分需要传递region参数 ... --> </div> </template> <script> // 导入RegionSelector组件 import RegionSelector from '@/components/RegionSelector.vue' // ... 其他导入 export default { components: { RegionSelector, // ... 其他组件 }, setup() { const currentRegion = ref('central'); // 默认华中 const regionData = ref({}); // 监听地区变化 const handleRegionChange = (region) => { currentRegion.value = region; loadRegionData(region); }; // 加载地区数据 const loadRegionData = async (region) => { try { regionData.value = await fetchRegionData(region); } catch (error) { console.error('地区数据加载失败:', error); } }; // 初始化 onMounted(() => { loadRegionData(currentRegion.value); }); return { // ... 其他返回 regionData, currentRegion, handleRegionChange }; } } </script>修改ProvinceOverview.vue组件将标题改为动态显示:<h2 class="section-title">{{regionLabel}}电力实时概览</h2>通过props传入region,然后根据region显示对应的地区名称后端API调整修改/api/province/overview为/api/region/overview,增加region参数根据region参数生成不同区域的模拟数据管理员模型运维页面设计思路页面功能需求:模型概览:展示当前使用的模型版本、状态和性能指标模型部署:上传新模型文件并部署模型测试:上传测试数据进行模型效果验证历史版本管理:查看和回滚历史模型版本训练数据管理:查看和更新训练数据集页面布局设计:graph TB A[管理员模型运维] --> B[模型概览] A --> C[模型部署] A --> D[模型测试] A --> E[历史版本] A --> F[训练数据]文件结构:src/ ├── views/ │ └── AdminModel.vue ├── components/ │ ├── admin/ │ │ ├── ModelOverview.vue │ │ ├── ModelDeploy.vue │ │ ├── ModelTesting.vue │ │ ├── ModelHistory.vue │ │ └── TrainingData.vue模型概览组件 (ModelOverview.vue)<template> <el-card class="model-overview"> <h2>模型概览</h2> <el-descriptions :column="2" border> <el-descriptions-item label="模型名称">电力负荷预测模型</el-descriptions-item> <el-descriptions-item label="当前版本">v2.3.1</el-descriptions-item> <el-descriptions-item label="状态"> <el-tag type="success">运行中</el-tag> </el-descriptions-item> <el-descriptions-item label="更新时间">2023-11-15 14:30</el-descriptions-item> <el-descriptions-item label="MAPE"> <span class="highlight">8.3%</span> </el-descriptions-item> <el-descriptions-item label="RMSE"> <span class="highlight">132.4 MW</span> </el-descriptions-item> </el-descriptions> <div class="model-chart"> <div id="metric-chart" style="height: 300px;"></div> </div> </el-card> </template> <script setup> import { onMounted } from 'vue'; import * as echarts from 'echarts'; onMounted(() => { // 渲染指标趋势图 const chart = echarts.init(document.getElementById('metric-chart')); chart.setOption({ title: { text: '模型性能指标趋势' }, tooltip: { trigger: 'axis' }, legend: { data: ['MAPE', 'RMSE'] }, xAxis: { data: ['v2.0', 'v2.1', 'v2.2', 'v2.3'] }, yAxis: [ { type: 'value', name: 'MAPE (%)', min: 0 }, { type: 'value', name: 'RMSE (MW)', min: 0 } ], series: [ { name: 'MAPE', type: 'line', data: [10.2, 9.1, 8.5, 8.3], smooth: true, lineStyle: { width: 3 } }, { name: 'RMSE', type: 'line', yAxisIndex: 1, data: [180, 162, 145, 132.4], smooth: true, lineStyle: { width: 3, type: 'dashed' } } ] }); }); </script> <style scoped> .model-overview { margin-bottom: 20px; } .highlight { font-weight: bold; color: #409EFF; } .model-chart { margin-top: 30px; } </style>模型部署组件 (ModelDeploy.vue)<template> <el-card class="model-deploy"> <h2>模型部署</h2> <el-steps :active="activeStep" align-center finish-status="success"> <el-step title="上传模型" description="上传模型文件" /> <el-step title="验证模型" description="验证模型有效性" /> <el-step title="部署模型" description="部署到生产环境" /> </el-steps> <div v-if="activeStep === 0" class="step-container"> <el-upload class="upload-model" drag action="/api/model/upload" :before-upload="beforeUpload" :on-success="handleUploadSuccess" :show-file-list="false" > <el-icon :size="60"><UploadFilled /></el-icon> <div class="el-upload__text"> 拖拽模型文件到此处,或 <em>点击上传</em> </div> <template #tip> <div class="upload-tip"> 支持格式: .pkl, .h5, .pt (最大1GB) </div> </template> </el-upload> <div v-if="uploadedFile" class="upload-info"> <el-icon><Document /></el-icon> <span>{{ uploadedFile }}</span> </div> </div> <div v-else-if="activeStep === 1" class="step-container"> <div class="step-title">模型验证</div> <div class="step-desc">已自动选择验证数据集,点击开始验证</div> <el-button type="primary" @click="runValidation" :loading="validating" > 开始验证 </el-button> <div v-if="validationResult" class="validation-result"> <el-descriptions :column="1" border> <el-descriptions-item label="MAPE"> {{ validationResult.mape }}% </el-descriptions-item> <el-descriptions-item label="RMSE"> {{ validationResult.rmse }} MW </el-descriptions-item> <el-descriptions-item label="通过标准"> <el-tag :type="validationResult.passed ? 'success' : 'danger'"> {{ validationResult.passed ? '验证通过' : '验证失败' }} </el-tag> </el-descriptions-item> </el-descriptions> </div> </div> <div v-else class="step-container"> <div class="step-title">模型部署</div> <div class="step-desc">确认将验证通过的模型部署到生产环境</div> <el-button type="primary" @click="deployModel" :loading="deploying" :disabled="!canDeploy" > 部署模型 </el-button> <div v-if="deployResult" class="deploy-result"> <el-result :icon="deployResult.success ? 'success' : 'error'"> <template #title> {{ deployResult.title }} </template> <template #sub-title> {{ deployResult.subtitle }} </template> <template #extra> <el-button @click="resetProcess">继续部署</el-button> </template> </el-result> </div> </div> </el-card> </template> <script setup> import { ref } from 'vue' import { ElMessage } from 'element-plus' import { UploadFilled, Document } from '@element-plus/icons-vue' const activeStep = ref(0) const uploadedFile = ref(null) const validating = ref(false) const validationResult = ref(null) const deploying = ref(false) const deployResult = ref(null) const canDeploy = computed(() => { return validationResult.value?.passed }) const beforeUpload = (file) => { const validTypes = ['.pkl', '.h5', '.pt'] const fileExt = file.name.substring(file.name.lastIndexOf('.')).toLowerCase() if (!validTypes.includes(fileExt)) { ElMessage.error('只支持模型格式文件: .pkl, .h5, .pt') return false } const maxSize = 1 * 1024 * 1024 * 1024 // 1GB if (file.size > maxSize) { ElMessage.error('模型文件不能超过1GB') return false } return true } const handleUploadSuccess = (response) => { if (response.success) { uploadedFile.value = response.filename activeStep.value = 1 ElMessage.success('模型上传成功') } else { ElMessage.error(`上传失败: ${response.message}`) } } const runValidation = () => { validating.value = true // 模拟验证过程 setTimeout(() => { validating.value = false validationResult.value = { mape: (Math.random() * 3 + 7).toFixed(1), rmse: (Math.random() * 50 + 120).toFixed(1), passed: Math.random() > 0.3 // 70%通过率 } if (validationResult.value.passed) { ElMessage.success('模型验证通过!') activeStep.value = 2 } else { ElMessage.error('模型验证未通过!') } }, 3000) } const deployModel = () => { deploying.value = true // 模拟部署 setTimeout(() => { deploying.value = false // 80%部署成功率 const success = Math.random() > 0.2 deployResult.value = success ? { success: true, title: '部署成功', subtitle: '新模型已成功部署到生产环境', } : { success: false, title: '部署失败', subtitle: '部署过程中出错,请检查日志' } }, 2000) } const resetProcess = () => { activeStep.value = 0 uploadedFile.value = null validationResult.value = null deployResult.value = null } </script> <style scoped> .model-deploy { margin-bottom: 20px; } .step-container { padding: 30px; text-align: center; } .step-title { font-size: 18px; margin-bottom: 10px; } .step-desc { color: #606266; margin-bottom: 20px; } .upload-tip { margin-top: 15px; color: #606266; font-size: 14px; } .upload-info { margin-top: 20px; display: flex; align-items: center; justify-content: center; gap: 10px; color: #606266; } .validation-result, .deploy-result { margin-top: 30px; max-width: 500px; margin-left: auto; margin-right: auto; } </style>模型测试组件 (ModelTesting.vue)<template> <el-card class="model-testing"> <h2>模型测试</h2> <div class="testing-actions"> <el-upload class="upload-data" action="/api/upload" :before-upload="beforeTestUpload" :show-file-list="false" :on-success="handleTestUploadSuccess" > <el-button type="primary"> <el-icon><Upload /></el-icon>上传测试数据 </el-button> <template #tip> <div class="upload-tip"> 支持格式: CSV (最大100MB) </div> </template> </el-upload> <el-button type="success" @click="runModelTest" :disabled="!testDataReady" > 运行测试 </el-button> </div> <div v-if="testResult" class="test-results"> <div class="result-metrics"> <el-row :gutter="20"> <el-col :span="12"> <el-statistic title="测试集MAPE" :value="testResult.mape"> <template #suffix>%</template> </el-statistic> </el-col> <el-col :span="12"> <el-statistic title="测试集RMSE" :value="testResult.rmse"> <template #suffix>MW</template> </el-statistic> </el-col> </el-row> </div> <div class="result-chart"> <div id="result-chart" style="height: 350px;"></div> </div> </div> </el-card> </template> <script setup> import { ref, onMounted } from 'vue' import { ElMessage } from 'element-plus' import { Upload } from '@element-plus/icons-vue' import * as echarts from 'echarts' const testDataReady = ref(false) const testResult = ref(null) const chartInstance = ref(null) const beforeTestUpload = (file) => { // 验证逻辑... return true } const handleTestUploadSuccess = () => { testDataReady.value = true ElMessage.success('测试数据上传成功') } const runModelTest = () => { // 模拟测试运行 const mockData = { times: Array.from({ length: 24 }, (_, i) => `${i}:00`), actual: Array.from({ length: 24 }, () => Math.floor(Math.random() * 500) + 1000), predicted: Array.from({ length: 24 }, () => Math.floor(Math.random() * 500) + 1000) } testResult.value = { mape: (Math.random() * 5 + 5).toFixed(2), rmse: (Math.random() * 80 + 100).toFixed(1), chartData: mockData } // 渲染图表 nextTick(() => { renderResultChart() }) } const renderResultChart = () => { if (!testResult.value) return const chart = chartInstance.value || echarts.init(document.getElementById('result-chart')) chartInstance.value = chart const option = { title: { text: '预测结果对比' }, tooltip: { trigger: 'axis' }, legend: { data: ['实际负荷', '预测负荷'] }, xAxis: { data: testResult.value.chartData.times }, yAxis: { name: '负荷 (MW)' }, series: [ { name: '实际负荷', type: 'line', data: testResult.value.chartData.actual, smooth: true }, { name: '预测负荷', type: 'line', data: testResult.value.chartData.predicted, smooth: true, lineStyle: { type: 'dashed' } } ] } chart.setOption(option) } onMounted(() => { // 初始化图表实例 if (!chartInstance.value && document.getElementById('result-chart')) { chartInstance.value = echarts.init(document.getElementById('result-chart')) } }) </script> <style scoped> .model-testing { margin-bottom: 20px; } .testing-actions { display: flex; gap: 20px; margin-bottom: 30px; } .upload-tip { margin-top: 10px; color: #606266; font-size: 14px; } .result-metrics { margin-bottom: 30px; } .result-chart { background: white; padding: 20px; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } </style>历史版本组件 (ModelHistory.vue)<template> <el-card class="model-history"> <h2>历史版本</h2> <el-table :data="versions" stripe style="width: 100%"> <el-table-column prop="version" label="版本号" width="120" /> <el-table-column prop="date" label="部署时间" width="180" /> <el-table-column label="性能指标"> <template #default="scope"> <div>MAPE: {{ scope.row.mape }}%</div> <div>RMSE: {{ scope.row.rmse }} MW</div> </template> </el-table-column> <el-table-column prop="status" label="状态" width="100"> <template #default="scope"> <el-tag :type="scope.row.active ? 'success' : 'info'"> {{ scope.row.active ? '活跃中' : '历史版本' }} </el-tag> </template> </el-table-column> <el-table-column label="操作"> <template #default="scope"> <el-button size="small" @click="downloadVersion(scope.row)" :disabled="scope.row.active" > 下载 </el-button> <el-button type="primary" size="small" @click="rollbackVersion(scope.row)" :disabled="scope.row.active" > 回滚 </el-button> </template> </el-table-column> </el-table> </el-card> </template> <script setup> import { ref } from 'vue' import { ElMessageBox } from 'element-plus' const versions = ref([ { version: 'v2.3.1', date: '2023-11-15 14:30', mape: 8.3, rmse: 132.4, active: true }, { version: 'v2.2.0', date: '2023-10-20 11:15', mape: 8.5, rmse: 145, active: false }, { version: 'v2.1.3', date: '2023-09-10 09:45', mape: 9.1, rmse: 162, active: false }, { version: 'v2.0.0', date: '2023-08-01 16:20', mape: 10.2, rmse: 180, active: false } ]) const downloadVersion = (version) => { console.log('下载模型版本:', version.version) // 实际项目中会有下载API调用 } const rollbackVersion = (version) => { ElMessageBox.confirm( `确认回滚到版本 ${version.version}?`, '模型回滚', { confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning' } ).then(() => { // 模拟回滚操作 versions.value.forEach(v => { v.active = v.version === version.version }) // 显示成功消息 ElMessageBox.alert(`成功回滚到版本 ${version.version}`, '回滚成功', { confirmButtonText: '确认', type: 'success' }) }).catch(() => {}) } </script> <style scoped> .model-history { margin-bottom: 20px; } </style>训练数据管理组件 (TrainingData.vue)<template> <el-card class="training-data"> <h2>训练数据管理</h2> <div class="data-actions"> <el-upload class="upload-data" action="/api/data/upload" :before-upload="beforeDataUpload" :on-success="handleDataUploadSuccess" :show-file-list="false" > <el-button type="primary"> <el-icon><Upload /></el-icon>上传新数据 </el-button> </el-upload> <el-button type="success" @click="updateModelWithNewData"> 使用新数据重新训练 </el-button> </div> <el-table :data="dataVersions" stripe style="width: 100%"> <el-table-column prop="version" label="数据版本" width="120" /> <el-table-column prop="date" label="上传时间" width="180" /> <el-table-column prop="fileSize" label="文件大小" width="120" /> <el-table-column prop="records" label="记录数量" width="100" /> <el-table-column prop="dateRange" label="日期范围" /> <el-table-column label="操作"> <template #default="scope"> <el-button size="small" @click="downloadData(scope.row)"> 下载 </el-button> <el-button size="small" type="danger" @click="deleteData(scope.row)" > 删除 </el-button> </template> </el-table-column> </el-table> </el-card> </template> <script setup> import { ref } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { Upload } from '@element-plus/icons-vue' const dataVersions = ref([ { version: 'v2023-11', date: '2023-11-10', fileSize: '1.2GB', records: '12M', dateRange: '2022-01至2023-10' }, { version: 'v2023-06', date: '2023-06-15', fileSize: '850MB', records: '8.5M', dateRange: '2021-06至2023-05' }, { version: 'v2023-01', date: '2023-01-05', fileSize: '650MB', records: '6.3M', dateRange: '2021-01至2022-12' } ]) const beforeDataUpload = (file) => { // 验证逻辑... return true } const handleDataUploadSuccess = () => { ElMessage.success('数据上传成功') // 刷新数据版本列表 // ... } const downloadData = (data) => { console.log('下载数据:', data.version) // 实际项目中会有下载API调用 } const deleteData = (data) => { ElMessageBox.confirm( `确认删除数据版本 ${data.version}?`, '删除数据', { confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning' } ).then(() => { // 删除操作 // ... ElMessage.success('删除成功') }).catch(() => {}) } const updateModelWithNewData = () => { ElMessageBox.confirm( '重新训练模型可能需要较长时间,确认继续?', '重新训练', { confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning' } ).then(() => { // 开始重新训练 ElMessage.info('开始重新训练模型,请稍候...') // ... }).catch(() => {}) } </script> <style scoped> .training-data { margin-bottom: 20px; } .data-actions { display: flex; gap: 20px; margin-bottom: 20px; } </style>管理员模型页面整合 (AdminModel.vue)<template> <div class="admin-model"> <div class="header"> <h1>模型运维管理</h1> </div> <el-tabs type="border-card"> <el-tab-pane label="模型概览"> <ModelOverview /> </el-tab-pane> <el-tab-pane label="模型部署"> <ModelDeploy /> </el-tab-pane> <el-tab-pane label="模型测试"> <ModelTesting /> </el-tab-pane> <el-tab-pane label="历史版本"> <ModelHistory /> </el-tab-pane> <el-tab-pane label="训练数据"> <TrainingData /> </el-tab-pane> </el-tabs> </div> </template> <script setup> import ModelOverview from '@/components/admin/ModelOverview.vue' import ModelDeploy from '@/components/admin/ModelDeploy.vue' import ModelTesting from '@/components/admin/ModelTesting.vue' import ModelHistory from '@/components/admin/ModelHistory.vue' import TrainingData from '@/components/admin/TrainingData.vue' </script> <style scoped> .admin-model { max-width: 1400px; margin: 0 auto; padding: 20px; } .header { text-align: center; margin-bottom: 30px; } </style>后端API补充需要为管理员功能提供新的API端点:# 模型相关API @app.route('/api/model/upload', methods=['POST']) def upload_model(): # 保存上传的模型文件 # ... return jsonify({ "success": True, "message": "模型文件上传成功", "filename": request.files['file'].filename }) @app.route('/api/model/validate', methods=['POST']) def validate_model(): # 模型验证逻辑 # ... return jsonify({ "success": True, "mape": 8.5, "rmse": 145.2 }) @app.route('/api/model/deploy', methods=['POST']) def deploy_model(): # 模型部署逻辑 # ... return jsonify({ "success": True, "message": "模型部署成功" }) @app.route('/api/model/list', methods=['GET']) def list_models(): # 获取模型版本列表 return jsonify([ {"version": "v2.3", "date": "2023-11-15", "active": True}, # ... 其他版本 ]) @app.route('/api/model/download/<version>', methods=['GET']) def download_model(version): # 提供模型下载 # ...总结这个设计包含了两大部分:1. 普通用户首页支持六大地区选择:华北、华东、华中、华南、西南、东北动态数据展示:根据选择地区显示相应电力数据功能区域保持不变:概览、导航、上传、通知2. 管理员模型运维页面五大功能模块:概览、部署、测试、历史、数据完整模型生命周期管理:上传、验证、部署、测试、训练操作日志记录:记录所有重要操作历史版本控制:支持回滚和下载这个设计在功能上完全满足需求,技术实现上采用Vue3组合式API和Element Plus组件,界面美观且用户友好。后端API为每个管理功能提供了支持。普通用户首页设计(支持六大地区选择)我将基于Vue3 + Element Plus + Flask技术栈,为您设计一个支持六大地区(华北、华东、华中、华南、西南、东北)选择的普通用户首页,并提供管理员模型运维页面的设计方案。普通用户首页设计方案(带地区选择)graph TD A[用户首页] --> B[顶部区域选择器] A --> C[当前地区电力概览] A --> D[功能导航区] A --> E[数据上传引导] A --> F[通知公告区]实现思路1. 顶部区域选择器添加大区选择下拉菜单默认展示"华中"大区(含湖北)选择后更新所有数据展示2. 当前地区电力概览根据选择的大区展示相应电力数据添加大区边界图显示大区关键指标3. 其他功能区功能导航区:根据是否上传数据展示不同状态数据上传引导:引导用户上传电厂数据通知公告区:展示系统通知和政策更新关键代码实现新增/修改文件src/ ├── views/ │ └── UserHome.vue # 主页面 ├── components/ │ ├── RegionSelector.vue # 新增:区域选择器 │ ├── ProvinceOverview.vue # 改为:RegionOverview.vue(地区概览) │ ├── FunctionCards.vue # 功能卡片 │ ├── DataUploadGuide.vue # 数据上传引导 │ └── Announcements.vue # 通知公告 └── services/ └── api.js # 更新API接口RegionSelector.vue (区域选择组件)<template> <div class="region-selector"> <el-select v-model="selectedRegion" placeholder="选择大区" @change="handleChange" size="large" > <el-option v-for="region in regions" :key="region.value" :label="region.label" :value="region.value" /> </el-select> <div v-if="selectedRegion" class="region-info"> <el-icon><Location /></el-icon> <span>{{ regions.find(r => r.value === selectedRegion)?.name }}</span> </div> </div> </template> <script setup> import { ref, defineEmits } from 'vue' import { Location } from '@element-plus/icons-vue' const regions = [ { value: 'north', label: '华北', name: '华北地区(京冀晋蒙津)' }, { value: 'east', label: '华东', name: '华东地区(鲁苏浙皖沪闽赣)' }, { value: 'central', label: '华中', name: '华中地区(鄂豫湘)' }, { value: 'south', label: '华南', name: '华南地区(粤桂琼)' }, { value: 'southwest', label: '西南', name: '西南地区(川渝贵云藏)' }, { value: 'northeast', label: '东北', name: '东北地区(黑吉辽)' } ] const selectedRegion = ref('central') // 默认华中 const emit = defineEmits(['region-change']) const handleChange = (value) => { emit('region-change', value) } </script> <style scoped> .region-selector { display: flex; align-items: center; gap: 20px; margin-bottom: 20px; padding: 15px; background: #f5f7fa; border-radius: 8px; } .region-info { display: flex; align-items: center; gap: 8px; font-size: 16px; color: #606266; } .el-select { width: 120px; } </style>UserHome.vue (更新主页面)<template> <div class="user-home"> <!-- 区域选择器 --> <RegionSelector @region-change="handleRegionChange" /> <!-- 地区电力概览 --> <RegionOverview :region="currentRegion" /> <!-- 其他功能区保持不变 --> <FunctionCards :hasData="hasUserData" /> <DataUploadGuide :hasData="hasUserData" :lastUpload="lastUploadTime" @upload-success="handleUploadSuccess" /> <Announcements :items="announcements" /> </div> </template> <script> // 导入RegionSelector和RegionOverview import RegionSelector from '@/components/RegionSelector.vue' import RegionOverview from '@/components/RegionOverview.vue' export default { components: { RegionSelector, RegionOverview, // 其他组件... }, setup() { // 新增当前地区状态 const currentRegion = ref('central') const handleRegionChange = (region) => { currentRegion.value = region loadRegionData(region) } const loadRegionData = async (region) => { try { // 更新加载地区数据的逻辑 provinceData.value = await fetchRegionData(region) } catch (error) { console.error('地区数据加载失败:', error) } } // 初始化时加载默认地区数据 onMounted(() => { loadRegionData(currentRegion.value) }) return { currentRegion, handleRegionChange, // 其他返回... } } } </script>RegionOverview.vue (地区概览组件)<template> <div class="region-overview"> <h2 class="section-title">{{ regionName }}电力实时概览</h2> <!-- 地区轮廓图 --> <div class="region-map"> <img :src="regionMap" alt="地区轮廓图" /> <div class="map-overlay"> <h3>{{ regionName }}</h3> <p>面积: {{ regionArea }}万平方公里</p> <p>覆盖省份: {{ provinces }}</p> </div> </div> <!-- 关键指标卡片 --> <div class="stats-cards"> <!-- 卡片内容保持不变,数据随地区变化 --> </div> <!-- 负荷曲线图 --> <div class="load-chart"> <!-- 图表内容保持不变 --> </div> </div> </template> <script> // 引入地区信息配置 import { regionInfo } from '@/utils/regionData' export default { props: { region: String }, computed: { regionName() { return regionInfo[this.region]?.name || '华中地区' }, regionMap() { return require(`@/assets/maps/${this.region}.png`) }, regionArea() { return regionInfo[this.region]?.area || 56 }, provinces() { return regionInfo[this.region]?.provinces || '湖北、河南、湖南' } } } </script> <style scoped> .region-overview { margin-bottom: 40px; } .region-map { position: relative; height: 200px; margin-bottom: 20px; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .region-map img { width: 100%; height: 100%; object-fit: cover; opacity: 0.8; } .map-overlay { position: absolute; top: 0; left: 0; padding: 20px; color: white; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); } .map-overlay h3 { font-size: 24px; margin-bottom: 10px; } .map-overlay p { margin: 5px 0; } </style>regionData.js (地区配置)// src/utils/regionData.js export const regionInfo = { north: { name: '华北地区', area: 155, provinces: '北京、天津、河北、山西、内蒙古', map: 'north.png' }, east: { name: '华东地区', area: 79, provinces: '上海、江苏、浙江、安徽、福建、江西、山东', map: 'east.png' }, central: { name: '华中地区', area: 56, provinces: '河南、湖北、湖南', map: 'central.png' }, south: { name: '华南地区', area: 45, provinces: '广东、广西、海南', map: 'south.png' }, southwest: { name: '西南地区', area: 234, provinces: '重庆、四川、贵州、云南、西藏', map: 'southwest.png' }, northeast: { name: '东北地区', area: 78, provinces: '辽宁、吉林、黑龙江', map: 'northeast.png' } }后端API调整 (Flask)# 添加地区参数的API @app.route('/api/region/overview', methods=['GET']) def region_overview(): region = request.args.get('region', 'central') # 根据不同地区生成不同数据 if region == 'north': # 华北地区数据 data = generate_north_power_data() elif region == 'east': # 华东地区数据 data = generate_east_power_data() # 其他地区... else: # 默认华中地区 data = generate_central_power_data() return jsonify(data) # 示例:华中地区数据生成 def generate_central_power_data(): # 基础参数 base_load = 35000 # 基础负荷 (MW) # ...具体实现与之前类似,但参数不同管理员模型运维页面设计graph TD A[模型运维] --> B[模型概览] A --> C[模型部署] A --> D[模型测试] A --> E[历史版本] A --> F[训练数据]实现思路页面组成模型概览:展示当前模型版本、性能和运行状态模型部署:上传新模型并部署到生产环境模型测试:测试模型预测效果历史版本:管理历史模型版本训练数据:管理训练数据集关键代码实现文件结构src/views/admin/ ├── ModelAdmin.vue # 主页面 ├── components/ │ ├── ModelOverview.vue # 模型概览 │ ├── ModelDeploy.vue # 模型部署 │ ├── ModelTesting.vue # 模型测试 │ ├── ModelHistory.vue # 历史版本 │ └── TrainingData.vue # 训练数据ModelAdmin.vue (主页面)<template> <div class="model-admin"> <h1 class="page-title">预测模型运维管理</h1> <el-tabs v-model="activeTab" type="card"> <el-tab-pane label="模型概览" name="overview"> <ModelOverview :model="currentModel" /> </el-tab-pane> <el-tab-pane label="模型部署" name="deploy"> <ModelDeploy @deployed="refreshModel" /> </el-tab-pane> <el-tab-pane label="模型测试" name="testing"> <ModelTesting :model="currentModel" /> </el-tab-pane> <el-tab-pane label="历史版本" name="history"> <ModelHistory @rollback="handleRollback" /> </el-tab-pane> <el-tab-pane label="训练数据" name="data"> <TrainingData /> </el-tab-pane> </el-tabs> </div> </template> <script> import { ref, onMounted } from 'vue' import { fetchCurrentModel } from '@/services/modelApi' import ModelOverview from './components/ModelOverview.vue' import ModelDeploy from './components/ModelDeploy.vue' import ModelTesting from './components/ModelTesting.vue' import ModelHistory from './components/ModelHistory.vue' import TrainingData from './components/TrainingData.vue' export default { components: { ModelOverview, ModelDeploy, ModelTesting, ModelHistory, TrainingData }, setup() { const activeTab = ref('overview') const currentModel = ref({}) const loadCurrentModel = async () => { try { currentModel.value = await fetchCurrentModel() } catch (error) { console.error('获取模型失败:', error) } } const refreshModel = () => { loadCurrentModel() activeTab.value = 'overview' } const handleRollback = (version) => { console.log('回滚到版本:', version) // 实际中调用回滚API refreshModel() } onMounted(loadCurrentModel) return { activeTab, currentModel, refreshModel, handleRollback } } } </script> <style scoped> .model-admin { padding: 20px; max-width: 1400px; margin: 0 auto; } .page-title { text-align: center; margin-bottom: 30px; color: #303133; } </style>ModelOverview.vue (模型概览)<template> <el-card> <div class="model-overview"> <div class="model-header"> <div class="model-info"> <h2>{{ model.name }}</h2> <el-tag :type="model.status === 'online' ? 'success' : 'danger'"> {{ model.status === 'online' ? '运行中' : '已下线' }} </el-tag> </div> <div class="model-version"> <span class="version-label">版本号:</span> <span class="version-value">{{ model.version }}</span> </div> </div> <div class="model-stats"> <el-descriptions title="性能指标" :column="2" border> <el-descriptions-item label="MAPE"> <span class="highlight">{{ model.mape || '--' }}%</span> </el-descriptions-item> <el-descriptions-item label="RMSE"> <span class="highlight">{{ model.rmse || '--' }}</span> </el-descriptions-item> <el-descriptions-item label="训练用时"> {{ model.trainingTime || '--' }} </el-descriptions-item> <el-descriptions-item label="部署时间"> {{ model.deployDate || '--' }} </el-descriptions-item> </el-descriptions> </div> <div class="model-charts"> <div class="chart-row"> <div class="chart-container"> <h3>模型特征重要性</h3> <div ref="featureChart" style="height: 300px;"></div> </div> <div class="chart-container"> <h3>预测误差分布</h3> <div ref="errorChart" style="height: 300px;"></div> </div> </div> </div> </div> </el-card> </template> <script> import { ref, onMounted } from 'vue' import * as echarts from 'echarts' export default { props: { model: { type: Object, default: () => ({}) } }, setup(props) { const featureChart = ref(null) const errorChart = ref(null) onMounted(() => { renderFeatureChart() renderErrorChart() }) const renderFeatureChart = () => { if (!featureChart.value) return const chart = echarts.init(featureChart.value) // 图表配置... } const renderErrorChart = () => { if (!errorChart.value) return const chart = echarts.init(errorChart.value) // 图表配置... } return { featureChart, errorChart } } } </script> <style scoped> .model-overview { padding: 20px; } .model-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .model-info { display: flex; align-items: center; gap: 15px; } .model-info h2 { margin: 0; } .model-version { font-size: 16px; } .version-label { color: #909399; } .version-value { font-weight: bold; margin-left: 8px; } .model-stats { margin-bottom: 30px; } .highlight { font-weight: bold; color: #409eff; } .model-charts { margin-top: 30px; } .chart-row { display: flex; gap: 20px; } .chart-container { flex: 1; background: #fff; padding: 15px; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .chart-container h3 { margin-top: 0; margin-bottom: 15px; text-align: center; } </style>ModelDeploy.vue (模型部署)<template> <el-card> <div class="model-deploy"> <h2>模型部署</h2> <el-steps :active="activeStep" align-center finish-status="success"> <el-step title="上传模型" description="上传新模型文件" /> <el-step title="验证模型" description="在测试环境验证" /> <el-step title="部署模型" description="部署到生产环境" /> </el-steps> <div v-if="activeStep === 0" class="step-content"> <el-upload class="upload-model" drag action="/api/model/upload" :on-success="handleUploadSuccess" :before-upload="beforeUpload" :show-file-list="false" > <el-icon :size="60"><Upload /></el-icon> <div class="el-upload__text">拖拽模型文件到此处,或 <em>点击上传</em></div> <template #tip> <div class="upload-tip">支持格式: .pkl, .h5, .pt (最大1GB)</div> </template> </el-upload> <div v-if="currentFile" class="file-info"> <el-icon><Document /></el-icon> <span>{{ currentFile.name }}</span> <span class="file-size">({{ formatFileSize(currentFile.size) }})</span> </div> </div> <div v-if="activeStep === 1" class="step-content"> <div class="validation-panel"> <h3>模型验证</h3> <p>将使用测试数据集验证模型性能</p> <div class="metrics-form"> <el-form label-width="200px"> <el-form-item label="允许的最小MAPE:"> <el-input-number v-model="minMAPE" :min="0" :max="20" :step="0.5" /> </el-form-item> <el-form-item label="允许的最大RMSE:"> <el-input-number v-model="maxRMSE" :min="100" :max="1000" :step="10" /> </el-form-item> </el-form> </div> <el-button type="primary" @click="runValidation" :loading="validating" > 开始验证 </el-button> <div v-if="validationResult" class="validation-result"> <h4>验证结果:</h4> <el-descriptions :column="2" border> <el-descriptions-item label="MAPE"> <span :class="{'success': validationResult.mapePassed}"> {{ validationResult.mape }}% </span> </el-descriptions-item> <el-descriptions-item label="RMSE"> <span :class="{'success': validationResult.rmsePassed}"> {{ validationResult.rmse }} </span> </el-descriptions-item> <el-descriptions-item label="总体状态"> <el-tag :type="validationResult.passed ? 'success' : 'danger'"> {{ validationResult.passed ? '验证通过' : '验证失败' }} </el-tag> </el-descriptions-item> </el-descriptions> </div> </div> </div> <div v-if="activeStep === 2" class="step-content"> <div class="deploy-panel"> <h3>部署到生产环境</h3> <div class="deploy-info"> <el-descriptions :column="1"> <el-descriptions-item label="模型文件"> {{ currentFile?.name }} </el-descriptions-item> <el-descriptions-item label="版本号"> <el-input v-model="version" placeholder="输入版本号 (如 v2.3.1)" /> </el-descriptions-item> <el-descriptions-item label="验证结果"> <span :class="{'success': validationResult?.passed}"> {{ validationResult?.passed ? '通过' : '未通过' }} </span> </el-descriptions-item> </el-descriptions> </div> <el-alert v-if="!validationResult?.passed" title="模型验证未通过,部署到生产环境可能有风险" type="warning" show-icon class="deploy-warning" /> <div class="deploy-actions"> <el-button type="primary" @click="deployModel" :disabled="!version || !validationResult?.passed" > 确认部署 </el-button> <el-button @click="cancelDeploy">取消</el-button> </div> </div> </div> <div class="step-actions"> <el-button :disabled="activeStep === 0" @click="prevStep">上一步</el-button> <el-button type="primary" :disabled="activeStep === 2 && !version" @click="nextStep" > {{ activeStep === 2 ? '完成' : '下一步' }} </el-button> </div> </div> </el-card> </template> <script setup> // 组件实现省略... </script>ModelTesting.vue (模型测试)<template> <el-card> <div class="model-testing"> <h2>模型测试</h2> <div class="test-panel"> <div class="test-options"> <el-form label-width="120px"> <el-form-item label="测试数据集"> <el-select v-model="testDataset" placeholder="请选择测试集"> <el-option v-for="item in datasets" :key="item.id" :label="item.name" :value="item.id" /> </el-select> </el-form-item> <el-form-item label="时间范围"> <el-date-picker v-model="dateRange" type="datetimerange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD HH:mm:ss" /> </el-form-item> <el-form-item label="指标计算"> <el-checkbox-group v-model="metrics"> <el-checkbox label="mape">MAPE(%)</el-checkbox> <el-checkbox label="rmse">RMSE(MW)</el-checkbox> <el-checkbox label="mae">MAE(MW)</el-checkbox> <el-checkbox label="r2">R²</el-checkbox> </el-checkbox-group> </el-form-item> </el-form> <el-button type="primary" @click="runTest" :loading="testing" :disabled="!testDataset" > 开始测试 </el-button> </div> <div v-if="testResult" class="test-results"> <h3>测试结果</h3> <div class="metrics-result"> <el-row :gutter="20"> <el-col :span="6" v-if="metrics.includes('mape')"> <div class="metric-card"> <div class="metric-name">MAPE</div> <div class="metric-value">{{ testResult.mape }}%</div> <div class="metric-bar"> <el-progress :percentage="getMapeScore(testResult.mape)" :status="getMapeStatus(testResult.mape)" stroke-width="15" /> </div> </div> </el-col> <!-- 其他指标卡片类似 --> </el-row> </div> <div class="chart-container"> <div ref="resultChart" style="height: 400px;"></div> </div> <div class="detailed-results"> <el-table :data="testResult.samples" height="300" stripe> <el-table-column prop="timestamp" label="时间点" width="180" /> <el-table-column label="实际值" width="100"> <template #default="{row}"> {{ row.actual.toFixed(0) }} </template> </el-table-column> <el-table-column label="预测值" width="100"> <template #default="{row}"> {{ row.predicted.toFixed(0) }} </template> </el-table-column> <el-table-column label="误差" width="100"> <template #default="{row}"> <span :class="{'error-negative': row.error < 0}"> {{ (row.error).toFixed(0) }} </span> </template> </el-table-column> <el-table-column label="误差率"> <template #default="{row}"> <el-progress :percentage="Math.abs(row.errorPercent)" :status="row.errorPercent > 0 ? 'success' : 'exception'" :show-text="false" /> <span :class="{'error-negative': row.errorPercent < 0}"> {{ Math.abs(row.errorPercent).toFixed(1) }}% </span> </template> </el-table-column> </el-table> </div> </div> </div> </div> </el-card> </template> <script setup> // 组件实现省略... </script>Flask后端API (模型运维)# 模型部署API @app.route('/api/model/deploy', methods=['POST']) def deploy_model(): if 'file' not in request.files: return jsonify({"success": False, "message": "没有文件上传"}) file = request.files['file'] version = request.form.get('version') # 保存模型文件 filename = f"model_{version}.pkl" file_path = os.path.join(MODEL_DIR, filename) file.save(file_path) # 更新当前激活模型 set_current_model(version) return jsonify({ "success": True, "message": f"模型{version}部署成功", "version": version }) # 模型测试API @app.route('/api/model/test', methods=['POST']) def test_model(): data = request.json dataset_id = data.get('dataset') metrics = data.get('metrics', ['mape', 'rmse']) # 加载测试数据 test_data = load_test_data(dataset_id) # 加载当前模型 model = load_current_model() # 运行预测 predictions = model.predict(test_data.features) # 计算指标 results = {} if 'mape' in metrics: results['mape'] = calculate_mape(test_data.targets, predictions) if 'rmse' in metrics: results['rmse'] = calculate_rmse(test_data.targets, predictions) # 其他指标... return jsonify({ "success": True, "results": results, "samples": prepare_samples(test_data, predictions) }) # 历史模型管理API @app.route('/api/model/history', methods=['GET']) def model_history(): models = [] for file in os.listdir(MODEL_DIR): if file.startswith('model_'): version = file[6:-4] stat = os.stat(os.path.join(MODEL_DIR, file)) models.append({ "version": version, "filename": file, "size": stat.st_size, "date": datetime.fromtimestamp(stat.st_ctime).strftime('%Y-%m-%d %H:%M'), "active": version == CURRENT_MODEL_VERSION }) return jsonify({ "success": True, "models": sorted(models, key=lambda m: m['date'], reverse=True) })设计特点与优势普通用户首页(带地区选择)多区域支持:直观的区域选择器各区域特色地图展示根据区域显示不同的电力数据可视化增强:区域轮廓图直观展示地理范围区域关键指标卡片动态负荷曲线图用户体验优化:地区选择反馈即时更新内容区域专属视觉效果清晰的导航和功能区布局管理员模型运维页面完整模型生命周期管理:模型部署流程分步引导严格的验证机制确保模型质量多维度测试支持专业运维功能:模型性能可视化(特征重要性,误差分布)详细的预测结果对比历史版本管理和回滚高效数据处理:支持多种测试数据集自定义时间范围和指标批量数据管理能力这个设计在原有系统基础上增强了地区选择功能,同时为管理员提供了专业的模型运维管理界面,满足不同角色的需求,保持系统的一致性和专业性。帮我生成一个项目,可以只有用户首页和模型运维界面,但是要可以看到页面