前端页面总算能显示出来了, 功能还没有开始做

This commit is contained in:
haotian 2025-02-25 14:59:28 +08:00
parent e43ac40027
commit bf574b3dc5
15 changed files with 2392 additions and 0 deletions

1546
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
frontend/package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "ml-platform-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.6.2",
"dayjs": "^1.11.10",
"element-plus": "^2.4.3",
"pinia": "^2.1.7",
"vue": "^3.3.9",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@types/node": "^20.10.3",
"@vitejs/plugin-vue": "^4.5.1",
"typescript": "^5.3.2",
"vite": "^4.5.0",
"vue-tsc": "^1.8.24"
}
}

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

@ -0,0 +1,68 @@
<template>
<el-config-provider :locale="zhCn">
<div class="app-container">
<!-- 侧边栏 -->
<el-menu
class="sidebar"
:router="true"
:default-active="$route.path"
>
<el-menu-item index="/data">
<el-icon><Document /></el-icon>
<span>数据管理</span>
</el-menu-item>
<el-menu-item index="/model">
<el-icon><Monitor /></el-icon>
<span>模型管理</span>
</el-menu-item>
<el-menu-item index="/system">
<el-icon><Setting /></el-icon>
<span>系统监控</span>
</el-menu-item>
</el-menu>
<!-- 主内容区 -->
<div class="main-content">
<router-view />
</div>
</div>
</el-config-provider>
</template>
<script setup lang="ts">
import { Document, Monitor, Setting } from '@element-plus/icons-vue'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
</script>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
height: 100%;
}
.app-container {
display: flex;
height: 100%;
}
.sidebar {
width: 200px;
height: 100%;
border-right: solid 1px #e6e6e6;
}
.main-content {
flex: 1;
padding: 20px;
overflow: auto;
}
</style>

67
frontend/src/api/data.ts Normal file
View File

@ -0,0 +1,67 @@
import request from '@/utils/request'
import type { AxiosResponse } from 'axios'
import type {
PreprocessMethod,
FeatureMethod,
ProcessRequest,
CSVRequest,
DatasetInfo
} from '@/types/data'
// 获取预处理方法列表
export const getPreprocessMethods = (): Promise<AxiosResponse<PreprocessMethod[]>> => {
return request({
url: '/data/preprocessing/methods',
method: 'get'
})
}
// 获取预处理方法详情
export const getPreprocessMethodDetails = (methodName: string): Promise<AxiosResponse> => {
return request({
url: `/data/preprocessing/method/${methodName}`,
method: 'get'
})
}
// 获取特征工程方法列表
export const getFeatureMethods = (): Promise<AxiosResponse<FeatureMethod[]>> => {
return request({
url: '/data/feature/methods',
method: 'get'
})
}
// 获取特征工程方法详情
export const getFeatureMethodDetails = (methodName: string): Promise<AxiosResponse> => {
return request({
url: `/data/feature/method/${methodName}`,
method: 'get'
})
}
// 处理数据集
export const processDataset = (data: ProcessRequest): Promise<AxiosResponse> => {
return request({
url: '/data/process',
method: 'post',
data
})
}
// 获取可用数据集列表
export const getDatasets = (): Promise<AxiosResponse<DatasetInfo[]>> => {
return request({
url: '/data/datasets',
method: 'get'
})
}
// 读取CSV文件
export const readCSV = (data: CSVRequest): Promise<AxiosResponse> => {
return request({
url: '/data/csv',
method: 'post',
data
})
}

View File

@ -0,0 +1,7 @@
// 健康检查
export const checkHealth = (): Promise<AxiosResponse> => {
return request({
url: '/health',
method: 'get'
})
}

View File

