chore: 移除 frontend/ 和图标资源目录;彻底清理遗留脚手架
8
.gitignore
vendored
@ -187,9 +187,11 @@ data/sub/waterindex*.xlsx
|
|||||||
data/sub/png/watermask.png
|
data/sub/png/watermask.png
|
||||||
|
|
||||||
# 图标文件(仅需保留 vector/svg,删除像素图标压缩包副本)
|
# 图标文件(仅需保留 vector/svg,删除像素图标压缩包副本)
|
||||||
data/icons-1/*.ico
|
data/icons-1/
|
||||||
data/icons/*.png
|
data/icons/
|
||||||
data/icons/word/*.png
|
|
||||||
|
|
||||||
# 旧版脚手架(遗留实验代码)
|
# 旧版脚手架(遗留实验代码)
|
||||||
new/
|
new/
|
||||||
|
|
||||||
|
# 前端脚手架(未集成的独立 Vue 项目)
|
||||||
|
frontend/
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 6 B |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 6 B |
|
Before Width: | Height: | Size: 6 B |
|
Before Width: | Height: | Size: 6 B |
|
Before Width: | Height: | Size: 6 B |
|
Before Width: | Height: | Size: 6 B |
|
Before Width: | Height: | Size: 6 B |
|
Before Width: | Height: | Size: 6 B |
|
Before Width: | Height: | Size: 6 B |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 6 B |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 6 B |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 11 KiB |
BIN
data/icons/1.png
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 3.4 MiB |
BIN
data/icons/2.png
|
Before Width: | Height: | Size: 70 KiB |
BIN
data/icons/3.png
|
Before Width: | Height: | Size: 55 KiB |
BIN
data/icons/4.png
|
Before Width: | Height: | Size: 51 KiB |
BIN
data/icons/5.png
|
Before Width: | Height: | Size: 46 KiB |
BIN
data/icons/6.png
|
Before Width: | Height: | Size: 51 KiB |
BIN
data/icons/7.png
|
Before Width: | Height: | Size: 78 KiB |
BIN
data/icons/8.png
|
Before Width: | Height: | Size: 59 KiB |
BIN
data/icons/9.png
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 950 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 5.3 MiB |
|
Before Width: | Height: | Size: 6.2 MiB |
|
Before Width: | Height: | Size: 978 KiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 300 KiB |
|
Before Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 16 MiB |
|
Before Width: | Height: | Size: 6.4 MiB |
|
Before Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 2.9 MiB |
|
Before Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 884 KiB |
@ -1,2 +0,0 @@
|
|||||||
# 联调期指向本地 FastAPI dev 服务
|
|
||||||
VITE_API_BASE_URL=http://127.0.0.1:9090
|
|
||||||
7
frontend/.gitignore
vendored
@ -1,7 +0,0 @@
|
|||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
.vite
|
|
||||||
*.local
|
|
||||||
.DS_Store
|
|
||||||
*.log
|
|
||||||
15
frontend/env.d.ts
vendored
@ -1,15 +0,0 @@
|
|||||||
/// <reference types="vite/client" />
|
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
|
||||||
readonly VITE_API_BASE_URL?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ImportMeta {
|
|
||||||
readonly env: ImportMetaEnv
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '*.vue' {
|
|
||||||
import type { DefineComponent } from 'vue'
|
|
||||||
const component: DefineComponent<{}, {}, any>
|
|
||||||
export default component
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>WQ_GUI · 水质反演联调控制台</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "wq-gui-frontend",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.1",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"type-check": "vue-tsc --noEmit"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"vue": "^3.4.27",
|
|
||||||
"element-plus": "^2.7.5",
|
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
|
||||||
"axios": "^1.7.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^20.12.12",
|
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
|
||||||
"typescript": "^5.4.5",
|
|
||||||
"vite": "^5.2.11",
|
|
||||||
"vue-tsc": "^2.0.19"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,225 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="dashboard-container">
|
|
||||||
<h1 class="title">高光谱水质反演控制台</h1>
|
|
||||||
<el-row :gutter="20">
|
|
||||||
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-card class="box-card" shadow="hover">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
<span class="header-title">🚀 模型训练 (Train)</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<el-form label-position="top">
|
|
||||||
<el-form-item label="算法选择 (Model Type)">
|
|
||||||
<el-select v-model="trainForm.model_type" placeholder="请选择算法" class="w-full">
|
|
||||||
<el-option label="随机森林 (RF)" value="RF" />
|
|
||||||
<el-option label="支持向量回归 (SVR)" value="SVR" />
|
|
||||||
<el-option label="线性回归 (LinearRegression)" value="LinearRegression" />
|
|
||||||
<el-option label="K近邻 (KNN)" value="KNN" />
|
|
||||||
<el-option label="偏最小二乘 (PLS)" value="PLS" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="目标参数 (Target)">
|
|
||||||
<el-input v-model="trainForm.target" placeholder="如 Chl-a" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="训练数据路径 (CSV 绝对路径)">
|
|
||||||
<el-input v-model="trainForm.train_data_path" placeholder="如 D:\111\data.csv" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="特征起始列 (如 4, 或列名)">
|
|
||||||
<el-input v-model="trainForm.feature_start" placeholder="填写数字或列名" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-button type="primary" @click="handleTrain" :loading="trainPoller?.isPolling?.value" class="w-full">
|
|
||||||
开始训练
|
|
||||||
</el-button>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<div v-if="trainTaskId" class="status-board">
|
|
||||||
<p><strong>任务 ID:</strong> <el-tag size="small" type="info">{{ trainTaskId }}</el-tag></p>
|
|
||||||
<p><strong>当前状态:</strong>
|
|
||||||
<el-tag :type="getStatusType(trainPoller?.status?.value || 'PENDING')" style="margin-left:10px">
|
|
||||||
{{ trainPoller?.status?.value || 'PENDING' }}
|
|
||||||
</el-tag>
|
|
||||||
</p>
|
|
||||||
<el-progress
|
|
||||||
v-if="trainPoller?.isPolling?.value || trainPoller?.status?.value === 'SUCCESS'"
|
|
||||||
:percentage="trainPoller?.status?.value === 'SUCCESS' ? 100 : 60"
|
|
||||||
:status="trainPoller?.status?.value === 'SUCCESS' ? 'success' : (trainPoller?.status?.value === 'FAILED' ? 'exception' : '')"
|
|
||||||
:indeterminate="trainPoller?.isPolling?.value"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div v-if="trainPoller?.error?.value" class="error-msg">
|
|
||||||
<el-alert :title="trainPoller.error.value" type="error" :closable="false" show-icon />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="trainPoller?.result?.value?.model_id" class="result-msg">
|
|
||||||
<el-descriptions border :column="1" size="small" title="训练指标">
|
|
||||||
<el-descriptions-item label="Model ID">{{ trainPoller.result.value.model_id }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="Test R²">{{ Number(trainPoller.result.value.test_r2).toFixed(4) }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="Test RMSE">{{ Number(trainPoller.result.value.test_rmse).toFixed(4) }}</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-card class="box-card" shadow="hover">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
<span class="header-title">🎯 模型推断 (Predict)</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<el-form label-position="top">
|
|
||||||
<el-form-item label="已训练模型 ID (Model ID)">
|
|
||||||
<el-input v-model="predictForm.model_id" placeholder="将自动填入左侧训练好的 ID" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="待推断影像路径 (Zarr 绝对路径)">
|
|
||||||
<el-input v-model="predictForm.input_zarr_path" placeholder="如 D:\111\image.zarr" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-button type="success" @click="handlePredict" :loading="predictPoller?.isPolling?.value" class="w-full">
|
|
||||||
开始大图反演推断
|
|
||||||
</el-button>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<div v-if="predictTaskId" class="status-board">
|
|
||||||
<p><strong>任务 ID:</strong> <el-tag size="small" type="info">{{ predictTaskId }}</el-tag></p>
|
|
||||||
<p><strong>当前状态:</strong>
|
|
||||||
<el-tag :type="getStatusType(predictPoller?.status?.value || 'PENDING')" style="margin-left:10px">
|
|
||||||
{{ predictPoller?.status?.value || 'PENDING' }}
|
|
||||||
</el-tag>
|
|
||||||
</p>
|
|
||||||
<el-progress
|
|
||||||
v-if="predictPoller?.isPolling?.value || predictPoller?.status?.value === 'SUCCESS'"
|
|
||||||
:percentage="predictPoller?.status?.value === 'SUCCESS' ? 100 : 50"
|
|
||||||
:status="predictPoller?.status?.value === 'SUCCESS' ? 'success' : (predictPoller?.status?.value === 'FAILED' ? 'exception' : '')"
|
|
||||||
:indeterminate="predictPoller?.isPolling?.value"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div v-if="predictPoller?.error?.value" class="error-msg">
|
|
||||||
<el-alert :title="predictPoller.error.value" type="error" :closable="false" show-icon />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="predictPoller?.result?.value?.output_zarr_path" class="result-msg">
|
|
||||||
<el-alert :title="'推断成功!结果已落盘至: ' + predictPoller.result.value.output_zarr_path" type="success" :closable="false" show-icon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
|
|
||||||
</el-row>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, watch, reactive } from 'vue'
|
|
||||||
import { submitTrain, submitPredict } from '@/api/tasks'
|
|
||||||
import { useTaskPoller } from '@/composables/useTaskPoller'
|
|
||||||
|
|
||||||
// 训练表单状态
|
|
||||||
const trainForm = reactive({
|
|
||||||
model_type: 'RF',
|
|
||||||
target: 'Chl-a',
|
|
||||||
train_data_path: '',
|
|
||||||
feature_start: '4'
|
|
||||||
})
|
|
||||||
const trainTaskId = ref<string | null>(null)
|
|
||||||
const trainPoller = useTaskPoller(trainTaskId)
|
|
||||||
|
|
||||||
// 推断表单状态
|
|
||||||
const predictForm = reactive({
|
|
||||||
model_id: '',
|
|
||||||
input_zarr_path: ''
|
|
||||||
})
|
|
||||||
const predictTaskId = ref<string | null>(null)
|
|
||||||
const predictPoller = useTaskPoller(predictTaskId)
|
|
||||||
|
|
||||||
// 自动填入联动
|
|
||||||
watch(() => trainPoller?.result?.value?.model_id, (newId) => {
|
|
||||||
if (newId) predictForm.model_id = newId as string
|
|
||||||
})
|
|
||||||
|
|
||||||
// 提交训练
|
|
||||||
const handleTrain = async () => {
|
|
||||||
try {
|
|
||||||
const res = await submitTrain({
|
|
||||||
model_type: trainForm.model_type,
|
|
||||||
target: trainForm.target,
|
|
||||||
train_data_path: trainForm.train_data_path,
|
|
||||||
feature_start: trainForm.feature_start,
|
|
||||||
params: {}
|
|
||||||
})
|
|
||||||
trainTaskId.value = res.task_id
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error('训练接口调用失败', e)
|
|
||||||
alert('提交失败,请检查后端是否在 9090 端口启动,或按 F12 查看控制台跨域报错')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交推断
|
|
||||||
const handlePredict = async () => {
|
|
||||||
try {
|
|
||||||
const res = await submitPredict({
|
|
||||||
model_id: predictForm.model_id,
|
|
||||||
input_zarr_path: predictForm.input_zarr_path
|
|
||||||
})
|
|
||||||
predictTaskId.value = res.task_id
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error('推断接口调用失败', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 样式辅助
|
|
||||||
const getStatusType = (status: string) => {
|
|
||||||
if (status === 'SUCCESS') return 'success'
|
|
||||||
if (status === 'FAILED') return 'danger'
|
|
||||||
if (status === 'PROCESSING') return 'warning'
|
|
||||||
return 'info'
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* 去除全局默认边距 */
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.dashboard-container {
|
|
||||||
padding: 40px;
|
|
||||||
min-height: 100vh;
|
|
||||||
background-color: #1e1e2d; /* 科技深色底 */
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
color: #ffffff;
|
|
||||||
font-weight: 300;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
}
|
|
||||||
.header-title {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.box-card {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
background-color: rgba(255, 255, 255, 0.95);
|
|
||||||
}
|
|
||||||
.w-full {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.status-board {
|
|
||||||
margin-top: 25px;
|
|
||||||
padding: 20px;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #e4e7ed;
|
|
||||||
}
|
|
||||||
.error-msg, .result-msg {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
/**
|
|
||||||
* Axios 单例 + 响应拦截器
|
|
||||||
* --------------------------------
|
|
||||||
* 1. baseURL 默认指向本地 FastAPI dev 服务。
|
|
||||||
* 通过 Vite 环境变量 VITE_API_BASE_URL 可覆盖, 例如:
|
|
||||||
* .env.development: VITE_API_BASE_URL=http://127.0.0.1:8000
|
|
||||||
* .env.production: VITE_API_BASE_URL=https://api.example.com
|
|
||||||
*
|
|
||||||
* 2. 响应拦截器统一 unwrap response.data, 调用方拿到的是真正的业务对象,
|
|
||||||
* 而不是 AxiosResponse 包装。失败时统一抛 Error, message 优先取
|
|
||||||
* FastAPI 的 detail 字段。
|
|
||||||
*
|
|
||||||
* 3. 类型增强: cast 成 UnwrappedAxiosInstance, 让 request.get<T>(url)
|
|
||||||
* 的返回类型直接是 T, 而不是 AxiosResponse<T>, 调用方无需二次解包。
|
|
||||||
*/
|
|
||||||
import axios, {
|
|
||||||
type AxiosInstance,
|
|
||||||
type AxiosRequestConfig,
|
|
||||||
} from 'axios'
|
|
||||||
|
|
||||||
// 在 Vite 下用 import.meta.env; 其它环境 (webpack/直接 ts-node) 兜底到 process.env
|
|
||||||
type ViteEnv = { env?: Record<string, string | undefined> }
|
|
||||||
const viteEnv: ViteEnv | undefined =
|
|
||||||
typeof import.meta !== 'undefined' ? ((import.meta as unknown) as ViteEnv) : undefined
|
|
||||||
|
|
||||||
const baseURL: string =
|
|
||||||
viteEnv?.env?.VITE_API_BASE_URL ??
|
|
||||||
(typeof process !== 'undefined' && process.env?.VITE_API_BASE_URL) ??
|
|
||||||
'http://127.0.0.1:9090'
|
|
||||||
|
|
||||||
const _instance: AxiosInstance = axios.create({
|
|
||||||
baseURL,
|
|
||||||
timeout: 15000,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
// FastAPI 开发期 CORS 是 allow_origins=["*"], 不需要带 cookie
|
|
||||||
withCredentials: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
// ----- 请求拦截器: 预留 token / 日志位 -----
|
|
||||||
_instance.interceptors.request.use(
|
|
||||||
(config) => {
|
|
||||||
// const token = localStorage.getItem('token')
|
|
||||||
// if (token) config.headers.Authorization = `Bearer ${token}`
|
|
||||||
return config
|
|
||||||
},
|
|
||||||
(error) => Promise.reject(error),
|
|
||||||
)
|
|
||||||
|
|
||||||
// ----- 响应拦截器: unwrap data + 统一错误 message -----
|
|
||||||
_instance.interceptors.response.use(
|
|
||||||
(response) => response.data,
|
|
||||||
(error) => {
|
|
||||||
const detail = error?.response?.data?.detail
|
|
||||||
const message =
|
|
||||||
(typeof detail === 'string' ? detail : detail?.msg) ??
|
|
||||||
error?.response?.data?.message ??
|
|
||||||
error?.message ??
|
|
||||||
'请求失败'
|
|
||||||
return Promise.reject(new Error(message))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// ----- 类型增强: 把响应拦截器 unwrap 的事实在类型上表达出来 -----
|
|
||||||
type UnwrappedAxiosInstance = Omit<
|
|
||||||
AxiosInstance,
|
|
||||||
'get' | 'delete' | 'head' | 'options' | 'post' | 'put' | 'patch'
|
|
||||||
> & {
|
|
||||||
get<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T>
|
|
||||||
delete<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T>
|
|
||||||
head<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T>
|
|
||||||
options<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T>
|
|
||||||
post<T = unknown, D = unknown>(
|
|
||||||
url: string,
|
|
||||||
data?: D,
|
|
||||||
config?: AxiosRequestConfig,
|
|
||||||
): Promise<T>
|
|
||||||
put<T = unknown, D = unknown>(
|
|
||||||
url: string,
|
|
||||||
data?: D,
|
|
||||||
config?: AxiosRequestConfig,
|
|
||||||
): Promise<T>
|
|
||||||
patch<T = unknown, D = unknown>(
|
|
||||||
url: string,
|
|
||||||
data?: D,
|
|
||||||
config?: AxiosRequestConfig,
|
|
||||||
): Promise<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
const request = _instance as UnwrappedAxiosInstance
|
|
||||||
|
|
||||||
export default request
|
|
||||||
export { baseURL }
|
|
||||||
@ -1,155 +0,0 @@
|
|||||||
/**
|
|
||||||
* 与 FastAPI 后端对接的 API 函数
|
|
||||||
* --------------------------------
|
|
||||||
* 全部用 request 单例, 调用方拿到的就是业务对象 (response 拦截器已 unwrap)。
|
|
||||||
*
|
|
||||||
* 后端路由:
|
|
||||||
* GET /api/algorithms
|
|
||||||
* POST /api/process/deglint
|
|
||||||
* POST /api/modeling/train
|
|
||||||
* POST /api/modeling/predict (额外, 与 train 配套)
|
|
||||||
* GET /api/tasks/{task_id}
|
|
||||||
*/
|
|
||||||
import request from './request'
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 通用类型
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
/** 后端任务状态机 (与 app.core.task_store.TASK_STORE 保持一致) */
|
|
||||||
export type TaskStatus = 'PENDING' | 'PROCESSING' | 'SUCCESS' | 'FAILED'
|
|
||||||
|
|
||||||
/** 任务类型, 区分去耀斑 / 训练 / 推断 */
|
|
||||||
export type TaskKind = 'deglint' | 'train' | 'predict'
|
|
||||||
|
|
||||||
/** 提交后端后立即返回的最小任务凭证 */
|
|
||||||
export interface TaskAcceptedResponse {
|
|
||||||
task_id: string
|
|
||||||
status: TaskStatus
|
|
||||||
kind: TaskKind
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 任务详情 (与后端 TASK_STORE 里记录的字段对齐, 通用 + 各 kind 增量字段)
|
|
||||||
* 用 [key: string]: unknown 兜底, 兼容未来后端新增字段
|
|
||||||
*/
|
|
||||||
export interface TaskRecord {
|
|
||||||
task_id: string
|
|
||||||
kind: TaskKind
|
|
||||||
status: TaskStatus
|
|
||||||
// 去耀斑
|
|
||||||
algorithm?: string
|
|
||||||
input_zarr_path?: string
|
|
||||||
output_zarr_path?: string | null
|
|
||||||
// 训练
|
|
||||||
model_type?: string
|
|
||||||
target?: string
|
|
||||||
train_data_path?: string
|
|
||||||
feature_start?: number | string
|
|
||||||
params?: Record<string, unknown>
|
|
||||||
model_id?: string | null
|
|
||||||
model_path?: string | null
|
|
||||||
test_r2?: number | null
|
|
||||||
test_rmse?: number | null
|
|
||||||
test_mae?: number | null
|
|
||||||
n_features?: number | null
|
|
||||||
n_samples?: number | null
|
|
||||||
// 推断
|
|
||||||
// (model_id / input_zarr_path / output_zarr_path 已在上方)
|
|
||||||
// 失败
|
|
||||||
error?: string | null
|
|
||||||
traceback?: string | null
|
|
||||||
// 元
|
|
||||||
created_at?: string
|
|
||||||
updated_at?: string
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 1) 算法列表 GET /api/algorithms
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
export interface AlgorithmInfo {
|
|
||||||
name: string
|
|
||||||
doc?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AlgorithmListResponse {
|
|
||||||
algorithms: AlgorithmInfo[]
|
|
||||||
count: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAlgorithms(): Promise<AlgorithmListResponse> {
|
|
||||||
return request.get<AlgorithmListResponse>('/api/algorithms')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 2) 提交去耀斑 POST /api/process/deglint
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
export interface DeglintParams {
|
|
||||||
input_zarr_path: string
|
|
||||||
output_zarr_path?: string
|
|
||||||
/** 算法自定义参数 (D_max / band 选择等) */
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export function submitDeglint(
|
|
||||||
method: string,
|
|
||||||
params: DeglintParams,
|
|
||||||
): Promise<TaskAcceptedResponse> {
|
|
||||||
return request.post<TaskAcceptedResponse, { method: string; params: DeglintParams }>(
|
|
||||||
'/api/process/deglint',
|
|
||||||
{ method, params },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 3) 提交训练 POST /api/modeling/train
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
export interface TrainRequest {
|
|
||||||
model_type: string
|
|
||||||
target: string
|
|
||||||
train_data_path: string
|
|
||||||
/** 特征起始列, int 索引或 str 列名, 默认 4 */
|
|
||||||
feature_start?: number | string
|
|
||||||
/** sklearn 估计器超参 */
|
|
||||||
params?: Record<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function submitTrain(payload: TrainRequest): Promise<TaskAcceptedResponse> {
|
|
||||||
return request.post<TaskAcceptedResponse, TrainRequest>(
|
|
||||||
'/api/modeling/train',
|
|
||||||
payload,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 4) 提交推断 POST /api/modeling/predict (配套, 训练后才能用)
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
export interface PredictRequest {
|
|
||||||
model_id: string
|
|
||||||
input_zarr_path: string
|
|
||||||
output_zarr_path?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function submitPredict(
|
|
||||||
payload: PredictRequest,
|
|
||||||
): Promise<TaskAcceptedResponse> {
|
|
||||||
return request.post<TaskAcceptedResponse, PredictRequest>(
|
|
||||||
'/api/modeling/predict',
|
|
||||||
payload,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 5) 查询任务状态 GET /api/tasks/{task_id}
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
export function getTaskStatus(task_id: string): Promise<TaskRecord> {
|
|
||||||
return request.get<TaskRecord>(
|
|
||||||
`/api/tasks/${encodeURIComponent(task_id)}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,238 +0,0 @@
|
|||||||
/**
|
|
||||||
* 任务轮询 Composable (Vue 3 + TypeScript)
|
|
||||||
* -----------------------------------------
|
|
||||||
* 用法 1 — 静态 task_id, 立即开始轮询:
|
|
||||||
* const { status, result, error, waitForCompletion } = useTaskPoller(taskId)
|
|
||||||
*
|
|
||||||
* 用法 2 — 响应式 task_id (异步拿到后赋值, 自动开始):
|
|
||||||
* const taskId = ref<string | null>(null)
|
|
||||||
* const poller = useTaskPoller(taskId)
|
|
||||||
* ;(async () => { taskId.value = (await submitTrain({...})).task_id })()
|
|
||||||
* await poller.waitForCompletion()
|
|
||||||
*
|
|
||||||
* 用法 3 — 手动控制:
|
|
||||||
* const poller = useTaskPoller()
|
|
||||||
* poller.start(taskId) // 开始
|
|
||||||
* poller.stop() // 停止
|
|
||||||
* poller.reset() // 清空状态
|
|
||||||
*
|
|
||||||
* 设计要点:
|
|
||||||
* - 终态 (SUCCESS/FAILED) 自动停止轮询
|
|
||||||
* - 组件卸载自动清理 (onUnmounted)
|
|
||||||
* - 网络错误不立刻终止, 计入 error.value 但继续轮询 (兼容临时抖动)
|
|
||||||
* - waitForCompletion 是单次承诺: SUCCESS resolve(record), FAILED reject(error)
|
|
||||||
* 外部 stop() 也会 reject
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
onUnmounted,
|
|
||||||
ref,
|
|
||||||
watch,
|
|
||||||
type MaybeRefOrGetter,
|
|
||||||
type Ref,
|
|
||||||
} from 'vue'
|
|
||||||
import { toValue } from 'vue'
|
|
||||||
import {
|
|
||||||
getTaskStatus,
|
|
||||||
type TaskRecord,
|
|
||||||
type TaskStatus,
|
|
||||||
} from '../api/tasks'
|
|
||||||
|
|
||||||
// 显式包含 'idle', 用于未开始轮询的初始态
|
|
||||||
export type PollerStatus = TaskStatus | 'idle'
|
|
||||||
|
|
||||||
export interface UseTaskPollerOptions {
|
|
||||||
/** 轮询间隔 ms, 默认 2000 */
|
|
||||||
intervalMs?: number
|
|
||||||
/** task_id 变 null 时是否自动停止, 默认 true */
|
|
||||||
autoStopOnNull?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UseTaskPollerReturn {
|
|
||||||
/** 当前任务状态, 初始 'idle' */
|
|
||||||
status: Ref<PollerStatus>
|
|
||||||
/** SUCCESS 时的完整任务记录 (含 output_zarr_path / model_id 等) */
|
|
||||||
result: Ref<TaskRecord | null>
|
|
||||||
/** FAILED 时的错误描述, 或轮询过程中网络异常的消息 */
|
|
||||||
error: Ref<string | null>
|
|
||||||
/** 最新一次拉取到的任务记录 (含 PENDING/PROCESSING 占位字段) */
|
|
||||||
record: Ref<TaskRecord | null>
|
|
||||||
/** 是否正在轮询中 */
|
|
||||||
isPolling: Ref<boolean>
|
|
||||||
/** 当前轮询的 task_id (可能为 null) */
|
|
||||||
taskId: Ref<string | null>
|
|
||||||
/** 开始轮询某 task, 已轮询同一 id 时是 no-op */
|
|
||||||
start: (taskId: string) => void
|
|
||||||
/** 主动停止 (会 reject 未完成的 waitForCompletion) */
|
|
||||||
stop: () => void
|
|
||||||
/** 清空所有状态回 'idle' */
|
|
||||||
reset: () => void
|
|
||||||
/**
|
|
||||||
* 等到 SUCCESS/FAILED。
|
|
||||||
* - SUCCESS: resolve(record)
|
|
||||||
* - FAILED : reject(Error)
|
|
||||||
* - stop() : reject(Error('Polling stopped'))
|
|
||||||
* - 组件卸载: reject(Error('Component unmounted'))
|
|
||||||
* 已处于终态时立刻 resolve/reject, 不重复等待。
|
|
||||||
*/
|
|
||||||
waitForCompletion: () => Promise<TaskRecord>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTaskPoller(
|
|
||||||
taskIdSource?: MaybeRefOrGetter<string | null>,
|
|
||||||
options: UseTaskPollerOptions = {},
|
|
||||||
): UseTaskPollerReturn {
|
|
||||||
const { intervalMs = 2000, autoStopOnNull = true } = options
|
|
||||||
|
|
||||||
const status = ref<PollerStatus>('idle')
|
|
||||||
const result = ref<TaskRecord | null>(null)
|
|
||||||
const error = ref<string | null>(null)
|
|
||||||
const record = ref<TaskRecord | null>(null)
|
|
||||||
const isPolling = ref(false)
|
|
||||||
const taskId = ref<string | null>(null)
|
|
||||||
|
|
||||||
let timerId: ReturnType<typeof setInterval> | null = null
|
|
||||||
let inFlightTick = false
|
|
||||||
let resolveWait: ((rec: TaskRecord) => void) | null = null
|
|
||||||
let rejectWait: ((err: Error) => void) | null = null
|
|
||||||
|
|
||||||
function clearTimer() {
|
|
||||||
if (timerId !== null) {
|
|
||||||
clearInterval(timerId)
|
|
||||||
timerId = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveOrRejectWait(rec: TaskRecord | null, err: Error | null) {
|
|
||||||
const r = resolveWait
|
|
||||||
const rj = rejectWait
|
|
||||||
resolveWait = null
|
|
||||||
rejectWait = null
|
|
||||||
if (rec && r) r(rec)
|
|
||||||
else if (err && rj) rj(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyTerminalRecord(rec: TaskRecord) {
|
|
||||||
record.value = rec
|
|
||||||
status.value = rec.status
|
|
||||||
if (rec.status === 'SUCCESS') {
|
|
||||||
result.value = rec
|
|
||||||
error.value = null
|
|
||||||
resolveOrRejectWait(rec, null)
|
|
||||||
} else if (rec.status === 'FAILED') {
|
|
||||||
result.value = null
|
|
||||||
error.value = rec.error ?? '任务失败 (无具体错误信息)'
|
|
||||||
resolveOrRejectWait(
|
|
||||||
null,
|
|
||||||
new Error(error.value ?? '任务失败'),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function tick() {
|
|
||||||
const currentId = taskId.value
|
|
||||||
if (!currentId || inFlightTick) return
|
|
||||||
inFlightTick = true
|
|
||||||
try {
|
|
||||||
const rec = await getTaskStatus(currentId)
|
|
||||||
// 防止 await 期间用户 stop() / start() 了别的 task
|
|
||||||
if (taskId.value !== currentId) return
|
|
||||||
if (rec.status === 'SUCCESS' || rec.status === 'FAILED') {
|
|
||||||
applyTerminalRecord(rec)
|
|
||||||
stop()
|
|
||||||
} else {
|
|
||||||
// PENDING / PROCESSING 阶段, 更新 record 与 status 供 UI 展示
|
|
||||||
record.value = rec
|
|
||||||
status.value = rec.status
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
const msg = e instanceof Error ? e.message : String(e)
|
|
||||||
// 单次失败不立刻终止, 写入 error 但保持轮询
|
|
||||||
error.value = `轮询异常: ${msg}`
|
|
||||||
} finally {
|
|
||||||
inFlightTick = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function start(nextId: string) {
|
|
||||||
if (!nextId) return
|
|
||||||
// 已在轮询同一 id, 幂等
|
|
||||||
if (taskId.value === nextId && isPolling.value) return
|
|
||||||
clearTimer()
|
|
||||||
taskId.value = nextId
|
|
||||||
status.value = 'idle'
|
|
||||||
result.value = null
|
|
||||||
error.value = null
|
|
||||||
record.value = null
|
|
||||||
isPolling.value = true
|
|
||||||
// 立刻拉一次, 避免 2s 空窗
|
|
||||||
void tick()
|
|
||||||
timerId = setInterval(() => void tick(), intervalMs)
|
|
||||||
}
|
|
||||||
|
|
||||||
function stop() {
|
|
||||||
const wasActive = isPolling.value
|
|
||||||
clearTimer()
|
|
||||||
isPolling.value = false
|
|
||||||
if (wasActive) {
|
|
||||||
resolveOrRejectWait(null, new Error('Polling stopped'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
stop()
|
|
||||||
taskId.value = null
|
|
||||||
status.value = 'idle'
|
|
||||||
result.value = null
|
|
||||||
error.value = null
|
|
||||||
record.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
function waitForCompletion(): Promise<TaskRecord> {
|
|
||||||
const r = record.value
|
|
||||||
if (r && r.status === 'SUCCESS') return Promise.resolve(r)
|
|
||||||
if (r && r.status === 'FAILED') {
|
|
||||||
return Promise.reject(
|
|
||||||
new Error(r.error ?? '任务失败 (无具体错误信息)'),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return new Promise<TaskRecord>((resolve, reject) => {
|
|
||||||
resolveWait = resolve
|
|
||||||
rejectWait = reject
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自动模式: 监听外部 taskIdSource
|
|
||||||
if (taskIdSource !== undefined) {
|
|
||||||
const stopWatch = watch(
|
|
||||||
() => toValue(taskIdSource),
|
|
||||||
(newId, oldId) => {
|
|
||||||
if (newId && newId !== oldId) start(newId)
|
|
||||||
else if (!newId && autoStopOnNull) stop()
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
onUnmounted(() => {
|
|
||||||
stopWatch()
|
|
||||||
reset()
|
|
||||||
resolveOrRejectWait(null, new Error('Component unmounted'))
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
onUnmounted(() => {
|
|
||||||
reset()
|
|
||||||
resolveOrRejectWait(null, new Error('Component unmounted'))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status,
|
|
||||||
result,
|
|
||||||
error,
|
|
||||||
record,
|
|
||||||
isPolling,
|
|
||||||
taskId,
|
|
||||||
start,
|
|
||||||
stop,
|
|
||||||
reset,
|
|
||||||
waitForCompletion,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import { createApp } from 'vue'
|
|
||||||
import ElementPlus from 'element-plus'
|
|
||||||
import 'element-plus/dist/index.css'
|
|
||||||
import App from './App.vue'
|
|
||||||
|
|
||||||
const app = createApp(App)
|
|
||||||
|
|
||||||
app.use(ElementPlus)
|
|
||||||
app.mount('#app')
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"strict": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["src/*"]
|
|
||||||
},
|
|
||||||
"types": ["vite/client"]
|
|
||||||
},
|
|
||||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue", "env.d.ts"],
|
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"types": ["node"]
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts"]
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import { defineConfig } from 'vite'
|
|
||||||
import vue from '@vitejs/plugin-vue'
|
|
||||||
import { fileURLToPath, URL } from 'node:url'
|
|
||||||
|
|
||||||
// Vite 配置:
|
|
||||||
// - @ -> frontend/src
|
|
||||||
// - dev server 监听 0.0.0.0:5173, 允许局域网内真机调试
|
|
||||||
// - VITE_API_BASE_URL 通过 .env.development 注入, 缺省走 src/api/request.ts 内的兜底 (http://127.0.0.1:8000)
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [vue()],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
host: '0.0.0.0',
|
|
||||||
port: 5173,
|
|
||||||
strictPort: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||