This commit is contained in:
Tian jianyong 2024-11-09 00:16:19 +08:00
parent 865c93c811
commit bef24985c9
26 changed files with 15669 additions and 1 deletions

@ -1 +0,0 @@
Subproject commit 96445d75411a5f9ace114085af0872cfbc116515

24
frontend/README.md Normal file
View File

@ -0,0 +1,24 @@
# frontend
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

5
frontend/babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

20
frontend/jsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"exclude": ["**/HelloWorld.vue"]
}

12736
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

61
frontend/package.json Normal file
View File

@ -0,0 +1,61 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"engines": {
"node": ">=16",
"npm": ">=8"
},
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^1.6.0",
"core-js": "^3.8.3",
"echarts": "^5.4.3",
"element-plus": "^2.4.2",
"vue": "^3.2.13",
"vue-router": "^4.0.3",
"vuex": "^4.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@element-plus/icons-vue": "^2.3.1",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"@vue/compiler-sfc": "^3.2.13",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"sass": "^1.32.7",
"sass-loader": "^12.0.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {
"vue/multi-word-component-names": "off",
"no-unused-vars": "warn"
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

43
frontend/src/App.vue Normal file
View File

@ -0,0 +1,43 @@
<template>
<el-container>
<el-header>
<el-menu
:router="true"
mode="horizontal"
:default-active="$route.path"
>
<el-menu-item index="/">首页</el-menu-item>
<el-menu-item index="/predict">机器学习预测</el-menu-item>
<el-menu-item index="/pls-predict">PLS回归预测</el-menu-item>
<el-menu-item index="/analysis">特征分析</el-menu-item>
<el-menu-item index="/training">模型训练</el-menu-item>
<el-menu-item index="/data">数据管理</el-menu-item>
<el-menu-item index="/datasets">数据集管理</el-menu-item>
<el-menu-item index="/models">模型管理</el-menu-item>
</el-menu>
</el-header>
<el-main>
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" :key="$route.fullPath" />
</keep-alive>
</router-view>
</el-main>
</el-container>
</template>
<style lang="scss" scoped>
.el-header {
padding: 0;
.el-menu {
border-bottom: none;
}
}
.el-main {
background-color: #f5f7fa;
min-height: calc(100vh - 60px);
}
</style>

43
frontend/src/api/index.js Normal file
View File