@ -0,0 +1,175 @@
<template>
<el-dialog
v-model="dialogVisible"
title="数据预览"
width="80%"
>
<el-tabs v-model="activeTab">
<!-- 训练集预览 -->
<el-tab-pane label="训练集" name="train">
<div v-loading="loading.train">
<el-descriptions title="数据集信息" :column="3" border>
<el-descriptions-item label="行数">
{{ dataInfo.train?.info?.rows || '-' }}
</el-descriptions-item>
<el-descriptions-item label="列数">
{{ dataInfo.train?.info?.columns || '-' }}
</el-descriptions-item>
<el-descriptions-item label="内存占用">
{{ dataInfo.train?.info?.memory_usage || '-' }}
</el-descriptions-item>
</el-descriptions>
<div class="mt-4">
<h4>数据预览</h4>
<el-table
:data="dataInfo.train?.head || []"
style="width: 100%"
max-height="400"
border
>
<el-table-column
v-for="col in getColumns(dataInfo.train?.head)"
:key="col"
:prop="col"
:label="col"
/>
</el-table>
</div>
<div class="mt-4">
<h4>数据统计</h4>
<el-table
:data="getDescribeData(dataInfo.train?.describe)"
style="width: 100%"
border
>
<el-table-column prop="metric" label="统计量" width="180" />
<el-table-column
v-for="col in getDescribeColumns(dataInfo.train?.describe)"
:key="col"
:prop="col"
:label="col"
/>
</el-table>
</div>
</div>
</el-tab-pane>
<!-- 验证集预览 -->
<el-tab-pane label="验证集" name="val">
<!-- 与训练集类似的内容 -->
</el-tab-pane>
<!-- 测试集预览 -->
<el-tab-pane label="测试集" name="test">
<!-- 与训练集类似的内容 -->
</el-tab-pane>
</el-tabs>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import type { DatasetInfo } from '@/types/data'
import { readCSV } from '@/api/data'
const props = defineProps<{
visible: boolean
dataset: DatasetInfo | null
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
}>()
const dialogVisible = ref(false)
const activeTab = ref('train')
const loading = ref({
train: false,
val: false,
test: false
})
const dataInfo = ref({
train: null,
val: null,
test: null
})
//
watch(() => props.visible, (val) => {
dialogVisible.value = val
if (val && props.dataset) {
loadData('train')
}
})
//
watch(dialogVisible, (val) => {
emit('update:visible', val)
})
//
watch(activeTab, (val) => {
if (!dataInfo.value[val]) {
loadData(val)
}
})
//
const loadData = async (type: 'train' | 'val' | 'test') => {
if (!props.dataset) return
const filePath = props.dataset.output_files[type]
if (!filePath) return
loading.value[type] = true
try {
const { data } = await readCSV({
data_path: filePath,
head: 10,
tail: 5,
info: true,
describe: true
})
dataInfo.value[type] = data.data
} catch (error) {
console.error(error)
} finally {
loading.value[type] = false
}
}
//
const getColumns = (data: any[] = []) => {
return data.length > 0 ? Object.keys(data[0]) : []
}
//
const getDescribeColumns = (describe: any = {}) => {
return Object.keys(describe)
}
//
const getDescribeData = (describe: any = {}) => {
if (!describe) return []
const metrics = ['count', 'mean', 'std', 'min', '25%', '50%', '75%', 'max']
return metrics.map(metric => ({
metric,
...Object.fromEntries(
Object.entries(describe).map(([col, stats]: [string, any]) => [
col,
typeof stats === 'object' ? stats[metric] : null
])
)
}))
}
</script>
<style scoped>
.mt-4 {
margin-top: 1rem;
}
</style>

View File

