Commit b107747a authored by luwei's avatar luwei

修改

parent 8160a34d
......@@ -54,6 +54,7 @@ def get_files(
async def upload_file(
file: UploadFile = File(...),
category_id: str = Form(default=''),
remark: str = Form(default=''),
):
try:
content = await file.read()
......@@ -64,6 +65,7 @@ async def upload_file(
filename=file.filename or 'unknown.xlsx',
content=content,
category_id=category_id,
remark=remark.strip() or None,
)
return success_response(data=file_meta, message='文件上传成功')
except ValueError as error:
......@@ -77,7 +79,7 @@ def download_template():
content=content,
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers={
'Content-Disposition': 'attachment; filename=data_template.xlsx',
'Content-Disposition': 'attachment; filename=上传文件模板.xlsx',
},
)
......
......@@ -157,7 +157,7 @@ class DataManagementService:
for item in rows
]
def upload_file(self, filename: str, content: bytes, category_id: str = 'all') -> dict[str, Any]:
def upload_file(self, filename: str, content: bytes, category_id: str = 'all', remark: str | None = None) -> dict[str, Any]:
suffix = Path(filename).suffix.lower()
if suffix not in {'.xlsx', '.xls', '.csv'}:
raise ValueError('仅支持数据文件(.xlsx/.xls/.csv)')
......@@ -194,6 +194,7 @@ class DataManagementService:
file_path=db_file_path,
category_id=category_db_id,
data_count=total_count,
remark=remark,
)
session.add(file_meta)
session.commit()
......@@ -205,6 +206,7 @@ class DataManagementService:
'stored_name': file_meta.stored_name,
'file_path': file_meta.file_path,
'category_id': file_meta.category_id,
'remark': file_meta.remark or '',
'uploaded_at': file_meta.uploaded_at.strftime('%Y-%m-%d %H:%M:%S') if file_meta.uploaded_at else '',
'data_count': file_meta.data_count,
}
......
......@@ -151,6 +151,10 @@ body {
border-bottom: 1px solid var(--border-color);
}
.el-dialog__body {
padding-top: 24px;
}
/* Tabs (navigation) */
.el-tabs__item {
font-size: 14px !important;
......
<script setup>
import { DataAnalysis, Delete, Document, Edit, Folder, FolderOpened, Upload } from '@element-plus/icons-vue'
import { DataAnalysis, Delete, Document, Edit, Folder, FolderOpened, Upload, Search } from '@element-plus/icons-vue'
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { ElAutoResizer, ElMessage, ElMessageBox, ElTableV2 } from 'element-plus'
import {
......@@ -57,6 +57,7 @@ const categoryForm = reactive({
const uploadDialogVisible = ref(false)
const uploadForm = reactive({
categoryId: '',
remark: '',
})
const qualityDialogVisible = ref(false)
......@@ -304,7 +305,7 @@ const handleDragging = (event) => {
}
const startDragging = (target, event) => {
if (window.innerWidth <= 980 || !layoutRef.value) {
if (!layoutRef.value) {
return
}
......@@ -427,6 +428,7 @@ const handleReset = async () => {
const openUploadDialog = () => {
uploadForm.categoryId = selectedCategory.value && selectedCategory.value !== 'all' ? String(selectedCategory.value) : ''
uploadForm.remark = ''
uploadDialogVisible.value = true
}
......@@ -454,6 +456,9 @@ const handleUpload = async (uploadRequest) => {
const formData = new FormData()
formData.append('file', uploadRequest.file)
formData.append('category_id', String(uploadForm.categoryId))
if (uploadForm.remark.trim()) {
formData.append('remark', uploadForm.remark.trim())
}
try {
await uploadDataFile(formData)
......@@ -472,7 +477,7 @@ const handleDownloadTemplate = async () => {
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = 'data_template.xlsx'
link.download = '上传文件模板.xlsx'
document.body.appendChild(link)
link.click()
link.remove()
......@@ -508,6 +513,32 @@ const handleDeleteFile = async (row) => {
}
}
const selectedFiles = ref([])
const handleFileSelectionChange = (rows) => {
selectedFiles.value = rows
}
const handleBatchDeleteFiles = async () => {
if (!selectedFiles.value.length) return
try {
await ElMessageBox.confirm(
`确定删除选中的 ${selectedFiles.value.length} 个文件吗?此操作不可恢复。`,
'批量删除',
{ type: 'warning' },
)
await Promise.all(selectedFiles.value.map((f) => deleteDataFile(f.id)))
ElMessage.success(`已删除 ${selectedFiles.value.length} 个文件`)
if (selectedFiles.value.some((f) => f.id === currentFile.value?.id)) {
currentFile.value = null
recordList.value = []
}
selectedFiles.value = []
await loadFileList()
} catch {
// user cancelled
}
}
onMounted(async () => {
loadLayoutCache()
await loadCategoryTree()
......@@ -572,22 +603,29 @@ onBeforeUnmount(() => {
<el-form :inline="true" class="search-form">
<el-form-item>
<el-input v-model="searchForm.filename" placeholder="文件名" clearable />
<el-input v-model="searchForm.filename" placeholder="文件名" clearable :prefix-icon="Search" />
</el-form-item>
<el-form-item>
<el-form-item class="form-actions">
<el-button
type="danger"
plain
:disabled="!selectedFiles.length"
@click="handleBatchDeleteFiles"
>批量删除</el-button>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
<el-button type="info" plain @click="handleDownloadTemplate">下载模板</el-button>
<el-button type="primary" :icon="Upload" @click="openUploadDialog">上传文件</el-button>
<el-button @click="handleDownloadTemplate">下载模板</el-button>
<el-button type="primary" @click="openUploadDialog">上传文件</el-button>
</el-form-item>
</el-form>
<el-table :data="fileList" border stripe v-loading="loadingFiles" height="calc(100vh - 250px)" row-class-name="file-row" @row-click="handleViewFile">
<el-table :data="fileList" border stripe v-loading="loadingFiles" height="calc(100vh - 250px)" row-class-name="file-row" @row-click="handleViewFile" @selection-change="handleFileSelectionChange">
<el-table-column type="selection" width="44" @click.stop />
<el-table-column prop="filename" label="文件名" min-width="160" />
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
<el-table-column prop="remark" label="备注" min-width="120" class-name="cell-wrap" />
<el-table-column prop="uploaded_at" label="上传时间" min-width="170" />
<el-table-column prop="data_count" label="数据量" width="90" />
<el-table-column label="操作" width="220" fixed="right">
<el-table-column label="操作" width="110" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :icon="Edit" @click.stop="openFileEditDialog(row)" />
<el-button link type="success" :icon="DataAnalysis" @click.stop="handleQualityCheck(row)" title="质量判定" />
......@@ -649,6 +687,16 @@ onBeforeUnmount(() => {
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="uploadForm.remark"
type="textarea"
:rows="2"
placeholder="可选,填写文件说明或标注"
maxlength="500"
/>
</el-form-item>
<el-form-item label="上传文件" required>
<el-upload
class="upload-panel"
......@@ -972,33 +1020,24 @@ onBeforeUnmount(() => {
}
.search-form {
display: flex;
margin-bottom: 4px;
flex-shrink: 0;
}
.form-actions {
margin-left: auto;
}
:deep(.el-table .cell-wrap .cell) {
white-space: normal;
word-break: break-word;
line-height: 1.5;
}
.content-wrap {
flex: 1;
min-height: 0;
}
@media (max-width: 980px) {
.data-layout {
display: block;
overflow-y: auto;
}
.pane-divider {
display: none;
}
.pane-card {
width: 100% !important;
min-height: 420px;
margin-bottom: 12px;
border-right: none;
border-bottom: 1px solid var(--border-color);
}
}
// quality dialog
.quality-filename {
......
......@@ -139,6 +139,29 @@ const handleDelete = async (row) => {
}
}
const selectedRecords = ref([])
const handleRecordSelectionChange = (rows) => {
selectedRecords.value = rows
}
const handleBatchDeleteRecords = async () => {
if (!selectedRecords.value.length) return
try {
await ElMessageBox.confirm(
`确定删除选中的 ${selectedRecords.value.length} 条评估记录吗?此操作不可恢复。`,
'批量删除',
{ type: 'warning' },
)
await Promise.all(selectedRecords.value.map((r) => deleteEvalRecord(r.id)))
ElMessage.success(`已删除 ${selectedRecords.value.length} 条评估记录`)
if (selectedRecords.value.some((r) => r.id === currentResult.value?.id)) currentResult.value = null
selectedRecords.value = []
await loadRecords()
} catch {
// user cancelled
}
}
const fmtMetric = (v) => (v != null ? Number(v).toFixed(5) : '-')
const fmtMape = (v) => (v != null ? Number(v).toFixed(3) + ' %' : '-')
const fmtR2 = (v) => (v != null ? Number(v).toFixed(6) : '-')
......@@ -253,9 +276,18 @@ onMounted(async () => {
<template #header>
<div class="card-header-row">
<span class="card-title">评估记录</span>
<el-button :icon="Refresh" size="small" plain :loading="loadingRecords" @click="loadRecords">
刷新
</el-button>
<div style="display:flex;gap:8px;align-items:center">
<el-button
type="danger"
plain
size="small"
:disabled="!selectedRecords.length"
@click="handleBatchDeleteRecords"
>批量删除</el-button>
<el-button :icon="Refresh" size="small" plain :loading="loadingRecords" @click="loadRecords">
刷新
</el-button>
</div>
</div>
</template>
......@@ -265,7 +297,9 @@ onMounted(async () => {
border
stripe
height="calc(100vh - 580px)"
@selection-change="handleRecordSelectionChange"
>
<el-table-column type="selection" width="44" />
<el-table-column prop="model_name" label="模型名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="package_name" label="评估数据包" min-width="130" show-overflow-tooltip />
<el-table-column label="样本数" width="88" align="center">
......
......@@ -69,6 +69,28 @@ const handleDelete = async (row) => {
}
}
const selectedModels = ref([])
const handleModelSelectionChange = (rows) => {
selectedModels.value = rows
}
const handleBatchDeleteModels = async () => {
if (!selectedModels.value.length) return
try {
await ElMessageBox.confirm(
`确定删除选中的 ${selectedModels.value.length} 个模型吗?删除后无法恢复。`,
'批量删除',
{ type: 'warning' },
)
await Promise.all(selectedModels.value.map((m) => deleteSavedModel(m.id)))
ElMessage.success(`已删除 ${selectedModels.value.length} 个模型`)
selectedModels.value = []
await loadModels()
} catch {
// user cancelled
}
}
// ── display helpers ───────────────────────────────────────────────────────────
const formatParams = (params) => {
if (!params) return '-'
......@@ -98,14 +120,23 @@ onMounted(loadModels)
<template #header>
<div class="card-header-row">
<span class="card-title">已保存模型</span>
<el-button :icon="Refresh" size="small" plain :loading="loading" @click="loadModels">
刷新
</el-button>
<div style="display:flex;gap:8px;align-items:center">
<el-button
type="danger"
plain
size="small"
:disabled="!selectedModels.length"
@click="handleBatchDeleteModels"
>批量删除</el-button>
<el-button :icon="Refresh" size="small" plain :loading="loading" @click="loadModels">
刷新
</el-button>
</div>
</div>
</template>
<el-table :data="models" v-loading="loading" border stripe height="calc(100vh - 160px)">
<el-table-column type="index" width="55" label="#" align="center" />
<el-table :data="models" v-loading="loading" border stripe height="calc(100vh - 160px)" @selection-change="handleModelSelectionChange">
<el-table-column type="selection" width="44" />
<el-table-column prop="model_name" label="模型名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="description" label="说明" min-width="160" show-overflow-tooltip>
......
......@@ -216,6 +216,36 @@ const handleDelete = async (task) => {
}
}
const selectedTasks = ref([])
const handleTaskSelectionChange = (rows) => {
selectedTasks.value = rows
}
const handleBatchDeleteTasks = async () => {
if (!selectedTasks.value.length) return
const deletable = selectedTasks.value.filter(
(t) => t.status !== 'pending' && t.status !== 'running',
)
if (!deletable.length) {
ElMessage.warning('选中的任务均在运行中,请先取消后再删除')
return
}
const skipped = selectedTasks.value.length - deletable.length
const tip = skipped > 0
? `确定删除选中的 ${deletable.length} 个任务?(${skipped} 个运行中的任务将被跳过)`
: `确定删除选中的 ${deletable.length} 个任务吗?此操作不可恢复。`
try {
await ElMessageBox.confirm(tip, '批量删除', { type: 'warning' })
await Promise.all(deletable.map((t) => deleteTrainTask(t.id)))
ElMessage.success(`已删除 ${deletable.length} 个任务`)
if (deletable.some((t) => t.id === selectedTask.value?.id)) selectedTask.value = null
selectedTasks.value = []
await loadTasks()
} catch {
// user cancelled
}
}
// ── task detail (epoch logs) ──────────────────────────────────────────────────
const selectedTask = ref(null)
const detailMode = ref('table') // 'table' | 'chart'
......@@ -408,7 +438,16 @@ onBeforeUnmount(() => {
<template #header>
<div class="card-header-row">
<span class="card-title">训练记录</span>
<el-button :icon="Refresh" size="small" plain :loading="loadingTasks" @click="loadTasks">刷新</el-button>
<div style="display:flex;gap:8px;align-items:center">
<el-button
type="danger"
plain
size="small"
:disabled="!selectedTasks.length"
@click="handleBatchDeleteTasks"
>批量删除</el-button>
<el-button :icon="Refresh" size="small" plain :loading="loadingTasks" @click="loadTasks">刷新</el-button>
</div>
</div>
</template>
......@@ -421,7 +460,9 @@ onBeforeUnmount(() => {
height="100%"
style="cursor:pointer"
@row-click="handleRowClick"
@selection-change="handleTaskSelectionChange"
>
<el-table-column type="selection" width="44" @click.stop />
<el-table-column prop="model_name" label="模型名称" min-width="130" show-overflow-tooltip />
<el-table-column label="训练集" min-width="120" show-overflow-tooltip>
<template #default="{ row }">{{ row.package_name }}</template>
......@@ -475,10 +516,13 @@ onBeforeUnmount(() => {
<template #header>
<div class="card-header-row">
<span class="card-title">训练过程 — {{ selectedTask.model_name }}</span>
<el-radio-group v-model="detailMode" size="small">
<el-radio-button value="table">表格</el-radio-button>
<el-radio-button value="chart">曲线</el-radio-button>
</el-radio-group>
<div style="display:flex;gap:8px;align-items:center">
<el-radio-group v-model="detailMode" size="small">
<el-radio-button value="table">表格</el-radio-button>
<el-radio-button value="chart">曲线</el-radio-button>
</el-radio-group>
<el-button size="small" plain @click="selectedTask = null">关闭</el-button>
</div>
</div>
</template>
......
......@@ -26,6 +26,11 @@ const loading = ref(false)
const packageList = ref([])
const searchName = ref('')
const currentPackage = ref(null)
const selectedPackages = ref([])
const handleSelectionChange = (rows) => {
selectedPackages.value = rows
}
const loadPackages = async () => {
loading.value = true
......@@ -122,6 +127,26 @@ const handleDelete = async (row) => {
}
}
const handleBatchDelete = async () => {
if (!selectedPackages.value.length) return
try {
await ElMessageBox.confirm(
`确定删除选中的 ${selectedPackages.value.length} 个数据包吗?此操作不可恢复。`,
'批量删除',
{ type: 'warning' },
)
await Promise.all(selectedPackages.value.map((p) => deletePackage(p.id)))
ElMessage.success(`已删除 ${selectedPackages.value.length} 个数据包`)
if (selectedPackages.value.some((p) => p.id === currentPackage.value?.id)) {
currentPackage.value = null
}
selectedPackages.value = []
await loadPackages()
} catch {
// user cancelled
}
}
watch(() => props.categoryId, loadPackages)
watch(() => props.refreshKey, () => {
currentPackage.value = null
......@@ -136,22 +161,32 @@ onMounted(loadPackages)
<template #header>
<div class="pane-header">
<span>数据包列表</span>
<el-button type="primary" :icon="Plus" size="small" @click="emit('add')">新增数据包</el-button>
</div>
</template>
<div class="search-bar">
<el-input
v-model="searchName"
placeholder="数据包名称"
clearable
:prefix-icon="Search"
@keyup.enter="handleSearch"
@clear="handleReset"
/>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<el-form :inline="true" class="search-form">
<el-form-item>
<el-input
v-model="searchName"
placeholder="数据包名称"
clearable
:prefix-icon="Search"
@keyup.enter="handleSearch"
@clear="handleReset"
/>
</el-form-item>
<el-form-item class="form-actions">
<el-button
type="danger"
plain
:disabled="!selectedPackages.length"
@click="handleBatchDelete"
>批量删除</el-button>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
<el-button type="primary" @click="emit('add')">新增数据包</el-button>
</el-form-item>
</el-form>
<el-table
:data="packageList"
......@@ -163,7 +198,9 @@ onMounted(loadPackages)
:row-class-name="({ row }) => (currentPackage?.id === row.id ? 'current-row' : '')"
style="cursor: pointer"
@row-click="handleView"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="44" @click.stop />
<el-table-column prop="name" label="数据包名称" min-width="100" show-overflow-tooltip />
<el-table-column prop="created_at" label="创建时间" min-width="100" />
<el-table-column prop="data_count" label="数据量" width="80" align="center" />
......@@ -243,14 +280,13 @@ onMounted(loadPackages)
color: var(--text-primary);
}
.search-bar {
.search-form {
display: flex;
gap: 8px;
margin-bottom: 12px;
.el-input {
flex: 1;
}
margin-bottom: 4px;
flex-shrink: 0;
}
.form-actions {
margin-left: auto;
}
:deep(.el-table .current-row > td) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment