Fix bugs
This commit is contained in:
parent
865c93c811
commit
bef24985c9
1
frontend
1
frontend
@ -1 +0,0 @@
|
||||
Subproject commit 96445d75411a5f9ace114085af0872cfbc116515
|
||||
24
frontend/README.md
Normal file
24
frontend/README.md
Normal 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
5
frontend/babel.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
20
frontend/jsconfig.json
Normal file
20
frontend/jsconfig.json
Normal 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
12736
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
61
frontend/package.json
Normal file
61
frontend/package.json
Normal 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
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
17
frontend/public/index.html
Normal file
17
frontend/public/index.html
Normal 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
43
frontend/src/App.vue
Normal 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
43
frontend/src/api/index.js
Normal 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}`)
|
||||
}
|
||||
BIN
frontend/src/assets/logo.png
Normal file
BIN
frontend/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
39
frontend/src/assets/styles/global.css
Normal file
39
frontend/src/assets/styles/global.css
Normal 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
8
frontend/src/config.js
Normal 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
55
frontend/src/main.js
Normal 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)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
58
frontend/src/router/index.js
Normal file
58
frontend/src/router/index.js
Normal 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
|
||||
14
frontend/src/store/index.js
Normal file
14
frontend/src/store/index.js
Normal file
@ -0,0 +1,14 @@
|
||||
import { createStore } from 'vuex'
|
||||
|
||||
export default createStore({
|
||||
state: {
|
||||
},
|
||||
getters: {
|
||||
},
|
||||
mutations: {
|
||||
},
|
||||
actions: {
|
||||
},
|
||||
modules: {
|
||||
}
|
||||
})
|
||||
73
frontend/src/utils/errorHandler.js
Normal file
73
frontend/src/utils/errorHandler.js
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
307
frontend/src/views/AnalysisPage.vue
Normal file
307
frontend/src/views/AnalysisPage.vue
Normal 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>
|
||||
725
frontend/src/views/DataPage.vue
Normal file
725
frontend/src/views/DataPage.vue
Normal 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>
|
||||
322
frontend/src/views/DatasetPage.vue
Normal file
322
frontend/src/views/DatasetPage.vue
Normal 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>
|
||||
101
frontend/src/views/HomePage.vue
Normal file
101
frontend/src/views/HomePage.vue
Normal 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>
|
||||
268
frontend/src/views/ModelPage.vue
Normal file
268
frontend/src/views/ModelPage.vue
Normal 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>
|
||||
243
frontend/src/views/PLSPredictPage.vue
Normal file
243
frontend/src/views/PLSPredictPage.vue
Normal 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>
|
||||
208
frontend/src/views/PredictPage.vue
Normal file
208
frontend/src/views/PredictPage.vue
Normal 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>
|
||||
294
frontend/src/views/TrainingPage.vue
Normal file
294
frontend/src/views/TrainingPage.vue
Normal 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
5
frontend/vue.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
devServer: {
|
||||
port: 8080
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user