@ -0,0 +1,192 @@
<template>
<div class="dataset-list">
<el-table :data="datasets" style="width: 100%">
<el-table-column prop="input_file" label="数据集" width="200">
<template #default="{ row }">
{{ getFileName(row.input_file) }}
</template>
</el-table-column>
<el-table-column prop="timestamp" label="处理时间" width="180">
<template #default="{ row }">
{{ formatTime(row.timestamp) }}
</template>
</el-table-column>
<el-table-column label="数据处理">
<template #default="{ row }">
<el-tag
v-for="method in row.process_methods"
:key="method.method_name"
class="mx-1"
>
{{ method.method_name }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="特征工程">
<template #default="{ row }">
<el-tag
v-for="method in row.feature_methods"
:key="method.method_name"
type="success"
class="mx-1"
>
{{ method.method_name }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="数据划分">
<template #default="{ row }">
<el-tooltip effect="dark" placement="top">
<template #content>
<div>训练集: {{ (1 - row.split_params.test_size - row.split_params.val_size) * 100 }}%</div>
<div>验证集: {{ row.split_params.val_size * 100 }}%</div>
<div>测试集: {{ row.split_params.test_size * 100 }}%</div>
</template>
<el-progress :percentage="100" :format="() => '查看详情'" />
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template #default="{ row }">
<el-button
type="primary"
link
@click="handlePreview(row)"
>
预览
</el-button>
<el-button
type="success"
link
@click="handleDownload(row)"
>
下载
</el-button>
<el-button
type="info"
link
@click="handleDetails(row)"
>
详情
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 预览对话框 -->
<DataPreview
v-model:visible="previewVisible"
:dataset="selectedDataset"
/>
<!-- 详情对话框 -->
<el-dialog
v-model="detailsVisible"
title="数据处理详情"
width="60%"
>
<el-timeline>
<el-timeline-item
v-for="step in selectedDataset?.steps"
:key="step.step"
:type="getStepType(step.step)"
>
<h4>{{ getStepTitle(step) }}</h4>
<p>数据形状: {{ step.shape[0] }} × {{ step.shape[1] }}</p>
<template v-if="step.method">
<p>处理方法: {{ step.method }}</p>
<p>参数配置:</p>
<el-descriptions :column="1" border>
<el-descriptions-item
v-for="(value, key) in step.params"
:key="key"
:label="key"
>
{{ value }}
</el-descriptions-item>
</el-descriptions>
</template>
</el-timeline-item>
</el-timeline>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { DatasetInfo } from '@/types/data'
import DataPreview from './DataPreview.vue'
import { ElMessage } from 'element-plus'
import dayjs from 'dayjs'
const props = defineProps<{
datasets: DatasetInfo[]
}>()
const previewVisible = ref(false)
const detailsVisible = ref(false)
const selectedDataset = ref<DatasetInfo | null>(null)
//
const getFileName = (path: string) => {
return path.split('/').pop() || path
}
//
const formatTime = (timestamp: string) => {
return dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')
}
//
const getStepType = (step: string) => {
const typeMap: Record<string, string> = {
'load_data': 'primary',
'cleaning': 'success',
'feature_engineering': 'warning'
}
return typeMap[step] || 'info'
}
//
const getStepTitle = (step: DatasetInfo['steps'][0]) => {
const titleMap: Record<string, string> = {
'load_data': '数据加载',
'cleaning': '数据清洗',
'feature_engineering': '特征工程'
}
return titleMap[step.step] || step.step
}
//
const handlePreview = (dataset: DatasetInfo) => {
selectedDataset.value = dataset
previewVisible.value = true
}
//
const handleDownload = (dataset: DatasetInfo) => {
//
ElMessage.success('开始下载数据集')
}
//
const handleDetails = (dataset: DatasetInfo) => {
selectedDataset.value = dataset
detailsVisible.value = true
}
</script>
<style scoped>
.dataset-list {
padding: 20px;
}
.el-tag {
margin-right: 5px;
}
</style>

14
frontend/src/main.ts Normal file
View File

@ -0,0 +1,14 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
app.mount('#app')

View File

@ -0,0 +1,37 @@
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
redirect: '/data'
},
{
path: '/data',
name: 'Data',
component: () => import('@/views/data/DatasetView.vue'),
meta: {
title: '数据管理'
}
},
{
path: '/model',
name: 'Model',
component: () => import('@/views/model/ModelView.vue'),
meta: {
title: '模型管理'
}
},
{
path: '/system',
name: 'System',
component: () => import('@/views/system/SystemView.vue'),
meta: {
title: '系统监控'
}
}
]
})
export default router