@ -0,0 +1,43 @@
import axios from 'axios'
import { API_BASE_URL } from '@/config'
const api = axios.create({
baseURL: API_BASE_URL,
timeout: 10000
})
export const predict = (data) => {
return api.post('/predict', data)
}
export const analyzeFeatures = (data) => {
return api.post('/analyze-features', data)
}
export const trainModel = (data) => {
return api.post('/train', data)
}
export const evaluateModel = (data) => {
return api.post('/evaluate', data)
}
export const importData = (formData) => {
return api.post('/data/import', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
export const getEquipmentData = () => {
return api.get('/data')
}
export const updateEquipment = (id, data) => {
return api.put(`/data/${id}`, data)
}
export const deleteEquipment = (id) => {
return api.delete(`/data/${id}`)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,39 @@
/* 禁用 ResizeObserver 警告 */
iframe[style*="position: fixed; top: 0px; left: 0px; width: 100%; height: 100%; border: none; z-index: 2147483647;"] {
display: none !important;
}
.el-overlay {
overflow: hidden !important;
}
/* 添加全局样式 */
body {
margin: 0;
padding: 0;
overflow-x: hidden;
}
/* 修复 Element Plus 的一些已知问题 */
.el-dialog__wrapper {
overflow: hidden !important;
}
.el-select-dropdown {
overflow: hidden !important;
}
/* 禁用 ResizeObserver 相关的警告样式 */
.resize-observer-warning {
display: none !important;
}
/* 优化滚动行为 */
* {
scroll-behavior: smooth;
}
/* 防止页面抖动 */
.el-main {
overflow-x: hidden;
}

8
frontend/src/config.js Normal file
View File

@ -0,0 +1,8 @@
export const API_BASE_URL = 'http://localhost:5001/api';
export const DB_CONFIG = {
host: 'localhost',
user: 'root',
password: '123456',
database: 'equipment_cost_db'
};

55
frontend/src/main.js Normal file
View File

@ -0,0 +1,55 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import './assets/styles/global.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// 创建应用实例
const app = createApp(App)
// 注册插件
app.use(ElementPlus, {
size: 'default'
})
app.use(router)
app.use(store)
// 注册图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
// 全局错误处理
app.config.errorHandler = (err, vm, info) => {
if (err.message && err.message.includes('ResizeObserver')) {
return
}
console.error(err)
}
// 全局警告处理
app.config.warnHandler = (msg, vm, trace) => {
if (msg.includes('ResizeObserver')) {
return
}
console.warn(msg, trace)
}
// 挂载应用
app.mount('#app')
// 处理 ResizeObserver 错误
const _ResizeObserver = window.ResizeObserver
window.ResizeObserver = class ResizeObserver extends _ResizeObserver {
constructor(callback) {
super((entries, observer) => {
requestAnimationFrame(() => {
if (!Array.isArray(entries)) return
callback(entries, observer)
})
})
}
}

View File

@ -0,0 +1,58 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomePage from '@/views/HomePage.vue'
import DataPage from '@/views/DataPage.vue'
import DatasetPage from '@/views/DatasetPage.vue'
import PredictPage from '@/views/PredictPage.vue'
import PLSPredictPage from '@/views/PLSPredictPage.vue'
import AnalysisPage from '@/views/AnalysisPage.vue'
import TrainingPage from '@/views/TrainingPage.vue'
const routes = [
{
path: '/',
name: 'Home',
component: HomePage
},
{
path: '/data',
name: 'Data',
component: DataPage
},
{
path: '/datasets',
name: 'Datasets',
component: DatasetPage
},
{
path: '/predict',
name: 'Predict',
component: PredictPage
},
{
path: '/pls-predict',
name: 'PLSPredict',
component: PLSPredictPage
},
{
path: '/analysis',
name: 'Analysis',
component: AnalysisPage
},
{
path: '/training',
name: 'Training',
component: TrainingPage
},
{
path: '/models',
name: 'Models',
component: () => import('../views/ModelPage.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router

View File

@ -0,0 +1,14 @@
import { createStore } from 'vuex'
export default createStore({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
}
})

View File

@ -0,0 +1,73 @@
// 处理 ResizeObserver 错误
const resizeHandler = () => {
const resizeObserverErrors = [
"ResizeObserver loop completed with undelivered notifications.",
"ResizeObserver loop limit exceeded",
"ResizeObserver loop completed"
]
// 添加全局错误处理
const handler = (event) => {
if (event && event.message && resizeObserverErrors.includes(event.message)) {
event.stopPropagation()
event.preventDefault()
event.stopImmediatePropagation()
return false
}
}
// 添加多个事件监听器
window.addEventListener('error', handler, true)
window.addEventListener('unhandledrejection', handler, true)
// 添加 ResizeObserver 错误处理
if (window.ResizeObserver) {
const resizeObserverPrototype = ResizeObserver.prototype
const originalObserve = resizeObserverPrototype.observe
resizeObserverPrototype.observe = function (...args) {
try {
return originalObserve.apply(this, args)
} catch (e) {
if (resizeObserverErrors.includes(e.message)) {
return null
}
throw e
}
}
}
}
export default {
install: (app) => {
resizeHandler()
// 添加全局错误处理器
app.config.errorHandler = (err, vm, info) => {
const resizeObserverErrors = [
"ResizeObserver loop completed with undelivered notifications.",
"ResizeObserver loop limit exceeded",
"ResizeObserver loop completed"
]
if (err && err.message && resizeObserverErrors.includes(err.message)) {
return
}
console.error('Vue Error:', err, info)
}
// 添加全局警告处理器
app.config.warnHandler = (msg, vm, trace) => {
const resizeObserverErrors = [
"ResizeObserver loop completed with undelivered notifications.",
"ResizeObserver loop limit exceeded",
"ResizeObserver loop completed"
]
if (resizeObserverErrors.includes(msg)) {
return
}
console.warn('Vue Warning:', msg, trace)
}
}
}

View File

@ -0,0 +1,307 @@
<template>
<div class="analysis-page">
<el-card class="analysis-card">
<template #header>
<div class="header-content">
<h2>特征分析</h2>
</div>
</template>
<!-- 数据集选择 -->
<div class="dataset-section">
<el-form :model="analysisForm" label-width="120px">
<el-form-item label="装备类型" required>
<el-select v-model="analysisForm.equipment_type" @change="handleEquipmentTypeChange">
<el-option label="火箭炮" value="火箭炮"></el-option>
<el-option label="巡飞弹" value="巡飞弹"></el-option>
</el-select>
</el-form-item>
<el-form-item label="选择数据集" required>
<el-select v-model="analysisForm.dataset_id" @change="handleDatasetChange">
<el-option
v-for="dataset in availableDatasets"
:key="dataset.id"
:label="dataset.name"
:value="dataset.id"
></el-option>
</el-select>
</el-form-item>
</el-form>
<!-- 数据集信息 -->
<el-descriptions v-if="selectedDataset" :column="2" border>
<el-descriptions-item label="数据集名称">{{ selectedDataset.name }}</el-descriptions-item>
<el-descriptions-item label="装备数量">{{ selectedDataset.equipment_count }}</el-descriptions-item>
<el-descriptions-item label="描述" :span="2">{{ selectedDataset.description }}</el-descriptions-item>
</el-descriptions>
</div>
<!-- 分析按钮 -->
<div class="action-section">
<el-button type="primary" @click="startAnalysis" :loading="analyzing" :disabled="!analysisForm.dataset_id">
{{ analyzing ? '分析中...' : '开始分析' }}
</el-button>
</div>
<!-- 分析结果 -->
<div v-if="analysisResult" class="result-section">
<el-divider content-position="left">分析结果</el-divider>
<!-- 特征重要性 -->
<h3>特征重要性</h3>
<div class="chart-container">
<div ref="importanceChartRef" style="width: 100%; height: 400px"></div>
</div>
<!-- 相关性分析 -->
<h3>相关性分析</h3>
<div class="chart-container">
<div ref="correlationChartRef" style="width: 100%; height: 500px"></div>
</div>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted, watch, nextTick, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import axios from 'axios'
import { API_BASE_URL } from '@/config'
import * as echarts from 'echarts'
//
const __name = 'AnalysisPage'
//
const analysisForm = ref({
equipment_type: '',
dataset_id: null
})
const availableDatasets = ref([])
const selectedDataset = ref(null)
const analyzing = ref(false)
const analysisResult = ref(null)
const importanceChartRef = ref(null)
const correlationChartRef = ref(null)
//
const importanceChart = ref(null)
const correlationChart = ref(null)
//
watch(() => analysisResult.value, async (newResult) => {
if (newResult) {
console.log('Analysis result updated:', newResult)
// tick DOM
await nextTick()
//
setTimeout(() => {
renderCharts()
}, 100)
}
}, { deep: true })
//
const loadDatasets = async (type) => {
try {
const response = await axios.get(`${API_BASE_URL}/datasets`, {
params: { equipment_type: type, purpose: '训练' }
})
availableDatasets.value = response.data
} catch (error) {
ElMessage.error('获取数据集列表失败')
}
}
//
const handleEquipmentTypeChange = () => {
analysisForm.value.dataset_id = null
selectedDataset.value = null
analysisResult.value = null
loadDatasets(analysisForm.value.equipment_type)
}
//
const handleDatasetChange = async () => {
try {
const response = await axios.get(`${API_BASE_URL}/datasets/${analysisForm.value.dataset_id}`)
selectedDataset.value = response.data
analysisResult.value = null
} catch (error) {
ElMessage.error('获取数据集详情失败')
}
}
//
const startAnalysis = async () => {
if (!analysisForm.value.dataset_id) {
ElMessage.warning('请先选择数据集')
return
}
analyzing.value = true
try {
const response = await axios.post(`${API_BASE_URL}/analyze-features`, {
dataset_id: analysisForm.value.dataset_id
})
analysisResult.value = response.data
console.log('Analysis completed, result:', analysisResult.value)
} catch (error) {
ElMessage.error('特征分析失败')
console.error('Analysis error:', error)
} finally {
analyzing.value = false
}
}
//
const renderCharts = () => {
console.log('Starting to render charts')
//
if (importanceChart.value) {
importanceChart.value.dispose()
}
if (correlationChart.value) {
correlationChart.value.dispose()
}
// DOM
if (!importanceChartRef.value || !correlationChartRef.value) {
console.log('Chart DOM elements not ready')
return
}
try {
//
importanceChart.value = echarts.init(importanceChartRef.value)
correlationChart.value = echarts.init(correlationChartRef.value)
//
importanceChart.value.setOption({
title: { text: '特征重要性排序' },
tooltip: {},
xAxis: {
type: 'value',
name: '重要性得分'
},
yAxis: {
type: 'category',
data: analysisResult.value.important_features.map(f => f.name)
},
series: [{
type: 'bar',
data: analysisResult.value.important_features.map(f => f.importance)
}]
})
correlationChart.value.setOption({
title: { text: '特征相关性热力图' },
tooltip: {
position: 'top',
formatter: function (params) {
const value = params.data[2].toFixed(2)
const feature1 = analysisResult.value.correlation_analysis.features[params.data[0]]
const feature2 = analysisResult.value.correlation_analysis.features[params.data[1]]
return `${feature1}${feature2} 的相关性: ${value}`
}
},
grid: {
height: '50%',
top: '10%'
},
xAxis: {
type: 'category',
data: analysisResult.value.correlation_analysis.features,
splitArea: { show: true },
axisLabel: {
interval: 0,
rotate: 45
}
},
yAxis: {
type: 'category',
data: analysisResult.value.correlation_analysis.features,
splitArea: { show: true }
},
visualMap: {
min: -1,
max: 1,
calculable: true,
orient: 'horizontal',
left: 'center',
bottom: '15%',
color: ['#cc3333', '#eeeeee', '#00007f']
},
series: [{
type: 'heatmap',
data: analysisResult.value.correlation_analysis.matrix,
label: {
show: true,
formatter: function(params) {
return params.data[2].toFixed(2)
}
}
}]
})
//
window.addEventListener('resize', () => {
importanceChart.value?.resize()
correlationChart.value?.resize()
})
console.log('Charts rendered successfully')
} catch (error) {
console.error('Error rendering charts:', error)
}
}
//
onMounted(() => {
//
})
//
onUnmounted(() => {
importanceChart.value?.dispose()
correlationChart.value?.dispose()
})
</script>
<style lang="scss" scoped>
.analysis-page {
padding: 20px;
.analysis-card {
.header-content {
h2 {
margin: 0;
}
}
.dataset-section {
margin-bottom: 20px;
}
.action-section {
margin: 20px 0;
text-align: center;
}
.result-section {
h3 {
margin: 20px 0;
}
.chart-container {
margin: 20px 0;
border: 1px solid #ebeef5;
border-radius: 4px;
}
}
}
}
</style>

View File

@ -0,0 +1,725 @@
<template>
<div class="data-page">
<el-card class="data-card">
<template #header>
<div class="header-content">
<h2>数据管理</h2>
<div class="header-buttons">
<el-upload
:action="null"
:auto-upload="false"
:show-file-list="false"
accept=".xls,.xlsx"
@change="handleFileChange"
>
<el-button type="primary">导入数据</el-button>
</el-upload>
<el-button type="primary" @click="downloadTemplate">下载模板</el-button>
</div>
</div>
</template>
<!-- 数据列表 -->
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="火箭炮数据" name="rocket">
<!-- 搜索和过滤 -->
<div class="filter-section">
<el-input
v-model="searchQuery"
placeholder="搜索装备名称或制造商"
style="width: 200px; margin-right: 10px;"
/>
<el-select v-model="filterManufacturer" placeholder="制造商" clearable>
<el-option
v-for="item in manufacturers"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</div>
<!-- 数据表格 -->
<el-table :data="filteredRocketData" border style="width: 100%">
<el-table-column prop="name" label="名称" sortable></el-table-column>
<el-table-column prop="manufacturer" label="制造商" sortable></el-table-column>
<el-table-column prop="length_m" label="总长(m)" sortable></el-table-column>
<el-table-column prop="weight_kg" label="重量(kg)" sortable></el-table-column>
<el-table-column prop="max_range_km" label="最大射程(km)" sortable></el-table-column>
<el-table-column prop="rocket_diameter_mm" label="口径(mm)" sortable></el-table-column>
<el-table-column prop="rate_of_fire" label="射速(发/分)" sortable></el-table-column>
<el-table-column label="操作" width="200">
<template #default="scope">
<el-button size="small" @click="viewDetails(scope.row)">
详情
</el-button>
<el-button size="small" type="primary" @click="editData(scope.row)">
编辑
</el-button>
<el-button size="small" type="danger" @click="deleteData(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="巡飞弹数据" name="missile">
<!-- 搜索和过滤 -->
<div class="filter-section">
<el-input
v-model="searchQuery"
placeholder="搜索装备名称或制造商"
style="width: 200px; margin-right: 10px;"
/>
<el-select v-model="filterManufacturer" placeholder="制造商" clearable>
<el-option
v-for="item in manufacturers"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</div>
<!-- 数据表格 -->
<el-table :data="filteredMissileData" border style="width: 100%">
<el-table-column prop="name" label="名称" sortable></el-table-column>
<el-table-column prop="manufacturer" label="制造" sortable></el-table-column>
<el-table-column prop="length_m" label="弹长(m)" sortable></el-table-column>
<el-table-column prop="weight_kg" label="重量(kg)" sortable></el-table-column>
<el-table-column prop="max_range_km" label="最大射程(km)" sortable></el-table-column>
<el-table-column prop="max_speed_ms" label="最大速度(m/s)" sortable></el-table-column>
<el-table-column prop="flight_time_min" label="巡飞时间(min)" sortable></el-table-column>
<el-table-column label="操作" width="200">
<template #default="scope">
<el-button size="small" @click="viewDetails(scope.row)">
详情
</el-button>
<el-button size="small" type="primary" @click="editData(scope.row)">
编辑
</el-button>
<el-button size="small" type="danger" @click="deleteData(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
<!-- 详情对话框 -->
<el-dialog v-model="detailsVisible" title="装备详情" width="70%">
<el-descriptions :column="2" border>
<!-- 基本信息 -->
<template>
<el-descriptions-item :span="2">
<template #label>
<el-divider content-position="left">基本信息</el-divider>
</template>
</el-descriptions-item>
<el-descriptions-item label="名称">{{ selectedData?.name }}</el-descriptions-item>
<el-descriptions-item label="制造商">{{ selectedData?.manufacturer }}</el-descriptions-item>
<!-- 通用参数 -->
<el-descriptions-item label="总长(m)">{{ formatNumber(selectedData?.length_m) }}</el-descriptions-item>
<el-descriptions-item label="宽度(m)">{{ formatNumber(selectedData?.width_m) }}</el-descriptions-item>
<el-descriptions-item label="高度(m)">{{ formatNumber(selectedData?.height_m) }}</el-descriptions-item>
<el-descriptions-item label="重量(kg)">{{ formatNumber(selectedData?.weight_kg) }}</el-descriptions-item>
<el-descriptions-item label="最大射程(km)">{{ formatNumber(selectedData?.max_range_km) }}</el-descriptions-item>
</template>
<!-- 火箭炮特有参数 -->
<template v-if="selectedData?.type === '火箭炮'">
<el-descriptions-item :span="2">
<template #label>
<el-divider content-position="left">火箭炮参数</el-divider>
</template>
</el-descriptions-item>
<el-descriptions-item label="口径(mm)">{{ formatNumber(selectedData?.rocket_diameter_mm) }}</el-descriptions-item>
<el-descriptions-item label="射速(发/分)">{{ formatNumber(selectedData?.rate_of_fire) }}</el-descriptions-item>
<el-descriptions-item label="火箭弹长度(m)">{{ formatNumber(selectedData?.rocket_length_m) }}</el-descriptions-item>
<el-descriptions-item label="火箭弹重量(kg)">{{ formatNumber(selectedData?.rocket_weight_kg) }}</el-descriptions-item>
<el-descriptions-item label="方向射界(度)">{{ formatNumber(selectedData?.firing_angle_horizontal) }}</el-descriptions-item>
<el-descriptions-item label="高低射界(度)">{{ formatNumber(selectedData?.firing_angle_vertical) }}</el-descriptions-item>
<el-descriptions-item label="机动方式">{{ selectedData?.mobility_type }}</el-descriptions-item>
<el-descriptions-item label="结构布局">{{ selectedData?.structure_layout }}</el-descriptions-item>
<el-descriptions-item label="发动机型号">{{ selectedData?.engine_model }}</el-descriptions-item>
<el-descriptions-item label="发动机参数">{{ selectedData?.engine_params }}</el-descriptions-item>
<el-descriptions-item label="功率(hp)">{{ formatNumber(selectedData?.power_hp) }}</el-descriptions-item>
<el-descriptions-item label="行程(km)">{{ formatNumber(selectedData?.travel_range_km) }}</el-descriptions-item>
</template>
<!-- 巡飞弹特有参数 -->
<template v-if="selectedData?.type === '巡飞弹'">
<el-descriptions-item :span="2">
<template #label>
<el-divider content-position="left">巡飞弹参数</el-divider>
</template>
</el-descriptions-item>
<el-descriptions-item label="最大速度(m/s)">{{ formatNumber(selectedData?.max_speed_ms) }}</el-descriptions-item>
<el-descriptions-item label="巡航速度(km/h)">{{ formatNumber(selectedData?.cruise_speed_kmh) }}</el-descriptions-item>
<el-descriptions-item label="巡飞时间(min)">{{ formatNumber(selectedData?.flight_time_min) }}</el-descriptions-item>
<el-descriptions-item label="战斗部类型">{{ selectedData?.warhead_type }}</el-descriptions-item>
<el-descriptions-item label="发射方式">{{ selectedData?.launch_mode }}</el-descriptions-item>
<el-descriptions-item label="动力装置">{{ selectedData?.power_system }}</el-descriptions-item>
<el-descriptions-item label="制导体制">{{ selectedData?.guidance_system }}</el-descriptions-item>
</template>
<!-- 特殊参数 -->
<template v-if="selectedData?.custom_params?.length > 0">
{{ console.log('Rendering custom params:', selectedData.custom_params) }}
<el-descriptions-item :span="2">
<template #label>
<el-divider content-position="left">特殊参数</el-divider>
</template>
</el-descriptions-item>
<el-descriptions-item
v-for="param in selectedData.custom_params"
:key="param.id"
:label="param.param_name"
>
{{ formatCustomParamValue(param) }}
</el-descriptions-item>
</template>
<!-- 成本信息 -->
<template>
<el-descriptions-item :span="2">
<template #label>
<el-divider content-position="left">成本信息</el-divider>
</template>
</el-descriptions-item>
<el-descriptions-item label="实际成本(元)">
{{ formatMoney(selectedData?.actual_cost) }}
</el-descriptions-item>
<el-descriptions-item label="预测成本(元)">
{{ formatMoney(selectedData?.predicted_cost) }}
</el-descriptions-item>
<el-descriptions-item label="成本估算时间">
{{ selectedData?.cost_estimate_date || '-' }}
</el-descriptions-item>
</template>
</el-descriptions>
</el-dialog>
<!-- 编辑对话框 -->
<el-dialog v-model="editVisible" title="编辑装备数据" width="70%">
<el-form :model="editForm" label-width="120px">
<!-- 基本信息 -->
<template>
<el-divider content-position="left">基本信息</el-divider>
<el-form-item label="名称">
<el-input v-model="editForm.name"></el-input>
</el-form-item>
<el-form-item label="制造商">
<el-input v-model="editForm.manufacturer"></el-input>
</el-form-item>
<!-- 通用参数 -->
<el-form-item label="总长(m)">
<el-input-number v-model="editForm.length_m" :precision="2"></el-input-number>
</el-form-item>
<el-form-item label="宽度(m)">
<el-input-number v-model="editForm.width_m" :precision="2"></el-input-number>
</el-form-item>
<el-form-item label="高度(m)">
<el-input-number v-model="editForm.height_m" :precision="2"></el-input-number>
</el-form-item>
<el-form-item label="重量(kg)">
<el-input-number v-model="editForm.weight_kg" :precision="1"></el-input-number>
</el-form-item>
<el-form-item label="最大射程(km)">
<el-input-number v-model="editForm.max_range_km" :precision="2"></el-input-number>
</el-form-item>
</template>
<!-- 火箭炮特有参数 -->
<template v-if="editForm.type === '火箭炮'">
<el-divider content-position="left">火箭炮参数</el-divider>
<el-form-item label="口径(mm)">
<el-input-number v-model="editForm.rocket_diameter_mm" :precision="0"></el-input-number>
</el-form-item>
<el-form-item label="射速(发/分)">
<el-input-number v-model="editForm.rate_of_fire" :precision="0"></el-input-number>
</el-form-item>
<el-form-item label="火箭弹长度(m)">
<el-input-number v-model="editForm.rocket_length_m" :precision="2"></el-input-number>
</el-form-item>
<el-form-item label="火箭弹重量(kg)">
<el-input-number v-model="editForm.rocket_weight_kg" :precision="1"></el-input-number>
</el-form-item>
<el-form-item label="方向射界(度)">
<el-input-number v-model="editForm.firing_angle_horizontal" :precision="1"></el-input-number>
</el-form-item>
<el-form-item label="低射界(度)">
<el-input-number v-model="editForm.firing_angle_vertical" :precision="1"></el-input-number>
</el-form-item>
<el-form-item label="机动方式">
<el-select v-model="editForm.mobility_type">
<el-option v-for="option in getSelectOptions('mobility_type')" :key="option" :label="option" :value="option"></el-option>
</el-select>
</el-form-item>
<el-form-item label="结构布局">
<el-input v-model="editForm.structure_layout"></el-input>
</el-form-item>
<el-form-item label="发动型号">
<el-input v-model="editForm.engine_model"></el-input>
</el-form-item>
<el-form-item label="发动机参数">
<el-input v-model="editForm.engine_params"></el-input>
</el-form-item>
<el-form-item label="功率(hp)">
<el-input-number v-model="editForm.power_hp" :precision="0"></el-input-number>
</el-form-item>
<el-form-item label="行程(km)">
<el-input-number v-model="editForm.travel_range_km" :precision="2"></el-input-number>
</el-form-item>
</template>
<!-- 巡飞弹特有参数 -->
<template v-if="editForm.type === '巡飞弹'">
<el-divider content-position="left">巡飞弹参数</el-divider>
<el-form-item label="最大速度(m/s)">
<el-input-number v-model="editForm.max_speed_ms" :precision="1"></el-input-number>
</el-form-item>
<el-form-item label="巡航速度(km/h)">
<el-input-number v-model="editForm.cruise_speed_kmh" :precision="1"></el-input-number>
</el-form-item>
<el-form-item label="巡飞时间(min)">
<el-input-number v-model="editForm.flight_time_min" :precision="0"></el-input-number>
</el-form-item>
<el-form-item label="战斗部类型">
<el-select v-model="editForm.warhead_type">
<el-option v-for="option in getSelectOptions('warhead_type')" :key="option" :label="option" :value="option"></el-option>
</el-select>
</el-form-item>
<el-form-item label="发射方式">
<el-select v-model="editForm.launch_mode">
<el-option v-for="option in getSelectOptions('launch_mode')" :key="option" :label="option" :value="option"></el-option>
</el-select>
</el-form-item>
<el-form-item label="动力装置">
<el-select v-model="editForm.power_system">
<el-option v-for="option in getSelectOptions('power_system')" :key="option" :label="option" :value="option"></el-option>
</el-select>
</el-form-item>
<el-form-item label="制导体制">
<el-select v-model="editForm.guidance_system">
<el-option v-for="option in getSelectOptions('guidance_system')" :key="option" :label="option" :value="option"></el-option>
</el-select>
</el-form-item>
</template>
<!-- 特殊参数 -->
<template v-if="editForm.custom_params?.length">
<el-divider content-position="left">特殊参数</el-divider>
<el-form-item
v-for="(param, index) in editForm.custom_params"
:key="param.id"
:label="param.param_name"
>
<el-input-number
v-if="isNumericParam(param)"
v-model="editForm.custom_params[index].param_value"
:precision="2"
:step="0.1"
>
<template #append v-if="param.param_unit">
{{ param.param_unit }}
</template>
</el-input-number>
<el-input
v-else
v-model="editForm.custom_params[index].param_value"
>
<template #append v-if="param.param_unit">
{{ param.param_unit }}
</template>
</el-input>
</el-form-item>
</template>
<!-- 成本信息 -->
<el-divider content-position="left">成本信息</el-divider>
<el-form-item label="实际成本(元)">
<el-input-number
v-model="editForm.actual_cost"
:precision="0"
:min="0"
:step="1000"
:controls="true"
style="width: 200px"
></el-input-number>
</el-form-item>
<el-form-item label="预测成本(元)">
<el-input-number
v-model="editForm.predicted_cost"
:precision="0"
disabled
style="width: 200px"
></el-input-number>
</el-form-item>
<el-form-item label="成本估算时间">
<el-date-picker
v-model="editForm.cost_estimate_date"
type="datetime"
placeholder="选择日期时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
disabled
></el-date-picker>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="editVisible = false">取消</el-button>
<el-button type="primary" @click="saveEdit">保存</el-button>
</span>
</template>
</el-dialog>
</el-card>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'
import { API_BASE_URL } from '@/config'
const activeTab = ref('rocket')
const rocketData = ref([])
const missileData = ref([])
const detailsVisible = ref(false)
const editVisible = ref(false)
const selectedData = ref(null)
const editForm = ref(null)
const searchQuery = ref('')
const filterManufacturer = ref('')
//
const manufacturers = computed(() => {
const data = activeTab.value === 'rocket' ? rocketData.value : missileData.value
return [...new Set(data.map(item => item.manufacturer))]
})
//
const filteredRocketData = computed(() => {
return filterData(rocketData.value)
})
const filteredMissileData = computed(() => {
return filterData(missileData.value)
})
const filterData = (data) => {
return data.filter(item => {
const matchesSearch = searchQuery.value ?
(item.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
item.manufacturer.toLowerCase().includes(searchQuery.value.toLowerCase())) : true
const matchesManufacturer = filterManufacturer.value ?
item.manufacturer === filterManufacturer.value : true
return matchesSearch && matchesManufacturer
})
}
//
const loadData = async () => {
try {
const response = await axios.get(`${API_BASE_URL}/data`)
if (response.data.error) {
throw new Error(response.data.error)
}
//
rocketData.value = response.data.rocket_artillery.map(item => ({
...item,
custom_params: item.custom_params || []
}))
//
missileData.value = response.data.loitering_munition.map(item => ({
...item,
custom_params: item.custom_params || []
}))
} catch (error) {
ElMessage.error('加载数据失败')
console.error('Error loading data:', error)
}
}
//
const handleFileChange = async (file) => {
try {
const formData = new FormData()
formData.append('file', file.raw)
const response = await axios.post(
`${API_BASE_URL}/data/import`,
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
)
if (response.data.success) {
ElMessage.success('数据导入成功')
loadData() //
} else {
throw new Error(response.data.error)
}
} catch (error) {
ElMessage.error(error.message || '数据导入失败')
}
}
//
const downloadTemplate = () => {
window.open(`${API_BASE_URL}/data/template`, '_blank')
}
//
const viewDetails = async (row) => {
try {
console.log('Requesting details for row:', row)
const response = await axios.get(`${API_BASE_URL}/data/details/${row.id}`)
if (response.data.error) {
throw new Error(response.data.error)
}
console.log('Details response:', response.data)
// custom_params
if (typeof response.data.custom_params === 'string') {
response.data.custom_params = JSON.parse(response.data.custom_params)
}
console.log('Parsed custom params:', response.data.custom_params)
selectedData.value = response.data
console.log('Selected data:', selectedData.value)
detailsVisible.value = true
} catch (error) {
ElMessage.error('获取详情失败')
console.error('Error getting details:', error)
}
}
//
const editData = async (row) => {
try {
console.log('Editing row:', row)
const response = await axios.get(`${API_BASE_URL}/data/details/${row.id}`)
if (response.data.error) {
throw new Error(response.data.error)
}
console.log('Edit data response:', response.data)
// custom_params
if (typeof response.data.custom_params === 'string') {
response.data.custom_params = JSON.parse(response.data.custom_params)
}
//
const data = { ...response.data }
Object.keys(data).forEach(key => {
if (isNumberInput(key) && data[key] !== null && data[key] !== undefined) {
data[key] = Number(data[key])
}
})
//
if (data.custom_params) {
data.custom_params = data.custom_params.map(param => ({
...param,
param_value: !isNaN(param.param_value) ? Number(param.param_value) : param.param_value
}))
}
console.log('Parsed custom params:', data.custom_params)
editForm.value = data
console.log('Edit form data:', editForm.value)
editVisible.value = true
} catch (error) {
ElMessage.error('获取编辑数据失败')
console.error('Error getting edit data:', error)
}
}
//
const saveEdit = async () => {
try {
//
const saveData = {
...editForm.value,
custom_params: editForm.value.custom_params.map(param => ({
id: param.id,
param_name: param.param_name,
param_value: param.param_value,
param_unit: param.param_unit
}))
}
const response = await axios.put(
`${API_BASE_URL}/data/${editForm.value.id}`,
saveData
)
if (response.data.success) {
ElMessage.success('保存成功')
editVisible.value = false
loadData() //
} else {
throw new Error(response.data.error)
}
} catch (error) {
ElMessage.error(error.message || '保存失败')
}
}
//
const deleteData = async (row) => {
try {
await ElMessageBox.confirm('确定要删除这条数据吗?', '警告', {
type: 'warning'
})
const response = await axios.delete(`${API_BASE_URL}/data/${row.id}`)
if (response.data.success) {
ElMessage.success('删除成功')
loadData() //
} else {
throw new Error(response.data.error)
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error(error.message || '删除失败')
}
}
}
//
const formatNumber = (value) => {
if (value === null || value === undefined) return '-'
return Number(value).toFixed(2)
}
//
const formatMoney = (value) => {
if (value === null || value === undefined) return '-'
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY'
}).format(value)
}
//
const formatCustomParamValue = (param) => {
if (!param || !param.param_value) return '-'
let value = param.param_value
//
if (!isNaN(value)) {
value = Number(value).toFixed(2)
}
//
if (param.param_unit) {
value = `${value} ${param.param_unit}`
}
return value
}
//
const isNumericParam = (param) => {
return !isNaN(param.param_value) && param.param_unit !== undefined
}
// handleTabClick
const handleTabClick = (tab) => {
//
searchQuery.value = ''
filterManufacturer.value = ''
}
//
onMounted(() => {
loadData()
})
//
const isNumberInput = (key) => {
const numberFields = [
'length_m',
'width_m',
'height_m',
'weight_kg',
'max_range_km',
'firing_angle_horizontal',
'firing_angle_vertical',
'rocket_length_m',
'rocket_diameter_mm',
'rocket_weight_kg',
'rate_of_fire',
'combat_weight_kg',
'speed_kmh',
'min_range_km',
'power_hp',
'travel_range_km',
'max_speed_ms',
'cruise_speed_kmh',
'flight_time_min',
'folded_length_mm',
'folded_width_mm',
'folded_height_mm',
'actual_cost',
'predicted_cost'
]
return numberFields.includes(key)
}
</script>
<style lang="scss" scoped>
.data-page {
padding: 20px;
.data-card {
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
h2 {
margin: 0;
}
.header-buttons {
display: flex;
gap: 10px;
}
}
}
.filter-section {
margin-bottom: 20px;
display: flex;
gap: 10px;
}
.el-table {
margin-top: 20px;
}
}
</style>

View File

@ -0,0 +1,322 @@
<template>
<div class="dataset-page">
<el-card class="dataset-card">
<template #header>
<div class="header-content">
<h2>数据集管理</h2>
<div class="header-buttons">
<el-button type="primary" @click="createDataset">创建数据集</el-button>
</div>
</div>
</template>
<!-- 数据集列表 -->
<el-table :data="datasets" border style="width: 100%">
<el-table-column prop="name" label="数据集名称"></el-table-column>
<el-table-column prop="equipment_type" label="装备类型"></el-table-column>
<el-table-column prop="purpose" label="用途"></el-table-column>
<el-table-column prop="description" label="描述"></el-table-column>
<el-table-column prop="equipment_count" label="装备数量"></el-table-column>
<el-table-column prop="created_at" label="创建时间">
<template #default="scope">
{{ formatDateTime(scope.row.created_at) }}
</template>
</el-table-column>
<el-table-column prop="updated_at" label="修改时间">
<template #default="scope">
{{ formatDateTime(scope.row.updated_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template #default="scope">
<el-button size="small" @click="viewDataset(scope.row)">查看</el-button>
<el-button size="small" type="primary" @click="editDataset(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteDataset(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 数据集详情对话框 -->
<el-dialog v-model="detailsVisible" :title="selectedDataset?.name" width="70%">
<el-descriptions :column="2" border>
<el-descriptions-item label="装备类型">{{ selectedDataset?.equipment_type }}</el-descriptions-item>
<el-descriptions-item label="用途">{{ selectedDataset?.purpose }}</el-descriptions-item>
<el-descriptions-item label="描述" :span="2">{{ selectedDataset?.description }}</el-descriptions-item>
</el-descriptions>
<!-- 数据集统计信息 -->
<div style="margin-top: 20px">
<el-divider content-position="left">统计信息</el-divider>
<el-descriptions :column="2" border>
<el-descriptions-item label="装备数量">{{ selectedDataset?.statistics?.equipment_count || 0 }}</el-descriptions-item>
<el-descriptions-item label="平均成本">{{ formatMoney(selectedDataset?.statistics?.average_cost) }}</el-descriptions-item>
<el-descriptions-item label="总成本">{{ formatMoney(selectedDataset?.statistics?.total_cost) }}</el-descriptions-item>
</el-descriptions>
</div>
<!-- 包含的装备列表 -->
<div style="margin-top: 20px">
<el-divider content-position="left">包含装备</el-divider>
<el-table :data="selectedDataset?.equipment" border>
<el-table-column prop="name" label="名称"></el-table-column>
<el-table-column prop="manufacturer" label="制造商"></el-table-column>
<el-table-column prop="actual_cost" label="成本(元)">
<template #default="scope">
{{ formatMoney(scope.row.actual_cost) }}
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button size="small" @click="viewEquipment(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
<!-- 创建/编辑数据集对话框 -->
<el-dialog v-model="editVisible" :title="datasetForm.id ? '编辑数据集' : '创建数据集'" width="70%">
<el-form :model="datasetForm" label-width="120px">
<el-form-item label="数据集名称" required>
<el-input v-model="datasetForm.name"></el-input>
</el-form-item>
<el-form-item label="装备类型" required>
<el-select v-model="datasetForm.equipment_type" @change="handleEquipmentTypeChange">
<el-option label="火箭炮" value="火箭炮"></el-option>
<el-option label="巡飞弹" value="巡飞弹"></el-option>
</el-select>
</el-form-item>
<el-form-item label="用途" required>
<el-select v-model="datasetForm.purpose">
<el-option label="训练" value="训练"></el-option>
<el-option label="验证" value="验证"></el-option>
</el-select>
</el-form-item>
<el-form-item label="描述">
<el-input type="textarea" v-model="datasetForm.description"></el-input>
</el-form-item>
<!-- 选择装备数据 -->
<el-form-item label="选择装备" required>
<el-table
:data="availableEquipment"
border
@selection-change="handleSelectionChange"
:max-height="400"
>
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="name" label="名称"></el-table-column>
<el-table-column prop="manufacturer" label="制造商"></el-table-column>
<el-table-column prop="actual_cost" label="成本(元)">
<template #default="scope">
{{ formatMoney(scope.row.actual_cost) }}
</template>
</el-table-column>
</el-table>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="editVisible = false">取消</el-button>
<el-button type="primary" @click="saveDataset">保存</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'
import { API_BASE_URL } from '@/config'
//
const datasets = ref([]) //
const selectedDataset = ref(null) //
const detailsVisible = ref(false) //
const editVisible = ref(false) //
const availableEquipment = ref([]) //
const selectedEquipment = ref([]) //
//
const datasetForm = ref({
id: null,
name: '',
equipment_type: '',
purpose: '',
description: '',
selected_equipment: []
})
//
const loadDatasets = async () => {
try {
const response = await axios.get(`${API_BASE_URL}/datasets`)
datasets.value = response.data
} catch (error) {
ElMessage.error('获取数据集列表失败')
}
}
//
const createDataset = () => {
datasetForm.value = {
id: null,
name: '',
equipment_type: '',
purpose: '',
description: '',
selected_equipment: []
}
editVisible.value = true
}
//
const editDataset = async (dataset) => {
try {
const response = await axios.get(`${API_BASE_URL}/datasets/${dataset.id}`)
datasetForm.value = response.data
await loadAvailableEquipment()
editVisible.value = true
} catch (error) {
ElMessage.error('获取数据集详情失败')
}
}
//
const viewDataset = async (dataset) => {
try {
const response = await axios.get(`${API_BASE_URL}/datasets/${dataset.id}`)
selectedDataset.value = response.data
detailsVisible.value = true
} catch (error) {
ElMessage.error('获取数据集详情失败')
}
}
//
const deleteDataset = async (dataset) => {
try {
await ElMessageBox.confirm('确定要删除这个数据集吗?', '警告', {
type: 'warning'
})
await axios.delete(`${API_BASE_URL}/datasets/${dataset.id}`)
ElMessage.success('删除成功')
loadDatasets()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
//
const loadAvailableEquipment = async () => {
try {
const response = await axios.get(`${API_BASE_URL}/data`)
availableEquipment.value = datasetForm.value.equipment_type === '火箭炮'
? response.data.rocket_artillery
: response.data.loitering_munition
} catch (error) {
ElMessage.error('获取装备列表失败')
}
}
//
const handleEquipmentTypeChange = () => {
selectedEquipment.value = [] //
loadAvailableEquipment() //
}
//
const handleSelectionChange = (selection) => {
selectedEquipment.value = selection
}
//
const saveDataset = async () => {
try {
const data = {
...datasetForm.value,
equipment_ids: selectedEquipment.value.map(item => item.id)
}
if (data.id) {
await axios.put(`${API_BASE_URL}/datasets/${data.id}`, data)
} else {
await axios.post(`${API_BASE_URL}/datasets`, data)
}
ElMessage.success('保存成功')
editVisible.value = false
loadDatasets()
} catch (error) {
ElMessage.error('保存失败')
}
}
//
const viewEquipment = (equipment) => {
//
console.log('View equipment:', equipment)
}
//
const formatMoney = (value) => {
if (value === null || value === undefined) return '-'
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY'
}).format(value)
}
//
const formatDateTime = (value) => {
if (!value) return '-'
const date = new Date(value)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
})
}
//
onMounted(() => {
loadDatasets()
})
</script>
<style lang="scss" scoped>
.dataset-page {
padding: 20px;
.dataset-card {
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
h2 {
margin: 0;
}
.header-buttons {
display: flex;
gap: 10px;
}
}
}
.el-table {
margin-top: 20px;
}
}
</style>

View File

@ -0,0 +1,101 @@
<template>
<div class="home-page">
<el-card class="welcome-card">
<template #header>
<h2>装备成本估算系统</h2>
</template>
<el-row :gutter="20">
<el-col :span="8">
<el-card @click="$router.push('/predict')">
<el-icon><Money /></el-icon>
<h3>机器学习预测</h3>
<p>基于机器学习模型的成本预测</p>
</el-card>
</el-col>
<el-col :span="8">
<el-card @click="$router.push('/pls-predict')">
<el-icon><TrendCharts /></el-icon>
<h3>PLS回归预测</h3>
<p>基于偏最小二乘回归的成本预测</p>
</el-card>
</el-col>
<el-col :span="8">
<el-card @click="$router.push('/analysis')">
<el-icon><DataAnalysis /></el-icon>
<h3>特征分析</h3>
<p>分析参数对成本的影响</p>
</el-card>
</el-col>
<el-col :span="8">
<el-card @click="$router.push('/training')">
<el-icon><Monitor /></el-icon>
<h3>模型训练</h3>
<p>训练和优化预测模型</p>
</el-card>
</el-col>
<el-col :span="8">
<el-card @click="$router.push('/data')">
<el-icon><Management /></el-icon>
<h3>数据管理</h3>
<p>管理装备数据和成本数据</p>
</el-card>
</el-col>
<el-col :span="8">
<el-card @click="$router.push('/datasets')">
<el-icon><Collection /></el-icon>
<h3>数据集管理</h3>
<p>管理训练和验证数据集</p>
</el-card>
</el-col>
</el-row>
</el-card>
</div>
</template>
<script setup>
import { Money, DataAnalysis, Monitor, Management, TrendCharts, Collection } from '@element-plus/icons-vue'
</script>
<style lang="scss" scoped>
.home-page {
padding: 20px;
.welcome-card {
max-width: 1200px;
margin: 0 auto;
h2 {
text-align: center;
margin: 0;
}
}
.el-card {
text-align: center;
cursor: pointer;
transition: all 0.3s;
margin-bottom: 20px;
&:hover {
transform: translateY(-5px);
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
}
.el-icon {
font-size: 48px;
color: #409EFF;
margin: 20px 0;
}
h3 {
margin: 10px 0;
font-size: 18px;
}
p {
color: #909399;
font-size: 14px;
}
}
}
</style>

View File

@ -0,0 +1,268 @@
<template>
<div class="model-page">
<el-card class="model-card">
<template #header>
<div class="header-content">
<h2>模型管理</h2>
</div>
</template>
<!-- 模型列表 -->
<el-table :data="models" border style="width: 100%">
<el-table-column prop="model_name" label="模型名称"></el-table-column>
<el-table-column prop="model_type" label="模型类型">
<template #default="scope">
{{ formatModelType(scope.row.model_type) }}
</template>
</el-table-column>
<el-table-column prop="equipment_type" label="装备类型"></el-table-column>
<el-table-column prop="r2_score" label="R²分数">
<template #default="scope">
{{ scope.row.r2_score.toFixed(4) }}
</template>
</el-table-column>
<el-table-column prop="mae" label="MAE (元)">
<template #default="scope">
{{ scope.row.mae.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="rmse" label="RMSE (元)">
<template #default="scope">
{{ scope.row.rmse.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="training_date" label="训练时间">
<template #default="scope">
{{ formatDateTime(scope.row.training_date) }}
</template>
</el-table-column>
<el-table-column prop="is_active" label="状态">
<template #default="scope">
<el-tag :type="scope.row.is_active ? 'success' : 'info'">
{{ scope.row.is_active ? '使用中' : '未使用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template #default="scope">
<el-button
size="small"
type="primary"
:disabled="scope.row.is_active"
@click="activateModel(scope.row)"
>
激活
</el-button>
<el-button
size="small"
@click="viewDetails(scope.row)"
>
详情
</el-button>
<el-button
size="small"
type="danger"
:disabled="scope.row.is_active"
@click="deleteModel(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 模型详情对话框 -->
<el-dialog v-model="detailsVisible" title="模型详情" width="70%">
<el-descriptions :column="2" border>
<el-descriptions-item label="模型名称">{{ selectedModel?.model_name }}</el-descriptions-item>
<el-descriptions-item label="模型类型">{{ formatModelType(selectedModel?.model_type) }}</el-descriptions-item>
<el-descriptions-item label="装备类型">{{ selectedModel?.equipment_type }}</el-descriptions-item>
<el-descriptions-item label="训练数据量">{{ selectedModel?.training_data_size }}</el-descriptions-item>
<el-descriptions-item label="训练时间">{{ formatDateTime(selectedModel?.training_date) }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="selectedModel?.is_active ? 'success' : 'info'">
{{ selectedModel?.is_active ? '使用中' : '未使用' }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
<!-- 评估指标 -->
<div style="margin-top: 20px">
<el-divider content-position="left">评估指标</el-divider>
<el-descriptions :column="3" border>
<el-descriptions-item label="R²分数">{{ selectedModel?.r2_score.toFixed(4) }}</el-descriptions-item>
<el-descriptions-item label="MAE">{{ selectedModel?.mae.toFixed(2) }} </el-descriptions-item>
<el-descriptions-item label="RMSE">{{ selectedModel?.rmse.toFixed(2) }} </el-descriptions-item>
</el-descriptions>
</div>
<!-- 特征重要性 -->
<div v-if="selectedModel?.feature_importance" style="margin-top: 20px">
<el-divider content-position="left">特征重要性</el-divider>
<div ref="importanceChartRef" style="width: 100%; height: 400px"></div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, watch, nextTick, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'
import { API_BASE_URL } from '@/config'
import * as echarts from 'echarts'
//
const models = ref([])
const selectedModel = ref(null)
const detailsVisible = ref(false)
const importanceChartRef = ref(null)
const importanceChart = ref(null)
//
const loadModels = async () => {
try {
const response = await axios.get(`${API_BASE_URL}/models`)
models.value = response.data
} catch (error) {
ElMessage.error('获取模型列表失败')
}
}
//
const activateModel = async (model) => {
try {
await ElMessageBox.confirm('确定要激活这个模型吗?', '提示', {
type: 'warning'
})
await axios.post(`${API_BASE_URL}/models/${model.id}/activate`)
ElMessage.success('模型激活成功')
loadModels()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('模型激活失败')
}
}
}
//
const deleteModel = async (model) => {
try {
await ElMessageBox.confirm('确定要删除这个模型吗?', '警告', {
type: 'warning'
})
await axios.delete(`${API_BASE_URL}/models/${model.id}`)
ElMessage.success('删除成功')
loadModels()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
//
const viewDetails = async (model) => {
selectedModel.value = model
detailsVisible.value = true
}
//
watch(() => detailsVisible.value, async (visible) => {
if (visible && selectedModel.value?.feature_importance) {
await nextTick()
renderImportanceChart()
}
})
//
const renderImportanceChart = () => {
if (importanceChart.value) {
importanceChart.value.dispose()
}
importanceChart.value = echarts.init(importanceChartRef.value)
const featureImportance = JSON.parse(selectedModel.value.feature_importance)
const features = Object.keys(featureImportance)
const values = Object.values(featureImportance)
importanceChart.value.setOption({
title: { text: '特征重要性' },
tooltip: {},
xAxis: {
type: 'value',
name: '重要性得分'
},
yAxis: {
type: 'category',
data: features
},
series: [{
type: 'bar',
data: values
}]
})
}
//
const formatModelType = (type) => {
const typeMap = {
'xgboost': 'XGBoost',
'lightgbm': 'LightGBM',
'gbdt': 'GBDT',
'rf': 'Random Forest'
}
return typeMap[type] || type
}
//
const formatDateTime = (value) => {
if (!value) return '-'
const date = new Date(value)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
})
}
//
onUnmounted(() => {
importanceChart.value?.dispose()
})
//
onMounted(() => {
loadModels()
})
</script>
<style lang="scss" scoped>
.model-page {
padding: 20px;
.model-card {
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
h2 {
margin: 0;
}
}
}
.el-table {
margin-top: 20px;
}
}
</style>

View File

@ -0,0 +1,243 @@
<template>
<div class="predict-page">
<el-card class="predict-card">
<template #header>
<h2>PLS回归预测</h2>
</template>
<el-form :model="formData" label-width="120px">
<!-- 装备类型选择 -->
<el-form-item label="装备类型">
<el-select v-model="formData.type" @change="handleTypeChange">
<el-option label="火箭炮" value="火箭炮"></el-option>
<el-option label="巡飞弹" value="巡飞弹"></el-option>
</el-select>
</el-form-item>
<!-- 通用参数 -->
<el-form-item label="总长(m)">
<el-input-number v-model="formData.length_m" :precision="2"></el-input-number>
</el-form-item>
<el-form-item label="宽度(m)">
<el-input-number v-model="formData.width_m" :precision="2"></el-input-number>
</el-form-item>
<el-form-item label="高度(m)">
<el-input-number v-model="formData.height_m" :precision="2"></el-input-number>
</el-form-item>
<el-form-item label="重量(kg)">
<el-input-number v-model="formData.weight_kg"></el-input-number>
</el-form-item>
<el-form-item label="最大射程(km)">
<el-input-number v-model="formData.max_range_km"></el-input-number>
</el-form-item>
<!-- 火箭炮特有参数 -->
<template v-if="formData.type === '火箭炮'">
<el-form-item label="方向射界(度)">
<el-input-number v-model="formData.firing_angle_horizontal"></el-input-number>
</el-form-item>
<el-form-item label="高低射界(度)">
<el-input-number v-model="formData.firing_angle_vertical"></el-input-number>
</el-form-item>
<el-form-item label="火箭弹长度(m)">
<el-input-number v-model="formData.rocket_length_m" :precision="2"></el-input-number>
</el-form-item>
<el-form-item label="弹体直径(mm)">
<el-input-number v-model="formData.rocket_diameter_mm"></el-input-number>
</el-form-item>
<el-form-item label="火箭弹重量(kg)">
<el-input-number v-model="formData.rocket_weight_kg"></el-input-number>
</el-form-item>
<el-form-item label="射速(发/分钟)">
<el-input-number v-model="formData.rate_of_fire"></el-input-number>
</el-form-item>
</template>
<!-- 巡飞弹特有参数 -->
<template v-if="formData.type === '巡飞弹'">
<el-form-item label="最大速度(km/h)">
<el-input-number v-model="formData.max_speed_kmh"></el-input-number>
</el-form-item>
<el-form-item label="巡航速度(km/h)">
<el-input-number v-model="formData.cruise_speed_kmh"></el-input-number>
</el-form-item>
<el-form-item label="巡飞时间(min)">
<el-input-number v-model="formData.flight_time_min"></el-input-number>
</el-form-item>
<el-form-item label="折叠长度(mm)">
<el-input-number v-model="formData.folded_length_mm"></el-input-number>
</el-form-item>
<el-form-item label="折叠宽度(mm)">
<el-input-number v-model="formData.folded_width_mm"></el-input-number>
</el-form-item>
<el-form-item label="折叠高度(mm)">
<el-input-number v-model="formData.folded_height_mm"></el-input-number>
</el-form-item>
</template>
<el-form-item>
<el-button type="primary" @click="submitForm">预测成本</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
<!-- 预测结果 -->
<div v-if="predictionResult" class="prediction-result">
<h3>预测结果</h3>
<el-descriptions border>
<el-descriptions-item label="预测成本">
{{ formatMoney(predictionResult.predicted_cost) }}
</el-descriptions-item>
<el-descriptions-item label="置信区间">
{{ formatMoney(predictionResult.confidence_interval.lower) }} ~
{{ formatMoney(predictionResult.confidence_interval.upper) }}
</el-descriptions-item>
</el-descriptions>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import axios from 'axios'
import { API_BASE_URL } from '@/config'
const formData = reactive({
type: '',
length_m: null,
width_m: null,
height_m: null,
weight_kg: null,
max_range_km: null
})
const predictionResult = ref(null)
const handleTypeChange = () => {
if (formData.type === '火箭炮') {
Object.assign(formData, {
firing_angle_horizontal: null,
firing_angle_vertical: null,
rocket_length_m: null,
rocket_diameter_mm: null,
rocket_weight_kg: null,
rate_of_fire: null
})
} else if (formData.type === '巡飞弹') {
Object.assign(formData, {
max_speed_kmh: null,
cruise_speed_kmh: null,
flight_time_min: null,
folded_length_mm: null,
folded_width_mm: null,
folded_height_mm: null
})
}
}
const submitForm = async () => {
try {
//
if (!formData.type) {
throw new Error('请选择装备类型')
}
//
const commonFields = ['length_m', 'width_m', 'height_m', 'weight_kg', 'max_range_km']
for (const field of commonFields) {
if (!formData[field]) {
throw new Error(`请输入${formatFieldName(field)}`)
}
}
//
if (formData.type === '火箭炮') {
const rocketFields = [
'firing_angle_horizontal', 'firing_angle_vertical',
'rocket_length_m', 'rocket_diameter_mm', 'rocket_weight_kg', 'rate_of_fire'
]
for (const field of rocketFields) {
if (!formData[field]) {
throw new Error(`请输入${formatFieldName(field)}`)
}
}
} else if (formData.type === '巡飞弹') {
const missileFields = [
'max_speed_kmh', 'cruise_speed_kmh', 'flight_time_min',
'folded_length_mm', 'folded_width_mm', 'folded_height_mm'
]
for (const field of missileFields) {
if (!formData[field]) {
throw new Error(`请输入${formatFieldName(field)}`)
}
}
}
//
const response = await axios.post(`${API_BASE_URL}/pls/predict`, formData)
predictionResult.value = response.data
} catch (error) {
ElMessage.error(error.message || '预测失败')
}
}
const resetForm = () => {
formData.type = ''
formData.length_m = null
formData.width_m = null
formData.height_m = null
formData.weight_kg = null
formData.max_range_km = null
predictionResult.value = null
}
const formatFieldName = (field) => {
const nameMap = {
'length_m': '总长',
'width_m': '宽度',
'height_m': '高度',
'weight_kg': '重量',
'max_range_km': '最大射程',
'firing_angle_horizontal': '方向射界',
'firing_angle_vertical': '高低射界',
'rocket_length_m': '火箭弹长度',
'rocket_diameter_mm': '弹体直径',
'rocket_weight_kg': '火箭弹重量',
'rate_of_fire': '射速',
'max_speed_kmh': '最大速度',
'cruise_speed_kmh': '巡航速度',
'flight_time_min': '巡飞时间',
'folded_length_mm': '折叠长度',
'folded_width_mm': '折叠宽度',
'folded_height_mm': '折叠高度'
}
return nameMap[field] || field
}
//
const formatMoney = (value) => {
return new Intl.NumberFormat('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(value)
}
</script>
<style scoped>
.predict-page {
padding: 20px;
}
.predict-card {
max-width: 800px;
margin: 0 auto;
}
.prediction-result {
margin-top: 20px;
padding: 20px;
background-color: #f5f7fa;
border-radius: 4px;
}
</style>

View File

@ -0,0 +1,208 @@
<template>
<div class="predict-page">
<el-card class="predict-card">
<template #header>
<h2>装备成本预测</h2>
</template>
<!-- 装备类型选择 -->
<el-form :model="formData" label-width="120px">
<el-form-item label="装备类型">
<el-select v-model="formData.type" @change="handleTypeChange">
<el-option label="火箭炮" value="火箭炮"></el-option>
<el-option label="巡飞弹" value="巡飞弹"></el-option>
</el-select>
</el-form-item>
<!-- 通用参数 -->
<el-form-item label="总长(m)">
<el-input-number v-model="formData.length_m" :precision="2"></el-input-number>
</el-form-item>
<el-form-item label="宽度(m)">
<el-input-number v-model="formData.width_m" :precision="2"></el-input-number>
</el-form-item>
<el-form-item label="高度(m)">
<el-input-number v-model="formData.height_m" :precision="2"></el-input-number>
</el-form-item>
<el-form-item label="重量(kg)">
<el-input-number v-model="formData.weight_kg"></el-input-number>
</el-form-item>
<el-form-item label="最大射程(km)">
<el-input-number v-model="formData.max_range_km"></el-input-number>
</el-form-item>
<!-- 火箭炮特有参数 -->
<template v-if="formData.type === '火箭炮'">
<el-form-item label="方向射界(度)">
<el-input-number v-model="formData.firing_angle_horizontal"></el-input-number>
</el-form-item>
<el-form-item label="高低射界(度)">
<el-input-number v-model="formData.firing_angle_vertical"></el-input-number>
</el-form-item>
<el-form-item label="火箭弹长度(m)">
<el-input-number v-model="formData.rocket_length_m" :precision="2"></el-input-number>
</el-form-item>
<el-form-item label="弹体直径(mm)">
<el-input-number v-model="formData.rocket_diameter_mm"></el-input-number>
</el-form-item>
<el-form-item label="火箭弹重量(kg)">
<el-input-number v-model="formData.rocket_weight_kg"></el-input-number>
</el-form-item>
<el-form-item label="射速(发/分钟)">
<el-input-number v-model="formData.rate_of_fire"></el-input-number>
</el-form-item>
</template>
<!-- 巡飞弹特有参数 -->
<template v-if="formData.type === '巡飞弹'">
<el-form-item label="最大速度(km/h)">
<el-input-number v-model="formData.max_speed_kmh"></el-input-number>
</el-form-item>
<el-form-item label="巡航速度(km/h)">
<el-input-number v-model="formData.cruise_speed_kmh"></el-input-number>
</el-form-item>
<el-form-item label="巡飞时间(min)">
<el-input-number v-model="formData.flight_time_min"></el-input-number>
</el-form-item>
<el-form-item label="战斗部类型">
<el-select v-model="formData.warhead_type">
<el-option label="破片杀伤战斗部" value="破片杀伤战斗部"></el-option>
<el-option label="破甲战斗部" value="破甲战斗部"></el-option>
<el-option label="高爆战斗部" value="高爆战斗部"></el-option>
</el-select>
</el-form-item>
<el-form-item label="发射方式">
<el-select v-model="formData.launch_mode">
<el-option label="箱式发射" value="箱式发射"></el-option>
<el-option label="凭自身动力起飞" value="凭自身动力起飞"></el-option>
</el-select>
</el-form-item>
<el-form-item label="折叠长度(mm)">
<el-input-number v-model="formData.folded_length_mm"></el-input-number>
</el-form-item>
<el-form-item label="折叠宽度(mm)">
<el-input-number v-model="formData.folded_width_mm"></el-input-number>
</el-form-item>
<el-form-item label="折叠高度(mm)">
<el-input-number v-model="formData.folded_height_mm"></el-input-number>
</el-form-item>
</template>
<el-form-item>
<el-button type="primary" @click="submitForm">预测成本</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
<!-- 预测结果 -->
<div v-if="predictionResult" class="prediction-result">
<h3>预测结果</h3>
<el-descriptions border>
<el-descriptions-item label="预测成本">
{{ formatCurrency(predictionResult.predicted_cost) }}
</el-descriptions-item>
<el-descriptions-item label="置信区间">
{{ formatCurrency(predictionResult.confidence_interval.lower) }} ~
{{ formatCurrency(predictionResult.confidence_interval.upper) }}
</el-descriptions-item>
</el-descriptions>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import axios from 'axios'
import { API_BASE_URL } from '@/config'
const formData = ref({
type: '',
length_m: null,
width_m: null,
height_m: null,
weight_kg: null,
max_range_km: null
})
const predictionResult = ref(null)
const handleTypeChange = () => {
//
if (formData.value.type === '火箭炮') {
formData.value = {
...formData.value,
firing_angle_horizontal: null,
firing_angle_vertical: null,
rocket_length_m: null,
rocket_diameter_mm: null,
rocket_weight_kg: null,
rate_of_fire: null
}
} else if (formData.value.type === '巡飞弹') {
formData.value = {
...formData.value,
max_speed_kmh: null,
cruise_speed_kmh: null,
flight_time_min: null,
warhead_type: '',
launch_mode: '',
folded_length_mm: null,
folded_width_mm: null,
folded_height_mm: null
}
}
}
const submitForm = async () => {
try {
const response = await axios.post(`${API_BASE_URL}/predict`, formData.value)
predictionResult.value = response.data
} catch (error) {
ElMessage.error(error.response?.data?.error || '预测失败')
}
}
const resetForm = () => {
formData.value = {
type: '',
length_m: null,
width_m: null,
height_m: null,
weight_kg: null,
max_range_km: null
}
predictionResult.value = null
}
const formatCurrency = (value) => {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY'
}).format(value)
}
</script>
<style lang="scss" scoped>
.predict-page {
padding: 20px;
.predict-card {
max-width: 800px;
margin: 0 auto;
h2 {
text-align: center;
margin: 0;
}
}
.prediction-result {
margin-top: 20px;
padding: 20px;
background-color: #f5f7fa;
border-radius: 4px;
}
}
</style>

View File

@ -0,0 +1,294 @@
<template>
<div class="training-page">
<el-card class="training-card">
<template #header>
<h2>模型训练</h2>
</template>
<!-- 训练配置 -->
<el-form :model="formData" label-width="120px">
<el-form-item label="装备类型" required>
<el-select v-model="formData.type" @change="handleTypeChange">
<el-option label="火箭炮" value="火箭炮"></el-option>
<el-option label="巡飞弹" value="巡飞弹"></el-option>
</el-select>
</el-form-item>
<!-- 选择训练集 -->
<el-form-item label="训练数据集" required>
<el-select v-model="formData.train_dataset_id" placeholder="选择训练数据集">
<el-option
v-for="dataset in trainingDatasets"
:key="dataset.id"
:label="dataset.name"
:value="dataset.id"
></el-option>
</el-select>
</el-form-item>
<!-- 选择验证集 -->
<el-form-item label="验证数据集">
<el-select v-model="formData.validation_dataset_id" placeholder="选择验证数据集" clearable>
<el-option
v-for="dataset in validationDatasets"
:key="dataset.id"
:label="dataset.name"
:value="dataset.id"
></el-option>
</el-select>
</el-form-item>
<!-- 模型选择 -->
<el-form-item label="训练模型" required>
<el-checkbox-group v-model="formData.models">
<el-checkbox label="xgboost">XGBoost</el-checkbox>
<el-checkbox label="lightgbm">LightGBM</el-checkbox>
<el-checkbox label="gbdt">GBDT</el-checkbox>
<el-checkbox label="rf">Random Forest</el-checkbox>
</el-checkbox-group>
</el-form-item>
<!-- 开始训练按钮 -->
<el-form-item>
<el-button type="primary" @click="startTraining" :loading="training">
{{ training ? '训练中...' : '开始训练' }}
</el-button>
</el-form-item>
</el-form>
<!-- 训练结果 -->
<div v-if="trainingResult" class="training-result">
<h3>训练结果</h3>
<!-- 模型评估指标 -->
<el-table :data="modelMetrics" border style="width: 100%">
<el-table-column prop="model" label="模型">
<template #default="scope">
{{ formatModelName(scope.row.model) }}
</template>
</el-table-column>
<el-table-column label="训练集评估">
<el-table-column prop="train.r2" label="R²分数">
<template #default="scope">
{{ scope.row.train.r2.toFixed(4) }}
</template>
</el-table-column>
<el-table-column prop="train.mae" label="MAE (元)">
<template #default="scope">
{{ scope.row.train.mae.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="train.rmse" label="RMSE (元)">
<template #default="scope">
{{ scope.row.train.rmse.toFixed(2) }}
</template>
</el-table-column>
</el-table-column>
<el-table-column label="验证集评估" v-if="formData.validation_dataset_id">
<el-table-column prop="validation.r2" label="R²分数">
<template #default="scope">
{{ scope.row.validation.r2.toFixed(4) }}
</template>
</el-table-column>
<el-table-column prop="validation.mae" label="MAE (元)">
<template #default="scope">
{{ scope.row.validation.mae.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="validation.rmse" label="RMSE (元)">
<template #default="scope">
{{ scope.row.validation.rmse.toFixed(2) }}
</template>
</el-table-column>
</el-table-column>
</el-table>
<!-- 特征重要性 -->
<div v-if="trainingResult.feature_importance" class="feature-importance">
<h4>特征重要性</h4>
<el-table :data="featureImportanceData" border style="width: 100%">
<el-table-column prop="feature" label="特征"></el-table-column>
<el-table-column prop="importance" label="重要性">
<template #default="scope">
<el-progress
:percentage="scope.row.importance * 100"
:format="format => format.toFixed(2) + '%'"
:color="getImportanceColor(scope.row.importance)"
></el-progress>
</template>
</el-table-column>
</el-table>
</div>
<!-- 最佳模型信息 -->
<div v-if="trainingResult.best_model" class="best-model">
<h4>最佳模型</h4>
<el-descriptions :column="2" border>
<el-descriptions-item label="模型类型">
{{ formatModelName(trainingResult.best_model.type) }}
</el-descriptions-item>
<el-descriptions-item label="R²分数">
{{ trainingResult.best_model.r2.toFixed(4) }}
</el-descriptions-item>
<el-descriptions-item label="MAE">
{{ formatMoney(trainingResult.best_model.mae) }}
</el-descriptions-item>
<el-descriptions-item label="RMSE">
{{ formatMoney(trainingResult.best_model.rmse) }}
</el-descriptions-item>
</el-descriptions>
</div>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import axios from 'axios'
import { API_BASE_URL } from '@/config'
//
const formData = ref({
type: '',
train_dataset_id: null,
validation_dataset_id: null,
models: ['xgboost', 'lightgbm', 'gbdt', 'rf']
})
const trainingDatasets = ref([])
const validationDatasets = ref([])
const training = ref(false)
const trainingResult = ref(null)
//
const loadDatasets = async (type) => {
try {
const response = await axios.get(`${API_BASE_URL}/datasets`, {
params: {
equipment_type: type,
purpose: '训练' //
}
})
trainingDatasets.value = response.data
//
const validationResponse = await axios.get(`${API_BASE_URL}/datasets`, {
params: {
equipment_type: type,
purpose: '验证'
}
})
validationDatasets.value = validationResponse.data
} catch (error) {
ElMessage.error('获取数据集列表失败')
}
}
//
const handleTypeChange = () => {
formData.value.train_dataset_id = null
formData.value.validation_dataset_id = null
loadDatasets(formData.value.type)
}
//
const startTraining = async () => {
try {
//
if (!formData.value.type) {
throw new Error('请选择装备类型')
}
if (!formData.value.train_dataset_id) {
throw new Error('请选择训练数据集')
}
if (formData.value.models.length === 0) {
throw new Error('请至少选择一个训练模型')
}
training.value = true
//
const response = await axios.post(`${API_BASE_URL}/train`, {
type: formData.value.type,
train_dataset_id: formData.value.train_dataset_id,
validation_dataset_id: formData.value.validation_dataset_id,
models: formData.value.models
})
trainingResult.value = response.data
ElMessage.success('训练完成')
} catch (error) {
console.error('Training error:', error)
ElMessage.error(error.message || '训练失败')
} finally {
training.value = false
}
}
//
const formatModelName = (name) => {
const nameMap = {
'xgboost': 'XGBoost',
'lightgbm': 'LightGBM',
'gbdt': 'GBDT',
'rf': 'Random Forest'
}
return nameMap[name] || name
}
//
const getImportanceColor = (value) => {
if (value >= 0.5) return '#67C23A' //
if (value >= 0.2) return '#E6A23C' //
return '#F56C6C' //
}
//
const modelMetrics = computed(() => {
if (!trainingResult.value?.metrics) return []
return Object.entries(trainingResult.value.metrics).map(([model, metrics]) => ({
model,
train: metrics.train,
validation: metrics.validation
}))
})
//
const formatMoney = (value) => {
if (value === null || value === undefined) return '-'
return `${value.toFixed(2)} 元 (预测误差)`
}
//
onMounted(() => {
//
})
</script>
<style lang="scss" scoped>
.training-page {
padding: 20px;
.training-card {
max-width: 800px;
margin: 0 auto;
}
.training-result {
margin-top: 20px;
padding: 20px;
background-color: #f5f7fa;
border-radius: 4px;
}
h3, h4 {
margin: 20px 0;
padding-left: 10px;
border-left: 4px solid #409EFF;
}
}
</style>

5
frontend/vue.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
devServer: {
port: 8080
}
}