View File

@ -0,0 +1,89 @@
// 预处理方法
export interface PreprocessMethod {
name: string
description: string
method: string[]
}
// 特征工程方法
export interface FeatureMethod {
name: string
description: string
method: string[]
}
// 方法参数
export interface MethodParameter {
name: string
type: string
default: any
description: string
}
// 方法详情
export interface MethodDetail {
name: string
description: string
principle: string
advantages: string[]
disadvantages: string[]
applicable_scenarios: string[]
parameters: MethodParameter[]
}
// 数据处理请求
export interface ProcessRequest {
input_path: string
output_dir: string
preprocessing: {
method: string
params: Record<string, any>
}[]
feature_methods: {
method: string
params: Record<string, any>
}[]
split_params: {
train: number
val: number
test: number
}
}
// CSV读取请求
export interface CSVRequest {
data_path: string
head?: number
tail?: number
info?: boolean
describe?: boolean
}
// 数据集信息
export interface DatasetInfo {
input_file: string
timestamp: string
process_methods: {
method_name: string
params: Record<string, any>
}[]
feature_methods: {
method_name: string
params: Record<string, any>
}[]
split_params: {
test_size: number
val_size: number
}
steps: {
step: string
method?: string
params?: Record<string, any>
shape: [number, number]
}[]
output_files: {
train: string
validation: string
test: string
}
}

View File

@ -0,0 +1,67 @@
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { ElMessage } from 'element-plus'
// 创建axios实例
const service: AxiosInstance = axios.create({
baseURL: '/',
timeout: 50000
})
// 请求拦截器
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
// 在这里可以添加token等认证信息
return config
},
(error) => {
console.error('Request error:', error)
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
const { status, data } = response
if (status === 200) {
if (data.status === 'success') {
return data
} else {
ElMessage.error(data.message || '请求失败')
return Promise.reject(new Error(data.message || '请求失败'))
}
} else {
ElMessage.error('请求失败')
return Promise.reject(new Error('请求失败'))
}
},
(error) => {
// 添加详细的错误处理
if (error.response) {
const { status, data } = error.response
switch (status) {
case 422:
ElMessage.error('请求参数验证失败: ' + (data.details?.[0]?.msg || '未知错误'))
break
case 404:
ElMessage.error('请求的资源不存在')
break
case 500:
ElMessage.error('服务器内部错误: ' + (data.details || data.message || '未知错误'))
break
default:
ElMessage.error(error.message || '请求失败')
}
} else if (error.request) {
ElMessage.error('无法连接到服务器')
} else {
ElMessage.error('请求配置错误')
}
console.error('Response error:', error)
return Promise.reject(error)
}
)
export default service

View File

@ -0,0 +1,30 @@
<template>
<div class="dataset-view">
<h2>数据集管理</h2>
<DatasetList :datasets="datasets" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getDatasets } from '@/api/data'
import DatasetList from '@/components/data/DatasetList.vue'
import type { DatasetInfo } from '@/types/data'
const datasets = ref<DatasetInfo[]>([])
onMounted(async () => {
try {
const { data } = await getDatasets()
datasets.value = data.datasets
} catch (error) {
console.error('Failed to load datasets:', error)
}
})
</script>
<style scoped>
.dataset-view {
min-height: 100%;
}
</style>

31
frontend/tsconfig.json Normal file
View File

@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path Alias */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

34
frontend/vite.config.ts Normal file
View File

@ -0,0 +1,34 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
server: {
host: true,
port: 5003,
proxy: {
'/data': {
target: 'http://10.0.0.202:8992',
changeOrigin: true
},
'/model': {
target: 'http://10.0.0.202:8992',
changeOrigin: true
},
'/system': {
target: 'http://10.0.0.202:8992',
changeOrigin: true
},
'/health': {
target: 'http://10.0.0.202:8992',
changeOrigin: true
}
}
}
})